@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.
- package/README.md +63 -4
- package/dist/backup/snapshots.js +12 -0
- package/dist/catalog.d.ts +83 -0
- package/dist/catalog.js +350 -0
- package/dist/cli/args.js +1 -0
- package/dist/cli/context.js +5 -0
- package/dist/cli/doctor.js +105 -14
- package/dist/cli/items.d.ts +1 -1
- package/dist/cli/items.js +1 -1
- package/dist/cli/maintenance.js +105 -51
- package/dist/cli/memory.js +9 -0
- package/dist/cli/onboarding.d.ts +33 -1
- package/dist/cli/onboarding.js +450 -54
- package/dist/cli/run.d.ts +3 -1
- package/dist/cli/run.js +10 -7
- package/dist/cli/types.d.ts +3 -0
- package/dist/concurrency.d.ts +5 -0
- package/dist/concurrency.js +23 -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 +154 -15
- package/dist/harness/memory/ledger.d.ts +6 -0
- package/dist/harness/memory/ledger.js +44 -2
- package/dist/manifest/manifest.d.ts +20 -1
- package/dist/manifest/manifest.js +35 -11
- package/dist/presets/materialize.d.ts +14 -1
- package/dist/presets/materialize.js +132 -11
- 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 +35 -1
- package/dist/sync/sync.d.ts +4 -0
- package/dist/sync/sync.js +68 -25
- package/dist/sync/targets.d.ts +13 -0
- package/dist/sync/targets.js +5 -0
- package/engine/engine.yaml +65 -31
- package/manifest.json +28 -47
- package/package.json +2 -1
- package/presets/angular-express/preset.yaml +2 -4
- package/presets/base/preset.yaml +1 -3
- package/presets/generic-ts/preset.yaml +1 -3
- package/presets/python/preset.yaml +1 -3
- package/rules/skill-quality/skill-behavior-rubric.md +63 -0
- package/rules/skill-quality/skill-harden-rubric.md +102 -0
- package/rules/skill-quality/skill-migration-brief.md +200 -0
- package/rules/skill-quality/skill-rubric.md +40 -0
- package/schema/engine.schema.json +22 -3
- package/schema/manifest.schema.json +22 -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,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
|
|
31
|
-
|
|
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:
|
|
53
|
-
agents: { source: '.ai-hub/agents', publish:
|
|
54
|
-
rules: { source: '.ai-hub/rules', publish: rules },
|
|
55
|
-
...(includeMcp ? { 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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
142
|
-
|
|
432
|
+
if (runSpeckit && !(await preflightOrReport(speckitSettings, ctx))) {
|
|
433
|
+
return 2;
|
|
143
434
|
}
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
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
|
-
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
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} · ${
|
|
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
|
-
|
|
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
|
-
|
|
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 {};
|