@dsalazardev/ai-hub-cli 0.1.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.
Files changed (91) hide show
  1. package/README.md +63 -4
  2. package/dist/backup/snapshots.js +12 -0
  3. package/dist/catalog.d.ts +83 -0
  4. package/dist/catalog.js +350 -0
  5. package/dist/cli/args.js +1 -0
  6. package/dist/cli/context.js +5 -0
  7. package/dist/cli/doctor.js +105 -14
  8. package/dist/cli/items.d.ts +1 -1
  9. package/dist/cli/items.js +1 -1
  10. package/dist/cli/maintenance.js +105 -51
  11. package/dist/cli/memory.js +9 -0
  12. package/dist/cli/onboarding.d.ts +33 -1
  13. package/dist/cli/onboarding.js +450 -54
  14. package/dist/cli/run.d.ts +3 -1
  15. package/dist/cli/run.js +10 -7
  16. package/dist/cli/types.d.ts +3 -0
  17. package/dist/concurrency.d.ts +5 -0
  18. package/dist/concurrency.js +23 -0
  19. package/dist/engine/engine.d.ts +19 -1
  20. package/dist/engine/engine.js +52 -0
  21. package/dist/engine/gates.d.ts +16 -0
  22. package/dist/engine/gates.js +154 -15
  23. package/dist/harness/memory/ledger.d.ts +6 -0
  24. package/dist/harness/memory/ledger.js +44 -2
  25. package/dist/manifest/manifest.d.ts +20 -1
  26. package/dist/manifest/manifest.js +35 -11
  27. package/dist/presets/materialize.d.ts +14 -1
  28. package/dist/presets/materialize.js +132 -11
  29. package/dist/speckit/binding.d.ts +15 -0
  30. package/dist/speckit/binding.js +66 -0
  31. package/dist/speckit/commands.d.ts +46 -0
  32. package/dist/speckit/commands.js +131 -0
  33. package/dist/speckit/doctor.d.ts +14 -0
  34. package/dist/speckit/doctor.js +103 -0
  35. package/dist/speckit/runner.d.ts +19 -0
  36. package/dist/speckit/runner.js +22 -0
  37. package/dist/speckit/seal.d.ts +12 -0
  38. package/dist/speckit/seal.js +47 -0
  39. package/dist/speckit/settings.d.ts +36 -0
  40. package/dist/speckit/settings.js +75 -0
  41. package/dist/speckit/state.d.ts +59 -0
  42. package/dist/speckit/state.js +279 -0
  43. package/dist/speckit/version.d.ts +8 -0
  44. package/dist/speckit/version.js +53 -0
  45. package/dist/sync/config.js +3 -2
  46. package/dist/sync/render.js +35 -1
  47. package/dist/sync/sync.d.ts +4 -0
  48. package/dist/sync/sync.js +68 -25
  49. package/dist/sync/targets.d.ts +13 -0
  50. package/dist/sync/targets.js +5 -0
  51. package/engine/engine.yaml +65 -31
  52. package/manifest.json +28 -47
  53. package/package.json +2 -1
  54. package/presets/angular-express/preset.yaml +2 -4
  55. package/presets/base/preset.yaml +1 -3
  56. package/presets/generic-ts/preset.yaml +1 -3
  57. package/presets/python/preset.yaml +1 -3
  58. package/rules/skill-quality/skill-behavior-rubric.md +63 -0
  59. package/rules/skill-quality/skill-harden-rubric.md +102 -0
  60. package/rules/skill-quality/skill-migration-brief.md +200 -0
  61. package/rules/skill-quality/skill-rubric.md +40 -0
  62. package/schema/engine.schema.json +22 -3
  63. package/schema/manifest.schema.json +22 -0
  64. package/skills/shared/analyze/SKILL.md +2 -1
  65. package/skills/shared/clarify/SKILL.md +2 -1
  66. package/skills/shared/implement/SKILL.md +2 -1
  67. package/skills/shared/plan/SKILL.md +2 -1
  68. package/skills/shared/review/SKILL.md +2 -1
  69. package/skills/shared/sdd/SKILL.md +2 -1
  70. package/skills/shared/sdd-init/SKILL.md +2 -1
  71. package/skills/shared/ship/SKILL.md +2 -1
  72. package/skills/shared/specify/SKILL.md +2 -1
  73. package/skills/shared/tasks/SKILL.md +2 -1
  74. package/skills/shared/verify/SKILL.md +2 -1
  75. package/skills/shared/speckit-analyze/SKILL.md +0 -35
  76. package/skills/shared/speckit-assess-decide/SKILL.md +0 -36
  77. package/skills/shared/speckit-assess-define/SKILL.md +0 -36
  78. package/skills/shared/speckit-assess-intake/SKILL.md +0 -38
  79. package/skills/shared/speckit-assess-research/SKILL.md +0 -36
  80. package/skills/shared/speckit-assess-shape/SKILL.md +0 -36
  81. package/skills/shared/speckit-bug-assess/SKILL.md +0 -38
  82. package/skills/shared/speckit-bug-fix/SKILL.md +0 -37
  83. package/skills/shared/speckit-bug-test/SKILL.md +0 -37
  84. package/skills/shared/speckit-checklist/SKILL.md +0 -35
  85. package/skills/shared/speckit-clarify/SKILL.md +0 -32
  86. package/skills/shared/speckit-constitution/SKILL.md +0 -35
  87. package/skills/shared/speckit-converge/SKILL.md +0 -36
  88. package/skills/shared/speckit-implement/SKILL.md +0 -32
  89. package/skills/shared/speckit-plan/SKILL.md +0 -37
  90. package/skills/shared/speckit-specify/SKILL.md +0 -36
  91. package/skills/shared/speckit-tasks/SKILL.md +0 -34
@@ -4,14 +4,20 @@ 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 { materializePreset } 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';
11
11
  import { emptyState, migrateLegacyState, readState, StateNotFoundError, writeState } from '../harness/memory/state.js';
12
12
  import { runSync } from '../sync/sync.js';
13
- import { TOOL_IDS, assertToolIds } from '../sync/targets.js';
13
+ import { TOOL_IDS, assertToolIds, TARGETS } from '../sync/targets.js';
14
14
  import { sha256 } from '../sync/lock.js';
15
+ import { isEngineCheckout } from '../source-checkout.js';
16
+ import { ENGINE_AGENTS_DIR, ENGINE_RULES_DIR, catalogIndex, catalogSeal, expectedRuleIds, hasFullDimension, listCatalogAgents, listCatalogSkills, } from '../catalog.js';
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';
15
21
  import { flagList, flagString } from './args.js';
16
22
  const RC_NAME = '.ai-hubrc.json';
17
23
  export async function cliVersion() {
@@ -19,6 +25,71 @@ export async function cliVersion() {
19
25
  const pkg = JSON.parse(raw);
20
26
  return typeof pkg.version === 'string' ? pkg.version : '0.0.0';
21
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
+ }
22
93
  function projectId(root) {
23
94
  const base = path.basename(path.resolve(root)).toLowerCase();
24
95
  const id = base.replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
@@ -27,9 +98,104 @@ function projectId(root) {
27
98
  async function exists(file) {
28
99
  return fs.access(file).then(() => true, () => false);
29
100
  }
30
- function manifestFromPreset(id, preset, targets, profilePackageManager, guardMode, includeMcp) {
31
- const rules = [...new Set(preset.rules.map((rule) => path.basename(rule, '.md')))];
101
+ function overallMode(modes) {
102
+ return modes.skills === modes.agents && modes.agents === modes.rules ? modes.skills : 'custom';
103
+ }
104
+ function parseExclusions(args) {
105
+ const skills = new Set();
106
+ const agents = new Set();
107
+ const rules = new Set();
108
+ for (const item of flagList(args, 'exclude')) {
109
+ const separator = item.indexOf(':');
110
+ const kind = separator >= 0 ? item.slice(0, separator) : 'skill';
111
+ const id = separator >= 0 ? item.slice(separator + 1) : item;
112
+ if (id === '') {
113
+ continue;
114
+ }
115
+ if (kind === 'agent') {
116
+ agents.add(id);
117
+ }
118
+ else if (kind === 'rule') {
119
+ if (id === 'core' || id === 'preflight') {
120
+ throw new Error(`la regla base "${id}" no se puede excluir`);
121
+ }
122
+ rules.add(id);
123
+ }
124
+ else {
125
+ skills.add(id);
126
+ }
127
+ }
128
+ return { skills, agents, rules };
129
+ }
130
+ function filterRules(rules, exclusions) {
131
+ return rules.filter((id) => {
132
+ for (const excluded of exclusions) {
133
+ if (excluded === id || (excluded === 'skill-quality' && id.startsWith('skill-quality/'))) {
134
+ return false;
135
+ }
136
+ }
137
+ return true;
138
+ });
139
+ }
140
+ async function buildSelection(args, preset, engine, existing) {
141
+ const fullFlag = Boolean(args.flags.full);
142
+ const presetOnly = Boolean(args.flags['preset-only']);
143
+ if (fullFlag && presetOnly) {
144
+ throw new Error('--full y --preset-only son incompatibles');
145
+ }
146
+ const exclusions = parseExclusions(args);
147
+ const hasExclusions = exclusions.skills.size > 0 || exclusions.agents.size > 0 || exclusions.rules.size > 0;
148
+ if (existing && !fullFlag && !presetOnly && !hasExclusions) {
149
+ const retired = new Set(await retiredCatalogSkills(existing.skills.publish));
150
+ return {
151
+ modes: {
152
+ skills: existing.skills.selection ?? 'preset',
153
+ agents: existing.agents.selection ?? 'preset',
154
+ rules: existing.rules.selection ?? 'preset',
155
+ },
156
+ skills: existing.skills.publish.filter((id) => !retired.has(id)),
157
+ agents: [...existing.agents.publish],
158
+ rules: [...existing.rules.publish],
159
+ };
160
+ }
161
+ const catalogSkills = await listCatalogSkills(engine);
162
+ const catalogAgents = await listCatalogAgents();
163
+ const defaultMode = presetOnly
164
+ ? 'preset'
165
+ : fullFlag
166
+ ? 'full'
167
+ : existing
168
+ ? (existing.skills.selection ?? 'preset')
169
+ : 'full';
170
+ const modes = {
171
+ skills: exclusions.skills.size > 0 ? 'custom' : defaultMode,
172
+ agents: exclusions.agents.size > 0 ? 'custom' : defaultMode,
173
+ rules: exclusions.rules.size > 0 ? 'custom' : defaultMode,
174
+ };
175
+ const skillsBase = modes.skills === 'preset'
176
+ ? [...preset.skills]
177
+ : modes.skills === 'custom' && existing
178
+ ? [...existing.skills.publish]
179
+ : [...catalogSkills];
180
+ const agentsBase = modes.agents === 'preset'
181
+ ? [...preset.agents]
182
+ : modes.agents === 'custom' && existing
183
+ ? [...existing.agents.publish]
184
+ : [...catalogAgents];
185
+ const rulesBase = modes.rules === 'preset'
186
+ ? expectedRuleIds('preset', preset)
187
+ : modes.rules === 'custom' && existing
188
+ ? [...existing.rules.publish]
189
+ : expectedRuleIds('full', preset);
32
190
  return {
191
+ modes,
192
+ skills: [...new Set(skillsBase.filter((id) => !exclusions.skills.has(id)))].sort(),
193
+ agents: [...new Set(agentsBase.filter((id) => !exclusions.agents.has(id)))].sort(),
194
+ rules: [...new Set(filterRules(rulesBase, exclusions.rules))].sort(),
195
+ };
196
+ }
197
+ function manifestFromSelection(id, preset, targets, profilePackageManager, guardMode, includeMcp, selection, speckit) {
198
+ const manifest = {
33
199
  version: 2,
34
200
  project: { id, name: id },
35
201
  preset: { id: preset.id, version: preset.version, source: 'package' },
@@ -49,22 +215,113 @@ function manifestFromPreset(id, preset, targets, profilePackageManager, guardMod
49
215
  ? { dangerPatterns: preset.policy.dangerPatterns }
50
216
  : {}),
51
217
  },
52
- skills: { source: '.ai-hub/skills', publish: preset.skills },
53
- agents: { source: '.ai-hub/agents', publish: preset.agents },
54
- rules: { source: '.ai-hub/rules', publish: rules },
55
- ...(includeMcp ? { mcp: { source: preset.mcp.source } } : {}),
218
+ skills: { source: '.ai-hub/skills', publish: selection.skills, selection: selection.modes.skills },
219
+ agents: { source: '.ai-hub/agents', publish: selection.agents, selection: selection.modes.agents },
220
+ rules: { source: '.ai-hub/rules', publish: selection.rules, selection: selection.modes.rules },
221
+ ...(includeMcp ? { mcp: { source: 'mcp-manifest.yaml' } } : {}),
222
+ speckit,
223
+ };
224
+ if (hasFullDimension(manifest)) {
225
+ manifest.catalog = catalogSeal(manifest);
226
+ }
227
+ return manifest;
228
+ }
229
+ function mergeMaterialized(target, source) {
230
+ target.written.push(...source.written);
231
+ target.kept.push(...source.kept);
232
+ target.collisions.push(...source.collisions);
233
+ }
234
+ async function materializeSelection(root, preset, selection) {
235
+ const result = { written: [], kept: [], collisions: [] };
236
+ mergeMaterialized(result, await materializePreset(root, preset, selection.rules));
237
+ if (selection.modes.rules !== 'preset') {
238
+ mergeMaterialized(result, await materializeUniversalRules(root, preset, selection.rules));
239
+ }
240
+ if (selection.modes.skills !== 'preset' || selection.modes.agents !== 'preset') {
241
+ const presetSkills = new Set(preset.skills);
242
+ const presetAgents = new Set(preset.agents);
243
+ mergeMaterialized(result, await materializeCatalog(root, selection.modes.skills === 'preset'
244
+ ? []
245
+ : selection.skills.filter((id) => !presetSkills.has(id)), selection.modes.agents === 'preset'
246
+ ? []
247
+ : selection.agents.filter((id) => !presetAgents.has(id))));
248
+ }
249
+ return result;
250
+ }
251
+ async function buildAssetPlan(selection, engine, targets, includeMcp) {
252
+ const index = await catalogIndex();
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)];
256
+ const projectSkillFiles = selection.skills.reduce((total, id) => total + (index.skillFiles[id]?.length ?? 0), 0);
257
+ const skillFiles = allSkills.reduce((total, id) => total + (index.skillFiles[id]?.length ?? 0), 0);
258
+ let destinations = 1; // opencode.json
259
+ for (const tool of targets) {
260
+ const target = TARGETS[tool];
261
+ destinations += target.skillShape === 'file' ? allSkills.length : skillFiles;
262
+ if (target.agents) {
263
+ destinations += selection.agents.length;
264
+ }
265
+ destinations += selection.rules.length + 2; // reglas materializadas + preflight + regla de enlace de Spec Kit
266
+ }
267
+ if (includeMcp) {
268
+ destinations += 3;
269
+ }
270
+ const materializedFiles = projectSkillFiles + selection.agents.length + selection.rules.length + 2;
271
+ const files = materializedFiles + destinations;
272
+ let bytes = 0;
273
+ const stat = async (file) => fs.stat(file).then((value) => value.size, () => 0);
274
+ for (const id of allSkills) {
275
+ for (const file of index.skillFiles[id] ?? []) {
276
+ bytes += await stat(file);
277
+ }
278
+ }
279
+ for (const id of selection.agents) {
280
+ bytes += await stat(path.join(ENGINE_AGENTS_DIR, `${id}.md`));
281
+ }
282
+ for (const id of selection.rules) {
283
+ if (id.startsWith('skill-quality/')) {
284
+ bytes += await stat(path.join(ENGINE_RULES_DIR, `${id}.md`));
285
+ }
286
+ }
287
+ return {
288
+ selection: overallMode(selection.modes),
289
+ selections: selection.modes,
290
+ skills: selection.skills,
291
+ agents: selection.agents,
292
+ rules: selection.rules,
293
+ destinations,
294
+ files,
295
+ bytes,
56
296
  };
57
297
  }
58
298
  export async function writeManifest(root, manifest) {
59
299
  await fs.writeFile(path.join(root, 'manifest.yaml'), stringify(manifest), 'utf8');
60
300
  }
301
+ const GENERATED_IGNORES = [
302
+ '.worktrees/',
303
+ '.claude/',
304
+ '.opencode/',
305
+ '.pi/',
306
+ '.cursor/',
307
+ '.github/skills/',
308
+ '.github/agents/',
309
+ '.github/instructions/',
310
+ '.mcp.json',
311
+ '.cursor/mcp.json',
312
+ '.vscode/mcp.json',
313
+ 'opencode.json',
314
+ ];
61
315
  async function ensureProjectIgnores(root) {
62
316
  const file = path.join(root, '.gitignore');
63
317
  const current = await fs.readFile(file, 'utf8').catch(() => '');
64
- if (!current.split(/\r?\n/).includes('.worktrees/')) {
65
- const next = current === '' ? '.worktrees/\n' : `${current.replace(/\s*$/, '')}\n.worktrees/\n`;
66
- await fs.writeFile(file, next, 'utf8');
318
+ const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
319
+ const missing = GENERATED_IGNORES.filter((entry) => !present.has(entry));
320
+ if (missing.length === 0) {
321
+ return;
67
322
  }
323
+ const base = current === '' ? '' : `${current.replace(/\s*$/, '')}\n`;
324
+ await fs.writeFile(file, `${base}${missing.join('\n')}\n`, 'utf8');
68
325
  }
69
326
  async function installPostMergeHook(root) {
70
327
  if (!(await exists(path.join(root, '.git')))) {
@@ -102,9 +359,13 @@ async function resolveSelection(args, ctx) {
102
359
  };
103
360
  }
104
361
  export async function cmdInit(args, ctx) {
362
+ const sourceCheckout = await isEngineCheckout(ctx.root);
105
363
  let selection;
106
364
  try {
107
365
  selection = await resolveSelection(args, ctx);
366
+ if (selection.migrate && !sourceCheckout) {
367
+ throw new Error('--migrate solo aplica en el checkout de desarrollo; la instalación no tiene .planning/');
368
+ }
108
369
  }
109
370
  catch (error) {
110
371
  ctx.io.err(error.message);
@@ -118,7 +379,29 @@ export async function cmdInit(args, ctx) {
118
379
  targets: selection.targets,
119
380
  guardMode: 'shadow',
120
381
  };
121
- const write = [RC_NAME, 'manifest.yaml', '.ai-hub/'];
382
+ const engine = await loadEngine();
383
+ let existing;
384
+ if (await exists(path.join(ctx.root, 'manifest.yaml'))) {
385
+ existing = (await loadManifestContext(ctx.root).catch(() => null))?.manifest;
386
+ }
387
+ let spec;
388
+ try {
389
+ spec = await buildSelection(args, selection.preset, engine, existing);
390
+ }
391
+ catch (error) {
392
+ ctx.io.err(error.message);
393
+ return 2;
394
+ }
395
+ const includeMcp = await exists(path.join(ctx.root, 'mcp-manifest.yaml'));
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;
401
+ const write = [RC_NAME, 'manifest.yaml', '.gitignore', '.ai-hub/'];
402
+ if (runSpeckit) {
403
+ write.push('.specify/');
404
+ }
122
405
  if (selection.migrate) {
123
406
  write.push('.planning/state.json');
124
407
  }
@@ -127,55 +410,133 @@ export async function cmdInit(args, ctx) {
127
410
  project: id,
128
411
  preset: selection.preset.id,
129
412
  targets: selection.targets,
413
+ ...assets,
414
+ skillCount: spec.skills.length,
415
+ speckitCommands: engine.commands.length,
416
+ agentCount: spec.agents.length,
417
+ ruleCount: spec.rules.length + 1,
418
+ mcp: includeMcp,
130
419
  migrate: selection.migrate,
131
420
  write,
421
+ speckit: speckitPlan(speckitSettings),
132
422
  };
133
423
  const plan = { ...planBody, hash: sha256(JSON.stringify(planBody)) };
134
424
  if (!args.flags.yes || args.flags['dry-run']) {
135
- ctx.io.out(JSON.stringify(plan, null, 2));
425
+ const environment = await environmentFor(speckitSettings, ctx);
426
+ ctx.io.out(JSON.stringify({ ...plan, environment }, null, 2));
136
427
  if (!args.flags['dry-run']) {
137
428
  ctx.io.out('Nada escrito. Repite con --yes para aplicar este plan.');
138
429
  }
139
430
  return 0;
140
431
  }
141
- if (await exists(path.join(ctx.root, 'manifest.yaml'))) {
142
- await createBackup(ctx.root, 'init');
432
+ if (runSpeckit && !(await preflightOrReport(speckitSettings, ctx))) {
433
+ return 2;
143
434
  }
144
- const materialized = await materializePreset(ctx.root, selection.preset);
145
- const includeMcp = await exists(path.join(ctx.root, selection.preset.mcp.source));
146
- const manifest = manifestFromPreset(id, selection.preset, selection.targets, selection.profilePackageManager, rc.guardMode ?? 'shadow', includeMcp);
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;
439
+ const materialized = await materializeSelection(ctx.root, selection.preset, spec);
440
+ const manifest = manifestFromSelection(id, selection.preset, selection.targets, selection.profilePackageManager, rc.guardMode ?? 'shadow', includeMcp, spec, speckit);
147
441
  await writeManifest(ctx.root, manifest);
148
442
  await writeInventory(ctx.root);
443
+ if (runSpeckit && !(await installSpeckit(speckitSettings, ctx, backupId))) {
444
+ return 1;
445
+ }
446
+ const sync = await runSync(ctx.root, { check: false });
149
447
  const existingRc = readAiHubRc(ctx.cwd);
150
448
  if (!existingRc || existingRc.version === 1 || args.flags.force) {
151
449
  writeAiHubRc(ctx.cwd, rc);
152
450
  }
153
- try {
154
- await readState(ctx.root);
155
- }
156
- catch (error) {
157
- if (error instanceof StateNotFoundError) {
158
- await writeState(ctx.root, emptyState(id, selection.preset.id));
451
+ if (sourceCheckout) {
452
+ try {
453
+ await readState(ctx.root);
159
454
  }
160
- else {
161
- throw error;
455
+ catch (error) {
456
+ if (error instanceof StateNotFoundError) {
457
+ await writeState(ctx.root, emptyState(id, selection.preset.id));
458
+ }
459
+ else {
460
+ throw error;
461
+ }
462
+ }
463
+ if (selection.migrate) {
464
+ await migrateLegacyState(ctx.root, id, selection.preset.id);
162
465
  }
163
- }
164
- if (selection.migrate) {
165
- await migrateLegacyState(ctx.root, id, selection.preset.id);
166
466
  }
167
467
  await ensureProjectIgnores(ctx.root);
168
468
  if (args.flags.hooks) {
169
469
  const hook = await installPostMergeHook(ctx.root);
170
470
  ctx.io.out(`hooks: ${hook.installed ? 'post-merge instalado' : hook.reason}`);
171
471
  }
172
- ctx.io.out(`init: ${selection.preset.id} · ${selection.targets.join(', ')} · ${materialized.written.length} archivos nuevos, ${materialized.kept.length} conservados, ${materialized.collisions.length} colisiones`);
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`);
173
473
  if (materialized.collisions.length > 0) {
174
474
  ctx.io.err(`colisiones (no se sobrescribieron): ${materialized.collisions.join(', ')}`);
175
475
  }
476
+ ctx.io.out(speckitSummary(speckitSettings));
176
477
  ctx.io.out(`plan: ${plan.hash}`);
177
478
  return 0;
178
479
  }
480
+ async function selectionForInstall(args, manifest, preset, engine) {
481
+ const fullFlag = Boolean(args.flags.full);
482
+ const presetOnly = Boolean(args.flags['preset-only']);
483
+ if (fullFlag && presetOnly) {
484
+ throw new Error('--full y --preset-only son incompatibles');
485
+ }
486
+ const exclusions = parseExclusions(args);
487
+ const modes = {
488
+ skills: exclusions.skills.size > 0
489
+ ? 'custom'
490
+ : presetOnly
491
+ ? 'preset'
492
+ : fullFlag
493
+ ? 'full'
494
+ : (manifest.skills.selection ?? 'preset'),
495
+ agents: exclusions.agents.size > 0
496
+ ? 'custom'
497
+ : presetOnly
498
+ ? 'preset'
499
+ : fullFlag
500
+ ? 'full'
501
+ : (manifest.agents.selection ?? 'preset'),
502
+ rules: exclusions.rules.size > 0
503
+ ? 'custom'
504
+ : presetOnly
505
+ ? 'preset'
506
+ : fullFlag
507
+ ? 'full'
508
+ : (manifest.rules.selection ?? 'preset'),
509
+ };
510
+ const catalogSkills = await listCatalogSkills(engine);
511
+ const catalogAgents = await listCatalogAgents();
512
+ const skills = modes.skills === 'full'
513
+ ? catalogSkills
514
+ : modes.skills === 'preset'
515
+ ? [...preset.skills]
516
+ : [...manifest.skills.publish];
517
+ const agents = modes.agents === 'full'
518
+ ? catalogAgents
519
+ : modes.agents === 'preset'
520
+ ? [...preset.agents]
521
+ : [...manifest.agents.publish];
522
+ const rulesBase = modes.rules === 'full'
523
+ ? expectedRuleIds('full', preset)
524
+ : modes.rules === 'preset'
525
+ ? expectedRuleIds('preset', preset)
526
+ : [...manifest.rules.publish];
527
+ const retired = new Set(await retiredCatalogSkills(skills));
528
+ return {
529
+ modes,
530
+ skills: [...new Set(skills.filter((id) => !exclusions.skills.has(id) && !retired.has(id)))].sort(),
531
+ agents: [...new Set(agents.filter((id) => !exclusions.agents.has(id)))].sort(),
532
+ rules: [...new Set(filterRules(rulesBase, exclusions.rules))].sort(),
533
+ };
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
+ }
179
540
  export async function cmdInstall(args, ctx) {
180
541
  let context;
181
542
  try {
@@ -185,16 +546,68 @@ export async function cmdInstall(args, ctx) {
185
546
  ctx.io.err(error.message);
186
547
  return 1;
187
548
  }
549
+ let spec;
550
+ try {
551
+ spec = await selectionForInstall(args, context.manifest, context.preset, context.engine);
552
+ }
553
+ catch (error) {
554
+ ctx.io.err(error.message);
555
+ return 2;
556
+ }
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));
188
562
  if (args.flags['dry-run']) {
563
+ const assets = await buildAssetPlan(spec, context.engine, context.manifest.targets, includeMcp);
189
564
  ctx.io.out(JSON.stringify({
190
565
  dryRun: true,
191
566
  action: 'install',
192
567
  preset: context.preset.id,
193
568
  targets: context.manifest.targets,
569
+ selection: spec.modes,
570
+ skills: spec.skills.length,
571
+ agents: spec.agents.length,
572
+ rules: spec.rules.length + 1,
573
+ destinations: assets.destinations,
574
+ speckit: speckitPlan(speckitSettings),
194
575
  }, null, 2));
195
576
  return 0;
196
577
  }
197
- const materialized = await materializePreset(ctx.root, context.preset);
578
+ if (runSpeckit && !(await preflightOrReport(speckitSettings, ctx))) {
579
+ return 2;
580
+ }
581
+ const backupId = runSpeckit ? (await createBackup(ctx.root, 'install')).id : null;
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
+ }
588
+ const next = {
589
+ ...context.manifest,
590
+ skills: { ...context.manifest.skills, publish: spec.skills, selection: spec.modes.skills },
591
+ agents: { ...context.manifest.agents, publish: spec.agents, selection: spec.modes.agents },
592
+ rules: { ...context.manifest.rules, publish: spec.rules, selection: spec.modes.rules },
593
+ };
594
+ if (includeMcp) {
595
+ next.mcp = { source: 'mcp-manifest.yaml' };
596
+ }
597
+ else {
598
+ delete next.mcp;
599
+ }
600
+ if (hasFullDimension(next)) {
601
+ next.catalog = catalogSeal(next);
602
+ }
603
+ else {
604
+ delete next.catalog;
605
+ }
606
+ next.speckit = speckit;
607
+ await writeManifest(ctx.root, next);
608
+ if (runSpeckit && !(await installSpeckit(speckitSettings, ctx, backupId))) {
609
+ return 1;
610
+ }
198
611
  const sync = await runSync(ctx.root, { check: false });
199
612
  await writeInventory(ctx.root);
200
613
  await ensureProjectIgnores(ctx.root);
@@ -202,32 +615,15 @@ export async function cmdInstall(args, ctx) {
202
615
  const hook = await installPostMergeHook(ctx.root);
203
616
  ctx.io.out(`hooks: ${hook.installed ? 'post-merge instalado' : hook.reason}`);
204
617
  }
205
- ctx.io.out(`install: ${materialized.written.length} nuevos · ${materialized.kept.length} conservados · ${sync.written.length} sincronizados`);
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`);
619
+ if (!Object.values(spec.modes).includes('full')) {
620
+ ctx.io.out('full: no aplica; usa install --full para expandir el catálogo');
621
+ }
622
+ ctx.io.out(speckitSummary(speckitSettings));
206
623
  return 0;
207
624
  }
208
625
  export async function cmdRepair(args, ctx) {
209
- let context;
210
- try {
211
- context = await loadManifestContext(ctx.root);
212
- }
213
- catch (error) {
214
- ctx.io.err(error.message);
215
- return 1;
216
- }
217
- if (args.flags['dry-run']) {
218
- ctx.io.out(JSON.stringify({
219
- dryRun: true,
220
- action: 'repair',
221
- preset: context.preset.id,
222
- targets: context.manifest.targets,
223
- }, null, 2));
224
- return 0;
225
- }
226
- const materialized = await materializePreset(ctx.root, context.preset);
227
- const sync = await runSync(ctx.root, { check: false });
228
- await writeInventory(ctx.root);
229
- ctx.io.out(`repair: ${materialized.written.length} scaffold · ${sync.written.length} archivos · ${sync.removed.length} eliminados`);
230
- return 0;
626
+ return cmdInstall(args, ctx);
231
627
  }
232
628
  export async function availablePresets() {
233
629
  return listPresetIds();
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 {};