@dsalazardev/ai-hub-cli 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -4
- package/dist/backup/snapshots.js +12 -0
- package/dist/catalog.d.ts +8 -1
- package/dist/catalog.js +38 -9
- package/dist/cli/args.js +1 -0
- package/dist/cli/doctor.js +20 -11
- package/dist/cli/maintenance.js +21 -39
- package/dist/cli/memory.js +4 -0
- package/dist/cli/onboarding.d.ts +13 -0
- package/dist/cli/onboarding.js +128 -14
- package/dist/cli/run.d.ts +3 -1
- package/dist/cli/run.js +5 -4
- package/dist/cli/types.d.ts +3 -0
- package/dist/engine/engine.d.ts +19 -1
- package/dist/engine/engine.js +52 -0
- package/dist/engine/gates.d.ts +16 -0
- package/dist/engine/gates.js +102 -1
- package/dist/harness/memory/ledger.d.ts +6 -0
- package/dist/harness/memory/ledger.js +44 -2
- package/dist/manifest/manifest.d.ts +3 -1
- package/dist/manifest/manifest.js +16 -10
- package/dist/presets/materialize.d.ts +5 -0
- package/dist/presets/materialize.js +30 -0
- package/dist/speckit/binding.d.ts +15 -0
- package/dist/speckit/binding.js +66 -0
- package/dist/speckit/commands.d.ts +46 -0
- package/dist/speckit/commands.js +131 -0
- package/dist/speckit/doctor.d.ts +14 -0
- package/dist/speckit/doctor.js +103 -0
- package/dist/speckit/runner.d.ts +19 -0
- package/dist/speckit/runner.js +22 -0
- package/dist/speckit/seal.d.ts +12 -0
- package/dist/speckit/seal.js +47 -0
- package/dist/speckit/settings.d.ts +36 -0
- package/dist/speckit/settings.js +75 -0
- package/dist/speckit/state.d.ts +59 -0
- package/dist/speckit/state.js +279 -0
- package/dist/speckit/version.d.ts +8 -0
- package/dist/speckit/version.js +53 -0
- package/dist/sync/config.js +3 -2
- package/dist/sync/render.js +30 -0
- package/dist/sync/sync.d.ts +4 -0
- package/dist/sync/sync.js +48 -5
- package/dist/sync/targets.d.ts +13 -0
- package/dist/sync/targets.js +5 -0
- package/engine/engine.yaml +65 -31
- package/manifest.json +20 -48
- package/package.json +1 -1
- package/schema/engine.schema.json +22 -3
- package/schema/manifest.schema.json +10 -0
- package/skills/shared/analyze/SKILL.md +2 -1
- package/skills/shared/clarify/SKILL.md +2 -1
- package/skills/shared/implement/SKILL.md +2 -1
- package/skills/shared/plan/SKILL.md +2 -1
- package/skills/shared/review/SKILL.md +2 -1
- package/skills/shared/sdd/SKILL.md +2 -1
- package/skills/shared/sdd-init/SKILL.md +2 -1
- package/skills/shared/ship/SKILL.md +2 -1
- package/skills/shared/specify/SKILL.md +2 -1
- package/skills/shared/tasks/SKILL.md +2 -1
- package/skills/shared/verify/SKILL.md +2 -1
- package/skills/shared/speckit-analyze/SKILL.md +0 -35
- package/skills/shared/speckit-assess-decide/SKILL.md +0 -36
- package/skills/shared/speckit-assess-define/SKILL.md +0 -36
- package/skills/shared/speckit-assess-intake/SKILL.md +0 -38
- package/skills/shared/speckit-assess-research/SKILL.md +0 -36
- package/skills/shared/speckit-assess-shape/SKILL.md +0 -36
- package/skills/shared/speckit-bug-assess/SKILL.md +0 -38
- package/skills/shared/speckit-bug-fix/SKILL.md +0 -37
- package/skills/shared/speckit-bug-test/SKILL.md +0 -37
- package/skills/shared/speckit-checklist/SKILL.md +0 -35
- package/skills/shared/speckit-clarify/SKILL.md +0 -32
- package/skills/shared/speckit-constitution/SKILL.md +0 -35
- package/skills/shared/speckit-converge/SKILL.md +0 -36
- package/skills/shared/speckit-implement/SKILL.md +0 -32
- package/skills/shared/speckit-plan/SKILL.md +0 -37
- package/skills/shared/speckit-specify/SKILL.md +0 -36
- package/skills/shared/speckit-tasks/SKILL.md +0 -34
package/dist/cli/onboarding.js
CHANGED
|
@@ -4,7 +4,7 @@ import { stringify } from 'yaml';
|
|
|
4
4
|
import { createBackup } from '../backup/snapshots.js';
|
|
5
5
|
import { readAiHubRc, writeAiHubRc } from '../paths.js';
|
|
6
6
|
import { detectPreset } from '../presets/detect.js';
|
|
7
|
-
import { materializeCatalog, materializePreset, materializeUniversalRules, } from '../presets/materialize.js';
|
|
7
|
+
import { materializeCatalog, materializePreset, materializeUniversalRules, removeOwned, } from '../presets/materialize.js';
|
|
8
8
|
import { resolvePreset } from '../presets/resolve.js';
|
|
9
9
|
import { listPresetIds } from '../presets/schema.js';
|
|
10
10
|
import { loadManifestContext, writeInventory } from '../manifest/manifest.js';
|
|
@@ -15,6 +15,9 @@ import { sha256 } from '../sync/lock.js';
|
|
|
15
15
|
import { isEngineCheckout } from '../source-checkout.js';
|
|
16
16
|
import { ENGINE_AGENTS_DIR, ENGINE_RULES_DIR, catalogIndex, catalogSeal, expectedRuleIds, hasFullDimension, listCatalogAgents, listCatalogSkills, } from '../catalog.js';
|
|
17
17
|
import { loadEngine } from '../engine/engine.js';
|
|
18
|
+
import { probeEnvironment, runSpeckitSequence, speckitPreflight, } from '../speckit/commands.js';
|
|
19
|
+
import { effectiveSpeckit } from '../speckit/settings.js';
|
|
20
|
+
import { checkExtensions, checkIntegrations, readSpeckitState } from '../speckit/state.js';
|
|
18
21
|
import { flagList, flagString } from './args.js';
|
|
19
22
|
const RC_NAME = '.ai-hubrc.json';
|
|
20
23
|
export async function cliVersion() {
|
|
@@ -22,6 +25,71 @@ export async function cliVersion() {
|
|
|
22
25
|
const pkg = JSON.parse(raw);
|
|
23
26
|
return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
|
|
24
27
|
}
|
|
28
|
+
function speckitPlan(settings) {
|
|
29
|
+
return {
|
|
30
|
+
managed: settings.managed,
|
|
31
|
+
package: settings.package,
|
|
32
|
+
version: settings.version,
|
|
33
|
+
excludeNewer: settings.excludeNewer,
|
|
34
|
+
script: settings.script,
|
|
35
|
+
defaultIntegration: settings.defaultIntegration,
|
|
36
|
+
integrations: settings.integrations,
|
|
37
|
+
extensions: settings.extensions,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Bloque `speckit` a escribir: conserva las restricciones del proyecto y fija `managed`. */
|
|
41
|
+
function speckitBlock(existing, managed) {
|
|
42
|
+
return { ...(existing ?? {}), managed };
|
|
43
|
+
}
|
|
44
|
+
function speckitSettingsFor(targets, block, engine) {
|
|
45
|
+
return effectiveSpeckit({ targets, speckit: block }, engine);
|
|
46
|
+
}
|
|
47
|
+
async function environmentFor(settings, ctx) {
|
|
48
|
+
if (!settings.managed) {
|
|
49
|
+
return { skipped: 'Spec Kit no gestionado (--no-speckit)' };
|
|
50
|
+
}
|
|
51
|
+
return probeEnvironment(settings, ctx.exec, ctx.root);
|
|
52
|
+
}
|
|
53
|
+
/** Preflight de Spec Kit; devuelve `false` y explica el motivo si hay que salir con 2. */
|
|
54
|
+
async function preflightOrReport(settings, ctx) {
|
|
55
|
+
const preflight = await speckitPreflight(settings, ctx.exec, ctx.root);
|
|
56
|
+
if (!preflight.ok) {
|
|
57
|
+
ctx.io.err('preflight de Spec Kit fallido; no se escribió nada:');
|
|
58
|
+
for (const problem of preflight.problems) {
|
|
59
|
+
ctx.io.err(` - ${problem}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return preflight.ok;
|
|
63
|
+
}
|
|
64
|
+
/** Ejecuta la secuencia de Spec Kit; devuelve `false` y explica cómo restaurar si falla. */
|
|
65
|
+
async function installSpeckit(settings, ctx, backupId) {
|
|
66
|
+
const result = await runSpeckitSequence(settings, ctx.exec, ctx.root);
|
|
67
|
+
const problems = [];
|
|
68
|
+
if (!result.ok) {
|
|
69
|
+
problems.push(result.failure ?? 'fallo desconocido');
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// Paso 9d: la secuencia solo cuenta si deja las integraciones y extensiones esperadas.
|
|
73
|
+
const state = await readSpeckitState(ctx.root);
|
|
74
|
+
problems.push(...checkIntegrations(settings, state), ...(await checkExtensions(ctx.root, settings, state)));
|
|
75
|
+
}
|
|
76
|
+
if (problems.length > 0) {
|
|
77
|
+
ctx.io.err('Spec Kit no quedó instalado como se esperaba:');
|
|
78
|
+
for (const problem of problems) {
|
|
79
|
+
ctx.io.err(` - ${problem}`);
|
|
80
|
+
}
|
|
81
|
+
if (backupId !== null) {
|
|
82
|
+
ctx.io.err(`restaura el estado previo con: ai-hub restore ${backupId}`);
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function speckitSummary(settings) {
|
|
89
|
+
return settings.managed
|
|
90
|
+
? `Spec Kit ${settings.version} · script ${settings.script} · integraciones ${settings.integrations.join(', ')}`
|
|
91
|
+
: 'Spec Kit no gestionado (--no-speckit): el arnés queda degradado; ejecuta ai-hub install para activarlo';
|
|
92
|
+
}
|
|
25
93
|
function projectId(root) {
|
|
26
94
|
const base = path.basename(path.resolve(root)).toLowerCase();
|
|
27
95
|
const id = base.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
@@ -78,13 +146,14 @@ async function buildSelection(args, preset, engine, existing) {
|
|
|
78
146
|
const exclusions = parseExclusions(args);
|
|
79
147
|
const hasExclusions = exclusions.skills.size > 0 || exclusions.agents.size > 0 || exclusions.rules.size > 0;
|
|
80
148
|
if (existing && !fullFlag && !presetOnly && !hasExclusions) {
|
|
149
|
+
const retired = new Set(await retiredCatalogSkills(existing.skills.publish));
|
|
81
150
|
return {
|
|
82
151
|
modes: {
|
|
83
152
|
skills: existing.skills.selection ?? 'preset',
|
|
84
153
|
agents: existing.agents.selection ?? 'preset',
|
|
85
154
|
rules: existing.rules.selection ?? 'preset',
|
|
86
155
|
},
|
|
87
|
-
skills:
|
|
156
|
+
skills: existing.skills.publish.filter((id) => !retired.has(id)),
|
|
88
157
|
agents: [...existing.agents.publish],
|
|
89
158
|
rules: [...existing.rules.publish],
|
|
90
159
|
};
|
|
@@ -125,7 +194,7 @@ async function buildSelection(args, preset, engine, existing) {
|
|
|
125
194
|
rules: [...new Set(filterRules(rulesBase, exclusions.rules))].sort(),
|
|
126
195
|
};
|
|
127
196
|
}
|
|
128
|
-
function manifestFromSelection(id, preset, targets, profilePackageManager, guardMode, includeMcp, selection) {
|
|
197
|
+
function manifestFromSelection(id, preset, targets, profilePackageManager, guardMode, includeMcp, selection, speckit) {
|
|
129
198
|
const manifest = {
|
|
130
199
|
version: 2,
|
|
131
200
|
project: { id, name: id },
|
|
@@ -150,6 +219,7 @@ function manifestFromSelection(id, preset, targets, profilePackageManager, guard
|
|
|
150
219
|
agents: { source: '.ai-hub/agents', publish: selection.agents, selection: selection.modes.agents },
|
|
151
220
|
rules: { source: '.ai-hub/rules', publish: selection.rules, selection: selection.modes.rules },
|
|
152
221
|
...(includeMcp ? { mcp: { source: 'mcp-manifest.yaml' } } : {}),
|
|
222
|
+
speckit,
|
|
153
223
|
};
|
|
154
224
|
if (hasFullDimension(manifest)) {
|
|
155
225
|
manifest.catalog = catalogSeal(manifest);
|
|
@@ -180,8 +250,9 @@ async function materializeSelection(root, preset, selection) {
|
|
|
180
250
|
}
|
|
181
251
|
async function buildAssetPlan(selection, engine, targets, includeMcp) {
|
|
182
252
|
const index = await catalogIndex();
|
|
183
|
-
|
|
184
|
-
|
|
253
|
+
// Los 17 comandos los proyecta Spec Kit; AI-Hub solo proyecta las skills de proyecto.
|
|
254
|
+
void engine;
|
|
255
|
+
const allSkills = [...new Set(selection.skills)];
|
|
185
256
|
const projectSkillFiles = selection.skills.reduce((total, id) => total + (index.skillFiles[id]?.length ?? 0), 0);
|
|
186
257
|
const skillFiles = allSkills.reduce((total, id) => total + (index.skillFiles[id]?.length ?? 0), 0);
|
|
187
258
|
let destinations = 1; // opencode.json
|
|
@@ -191,7 +262,7 @@ async function buildAssetPlan(selection, engine, targets, includeMcp) {
|
|
|
191
262
|
if (target.agents) {
|
|
192
263
|
destinations += selection.agents.length;
|
|
193
264
|
}
|
|
194
|
-
destinations += selection.rules.length +
|
|
265
|
+
destinations += selection.rules.length + 2; // reglas materializadas + preflight + regla de enlace de Spec Kit
|
|
195
266
|
}
|
|
196
267
|
if (includeMcp) {
|
|
197
268
|
destinations += 3;
|
|
@@ -323,7 +394,14 @@ export async function cmdInit(args, ctx) {
|
|
|
323
394
|
}
|
|
324
395
|
const includeMcp = await exists(path.join(ctx.root, 'mcp-manifest.yaml'));
|
|
325
396
|
const assets = await buildAssetPlan(spec, engine, selection.targets, includeMcp);
|
|
397
|
+
const speckit = speckitBlock(existing?.speckit, !args.flags['no-speckit']);
|
|
398
|
+
const speckitSettings = speckitSettingsFor(selection.targets, speckit, engine);
|
|
399
|
+
// El taller nunca ejecuta Spec Kit: su .specify/ es del mantenedor.
|
|
400
|
+
const runSpeckit = speckitSettings.managed && !sourceCheckout;
|
|
326
401
|
const write = [RC_NAME, 'manifest.yaml', '.gitignore', '.ai-hub/'];
|
|
402
|
+
if (runSpeckit) {
|
|
403
|
+
write.push('.specify/');
|
|
404
|
+
}
|
|
327
405
|
if (selection.migrate) {
|
|
328
406
|
write.push('.planning/state.json');
|
|
329
407
|
}
|
|
@@ -333,28 +411,38 @@ export async function cmdInit(args, ctx) {
|
|
|
333
411
|
preset: selection.preset.id,
|
|
334
412
|
targets: selection.targets,
|
|
335
413
|
...assets,
|
|
336
|
-
skillCount:
|
|
414
|
+
skillCount: spec.skills.length,
|
|
415
|
+
speckitCommands: engine.commands.length,
|
|
337
416
|
agentCount: spec.agents.length,
|
|
338
417
|
ruleCount: spec.rules.length + 1,
|
|
339
418
|
mcp: includeMcp,
|
|
340
419
|
migrate: selection.migrate,
|
|
341
420
|
write,
|
|
421
|
+
speckit: speckitPlan(speckitSettings),
|
|
342
422
|
};
|
|
343
423
|
const plan = { ...planBody, hash: sha256(JSON.stringify(planBody)) };
|
|
344
424
|
if (!args.flags.yes || args.flags['dry-run']) {
|
|
345
|
-
|
|
425
|
+
const environment = await environmentFor(speckitSettings, ctx);
|
|
426
|
+
ctx.io.out(JSON.stringify({ ...plan, environment }, null, 2));
|
|
346
427
|
if (!args.flags['dry-run']) {
|
|
347
428
|
ctx.io.out('Nada escrito. Repite con --yes para aplicar este plan.');
|
|
348
429
|
}
|
|
349
430
|
return 0;
|
|
350
431
|
}
|
|
351
|
-
if (
|
|
352
|
-
|
|
432
|
+
if (runSpeckit && !(await preflightOrReport(speckitSettings, ctx))) {
|
|
433
|
+
return 2;
|
|
353
434
|
}
|
|
435
|
+
// Backup restaurable antes de que Spec Kit toque un proyecto existente (constitución incluida).
|
|
436
|
+
const backupId = existing || (await exists(path.join(ctx.root, '.specify')))
|
|
437
|
+
? (await createBackup(ctx.root, 'init')).id
|
|
438
|
+
: null;
|
|
354
439
|
const materialized = await materializeSelection(ctx.root, selection.preset, spec);
|
|
355
|
-
const manifest = manifestFromSelection(id, selection.preset, selection.targets, selection.profilePackageManager, rc.guardMode ?? 'shadow', includeMcp, spec);
|
|
440
|
+
const manifest = manifestFromSelection(id, selection.preset, selection.targets, selection.profilePackageManager, rc.guardMode ?? 'shadow', includeMcp, spec, speckit);
|
|
356
441
|
await writeManifest(ctx.root, manifest);
|
|
357
442
|
await writeInventory(ctx.root);
|
|
443
|
+
if (runSpeckit && !(await installSpeckit(speckitSettings, ctx, backupId))) {
|
|
444
|
+
return 1;
|
|
445
|
+
}
|
|
358
446
|
const sync = await runSync(ctx.root, { check: false });
|
|
359
447
|
const existingRc = readAiHubRc(ctx.cwd);
|
|
360
448
|
if (!existingRc || existingRc.version === 1 || args.flags.force) {
|
|
@@ -381,10 +469,11 @@ export async function cmdInit(args, ctx) {
|
|
|
381
469
|
const hook = await installPostMergeHook(ctx.root);
|
|
382
470
|
ctx.io.out(`hooks: ${hook.installed ? 'post-merge instalado' : hook.reason}`);
|
|
383
471
|
}
|
|
384
|
-
ctx.io.out(`init: ${selection.preset.id} · skills=${spec.modes.skills} agents=${spec.modes.agents} rules=${spec.modes.rules} · ${spec.skills.length +
|
|
472
|
+
ctx.io.out(`init: ${selection.preset.id} · skills=${spec.modes.skills} agents=${spec.modes.agents} rules=${spec.modes.rules} · ${spec.skills.length} skills de proyecto + ${engine.commands.length} comandos Spec Kit · ${spec.agents.length} agentes · ${spec.rules.length + 1} reglas · ${assets.destinations} destinos · ${sync.written.length} sincronizados · ${materialized.written.length} nuevos, ${materialized.kept.length} conservados, ${materialized.collisions.length} colisiones`);
|
|
385
473
|
if (materialized.collisions.length > 0) {
|
|
386
474
|
ctx.io.err(`colisiones (no se sobrescribieron): ${materialized.collisions.join(', ')}`);
|
|
387
475
|
}
|
|
476
|
+
ctx.io.out(speckitSummary(speckitSettings));
|
|
388
477
|
ctx.io.out(`plan: ${plan.hash}`);
|
|
389
478
|
return 0;
|
|
390
479
|
}
|
|
@@ -435,13 +524,19 @@ async function selectionForInstall(args, manifest, preset, engine) {
|
|
|
435
524
|
: modes.rules === 'preset'
|
|
436
525
|
? expectedRuleIds('preset', preset)
|
|
437
526
|
: [...manifest.rules.publish];
|
|
527
|
+
const retired = new Set(await retiredCatalogSkills(skills));
|
|
438
528
|
return {
|
|
439
529
|
modes,
|
|
440
|
-
skills: [...new Set(skills.filter((id) => !exclusions.skills.has(id)))].sort(),
|
|
530
|
+
skills: [...new Set(skills.filter((id) => !exclusions.skills.has(id) && !retired.has(id)))].sort(),
|
|
441
531
|
agents: [...new Set(agents.filter((id) => !exclusions.agents.has(id)))].sort(),
|
|
442
532
|
rules: [...new Set(filterRules(rulesBase, exclusions.rules))].sort(),
|
|
443
533
|
};
|
|
444
534
|
}
|
|
535
|
+
/** Ids de `ids` que el catálogo publica con `runtime: false` (no instalables). */
|
|
536
|
+
async function retiredCatalogSkills(ids) {
|
|
537
|
+
const retired = new Set((await catalogIndex()).retiredSkills);
|
|
538
|
+
return ids.filter((id) => retired.has(id));
|
|
539
|
+
}
|
|
445
540
|
export async function cmdInstall(args, ctx) {
|
|
446
541
|
let context;
|
|
447
542
|
try {
|
|
@@ -460,6 +555,10 @@ export async function cmdInstall(args, ctx) {
|
|
|
460
555
|
return 2;
|
|
461
556
|
}
|
|
462
557
|
const includeMcp = await exists(path.join(ctx.root, 'mcp-manifest.yaml'));
|
|
558
|
+
// install sin --no-speckit reactiva Spec Kit en un arnés degradado; con la bandera lo mantiene apagado.
|
|
559
|
+
const speckit = speckitBlock(context.manifest.speckit, !args.flags['no-speckit']);
|
|
560
|
+
const speckitSettings = speckitSettingsFor(context.manifest.targets, speckit, context.engine);
|
|
561
|
+
const runSpeckit = speckitSettings.managed && !(await isEngineCheckout(ctx.root));
|
|
463
562
|
if (args.flags['dry-run']) {
|
|
464
563
|
const assets = await buildAssetPlan(spec, context.engine, context.manifest.targets, includeMcp);
|
|
465
564
|
ctx.io.out(JSON.stringify({
|
|
@@ -472,10 +571,20 @@ export async function cmdInstall(args, ctx) {
|
|
|
472
571
|
agents: spec.agents.length,
|
|
473
572
|
rules: spec.rules.length + 1,
|
|
474
573
|
destinations: assets.destinations,
|
|
574
|
+
speckit: speckitPlan(speckitSettings),
|
|
475
575
|
}, null, 2));
|
|
476
576
|
return 0;
|
|
477
577
|
}
|
|
578
|
+
if (runSpeckit && !(await preflightOrReport(speckitSettings, ctx))) {
|
|
579
|
+
return 2;
|
|
580
|
+
}
|
|
581
|
+
const backupId = runSpeckit ? (await createBackup(ctx.root, 'install')).id : null;
|
|
478
582
|
const materialized = await materializeSelection(ctx.root, context.preset, spec);
|
|
583
|
+
// Migración: las skills `runtime: false` salen de .ai-hub/ si las escribió ai-hub;
|
|
584
|
+
// sync retira después sus copias de los destinos.
|
|
585
|
+
for (const id of await retiredCatalogSkills(context.manifest.skills.publish)) {
|
|
586
|
+
await removeOwned(ctx.root, path.join(context.manifest.skills.source, id), true);
|
|
587
|
+
}
|
|
479
588
|
const next = {
|
|
480
589
|
...context.manifest,
|
|
481
590
|
skills: { ...context.manifest.skills, publish: spec.skills, selection: spec.modes.skills },
|
|
@@ -494,7 +603,11 @@ export async function cmdInstall(args, ctx) {
|
|
|
494
603
|
else {
|
|
495
604
|
delete next.catalog;
|
|
496
605
|
}
|
|
606
|
+
next.speckit = speckit;
|
|
497
607
|
await writeManifest(ctx.root, next);
|
|
608
|
+
if (runSpeckit && !(await installSpeckit(speckitSettings, ctx, backupId))) {
|
|
609
|
+
return 1;
|
|
610
|
+
}
|
|
498
611
|
const sync = await runSync(ctx.root, { check: false });
|
|
499
612
|
await writeInventory(ctx.root);
|
|
500
613
|
await ensureProjectIgnores(ctx.root);
|
|
@@ -502,10 +615,11 @@ export async function cmdInstall(args, ctx) {
|
|
|
502
615
|
const hook = await installPostMergeHook(ctx.root);
|
|
503
616
|
ctx.io.out(`hooks: ${hook.installed ? 'post-merge instalado' : hook.reason}`);
|
|
504
617
|
}
|
|
505
|
-
ctx.io.out(`install: skills=${spec.modes.skills} agents=${spec.modes.agents} rules=${spec.modes.rules} · ${spec.skills.length +
|
|
618
|
+
ctx.io.out(`install: skills=${spec.modes.skills} agents=${spec.modes.agents} rules=${spec.modes.rules} · ${spec.skills.length} skills de proyecto + ${context.engine.commands.length} comandos Spec Kit · ${spec.agents.length} agentes · ${spec.rules.length + 1} reglas · ${materialized.written.length} nuevos · ${materialized.kept.length} conservados · ${sync.written.length} sincronizados`);
|
|
506
619
|
if (!Object.values(spec.modes).includes('full')) {
|
|
507
620
|
ctx.io.out('full: no aplica; usa install --full para expandir el catálogo');
|
|
508
621
|
}
|
|
622
|
+
ctx.io.out(speckitSummary(speckitSettings));
|
|
509
623
|
return 0;
|
|
510
624
|
}
|
|
511
625
|
export async function cmdRepair(args, ctx) {
|
package/dist/cli/run.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CliContext, CliIO, CommandHandler } from './types.js';
|
|
2
|
+
import { type ProcessRunner } from '../speckit/runner.js';
|
|
2
3
|
interface CommandSpec {
|
|
3
4
|
summary: string;
|
|
4
5
|
handler: CommandHandler;
|
|
@@ -6,10 +7,11 @@ interface CommandSpec {
|
|
|
6
7
|
export declare const COMMANDS: Record<string, CommandSpec>;
|
|
7
8
|
export declare const COMMAND_NAMES: string[];
|
|
8
9
|
export declare function printHelp(io: CliIO): void;
|
|
9
|
-
export declare function createContext(cwd: string, io: CliIO): Promise<CliContext>;
|
|
10
|
+
export declare function createContext(cwd: string, io: CliIO, exec?: ProcessRunner): Promise<CliContext>;
|
|
10
11
|
export interface RunCliOptions {
|
|
11
12
|
cwd?: string;
|
|
12
13
|
io?: CliIO;
|
|
14
|
+
exec?: ProcessRunner;
|
|
13
15
|
}
|
|
14
16
|
export declare function runCli(argv: string[], options?: RunCliOptions): Promise<number>;
|
|
15
17
|
export {};
|
package/dist/cli/run.js
CHANGED
|
@@ -14,6 +14,7 @@ import { cmdWorktrees } from './worktrees.js';
|
|
|
14
14
|
import { cliVersion } from './onboarding.js';
|
|
15
15
|
import { sourceCheckout } from './source.js';
|
|
16
16
|
import { processIO } from './types.js';
|
|
17
|
+
import { defaultRunner } from '../speckit/runner.js';
|
|
17
18
|
function printLockCheck(result, ctx) {
|
|
18
19
|
const header = `destinos esperados ${result.expectedCount} / actuales ${result.actualCount} · sources ${result.expectedSourceCount}/${result.actualSourceCount}`;
|
|
19
20
|
const extraCategories = result.breakdown.other > 0 ? `, other ${result.breakdown.other}` : '';
|
|
@@ -136,14 +137,14 @@ export function printHelp(io) {
|
|
|
136
137
|
io.out('Publicación: prepublishOnly ejecuta build, manifest:build y npm test (unit + integración).');
|
|
137
138
|
io.out('Compatibilidad: verify, judge (fuera del catálogo).');
|
|
138
139
|
}
|
|
139
|
-
export async function createContext(cwd, io) {
|
|
140
|
+
export async function createContext(cwd, io, exec = defaultRunner) {
|
|
140
141
|
try {
|
|
141
142
|
const location = resolveAiHubDir(cwd);
|
|
142
|
-
return { cwd, root: location.dir, source: location.source, io };
|
|
143
|
+
return { cwd, root: location.dir, source: location.source, io, exec };
|
|
143
144
|
}
|
|
144
145
|
catch (error) {
|
|
145
146
|
if (error instanceof AiHubDirNotFoundError) {
|
|
146
|
-
return { cwd, root: cwd, source: null, io };
|
|
147
|
+
return { cwd, root: cwd, source: null, io, exec };
|
|
147
148
|
}
|
|
148
149
|
throw error;
|
|
149
150
|
}
|
|
@@ -152,7 +153,7 @@ export async function runCli(argv, options = {}) {
|
|
|
152
153
|
const io = options.io ?? processIO;
|
|
153
154
|
const cwd = options.cwd ?? process.cwd();
|
|
154
155
|
const [command, ...rest] = argv;
|
|
155
|
-
const ctx = await createContext(cwd, io);
|
|
156
|
+
const ctx = await createContext(cwd, io, options.exec);
|
|
156
157
|
if (!command) {
|
|
157
158
|
printHelp(io);
|
|
158
159
|
return 2;
|
package/dist/cli/types.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AiHubSource } from '../paths.js';
|
|
2
|
+
import type { ProcessRunner } from '../speckit/runner.js';
|
|
2
3
|
export interface CliIO {
|
|
3
4
|
out(message: string): void;
|
|
4
5
|
err(message: string): void;
|
|
@@ -8,6 +9,8 @@ export interface CliContext {
|
|
|
8
9
|
root: string;
|
|
9
10
|
source: AiHubSource | null;
|
|
10
11
|
io: CliIO;
|
|
12
|
+
/** Ejecutor de procesos externos (uvx, bash); los tests lo sustituyen. */
|
|
13
|
+
exec: ProcessRunner;
|
|
11
14
|
}
|
|
12
15
|
export interface ParsedArgs {
|
|
13
16
|
positionals: string[];
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -28,15 +28,33 @@ export interface EngineCommand {
|
|
|
28
28
|
exit: string[];
|
|
29
29
|
role: string;
|
|
30
30
|
co_roles?: string[];
|
|
31
|
-
|
|
31
|
+
/** Id canónico del comando en Spec Kit, en notación con punto (`speckit.bug.assess`). */
|
|
32
|
+
speckit: string;
|
|
32
33
|
}
|
|
34
|
+
export type SpeckitScript = 'sh' | 'ps';
|
|
35
|
+
/** Dependencia de Spec Kit fijada por el paquete; un proyecto solo puede restringirla. */
|
|
36
|
+
export interface EngineSpeckit {
|
|
37
|
+
package: string;
|
|
38
|
+
pin: string;
|
|
39
|
+
supported: string;
|
|
40
|
+
excludeNewer: string;
|
|
41
|
+
script: SpeckitScript;
|
|
42
|
+
extensions: Record<string, string>;
|
|
43
|
+
}
|
|
44
|
+
/** Extensiones de Spec Kit que proveen los 8 comandos de assessment y bug fix. */
|
|
45
|
+
export declare const REQUIRED_SPECKIT_EXTENSIONS: readonly ["assess", "bug"];
|
|
33
46
|
export interface EngineManifest {
|
|
34
47
|
version: 2;
|
|
35
48
|
roles: EngineRole[];
|
|
36
49
|
commands: EngineCommand[];
|
|
50
|
+
/** Efecto de cada estado de salida sobre las etiquetas; tabla única proyectada en la regla de enlace. */
|
|
51
|
+
exit_effects: Record<string, string>;
|
|
37
52
|
labels: string[];
|
|
38
53
|
targets: string[];
|
|
54
|
+
speckit: EngineSpeckit;
|
|
39
55
|
}
|
|
56
|
+
/** Estados de salida que el vocabulario normalizado de Spec Kit retiró. */
|
|
57
|
+
export declare const RETIRED_EXIT_STATES: readonly ["ROOT_CAUSE", "NEEDS_DATA", "FIXED", "RESOLVED", "OPEN", "DRAFT", "READY"];
|
|
40
58
|
export declare class EngineError extends Error {
|
|
41
59
|
readonly errors: string[];
|
|
42
60
|
constructor(errors: string[]);
|
package/dist/engine/engine.js
CHANGED
|
@@ -3,12 +3,17 @@ import path from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Ajv } from 'ajv';
|
|
5
5
|
import { parse } from 'yaml';
|
|
6
|
+
import { satisfiesRange } from '../speckit/version.js';
|
|
6
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const ENGINE_PATH = path.join(__dirname, '..', '..', 'engine', 'engine.yaml');
|
|
8
9
|
const SCHEMA_PATH = path.join(__dirname, '..', '..', 'schema', 'engine.schema.json');
|
|
9
10
|
export const ENGINE_SKILLS_DIR = path.join(__dirname, '..', '..', 'skills', 'shared');
|
|
10
11
|
export const ENGINE_COUNTS = { roles: 8, commands: 17, labels: 7 };
|
|
11
12
|
export const ENGINE_LANES = { assessment: 5, feature: 9, bug: 3 };
|
|
13
|
+
/** Extensiones de Spec Kit que proveen los 8 comandos de assessment y bug fix. */
|
|
14
|
+
export const REQUIRED_SPECKIT_EXTENSIONS = ['assess', 'bug'];
|
|
15
|
+
/** Estados de salida que el vocabulario normalizado de Spec Kit retiró. */
|
|
16
|
+
export const RETIRED_EXIT_STATES = ['ROOT_CAUSE', 'NEEDS_DATA', 'FIXED', 'RESOLVED', 'OPEN', 'DRAFT', 'READY'];
|
|
12
17
|
export class EngineError extends Error {
|
|
13
18
|
errors;
|
|
14
19
|
constructor(errors) {
|
|
@@ -104,6 +109,53 @@ export function crossValidate(engine) {
|
|
|
104
109
|
errors.push(`lane "${lane}" debe tener ${count} comandos (tiene ${actual})`);
|
|
105
110
|
}
|
|
106
111
|
}
|
|
112
|
+
for (const id of findDuplicateIds(engine.commands.map((command) => command.speckit))) {
|
|
113
|
+
errors.push(`id de Spec Kit duplicado: ${id}`);
|
|
114
|
+
}
|
|
115
|
+
const retired = new Set(RETIRED_EXIT_STATES);
|
|
116
|
+
const states = new Set();
|
|
117
|
+
for (const command of engine.commands) {
|
|
118
|
+
for (const state of command.exit) {
|
|
119
|
+
states.add(state);
|
|
120
|
+
if (retired.has(state)) {
|
|
121
|
+
errors.push(`commands "${command.id}".exit usa el estado retirado ${state}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const effects = engine.exit_effects ?? {};
|
|
126
|
+
for (const state of [...states].sort()) {
|
|
127
|
+
if (!(state in effects)) {
|
|
128
|
+
errors.push(`exit_effects no define el efecto de ${state}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const state of Object.keys(effects).sort()) {
|
|
132
|
+
if (!states.has(state)) {
|
|
133
|
+
errors.push(`exit_effects.${state} no corresponde a ningún estado de salida`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
errors.push(...validateEngineSpeckit(engine.speckit));
|
|
137
|
+
return errors;
|
|
138
|
+
}
|
|
139
|
+
function validateEngineSpeckit(speckit) {
|
|
140
|
+
if (speckit === undefined) {
|
|
141
|
+
return ['falta el bloque speckit'];
|
|
142
|
+
}
|
|
143
|
+
const errors = [];
|
|
144
|
+
for (const field of ['package', 'pin', 'supported', 'excludeNewer', 'script']) {
|
|
145
|
+
if (typeof speckit[field] !== 'string' || speckit[field] === '') {
|
|
146
|
+
errors.push(`falta speckit.${field}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
for (const extension of REQUIRED_SPECKIT_EXTENSIONS) {
|
|
150
|
+
if (typeof speckit.extensions?.[extension] !== 'string') {
|
|
151
|
+
errors.push(`falta speckit.extensions.${extension}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (typeof speckit.pin === 'string' &&
|
|
155
|
+
typeof speckit.supported === 'string' &&
|
|
156
|
+
!satisfiesRange(speckit.pin, speckit.supported)) {
|
|
157
|
+
errors.push(`speckit.pin ${speckit.pin} está fuera de speckit.supported "${speckit.supported}"`);
|
|
158
|
+
}
|
|
107
159
|
return errors;
|
|
108
160
|
}
|
|
109
161
|
export function commandById(engine, id) {
|
package/dist/engine/gates.d.ts
CHANGED
|
@@ -1,2 +1,18 @@
|
|
|
1
1
|
export declare function scanLegacyArtifacts(root: string): Promise<string[]>;
|
|
2
2
|
export declare function scanCoupling(root: string, terms: string[]): Promise<string[]>;
|
|
3
|
+
/** Prefijos que Spec Kit reserva en los directorios de comandos de cada destino. */
|
|
4
|
+
export declare const SPECKIT_RESERVED_PREFIXES: readonly ["speckit-", "speckit."];
|
|
5
|
+
/**
|
|
6
|
+
* Gate del catálogo publicado: ninguna skill, agente o regla puede proyectarse con un nombre
|
|
7
|
+
* del espacio reservado de Spec Kit (`speckit-*` / `speckit.*`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function scanReservedNamespace(dirs?: {
|
|
10
|
+
skills: string;
|
|
11
|
+
agents: string;
|
|
12
|
+
rules: string;
|
|
13
|
+
}): Promise<string[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Gate del vocabulario de estados retirado (`ROOT_CAUSE`, `FIXED`, `READY`…): devuelve
|
|
16
|
+
* `ruta:línea vocabulario obsoleto "TOKEN"` relativo a `root`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function scanRetiredStates(files: string[] | undefined, root: string): Promise<string[]>;
|
package/dist/engine/gates.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isEngineCheckout } from '../source-checkout.js';
|
|
4
|
-
import { ENGINE_AGENTS_DIR, catalogIndex } from '../catalog.js';
|
|
4
|
+
import { ENGINE_AGENTS_DIR, ENGINE_RULES_DIR, catalogIndex } from '../catalog.js';
|
|
5
5
|
import { ENGINE_SKILLS_DIR } from './engine.js';
|
|
6
6
|
const LEGACY_SOURCE_DIRS = ['core/presets', 'core/skills', 'core/agents'];
|
|
7
7
|
const LEGACY_TARGET_DIRS = [
|
|
@@ -129,3 +129,104 @@ export async function scanCoupling(root, terms) {
|
|
|
129
129
|
}
|
|
130
130
|
return findings.sort();
|
|
131
131
|
}
|
|
132
|
+
/** Prefijos que Spec Kit reserva en los directorios de comandos de cada destino. */
|
|
133
|
+
export const SPECKIT_RESERVED_PREFIXES = ['speckit-', 'speckit.'];
|
|
134
|
+
function reservedName(name) {
|
|
135
|
+
return SPECKIT_RESERVED_PREFIXES.some((prefix) => name.startsWith(prefix));
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Gate del catálogo publicado: ninguna skill, agente o regla puede proyectarse con un nombre
|
|
139
|
+
* del espacio reservado de Spec Kit (`speckit-*` / `speckit.*`).
|
|
140
|
+
*/
|
|
141
|
+
export async function scanReservedNamespace(dirs = {
|
|
142
|
+
skills: ENGINE_SKILLS_DIR,
|
|
143
|
+
agents: ENGINE_AGENTS_DIR,
|
|
144
|
+
rules: ENGINE_RULES_DIR,
|
|
145
|
+
}) {
|
|
146
|
+
const findings = [];
|
|
147
|
+
for (const entry of await fs.readdir(dirs.skills, { withFileTypes: true }).catch(() => [])) {
|
|
148
|
+
if (entry.isDirectory() && reservedName(entry.name)) {
|
|
149
|
+
findings.push(`skills/shared/${entry.name} (espacio reservado de Spec Kit)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
for (const [kind, dir] of [['agents', dirs.agents], ['rules', dirs.rules]]) {
|
|
153
|
+
for (const name of await listNames(dir)) {
|
|
154
|
+
if (reservedName(path.basename(name))) {
|
|
155
|
+
findings.push(`${kind}/${name} (espacio reservado de Spec Kit)`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return findings.sort();
|
|
160
|
+
}
|
|
161
|
+
async function listNames(dir, prefix = '') {
|
|
162
|
+
const names = [];
|
|
163
|
+
for (const entry of await fs.readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
164
|
+
const rel = prefix === '' ? entry.name : `${prefix}/${entry.name}`;
|
|
165
|
+
if (entry.isDirectory()) {
|
|
166
|
+
names.push(...(await listNames(path.join(dir, entry.name), rel)));
|
|
167
|
+
}
|
|
168
|
+
else if (entry.isFile()) {
|
|
169
|
+
names.push(rel);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return names;
|
|
173
|
+
}
|
|
174
|
+
/** Estados retirados que solo existían como estado de salida: se marcan en cualquier contexto. */
|
|
175
|
+
const RETIRED_ALWAYS = ['ROOT_CAUSE', 'NEEDS_DATA'];
|
|
176
|
+
/** Estados retirados que también son palabras corrientes: se marcan solo usados como estado. */
|
|
177
|
+
const RETIRED_IN_STATE_CONTEXT = ['FIXED', 'RESOLVED', 'OPEN', 'DRAFT', 'READY'];
|
|
178
|
+
const STATE_CONTEXT = /\b(estados?|exit|exits|termina(n)? en|ends? in|outcome|veredicto|verdict)\b/i;
|
|
179
|
+
function retiredInLine(line) {
|
|
180
|
+
const found = [];
|
|
181
|
+
const words = new Set(line.split(/[^A-Za-z0-9_-]+/));
|
|
182
|
+
for (const token of RETIRED_ALWAYS) {
|
|
183
|
+
if (words.has(token)) {
|
|
184
|
+
found.push(token);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
for (const token of RETIRED_IN_STATE_CONTEXT) {
|
|
188
|
+
const quoted = line.includes(`\`${token}\``);
|
|
189
|
+
const inContext = STATE_CONTEXT.test(line) && words.has(token);
|
|
190
|
+
if (quoted || inContext) {
|
|
191
|
+
found.push(token);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return found;
|
|
195
|
+
}
|
|
196
|
+
/** Archivos que el gate revisa en el checkout del motor: motor, documentación y catálogo publicado. */
|
|
197
|
+
async function retiredStateFiles(root) {
|
|
198
|
+
const files = [
|
|
199
|
+
path.join(root, 'core', 'engine', 'engine.yaml'),
|
|
200
|
+
path.join(root, 'AGENTS.md'),
|
|
201
|
+
path.join(root, 'README.md'),
|
|
202
|
+
path.join(root, 'core', 'README.md'),
|
|
203
|
+
];
|
|
204
|
+
for (const dir of ['core/rules', 'core/agents', 'core/skills/shared']) {
|
|
205
|
+
for (const rel of await listNames(path.join(root, ...dir.split('/')))) {
|
|
206
|
+
if (TEXT_FILE.test(rel)) {
|
|
207
|
+
files.push(path.join(root, ...dir.split('/'), ...rel.split('/')));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return files;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Gate del vocabulario de estados retirado (`ROOT_CAUSE`, `FIXED`, `READY`…): devuelve
|
|
215
|
+
* `ruta:línea vocabulario obsoleto "TOKEN"` relativo a `root`.
|
|
216
|
+
*/
|
|
217
|
+
export async function scanRetiredStates(files, root) {
|
|
218
|
+
const findings = [];
|
|
219
|
+
for (const file of files ?? (await retiredStateFiles(root))) {
|
|
220
|
+
const text = await fs.readFile(file, 'utf8').catch(() => null);
|
|
221
|
+
if (text === null) {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
225
|
+
text.split(/\r?\n/).forEach((line, index) => {
|
|
226
|
+
for (const token of retiredInLine(line)) {
|
|
227
|
+
findings.push(`${rel}:${index + 1} vocabulario obsoleto "${token}"`);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return findings;
|
|
232
|
+
}
|
|
@@ -49,5 +49,11 @@ export interface ResumeSummary {
|
|
|
49
49
|
done: number;
|
|
50
50
|
};
|
|
51
51
|
state: ProjectState | null;
|
|
52
|
+
/** Feature de Spec Kit activa y de dónde salió: `state` (versionado) o `local` (.specify/feature.json). */
|
|
53
|
+
feature: {
|
|
54
|
+
dir: string;
|
|
55
|
+
source: 'state' | 'local';
|
|
56
|
+
pending: number;
|
|
57
|
+
} | null;
|
|
52
58
|
}
|
|
53
59
|
export declare function resumeLedger(root: string): Promise<ResumeSummary>;
|