agentic-workflow-manager 3.13.5 → 3.13.7
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/scripts/support-matrix.js +138 -0
- 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 +34 -1
- 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/commands/watch/runner.test.js +10 -0
- 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/mutation-targets-canonical.test.js +81 -0
- package/dist/tests/core/init/mutation-targets.test.js +4 -4
- 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/support-matrix-is-current.test.js +100 -0
- package/dist/tests/structural/symlink-type-is-explicit.test.js +73 -0
- package/dist/tests/support/tmp.js +31 -0
- package/dist/tests/utils/registry-view-overrides.test.js +2 -2
- package/package.json +6 -2
|
@@ -23,6 +23,7 @@ exports.planRemoval = planRemoval;
|
|
|
23
23
|
// persistence (artifact-state.ts). This module only computes the plan.
|
|
24
24
|
const path_1 = __importDefault(require("path"));
|
|
25
25
|
const providers_1 = require("../providers");
|
|
26
|
+
const registry_1 = require("./renderers/registry");
|
|
26
27
|
/** Resolves the single physical filesystem location an intent renders to for one agent. */
|
|
27
28
|
/**
|
|
28
29
|
* Resolves the physical target path + renderer for one artifact intent on one
|
|
@@ -44,21 +45,27 @@ function physicalTarget(intent, agent, scope, projectRoot) {
|
|
|
44
45
|
// trailing `.md` (if any) on intent.installName, so e.g. `using-awm`
|
|
45
46
|
// (skills carry no extension) or `using-awm.md` both become
|
|
46
47
|
// `using-awm.instructions.md`, never `using-awm.md.instructions.md`.
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
48
|
+
// `renderedFilename` (core/renderers/registry.ts) es la unica fuente de
|
|
49
|
+
// verdad de este mapeo. Antes vivia inline aca y re-derivado a mano en tres
|
|
50
|
+
// lectores mas, uno de los cuales lo omitia por completo — y ese era
|
|
51
|
+
// justamente el que decidia si un artefacto estaba instalado.
|
|
52
|
+
const filename = (0, registry_1.renderedFilename)(intent.installName, config.renderer);
|
|
53
|
+
const targetPath = path_1.default.join(dir, filename);
|
|
54
|
+
// Asercion de contencion: el destino resuelto DEBE caer dentro del
|
|
55
|
+
// directorio del provider. `dir` es el ancla de confianza real (sale de la
|
|
56
|
+
// config del provider, no del contenido), asi que verificarlo aca atrapa
|
|
57
|
+
// cualquier camino — presente o futuro — que construya un `installName` sin
|
|
58
|
+
// pasar por `assertSafeArtifactName`. Los nombres llegan desde el
|
|
59
|
+
// `bundle.json` de un registry, y el instalador borra el destino de forma
|
|
60
|
+
// recursiva antes de enlazar: un escape aca no es un archivo mal ubicado,
|
|
61
|
+
// es borrado arbitrario fuera del sandbox.
|
|
62
|
+
const resolvedDir = path_1.default.resolve(dir);
|
|
63
|
+
const resolved = path_1.default.resolve(targetPath);
|
|
64
|
+
if (resolved !== resolvedDir && !resolved.startsWith(resolvedDir + path_1.default.sep)) {
|
|
65
|
+
throw new Error(`refusing to install outside the target directory: ${JSON.stringify(intent.installName)} ` +
|
|
66
|
+
`resolves to ${resolved}, which is not inside ${resolvedDir}`);
|
|
67
|
+
}
|
|
68
|
+
return { targetPath, renderer: config.renderer };
|
|
62
69
|
}
|
|
63
70
|
/**
|
|
64
71
|
* The physical directory `agent`'s skill artifacts resolve to at `scope` —
|
|
@@ -121,7 +128,19 @@ function assertCompleteSharedGroup(intent, selected, enabled, scope, projectRoot
|
|
|
121
128
|
if (intent.type !== 'skill')
|
|
122
129
|
return;
|
|
123
130
|
for (const agent of selected) {
|
|
124
|
-
|
|
131
|
+
// El `.filter()` de abajo ya tenia esta guarda; esta linea no — y es la
|
|
132
|
+
// que corre PRIMERO. Con copilot en `selected` (que es lo que hace
|
|
133
|
+
// `awm update`, que apunta a todos los agentes habilitados) tiraba aca,
|
|
134
|
+
// antes de llegar al loop de planificacion, rompiendo el comando para
|
|
135
|
+
// TODOS los demas agentes de la maquina. Un agente que no soporta este
|
|
136
|
+
// scope no tiene grupo compartido que verificar: no es parte del grupo.
|
|
137
|
+
let target;
|
|
138
|
+
try {
|
|
139
|
+
target = physicalTarget(intent, agent, scope, projectRoot).targetPath;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
125
144
|
// Same reasoning as agentsSharingSkillTarget above: a candidate in
|
|
126
145
|
// `enabled` that doesn't support this scope (e.g. Copilot at `global`)
|
|
127
146
|
// can't be part of the shared-target group — it just isn't a
|
|
@@ -172,7 +191,33 @@ function planInstall(params) {
|
|
|
172
191
|
}
|
|
173
192
|
const groups = new Map();
|
|
174
193
|
for (const intent of artifacts) {
|
|
175
|
-
|
|
194
|
+
// Agentes que estructuralmente NO pueden recibir este artefacto en este
|
|
195
|
+
// scope — hoy: copilot en scope global, que no tiene mecanismo de
|
|
196
|
+
// descubrimiento de skills a nivel usuario.
|
|
197
|
+
//
|
|
198
|
+
// Se saltean, pero SOLO si algun otro agente seleccionado si puede. La
|
|
199
|
+
// distincion es deliberada:
|
|
200
|
+
// - `awm update` / `awm add <bundle>` corren sobre TODOS los agentes
|
|
201
|
+
// habilitados. Que uno no pueda no es un error del usuario, y hacer
|
|
202
|
+
// tirar todo el plan rompia el comando para TODOS los demas agentes
|
|
203
|
+
// de la maquina — incluido el re-sync de hooks, que ni se alcanzaba.
|
|
204
|
+
// - `awm add x -a copilot -s global` es una peticion EXPLICITA. Ahi no
|
|
205
|
+
// hay a quien saltear, asi que sigue tirando con la explicacion
|
|
206
|
+
// (`globalUnsupportedReason`), que es justo lo que el usuario
|
|
207
|
+
// necesita leer.
|
|
208
|
+
const capable = selectedAgents.filter((agent) => {
|
|
209
|
+
if (!(0, providers_1.providerFor)(agent)[intent.type])
|
|
210
|
+
return false;
|
|
211
|
+
try {
|
|
212
|
+
physicalTarget(intent, agent, scope, projectRoot);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
const effectiveAgents = capable.length > 0 ? capable : selectedAgents;
|
|
220
|
+
for (const agent of effectiveAgents) {
|
|
176
221
|
const config = (0, providers_1.providerFor)(agent)[intent.type];
|
|
177
222
|
if (!config)
|
|
178
223
|
continue;
|
|
@@ -41,8 +41,20 @@ const executor_1 = require("./executor");
|
|
|
41
41
|
* accepts. Strips both `:` and `.` (an ISO timestamp has both) so the result
|
|
42
42
|
* is filesystem- and regex-safe either way.
|
|
43
43
|
*/
|
|
44
|
+
/** Id de transaccion: timestamp + sufijo aleatorio.
|
|
45
|
+
*
|
|
46
|
+
* El timestamp solo tiene resolucion de 1 ms y NO era unico. Dos
|
|
47
|
+
* transacciones en el mismo milisegundo (p. ej. `syncProfile`, que corre una
|
|
48
|
+
* por extension en un loop) producian el mismo id, y como cada una reescribe
|
|
49
|
+
* `manifest.json` con SUS entradas y numera los slots de backup desde 0, la
|
|
50
|
+
* segunda pisaba el backup de la primera: el respaldo del archivo original del
|
|
51
|
+
* usuario quedaba irrecuperable y `awm backup restore <id>` restauraba el
|
|
52
|
+
* target equivocado. Es un agujero en el unico mecanismo del que depende toda
|
|
53
|
+
* la promesa de "siempre se puede revertir". El regex de `restoreBackup` ya
|
|
54
|
+
* acepta este formato. */
|
|
44
55
|
function sanitizeTransactionTimestamp() {
|
|
45
|
-
|
|
56
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
57
|
+
return `${stamp}-${crypto_1.default.randomBytes(4).toString('hex')}`;
|
|
46
58
|
}
|
|
47
59
|
function ensureBackupDir(backupDir) {
|
|
48
60
|
fs_1.default.mkdirSync(backupDir, { recursive: true });
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WATCH_PROVIDERS = void 0;
|
|
4
|
+
exports.isWatchProvider = isWatchProvider;
|
|
3
5
|
exports.adapterFor = adapterFor;
|
|
4
6
|
const process_1 = require("./process");
|
|
5
7
|
const RESUME_PROMPT_PREFIX = 'Sos el orquestador SDD de este repo. Corre `awm job reconcile` y ejecuta ';
|
|
@@ -18,10 +20,17 @@ const claudeAdapter = {
|
|
|
18
20
|
activity: process_1.activitySnapshot,
|
|
19
21
|
safeToReplace: baseSafeToReplace,
|
|
20
22
|
};
|
|
23
|
+
/** Los providers que `awm watch` sabe supervisar. Exportado para que el CLI valide en
|
|
24
|
+
* el borde en vez de dejar que un `--provider` invalido llegue hasta el primer tick,
|
|
25
|
+
* ya con el journal tocado y el lock tomado. */
|
|
26
|
+
exports.WATCH_PROVIDERS = ['codex', 'claude-code'];
|
|
27
|
+
function isWatchProvider(value) {
|
|
28
|
+
return typeof value === 'string' && exports.WATCH_PROVIDERS.includes(value);
|
|
29
|
+
}
|
|
21
30
|
function adapterFor(provider) {
|
|
22
31
|
if (provider === 'codex')
|
|
23
32
|
return codexAdapter;
|
|
24
33
|
if (provider === 'claude-code')
|
|
25
34
|
return claudeAdapter;
|
|
26
|
-
throw new Error(`provider desconocido: ${provider} (validos:
|
|
35
|
+
throw new Error(`provider desconocido: ${provider} (validos: ${exports.WATCH_PROVIDERS.join(', ')})`);
|
|
27
36
|
}
|
package/dist/src/core/paths.js
CHANGED
|
@@ -17,8 +17,8 @@ exports.resolveOnPath = resolveOnPath;
|
|
|
17
17
|
// Functions are evaluated at CALL TIME (not require time) so env overrides are
|
|
18
18
|
// always honored and tests need no jest.resetModules().
|
|
19
19
|
const os_1 = __importDefault(require("os"));
|
|
20
|
+
const fs_1 = __importDefault(require("fs"));
|
|
20
21
|
const path_1 = __importDefault(require("path"));
|
|
21
|
-
const child_process_1 = require("child_process");
|
|
22
22
|
/** User home directory with a robust fallback. Never returns a raw, possibly-empty process.env.HOME. */
|
|
23
23
|
function homeDir() {
|
|
24
24
|
return process.env.HOME || os_1.default.homedir();
|
|
@@ -74,14 +74,67 @@ function noteWindowsCaveat(log) {
|
|
|
74
74
|
if (isWindowsNative())
|
|
75
75
|
log(exports.WINDOWS_KNOWN_GAP);
|
|
76
76
|
}
|
|
77
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a binary on PATH, IN PROCESS — no shell, no subprocess.
|
|
79
|
+
*
|
|
80
|
+
* SECURITY (this is the whole reason for the implementation below): this used
|
|
81
|
+
* to build `command -v ${bin}` / `where ${bin}` and hand it to `execSync`,
|
|
82
|
+
* which runs it through a shell. `bin` is the first token of a sensor's `cmd`,
|
|
83
|
+
* and that string comes from `.awm/sensors.json` — a file committed in the
|
|
84
|
+
* repo, and also regenerated from a registry sensor-pack's `defaultCmd`. So a
|
|
85
|
+
* cloned untrusted repo, or a third-party registry, could put shell syntax
|
|
86
|
+
* there. The token can't contain whitespace (it's a split token), but `${IFS}`
|
|
87
|
+
* and `$(...)` make that irrelevant. Confirmed against the real binary: a
|
|
88
|
+
* crafted `cmd` executed an arbitrary command during `awm sensors status` —
|
|
89
|
+
* a read-only inspection command — which then reported the sensor HEALTHY,
|
|
90
|
+
* because the injected command exited 0.
|
|
91
|
+
*
|
|
92
|
+
* Resolving PATH ourselves removes the interpreter entirely, so there is
|
|
93
|
+
* nothing left to inject into on any platform. It's also faster (no process
|
|
94
|
+
* spawn) and more predictable than depending on `where`/`command -v` being
|
|
95
|
+
* present and well-behaved.
|
|
96
|
+
*/
|
|
78
97
|
function resolveOnPath(bin) {
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
(0, child_process_1.execSync)(cmd, { stdio: 'pipe' });
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
catch {
|
|
98
|
+
if (typeof bin !== 'string' || bin.trim() === '')
|
|
85
99
|
return false;
|
|
100
|
+
// win32 resolves `eslint` to `eslint.cmd`/`eslint.exe` via PATHEXT. Missing
|
|
101
|
+
// this broke every npm-shim-backed sensor on Windows (real bug, published in
|
|
102
|
+
// v3.9.0). POSIX has no such concept — the name is the name.
|
|
103
|
+
const extensions = isWindowsNative()
|
|
104
|
+
? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
|
|
105
|
+
: [];
|
|
106
|
+
const isExecutable = (candidate) => {
|
|
107
|
+
try {
|
|
108
|
+
if (!fs_1.default.statSync(candidate).isFile())
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
// On win32 the executable bit doesn't exist; being a file with a PATHEXT
|
|
115
|
+
// extension is the whole contract.
|
|
116
|
+
if (isWindowsNative())
|
|
117
|
+
return true;
|
|
118
|
+
try {
|
|
119
|
+
fs_1.default.accessSync(candidate, fs_1.default.constants.X_OK);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// PATHEXT is conventionally uppercase (`.COM;.EXE;.BAT;.CMD`) while the files
|
|
127
|
+
// on disk are usually lowercase (`eslint.cmd`). On real Windows that's a
|
|
128
|
+
// non-issue because the filesystem is case-insensitive — but it IS an issue
|
|
129
|
+
// on any case-sensitive volume, so try both spellings rather than depend on
|
|
130
|
+
// filesystem behavior we don't control.
|
|
131
|
+
const matches = (base) => isExecutable(base) ||
|
|
132
|
+
extensions.some((ext) => isExecutable(base + ext) || isExecutable(base + ext.toLowerCase()));
|
|
133
|
+
// A name carrying a separator is a path, not a PATH lookup — resolve it
|
|
134
|
+
// directly instead of joining it onto every PATH entry.
|
|
135
|
+
if (bin.includes('/') || (isWindowsNative() && bin.includes('\\'))) {
|
|
136
|
+
return matches(path_1.default.resolve(bin));
|
|
86
137
|
}
|
|
138
|
+
const entries = (process.env.PATH ?? '').split(path_1.default.delimiter).filter(Boolean);
|
|
139
|
+
return entries.some((dir) => matches(path_1.default.join(dir, bin)));
|
|
87
140
|
}
|
package/dist/src/core/profile.js
CHANGED
|
@@ -125,6 +125,18 @@ function ensureSkillsGitignored(root, agents) {
|
|
|
125
125
|
const config = provider[type];
|
|
126
126
|
if (!config)
|
|
127
127
|
continue;
|
|
128
|
+
// Solo se ignoran los artefactos que son SYMLINKS especificos de la
|
|
129
|
+
// maquina (renderer `link`), que es lo que `awm sync` reconstruye.
|
|
130
|
+
// Un renderer que produce CONTENIDO (`.mdc` de cursor,
|
|
131
|
+
// `.instructions.md` de copilot) genera archivos reales que el
|
|
132
|
+
// equipo necesita commitear — y para copilot ese es su UNICO canal
|
|
133
|
+
// de entrega, asi que ignorarlos hacia que los companeros no
|
|
134
|
+
// recibieran absolutamente nada. Tambien arrastraba a
|
|
135
|
+
// `.cursor/rules/awm.mdc`, el carrier que existe justamente para
|
|
136
|
+
// que el contexto sobreviva al Background Agent de Cursor, que lee
|
|
137
|
+
// del checkout de git.
|
|
138
|
+
if (config.renderer !== 'link')
|
|
139
|
+
continue;
|
|
128
140
|
const entry = config.local.endsWith('/') ? config.local : `${config.local}/`;
|
|
129
141
|
if (!toIgnore.includes(entry))
|
|
130
142
|
toIgnore.push(entry);
|
|
@@ -8,39 +8,64 @@ exports.scanLegacyArtifacts = scanLegacyArtifacts;
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const providers_1 = require("../providers");
|
|
11
|
+
const artifact_state_1 = require("./artifact-state");
|
|
11
12
|
const ARTIFACT_TYPES = ['skill', 'workflow', 'agent'];
|
|
12
13
|
function preflightLinkArtifactPairs(pairs) {
|
|
13
14
|
for (const { agent, artifact } of pairs) {
|
|
14
15
|
(0, providers_1.assertLinkRenderer)(artifact.type, agent);
|
|
15
16
|
}
|
|
16
17
|
}
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Artefactos instalados que AWM puede ofrecer para remover.
|
|
20
|
+
*
|
|
21
|
+
* Cuatro defectos corregidos aca, todos con perdida de datos o no-operacion:
|
|
22
|
+
*
|
|
23
|
+
* 1. **Clave por nombre solo**, a traves de TODOS los tipos y agentes. Un
|
|
24
|
+
* workflow `deploy.md` de antigravity y un agent `deploy.md` de claude-code
|
|
25
|
+
* colapsaban en UNA entrada, asi que elegir "deploy.md" borraba los dos. El
|
|
26
|
+
* escritor (`planInstall`) siempre clavea por ubicacion fisica.
|
|
27
|
+
* 2. **Scope local resuelto contra `process.cwd()`**: `config.local` es
|
|
28
|
+
* relativo (`.claude/skills`), asi que desde un subdirectorio no encontraba
|
|
29
|
+
* nada, y cuando encontraba algo le pasaba una ruta RELATIVA a `fs.rmSync`.
|
|
30
|
+
* Ahora recibe el projectRoot explicito, como todos los demas consumidores.
|
|
31
|
+
* 3. **Listaba el directorio entero**, ofreciendo borrar skills que el usuario
|
|
32
|
+
* escribio a mano. Ahora se cruza contra `readArtifactState()`: solo se
|
|
33
|
+
* ofrece lo que AWM realmente instalo. El registro de propiedad existe
|
|
34
|
+
* justamente para poder distinguirlo.
|
|
35
|
+
* 4. **Se tragaba `UnsupportedRendererError`**, asi que era un no-op permanente
|
|
36
|
+
* para skills de cursor/copilot y agents de codex: `awm remove -a cursor`
|
|
37
|
+
* decia "no hay artefactos instalados" con el directorio lleno.
|
|
38
|
+
*/
|
|
39
|
+
function scanLegacyArtifacts(agents, scope, projectRoot = process.cwd(), owned = (0, artifact_state_1.readArtifactState)()) {
|
|
18
40
|
const artifacts = new Map();
|
|
41
|
+
const ownedPaths = new Set(owned.map((r) => path_1.default.resolve(r.targetPath)));
|
|
19
42
|
for (const agent of agents) {
|
|
20
43
|
for (const type of ARTIFACT_TYPES) {
|
|
21
|
-
|
|
22
|
-
try {
|
|
23
|
-
config = (0, providers_1.assertLinkRenderer)(type, agent);
|
|
24
|
-
}
|
|
25
|
-
catch (error) {
|
|
26
|
-
if (error instanceof providers_1.UnsupportedRendererError)
|
|
27
|
-
continue;
|
|
28
|
-
throw error;
|
|
29
|
-
}
|
|
44
|
+
const config = (0, providers_1.providerFor)(agent)[type];
|
|
30
45
|
if (!config)
|
|
31
46
|
continue;
|
|
32
|
-
|
|
47
|
+
// Se usa la config del provider directamente: `assertLinkRenderer`
|
|
48
|
+
// TIRA para renderers que producen contenido, y atrapar ese throw
|
|
49
|
+
// convertia a cursor/copilot/codex en invisibles para `awm remove`.
|
|
50
|
+
const dir = scope === 'local' ? path_1.default.join(projectRoot, config.local) : config.global;
|
|
33
51
|
if (dir === null || !fs_1.default.existsSync(dir))
|
|
34
52
|
continue;
|
|
35
53
|
for (const entry of fs_1.default.readdirSync(dir, { withFileTypes: true })) {
|
|
36
54
|
const fullPath = path_1.default.join(dir, entry.name);
|
|
37
|
-
|
|
55
|
+
// Solo lo que AWM instalo. Un archivo del usuario en el mismo
|
|
56
|
+
// directorio no es nuestro para ofrecerlo en un menu de borrado.
|
|
57
|
+
if (!ownedPaths.has(path_1.default.resolve(fullPath)))
|
|
58
|
+
continue;
|
|
59
|
+
// Clave por TIPO + nombre: dos artefactos de tipos distintos con
|
|
60
|
+
// el mismo nombre son cosas distintas en lugares distintos.
|
|
61
|
+
const key = `${type}\u0000${entry.name}`;
|
|
62
|
+
const existing = artifacts.get(key);
|
|
38
63
|
if (existing) {
|
|
39
64
|
existing.installedIn.push(agent);
|
|
40
65
|
existing.fullPaths.push(fullPath);
|
|
41
66
|
}
|
|
42
67
|
else {
|
|
43
|
-
artifacts.set(
|
|
68
|
+
artifacts.set(key, {
|
|
44
69
|
name: entry.name,
|
|
45
70
|
type,
|
|
46
71
|
installedIn: [agent],
|
|
@@ -19,18 +19,24 @@ function assertProviderSupported(agent, exec = child_process_1.execFileSync) {
|
|
|
19
19
|
}
|
|
20
20
|
catch (error) {
|
|
21
21
|
const code = error.code;
|
|
22
|
+
// `provider.label` y `versionCommand`, no "Codex" literal. Esta funcion es
|
|
23
|
+
// generica sobre AgentTarget desde siempre, pero cada mensaje y el patron de
|
|
24
|
+
// parseo nombraban al unico provider que hoy declara `versionCommand` — el
|
|
25
|
+
// segundo en declararlo habria reportado "Codex no esta instalado" al no
|
|
26
|
+
// encontrar SU binario, y habria fallado a parsear una salida perfectamente
|
|
27
|
+
// valida contra el formato de otro programa.
|
|
22
28
|
if (code === 'ENOENT') {
|
|
23
|
-
throw new Error(
|
|
24
|
-
|
|
29
|
+
throw new Error(`${provider.label} is not installed or not available on PATH ` +
|
|
30
|
+
`(tried \`${provider.versionCommand.command}\`). Install it, then re-run.`);
|
|
25
31
|
}
|
|
26
|
-
throw new Error(
|
|
32
|
+
throw new Error(`${provider.label} version probe failed: ${error.message}`);
|
|
27
33
|
}
|
|
28
|
-
const match = output.trim().match(
|
|
34
|
+
const match = output.trim().match(provider.versionCommand.versionPattern);
|
|
29
35
|
if (!match) {
|
|
30
|
-
throw new Error(`could not parse
|
|
36
|
+
throw new Error(`could not parse ${provider.label} version from: ${output.trim()}`);
|
|
31
37
|
}
|
|
32
38
|
if ((0, versioning_1.compareSemver)(match[1], provider.minimumVersion) < 0) {
|
|
33
|
-
throw new Error(`requires
|
|
39
|
+
throw new Error(`requires ${provider.label} >= ${provider.minimumVersion}; found ${match[1]}`);
|
|
34
40
|
}
|
|
35
41
|
return { provider: agent, version: match[1] };
|
|
36
42
|
}
|
|
@@ -3,7 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.REGISTRY_MANIFEST_NAME = exports.CONTENT_DIR_NAMES =
|
|
6
|
+
exports.REGISTRY_MANIFEST_NAME = exports.CONTENT_DIR_NAMES = void 0;
|
|
7
|
+
exports.registriesDir = registriesDir;
|
|
8
|
+
exports.registriesConfigPath = registriesConfigPath;
|
|
7
9
|
exports.registryContentRoot = registryContentRoot;
|
|
8
10
|
exports.readRegistriesConfig = readRegistriesConfig;
|
|
9
11
|
exports.writeRegistriesConfig = writeRegistriesConfig;
|
|
@@ -30,49 +32,57 @@ const versioning_1 = require("./versioning");
|
|
|
30
32
|
const cli_version_1 = require("./cli-version");
|
|
31
33
|
const paths_1 = require("./paths");
|
|
32
34
|
// Computed at require-time by calling the shared resolver (single source of truth: core/paths.ts).
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
// Funciones, no constantes de nivel de modulo. `awmHome()` lee AWM_HOME/HOME EN CADA
|
|
36
|
+
// LLAMADA a proposito (paths.ts) — resolverlo una sola vez al hacer `require` congelaba
|
|
37
|
+
// la respuesta en el instante del import, asi que cualquier cosa que fijara AWM_HOME
|
|
38
|
+
// despues (un test, un comando que cambia de home) seguia apuntando al viejo. Este
|
|
39
|
+
// modulo era el unico lugar que contradecia esa regla.
|
|
40
|
+
function registriesDir() {
|
|
41
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'registries');
|
|
42
|
+
}
|
|
43
|
+
function registriesConfigPath() {
|
|
44
|
+
return path_1.default.join((0, paths_1.awmHome)(), 'registries.json');
|
|
45
|
+
}
|
|
36
46
|
exports.CONTENT_DIR_NAMES = ['skills', 'bundles', 'workflows', 'agents'];
|
|
37
47
|
function registryContentRoot(name) {
|
|
38
|
-
const root = path_1.default.join(
|
|
39
|
-
if (!path_1.default.resolve(root).startsWith(path_1.default.resolve(
|
|
48
|
+
const root = path_1.default.join(registriesDir(), name);
|
|
49
|
+
if (!path_1.default.resolve(root).startsWith(path_1.default.resolve(registriesDir()) + path_1.default.sep)) {
|
|
40
50
|
throw new Error(`Invalid registry name "${name}" — must not contain path separators`);
|
|
41
51
|
}
|
|
42
52
|
return root;
|
|
43
53
|
}
|
|
44
54
|
function readRegistriesConfig() {
|
|
45
|
-
if (!fs_1.default.existsSync(
|
|
55
|
+
if (!fs_1.default.existsSync(registriesConfigPath()))
|
|
46
56
|
return [];
|
|
47
57
|
let raw;
|
|
48
58
|
try {
|
|
49
|
-
raw = JSON.parse(fs_1.default.readFileSync(
|
|
59
|
+
raw = JSON.parse(fs_1.default.readFileSync(registriesConfigPath(), 'utf-8'));
|
|
50
60
|
}
|
|
51
61
|
catch (e) {
|
|
52
|
-
throw new Error(`Invalid registries config at ${
|
|
62
|
+
throw new Error(`Invalid registries config at ${registriesConfigPath()}: ${e instanceof Error ? e.message : String(e)}`);
|
|
53
63
|
}
|
|
54
64
|
if (!Array.isArray(raw)) {
|
|
55
|
-
throw new Error(`Invalid registries config at ${
|
|
65
|
+
throw new Error(`Invalid registries config at ${registriesConfigPath()}: expected a JSON array`);
|
|
56
66
|
}
|
|
57
67
|
for (const entry of raw) {
|
|
58
68
|
if (typeof entry?.name !== 'string' || typeof entry?.remote !== 'string') {
|
|
59
|
-
throw new Error(`Invalid registries config at ${
|
|
69
|
+
throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry ${JSON.stringify(entry)}`);
|
|
60
70
|
}
|
|
61
71
|
if (entry.name === '.' || entry.name.includes('/') || entry.name.includes('\\') || entry.name.includes('..')) {
|
|
62
|
-
throw new Error(`Invalid registries config at ${
|
|
72
|
+
throw new Error(`Invalid registries config at ${registriesConfigPath()}: malformed entry name "${entry.name}" (path traversal)`);
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
75
|
return raw;
|
|
66
76
|
}
|
|
67
77
|
function writeRegistriesConfig(entries) {
|
|
68
|
-
fs_1.default.mkdirSync(
|
|
69
|
-
fs_1.default.writeFileSync(
|
|
78
|
+
fs_1.default.mkdirSync((0, paths_1.awmHome)(), { recursive: true });
|
|
79
|
+
fs_1.default.writeFileSync(registriesConfigPath(), JSON.stringify(entries, null, 2) + '\n', 'utf-8');
|
|
70
80
|
}
|
|
71
81
|
/** Bootstrap de máquina (awm init): si no hay registries.json, lo crea con la
|
|
72
82
|
* entrada baseline (remote por cadena WS-2: env > prefs > default).
|
|
73
83
|
* Devuelve true si sembró. Nunca toca un registries.json existente. */
|
|
74
84
|
function seedBaselineRegistry() {
|
|
75
|
-
if (fs_1.default.existsSync(
|
|
85
|
+
if (fs_1.default.existsSync(registriesConfigPath()))
|
|
76
86
|
return false;
|
|
77
87
|
writeRegistriesConfig([{ name: 'baseline', remote: (0, registry_1.resolveBaseRemote)() }]);
|
|
78
88
|
return true;
|
|
@@ -111,7 +121,7 @@ async function syncRegistries() {
|
|
|
111
121
|
try {
|
|
112
122
|
const freshClone = !fs_1.default.existsSync(reg.contentRoot);
|
|
113
123
|
if (freshClone) {
|
|
114
|
-
fs_1.default.mkdirSync(
|
|
124
|
+
fs_1.default.mkdirSync(registriesDir(), { recursive: true });
|
|
115
125
|
try {
|
|
116
126
|
await (0, simple_git_1.default)().clone(reg.remote, reg.contentRoot);
|
|
117
127
|
}
|
|
@@ -219,11 +229,11 @@ function readRegistryManifest(root) {
|
|
|
219
229
|
}
|
|
220
230
|
return { overrides: new Set(overrides), minCliVersion };
|
|
221
231
|
}
|
|
222
|
-
/** Nombre del registry dueño de un path: el nombre del clone bajo
|
|
232
|
+
/** Nombre del registry dueño de un path: el nombre del clone bajo registriesDir(),
|
|
223
233
|
* o null si no pertenece a ninguno. */
|
|
224
234
|
function registryNameForPath(p) {
|
|
225
235
|
const resolved = path_1.default.resolve(p);
|
|
226
|
-
const regsRoot = path_1.default.resolve(
|
|
236
|
+
const regsRoot = path_1.default.resolve(registriesDir()) + path_1.default.sep;
|
|
227
237
|
if (resolved.startsWith(regsRoot)) {
|
|
228
238
|
const first = resolved.slice(regsRoot.length).split(path_1.default.sep)[0];
|
|
229
239
|
return first || null;
|
|
@@ -243,8 +253,19 @@ function verifyMinCliVersions(current = (0, cli_version_1.cliVersion)()) {
|
|
|
243
253
|
catch {
|
|
244
254
|
continue;
|
|
245
255
|
}
|
|
246
|
-
|
|
256
|
+
// Falla CERRADO: si alguna de las dos versiones no es legible, se
|
|
257
|
+
// reporta el fallo en vez de dejar pasar. Antes `compareSemver`
|
|
258
|
+
// devolvia NaN y `NaN < 0` es false, asi que un `minCliVersion`
|
|
259
|
+
// malformado hacia que el gate se saltara en silencio.
|
|
260
|
+
if (!min)
|
|
261
|
+
continue;
|
|
262
|
+
try {
|
|
263
|
+
if ((0, versioning_1.compareSemver)(current, min) < 0)
|
|
264
|
+
failures.push({ name: reg.name, min });
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
247
267
|
failures.push({ name: reg.name, min });
|
|
268
|
+
}
|
|
248
269
|
}
|
|
249
270
|
return failures;
|
|
250
271
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderedFilename = renderedFilename;
|
|
4
|
+
exports.rendererExtension = rendererExtension;
|
|
5
|
+
/** Extension que el renderer estampa sobre el nombre base, o `null` cuando el
|
|
6
|
+
* artefacto se instala con el nombre tal cual (renderer `link`). */
|
|
7
|
+
const RENDERER_EXTENSION = {
|
|
8
|
+
link: null,
|
|
9
|
+
'codex-agent-toml': '.toml',
|
|
10
|
+
'cursor-mdc': '.mdc',
|
|
11
|
+
'copilot-instructions': '.instructions.md',
|
|
12
|
+
};
|
|
13
|
+
/** `.md` es la unica extension que un `installName` de artefacto puede traer.
|
|
14
|
+
* Deliberadamente NO se usa `path.parse().name`: eso corta desde el ULTIMO
|
|
15
|
+
* punto, asi que un skill llamado `v1.2-migration` se truncaria a `v1`,
|
|
16
|
+
* perdiendo `2-migration` y arriesgando colision con otro skill `v1`. */
|
|
17
|
+
function stripMarkdownSuffix(installName) {
|
|
18
|
+
return installName.endsWith('.md') ? installName.slice(0, -'.md'.length) : installName;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Nombre de archivo FISICO que `renderer` produce para `installName`.
|
|
22
|
+
*
|
|
23
|
+
* Es la funcion que deben usar por igual el escritor (al planificar la
|
|
24
|
+
* instalacion) y todo lector (al preguntar "¿esta instalado?"). Que ambos lados
|
|
25
|
+
* salgan de aca es lo que hace estructuralmente imposible que vuelvan a
|
|
26
|
+
* discrepar.
|
|
27
|
+
*/
|
|
28
|
+
function renderedFilename(installName, renderer) {
|
|
29
|
+
const extension = RENDERER_EXTENSION[renderer];
|
|
30
|
+
return extension === null ? installName : `${stripMarkdownSuffix(installName)}${extension}`;
|
|
31
|
+
}
|
|
32
|
+
/** Extension del renderer, o `null` si instala con el nombre tal cual. Para los
|
|
33
|
+
* pocos casos que necesitan la extension suelta (p. ej. "¿hay ALGUN archivo
|
|
34
|
+
* renderizado en este directorio?") en vez de un nombre concreto. */
|
|
35
|
+
function rendererExtension(renderer) {
|
|
36
|
+
return RENDERER_EXTENSION[renderer];
|
|
37
|
+
}
|