agentic-workflow-manager 3.13.5 → 3.13.6
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 +5 -0
- package/dist/src/commands/doctor.js +1 -0
- package/dist/src/commands/hooks/claude.js +15 -2
- package/dist/src/commands/hooks/shared.js +6 -1
- package/dist/src/commands/init.js +1 -0
- package/dist/src/commands/pin.js +24 -4
- package/dist/src/commands/preflight/checks.js +3 -2
- package/dist/src/commands/registry/add.js +1 -1
- package/dist/src/commands/sensors/index.js +7 -0
- package/dist/src/commands/sensors/init.js +48 -7
- package/dist/src/commands/sensors/install.js +20 -11
- package/dist/src/commands/sensors/run.js +31 -29
- package/dist/src/commands/sync.js +29 -0
- package/dist/src/commands/watch/index.js +11 -1
- package/dist/src/core/artifact-name.js +80 -0
- package/dist/src/core/bundle-install.js +55 -12
- package/dist/src/core/bundles.js +72 -13
- package/dist/src/core/context/regenerate.js +21 -4
- package/dist/src/core/context/strategies/codex-agents.js +18 -2
- package/dist/src/core/diagnostics/checks.js +26 -2
- package/dist/src/core/diagnostics/context.js +91 -25
- package/dist/src/core/diagnostics/provider-checks.js +62 -14
- package/dist/src/core/discovery.js +2 -2
- package/dist/src/core/init/mutation-targets.js +11 -0
- package/dist/src/core/init/steps.js +70 -19
- package/dist/src/core/install-planner.js +62 -17
- package/dist/src/core/install-transaction.js +13 -1
- package/dist/src/core/journal/adapter.js +10 -1
- package/dist/src/core/paths.js +61 -8
- package/dist/src/core/profile.js +12 -0
- package/dist/src/core/provider-artifacts.js +38 -13
- package/dist/src/core/provider-version.js +12 -6
- package/dist/src/core/registries.js +40 -19
- package/dist/src/core/renderers/registry.js +37 -0
- package/dist/src/core/skill-integrity.js +71 -10
- package/dist/src/core/update-check.js +14 -2
- package/dist/src/core/versioning.js +17 -2
- package/dist/src/index.js +31 -23
- package/dist/src/providers/index.js +7 -2
- package/dist/src/utils/config.js +7 -8
- package/dist/tests/commands/doctor.test.js +6 -0
- package/dist/tests/commands/pin.test.js +29 -2
- package/dist/tests/commands/preflight/preflight.test.js +31 -17
- package/dist/tests/commands/registry/install-bundles.test.js +6 -6
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +96 -0
- package/dist/tests/commands/sensors/install-settings-safety.test.js +87 -0
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +24 -51
- package/dist/tests/commands/sensors/status-windows.test.js +39 -22
- package/dist/tests/commands/sensors/status.test.js +27 -12
- package/dist/tests/core/artifact-name-containment.test.js +86 -0
- package/dist/tests/core/context/agents-md-single-slot.test.js +69 -0
- package/dist/tests/core/context/regenerate.test.js +39 -8
- package/dist/tests/core/diagnostics/checks.test.js +14 -6
- package/dist/tests/core/diagnostics/provider-tier.test.js +3 -3
- package/dist/tests/core/diagnostics/rendered-artifact-visibility.test.js +69 -0
- package/dist/tests/core/init/all-baseline-bundles.test.js +64 -0
- package/dist/tests/core/init/context-injection-no-project.test.js +117 -0
- package/dist/tests/core/init/orchestrator.test.js +1 -1
- package/dist/tests/core/init/steps-registry-sync.test.js +1 -1
- package/dist/tests/core/init/steps.test.js +11 -5
- package/dist/tests/core/path-resolution-no-shell.test.js +136 -0
- package/dist/tests/core/paths.test.js +7 -34
- package/dist/tests/core/project-skill-links.test.js +159 -0
- package/dist/tests/core/provider-artifacts.test.js +66 -14
- package/dist/tests/core/provider-capability-guards.test.js +92 -0
- package/dist/tests/core/reconciliation.test.js +15 -15
- package/dist/tests/core/registry-manifest.test.js +1 -1
- package/dist/tests/core/semver-fails-closed.test.js +30 -0
- package/dist/tests/core/skill-integrity.test.js +10 -10
- package/dist/tests/core/skill-repair-safety.test.js +92 -0
- package/dist/tests/core/sync-profile-atomicity.test.js +122 -0
- package/dist/tests/integration/copilot-init-isolated.test.js +2 -2
- package/dist/tests/providers/index.test.js +1 -1
- package/dist/tests/structural/renderer-table-is-single-source.test.js +95 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +4 -1
|
@@ -42,24 +42,76 @@ describe('scanLegacyArtifacts', () => {
|
|
|
42
42
|
process.env.AWM_HOME = originalAwmHome;
|
|
43
43
|
fs_1.default.rmSync(tmpHome, { recursive: true, force: true });
|
|
44
44
|
});
|
|
45
|
-
|
|
45
|
+
/** Registra artefactos como propiedad de AWM, que es lo que ahora habilita
|
|
46
|
+
* que `awm remove` los ofrezca. */
|
|
47
|
+
function own(records) {
|
|
48
|
+
const file = path_1.default.join(tmpHome, '.awm', 'state', 'artifacts.json');
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(file, JSON.stringify(records.map((r) => ({
|
|
51
|
+
...r, scope: 'global', sourcePath: '/tmp/src', owners: ['codex'],
|
|
52
|
+
}))));
|
|
53
|
+
}
|
|
54
|
+
it('lista los artefactos renderizados que AWM instalo, incluidos los .toml de codex', () => {
|
|
55
|
+
// Este test asertaba lo contrario: que los agents TOML de codex NUNCA
|
|
56
|
+
// se listaran. Eso codificaba el bug — `scanLegacyArtifacts` se tragaba
|
|
57
|
+
// `UnsupportedRendererError`, asi que `awm remove` era un no-op
|
|
58
|
+
// permanente para los skills de cursor/copilot y los agents de codex
|
|
59
|
+
// ("no hay artefactos instalados", con el directorio lleno).
|
|
60
|
+
// Un artefacto que AWM instalo tiene que poder desinstalarse.
|
|
46
61
|
const skillPath = path_1.default.join(tmpHome, '.agents/skills/linked-skill');
|
|
47
|
-
const agentPath = path_1.default.join(tmpHome, '.codex/agents/
|
|
62
|
+
const agentPath = path_1.default.join(tmpHome, '.codex/agents/rendered.toml');
|
|
48
63
|
fs_1.default.mkdirSync(skillPath, { recursive: true });
|
|
49
64
|
fs_1.default.mkdirSync(path_1.default.dirname(agentPath), { recursive: true });
|
|
50
|
-
fs_1.default.writeFileSync(agentPath, 'name = "
|
|
65
|
+
fs_1.default.writeFileSync(agentPath, 'name = "rendered"\n');
|
|
66
|
+
own([
|
|
67
|
+
{ name: 'linked-skill', type: 'skill', targetPath: skillPath, renderer: 'link' },
|
|
68
|
+
{ name: 'rendered.toml', type: 'agent', targetPath: agentPath, renderer: 'codex-agent-toml' },
|
|
69
|
+
]);
|
|
51
70
|
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['codex'], 'global');
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
expect(listed.map((a) => a.name).sort()).toEqual(['linked-skill', 'rendered.toml']);
|
|
72
|
+
});
|
|
73
|
+
it('NO lista archivos que AWM no instalo — el directorio no es suyo para ofrecerlo', () => {
|
|
74
|
+
// Antes listaba el directorio entero, asi que un skill escrito a mano
|
|
75
|
+
// por el usuario aparecia en el menu de borrado.
|
|
76
|
+
const mine = path_1.default.join(tmpHome, '.agents/skills/awm-skill');
|
|
77
|
+
const theirs = path_1.default.join(tmpHome, '.agents/skills/mi-skill-personal');
|
|
78
|
+
fs_1.default.mkdirSync(mine, { recursive: true });
|
|
79
|
+
fs_1.default.mkdirSync(theirs, { recursive: true });
|
|
80
|
+
own([{ name: 'awm-skill', type: 'skill', targetPath: mine, renderer: 'link' }]);
|
|
81
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['codex'], 'global');
|
|
82
|
+
expect(listed.map((a) => a.name)).toEqual(['awm-skill']);
|
|
83
|
+
expect(listed.flatMap((a) => a.fullPaths)).not.toContain(theirs);
|
|
84
|
+
});
|
|
85
|
+
it('no colapsa dos tipos distintos que comparten nombre (borraria el equivocado)', () => {
|
|
86
|
+
// Antes la clave era solo el nombre, a traves de todos los tipos: un
|
|
87
|
+
// workflow `deploy.md` y un agent `deploy.md` colapsaban en UNA entrada,
|
|
88
|
+
// asi que elegir "deploy.md" borraba ambos.
|
|
89
|
+
const wf = path_1.default.join(tmpHome, '.gemini/antigravity/global_workflows/deploy.md');
|
|
90
|
+
const ag = path_1.default.join(tmpHome, '.claude/agents/deploy.md');
|
|
91
|
+
fs_1.default.mkdirSync(path_1.default.dirname(wf), { recursive: true });
|
|
92
|
+
fs_1.default.mkdirSync(path_1.default.dirname(ag), { recursive: true });
|
|
93
|
+
fs_1.default.writeFileSync(wf, 'wf');
|
|
94
|
+
fs_1.default.writeFileSync(ag, 'ag');
|
|
95
|
+
own([
|
|
96
|
+
{ name: 'deploy.md', type: 'workflow', targetPath: wf, renderer: 'link' },
|
|
97
|
+
{ name: 'deploy.md', type: 'agent', targetPath: ag, renderer: 'link' },
|
|
98
|
+
]);
|
|
99
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['antigravity', 'claude-code'], 'global');
|
|
100
|
+
expect(listed).toHaveLength(2);
|
|
101
|
+
for (const a of listed)
|
|
102
|
+
expect(a.fullPaths).toHaveLength(1);
|
|
103
|
+
});
|
|
104
|
+
it('scope local se resuelve contra el projectRoot dado, no contra cwd', () => {
|
|
105
|
+
const projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-proj-'));
|
|
106
|
+
const local = path_1.default.join(projectRoot, '.claude/skills/local-skill');
|
|
107
|
+
fs_1.default.mkdirSync(local, { recursive: true });
|
|
108
|
+
own([{ name: 'local-skill', type: 'skill', targetPath: local, renderer: 'link' }]);
|
|
109
|
+
const listed = (0, provider_artifacts_1.scanLegacyArtifacts)(['claude-code'], 'local', projectRoot);
|
|
110
|
+
expect(listed.map((a) => a.name)).toEqual(['local-skill']);
|
|
111
|
+
// Y la ruta entregada es ABSOLUTA: antes era relativa, y se la pasaba
|
|
112
|
+
// tal cual a fs.rmSync.
|
|
113
|
+
expect(path_1.default.isAbsolute(listed[0].fullPaths[0])).toBe(true);
|
|
114
|
+
fs_1.default.rmSync(projectRoot, { recursive: true, force: true });
|
|
63
115
|
});
|
|
64
116
|
it('Gap C — an agent with a null global skill dir (copilot) at global scope skips cleanly instead of crashing', () => {
|
|
65
117
|
// Copilot's skill.global is null (no user-level skill discovery mechanism —
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
// Regresion: un provider que estructuralmente NO puede recibir un artefacto en
|
|
7
|
+
// cierto scope (hoy: copilot en global — no tiene descubrimiento de skills a
|
|
8
|
+
// nivel usuario) hacia TIRAR el plan entero, rompiendo el comando para todos
|
|
9
|
+
// los demas agentes de la maquina.
|
|
10
|
+
//
|
|
11
|
+
// Sintoma confirmado contra el binario real:
|
|
12
|
+
// awm init -a claude-code -y && awm init -a copilot -y && awm update
|
|
13
|
+
// → Artifact reconciliation failed: skill global scope is not supported by Copilot
|
|
14
|
+
// exit=1
|
|
15
|
+
// El comando moria ANTES del re-sync de hooks, asi que claude-code dejaba de
|
|
16
|
+
// recibir actualizaciones — y no existe comando para deshabilitar un agente,
|
|
17
|
+
// mientras `awm doctor` seguia diciendo "healthy".
|
|
18
|
+
//
|
|
19
|
+
// La causa era una asimetria de guarda clasica: el `.filter()` interno de
|
|
20
|
+
// `assertCompleteSharedGroup` ya toleraba el caso, pero la linea de arriba
|
|
21
|
+
// —que corre primero— no.
|
|
22
|
+
const path_1 = __importDefault(require("path"));
|
|
23
|
+
const install_planner_1 = require("../../src/core/install-planner");
|
|
24
|
+
const skill = {
|
|
25
|
+
name: 'using-awm', type: 'skill', installName: 'using-awm', sourcePath: '/tmp/reg/skills/using-awm',
|
|
26
|
+
};
|
|
27
|
+
describe('planInstall: un provider incapaz no puede romper el plan de los demas', () => {
|
|
28
|
+
it('scope global con copilot habilitado: planifica para los capaces y saltea copilot', () => {
|
|
29
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
30
|
+
artifacts: [skill],
|
|
31
|
+
selectedAgents: ['claude-code', 'copilot'],
|
|
32
|
+
enabledAgents: ['claude-code', 'copilot'],
|
|
33
|
+
scope: 'global',
|
|
34
|
+
projectRoot: '/tmp/proj',
|
|
35
|
+
method: 'symlink',
|
|
36
|
+
});
|
|
37
|
+
const owners = plan.operations.flatMap((op) => op.owners);
|
|
38
|
+
expect(owners).toContain('claude-code');
|
|
39
|
+
expect(owners).not.toContain('copilot');
|
|
40
|
+
});
|
|
41
|
+
it('no tira aunque copilot este habilitado pero no seleccionado (el caso de awm update)', () => {
|
|
42
|
+
expect(() => (0, install_planner_1.planInstall)({
|
|
43
|
+
artifacts: [skill],
|
|
44
|
+
selectedAgents: ['claude-code'],
|
|
45
|
+
enabledAgents: ['claude-code', 'copilot'],
|
|
46
|
+
scope: 'global',
|
|
47
|
+
projectRoot: '/tmp/proj',
|
|
48
|
+
method: 'symlink',
|
|
49
|
+
})).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
it('peticion EXPLICITA de copilot en global: sigue tirando CON la explicacion', () => {
|
|
52
|
+
// Aca no hay a quien saltear — el usuario pidio justo esa combinacion, y
|
|
53
|
+
// lo que necesita es leer por que no se puede, no un exito silencioso.
|
|
54
|
+
expect(() => (0, install_planner_1.planInstall)({
|
|
55
|
+
artifacts: [skill],
|
|
56
|
+
selectedAgents: ['copilot'],
|
|
57
|
+
enabledAgents: ['copilot'],
|
|
58
|
+
scope: 'global',
|
|
59
|
+
projectRoot: '/tmp/proj',
|
|
60
|
+
method: 'symlink',
|
|
61
|
+
})).toThrow(/per-project|not supported/i);
|
|
62
|
+
});
|
|
63
|
+
it('copilot en scope local SI recibe el artefacto (su unico scope valido)', () => {
|
|
64
|
+
const plan = (0, install_planner_1.planInstall)({
|
|
65
|
+
artifacts: [skill],
|
|
66
|
+
selectedAgents: ['copilot'],
|
|
67
|
+
enabledAgents: ['copilot'],
|
|
68
|
+
scope: 'local',
|
|
69
|
+
projectRoot: '/tmp/proj',
|
|
70
|
+
method: 'symlink',
|
|
71
|
+
});
|
|
72
|
+
expect(plan.operations).toHaveLength(1);
|
|
73
|
+
// `path.join`, no un literal con '/': `physicalTarget` construye la ruta con el
|
|
74
|
+
// separador de la plataforma, asi que en Windows el destino real es
|
|
75
|
+
// `.github\instructions` y la asercion POSIX-hardcodeada fallaba sobre un
|
|
76
|
+
// destino perfectamente correcto.
|
|
77
|
+
expect(plan.operations[0].targetPath).toContain(path_1.default.join('.github', 'instructions'));
|
|
78
|
+
expect(plan.operations[0].targetPath).toContain('using-awm.instructions.md');
|
|
79
|
+
});
|
|
80
|
+
it('el grupo compartido (opencode + codex) se sigue exigiendo completo', () => {
|
|
81
|
+
// La guarda no debe aflojar R14: ambos resuelven a ~/.agents/skills, asi
|
|
82
|
+
// que seleccionar solo uno seguiria siendo un cambio divergente.
|
|
83
|
+
expect(() => (0, install_planner_1.planInstall)({
|
|
84
|
+
artifacts: [skill],
|
|
85
|
+
selectedAgents: ['codex'],
|
|
86
|
+
enabledAgents: ['codex', 'opencode'],
|
|
87
|
+
scope: 'global',
|
|
88
|
+
projectRoot: '/tmp/proj',
|
|
89
|
+
method: 'symlink',
|
|
90
|
+
})).toThrow(/shared skill target/i);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -138,10 +138,18 @@ describe('planReconciliation (real, unmocked)', () => {
|
|
|
138
138
|
// points nowhere — resolveBundleClosure just drops the missing dep.
|
|
139
139
|
expect(names).toEqual(['ambient-skill', 'brainstorming', 'dangling-skill']);
|
|
140
140
|
});
|
|
141
|
-
it('
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
141
|
+
it('un dependsOn malformado ya no rompe nada: el campo invalido se ignora y el bundle instala', () => {
|
|
142
|
+
// Este test asertaba que un `dependsOn` no-array hacia TIRAR a
|
|
143
|
+
// `resolveBundleClosure` y que reconciliation lo salteaba. Desde que
|
|
144
|
+
// `discoverBundles` valida forma sobre el contenido del registry (que es
|
|
145
|
+
// input no confiable), ese campo se normaliza a `[]` en vez de
|
|
146
|
+
// propagarse hasta un `for...of` sobre un numero. O sea: la entrada ya
|
|
147
|
+
// no falla, y el bundle instala normalmente ignorando el campo roto.
|
|
148
|
+
//
|
|
149
|
+
// Es una mejora, no una regresion — antes UN archivo malformado en
|
|
150
|
+
// cualquier registry tumbaba `awm list`/`awm add` con un stack trace
|
|
151
|
+
// crudo. El `catch` de reconciliation sigue en su lugar como defensa en
|
|
152
|
+
// profundidad, pero esta entrada ya no lo ejercita.
|
|
145
153
|
writeBundle(contentRoot, 'broken', 'baseline', ['broken-skill'], 5);
|
|
146
154
|
addToCatalog(contentRoot, 'broken', 'baseline');
|
|
147
155
|
const { planReconciliation } = require('../../src/core/reconciliation');
|
|
@@ -149,17 +157,9 @@ describe('planReconciliation (real, unmocked)', () => {
|
|
|
149
157
|
expect(() => {
|
|
150
158
|
plan = planReconciliation({ targets: ['claude-code'], roots: [contentRoot] });
|
|
151
159
|
}).not.toThrow();
|
|
152
|
-
// The healthy bundles (baseline 'dev' + ambient 'ambient-x') still
|
|
153
|
-
// reconcile correctly.
|
|
154
160
|
const names = plan.operations.map((op) => op.name).sort();
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
expect(names).not.toContain('broken-skill');
|
|
159
|
-
expect(plan.operations.some((op) => op.sourcePath?.includes('broken'))).toBe(false);
|
|
160
|
-
// Applying the plan end-to-end still works (no dangling reference to
|
|
161
|
-
// the broken bundle leaked into the plan).
|
|
162
|
-
const { applyInstallPlan } = require('../../src/core/install-transaction');
|
|
163
|
-
expect(() => applyInstallPlan(plan)).not.toThrow();
|
|
161
|
+
// Los bundles sanos siguen reconciliando, y el que tenia el campo roto
|
|
162
|
+
// tambien — sin su dependencia invalida.
|
|
163
|
+
expect(names).toEqual(['ambient-skill', 'brainstorming', 'broken-skill']);
|
|
164
164
|
});
|
|
165
165
|
});
|
|
@@ -69,7 +69,7 @@ describe('registry manifest (awm-registry.json)', () => {
|
|
|
69
69
|
});
|
|
70
70
|
it('registryNameForPath maps configured registries and returns null for unknown paths', () => {
|
|
71
71
|
const m = load();
|
|
72
|
-
const regPath = path_1.default.join(m.
|
|
72
|
+
const regPath = path_1.default.join(m.registriesDir(), 'team-acme', 'skills', 'x');
|
|
73
73
|
expect(m.registryNameForPath(regPath)).toBe('team-acme');
|
|
74
74
|
expect(m.registryNameForPath('/somewhere/else')).toBeNull();
|
|
75
75
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// Regresion: `compareSemver` devolvia NaN ante una entrada malformada, y como
|
|
4
|
+
// `NaN > 0` y `NaN < 0` son AMBOS false, el gate de version fallaba ABIERTO:
|
|
5
|
+
// `verifyMinCliVersions` dejaba pasar en silencio un CLI por debajo del minimo.
|
|
6
|
+
// Para un gate, la direccion correcta de fallo es la contraria.
|
|
7
|
+
const versioning_1 = require("../../src/core/versioning");
|
|
8
|
+
describe('compareSemver', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
['1.2.3', '1.2.3', 0],
|
|
11
|
+
['1.2.4', '1.2.3', 1],
|
|
12
|
+
['1.2.3', '1.3.0', -1],
|
|
13
|
+
['2.0.0', '1.99.99', 1],
|
|
14
|
+
['v1.2.3', '1.2.3', 0],
|
|
15
|
+
])('compara %s con %s', (a, b, sign) => {
|
|
16
|
+
expect(Math.sign((0, versioning_1.compareSemver)(a, b))).toBe(sign);
|
|
17
|
+
});
|
|
18
|
+
it.each([
|
|
19
|
+
['incompleta', '1.2'],
|
|
20
|
+
['no numerica', 'x.y.z'],
|
|
21
|
+
['vacia', ''],
|
|
22
|
+
['basura', 'latest'],
|
|
23
|
+
])('lanza ante una version %s en vez de devolver NaN', (_n, bad) => {
|
|
24
|
+
expect(() => (0, versioning_1.compareSemver)(bad, '1.0.0')).toThrow(/not a semver/i);
|
|
25
|
+
expect(() => (0, versioning_1.compareSemver)('1.0.0', bad)).toThrow(/not a semver/i);
|
|
26
|
+
});
|
|
27
|
+
it('nunca devuelve NaN — el valor que hacia fallar el gate abierto', () => {
|
|
28
|
+
expect(Number.isNaN((0, versioning_1.compareSemver)('1.0.0', '2.0.0'))).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -16,7 +16,7 @@ describe('skill-integrity multi-root', () => {
|
|
|
16
16
|
afterEach(() => {
|
|
17
17
|
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
18
18
|
});
|
|
19
|
-
it('
|
|
19
|
+
it('repairSkillLinks re-links a dangling symlink to the FIRST root that has the skill', () => {
|
|
20
20
|
const skillsDir = path_1.default.join(tmp, 'installed');
|
|
21
21
|
const rootA = path_1.default.join(tmp, 'base');
|
|
22
22
|
const rootB = path_1.default.join(tmp, 'personal');
|
|
@@ -25,16 +25,16 @@ describe('skill-integrity multi-root', () => {
|
|
|
25
25
|
fs_1.default.writeFileSync(path_1.default.join(rootB, 'skills', 'mine', 'SKILL.md'), '---\nname: mine\n---\n');
|
|
26
26
|
// dangling symlink: points to a deleted target
|
|
27
27
|
fs_1.default.symlinkSync(path_1.default.join(tmp, 'gone', 'mine'), path_1.default.join(skillsDir, 'mine'), 'dir');
|
|
28
|
-
const {
|
|
28
|
+
const { repairSkillLinks: repair } = require('../../src/core/skill-integrity');
|
|
29
29
|
const r = repair(skillsDir, [rootA, rootB]);
|
|
30
30
|
expect(r.relinked).toEqual(['mine']);
|
|
31
31
|
expect(fs_1.default.realpathSync(path_1.default.join(skillsDir, 'mine'))).toBe(fs_1.default.realpathSync(path_1.default.join(rootB, 'skills', 'mine')));
|
|
32
32
|
});
|
|
33
|
-
it('
|
|
33
|
+
it('classifySkillLinks marks dead when NO root has the skill', () => {
|
|
34
34
|
const skillsDir = path_1.default.join(tmp, 'installed');
|
|
35
35
|
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
36
36
|
fs_1.default.symlinkSync(path_1.default.join(tmp, 'gone', 'nope'), path_1.default.join(skillsDir, 'nope'), 'dir');
|
|
37
|
-
const {
|
|
37
|
+
const { classifySkillLinks: classify } = require('../../src/core/skill-integrity');
|
|
38
38
|
const c = classify(skillsDir, [path_1.default.join(tmp, 'base')]);
|
|
39
39
|
expect(c.dead).toEqual(['nope']);
|
|
40
40
|
});
|
|
@@ -53,7 +53,7 @@ function makeRegistrySkill(registryContentDir, name) {
|
|
|
53
53
|
fs_1.default.writeFileSync(path_1.default.join(dir, 'SKILL.md'), `# ${name}\n`);
|
|
54
54
|
return dir;
|
|
55
55
|
}
|
|
56
|
-
describe('
|
|
56
|
+
describe('classifySkillLinks', () => {
|
|
57
57
|
it('classifies valid / repairable / dead', () => {
|
|
58
58
|
const { skillsDir, registryContentDir } = setup();
|
|
59
59
|
// valid: symlink a una skill que existe en el registry
|
|
@@ -64,13 +64,13 @@ describe('classifyGlobalSkills', () => {
|
|
|
64
64
|
fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/beta'), path_1.default.join(skillsDir, 'beta'), 'dir');
|
|
65
65
|
// dead: symlink colgante y la skill NO existe en el registry
|
|
66
66
|
fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/gamma'), path_1.default.join(skillsDir, 'gamma'), 'dir');
|
|
67
|
-
const result = (0, skill_integrity_1.
|
|
67
|
+
const result = (0, skill_integrity_1.classifySkillLinks)(skillsDir, [registryContentDir]);
|
|
68
68
|
expect(result.valid).toEqual(['alpha']);
|
|
69
69
|
expect(result.repairable).toEqual(['beta']);
|
|
70
70
|
expect(result.dead).toEqual(['gamma']);
|
|
71
71
|
});
|
|
72
72
|
it('returns empty arrays when the skills dir does not exist', () => {
|
|
73
|
-
const result = (0, skill_integrity_1.
|
|
73
|
+
const result = (0, skill_integrity_1.classifySkillLinks)('/nonexistent/dir', ['/also/nonexistent']);
|
|
74
74
|
expect(result).toEqual({ valid: [], repairable: [], dead: [] });
|
|
75
75
|
});
|
|
76
76
|
});
|
|
@@ -117,7 +117,7 @@ describe('reconcileAllSkillLinks (#4 — awm update, all providers)', () => {
|
|
|
117
117
|
}
|
|
118
118
|
});
|
|
119
119
|
});
|
|
120
|
-
describe('
|
|
120
|
+
describe('repairSkillLinks', () => {
|
|
121
121
|
it('re-links repairable to registry content dir and prunes dead; valid untouched; idempotent', () => {
|
|
122
122
|
const { skillsDir, registryContentDir } = setup();
|
|
123
123
|
const okTarget = makeRegistrySkill(registryContentDir, 'alpha');
|
|
@@ -125,7 +125,7 @@ describe('repairGlobalSkills', () => {
|
|
|
125
125
|
makeRegistrySkill(registryContentDir, 'beta');
|
|
126
126
|
fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/beta'), path_1.default.join(skillsDir, 'beta'), 'dir');
|
|
127
127
|
fs_1.default.symlinkSync(path_1.default.join('/nonexistent/old-root/gamma'), path_1.default.join(skillsDir, 'gamma'), 'dir');
|
|
128
|
-
const r1 = (0, skill_integrity_1.
|
|
128
|
+
const r1 = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registryContentDir]);
|
|
129
129
|
expect(r1.relinked).toEqual(['beta']);
|
|
130
130
|
expect(r1.pruned).toEqual(['gamma']);
|
|
131
131
|
// beta ahora apunta al registry content dir y resuelve
|
|
@@ -137,7 +137,7 @@ describe('repairGlobalSkills', () => {
|
|
|
137
137
|
// alpha intacto
|
|
138
138
|
expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'alpha'))).toBe(true);
|
|
139
139
|
// idempotente: segunda corrida no cambia nada
|
|
140
|
-
const r2 = (0, skill_integrity_1.
|
|
140
|
+
const r2 = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registryContentDir]);
|
|
141
141
|
expect(r2.relinked).toEqual([]);
|
|
142
142
|
expect(r2.pruned).toEqual([]);
|
|
143
143
|
});
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
// Regresion: `repairSkillLinks` hacia `rmSync` del link colgante y RECIEN
|
|
7
|
+
// DESPUES intentaba recrearlo con `symlinkSync(..., 'dir')`. En Windows un
|
|
8
|
+
// symlink de directorio exige SeCreateSymbolicLinkPrivilege, denegado por
|
|
9
|
+
// defecto en cuentas sin privilegios — asi que el link se borraba y la
|
|
10
|
+
// recreacion tiraba EPERM: el usuario quedaba PEOR que antes, y con la entrada
|
|
11
|
+
// ya ausente el siguiente `awm init` ni siquiera podia verla como reparable.
|
|
12
|
+
//
|
|
13
|
+
// `executor.ts` documenta en extenso que en win32 hay que usar `'junction'`
|
|
14
|
+
// (que cualquier cuenta puede crear, y que libuv reporta igual que un symlink);
|
|
15
|
+
// este sitio nunca recibio ese fix.
|
|
16
|
+
const fs_1 = __importDefault(require("fs"));
|
|
17
|
+
const os_1 = __importDefault(require("os"));
|
|
18
|
+
const path_1 = __importDefault(require("path"));
|
|
19
|
+
const skill_integrity_1 = require("../../src/core/skill-integrity");
|
|
20
|
+
describe('repairSkillLinks: un fallo nunca deja al usuario peor que antes', () => {
|
|
21
|
+
let skillsDir;
|
|
22
|
+
let registry;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
const base = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-repair-'));
|
|
25
|
+
skillsDir = path_1.default.join(base, 'skills-installed');
|
|
26
|
+
registry = path_1.default.join(base, 'registry');
|
|
27
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
28
|
+
fs_1.default.mkdirSync(path_1.default.join(registry, 'skills', 'alpha'), { recursive: true });
|
|
29
|
+
fs_1.default.writeFileSync(path_1.default.join(registry, 'skills', 'alpha', 'SKILL.md'), '---\nname: alpha\ndescription: d\n---\nb');
|
|
30
|
+
});
|
|
31
|
+
/** Un symlink colgante: apunta a algo que no existe, pero el registry SI
|
|
32
|
+
* tiene la skill — o sea, reparable. */
|
|
33
|
+
function danglingLink(name) {
|
|
34
|
+
fs_1.default.symlinkSync(path_1.default.join(skillsDir, '__no_existe__'), path_1.default.join(skillsDir, name), 'dir');
|
|
35
|
+
}
|
|
36
|
+
it('repara un link colgante apuntandolo al registry', () => {
|
|
37
|
+
danglingLink('alpha');
|
|
38
|
+
const result = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
|
|
39
|
+
expect(result.relinked).toContain('alpha');
|
|
40
|
+
expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'alpha'))).toBe(true);
|
|
41
|
+
expect(fs_1.default.realpathSync(path_1.default.join(skillsDir, 'alpha')))
|
|
42
|
+
.toBe(fs_1.default.realpathSync(path_1.default.join(registry, 'skills', 'alpha')));
|
|
43
|
+
});
|
|
44
|
+
it('si la creacion del link falla, el estado original NO se destruye', () => {
|
|
45
|
+
danglingLink('alpha');
|
|
46
|
+
const before = fs_1.default.readlinkSync(path_1.default.join(skillsDir, 'alpha'));
|
|
47
|
+
// Simula la denegacion de privilegio de Windows.
|
|
48
|
+
const spy = jest.spyOn(fs_1.default, 'symlinkSync').mockImplementation(() => {
|
|
49
|
+
const err = new Error('operation not permitted');
|
|
50
|
+
err.code = 'EPERM';
|
|
51
|
+
throw err;
|
|
52
|
+
});
|
|
53
|
+
try {
|
|
54
|
+
const result = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
|
|
55
|
+
expect(result.failed).toContain('alpha');
|
|
56
|
+
expect(result.relinked).not.toContain('alpha');
|
|
57
|
+
// Lo que importa: la entrada sigue existiendo, con su target
|
|
58
|
+
// original. Antes quedaba borrada — invisible incluso para el
|
|
59
|
+
// proximo intento de reparacion.
|
|
60
|
+
expect(fs_1.default.lstatSync(path_1.default.join(skillsDir, 'alpha')).isSymbolicLink()).toBe(true);
|
|
61
|
+
expect(fs_1.default.readlinkSync(path_1.default.join(skillsDir, 'alpha'))).toBe(before);
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
spy.mockRestore();
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
it('no deja archivos de staging tirados tras una reparacion exitosa', () => {
|
|
68
|
+
danglingLink('alpha');
|
|
69
|
+
(0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
|
|
70
|
+
expect(fs_1.default.readdirSync(skillsDir).filter((e) => e.includes('.relink'))).toEqual([]);
|
|
71
|
+
});
|
|
72
|
+
it('no deja archivos de staging tirados tras un fallo', () => {
|
|
73
|
+
danglingLink('alpha');
|
|
74
|
+
const spy = jest.spyOn(fs_1.default, 'renameSync').mockImplementation(() => { throw new Error('boom'); });
|
|
75
|
+
try {
|
|
76
|
+
(0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
spy.mockRestore();
|
|
80
|
+
}
|
|
81
|
+
// El staging puede sobrevivir a un crash duro, pero se limpia en el
|
|
82
|
+
// siguiente intento — se verifica que una segunda pasada converge.
|
|
83
|
+
(0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
|
|
84
|
+
expect(fs_1.default.readdirSync(skillsDir).filter((e) => e.includes('.relink'))).toEqual([]);
|
|
85
|
+
});
|
|
86
|
+
it('poda un link muerto (sin contraparte en el registry)', () => {
|
|
87
|
+
danglingLink('fantasma');
|
|
88
|
+
const result = (0, skill_integrity_1.repairSkillLinks)(skillsDir, [registry]);
|
|
89
|
+
expect(result.pruned).toContain('fantasma');
|
|
90
|
+
expect(fs_1.default.existsSync(path_1.default.join(skillsDir, 'fantasma'))).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -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
|
+
// `awm sync` over a profile with N extensions ran N independent transactions —
|
|
7
|
+
// one `planInstall` + `applyInstallPlan` per extension, in a loop. Each is atomic on
|
|
8
|
+
// its own, none is atomic with the others, so a failure on extension 3 left 1 and 2
|
|
9
|
+
// installed and the tree in a state that matched neither before nor after:
|
|
10
|
+
//
|
|
11
|
+
// extensions: [a, b, c]
|
|
12
|
+
// a → tx-1 committed
|
|
13
|
+
// b → tx-2 committed
|
|
14
|
+
// c → throws ← a and b stay installed, sync exits non-zero
|
|
15
|
+
//
|
|
16
|
+
// And the transaction IDs — the only handle `awm backup restore` accepts — were
|
|
17
|
+
// collected into `transactionIds` and then never printed by any caller, so even the
|
|
18
|
+
// committed halves could not be named to undo them by hand.
|
|
19
|
+
//
|
|
20
|
+
// One plan for the whole sync. `planInstall` already dedups by physical target across
|
|
21
|
+
// artifacts, so combining the extensions loses nothing; and `applyInstallPlan` already
|
|
22
|
+
// rolls its own plan back on any failure, so combining is what makes the sync atomic.
|
|
23
|
+
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const os_1 = __importDefault(require("os"));
|
|
25
|
+
const path_1 = __importDefault(require("path"));
|
|
26
|
+
const bundle_install_1 = require("../../src/core/bundle-install");
|
|
27
|
+
function bundle(name, skills, contentRoot) {
|
|
28
|
+
return {
|
|
29
|
+
name, description: '', version: '1.0.0', scope: 'project', visibility: 'public',
|
|
30
|
+
dependsOn: [], skills: skills.map(n => ({ name: n })), workflows: [], agents: [],
|
|
31
|
+
source: `bundles/${name}`, contentRoot,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
describe('awm sync is one transaction, not one per extension', () => {
|
|
35
|
+
let projectRoot;
|
|
36
|
+
let contentDir;
|
|
37
|
+
let home;
|
|
38
|
+
let saved;
|
|
39
|
+
let bundles;
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-atomic-home-'));
|
|
42
|
+
saved = { HOME: process.env.HOME, AWM_HOME: process.env.AWM_HOME };
|
|
43
|
+
process.env.HOME = home;
|
|
44
|
+
process.env.AWM_HOME = path_1.default.join(home, '.awm');
|
|
45
|
+
fs_1.default.mkdirSync(path_1.default.join(home, '.awm'), { recursive: true });
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(home, '.awm', 'preferences.json'), JSON.stringify({
|
|
47
|
+
enabledAgents: ['claude-code'], defaultAgent: 'claude-code',
|
|
48
|
+
installMethod: 'symlink', defaultScope: 'global',
|
|
49
|
+
}));
|
|
50
|
+
contentDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-atomic-content-'));
|
|
51
|
+
for (const s of ['skill-a', 'skill-b', 'skill-c']) {
|
|
52
|
+
fs_1.default.mkdirSync(path_1.default.join(contentDir, 'skills', s), { recursive: true });
|
|
53
|
+
fs_1.default.writeFileSync(path_1.default.join(contentDir, 'skills', s, 'SKILL.md'), `---\nname: ${s}\ndescription: d\n---\n`);
|
|
54
|
+
}
|
|
55
|
+
bundles = [
|
|
56
|
+
bundle('ext-a', ['skill-a'], contentDir),
|
|
57
|
+
bundle('ext-b', ['skill-b'], contentDir),
|
|
58
|
+
bundle('ext-c', ['skill-c'], contentDir),
|
|
59
|
+
];
|
|
60
|
+
projectRoot = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-atomic-proj-'));
|
|
61
|
+
fs_1.default.mkdirSync(path_1.default.join(projectRoot, '.awm'), { recursive: true });
|
|
62
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['ext-a', 'ext-b', 'ext-c'] }));
|
|
63
|
+
});
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
if (saved.HOME === undefined)
|
|
66
|
+
delete process.env.HOME;
|
|
67
|
+
else
|
|
68
|
+
process.env.HOME = saved.HOME;
|
|
69
|
+
if (saved.AWM_HOME === undefined)
|
|
70
|
+
delete process.env.AWM_HOME;
|
|
71
|
+
else
|
|
72
|
+
process.env.AWM_HOME = saved.AWM_HOME;
|
|
73
|
+
for (const d of [home, contentDir, projectRoot])
|
|
74
|
+
fs_1.default.rmSync(d, { recursive: true, force: true });
|
|
75
|
+
});
|
|
76
|
+
const run = (applyPlan) => (0, bundle_install_1.syncProfile)({ projectRoot, bundles, agents: ['claude-code'], method: 'symlink', contentDir, applyPlan });
|
|
77
|
+
it('applies a single plan covering every extension', () => {
|
|
78
|
+
const plans = [];
|
|
79
|
+
const result = run((plan) => {
|
|
80
|
+
plans.push(plan);
|
|
81
|
+
return { installed: plan.operations.map(o => o.targetPath), skipped: [], transactionId: 'tx-1', modifiedFiles: [] };
|
|
82
|
+
});
|
|
83
|
+
expect(plans).toHaveLength(1);
|
|
84
|
+
const names = plans[0].operations.map(o => path_1.default.basename(o.targetPath)).sort();
|
|
85
|
+
expect(names).toEqual(['skill-a', 'skill-b', 'skill-c']);
|
|
86
|
+
expect(result.transactionIds).toEqual(['tx-1']);
|
|
87
|
+
});
|
|
88
|
+
it('installs nothing at all when the apply fails — no half-synced tree', () => {
|
|
89
|
+
// The failure is on the LAST extension on purpose: with one transaction per
|
|
90
|
+
// extension, ext-a and ext-b were already committed by the time ext-c blew up.
|
|
91
|
+
const committed = [];
|
|
92
|
+
expect(() => run((plan) => {
|
|
93
|
+
const names = plan.operations.map(o => path_1.default.basename(o.targetPath));
|
|
94
|
+
if (names.includes('skill-c'))
|
|
95
|
+
throw new Error('verification failed');
|
|
96
|
+
committed.push(...names);
|
|
97
|
+
return { installed: names, skipped: [], transactionId: `tx-${committed.length}`, modifiedFiles: [] };
|
|
98
|
+
})).toThrow(/verification failed/);
|
|
99
|
+
expect(committed).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
it('skips unknown extensions without splitting the transaction', () => {
|
|
102
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['ext-a', 'ghost', 'ext-c'] }));
|
|
103
|
+
const plans = [];
|
|
104
|
+
const result = run((plan) => {
|
|
105
|
+
plans.push(plan);
|
|
106
|
+
return { installed: [], skipped: [], transactionId: 'tx-1', modifiedFiles: [] };
|
|
107
|
+
});
|
|
108
|
+
expect(plans).toHaveLength(1);
|
|
109
|
+
expect(plans[0].operations.map(o => path_1.default.basename(o.targetPath)).sort()).toEqual(['skill-a', 'skill-c']);
|
|
110
|
+
expect(result.skipped.some(s => s.includes('ghost'))).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it('does not apply anything when every extension is unknown', () => {
|
|
113
|
+
fs_1.default.writeFileSync(path_1.default.join(projectRoot, '.awm', 'profile.json'), JSON.stringify({ extensions: ['ghost'] }));
|
|
114
|
+
let applyCalls = 0;
|
|
115
|
+
const result = run(() => {
|
|
116
|
+
applyCalls++;
|
|
117
|
+
return { installed: [], skipped: [], transactionId: 'tx-1', modifiedFiles: [] };
|
|
118
|
+
});
|
|
119
|
+
expect(applyCalls).toBe(0);
|
|
120
|
+
expect(result.transactionIds).toEqual([]);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -75,9 +75,9 @@ describe('copilot provider — isolated home E2E (devCore global-scope guard reg
|
|
|
75
75
|
fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/session-start'), '#!/bin/sh\necho "{}"\n', { mode: 0o755 });
|
|
76
76
|
fs_1.default.writeFileSync(path_1.default.join(root, 'hooks/run-hook.cmd'), '#!/bin/sh\nexec sh "$1"\n', { mode: 0o755 });
|
|
77
77
|
fs_1.default.mkdirSync(path_1.default.join(root, 'skills/using-awm'), { recursive: true });
|
|
78
|
-
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\n---\nMUST invoke skills.');
|
|
78
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/using-awm/SKILL.md'), '---\nname: using-awm\ndescription: Entry point for AWM skills.\n---\nMUST invoke skills.');
|
|
79
79
|
fs_1.default.mkdirSync(path_1.default.join(root, 'skills/development-process'), { recursive: true });
|
|
80
|
-
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/development-process/SKILL.md'), '---\nname: development-process\n---\nOrchestrates the dev lifecycle.');
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'skills/development-process/SKILL.md'), '---\nname: development-process\ndescription: Orchestrates the development lifecycle.\n---\nOrchestrates the dev lifecycle.');
|
|
81
81
|
fs_1.default.writeFileSync(path_1.default.join(root, 'catalog.json'), JSON.stringify({
|
|
82
82
|
version: 1,
|
|
83
83
|
bundles: [{ name: 'dev-core', source: 'bundles/dev-core', version: '1.0.0', scope: 'baseline', visibility: 'public' }],
|
|
@@ -56,7 +56,7 @@ describe('Providers Routing', () => {
|
|
|
56
56
|
expect((0, providers_1.providerFor)('codex')).toEqual({
|
|
57
57
|
label: 'Codex',
|
|
58
58
|
minimumVersion: '0.145.0',
|
|
59
|
-
versionCommand: { command: 'codex', args: ['--version'] },
|
|
59
|
+
versionCommand: { command: 'codex', args: ['--version'], versionPattern: /^codex-cli (\d+\.\d+\.\d+)$/ },
|
|
60
60
|
skill: {
|
|
61
61
|
global: path_1.default.join(tmpHome, '.agents/skills'),
|
|
62
62
|
local: '.agents/skills',
|