@codemcp/ade 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agentskills/skills/conventional-commits/SKILL.md +36 -0
- package/.beads/issues.jsonl +6 -0
- package/.beads/last-touched +1 -1
- package/.kiro/agents/ade.json +9 -2
- package/.opencode/agents/ade.md +9 -18
- package/.vibe/beads-state-ade-fix-no-git-k396xs.json +34 -0
- package/.vibe/development-plan-fix-no-git.md +76 -0
- package/AGENTS.md +27 -0
- package/config.lock.yaml +33 -9
- package/config.yaml +3 -0
- package/package.json +1 -1
- package/packages/cli/dist/index.js +404 -343
- package/packages/cli/package.json +1 -1
- package/packages/cli/src/commands/conventions.integration.spec.ts +7 -1
- package/packages/cli/src/commands/install.ts +19 -1
- package/packages/cli/src/commands/setup.ts +19 -1
- package/packages/core/package.json +1 -1
- package/packages/core/src/catalog/catalog.spec.ts +1 -10
- package/packages/core/src/catalog/facets/autonomy.ts +4 -62
- package/packages/core/src/index.ts +1 -4
- package/packages/core/src/resolver.spec.ts +4 -22
- package/packages/core/src/resolver.ts +1 -5
- package/packages/core/src/types.ts +0 -20
- package/packages/harnesses/package.json +2 -1
- package/packages/harnesses/src/permission-policy.ts +1 -165
- package/packages/harnesses/src/util.spec.ts +97 -0
- package/packages/harnesses/src/util.ts +32 -4
- package/packages/harnesses/src/writers/claude-code.spec.ts +14 -46
- package/packages/harnesses/src/writers/claude-code.ts +33 -16
- package/packages/harnesses/src/writers/cline.spec.ts +1 -41
- package/packages/harnesses/src/writers/copilot.spec.ts +2 -42
- package/packages/harnesses/src/writers/copilot.ts +19 -32
- package/packages/harnesses/src/writers/cursor.spec.ts +1 -41
- package/packages/harnesses/src/writers/cursor.ts +28 -40
- package/packages/harnesses/src/writers/kiro.spec.ts +1 -41
- package/packages/harnesses/src/writers/kiro.ts +23 -24
- package/packages/harnesses/src/writers/opencode.spec.ts +5 -47
- package/packages/harnesses/src/writers/opencode.ts +153 -10
- package/packages/harnesses/src/writers/roo-code.spec.ts +2 -42
- package/packages/harnesses/src/writers/roo-code.ts +25 -10
- package/packages/harnesses/src/writers/universal.spec.ts +1 -41
- package/packages/harnesses/src/writers/universal.ts +45 -31
- package/packages/harnesses/src/writers/windsurf.spec.ts +5 -42
- package/packages/harnesses/src/writers/windsurf.ts +30 -47
- package/skills-lock.json +6 -1
|
@@ -8,9 +8,15 @@ vi.mock("@clack/prompts", () => ({
|
|
|
8
8
|
outro: vi.fn(),
|
|
9
9
|
select: vi.fn(),
|
|
10
10
|
multiselect: vi.fn(),
|
|
11
|
-
confirm: vi.fn(),
|
|
11
|
+
confirm: vi.fn().mockResolvedValue(true),
|
|
12
12
|
isCancel: vi.fn().mockReturnValue(false),
|
|
13
13
|
cancel: vi.fn(),
|
|
14
|
+
log: {
|
|
15
|
+
info: vi.fn(),
|
|
16
|
+
warn: vi.fn(),
|
|
17
|
+
error: vi.fn(),
|
|
18
|
+
success: vi.fn()
|
|
19
|
+
},
|
|
14
20
|
spinner: vi.fn().mockReturnValue({ start: vi.fn(), stop: vi.fn() })
|
|
15
21
|
}));
|
|
16
22
|
|
|
@@ -51,7 +51,25 @@ export async function runInstall(
|
|
|
51
51
|
);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
if (logicalConfig.skills.length > 0) {
|
|
55
|
+
const confirmInstall = await clack.confirm({
|
|
56
|
+
message: `Install ${logicalConfig.skills.length} skill(s) now?`,
|
|
57
|
+
initialValue: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (typeof confirmInstall === "symbol") {
|
|
61
|
+
clack.cancel("Install cancelled.");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (confirmInstall) {
|
|
66
|
+
await installSkills(logicalConfig.skills, projectRoot);
|
|
67
|
+
} else {
|
|
68
|
+
clack.log.info(
|
|
69
|
+
"Skills not installed. Run manually when ready:\n npx @codemcp/skills experimental_install"
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
55
73
|
|
|
56
74
|
if (logicalConfig.knowledge_sources.length > 0) {
|
|
57
75
|
clack.log.info(
|
|
@@ -172,7 +172,25 @@ export async function runSetup(
|
|
|
172
172
|
);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
|
|
175
|
+
if (logicalConfig.skills.length > 0) {
|
|
176
|
+
const confirmInstall = await clack.confirm({
|
|
177
|
+
message: `Install ${logicalConfig.skills.length} skill(s) now?`,
|
|
178
|
+
initialValue: true
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
if (typeof confirmInstall === "symbol") {
|
|
182
|
+
clack.cancel("Setup cancelled.");
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (confirmInstall) {
|
|
187
|
+
await installSkills(logicalConfig.skills, projectRoot);
|
|
188
|
+
} else {
|
|
189
|
+
clack.log.info(
|
|
190
|
+
"Skills not installed. Run manually when ready:\n npx @codemcp/skills experimental_install"
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
176
194
|
|
|
177
195
|
if (logicalConfig.knowledge_sources.length > 0) {
|
|
178
196
|
clack.log.info(
|
|
@@ -486,16 +486,7 @@ describe("catalog", () => {
|
|
|
486
486
|
|
|
487
487
|
expect(provision).toBeDefined();
|
|
488
488
|
expect(provision!.config).toEqual({
|
|
489
|
-
profile: "sensible-defaults"
|
|
490
|
-
capabilities: {
|
|
491
|
-
read: "allow",
|
|
492
|
-
edit_write: "allow",
|
|
493
|
-
search_list: "allow",
|
|
494
|
-
bash_safe: "allow",
|
|
495
|
-
bash_unsafe: "ask",
|
|
496
|
-
web: "ask",
|
|
497
|
-
task_agent: "allow"
|
|
498
|
-
}
|
|
489
|
+
profile: "sensible-defaults"
|
|
499
490
|
});
|
|
500
491
|
});
|
|
501
492
|
});
|
|
@@ -1,62 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AutonomyCapability,
|
|
3
|
-
Facet,
|
|
4
|
-
PermissionDecision,
|
|
5
|
-
PermissionPolicy
|
|
6
|
-
} from "../../types.js";
|
|
7
|
-
|
|
8
|
-
const ALL_CAPABILITIES: AutonomyCapability[] = [
|
|
9
|
-
"read",
|
|
10
|
-
"edit_write",
|
|
11
|
-
"search_list",
|
|
12
|
-
"bash_safe",
|
|
13
|
-
"bash_unsafe",
|
|
14
|
-
"web",
|
|
15
|
-
"task_agent"
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
function capabilityMap(
|
|
19
|
-
defaultDecision: PermissionDecision,
|
|
20
|
-
overrides: Partial<Record<AutonomyCapability, PermissionDecision>> = {}
|
|
21
|
-
): Record<AutonomyCapability, PermissionDecision> {
|
|
22
|
-
return Object.fromEntries(
|
|
23
|
-
ALL_CAPABILITIES.map((capability) => [
|
|
24
|
-
capability,
|
|
25
|
-
overrides[capability] ?? defaultDecision
|
|
26
|
-
])
|
|
27
|
-
) as Record<AutonomyCapability, PermissionDecision>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function autonomyPolicy(
|
|
31
|
-
profile: PermissionPolicy["profile"]
|
|
32
|
-
): PermissionPolicy {
|
|
33
|
-
switch (profile) {
|
|
34
|
-
case "rigid":
|
|
35
|
-
return {
|
|
36
|
-
profile,
|
|
37
|
-
capabilities: capabilityMap("ask")
|
|
38
|
-
};
|
|
39
|
-
case "sensible-defaults":
|
|
40
|
-
return {
|
|
41
|
-
profile,
|
|
42
|
-
capabilities: capabilityMap("ask", {
|
|
43
|
-
read: "allow",
|
|
44
|
-
edit_write: "allow",
|
|
45
|
-
search_list: "allow",
|
|
46
|
-
bash_safe: "allow",
|
|
47
|
-
task_agent: "allow",
|
|
48
|
-
web: "ask"
|
|
49
|
-
})
|
|
50
|
-
};
|
|
51
|
-
case "max-autonomy":
|
|
52
|
-
return {
|
|
53
|
-
profile,
|
|
54
|
-
capabilities: capabilityMap("allow", {
|
|
55
|
-
web: "ask"
|
|
56
|
-
})
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
}
|
|
1
|
+
import type { Facet } from "../../types.js";
|
|
60
2
|
|
|
61
3
|
export const autonomyFacet: Facet = {
|
|
62
4
|
id: "autonomy",
|
|
@@ -74,7 +16,7 @@ export const autonomyFacet: Facet = {
|
|
|
74
16
|
recipe: [
|
|
75
17
|
{
|
|
76
18
|
writer: "permission-policy",
|
|
77
|
-
config:
|
|
19
|
+
config: { profile: "rigid" }
|
|
78
20
|
}
|
|
79
21
|
]
|
|
80
22
|
},
|
|
@@ -86,7 +28,7 @@ export const autonomyFacet: Facet = {
|
|
|
86
28
|
recipe: [
|
|
87
29
|
{
|
|
88
30
|
writer: "permission-policy",
|
|
89
|
-
config:
|
|
31
|
+
config: { profile: "sensible-defaults" }
|
|
90
32
|
}
|
|
91
33
|
]
|
|
92
34
|
},
|
|
@@ -98,7 +40,7 @@ export const autonomyFacet: Facet = {
|
|
|
98
40
|
recipe: [
|
|
99
41
|
{
|
|
100
42
|
writer: "permission-policy",
|
|
101
|
-
config:
|
|
43
|
+
config: { profile: "max-autonomy" }
|
|
102
44
|
}
|
|
103
45
|
]
|
|
104
46
|
}
|
|
@@ -15,10 +15,7 @@ export {
|
|
|
15
15
|
type ExternalSkill,
|
|
16
16
|
type GitHook,
|
|
17
17
|
type PermissionPolicy,
|
|
18
|
-
type AutonomyProfile
|
|
19
|
-
type AutonomyCapability,
|
|
20
|
-
type PermissionDecision,
|
|
21
|
-
type PermissionRule
|
|
18
|
+
type AutonomyProfile
|
|
22
19
|
} from "./types.js";
|
|
23
20
|
export { type ResolutionContext, type ResolvedFacet } from "./types.js";
|
|
24
21
|
export { type UserConfig, type LockFile } from "./types.js";
|
|
@@ -580,7 +580,7 @@ describe("resolve", () => {
|
|
|
580
580
|
});
|
|
581
581
|
|
|
582
582
|
describe("autonomy permission policy", () => {
|
|
583
|
-
it("adds a
|
|
583
|
+
it("adds a profile-based permission policy to LogicalConfig", async () => {
|
|
584
584
|
const userConfig: UserConfig = {
|
|
585
585
|
choices: { autonomy: "rigid" }
|
|
586
586
|
};
|
|
@@ -589,20 +589,11 @@ describe("resolve", () => {
|
|
|
589
589
|
|
|
590
590
|
expect(result).toHaveProperty("permission_policy");
|
|
591
591
|
expect((result as Record<string, unknown>).permission_policy).toEqual({
|
|
592
|
-
profile: "rigid"
|
|
593
|
-
capabilities: {
|
|
594
|
-
read: "ask",
|
|
595
|
-
edit_write: "ask",
|
|
596
|
-
search_list: "ask",
|
|
597
|
-
bash_safe: "ask",
|
|
598
|
-
bash_unsafe: "ask",
|
|
599
|
-
web: "ask",
|
|
600
|
-
task_agent: "ask"
|
|
601
|
-
}
|
|
592
|
+
profile: "rigid"
|
|
602
593
|
});
|
|
603
594
|
});
|
|
604
595
|
|
|
605
|
-
it("uses
|
|
596
|
+
it("uses the sensible-defaults autonomy profile", async () => {
|
|
606
597
|
const userConfig: UserConfig = {
|
|
607
598
|
choices: { autonomy: "sensible-defaults" }
|
|
608
599
|
};
|
|
@@ -610,16 +601,7 @@ describe("resolve", () => {
|
|
|
610
601
|
const result = await resolve(userConfig, catalog, registry);
|
|
611
602
|
|
|
612
603
|
expect(result.permission_policy).toEqual({
|
|
613
|
-
profile: "sensible-defaults"
|
|
614
|
-
capabilities: {
|
|
615
|
-
read: "allow",
|
|
616
|
-
edit_write: "allow",
|
|
617
|
-
search_list: "allow",
|
|
618
|
-
bash_safe: "allow",
|
|
619
|
-
bash_unsafe: "ask",
|
|
620
|
-
web: "ask",
|
|
621
|
-
task_agent: "allow"
|
|
622
|
-
}
|
|
604
|
+
profile: "sensible-defaults"
|
|
623
605
|
});
|
|
624
606
|
});
|
|
625
607
|
});
|
|
@@ -68,28 +68,8 @@ export interface GitHook {
|
|
|
68
68
|
|
|
69
69
|
export type AutonomyProfile = "rigid" | "sensible-defaults" | "max-autonomy";
|
|
70
70
|
|
|
71
|
-
export type PermissionDecision = "ask" | "allow" | "deny";
|
|
72
|
-
|
|
73
|
-
export type AutonomyCapability =
|
|
74
|
-
| "read"
|
|
75
|
-
| "edit_write"
|
|
76
|
-
| "search_list"
|
|
77
|
-
| "bash_safe"
|
|
78
|
-
| "bash_unsafe"
|
|
79
|
-
| "web"
|
|
80
|
-
| "task_agent";
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* @deprecated Harness-specific tool-level rules are no longer produced by core.
|
|
84
|
-
* Kept temporarily as a compatibility type for downstream packages.
|
|
85
|
-
*/
|
|
86
|
-
export type PermissionRule =
|
|
87
|
-
| PermissionDecision
|
|
88
|
-
| Record<string, PermissionDecision>;
|
|
89
|
-
|
|
90
71
|
export interface PermissionPolicy extends Record<string, unknown> {
|
|
91
72
|
profile: AutonomyProfile;
|
|
92
|
-
capabilities: Record<AutonomyCapability, PermissionDecision>;
|
|
93
73
|
}
|
|
94
74
|
|
|
95
75
|
export interface LogicalConfig extends Record<string, unknown> {
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"typecheck": "tsc --noEmit"
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
+
"@clack/prompts": "^1.1.0",
|
|
31
32
|
"@codemcp/ade-core": "workspace:*",
|
|
32
33
|
"@codemcp/skills": "^2.3.0"
|
|
33
34
|
},
|
|
@@ -39,5 +40,5 @@
|
|
|
39
40
|
"typescript": "catalog:",
|
|
40
41
|
"vitest": "catalog:"
|
|
41
42
|
},
|
|
42
|
-
"version": "0.
|
|
43
|
+
"version": "0.3.0"
|
|
43
44
|
}
|
|
@@ -1,121 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AutonomyCapability,
|
|
3
|
-
LogicalConfig,
|
|
4
|
-
PermissionDecision,
|
|
5
|
-
PermissionRule
|
|
6
|
-
} from "@codemcp/ade-core";
|
|
7
|
-
|
|
8
|
-
const SENSIBLE_DEFAULTS_RULES: Record<string, PermissionRule> = {
|
|
9
|
-
read: {
|
|
10
|
-
"*": "allow",
|
|
11
|
-
"*.env": "deny",
|
|
12
|
-
"*.env.*": "deny",
|
|
13
|
-
"*.env.example": "allow"
|
|
14
|
-
},
|
|
15
|
-
edit: "allow",
|
|
16
|
-
glob: "allow",
|
|
17
|
-
grep: "allow",
|
|
18
|
-
list: "allow",
|
|
19
|
-
lsp: "allow",
|
|
20
|
-
task: "allow",
|
|
21
|
-
todoread: "deny",
|
|
22
|
-
todowrite: "deny",
|
|
23
|
-
skill: "deny",
|
|
24
|
-
webfetch: "ask",
|
|
25
|
-
websearch: "ask",
|
|
26
|
-
codesearch: "ask",
|
|
27
|
-
bash: {
|
|
28
|
-
"*": "deny",
|
|
29
|
-
"grep *": "allow",
|
|
30
|
-
"rg *": "allow",
|
|
31
|
-
"find *": "allow",
|
|
32
|
-
"fd *": "allow",
|
|
33
|
-
ls: "allow",
|
|
34
|
-
"ls *": "allow",
|
|
35
|
-
"cat *": "allow",
|
|
36
|
-
"head *": "allow",
|
|
37
|
-
"tail *": "allow",
|
|
38
|
-
"wc *": "allow",
|
|
39
|
-
"sort *": "allow",
|
|
40
|
-
"uniq *": "allow",
|
|
41
|
-
"diff *": "allow",
|
|
42
|
-
"echo *": "allow",
|
|
43
|
-
"printf *": "allow",
|
|
44
|
-
pwd: "allow",
|
|
45
|
-
"which *": "allow",
|
|
46
|
-
"type *": "allow",
|
|
47
|
-
whoami: "allow",
|
|
48
|
-
date: "allow",
|
|
49
|
-
"date *": "allow",
|
|
50
|
-
env: "allow",
|
|
51
|
-
"tree *": "allow",
|
|
52
|
-
"file *": "allow",
|
|
53
|
-
"stat *": "allow",
|
|
54
|
-
"readlink *": "allow",
|
|
55
|
-
"realpath *": "allow",
|
|
56
|
-
"dirname *": "allow",
|
|
57
|
-
"basename *": "allow",
|
|
58
|
-
"sed *": "allow",
|
|
59
|
-
"awk *": "allow",
|
|
60
|
-
"cut *": "allow",
|
|
61
|
-
"tr *": "allow",
|
|
62
|
-
"tee *": "allow",
|
|
63
|
-
"xargs *": "allow",
|
|
64
|
-
"jq *": "allow",
|
|
65
|
-
"yq *": "allow",
|
|
66
|
-
"mkdir *": "allow",
|
|
67
|
-
"touch *": "allow",
|
|
68
|
-
"cp *": "ask",
|
|
69
|
-
"mv *": "ask",
|
|
70
|
-
"ln *": "ask",
|
|
71
|
-
"npm *": "ask",
|
|
72
|
-
"node *": "ask",
|
|
73
|
-
"pip *": "ask",
|
|
74
|
-
"python *": "ask",
|
|
75
|
-
"python3 *": "ask",
|
|
76
|
-
"rm *": "deny",
|
|
77
|
-
"rmdir *": "deny",
|
|
78
|
-
"curl *": "deny",
|
|
79
|
-
"wget *": "deny",
|
|
80
|
-
"chmod *": "deny",
|
|
81
|
-
"chown *": "deny",
|
|
82
|
-
"sudo *": "deny",
|
|
83
|
-
"su *": "deny",
|
|
84
|
-
"sh *": "deny",
|
|
85
|
-
"bash *": "deny",
|
|
86
|
-
"zsh *": "deny",
|
|
87
|
-
"eval *": "deny",
|
|
88
|
-
"exec *": "deny",
|
|
89
|
-
"source *": "deny",
|
|
90
|
-
". *": "deny",
|
|
91
|
-
"nohup *": "deny",
|
|
92
|
-
"dd *": "deny",
|
|
93
|
-
"mkfs *": "deny",
|
|
94
|
-
"mount *": "deny",
|
|
95
|
-
"umount *": "deny",
|
|
96
|
-
"kill *": "deny",
|
|
97
|
-
"killall *": "deny",
|
|
98
|
-
"pkill *": "deny",
|
|
99
|
-
"nc *": "deny",
|
|
100
|
-
"ncat *": "deny",
|
|
101
|
-
"ssh *": "deny",
|
|
102
|
-
"scp *": "deny",
|
|
103
|
-
"rsync *": "deny",
|
|
104
|
-
"docker *": "deny",
|
|
105
|
-
"kubectl *": "deny",
|
|
106
|
-
"systemctl *": "deny",
|
|
107
|
-
"service *": "deny",
|
|
108
|
-
"crontab *": "deny",
|
|
109
|
-
reboot: "deny",
|
|
110
|
-
"shutdown *": "deny",
|
|
111
|
-
"passwd *": "deny",
|
|
112
|
-
"useradd *": "deny",
|
|
113
|
-
"userdel *": "deny",
|
|
114
|
-
"iptables *": "deny"
|
|
115
|
-
},
|
|
116
|
-
external_directory: "deny",
|
|
117
|
-
doom_loop: "deny"
|
|
118
|
-
};
|
|
1
|
+
import type { LogicalConfig } from "@codemcp/ade-core";
|
|
119
2
|
|
|
120
3
|
export function getAutonomyProfile(config: LogicalConfig) {
|
|
121
4
|
return config.permission_policy?.profile;
|
|
@@ -124,50 +7,3 @@ export function getAutonomyProfile(config: LogicalConfig) {
|
|
|
124
7
|
export function hasPermissionPolicy(config: LogicalConfig): boolean {
|
|
125
8
|
return config.permission_policy !== undefined;
|
|
126
9
|
}
|
|
127
|
-
|
|
128
|
-
export function getCapabilityDecision(
|
|
129
|
-
config: LogicalConfig,
|
|
130
|
-
capability: AutonomyCapability
|
|
131
|
-
): PermissionDecision | undefined {
|
|
132
|
-
return config.permission_policy?.capabilities?.[capability];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function allowsCapability(
|
|
136
|
-
config: LogicalConfig,
|
|
137
|
-
capability: AutonomyCapability
|
|
138
|
-
): boolean {
|
|
139
|
-
return getCapabilityDecision(config, capability) === "allow";
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export function keepsWebOnAsk(config: LogicalConfig): boolean {
|
|
143
|
-
return getCapabilityDecision(config, "web") === "ask";
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function getHarnessPermissionRules(
|
|
147
|
-
config: LogicalConfig
|
|
148
|
-
): Record<string, PermissionRule> | undefined {
|
|
149
|
-
switch (config.permission_policy?.profile) {
|
|
150
|
-
case "rigid":
|
|
151
|
-
return {
|
|
152
|
-
"*": "ask",
|
|
153
|
-
webfetch: "ask",
|
|
154
|
-
websearch: "ask",
|
|
155
|
-
codesearch: "ask",
|
|
156
|
-
external_directory: "deny",
|
|
157
|
-
doom_loop: "deny"
|
|
158
|
-
};
|
|
159
|
-
case "sensible-defaults":
|
|
160
|
-
return SENSIBLE_DEFAULTS_RULES;
|
|
161
|
-
case "max-autonomy":
|
|
162
|
-
return {
|
|
163
|
-
"*": "allow",
|
|
164
|
-
webfetch: "ask",
|
|
165
|
-
websearch: "ask",
|
|
166
|
-
codesearch: "ask",
|
|
167
|
-
external_directory: "deny",
|
|
168
|
-
doom_loop: "deny"
|
|
169
|
-
};
|
|
170
|
-
default:
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
|
2
|
+
import { mkdtemp, rm, mkdir, readFile, stat } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import * as clack from "@clack/prompts";
|
|
6
|
+
import type { GitHook } from "@codemcp/ade-core";
|
|
7
|
+
import { writeGitHooks } from "./util.js";
|
|
8
|
+
|
|
9
|
+
describe("writeGitHooks", () => {
|
|
10
|
+
let dir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
dir = await mkdtemp(join(tmpdir(), "ade-util-git-hooks-"));
|
|
14
|
+
vi.spyOn(clack.log, "warn").mockImplementation(() => undefined);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(async () => {
|
|
18
|
+
await rm(dir, { recursive: true, force: true });
|
|
19
|
+
vi.restoreAllMocks();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("is a no-op when hooks is undefined", async () => {
|
|
23
|
+
await expect(writeGitHooks(undefined, dir)).resolves.toBeUndefined();
|
|
24
|
+
expect(clack.log.warn).not.toHaveBeenCalled();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("is a no-op when hooks array is empty", async () => {
|
|
28
|
+
await expect(writeGitHooks([], dir)).resolves.toBeUndefined();
|
|
29
|
+
expect(clack.log.warn).not.toHaveBeenCalled();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("warns and skips gracefully when .git directory does not exist", async () => {
|
|
33
|
+
const hooks: GitHook[] = [
|
|
34
|
+
{ phase: "pre-commit", script: "#!/bin/sh\nnpx lint-staged" }
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
await expect(writeGitHooks(hooks, dir)).resolves.toBeUndefined();
|
|
38
|
+
|
|
39
|
+
expect(clack.log.warn).toHaveBeenCalledOnce();
|
|
40
|
+
expect(clack.log.warn).toHaveBeenCalledWith(
|
|
41
|
+
expect.stringContaining("not a git repository")
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// No .git/hooks directory should have been created
|
|
45
|
+
await expect(stat(join(dir, ".git"))).rejects.toThrow();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("writes hook files when .git exists", async () => {
|
|
49
|
+
await mkdir(join(dir, ".git"), { recursive: true });
|
|
50
|
+
|
|
51
|
+
const script = "#!/bin/sh\nnpx lint-staged\n";
|
|
52
|
+
const hooks: GitHook[] = [{ phase: "pre-commit", script }];
|
|
53
|
+
|
|
54
|
+
await writeGitHooks(hooks, dir);
|
|
55
|
+
|
|
56
|
+
expect(clack.log.warn).not.toHaveBeenCalled();
|
|
57
|
+
|
|
58
|
+
const written = await readFile(
|
|
59
|
+
join(dir, ".git", "hooks", "pre-commit"),
|
|
60
|
+
"utf-8"
|
|
61
|
+
);
|
|
62
|
+
expect(written).toBe(script);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("creates .git/hooks directory if it does not exist yet", async () => {
|
|
66
|
+
// .git exists but no hooks subdir
|
|
67
|
+
await mkdir(join(dir, ".git"), { recursive: true });
|
|
68
|
+
|
|
69
|
+
const hooks: GitHook[] = [{ phase: "pre-commit", script: "#!/bin/sh\n" }];
|
|
70
|
+
await writeGitHooks(hooks, dir);
|
|
71
|
+
|
|
72
|
+
const hookStat = await stat(join(dir, ".git", "hooks"));
|
|
73
|
+
expect(hookStat.isDirectory()).toBe(true);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("writes multiple hooks", async () => {
|
|
77
|
+
await mkdir(join(dir, ".git"), { recursive: true });
|
|
78
|
+
|
|
79
|
+
const hooks: GitHook[] = [
|
|
80
|
+
{ phase: "pre-commit", script: "#!/bin/sh\necho pre-commit\n" },
|
|
81
|
+
{ phase: "pre-push", script: "#!/bin/sh\necho pre-push\n" }
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
await writeGitHooks(hooks, dir);
|
|
85
|
+
|
|
86
|
+
const preCommit = await readFile(
|
|
87
|
+
join(dir, ".git", "hooks", "pre-commit"),
|
|
88
|
+
"utf-8"
|
|
89
|
+
);
|
|
90
|
+
const prePush = await readFile(
|
|
91
|
+
join(dir, ".git", "hooks", "pre-push"),
|
|
92
|
+
"utf-8"
|
|
93
|
+
);
|
|
94
|
+
expect(preCommit).toBe(hooks[0].script);
|
|
95
|
+
expect(prePush).toBe(hooks[1].script);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
|
+
import * as clack from "@clack/prompts";
|
|
3
4
|
import type { GitHook, LogicalConfig, McpServerEntry } from "@codemcp/ade-core";
|
|
4
5
|
|
|
5
6
|
// ---------------------------------------------------------------------------
|
|
@@ -169,15 +170,31 @@ export async function writeAgentMd(
|
|
|
169
170
|
/**
|
|
170
171
|
* Write git hook scripts to `.git/hooks/<phase>`.
|
|
171
172
|
* Files are created with executable permissions (0o755).
|
|
172
|
-
* No-op when the hooks array is empty.
|
|
173
|
+
* No-op when the hooks array is empty or undefined.
|
|
174
|
+
* Emits a warning and skips gracefully when the project root is not a git repository.
|
|
173
175
|
*/
|
|
174
176
|
export async function writeGitHooks(
|
|
175
177
|
hooks: GitHook[] | undefined,
|
|
176
178
|
projectRoot: string
|
|
177
179
|
): Promise<void> {
|
|
178
|
-
if (!hooks) return;
|
|
180
|
+
if (!hooks || hooks.length === 0) return;
|
|
181
|
+
|
|
182
|
+
const gitDir = join(projectRoot, ".git");
|
|
183
|
+
try {
|
|
184
|
+
await access(gitDir);
|
|
185
|
+
} catch {
|
|
186
|
+
clack.log.warn(
|
|
187
|
+
"Git hooks were configured but could not be installed: the project is not a git repository.\n" +
|
|
188
|
+
"Run `git init` and re-run setup to install the hooks."
|
|
189
|
+
);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const hooksDir = join(gitDir, "hooks");
|
|
194
|
+
await mkdir(hooksDir, { recursive: true });
|
|
195
|
+
|
|
179
196
|
for (const hook of hooks) {
|
|
180
|
-
const hookPath = join(
|
|
197
|
+
const hookPath = join(hooksDir, hook.phase);
|
|
181
198
|
await writeFile(hookPath, hook.script, { mode: 0o755 });
|
|
182
199
|
}
|
|
183
200
|
}
|
|
@@ -219,3 +236,14 @@ export async function writeInlineSkills(
|
|
|
219
236
|
|
|
220
237
|
return modified;
|
|
221
238
|
}
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// YAML helpers
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
/** Quote a YAML key only when it contains characters that require quoting. */
|
|
245
|
+
export function formatYamlKey(value: string): string {
|
|
246
|
+
return /^[A-Za-z_][A-Za-z0-9_-]*$/.test(value)
|
|
247
|
+
? value
|
|
248
|
+
: JSON.stringify(value);
|
|
249
|
+
}
|