agentic-workflow-manager 3.0.1 → 3.2.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/dist/src/commands/add.js +84 -0
- package/dist/src/commands/agent.js +95 -0
- package/dist/src/commands/backup.js +46 -0
- package/dist/src/commands/doctor.js +80 -4
- package/dist/src/commands/export.js +48 -0
- package/dist/src/commands/hooks/claude.js +192 -0
- package/dist/src/commands/hooks/codex.js +191 -0
- package/dist/src/commands/hooks/index.js +23 -8
- package/dist/src/commands/hooks/install.js +11 -107
- package/dist/src/commands/hooks/resync.js +28 -18
- package/dist/src/commands/hooks/shared.js +94 -0
- package/dist/src/commands/hooks/status.js +13 -64
- package/dist/src/commands/hooks/uninstall.js +11 -47
- package/dist/src/commands/init.js +90 -28
- package/dist/src/commands/registry/index.js +1 -1
- package/dist/src/commands/sync.js +113 -0
- package/dist/src/commands/update.js +107 -0
- package/dist/src/core/agent-targets.js +50 -0
- package/dist/src/core/artifact-state.js +65 -0
- package/dist/src/core/atomic-file.js +72 -0
- package/dist/src/core/bundle-install.js +60 -38
- package/dist/src/core/bundles.js +21 -0
- package/dist/src/core/context/managed-block.js +190 -0
- package/dist/src/core/context/orchestrator.js +3 -1
- package/dist/src/core/context/project-constitution-inject.js +1 -1
- package/dist/src/core/context/regenerate.js +3 -3
- package/dist/src/core/context/strategies/codex-agents.js +100 -0
- package/dist/src/core/diagnostics/checks.js +16 -0
- package/dist/src/core/diagnostics/context.js +37 -8
- package/dist/src/core/diagnostics/provider-checks.js +219 -0
- package/dist/src/core/discovery.js +9 -3
- package/dist/src/core/executor.js +30 -9
- package/dist/src/core/export/index.js +51 -0
- package/dist/src/core/export/pack.js +58 -0
- package/dist/src/core/export/resolve.js +77 -0
- package/dist/src/core/export/transform.js +57 -0
- package/dist/src/core/export/types.js +2 -0
- package/dist/src/core/init/mutation-targets.js +139 -0
- package/dist/src/core/init/provider-facts.js +164 -0
- package/dist/src/core/init/steps.js +65 -9
- package/dist/src/core/install-planner.js +206 -0
- package/dist/src/core/install-transaction.js +405 -0
- package/dist/src/core/profile.js +1 -1
- package/dist/src/core/provider-artifacts.js +54 -0
- package/dist/src/core/provider-version.js +36 -0
- package/dist/src/core/reconciliation.js +61 -0
- package/dist/src/core/registries.js +10 -0
- package/dist/src/core/renderers/canonical-agent.js +27 -0
- package/dist/src/core/renderers/codex-agent.js +60 -0
- package/dist/src/core/skill-integrity.js +4 -3
- package/dist/src/index.js +99 -289
- package/dist/src/providers/index.js +143 -39
- package/dist/src/ui/provider-preflight.js +26 -0
- package/dist/src/utils/config.js +101 -9
- package/dist/tests/commands/agent.test.js +103 -0
- package/dist/tests/commands/backup.test.js +109 -0
- package/dist/tests/commands/doctor.test.js +65 -4
- package/dist/tests/commands/export.test.js +64 -0
- package/dist/tests/commands/hooks/codex.test.js +230 -0
- package/dist/tests/commands/hooks/install.test.js +20 -1
- package/dist/tests/commands/hooks/resync.test.js +53 -6
- package/dist/tests/commands/hooks/status.test.js +10 -0
- package/dist/tests/commands/hooks/uninstall.test.js +12 -0
- package/dist/tests/commands/init.test.js +195 -8
- package/dist/tests/commands/multi-agent-targeting.test.js +231 -0
- package/dist/tests/core/agent-targets.test.js +45 -0
- package/dist/tests/core/artifact-state.test.js +144 -0
- package/dist/tests/core/atomic-file.test.js +96 -0
- package/dist/tests/core/bundle-install.test.js +208 -17
- package/dist/tests/core/bundles.test.js +12 -0
- package/dist/tests/core/context/managed-block.test.js +90 -0
- package/dist/tests/core/context/orchestrator.test.js +48 -3
- package/dist/tests/core/context/regenerate.test.js +1 -1
- package/dist/tests/core/context/strategies/codex-agents.test.js +185 -0
- package/dist/tests/core/context/strategies/config-instructions.test.js +1 -1
- package/dist/tests/core/diagnostics/checks.test.js +19 -0
- package/dist/tests/core/diagnostics/context.test.js +74 -0
- package/dist/tests/core/diagnostics/provider-checks.test.js +252 -0
- package/dist/tests/core/executor.test.js +20 -0
- package/dist/tests/core/export/engine.test.js +104 -0
- package/dist/tests/core/export/pack.test.js +122 -0
- package/dist/tests/core/export/resolve.test.js +78 -0
- package/dist/tests/core/export/transform.test.js +74 -0
- package/dist/tests/core/init/mutation-targets.test.js +235 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/provider-facts.test.js +129 -0
- package/dist/tests/core/init/steps.test.js +106 -2
- package/dist/tests/core/install-planner.test.js +224 -0
- package/dist/tests/core/install-transaction.test.js +257 -0
- package/dist/tests/core/provider-artifacts.test.js +64 -0
- package/dist/tests/core/provider-version.test.js +53 -0
- package/dist/tests/core/reconciliation.test.js +165 -0
- package/dist/tests/core/registries-sync.test.js +4 -4
- package/dist/tests/core/renderers/canonical-agent.test.js +58 -0
- package/dist/tests/core/renderers/codex-agent.test.js +128 -0
- package/dist/tests/core/versioning.test.js +1 -1
- package/dist/tests/integration/codex-provider-isolated.test.js +206 -0
- package/dist/tests/providers/hooks-config.test.js +43 -25
- package/dist/tests/providers/index.test.js +144 -39
- package/dist/tests/structural/codex-agent-escaping-completeness.test.js +77 -0
- package/dist/tests/ui/provider-preflight.test.js +25 -0
- package/dist/tests/utils/config.test.js +178 -18
- package/package.json +1 -1
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const export_1 = require("../../../src/core/export");
|
|
10
|
+
const okZip = (cwd, zipName) => {
|
|
11
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, zipName), 'fake-zip');
|
|
12
|
+
return { ok: true, missing: false };
|
|
13
|
+
};
|
|
14
|
+
/** Mismo fixture que resolve.test.ts (root falso con bundle dev + 3 skills). */
|
|
15
|
+
function makeRoot() {
|
|
16
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-engine-root-'));
|
|
17
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'bundles/dev'), { recursive: true });
|
|
18
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
|
|
19
|
+
version: 1,
|
|
20
|
+
bundles: [{ name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' }],
|
|
21
|
+
}));
|
|
22
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'bundles/dev/bundle.json'), JSON.stringify({
|
|
23
|
+
name: 'dev', version: '1.0.0', scope: 'baseline', dependsOn: [],
|
|
24
|
+
skills: ['proc-skill', { name: 'mermaid', onSignal: true }, { name: 'ported', onSignal: true }],
|
|
25
|
+
workflows: [], agents: [],
|
|
26
|
+
}));
|
|
27
|
+
const mk = (name, fm) => {
|
|
28
|
+
const dir = path_1.default.join(root, 'skills', name);
|
|
29
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
30
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `---\n${fm.join('\n')}\n---\nBody of ${name}.\n`);
|
|
31
|
+
return dir;
|
|
32
|
+
};
|
|
33
|
+
mk('proc-skill', ['name: proc-skill', 'description: "Process skill."']);
|
|
34
|
+
const mermaid = mk('mermaid', ['name: mermaid', 'version: "1.0.0"', 'portable: true', 'description: "Diagrams."']);
|
|
35
|
+
fs_1.default.mkdirSync(path_1.default.join(mermaid, 'references'));
|
|
36
|
+
fs_1.default.writeFileSync(path_1.default.join(mermaid, 'references/flow.md'), 'flow reference bytes');
|
|
37
|
+
const ported = mk('ported', ['name: ported', 'portable: true', 'description: "Ported."']);
|
|
38
|
+
fs_1.default.writeFileSync(path_1.default.join(ported, 'port.claude-ai.md'), '---\nname: ported\ndescription: "Custom port."\n---\nOverride body, verbatim.\n');
|
|
39
|
+
return root;
|
|
40
|
+
}
|
|
41
|
+
describe('runExport (engine end-to-end)', () => {
|
|
42
|
+
let root;
|
|
43
|
+
let out;
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
root = makeRoot();
|
|
46
|
+
out = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-engine-out-'));
|
|
47
|
+
});
|
|
48
|
+
afterEach(() => {
|
|
49
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
50
|
+
fs_1.default.rmSync(out, { recursive: true, force: true });
|
|
51
|
+
});
|
|
52
|
+
it('exports a bundle: transform for mermaid, verbatim override for ported, skips proc-skill', () => {
|
|
53
|
+
const summary = (0, export_1.runExport)({ name: 'dev', out, roots: [root], zip: okZip });
|
|
54
|
+
expect(summary.kind).toBe('bundle');
|
|
55
|
+
expect(summary.exported.map((e) => e.name).sort()).toEqual(['mermaid', 'ported']);
|
|
56
|
+
expect(summary.skipped).toEqual(['proc-skill']);
|
|
57
|
+
const mermaidMd = fs_1.default.readFileSync(path_1.default.join(out, 'claude-ai/mermaid/SKILL.md'), 'utf-8');
|
|
58
|
+
expect(mermaidMd).not.toMatch(/^version:/m);
|
|
59
|
+
expect(mermaidMd).not.toMatch(/^portable:/m);
|
|
60
|
+
expect(mermaidMd).toContain('defer to the registry');
|
|
61
|
+
expect(fs_1.default.readFileSync(path_1.default.join(out, 'claude-ai/mermaid/references/flow.md'), 'utf-8')).toBe('flow reference bytes');
|
|
62
|
+
const portedMd = fs_1.default.readFileSync(path_1.default.join(out, 'claude-ai/ported/SKILL.md'), 'utf-8');
|
|
63
|
+
expect(portedMd).toBe('---\nname: ported\ndescription: "Custom port."\n---\nOverride body, verbatim.\n'); // cero transforms
|
|
64
|
+
});
|
|
65
|
+
it('rejects an unknown target listing the valid ones', () => {
|
|
66
|
+
expect(() => (0, export_1.runExport)({ name: 'dev', target: 'hermes', out, roots: [root], zip: okZip }))
|
|
67
|
+
.toThrow(new RegExp(export_1.EXPORT_TARGETS.join('|')));
|
|
68
|
+
});
|
|
69
|
+
it('wraps transform errors with the offending file path', () => {
|
|
70
|
+
// portable: true se mantiene (si no, resolve.ts corta antes por R2.x y nunca llega
|
|
71
|
+
// a transform.ts); lo que rompe es la ausencia de "description:", que transform.ts exige.
|
|
72
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/mermaid/SKILL.md'), '---\nname: mermaid\nportable: true\n---\nNo description field.\n');
|
|
73
|
+
expect(() => (0, export_1.runExport)({ name: 'mermaid', out, roots: [root], zip: okZip }))
|
|
74
|
+
.toThrow(/mermaid[/\\]SKILL\.md/);
|
|
75
|
+
});
|
|
76
|
+
it('reads only the provided roots (never ~/.claude/skills)', () => {
|
|
77
|
+
// La lectura sale exclusivamente de roots: un root vacío no resuelve nada.
|
|
78
|
+
// Control negativo real: contentRoots() (el fallback al registry instalado)
|
|
79
|
+
// no debe invocarse en absoluto cuando opts.roots viene dado explícitamente.
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
81
|
+
const registries = require('../../../src/core/registries');
|
|
82
|
+
const spy = jest.spyOn(registries, 'contentRoots');
|
|
83
|
+
const empty = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-empty-root-'));
|
|
84
|
+
expect(() => (0, export_1.runExport)({ name: 'mermaid', out, roots: [empty], zip: okZip })).toThrow(/neither a bundle nor a skill/);
|
|
85
|
+
expect(spy).not.toHaveBeenCalled();
|
|
86
|
+
spy.mockRestore();
|
|
87
|
+
fs_1.default.rmSync(empty, { recursive: true, force: true });
|
|
88
|
+
});
|
|
89
|
+
it('defaults --out to ./awm-export/<target> under the current working directory', () => {
|
|
90
|
+
const cwdTmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-engine-cwd-'));
|
|
91
|
+
const originalCwd = process.cwd();
|
|
92
|
+
process.chdir(cwdTmp);
|
|
93
|
+
try {
|
|
94
|
+
const actualCwd = process.cwd();
|
|
95
|
+
const summary = (0, export_1.runExport)({ name: 'mermaid', roots: [root], zip: okZip }); // sin `out`
|
|
96
|
+
expect(summary.outDir).toBe(path_1.default.join(actualCwd, 'awm-export', 'claude-ai'));
|
|
97
|
+
expect(fs_1.default.existsSync(path_1.default.join(actualCwd, 'awm-export/claude-ai/mermaid/SKILL.md'))).toBe(true);
|
|
98
|
+
}
|
|
99
|
+
finally {
|
|
100
|
+
process.chdir(originalCwd);
|
|
101
|
+
fs_1.default.rmSync(cwdTmp, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const pack_1 = require("../../../src/core/export/pack");
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
11
|
+
const childProcess = require('child_process');
|
|
12
|
+
const okZip = (cwd, zipName) => {
|
|
13
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, zipName), 'fake-zip');
|
|
14
|
+
return { ok: true, missing: false };
|
|
15
|
+
};
|
|
16
|
+
const missingZip = () => ({ ok: false, missing: true });
|
|
17
|
+
describe('packSkill', () => {
|
|
18
|
+
let src;
|
|
19
|
+
let out;
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
src = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-src-'));
|
|
22
|
+
out = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-out-'));
|
|
23
|
+
fs_1.default.writeFileSync(path_1.default.join(src, 'SKILL.md'), 'canonical');
|
|
24
|
+
fs_1.default.mkdirSync(path_1.default.join(src, 'references'));
|
|
25
|
+
fs_1.default.writeFileSync(path_1.default.join(src, 'references/a.md'), 'ref-A bytes');
|
|
26
|
+
});
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs_1.default.rmSync(src, { recursive: true, force: true });
|
|
29
|
+
fs_1.default.rmSync(out, { recursive: true, force: true });
|
|
30
|
+
});
|
|
31
|
+
it('writes adapted SKILL.md and byte-identical references', () => {
|
|
32
|
+
const r = (0, pack_1.packSkill)({ name: 'x', adaptedSkillMd: 'adapted', srcDir: src, targetRoot: out, zip: okZip });
|
|
33
|
+
expect(fs_1.default.readFileSync(path_1.default.join(out, 'x/SKILL.md'), 'utf-8')).toBe('adapted');
|
|
34
|
+
expect(fs_1.default.readFileSync(path_1.default.join(out, 'x/references/a.md'), 'utf-8')).toBe('ref-A bytes');
|
|
35
|
+
expect(r.zip).toBe(path_1.default.join(out, 'x.zip'));
|
|
36
|
+
});
|
|
37
|
+
it('re-export cleans its own subtree first (stale files gone)', () => {
|
|
38
|
+
fs_1.default.mkdirSync(path_1.default.join(out, 'x'), { recursive: true });
|
|
39
|
+
fs_1.default.writeFileSync(path_1.default.join(out, 'x/stale.md'), 'old');
|
|
40
|
+
fs_1.default.writeFileSync(path_1.default.join(out, 'x.zip'), 'old-zip');
|
|
41
|
+
(0, pack_1.packSkill)({ name: 'x', adaptedSkillMd: 'adapted', srcDir: src, targetRoot: out, zip: missingZip });
|
|
42
|
+
expect(fs_1.default.existsSync(path_1.default.join(out, 'x/stale.md'))).toBe(false);
|
|
43
|
+
expect(fs_1.default.existsSync(path_1.default.join(out, 'x.zip'))).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
it('falls back to folder-only when zip binary is missing', () => {
|
|
46
|
+
const r = (0, pack_1.packSkill)({ name: 'x', adaptedSkillMd: 'adapted', srcDir: src, targetRoot: out, zip: missingZip });
|
|
47
|
+
expect(r.zip).toBeNull();
|
|
48
|
+
expect(r.zipMissing).toBe(true);
|
|
49
|
+
expect(fs_1.default.existsSync(path_1.default.join(out, 'x/SKILL.md'))).toBe(true);
|
|
50
|
+
});
|
|
51
|
+
it('skill without references/ packs SKILL.md alone', () => {
|
|
52
|
+
fs_1.default.rmSync(path_1.default.join(src, 'references'), { recursive: true });
|
|
53
|
+
(0, pack_1.packSkill)({ name: 'x', adaptedSkillMd: 'adapted', srcDir: src, targetRoot: out, zip: okZip });
|
|
54
|
+
expect(fs_1.default.existsSync(path_1.default.join(out, 'x/references'))).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it('throws when the zip function reports a real failure (not missing binary)', () => {
|
|
57
|
+
const failingZip = () => ({ ok: false, missing: false });
|
|
58
|
+
expect(() => (0, pack_1.packSkill)({ name: 'x', adaptedSkillMd: 'adapted', srcDir: src, targetRoot: out, zip: failingZip }))
|
|
59
|
+
.toThrow(/zip failed/);
|
|
60
|
+
});
|
|
61
|
+
it('refuses to export when references/ contains a symlink (blocks zip-dereference exfiltration)', () => {
|
|
62
|
+
// Simulate the exfiltration shape: a "secret" file living entirely
|
|
63
|
+
// outside srcDir, symlinked from within references/.
|
|
64
|
+
const secretsDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-pack-secret-'));
|
|
65
|
+
const secretPath = path_1.default.join(secretsDir, 'id_rsa');
|
|
66
|
+
const secretContents = 'THIS-IS-A-PRIVATE-KEY-SHOULD-NEVER-LEAK';
|
|
67
|
+
fs_1.default.writeFileSync(secretPath, secretContents);
|
|
68
|
+
fs_1.default.symlinkSync(secretPath, path_1.default.join(src, 'references/evil-link'));
|
|
69
|
+
try {
|
|
70
|
+
expect(() => (0, pack_1.packSkill)({ name: 'x', adaptedSkillMd: 'adapted', srcDir: src, targetRoot: out, zip: okZip })).toThrow(/symlink/i);
|
|
71
|
+
// Nothing under targetRoot should ever contain the secret's bytes,
|
|
72
|
+
// whether as a copied symlink or a dereferenced regular file.
|
|
73
|
+
const walk = (dir) => {
|
|
74
|
+
if (!fs_1.default.existsSync(dir))
|
|
75
|
+
return [];
|
|
76
|
+
return fs_1.default.readdirSync(dir, { withFileTypes: true }).flatMap((e) => {
|
|
77
|
+
const full = path_1.default.join(dir, e.name);
|
|
78
|
+
if (e.isDirectory())
|
|
79
|
+
return walk(full);
|
|
80
|
+
if (e.isSymbolicLink())
|
|
81
|
+
return [full]; // leaked symlink itself is also unacceptable
|
|
82
|
+
return [full];
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
for (const f of walk(out)) {
|
|
86
|
+
const st = fs_1.default.lstatSync(f);
|
|
87
|
+
if (st.isSymbolicLink()) {
|
|
88
|
+
throw new Error(`leaked symlink found under targetRoot: ${f}`);
|
|
89
|
+
}
|
|
90
|
+
const contents = fs_1.default.readFileSync(f, 'utf-8');
|
|
91
|
+
expect(contents).not.toContain(secretContents);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
fs_1.default.rmSync(secretsDir, { recursive: true, force: true });
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
describe('defaultZip (system binary, layered)', () => {
|
|
100
|
+
it('produces a real zip when the binary exists, or reports missing', () => {
|
|
101
|
+
const cwd = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-zip-'));
|
|
102
|
+
fs_1.default.mkdirSync(path_1.default.join(cwd, 'folder'));
|
|
103
|
+
fs_1.default.writeFileSync(path_1.default.join(cwd, 'folder/f.txt'), 'x');
|
|
104
|
+
const r = (0, pack_1.defaultZip)(cwd, 'folder.zip', 'folder');
|
|
105
|
+
if (r.missing) {
|
|
106
|
+
expect(fs_1.default.existsSync(path_1.default.join(cwd, 'folder.zip'))).toBe(false); // degrade limpio
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
expect(r.ok).toBe(true);
|
|
110
|
+
expect(fs_1.default.existsSync(path_1.default.join(cwd, 'folder.zip'))).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
fs_1.default.rmSync(cwd, { recursive: true, force: true });
|
|
113
|
+
});
|
|
114
|
+
it('returns missing:true when spawnSync reports ENOENT (binary absent)', () => {
|
|
115
|
+
const spy = jest.spyOn(childProcess, 'spawnSync').mockReturnValue({
|
|
116
|
+
error: Object.assign(new Error('spawn zip ENOENT'), { code: 'ENOENT' }),
|
|
117
|
+
});
|
|
118
|
+
const r = (0, pack_1.defaultZip)('/irrelevant', 'folder.zip', 'folder');
|
|
119
|
+
expect(r).toEqual({ ok: false, missing: true });
|
|
120
|
+
spy.mockRestore();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const resolve_1 = require("../../../src/core/export/resolve");
|
|
10
|
+
/** Content root falso: catalog + bundle dev + 3 skills. */
|
|
11
|
+
function makeRoot() {
|
|
12
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-export-root-'));
|
|
13
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'bundles/dev'), { recursive: true });
|
|
14
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
|
|
15
|
+
version: 1,
|
|
16
|
+
bundles: [{ name: 'dev', source: './bundles/dev', version: '1.0.0', scope: 'baseline' }],
|
|
17
|
+
}));
|
|
18
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'bundles/dev/bundle.json'), JSON.stringify({
|
|
19
|
+
name: 'dev', version: '1.0.0', scope: 'baseline', dependsOn: [],
|
|
20
|
+
skills: ['proc-skill', { name: 'mermaid', onSignal: true }, { name: 'ported', onSignal: true }],
|
|
21
|
+
workflows: [], agents: [],
|
|
22
|
+
}));
|
|
23
|
+
const mk = (name, fm) => {
|
|
24
|
+
const dir = path_1.default.join(root, 'skills', name);
|
|
25
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
26
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `---\n${fm.join('\n')}\n---\nBody.\n`);
|
|
27
|
+
return dir;
|
|
28
|
+
};
|
|
29
|
+
mk('proc-skill', ['name: proc-skill', 'description: "Process skill."']); // NO portable
|
|
30
|
+
mk('mermaid', ['name: mermaid', 'portable: true', 'description: "Diagrams."']);
|
|
31
|
+
const ported = mk('ported', ['name: ported', 'portable: true', 'description: "Ported."']);
|
|
32
|
+
fs_1.default.writeFileSync(path_1.default.join(ported, 'port.claude-ai.md'), '---\nname: ported\ndescription: "Custom."\n---\nCustom body.\n');
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
describe('resolveExport', () => {
|
|
36
|
+
let root;
|
|
37
|
+
beforeEach(() => { root = makeRoot(); });
|
|
38
|
+
afterEach(() => { fs_1.default.rmSync(root, { recursive: true, force: true }); });
|
|
39
|
+
it('resolves a bundle: portable skills in, non-portable listed as skipped', () => {
|
|
40
|
+
const res = (0, resolve_1.resolveExport)('dev', [root]);
|
|
41
|
+
expect(res.kind).toBe('bundle');
|
|
42
|
+
expect(res.skills.map((s) => s.name).sort()).toEqual(['mermaid', 'ported']);
|
|
43
|
+
expect(res.skipped).toEqual(['proc-skill']);
|
|
44
|
+
});
|
|
45
|
+
it('detects the override path when present', () => {
|
|
46
|
+
const res = (0, resolve_1.resolveExport)('dev', [root]);
|
|
47
|
+
const ported = res.skills.find((s) => s.name === 'ported');
|
|
48
|
+
expect(ported.overridePath).toBe(path_1.default.join(root, 'skills/ported/port.claude-ai.md'));
|
|
49
|
+
const mermaid = res.skills.find((s) => s.name === 'mermaid');
|
|
50
|
+
expect(mermaid.overridePath).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
it('resolves an individual portable skill', () => {
|
|
53
|
+
const res = (0, resolve_1.resolveExport)('mermaid', [root]);
|
|
54
|
+
expect(res.kind).toBe('skill');
|
|
55
|
+
expect(res.skills).toHaveLength(1);
|
|
56
|
+
expect(res.skipped).toEqual([]);
|
|
57
|
+
});
|
|
58
|
+
it('fails on an explicitly requested non-portable skill', () => {
|
|
59
|
+
expect(() => (0, resolve_1.resolveExport)('proc-skill', [root])).toThrow(/portable/);
|
|
60
|
+
});
|
|
61
|
+
it('fails on unknown name, listing available bundles', () => {
|
|
62
|
+
expect(() => (0, resolve_1.resolveExport)('nope', [root])).toThrow(/dev/);
|
|
63
|
+
});
|
|
64
|
+
it('fails when a bundle has zero portable skills', () => {
|
|
65
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/mermaid/SKILL.md'), '---\nname: mermaid\ndescription: "D."\n---\nB.\n');
|
|
66
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/ported/SKILL.md'), '---\nname: ported\ndescription: "P."\n---\nB.\n');
|
|
67
|
+
fs_1.default.rmSync(path_1.default.join(root, 'skills/ported/port.claude-ai.md'));
|
|
68
|
+
expect(() => (0, resolve_1.resolveExport)('dev', [root])).toThrow(/no portable skills/i);
|
|
69
|
+
});
|
|
70
|
+
it('fails on override without portable: true (inconsistent metadata)', () => {
|
|
71
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/ported/SKILL.md'), '---\nname: ported\ndescription: "P."\n---\nB.\n');
|
|
72
|
+
expect(() => (0, resolve_1.resolveExport)('ported', [root])).toThrow(/inconsistent/i);
|
|
73
|
+
});
|
|
74
|
+
it('fails on override without portable: true when resolved via a bundle (assertOverrideConsistency bundle-loop path)', () => {
|
|
75
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/ported/SKILL.md'), '---\nname: ported\ndescription: "P."\n---\nB.\n');
|
|
76
|
+
expect(() => (0, resolve_1.resolveExport)('dev', [root])).toThrow(/inconsistent/i);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const transform_1 = require("../../../src/core/export/transform");
|
|
4
|
+
const FM = (lines) => `---\n${lines.join('\n')}\n---\nBody line.\n`;
|
|
5
|
+
describe('claudeAiTransform', () => {
|
|
6
|
+
it('strips version and portable, keeps other keys and body intact', () => {
|
|
7
|
+
const input = FM(['name: mermaid-diagrams', 'version: "1.0.0"', 'portable: true', 'description: "Guide."']);
|
|
8
|
+
const out = (0, transform_1.claudeAiTransform)(input, 'mermaid-diagrams');
|
|
9
|
+
expect(out).not.toMatch(/^version:/m);
|
|
10
|
+
expect(out).not.toMatch(/^portable:/m);
|
|
11
|
+
expect(out).toMatch(/^name: mermaid-diagrams$/m);
|
|
12
|
+
expect(out).toContain('Body line.\n');
|
|
13
|
+
});
|
|
14
|
+
it('appends the deference line inside a quoted description', () => {
|
|
15
|
+
const input = FM(['name: x', 'portable: true', 'description: "Does things."']);
|
|
16
|
+
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
17
|
+
expect(out).toContain(`description: "Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}"`);
|
|
18
|
+
});
|
|
19
|
+
it('appends the deference line to an unquoted description', () => {
|
|
20
|
+
const input = FM(['name: x', 'portable: true', 'description: Does things.']);
|
|
21
|
+
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
22
|
+
expect(out).toContain(`description: Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}`);
|
|
23
|
+
});
|
|
24
|
+
it('appends the deference line inside a single-quoted description', () => {
|
|
25
|
+
const input = FM(['name: x', 'portable: true', "description: 'Does things.'"]);
|
|
26
|
+
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
27
|
+
// DEFERENCE_LINE itself always contains an apostrophe ("registry's"), so
|
|
28
|
+
// even a fixture with no apostrophe of its own must see it doubled ('')
|
|
29
|
+
// per YAML single-quote escaping once spliced into a single-quoted scalar.
|
|
30
|
+
expect(out).toContain(`description: 'Does things. ${(0, transform_1.DEFERENCE_LINE)('x').replace(/'/g, "''")}'`);
|
|
31
|
+
});
|
|
32
|
+
it('appends the deference line inside a double-quoted description with trailing whitespace', () => {
|
|
33
|
+
const input = FM(['name: x', 'portable: true', 'description: "Does things." ']);
|
|
34
|
+
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
35
|
+
expect(out).toContain(`description: "Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}"`);
|
|
36
|
+
});
|
|
37
|
+
it('accepts CRLF-terminated frontmatter', () => {
|
|
38
|
+
const input = '---\r\nname: x\r\nportable: true\r\ndescription: "Does things."\r\n---\r\nBody line.\r\n';
|
|
39
|
+
const out = (0, transform_1.claudeAiTransform)(input, 'x');
|
|
40
|
+
expect(out).not.toMatch(/^portable:/m);
|
|
41
|
+
expect(out).toContain(`description: "Does things. ${(0, transform_1.DEFERENCE_LINE)('x')}"`);
|
|
42
|
+
expect(out).toContain('Body line.\r\n');
|
|
43
|
+
});
|
|
44
|
+
it('throws on missing frontmatter block', () => {
|
|
45
|
+
expect(() => (0, transform_1.claudeAiTransform)('No frontmatter here.', 'x')).toThrow(/frontmatter/);
|
|
46
|
+
});
|
|
47
|
+
it('throws on unterminated frontmatter block', () => {
|
|
48
|
+
expect(() => (0, transform_1.claudeAiTransform)('---\nname: x\ndescription: "D."\n', 'x')).toThrow(/unterminated/);
|
|
49
|
+
});
|
|
50
|
+
it('throws on frontmatter without description', () => {
|
|
51
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'portable: true']), 'x')).toThrow(/description/);
|
|
52
|
+
});
|
|
53
|
+
it('throws on multi-line (block scalar) description', () => {
|
|
54
|
+
expect(() => (0, transform_1.claudeAiTransform)(FM(['name: x', 'description: >', ' folded text']), 'x')).toThrow(/single-line/);
|
|
55
|
+
});
|
|
56
|
+
it('escapes an apostrophe in the deference line when appending to a single-quoted description', () => {
|
|
57
|
+
const input = FM(['name: mermaid', 'portable: true', "description: 'Diagrams and flowcharts.'"]);
|
|
58
|
+
const out = (0, transform_1.claudeAiTransform)(input, 'mermaid');
|
|
59
|
+
// The apostrophe in "registry's" must be doubled ('') per YAML single-quote escaping.
|
|
60
|
+
expect(out).toContain("registry''s mermaid skill");
|
|
61
|
+
expect(out).not.toContain("registry's mermaid skill");
|
|
62
|
+
const descLine = out.split('\n').find((l) => l.startsWith('description:'));
|
|
63
|
+
expect(descLine).toBe(`description: 'Diagrams and flowcharts. ${(0, transform_1.DEFERENCE_LINE)('mermaid').replace(/'/g, "''")}'`);
|
|
64
|
+
// Sanity check the result is well-formed: single-quoted scalar body has no
|
|
65
|
+
// lone (unescaped) apostrophes — every ' is either the opening/closing
|
|
66
|
+
// quote or part of a doubled '' pair.
|
|
67
|
+
const body = descLine.slice('description: \''.length, -1);
|
|
68
|
+
expect(body.replace(/''/g, '')).not.toMatch(/'/);
|
|
69
|
+
});
|
|
70
|
+
it('throws when a quoted description has trailing content after its closing quote (e.g. inline comment)', () => {
|
|
71
|
+
const input = FM(['name: x', 'portable: true', 'description: "Does things." # a comment']);
|
|
72
|
+
expect(() => (0, transform_1.claudeAiTransform)(input, 'x')).toThrow(/trailing content|comment/i);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
// Direct, focused tests for planInitMutationTargets (mutation-targets.ts).
|
|
7
|
+
//
|
|
8
|
+
// Previously this module was only exercised indirectly via
|
|
9
|
+
// tests/commands/init.test.ts's broader init-flow tests, which happen to hit
|
|
10
|
+
// the hooks.settingsPath branch but not the ambient/baseline/project-extension
|
|
11
|
+
// branches, nor the Finding-1 fix (global skills directory) directly.
|
|
12
|
+
//
|
|
13
|
+
// registries.ts computes AWM_HOME at module-require time (not call time), so
|
|
14
|
+
// every test here sets HOME/AWM_HOME BEFORE requiring
|
|
15
|
+
// core/init/mutation-targets.ts, and calls jest.resetModules() first — the
|
|
16
|
+
// same pattern used throughout tests/commands/hooks/install.test.ts and
|
|
17
|
+
// tests/core/skill-integrity.test.ts. Per CLAUDE.md, no test may touch the
|
|
18
|
+
// real ~/.awm.
|
|
19
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
|
+
const os_1 = __importDefault(require("os"));
|
|
21
|
+
const path_1 = __importDefault(require("path"));
|
|
22
|
+
function bundle(name, scope, skills) {
|
|
23
|
+
return {
|
|
24
|
+
name, description: '', version: '1.0.0', scope, visibility: 'public',
|
|
25
|
+
dependsOn: [], skills: skills.map((s) => ({ name: s, onSignal: false })),
|
|
26
|
+
workflows: [], agents: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
let tmpHome;
|
|
30
|
+
let originalHome;
|
|
31
|
+
let originalAwmHome;
|
|
32
|
+
let extraDirs;
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
tmpHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-'));
|
|
35
|
+
originalHome = process.env.HOME;
|
|
36
|
+
originalAwmHome = process.env.AWM_HOME;
|
|
37
|
+
process.env.HOME = tmpHome;
|
|
38
|
+
process.env.AWM_HOME = path_1.default.join(tmpHome, '.awm');
|
|
39
|
+
jest.resetModules();
|
|
40
|
+
extraDirs = [];
|
|
41
|
+
});
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
44
|
+
for (const dir of extraDirs)
|
|
45
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
46
|
+
if (originalHome === undefined)
|
|
47
|
+
delete process.env.HOME;
|
|
48
|
+
else
|
|
49
|
+
process.env.HOME = originalHome;
|
|
50
|
+
if (originalAwmHome === undefined)
|
|
51
|
+
delete process.env.AWM_HOME;
|
|
52
|
+
else
|
|
53
|
+
process.env.AWM_HOME = originalAwmHome;
|
|
54
|
+
});
|
|
55
|
+
/**
|
|
56
|
+
* Loads a fresh copy of the module under test (and providerFor, which needs
|
|
57
|
+
* the same env-var-driven paths) AFTER env vars are set for this test.
|
|
58
|
+
*/
|
|
59
|
+
function load() {
|
|
60
|
+
const mutationTargets = require('../../../src/core/init/mutation-targets');
|
|
61
|
+
const providers = require('../../../src/providers');
|
|
62
|
+
return {
|
|
63
|
+
planInitMutationTargets: mutationTargets.planInitMutationTargets,
|
|
64
|
+
providerFor: providers.providerFor,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** A cwd guaranteed to have no project-root marker (.git/package.json/.awm) in its ancestry. */
|
|
68
|
+
function bareCwd() {
|
|
69
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-cwd-'));
|
|
70
|
+
extraDirs.push(dir);
|
|
71
|
+
return dir;
|
|
72
|
+
}
|
|
73
|
+
describe('planInitMutationTargets', () => {
|
|
74
|
+
it('always includes preferences.json and the artifact-state file', () => {
|
|
75
|
+
const { planInitMutationTargets } = load();
|
|
76
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles: [] });
|
|
77
|
+
expect(targets).toContain(path_1.default.join(tmpHome, '.awm', 'preferences.json'));
|
|
78
|
+
expect(targets).toContain(path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json'));
|
|
79
|
+
});
|
|
80
|
+
describe('hooks', () => {
|
|
81
|
+
it('includes hooks.settingsPath and hooks.scriptsDir for an agent with a hook config (claude-code)', () => {
|
|
82
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
83
|
+
const provider = providerFor('claude-code');
|
|
84
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles: [] });
|
|
85
|
+
expect(provider.hooks).toBeDefined();
|
|
86
|
+
expect(targets).toContain(provider.hooks.settingsPath);
|
|
87
|
+
expect(targets).toContain(provider.hooks.scriptsDir);
|
|
88
|
+
});
|
|
89
|
+
it('includes hooks.json + scriptsDir for codex (its own hook config shape)', () => {
|
|
90
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
91
|
+
const provider = providerFor('codex');
|
|
92
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'codex', bundles: [] });
|
|
93
|
+
expect(targets).toContain(provider.hooks.settingsPath);
|
|
94
|
+
expect(targets).toContain(provider.hooks.scriptsDir);
|
|
95
|
+
});
|
|
96
|
+
it('includes no hook paths for an agent with no hook config (antigravity)', () => {
|
|
97
|
+
const { planInitMutationTargets } = load();
|
|
98
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'antigravity', bundles: [] });
|
|
99
|
+
// antigravity has neither hooks nor injection — its only entries are
|
|
100
|
+
// the two always-present machine paths.
|
|
101
|
+
expect(targets).toEqual([
|
|
102
|
+
path_1.default.join(tmpHome, '.awm', 'preferences.json'),
|
|
103
|
+
path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json'),
|
|
104
|
+
path_1.default.join(tmpHome, '.gemini', 'antigravity', 'skills'),
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe('injection', () => {
|
|
109
|
+
it('includes the config-instructions path for opencode', () => {
|
|
110
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
111
|
+
const provider = providerFor('opencode');
|
|
112
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'opencode', bundles: [] });
|
|
113
|
+
expect(provider.injection?.type).toBe('config-instructions');
|
|
114
|
+
expect(targets).toContain(provider.injection.configPath);
|
|
115
|
+
});
|
|
116
|
+
it('includes the managed-agents-md global path for codex', () => {
|
|
117
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
118
|
+
const provider = providerFor('codex');
|
|
119
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'codex', bundles: [] });
|
|
120
|
+
expect(provider.injection?.type).toBe('managed-agents-md');
|
|
121
|
+
expect(targets).toContain(provider.injection.globalPath);
|
|
122
|
+
});
|
|
123
|
+
it('adds no separate injection path for claude-code (cc-settings-merge is covered by the hook)', () => {
|
|
124
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
125
|
+
const provider = providerFor('claude-code');
|
|
126
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles: [] });
|
|
127
|
+
expect(provider.injection?.type).toBe('cc-settings-merge');
|
|
128
|
+
// Every target should be explainable by preferences/state/hooks/skills-dir —
|
|
129
|
+
// none of them should be a bespoke injection config file.
|
|
130
|
+
expect(targets).toEqual([
|
|
131
|
+
path_1.default.join(tmpHome, '.awm', 'preferences.json'),
|
|
132
|
+
path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json'),
|
|
133
|
+
provider.hooks.settingsPath,
|
|
134
|
+
provider.hooks.scriptsDir,
|
|
135
|
+
provider.skill.global,
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
describe('machine-level bundle targets (baseline + ambient)', () => {
|
|
140
|
+
it('includes physical skill targets for baseline and ambient bundles, but not a project-scope bundle', () => {
|
|
141
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
142
|
+
const bundles = [
|
|
143
|
+
bundle('dev-core', 'baseline', ['using-awm']),
|
|
144
|
+
bundle('ambient-pack', 'ambient', ['ambient-skill']),
|
|
145
|
+
bundle('project-only', 'project', ['project-skill']),
|
|
146
|
+
];
|
|
147
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles });
|
|
148
|
+
const skillsDir = providerFor('claude-code').skill.global;
|
|
149
|
+
expect(targets).toContain(path_1.default.join(skillsDir, 'using-awm'));
|
|
150
|
+
expect(targets).toContain(path_1.default.join(skillsDir, 'ambient-skill'));
|
|
151
|
+
// project-scope bundles are only enumerated via .awm/profile.json
|
|
152
|
+
// extensions below, never from the machine-level baseline/ambient pass.
|
|
153
|
+
expect(targets).not.toContain(path_1.default.join(skillsDir, 'project-skill'));
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
describe('project-level targets', () => {
|
|
157
|
+
function makeProjectRoot() {
|
|
158
|
+
const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-mutation-targets-project-'));
|
|
159
|
+
fs_1.default.mkdirSync(path_1.default.join(root, '.git')); // project-root marker for findProjectRoot
|
|
160
|
+
extraDirs.push(root);
|
|
161
|
+
return root;
|
|
162
|
+
}
|
|
163
|
+
it('includes profile.json, sensors.json, and extension bundle targets when a project root is found', () => {
|
|
164
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
165
|
+
const projectRoot = makeProjectRoot();
|
|
166
|
+
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
167
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['frontend'] }));
|
|
168
|
+
const bundles = [bundle('frontend', 'project', ['frontend-skill'])];
|
|
169
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'claude-code', bundles });
|
|
170
|
+
expect(targets).toContain(path_1.default.join(projectRoot, '.awm', 'profile.json'));
|
|
171
|
+
expect(targets).toContain(path_1.default.join(projectRoot, '.awm', 'sensors.json'));
|
|
172
|
+
// frontend is a project-scope bundle -> installs locally under the project root
|
|
173
|
+
const localSkillsDir = path_1.default.join(projectRoot, providerFor('claude-code').skill.local);
|
|
174
|
+
expect(targets).toContain(path_1.default.join(localSkillsDir, 'frontend-skill'));
|
|
175
|
+
});
|
|
176
|
+
it('includes the project injection file for config-instructions agents (opencode)', () => {
|
|
177
|
+
const { planInitMutationTargets } = load();
|
|
178
|
+
const projectRoot = makeProjectRoot();
|
|
179
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'opencode', bundles: [] });
|
|
180
|
+
expect(targets).toContain(path_1.default.join(projectRoot, 'opencode.json'));
|
|
181
|
+
});
|
|
182
|
+
it('includes the project injection file (AGENTS.md) for managed-agents-md agents (codex)', () => {
|
|
183
|
+
const { planInitMutationTargets } = load();
|
|
184
|
+
const projectRoot = makeProjectRoot();
|
|
185
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'codex', bundles: [] });
|
|
186
|
+
expect(targets).toContain(path_1.default.join(projectRoot, 'AGENTS.md'));
|
|
187
|
+
});
|
|
188
|
+
it('omits every project-level target when no project root is found', () => {
|
|
189
|
+
const { planInitMutationTargets } = load();
|
|
190
|
+
const cwd = bareCwd(); // no .git/package.json/.awm ancestor
|
|
191
|
+
const targets = planInitMutationTargets({ cwd, agent: 'claude-code', bundles: [] });
|
|
192
|
+
expect(targets.some((t) => t.endsWith(path_1.default.join('.awm', 'profile.json')))).toBe(false);
|
|
193
|
+
expect(targets.some((t) => t.endsWith(path_1.default.join('.awm', 'sensors.json')))).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
it('ignores an extension named in the profile that no longer resolves to a known bundle', () => {
|
|
196
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
197
|
+
const projectRoot = makeProjectRoot();
|
|
198
|
+
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
199
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['stale-bundle'] }));
|
|
200
|
+
const targets = planInitMutationTargets({ cwd: projectRoot, agent: 'claude-code', bundles: [] });
|
|
201
|
+
const localSkillsDir = path_1.default.join(projectRoot, providerFor('claude-code').skill.local);
|
|
202
|
+
// No target should be produced under the local skills dir for a bundle
|
|
203
|
+
// that isn't in the (empty) known-bundles list passed in.
|
|
204
|
+
expect(targets.some((t) => t.startsWith(localSkillsDir))).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
describe('Finding 1 fix — global skills directory itself', () => {
|
|
208
|
+
it('includes providerFor(agent).skill.global for every agent, independent of the bundle catalog', () => {
|
|
209
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
210
|
+
for (const agent of ['claude-code', 'codex', 'opencode', 'antigravity']) {
|
|
211
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent, bundles: [] });
|
|
212
|
+
expect(targets).toContain(providerFor(agent).skill.global);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
it('the skills-directory target covers orphaned entries repairGlobalSkills would mutate, not just bundle-derived subpaths', () => {
|
|
216
|
+
// Regression guard for the logic gap: before the fix, only
|
|
217
|
+
// bundle-DERIVED skill paths (e.g. .../skills/using-awm) were
|
|
218
|
+
// enumerated. An orphaned symlink like .../skills/some-old-skill
|
|
219
|
+
// (belonging to no currently-known bundle — the exact condition
|
|
220
|
+
// that makes repairGlobalSkills classify it as repairable/dead)
|
|
221
|
+
// would never appear in the bundle-driven enumeration. The parent
|
|
222
|
+
// directory itself must be present so beginBackupSession snapshots
|
|
223
|
+
// the whole tree.
|
|
224
|
+
const { planInitMutationTargets, providerFor } = load();
|
|
225
|
+
const bundles = [bundle('dev-core', 'baseline', ['using-awm'])];
|
|
226
|
+
const targets = planInitMutationTargets({ cwd: bareCwd(), agent: 'claude-code', bundles });
|
|
227
|
+
const skillsDir = providerFor('claude-code').skill.global;
|
|
228
|
+
expect(targets).toContain(skillsDir);
|
|
229
|
+
// Sanity: the orphan path itself is a child of the now-covered dir,
|
|
230
|
+
// even though it is never separately enumerated.
|
|
231
|
+
const orphanPath = path_1.default.join(skillsDir, 'some-old-orphaned-skill');
|
|
232
|
+
expect(orphanPath.startsWith(skillsDir)).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -74,7 +74,7 @@ describe('runInitSteps — orchestrator', () => {
|
|
|
74
74
|
const contentDir = roots[0] ?? '';
|
|
75
75
|
const ctx = gatherContext({ cwd, bundles });
|
|
76
76
|
return {
|
|
77
|
-
cwd, ctx, bundles, agent: 'claude-code', installMethod: 'symlink',
|
|
77
|
+
cwd, ctx, bundles, agent: 'claude-code', enabledAgents: ['claude-code'], installMethod: 'symlink',
|
|
78
78
|
registryRoot: contentRoot, contentDir, sensorPacksRoot: contentRoot,
|
|
79
79
|
confirmExtensions: async (p) => p,
|
|
80
80
|
// syncCache es no-op: el cache ya está sembrado en disco
|