Merge pull request #2332 from devxoul/feat/git-master-env-prefix
feat(git-master): add GIT_MASTER=1 env prefix for all git commands
This commit is contained in:
@@ -884,6 +884,25 @@ describe("GitMasterConfigSchema", () => {
|
||||
//#then
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
|
||||
test("accepts shell-safe git_env_prefix", () => {
|
||||
const config = { git_env_prefix: "MY_HOOK=active" }
|
||||
|
||||
const result = GitMasterConfigSchema.safeParse(config)
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
if (result.success) {
|
||||
expect(result.data.git_env_prefix).toBe("MY_HOOK=active")
|
||||
}
|
||||
})
|
||||
|
||||
test("rejects git_env_prefix with shell metacharacters", () => {
|
||||
const config = { git_env_prefix: "A=1; rm -rf /" }
|
||||
|
||||
const result = GitMasterConfigSchema.safeParse(config)
|
||||
|
||||
expect(result.success).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("skills schema", () => {
|
||||
|
||||
@@ -10,6 +10,7 @@ export * from "./schema/commands"
|
||||
export * from "./schema/dynamic-context-pruning"
|
||||
export * from "./schema/experimental"
|
||||
export * from "./schema/fallback-models"
|
||||
export * from "./schema/git-env-prefix"
|
||||
export * from "./schema/git-master"
|
||||
export * from "./schema/hooks"
|
||||
export * from "./schema/notification"
|
||||
|
||||
28
src/config/schema/git-env-prefix.ts
Normal file
28
src/config/schema/git-env-prefix.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const GIT_ENV_ASSIGNMENT_PATTERN =
|
||||
/^(?:[A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9_-]*)(?: [A-Za-z_][A-Za-z0-9_]*=[A-Za-z0-9_-]*)*$/
|
||||
|
||||
export const GIT_ENV_PREFIX_VALIDATION_MESSAGE =
|
||||
'git_env_prefix must be empty or use shell-safe env assignments like "GIT_MASTER=1"'
|
||||
|
||||
export function isValidGitEnvPrefix(value: string): boolean {
|
||||
if (value === "") {
|
||||
return true
|
||||
}
|
||||
|
||||
return GIT_ENV_ASSIGNMENT_PATTERN.test(value)
|
||||
}
|
||||
|
||||
export function assertValidGitEnvPrefix(value: string): string {
|
||||
if (!isValidGitEnvPrefix(value)) {
|
||||
throw new Error(GIT_ENV_PREFIX_VALIDATION_MESSAGE)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
export const GitEnvPrefixSchema = z
|
||||
.string()
|
||||
.refine(isValidGitEnvPrefix, { message: GIT_ENV_PREFIX_VALIDATION_MESSAGE })
|
||||
.default("GIT_MASTER=1")
|
||||
@@ -1,10 +1,14 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import { GitEnvPrefixSchema } from "./git-env-prefix"
|
||||
|
||||
export const GitMasterConfigSchema = z.object({
|
||||
/** Add "Ultraworked with Sisyphus" footer to commit messages (default: true). Can be boolean or custom string. */
|
||||
commit_footer: z.union([z.boolean(), z.string()]).default(true),
|
||||
/** Add "Co-authored-by: Sisyphus" trailer to commit messages (default: true) */
|
||||
include_co_authored_by: z.boolean().default(true),
|
||||
/** Environment variable prefix for all git commands (default: "GIT_MASTER=1"). Set to "" to disable. Allows custom git hooks to detect git-master skill usage. */
|
||||
git_env_prefix: GitEnvPrefixSchema,
|
||||
})
|
||||
|
||||
export type GitMasterConfig = z.infer<typeof GitMasterConfigSchema>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, it, expect } from "bun:test"
|
||||
import { injectGitMasterConfig } from "./git-master-template-injection"
|
||||
|
||||
const SAMPLE_TEMPLATE = [
|
||||
"# Git Master Agent",
|
||||
"",
|
||||
"## MODE DETECTION (FIRST STEP)",
|
||||
"",
|
||||
"Analyze the request.",
|
||||
"",
|
||||
"```bash",
|
||||
"git status",
|
||||
"git merge-base HEAD main 2>/dev/null || git merge-base HEAD master 2>/dev/null",
|
||||
"MERGE_BASE=$(git merge-base HEAD main)",
|
||||
"GIT_SEQUENCE_EDITOR=: git rebase -i --autosquash $MERGE_BASE",
|
||||
"```",
|
||||
"",
|
||||
"```",
|
||||
"</execution>",
|
||||
].join("\n")
|
||||
|
||||
describe("#given git_env_prefix config", () => {
|
||||
describe("#when default config (GIT_MASTER=1)", () => {
|
||||
it("#then injects env prefix section before MODE DETECTION", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("## GIT COMMAND PREFIX (MANDATORY)")
|
||||
expect(result).toContain("GIT_MASTER=1 git status")
|
||||
expect(result).toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("GIT_MASTER=1 git push")
|
||||
expect(result).toContain("EVERY git command MUST be prefixed with `GIT_MASTER=1`")
|
||||
|
||||
const prefixIndex = result.indexOf("## GIT COMMAND PREFIX")
|
||||
const modeIndex = result.indexOf("## MODE DETECTION")
|
||||
expect(prefixIndex).toBeLessThan(modeIndex)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when git_env_prefix is empty string", () => {
|
||||
it("#then does NOT inject env prefix section", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "",
|
||||
})
|
||||
|
||||
expect(result).not.toContain("## GIT COMMAND PREFIX")
|
||||
expect(result).not.toContain("GIT_MASTER=1")
|
||||
expect(result).not.toContain("git_env_prefix")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when git_env_prefix is custom value", () => {
|
||||
it("#then injects custom prefix in section", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "MY_HOOK=active",
|
||||
})
|
||||
|
||||
expect(result).toContain("MY_HOOK=active git status")
|
||||
expect(result).toContain("MY_HOOK=active git commit")
|
||||
expect(result).not.toContain("GIT_MASTER=1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when git_env_prefix contains shell metacharacters", () => {
|
||||
it("#then rejects the malicious value", () => {
|
||||
expect(() =>
|
||||
injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "A=1; rm -rf /",
|
||||
})
|
||||
).toThrow('git_env_prefix must be empty or use shell-safe env assignments like "GIT_MASTER=1"')
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when no config provided", () => {
|
||||
it("#then uses default GIT_MASTER=1 prefix", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE)
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git status")
|
||||
expect(result).toContain("## GIT COMMAND PREFIX (MANDATORY)")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("#given git_env_prefix with commit footer", () => {
|
||||
describe("#when both env prefix and footer are enabled", () => {
|
||||
it("#then commit examples include the env prefix", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("Ultraworked with [Sisyphus]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when the template already contains bare git commands in bash blocks", () => {
|
||||
it("#then prefixes every git invocation in the final output", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git status")
|
||||
expect(result).toContain(
|
||||
"GIT_MASTER=1 git merge-base HEAD main 2>/dev/null || GIT_MASTER=1 git merge-base HEAD master 2>/dev/null"
|
||||
)
|
||||
expect(result).toContain("MERGE_BASE=$(GIT_MASTER=1 git merge-base HEAD main)")
|
||||
expect(result).toContain(
|
||||
"GIT_SEQUENCE_EDITOR=: GIT_MASTER=1 git rebase -i --autosquash $MERGE_BASE"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when env prefix disabled but footer enabled", () => {
|
||||
it("#then commit examples have no env prefix", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "",
|
||||
})
|
||||
|
||||
expect(result).not.toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("git commit -m")
|
||||
expect(result).toContain("Ultraworked with [Sisyphus]")
|
||||
})
|
||||
})
|
||||
|
||||
describe("#when both env prefix and co-author are enabled", () => {
|
||||
it("#then commit example includes prefix, footer, and co-author", () => {
|
||||
const result = injectGitMasterConfig(SAMPLE_TEMPLATE, {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: true,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
})
|
||||
|
||||
expect(result).toContain("GIT_MASTER=1 git commit")
|
||||
expect(result).toContain("Ultraworked with [Sisyphus]")
|
||||
expect(result).toContain("Co-authored-by: Sisyphus")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,14 +1,88 @@
|
||||
import type { GitMasterConfig } from "../../config/schema"
|
||||
import { assertValidGitEnvPrefix, type GitMasterConfig } from "../../config/schema"
|
||||
|
||||
const BASH_CODE_BLOCK_PATTERN = /```bash\r?\n([\s\S]*?)```/g
|
||||
const LEADING_GIT_COMMAND_PATTERN = /^([ \t]*(?:[A-Za-z_][A-Za-z0-9_]*=[^ \t]+\s+)*)git(?=[ \t]|$)/gm
|
||||
const INLINE_GIT_COMMAND_PATTERN = /([;&|()][ \t]*)git(?=[ \t]|$)/g
|
||||
|
||||
export function injectGitMasterConfig(template: string, config?: GitMasterConfig): string {
|
||||
const commitFooter = config?.commit_footer ?? true
|
||||
const includeCoAuthoredBy = config?.include_co_authored_by ?? true
|
||||
const gitEnvPrefix = assertValidGitEnvPrefix(config?.git_env_prefix ?? "GIT_MASTER=1")
|
||||
|
||||
if (!commitFooter && !includeCoAuthoredBy) {
|
||||
return template
|
||||
let result = gitEnvPrefix ? injectGitEnvPrefix(template, gitEnvPrefix) : template
|
||||
|
||||
if (commitFooter || includeCoAuthoredBy) {
|
||||
const injection = buildCommitFooterInjection(commitFooter, includeCoAuthoredBy, gitEnvPrefix)
|
||||
const insertionPoint = result.indexOf("```\n</execution>")
|
||||
|
||||
result =
|
||||
insertionPoint !== -1
|
||||
? result.slice(0, insertionPoint) +
|
||||
"```\n\n" +
|
||||
injection +
|
||||
"\n</execution>" +
|
||||
result.slice(insertionPoint + "```\n</execution>".length)
|
||||
: result + "\n\n" + injection
|
||||
}
|
||||
|
||||
return gitEnvPrefix ? prefixGitCommandsInBashCodeBlocks(result, gitEnvPrefix) : result
|
||||
}
|
||||
|
||||
function injectGitEnvPrefix(template: string, prefix: string): string {
|
||||
const envPrefixSection = [
|
||||
"## GIT COMMAND PREFIX (MANDATORY)",
|
||||
"",
|
||||
`<git_env_prefix>`,
|
||||
`**EVERY git command MUST be prefixed with \`${prefix}\`.**`,
|
||||
"",
|
||||
"This allows custom git hooks to detect when git-master skill is active.",
|
||||
"",
|
||||
"```bash",
|
||||
`${prefix} git status`,
|
||||
`${prefix} git add <files>`,
|
||||
`${prefix} git commit -m "message"`,
|
||||
`${prefix} git push`,
|
||||
`${prefix} git rebase ...`,
|
||||
`${prefix} git log ...`,
|
||||
"```",
|
||||
"",
|
||||
"**NO EXCEPTIONS. Every `git` invocation must include this prefix.**",
|
||||
`</git_env_prefix>`,
|
||||
].join("\n")
|
||||
|
||||
const modeDetectionMarker = "## MODE DETECTION (FIRST STEP)"
|
||||
const markerIndex = template.indexOf(modeDetectionMarker)
|
||||
if (markerIndex !== -1) {
|
||||
return (
|
||||
template.slice(0, markerIndex) +
|
||||
envPrefixSection +
|
||||
"\n\n---\n\n" +
|
||||
template.slice(markerIndex)
|
||||
)
|
||||
}
|
||||
|
||||
return envPrefixSection + "\n\n---\n\n" + template
|
||||
}
|
||||
|
||||
function prefixGitCommandsInBashCodeBlocks(template: string, prefix: string): string {
|
||||
return template.replace(BASH_CODE_BLOCK_PATTERN, (block, codeBlock: string) => {
|
||||
return block.replace(codeBlock, prefixGitCommandsInCodeBlock(codeBlock, prefix))
|
||||
})
|
||||
}
|
||||
|
||||
function prefixGitCommandsInCodeBlock(codeBlock: string, prefix: string): string {
|
||||
return codeBlock
|
||||
.replace(LEADING_GIT_COMMAND_PATTERN, `$1${prefix} git`)
|
||||
.replace(INLINE_GIT_COMMAND_PATTERN, `$1${prefix} git`)
|
||||
}
|
||||
|
||||
function buildCommitFooterInjection(
|
||||
commitFooter: boolean | string,
|
||||
includeCoAuthoredBy: boolean,
|
||||
gitEnvPrefix: string,
|
||||
): string {
|
||||
const sections: string[] = []
|
||||
const cmdPrefix = gitEnvPrefix ? `${gitEnvPrefix} ` : ""
|
||||
|
||||
sections.push("### 5.5 Commit Footer & Co-Author")
|
||||
sections.push("")
|
||||
@@ -43,7 +117,7 @@ export function injectGitMasterConfig(template: string, config?: GitMasterConfig
|
||||
sections.push("**Example (both enabled):**")
|
||||
sections.push("```bash")
|
||||
sections.push(
|
||||
`git commit -m "{Commit Message}" -m "${footerText}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
|
||||
`${cmdPrefix}git commit -m "{Commit Message}" -m "${footerText}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
|
||||
)
|
||||
sections.push("```")
|
||||
} else if (commitFooter) {
|
||||
@@ -53,29 +127,16 @@ export function injectGitMasterConfig(template: string, config?: GitMasterConfig
|
||||
: "Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)"
|
||||
sections.push("**Example:**")
|
||||
sections.push("```bash")
|
||||
sections.push(`git commit -m "{Commit Message}" -m "${footerText}"`)
|
||||
sections.push(`${cmdPrefix}git commit -m "{Commit Message}" -m "${footerText}"`)
|
||||
sections.push("```")
|
||||
} else if (includeCoAuthoredBy) {
|
||||
sections.push("**Example:**")
|
||||
sections.push("```bash")
|
||||
sections.push(
|
||||
"git commit -m \"{Commit Message}\" -m \"Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>\""
|
||||
`${cmdPrefix}git commit -m "{Commit Message}" -m "Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>"`
|
||||
)
|
||||
sections.push("```")
|
||||
}
|
||||
|
||||
const injection = sections.join("\n")
|
||||
|
||||
const insertionPoint = template.indexOf("```\n</execution>")
|
||||
if (insertionPoint !== -1) {
|
||||
return (
|
||||
template.slice(0, insertionPoint) +
|
||||
"```\n\n" +
|
||||
injection +
|
||||
"\n</execution>" +
|
||||
template.slice(insertionPoint + "```\n</execution>".length)
|
||||
)
|
||||
}
|
||||
|
||||
return template + "\n\n" + injection
|
||||
return sections.join("\n")
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -249,6 +250,7 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: true,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -269,6 +271,7 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -302,6 +305,7 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: false,
|
||||
include_co_authored_by: true,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -322,6 +326,7 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: customFooter,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -341,6 +346,7 @@ describe("resolveMultipleSkillsAsync", () => {
|
||||
gitMasterConfig: {
|
||||
commit_footer: true,
|
||||
include_co_authored_by: false,
|
||||
git_env_prefix: "GIT_MASTER=1",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user