@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
@@ -1,14 +1,17 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
+ import { spawnSync } from 'node:child_process';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { readAiHubRc } from '../paths.js';
5
- import { ENGINE_COUNTS, ENGINE_LANES, ENGINE_SKILLS_DIR } from '../engine/engine.js';
6
- import { scanCoupling, scanLegacyArtifacts } from '../engine/gates.js';
6
+ import { auditSelection } from '../catalog.js';
7
+ import { ENGINE_COUNTS, ENGINE_LANES } from '../engine/engine.js';
8
+ import { scanCoupling, scanLegacyArtifacts, scanReservedNamespace, scanRetiredStates } from '../engine/gates.js';
7
9
  import { checkInventory, loadManifestContext, } from '../manifest/manifest.js';
8
10
  import { checkLock, runSync } from '../sync/sync.js';
9
11
  import { CONTEXT_RELATIVE, contextHash } from './context.js';
10
12
  import { TARGETS, TOOL_IDS, isToolId } from '../sync/targets.js';
11
13
  import { sourceCheckout } from './source.js';
14
+ import { speckitDoctorChecks, speckitNotApplicable } from '../speckit/doctor.js';
12
15
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
16
  const COUPLING_FILE = path.join(__dirname, '..', '..', 'engine', 'coupling.json');
14
17
  async function couplingRules() {
@@ -24,6 +27,45 @@ async function couplingRules() {
24
27
  async function exists(file) {
25
28
  return fs.access(file).then(() => true, () => false);
26
29
  }
30
+ function gitOutput(root, args) {
31
+ const result = spawnSync('git', args, {
32
+ cwd: root,
33
+ encoding: 'utf8',
34
+ shell: false,
35
+ });
36
+ return {
37
+ status: result.status ?? 1,
38
+ text: `${result.stdout ?? ''}${result.stderr ?? ''}`.trim(),
39
+ };
40
+ }
41
+ function versionedDestinations(root) {
42
+ const inside = gitOutput(root, ['rev-parse', '--is-inside-work-tree']);
43
+ if (inside.status !== 0 || inside.text !== 'true') {
44
+ return { applicable: false, tracked: [] };
45
+ }
46
+ const listed = gitOutput(root, [
47
+ 'ls-files',
48
+ '--',
49
+ '.claude',
50
+ '.opencode',
51
+ '.pi',
52
+ '.cursor',
53
+ '.github/skills',
54
+ '.github/agents',
55
+ '.github/instructions',
56
+ '.mcp.json',
57
+ '.cursor/mcp.json',
58
+ '.vscode/mcp.json',
59
+ 'opencode.json',
60
+ ]);
61
+ if (listed.status !== 0) {
62
+ return { applicable: false, tracked: [] };
63
+ }
64
+ return {
65
+ applicable: true,
66
+ tracked: listed.text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line !== ''),
67
+ };
68
+ }
27
69
  export async function diagnose(ctx) {
28
70
  const checks = [];
29
71
  const add = (id, ok, detail) => {
@@ -32,16 +74,18 @@ export async function diagnose(ctx) {
32
74
  const source = await sourceCheckout(ctx.root);
33
75
  let manifest = null;
34
76
  let engine = null;
77
+ let preset = null;
35
78
  try {
36
79
  const context = await loadManifestContext(ctx.root);
37
80
  manifest = context.manifest;
38
81
  engine = context.engine;
82
+ preset = context.preset;
39
83
  add('manifest', true, `manifest.yaml válido (proyecto ${manifest.project.id})`);
40
84
  }
41
85
  catch (error) {
42
86
  add('manifest', false, error instanceof Error ? error.message : String(error));
43
87
  }
44
- if (manifest && engine) {
88
+ if (manifest && engine && preset) {
45
89
  add('preset', true, `${manifest.preset.id}@${manifest.preset.version} (${manifest.preset.source})`);
46
90
  add('engine:roles', engine.roles.length === ENGINE_COUNTS.roles, `${engine.roles.length}/${ENGINE_COUNTS.roles}`);
47
91
  add('engine:commands', engine.commands.length === ENGINE_COUNTS.commands, `${engine.commands.length}/${ENGINE_COUNTS.commands}`);
@@ -60,20 +104,49 @@ export async function diagnose(ctx) {
60
104
  add('policy:optOuts', true, manifest.policy.optOuts.length === 0
61
105
  ? 'ninguno'
62
106
  : `opt-outs activos: ${manifest.policy.optOuts.join(', ')}`);
63
- const missingSkills = [];
64
- for (const command of engine.commands) {
65
- if (!(await exists(path.join(ENGINE_SKILLS_DIR, command.skill, 'SKILL.md')))) {
66
- missingSkills.push(`${command.id} -> ${command.skill}`);
67
- }
107
+ const audit = await auditSelection(ctx.root, manifest, preset, engine);
108
+ const missingProjectSkills = audit.missing
109
+ .filter((item) => item.startsWith('skill:'))
110
+ .map((item) => item.slice('skill:'.length));
111
+ add('skills', missingProjectSkills.length === 0, missingProjectSkills.length === 0
112
+ ? `${manifest.skills.publish.length} skills de proyecto + ${engine.commands.length} comandos Spec Kit (${audit.modes.skills})`
113
+ : missingProjectSkills.map((id) => `${id} -> ${manifest.skills.source}`).join(', '));
114
+ const missingAgents = audit.missing
115
+ .filter((item) => item.startsWith('agent:'))
116
+ .map((item) => item.slice('agent:'.length));
117
+ add('agents', missingAgents.length === 0, missingAgents.length === 0
118
+ ? `${manifest.agents.publish.length} agentes (${audit.modes.agents})`
119
+ : `faltan: ${missingAgents.join(', ')}`);
120
+ const missingRules = audit.missing
121
+ .filter((item) => item.startsWith('rule:'))
122
+ .map((item) => item.slice('rule:'.length));
123
+ add('rules', missingRules.length === 0, missingRules.length === 0
124
+ ? `${manifest.rules.publish.length + 1} reglas universales (${audit.modes.rules})`
125
+ : `faltan: ${missingRules.join(', ')}`);
126
+ const primaryError = audit.errors.find((error) => error.includes('publish editado')) ?? audit.errors[0];
127
+ add('completeness', audit.errors.length === 0, audit.errors.length === 0
128
+ ? `skills=${audit.modes.skills} agents=${audit.modes.agents} rules=${audit.modes.rules}`
129
+ : primaryError);
130
+ if (Object.values(audit.modes).includes('full')) {
131
+ add('full', true, audit.warnings.length > 0 ? audit.warnings.join('; ') : 'catálogo al día');
68
132
  }
69
- for (const skill of manifest.skills.publish) {
70
- if (!(await exists(path.join(ctx.root, manifest.skills.source, skill, 'SKILL.md')))) {
71
- missingSkills.push(`${skill} -> ${manifest.skills.source}`);
72
- }
133
+ if (!manifest.mcp) {
134
+ add('mcp', true, 'MCP: no configurado');
135
+ }
136
+ else {
137
+ const direct = path.join(ctx.root, manifest.mcp.source);
138
+ const project = path.join(ctx.root, '.ai-hub', path.basename(manifest.mcp.source));
139
+ const present = (await exists(direct)) || (await exists(project));
140
+ add('mcp', present, present ? `MCP configurado (${manifest.mcp.source})` : `MCP: falta ${manifest.mcp.source}`);
141
+ }
142
+ const speckitChecks = source.isSource
143
+ ? speckitNotApplicable()
144
+ : await speckitDoctorChecks(ctx.root, manifest, engine, ctx.exec);
145
+ for (const check of speckitChecks) {
146
+ add(check.id, check.ok, check.detail);
73
147
  }
74
- add('skills', missingSkills.length === 0, missingSkills.length === 0 ? '17 skills nativas + extras' : missingSkills.join(', '));
75
148
  if (source.isSource) {
76
- const required = ['dist', 'schema', 'manifest.json', 'engine', 'presets', 'agents', 'skills'];
149
+ const required = ['dist', 'schema', 'manifest.json', 'engine', 'presets', 'rules', 'agents', 'skills'];
77
150
  const missing = required.filter((entry) => !source.files.includes(entry));
78
151
  add('package:files', missing.length === 0, missing.length === 0 ? required.join(', ') : `faltan: ${missing.join(', ')}`);
79
152
  add('package:entry', await exists(path.join(ctx.root, 'core', 'dist', 'bin', 'ai-hub.js')), 'core/dist/bin/ai-hub.js');
@@ -94,6 +167,10 @@ export async function diagnose(ctx) {
94
167
  }
95
168
  add('hooks', missingHooks.length === 0, missingHooks.length === 0 ? 'superficies presentes' : missingHooks.join(', '));
96
169
  }
170
+ const reserved = await scanReservedNamespace();
171
+ add('catalog:reserved-namespace', reserved.length === 0, reserved.length === 0
172
+ ? 'ningún activo del catálogo usa el espacio speckit-*/speckit.* de Spec Kit'
173
+ : reserved.slice(0, 5).join('; ') + (reserved.length > 5 ? ` (+${reserved.length - 5})` : ''));
97
174
  const legacy = await scanLegacyArtifacts(ctx.root);
98
175
  add('engine:no-legacy-prefix', legacy.length === 0, legacy.length === 0
99
176
  ? source.isSource
@@ -169,8 +246,22 @@ export async function diagnose(ctx) {
169
246
  const ignored = ignore.split(/\r?\n/).includes('.worktrees/');
170
247
  add('worktrees:ignored', ignored, ignored ? '.worktrees/ ignorado' : 'añade .worktrees/ a .gitignore');
171
248
  }
249
+ const versioning = versionedDestinations(ctx.root);
250
+ if (!versioning.applicable) {
251
+ add('versionado', true, 'no aplica (sin repositorio git)');
252
+ }
253
+ else if (versioning.tracked.length === 0) {
254
+ add('versionado', true, 'sin destinos generados versionados');
255
+ }
256
+ else {
257
+ add('versionado', true, `aviso: destinos generados versionados: ${versioning.tracked.slice(0, 5).join(', ')}${versioning.tracked.length > 5 ? ` (+${versioning.tracked.length - 5})` : ''}`);
258
+ }
172
259
  const coupling = await couplingRules();
173
260
  if (source.isSource) {
261
+ const retired = await scanRetiredStates(undefined, ctx.root);
262
+ add('engine:retired-states', retired.length === 0, retired.length === 0
263
+ ? 'sin vocabulario de estados retirado (ROOT_CAUSE, FIXED, READY…)'
264
+ : retired.slice(0, 5).join('; ') + (retired.length > 5 ? ` (+${retired.length - 5})` : ''));
174
265
  const findings = await scanCoupling(ctx.root, coupling.terms);
175
266
  add('engine:no-coupling', findings.length === 0, findings.length === 0
176
267
  ? 'sin términos de dominio en el motor'
@@ -1,5 +1,5 @@
1
1
  import type { ParsedArgs } from './types.js';
2
2
  export declare function splitItem(args: ParsedArgs): {
3
- kind: 'skill' | 'agent' | null;
3
+ kind: 'skill' | 'agent' | 'rule' | null;
4
4
  id: string | null;
5
5
  };
package/dist/cli/items.js CHANGED
@@ -3,7 +3,7 @@ export function splitItem(args) {
3
3
  if (!first) {
4
4
  return { kind: null, id: null };
5
5
  }
6
- if (first === 'skill' || first === 'agent') {
6
+ if (first === 'skill' || first === 'agent' || first === 'rule') {
7
7
  return { kind: first, id: second ?? null };
8
8
  }
9
9
  return { kind: null, id: first };
@@ -5,15 +5,29 @@ import { createBackup, listBackups, purgeGenerated, restoreBackup } from '../bac
5
5
  import { readAiHubRc } from '../paths.js';
6
6
  import { loadManifestContext, writeInventory } from '../manifest/manifest.js';
7
7
  import { findPresetAsset } from '../presets/resolve.js';
8
- import { ENGINE_AGENTS, readScaffold, SCAFFOLD_RELATIVE } from '../presets/materialize.js';
8
+ import { ENGINE_AGENTS, materializeAgent, materializeSkill, removeOwned, SCAFFOLD_RELATIVE, } from '../presets/materialize.js';
9
9
  import { ENGINE_SKILLS_DIR } from '../engine/engine.js';
10
+ import { catalogIndex, catalogSeal, hasFullDimension } from '../catalog.js';
10
11
  import { readLock } from '../sync/lock.js';
11
12
  import { runSync } from '../sync/sync.js';
12
- import { cliVersion, writeManifest } from './onboarding.js';
13
+ import { cliVersion, cmdInstall, writeManifest } from './onboarding.js';
13
14
  import { splitItem } from './items.js';
14
15
  async function exists(file) {
15
16
  return fs.access(file).then(() => true, () => false);
16
17
  }
18
+ /** Nombres del espacio de Spec Kit: sus comandos no se añaden ni se desinstalan desde ai-hub. */
19
+ function isSpeckitCommandName(id) {
20
+ return id.startsWith('speckit-') || id.startsWith('speckit.');
21
+ }
22
+ function markCustom(manifest, dimension) {
23
+ manifest[dimension].selection = 'custom';
24
+ if (hasFullDimension(manifest)) {
25
+ manifest.catalog = catalogSeal(manifest);
26
+ }
27
+ else {
28
+ delete manifest.catalog;
29
+ }
30
+ }
17
31
  export async function cmdList(args, ctx) {
18
32
  let context;
19
33
  try {
@@ -24,11 +38,18 @@ export async function cmdList(args, ctx) {
24
38
  return 1;
25
39
  }
26
40
  const { manifest, engine, preset } = context;
41
+ const mode = manifest.skills.selection ?? 'preset';
27
42
  const data = {
28
43
  project: manifest.project,
29
44
  preset: { id: preset.id, version: preset.version, chain: preset.chain },
30
45
  targets: manifest.targets,
46
+ selection: {
47
+ skills: manifest.skills.selection ?? 'preset',
48
+ agents: manifest.agents.selection ?? 'preset',
49
+ rules: manifest.rules.selection ?? 'preset',
50
+ },
31
51
  commands: engine.commands.map((command) => ({ id: command.id, lane: command.lane, role: command.role })),
52
+ speckitCommands: engine.commands.map((command) => command.speckit),
32
53
  skills: manifest.skills.publish,
33
54
  agents: manifest.agents.publish,
34
55
  rules: manifest.rules.publish,
@@ -40,10 +61,11 @@ export async function cmdList(args, ctx) {
40
61
  ctx.io.out(`project: ${manifest.project.id} (${manifest.project.name})`);
41
62
  ctx.io.out(`preset: ${preset.id}@${preset.version} [${preset.chain.join(' -> ')}]`);
42
63
  ctx.io.out(`targets: ${manifest.targets.join(', ')}`);
64
+ ctx.io.out(`selection: skills=${data.selection.skills} agents=${data.selection.agents} rules=${data.selection.rules}`);
43
65
  ctx.io.out(`commands (${data.commands.length}): ${data.commands.map((command) => command.id).join(', ')}`);
44
- ctx.io.out(`skills (${data.skills.length}): ${data.skills.join(', ') || 'solo las 17 del motor'}`);
66
+ ctx.io.out(`skills: ${data.skills.length} de proyecto (${mode}) + ${data.speckitCommands.length} comandos Spec Kit`);
45
67
  ctx.io.out(`agents (${data.agents.length}): ${data.agents.join(', ')}`);
46
- ctx.io.out(`rules (${data.rules.length}): ${data.rules.join(', ')}`);
68
+ ctx.io.out(`rules: ${data.rules.length + 1} universales (${data.rules.length} materializadas + preflight)`);
47
69
  return 0;
48
70
  }
49
71
  export async function cmdAdd(args, ctx) {
@@ -52,6 +74,10 @@ export async function cmdAdd(args, ctx) {
52
74
  ctx.io.err('uso: ai-hub add <skill|agent> <id>');
53
75
  return 2;
54
76
  }
77
+ if (parsed.kind === 'rule') {
78
+ ctx.io.err('las reglas universales se seleccionan con --exclude rule:<id> en init; no se añaden con add');
79
+ return 2;
80
+ }
55
81
  let context;
56
82
  try {
57
83
  context = await loadManifestContext(ctx.root);
@@ -62,18 +88,31 @@ export async function cmdAdd(args, ctx) {
62
88
  }
63
89
  const { manifest, preset } = context;
64
90
  const id = parsed.id;
65
- const engineSkill = context.engine.commands.some((command) => command.skill === id);
66
- const catalogSkill = await exists(path.join(ENGINE_SKILLS_DIR, id, 'SKILL.md'));
91
+ if (isSpeckitCommandName(id)) {
92
+ ctx.io.err(`"${id}" lo provee Spec Kit; ai-hub no añade ni desinstala sus comandos`);
93
+ return 1;
94
+ }
95
+ const catalogSkill = (await catalogIndex()).skills.includes(id);
96
+ const catalogAgent = await exists(path.join(ENGINE_AGENTS, `${id}.md`));
67
97
  const kind = parsed.kind ??
68
- (preset.skills.includes(id) || engineSkill || catalogSkill ? 'skill' : 'agent');
98
+ (preset.skills.includes(id) || catalogSkill
99
+ ? 'skill'
100
+ : catalogAgent || preset.agents.includes(id)
101
+ ? 'agent'
102
+ : null);
103
+ if (kind === null) {
104
+ ctx.io.err(`no existe "${id}" en el preset ${preset.id} ni en el catálogo del motor`);
105
+ return 1;
106
+ }
69
107
  if (kind === 'skill') {
70
- if (engineSkill) {
71
- ctx.io.out(`"${id}" ya forma parte de las 17 skills del motor`);
72
- return 0;
73
- }
74
108
  const fromPreset = preset.skills.includes(id)
75
109
  ? await findPresetAsset(preset.chain, `skills/${id}`)
76
110
  : null;
111
+ if (fromPreset === null && !catalogSkill) {
112
+ // Una skill con `runtime: false` sigue en el paquete pero no es instalable.
113
+ ctx.io.err(`la skill "${id}" no pertenece al catálogo instalable (no existe o declara runtime: false)`);
114
+ return 1;
115
+ }
77
116
  const source = fromPreset ?? path.join(ENGINE_SKILLS_DIR, id);
78
117
  if (!(await exists(path.join(source, 'SKILL.md')))) {
79
118
  ctx.io.err(`no existe la skill "${id}" en ${preset.id} ni en el catálogo del motor`);
@@ -84,29 +123,25 @@ export async function cmdAdd(args, ctx) {
84
123
  ctx.io.out(JSON.stringify({ dryRun: true, action: 'add', kind, id, destination }, null, 2));
85
124
  return 0;
86
125
  }
87
- if (!(await exists(destination))) {
88
- await fs.cp(source, destination, { recursive: true });
89
- }
126
+ await materializeSkill(ctx.root, id, source, fromPreset ? `@preset/${preset.id}/skills/${id}` : `@engine/skills/shared/${id}`);
90
127
  manifest.skills.publish = [...new Set([...manifest.skills.publish, id])];
91
128
  }
92
129
  else {
93
- if (!preset.agents.includes(id)) {
94
- ctx.io.err(`el agente "${id}" no pertenece al preset ${preset.id}`);
95
- return 1;
96
- }
97
130
  const fromPreset = await findPresetAsset(preset.chain, `agents/${id}.md`);
98
131
  const source = fromPreset ?? path.join(ENGINE_AGENTS, `${id}.md`);
132
+ if (!(await exists(source))) {
133
+ ctx.io.err(`no existe el agente "${id}" en el catálogo ${ENGINE_AGENTS}`);
134
+ return 1;
135
+ }
99
136
  const destination = path.join(ctx.root, manifest.agents.source, `${id}.md`);
100
137
  if (args.flags['dry-run']) {
101
138
  ctx.io.out(JSON.stringify({ dryRun: true, action: 'add', kind, id, destination }, null, 2));
102
139
  return 0;
103
140
  }
104
- if (!(await exists(destination))) {
105
- await fs.mkdir(path.dirname(destination), { recursive: true });
106
- await fs.copyFile(source, destination);
107
- }
141
+ await materializeAgent(ctx.root, id, source, fromPreset ? `@preset/${preset.id}/agents/${id}.md` : `@engine/agents/${id}.md`);
108
142
  manifest.agents.publish = [...new Set([...manifest.agents.publish, id])];
109
143
  }
144
+ markCustom(manifest, kind === 'agent' ? 'agents' : 'skills');
110
145
  await writeManifest(ctx.root, manifest);
111
146
  const sync = await runSync(ctx.root, { check: false });
112
147
  await writeInventory(ctx.root);
@@ -116,7 +151,7 @@ export async function cmdAdd(args, ctx) {
116
151
  export async function cmdUninstall(args, ctx) {
117
152
  const parsed = splitItem(args);
118
153
  if (!parsed.id) {
119
- ctx.io.err('uso: ai-hub uninstall <skill|agent> <id>');
154
+ ctx.io.err('uso: ai-hub uninstall <skill|agent|rule> <id>');
120
155
  return 2;
121
156
  }
122
157
  let context;
@@ -129,9 +164,20 @@ export async function cmdUninstall(args, ctx) {
129
164
  }
130
165
  const { manifest } = context;
131
166
  const id = parsed.id;
132
- const kind = parsed.kind ?? (manifest.skills.publish.includes(id) ? 'skill' : 'agent');
133
- if (kind === 'skill' && context.engine.commands.some((command) => command.skill === id)) {
134
- ctx.io.err(`"${id}" es una skill del motor y no se puede desinstalar`);
167
+ const kind = parsed.kind ??
168
+ (manifest.skills.publish.includes(id)
169
+ ? 'skill'
170
+ : manifest.agents.publish.includes(id)
171
+ ? 'agent'
172
+ : manifest.rules.publish.includes(id) || id === 'skill-quality'
173
+ ? 'rule'
174
+ : 'agent');
175
+ if (isSpeckitCommandName(id)) {
176
+ ctx.io.err(`"${id}" lo provee Spec Kit; ai-hub no añade ni desinstala sus comandos`);
177
+ return 1;
178
+ }
179
+ if (kind === 'rule' && (id === 'core' || id === 'preflight')) {
180
+ ctx.io.err(`la regla base "${id}" no se puede desinstalar`);
135
181
  return 1;
136
182
  }
137
183
  if (args.flags['dry-run']) {
@@ -142,10 +188,22 @@ export async function cmdUninstall(args, ctx) {
142
188
  if (kind === 'skill') {
143
189
  manifest.skills.publish = manifest.skills.publish.filter((skill) => skill !== id);
144
190
  await removeOwned(ctx.root, path.join(manifest.skills.source, id), true);
191
+ markCustom(manifest, 'skills');
145
192
  }
146
- else {
193
+ else if (kind === 'agent') {
147
194
  manifest.agents.publish = manifest.agents.publish.filter((agent) => agent !== id);
148
195
  await removeOwned(ctx.root, path.join(manifest.agents.source, `${id}.md`), false);
196
+ markCustom(manifest, 'agents');
197
+ }
198
+ else {
199
+ const removed = id === 'skill-quality'
200
+ ? manifest.rules.publish.filter((rule) => rule.startsWith('skill-quality/'))
201
+ : manifest.rules.publish.filter((rule) => rule === id);
202
+ manifest.rules.publish = manifest.rules.publish.filter((rule) => !removed.includes(rule));
203
+ for (const rule of removed) {
204
+ await removeOwned(ctx.root, path.join(manifest.rules.source, `${rule}.md`), false);
205
+ }
206
+ markCustom(manifest, 'rules');
149
207
  }
150
208
  await writeManifest(ctx.root, manifest);
151
209
  const sync = await runSync(ctx.root, { check: false });
@@ -153,30 +211,6 @@ export async function cmdUninstall(args, ctx) {
153
211
  ctx.io.out(`uninstall: ${kind} "${id}" · ${sync.removed.length} archivos eliminados`);
154
212
  return 0;
155
213
  }
156
- async function removeOwned(root, relative, directory) {
157
- const absolute = path.join(root, relative);
158
- if (!(await exists(absolute))) {
159
- return;
160
- }
161
- const record = await readScaffold(root);
162
- if (directory) {
163
- const files = record.files;
164
- const prefix = `${relative.split(path.sep).join('/')}/`;
165
- const owned = Object.entries(files).filter(([name]) => name.startsWith(prefix));
166
- if (owned.length === 0) {
167
- const marker = await fs.readFile(path.join(absolute, 'SKILL.md'), 'utf8').catch(() => '');
168
- if (!marker.includes('GENERADO por ai-hub')) {
169
- return;
170
- }
171
- }
172
- await fs.rm(absolute, { recursive: true, force: true });
173
- return;
174
- }
175
- const text = await fs.readFile(absolute, 'utf8').catch(() => null);
176
- if (text === null || text.includes('GENERADO por ai-hub')) {
177
- await fs.rm(absolute, { force: true });
178
- }
179
- }
180
214
  export async function cmdBackups(args, ctx) {
181
215
  const snapshots = await listBackups(ctx.root);
182
216
  if (args.flags.json) {
@@ -256,6 +290,26 @@ export async function cmdUpgrade(args, ctx) {
256
290
  return 1;
257
291
  }
258
292
  }
293
+ let context;
294
+ try {
295
+ context = await loadManifestContext(ctx.root);
296
+ }
297
+ catch (error) {
298
+ ctx.io.err(error.message);
299
+ return 1;
300
+ }
301
+ const refreshFull = Boolean(args.flags.full) ||
302
+ (hasFullDimension(context.manifest) && !args.flags['preset-only']);
303
+ if (refreshFull) {
304
+ const code = await cmdInstall({
305
+ positionals: [],
306
+ flags: args.flags.full ? { full: true } : {},
307
+ }, ctx);
308
+ if (code === 0) {
309
+ ctx.io.out('upgrade: dimensiones full actualizadas y sello refrescado');
310
+ }
311
+ return code;
312
+ }
259
313
  const result = await runSync(ctx.root, { check: false });
260
314
  ctx.io.out(`upgrade: ${result.written.length} archivos regenerados`);
261
315
  return 0;
@@ -1,4 +1,5 @@
1
1
  import { appendLedger, queryLedger, readLedger, resumeLedger, } from '../harness/memory/ledger.js';
2
+ import { isEngineCheckout } from '../source-checkout.js';
2
3
  import { flagList, flagString } from './args.js';
3
4
  function numberFlag(args, name) {
4
5
  const raw = flagString(args, name);
@@ -9,6 +10,10 @@ function numberFlag(args, name) {
9
10
  return Number.isFinite(value) ? value : null;
10
11
  }
11
12
  export async function cmdMemory(args, ctx) {
13
+ if (!(await isEngineCheckout(ctx.root))) {
14
+ ctx.io.out('memory: no aplica en instalación');
15
+ return 0;
16
+ }
12
17
  const sub = args.positionals[0] ?? 'list';
13
18
  if (sub === 'add') {
14
19
  const sessionId = flagString(args, 'session') ?? process.env.AI_HUB_SESSION ?? 'local';
@@ -74,6 +79,10 @@ export async function cmdMemory(args, ctx) {
74
79
  ctx.io.out(`cambio activo: ${summary.activeChange ?? 'ninguno'}`);
75
80
  ctx.io.out(`fase: ${summary.phase}`);
76
81
  ctx.io.out(`tareas: ${summary.tasks.done}/${summary.tasks.total}`);
82
+ if (summary.feature !== null) {
83
+ ctx.io.out(`feature Spec Kit: ${summary.feature.dir} · ${summary.feature.pending} pendientes` +
84
+ (summary.feature.source === 'local' ? ' (puntero local .specify/feature.json, no versionado)' : ''));
85
+ }
77
86
  ctx.io.out(last === null
78
87
  ? 'ledger: sin eventos'
79
88
  : `último evento: ${last.event} ${last.timestamps.at} ${last.sessionId} (${last.branch ?? '-'})`);
@@ -1,15 +1,47 @@
1
1
  import { type ProjectManifest } from '../manifest/manifest.js';
2
+ import { type SelectionMode } from '../catalog.js';
2
3
  import type { CliContext, ParsedArgs } from './types.js';
3
4
  export declare function cliVersion(): Promise<string>;
4
- export interface Plan {
5
+ export interface AssetPlan {
6
+ selection: SelectionMode;
7
+ selections: {
8
+ skills: SelectionMode;
9
+ agents: SelectionMode;
10
+ rules: SelectionMode;
11
+ };
12
+ skills: string[];
13
+ agents: string[];
14
+ rules: string[];
15
+ destinations: number;
16
+ files: number;
17
+ bytes: number;
18
+ }
19
+ export interface Plan extends AssetPlan {
5
20
  command: 'init';
6
21
  project: string;
7
22
  preset: string;
8
23
  targets: string[];
24
+ skillCount: number;
25
+ speckitCommands: number;
26
+ agentCount: number;
27
+ ruleCount: number;
28
+ mcp: boolean;
9
29
  migrate: boolean;
10
30
  write: string[];
31
+ speckit: SpeckitPlan;
11
32
  hash: string;
12
33
  }
34
+ /** Intención de Spec Kit dentro del hash del plan; el entorno (uv, shell) queda fuera. */
35
+ export interface SpeckitPlan {
36
+ managed: boolean;
37
+ package: string;
38
+ version: string;
39
+ excludeNewer: string;
40
+ script: string;
41
+ defaultIntegration: string | null;
42
+ integrations: string[];
43
+ extensions: Record<string, string>;
44
+ }
13
45
  export declare function writeManifest(root: string, manifest: ProjectManifest): Promise<void>;
14
46
  export declare function cmdInit(args: ParsedArgs, ctx: CliContext): Promise<number>;
15
47
  export declare function cmdInstall(args: ParsedArgs, ctx: CliContext): Promise<number>;