@dsalazardev/ai-hub-cli 0.1.0 → 0.2.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/dist/catalog.d.ts +76 -0
- package/dist/catalog.js +321 -0
- package/dist/cli/context.js +5 -0
- package/dist/cli/doctor.js +91 -9
- package/dist/cli/items.d.ts +1 -1
- package/dist/cli/items.js +1 -1
- package/dist/cli/maintenance.js +93 -21
- package/dist/cli/memory.js +5 -0
- package/dist/cli/onboarding.d.ts +20 -1
- package/dist/cli/onboarding.js +334 -52
- package/dist/cli/run.js +5 -3
- package/dist/concurrency.d.ts +5 -0
- package/dist/concurrency.js +23 -0
- package/dist/engine/gates.js +53 -15
- package/dist/manifest/manifest.d.ts +17 -0
- package/dist/manifest/manifest.js +19 -1
- package/dist/presets/materialize.d.ts +9 -1
- package/dist/presets/materialize.js +102 -11
- package/dist/sync/render.js +5 -1
- package/dist/sync/sync.js +20 -20
- package/manifest.json +10 -1
- 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/manifest.schema.json +12 -0
package/dist/cli/onboarding.js
CHANGED
|
@@ -4,14 +4,17 @@ 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, } 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';
|
|
15
18
|
import { flagList, flagString } from './args.js';
|
|
16
19
|
const RC_NAME = '.ai-hubrc.json';
|
|
17
20
|
export async function cliVersion() {
|
|
@@ -27,9 +30,103 @@ function projectId(root) {
|
|
|
27
30
|
async function exists(file) {
|
|
28
31
|
return fs.access(file).then(() => true, () => false);
|
|
29
32
|
}
|
|
30
|
-
function
|
|
31
|
-
|
|
33
|
+
function overallMode(modes) {
|
|
34
|
+
return modes.skills === modes.agents && modes.agents === modes.rules ? modes.skills : 'custom';
|
|
35
|
+
}
|
|
36
|
+
function parseExclusions(args) {
|
|
37
|
+
const skills = new Set();
|
|
38
|
+
const agents = new Set();
|
|
39
|
+
const rules = new Set();
|
|
40
|
+
for (const item of flagList(args, 'exclude')) {
|
|
41
|
+
const separator = item.indexOf(':');
|
|
42
|
+
const kind = separator >= 0 ? item.slice(0, separator) : 'skill';
|
|
43
|
+
const id = separator >= 0 ? item.slice(separator + 1) : item;
|
|
44
|
+
if (id === '') {
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (kind === 'agent') {
|
|
48
|
+
agents.add(id);
|
|
49
|
+
}
|
|
50
|
+
else if (kind === 'rule') {
|
|
51
|
+
if (id === 'core' || id === 'preflight') {
|
|
52
|
+
throw new Error(`la regla base "${id}" no se puede excluir`);
|
|
53
|
+
}
|
|
54
|
+
rules.add(id);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
skills.add(id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { skills, agents, rules };
|
|
61
|
+
}
|
|
62
|
+
function filterRules(rules, exclusions) {
|
|
63
|
+
return rules.filter((id) => {
|
|
64
|
+
for (const excluded of exclusions) {
|
|
65
|
+
if (excluded === id || (excluded === 'skill-quality' && id.startsWith('skill-quality/'))) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
async function buildSelection(args, preset, engine, existing) {
|
|
73
|
+
const fullFlag = Boolean(args.flags.full);
|
|
74
|
+
const presetOnly = Boolean(args.flags['preset-only']);
|
|
75
|
+
if (fullFlag && presetOnly) {
|
|
76
|
+
throw new Error('--full y --preset-only son incompatibles');
|
|
77
|
+
}
|
|
78
|
+
const exclusions = parseExclusions(args);
|
|
79
|
+
const hasExclusions = exclusions.skills.size > 0 || exclusions.agents.size > 0 || exclusions.rules.size > 0;
|
|
80
|
+
if (existing && !fullFlag && !presetOnly && !hasExclusions) {
|
|
81
|
+
return {
|
|
82
|
+
modes: {
|
|
83
|
+
skills: existing.skills.selection ?? 'preset',
|
|
84
|
+
agents: existing.agents.selection ?? 'preset',
|
|
85
|
+
rules: existing.rules.selection ?? 'preset',
|
|
86
|
+
},
|
|
87
|
+
skills: [...existing.skills.publish],
|
|
88
|
+
agents: [...existing.agents.publish],
|
|
89
|
+
rules: [...existing.rules.publish],
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const catalogSkills = await listCatalogSkills(engine);
|
|
93
|
+
const catalogAgents = await listCatalogAgents();
|
|
94
|
+
const defaultMode = presetOnly
|
|
95
|
+
? 'preset'
|
|
96
|
+
: fullFlag
|
|
97
|
+
? 'full'
|
|
98
|
+
: existing
|
|
99
|
+
? (existing.skills.selection ?? 'preset')
|
|
100
|
+
: 'full';
|
|
101
|
+
const modes = {
|
|
102
|
+
skills: exclusions.skills.size > 0 ? 'custom' : defaultMode,
|
|
103
|
+
agents: exclusions.agents.size > 0 ? 'custom' : defaultMode,
|
|
104
|
+
rules: exclusions.rules.size > 0 ? 'custom' : defaultMode,
|
|
105
|
+
};
|
|
106
|
+
const skillsBase = modes.skills === 'preset'
|
|
107
|
+
? [...preset.skills]
|
|
108
|
+
: modes.skills === 'custom' && existing
|
|
109
|
+
? [...existing.skills.publish]
|
|
110
|
+
: [...catalogSkills];
|
|
111
|
+
const agentsBase = modes.agents === 'preset'
|
|
112
|
+
? [...preset.agents]
|
|
113
|
+
: modes.agents === 'custom' && existing
|
|
114
|
+
? [...existing.agents.publish]
|
|
115
|
+
: [...catalogAgents];
|
|
116
|
+
const rulesBase = modes.rules === 'preset'
|
|
117
|
+
? expectedRuleIds('preset', preset)
|
|
118
|
+
: modes.rules === 'custom' && existing
|
|
119
|
+
? [...existing.rules.publish]
|
|
120
|
+
: expectedRuleIds('full', preset);
|
|
32
121
|
return {
|
|
122
|
+
modes,
|
|
123
|
+
skills: [...new Set(skillsBase.filter((id) => !exclusions.skills.has(id)))].sort(),
|
|
124
|
+
agents: [...new Set(agentsBase.filter((id) => !exclusions.agents.has(id)))].sort(),
|
|
125
|
+
rules: [...new Set(filterRules(rulesBase, exclusions.rules))].sort(),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function manifestFromSelection(id, preset, targets, profilePackageManager, guardMode, includeMcp, selection) {
|
|
129
|
+
const manifest = {
|
|
33
130
|
version: 2,
|
|
34
131
|
project: { id, name: id },
|
|
35
132
|
preset: { id: preset.id, version: preset.version, source: 'package' },
|
|
@@ -49,22 +146,111 @@ function manifestFromPreset(id, preset, targets, profilePackageManager, guardMod
|
|
|
49
146
|
? { dangerPatterns: preset.policy.dangerPatterns }
|
|
50
147
|
: {}),
|
|
51
148
|
},
|
|
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:
|
|
149
|
+
skills: { source: '.ai-hub/skills', publish: selection.skills, selection: selection.modes.skills },
|
|
150
|
+
agents: { source: '.ai-hub/agents', publish: selection.agents, selection: selection.modes.agents },
|
|
151
|
+
rules: { source: '.ai-hub/rules', publish: selection.rules, selection: selection.modes.rules },
|
|
152
|
+
...(includeMcp ? { mcp: { source: 'mcp-manifest.yaml' } } : {}),
|
|
153
|
+
};
|
|
154
|
+
if (hasFullDimension(manifest)) {
|
|
155
|
+
manifest.catalog = catalogSeal(manifest);
|
|
156
|
+
}
|
|
157
|
+
return manifest;
|
|
158
|
+
}
|
|
159
|
+
function mergeMaterialized(target, source) {
|
|
160
|
+
target.written.push(...source.written);
|
|
161
|
+
target.kept.push(...source.kept);
|
|
162
|
+
target.collisions.push(...source.collisions);
|
|
163
|
+
}
|
|
164
|
+
async function materializeSelection(root, preset, selection) {
|
|
165
|
+
const result = { written: [], kept: [], collisions: [] };
|
|
166
|
+
mergeMaterialized(result, await materializePreset(root, preset, selection.rules));
|
|
167
|
+
if (selection.modes.rules !== 'preset') {
|
|
168
|
+
mergeMaterialized(result, await materializeUniversalRules(root, preset, selection.rules));
|
|
169
|
+
}
|
|
170
|
+
if (selection.modes.skills !== 'preset' || selection.modes.agents !== 'preset') {
|
|
171
|
+
const presetSkills = new Set(preset.skills);
|
|
172
|
+
const presetAgents = new Set(preset.agents);
|
|
173
|
+
mergeMaterialized(result, await materializeCatalog(root, selection.modes.skills === 'preset'
|
|
174
|
+
? []
|
|
175
|
+
: selection.skills.filter((id) => !presetSkills.has(id)), selection.modes.agents === 'preset'
|
|
176
|
+
? []
|
|
177
|
+
: selection.agents.filter((id) => !presetAgents.has(id))));
|
|
178
|
+
}
|
|
179
|
+
return result;
|
|
180
|
+
}
|
|
181
|
+
async function buildAssetPlan(selection, engine, targets, includeMcp) {
|
|
182
|
+
const index = await catalogIndex();
|
|
183
|
+
const engineSkills = engine.commands.map((command) => command.skill);
|
|
184
|
+
const allSkills = [...new Set([...engineSkills, ...selection.skills])];
|
|
185
|
+
const projectSkillFiles = selection.skills.reduce((total, id) => total + (index.skillFiles[id]?.length ?? 0), 0);
|
|
186
|
+
const skillFiles = allSkills.reduce((total, id) => total + (index.skillFiles[id]?.length ?? 0), 0);
|
|
187
|
+
let destinations = 1; // opencode.json
|
|
188
|
+
for (const tool of targets) {
|
|
189
|
+
const target = TARGETS[tool];
|
|
190
|
+
destinations += target.skillShape === 'file' ? allSkills.length : skillFiles;
|
|
191
|
+
if (target.agents) {
|
|
192
|
+
destinations += selection.agents.length;
|
|
193
|
+
}
|
|
194
|
+
destinations += selection.rules.length + 1; // reglas materializadas + preflight
|
|
195
|
+
}
|
|
196
|
+
if (includeMcp) {
|
|
197
|
+
destinations += 3;
|
|
198
|
+
}
|
|
199
|
+
const materializedFiles = projectSkillFiles + selection.agents.length + selection.rules.length + 2;
|
|
200
|
+
const files = materializedFiles + destinations;
|
|
201
|
+
let bytes = 0;
|
|
202
|
+
const stat = async (file) => fs.stat(file).then((value) => value.size, () => 0);
|
|
203
|
+
for (const id of allSkills) {
|
|
204
|
+
for (const file of index.skillFiles[id] ?? []) {
|
|
205
|
+
bytes += await stat(file);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
for (const id of selection.agents) {
|
|
209
|
+
bytes += await stat(path.join(ENGINE_AGENTS_DIR, `${id}.md`));
|
|
210
|
+
}
|
|
211
|
+
for (const id of selection.rules) {
|
|
212
|
+
if (id.startsWith('skill-quality/')) {
|
|
213
|
+
bytes += await stat(path.join(ENGINE_RULES_DIR, `${id}.md`));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
selection: overallMode(selection.modes),
|
|
218
|
+
selections: selection.modes,
|
|
219
|
+
skills: selection.skills,
|
|
220
|
+
agents: selection.agents,
|
|
221
|
+
rules: selection.rules,
|
|
222
|
+
destinations,
|
|
223
|
+
files,
|
|
224
|
+
bytes,
|
|
56
225
|
};
|
|
57
226
|
}
|
|
58
227
|
export async function writeManifest(root, manifest) {
|
|
59
228
|
await fs.writeFile(path.join(root, 'manifest.yaml'), stringify(manifest), 'utf8');
|
|
60
229
|
}
|
|
230
|
+
const GENERATED_IGNORES = [
|
|
231
|
+
'.worktrees/',
|
|
232
|
+
'.claude/',
|
|
233
|
+
'.opencode/',
|
|
234
|
+
'.pi/',
|
|
235
|
+
'.cursor/',
|
|
236
|
+
'.github/skills/',
|
|
237
|
+
'.github/agents/',
|
|
238
|
+
'.github/instructions/',
|
|
239
|
+
'.mcp.json',
|
|
240
|
+
'.cursor/mcp.json',
|
|
241
|
+
'.vscode/mcp.json',
|
|
242
|
+
'opencode.json',
|
|
243
|
+
];
|
|
61
244
|
async function ensureProjectIgnores(root) {
|
|
62
245
|
const file = path.join(root, '.gitignore');
|
|
63
246
|
const current = await fs.readFile(file, 'utf8').catch(() => '');
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
247
|
+
const present = new Set(current.split(/\r?\n/).map((line) => line.trim()));
|
|
248
|
+
const missing = GENERATED_IGNORES.filter((entry) => !present.has(entry));
|
|
249
|
+
if (missing.length === 0) {
|
|
250
|
+
return;
|
|
67
251
|
}
|
|
252
|
+
const base = current === '' ? '' : `${current.replace(/\s*$/, '')}\n`;
|
|
253
|
+
await fs.writeFile(file, `${base}${missing.join('\n')}\n`, 'utf8');
|
|
68
254
|
}
|
|
69
255
|
async function installPostMergeHook(root) {
|
|
70
256
|
if (!(await exists(path.join(root, '.git')))) {
|
|
@@ -102,9 +288,13 @@ async function resolveSelection(args, ctx) {
|
|
|
102
288
|
};
|
|
103
289
|
}
|
|
104
290
|
export async function cmdInit(args, ctx) {
|
|
291
|
+
const sourceCheckout = await isEngineCheckout(ctx.root);
|
|
105
292
|
let selection;
|
|
106
293
|
try {
|
|
107
294
|
selection = await resolveSelection(args, ctx);
|
|
295
|
+
if (selection.migrate && !sourceCheckout) {
|
|
296
|
+
throw new Error('--migrate solo aplica en el checkout de desarrollo; la instalación no tiene .planning/');
|
|
297
|
+
}
|
|
108
298
|
}
|
|
109
299
|
catch (error) {
|
|
110
300
|
ctx.io.err(error.message);
|
|
@@ -118,7 +308,22 @@ export async function cmdInit(args, ctx) {
|
|
|
118
308
|
targets: selection.targets,
|
|
119
309
|
guardMode: 'shadow',
|
|
120
310
|
};
|
|
121
|
-
const
|
|
311
|
+
const engine = await loadEngine();
|
|
312
|
+
let existing;
|
|
313
|
+
if (await exists(path.join(ctx.root, 'manifest.yaml'))) {
|
|
314
|
+
existing = (await loadManifestContext(ctx.root).catch(() => null))?.manifest;
|
|
315
|
+
}
|
|
316
|
+
let spec;
|
|
317
|
+
try {
|
|
318
|
+
spec = await buildSelection(args, selection.preset, engine, existing);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
ctx.io.err(error.message);
|
|
322
|
+
return 2;
|
|
323
|
+
}
|
|
324
|
+
const includeMcp = await exists(path.join(ctx.root, 'mcp-manifest.yaml'));
|
|
325
|
+
const assets = await buildAssetPlan(spec, engine, selection.targets, includeMcp);
|
|
326
|
+
const write = [RC_NAME, 'manifest.yaml', '.gitignore', '.ai-hub/'];
|
|
122
327
|
if (selection.migrate) {
|
|
123
328
|
write.push('.planning/state.json');
|
|
124
329
|
}
|
|
@@ -127,6 +332,11 @@ export async function cmdInit(args, ctx) {
|
|
|
127
332
|
project: id,
|
|
128
333
|
preset: selection.preset.id,
|
|
129
334
|
targets: selection.targets,
|
|
335
|
+
...assets,
|
|
336
|
+
skillCount: 17 + spec.skills.length,
|
|
337
|
+
agentCount: spec.agents.length,
|
|
338
|
+
ruleCount: spec.rules.length + 1,
|
|
339
|
+
mcp: includeMcp,
|
|
130
340
|
migrate: selection.migrate,
|
|
131
341
|
write,
|
|
132
342
|
};
|
|
@@ -138,44 +348,100 @@ export async function cmdInit(args, ctx) {
|
|
|
138
348
|
}
|
|
139
349
|
return 0;
|
|
140
350
|
}
|
|
141
|
-
if (
|
|
351
|
+
if (existing) {
|
|
142
352
|
await createBackup(ctx.root, 'init');
|
|
143
353
|
}
|
|
144
|
-
const materialized = await
|
|
145
|
-
const
|
|
146
|
-
const manifest = manifestFromPreset(id, selection.preset, selection.targets, selection.profilePackageManager, rc.guardMode ?? 'shadow', includeMcp);
|
|
354
|
+
const materialized = await materializeSelection(ctx.root, selection.preset, spec);
|
|
355
|
+
const manifest = manifestFromSelection(id, selection.preset, selection.targets, selection.profilePackageManager, rc.guardMode ?? 'shadow', includeMcp, spec);
|
|
147
356
|
await writeManifest(ctx.root, manifest);
|
|
148
357
|
await writeInventory(ctx.root);
|
|
358
|
+
const sync = await runSync(ctx.root, { check: false });
|
|
149
359
|
const existingRc = readAiHubRc(ctx.cwd);
|
|
150
360
|
if (!existingRc || existingRc.version === 1 || args.flags.force) {
|
|
151
361
|
writeAiHubRc(ctx.cwd, rc);
|
|
152
362
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
catch (error) {
|
|
157
|
-
if (error instanceof StateNotFoundError) {
|
|
158
|
-
await writeState(ctx.root, emptyState(id, selection.preset.id));
|
|
363
|
+
if (sourceCheckout) {
|
|
364
|
+
try {
|
|
365
|
+
await readState(ctx.root);
|
|
159
366
|
}
|
|
160
|
-
|
|
161
|
-
|
|
367
|
+
catch (error) {
|
|
368
|
+
if (error instanceof StateNotFoundError) {
|
|
369
|
+
await writeState(ctx.root, emptyState(id, selection.preset.id));
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (selection.migrate) {
|
|
376
|
+
await migrateLegacyState(ctx.root, id, selection.preset.id);
|
|
162
377
|
}
|
|
163
|
-
}
|
|
164
|
-
if (selection.migrate) {
|
|
165
|
-
await migrateLegacyState(ctx.root, id, selection.preset.id);
|
|
166
378
|
}
|
|
167
379
|
await ensureProjectIgnores(ctx.root);
|
|
168
380
|
if (args.flags.hooks) {
|
|
169
381
|
const hook = await installPostMergeHook(ctx.root);
|
|
170
382
|
ctx.io.out(`hooks: ${hook.installed ? 'post-merge instalado' : hook.reason}`);
|
|
171
383
|
}
|
|
172
|
-
ctx.io.out(`init: ${selection.preset.id} · ${
|
|
384
|
+
ctx.io.out(`init: ${selection.preset.id} · skills=${spec.modes.skills} agents=${spec.modes.agents} rules=${spec.modes.rules} · ${spec.skills.length + 17} skills · ${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
385
|
if (materialized.collisions.length > 0) {
|
|
174
386
|
ctx.io.err(`colisiones (no se sobrescribieron): ${materialized.collisions.join(', ')}`);
|
|
175
387
|
}
|
|
176
388
|
ctx.io.out(`plan: ${plan.hash}`);
|
|
177
389
|
return 0;
|
|
178
390
|
}
|
|
391
|
+
async function selectionForInstall(args, manifest, preset, engine) {
|
|
392
|
+
const fullFlag = Boolean(args.flags.full);
|
|
393
|
+
const presetOnly = Boolean(args.flags['preset-only']);
|
|
394
|
+
if (fullFlag && presetOnly) {
|
|
395
|
+
throw new Error('--full y --preset-only son incompatibles');
|
|
396
|
+
}
|
|
397
|
+
const exclusions = parseExclusions(args);
|
|
398
|
+
const modes = {
|
|
399
|
+
skills: exclusions.skills.size > 0
|
|
400
|
+
? 'custom'
|
|
401
|
+
: presetOnly
|
|
402
|
+
? 'preset'
|
|
403
|
+
: fullFlag
|
|
404
|
+
? 'full'
|
|
405
|
+
: (manifest.skills.selection ?? 'preset'),
|
|
406
|
+
agents: exclusions.agents.size > 0
|
|
407
|
+
? 'custom'
|
|
408
|
+
: presetOnly
|
|
409
|
+
? 'preset'
|
|
410
|
+
: fullFlag
|
|
411
|
+
? 'full'
|
|
412
|
+
: (manifest.agents.selection ?? 'preset'),
|
|
413
|
+
rules: exclusions.rules.size > 0
|
|
414
|
+
? 'custom'
|
|
415
|
+
: presetOnly
|
|
416
|
+
? 'preset'
|
|
417
|
+
: fullFlag
|
|
418
|
+
? 'full'
|
|
419
|
+
: (manifest.rules.selection ?? 'preset'),
|
|
420
|
+
};
|
|
421
|
+
const catalogSkills = await listCatalogSkills(engine);
|
|
422
|
+
const catalogAgents = await listCatalogAgents();
|
|
423
|
+
const skills = modes.skills === 'full'
|
|
424
|
+
? catalogSkills
|
|
425
|
+
: modes.skills === 'preset'
|
|
426
|
+
? [...preset.skills]
|
|
427
|
+
: [...manifest.skills.publish];
|
|
428
|
+
const agents = modes.agents === 'full'
|
|
429
|
+
? catalogAgents
|
|
430
|
+
: modes.agents === 'preset'
|
|
431
|
+
? [...preset.agents]
|
|
432
|
+
: [...manifest.agents.publish];
|
|
433
|
+
const rulesBase = modes.rules === 'full'
|
|
434
|
+
? expectedRuleIds('full', preset)
|
|
435
|
+
: modes.rules === 'preset'
|
|
436
|
+
? expectedRuleIds('preset', preset)
|
|
437
|
+
: [...manifest.rules.publish];
|
|
438
|
+
return {
|
|
439
|
+
modes,
|
|
440
|
+
skills: [...new Set(skills.filter((id) => !exclusions.skills.has(id)))].sort(),
|
|
441
|
+
agents: [...new Set(agents.filter((id) => !exclusions.agents.has(id)))].sort(),
|
|
442
|
+
rules: [...new Set(filterRules(rulesBase, exclusions.rules))].sort(),
|
|
443
|
+
};
|
|
444
|
+
}
|
|
179
445
|
export async function cmdInstall(args, ctx) {
|
|
180
446
|
let context;
|
|
181
447
|
try {
|
|
@@ -185,16 +451,50 @@ export async function cmdInstall(args, ctx) {
|
|
|
185
451
|
ctx.io.err(error.message);
|
|
186
452
|
return 1;
|
|
187
453
|
}
|
|
454
|
+
let spec;
|
|
455
|
+
try {
|
|
456
|
+
spec = await selectionForInstall(args, context.manifest, context.preset, context.engine);
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
ctx.io.err(error.message);
|
|
460
|
+
return 2;
|
|
461
|
+
}
|
|
462
|
+
const includeMcp = await exists(path.join(ctx.root, 'mcp-manifest.yaml'));
|
|
188
463
|
if (args.flags['dry-run']) {
|
|
464
|
+
const assets = await buildAssetPlan(spec, context.engine, context.manifest.targets, includeMcp);
|
|
189
465
|
ctx.io.out(JSON.stringify({
|
|
190
466
|
dryRun: true,
|
|
191
467
|
action: 'install',
|
|
192
468
|
preset: context.preset.id,
|
|
193
469
|
targets: context.manifest.targets,
|
|
470
|
+
selection: spec.modes,
|
|
471
|
+
skills: spec.skills.length,
|
|
472
|
+
agents: spec.agents.length,
|
|
473
|
+
rules: spec.rules.length + 1,
|
|
474
|
+
destinations: assets.destinations,
|
|
194
475
|
}, null, 2));
|
|
195
476
|
return 0;
|
|
196
477
|
}
|
|
197
|
-
const materialized = await
|
|
478
|
+
const materialized = await materializeSelection(ctx.root, context.preset, spec);
|
|
479
|
+
const next = {
|
|
480
|
+
...context.manifest,
|
|
481
|
+
skills: { ...context.manifest.skills, publish: spec.skills, selection: spec.modes.skills },
|
|
482
|
+
agents: { ...context.manifest.agents, publish: spec.agents, selection: spec.modes.agents },
|
|
483
|
+
rules: { ...context.manifest.rules, publish: spec.rules, selection: spec.modes.rules },
|
|
484
|
+
};
|
|
485
|
+
if (includeMcp) {
|
|
486
|
+
next.mcp = { source: 'mcp-manifest.yaml' };
|
|
487
|
+
}
|
|
488
|
+
else {
|
|
489
|
+
delete next.mcp;
|
|
490
|
+
}
|
|
491
|
+
if (hasFullDimension(next)) {
|
|
492
|
+
next.catalog = catalogSeal(next);
|
|
493
|
+
}
|
|
494
|
+
else {
|
|
495
|
+
delete next.catalog;
|
|
496
|
+
}
|
|
497
|
+
await writeManifest(ctx.root, next);
|
|
198
498
|
const sync = await runSync(ctx.root, { check: false });
|
|
199
499
|
await writeInventory(ctx.root);
|
|
200
500
|
await ensureProjectIgnores(ctx.root);
|
|
@@ -202,32 +502,14 @@ export async function cmdInstall(args, ctx) {
|
|
|
202
502
|
const hook = await installPostMergeHook(ctx.root);
|
|
203
503
|
ctx.io.out(`hooks: ${hook.installed ? 'post-merge instalado' : hook.reason}`);
|
|
204
504
|
}
|
|
205
|
-
ctx.io.out(`install: ${materialized.written.length} nuevos · ${materialized.kept.length} conservados · ${sync.written.length} sincronizados`);
|
|
505
|
+
ctx.io.out(`install: skills=${spec.modes.skills} agents=${spec.modes.agents} rules=${spec.modes.rules} · ${spec.skills.length + 17} skills · ${spec.agents.length} agentes · ${spec.rules.length + 1} reglas · ${materialized.written.length} nuevos · ${materialized.kept.length} conservados · ${sync.written.length} sincronizados`);
|
|
506
|
+
if (!Object.values(spec.modes).includes('full')) {
|
|
507
|
+
ctx.io.out('full: no aplica; usa install --full para expandir el catálogo');
|
|
508
|
+
}
|
|
206
509
|
return 0;
|
|
207
510
|
}
|
|
208
511
|
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;
|
|
512
|
+
return cmdInstall(args, ctx);
|
|
231
513
|
}
|
|
232
514
|
export async function availablePresets() {
|
|
233
515
|
return listPresetIds();
|
package/dist/cli/run.js
CHANGED
|
@@ -101,8 +101,8 @@ async function cmdCheck(_args, ctx) {
|
|
|
101
101
|
}
|
|
102
102
|
export const COMMANDS = {
|
|
103
103
|
init: { summary: 'Detecta el stack, elige preset y aplica un plan reproducible', handler: cmdInit },
|
|
104
|
-
install: { summary: 'Materializa el preset y sincroniza
|
|
105
|
-
add: { summary: 'Añade una skill o
|
|
104
|
+
install: { summary: 'Materializa el catálogo o el preset según la selección y sincroniza', handler: cmdInstall },
|
|
105
|
+
add: { summary: 'Añade una skill o cualquier agente del catálogo', handler: cmdAdd },
|
|
106
106
|
sync: { summary: 'Proyecta las fuentes a los destinos; --check informa deriva', handler: cmdSync },
|
|
107
107
|
drift: { summary: 'Informa deriva de hash sin escribir', handler: cmdDrift },
|
|
108
108
|
list: { summary: 'Lista preset, targets, comandos, skills y agentes', handler: cmdList },
|
|
@@ -110,7 +110,7 @@ export const COMMANDS = {
|
|
|
110
110
|
check: { summary: 'Alias de doctor', handler: cmdCheck },
|
|
111
111
|
backups: { summary: 'Lista los snapshots disponibles', handler: cmdBackups },
|
|
112
112
|
restore: { summary: 'Restaura un snapshot', handler: cmdRestore },
|
|
113
|
-
uninstall: { summary: 'Elimina una skill o
|
|
113
|
+
uninstall: { summary: 'Elimina una skill, agente o regla añadida', handler: cmdUninstall },
|
|
114
114
|
purge: { summary: 'Elimina el arnés tras crear un snapshot', handler: cmdPurge },
|
|
115
115
|
upgrade: { summary: 'Actualiza el paquete y re-sincroniza', handler: cmdUpgrade },
|
|
116
116
|
repair: { summary: 'Reaplica solo lo que falta o está roto', handler: cmdRepair },
|
|
@@ -132,6 +132,8 @@ export function printHelp(io) {
|
|
|
132
132
|
io.out(` ${name.padEnd(10)} ${COMMANDS[name].summary}`);
|
|
133
133
|
}
|
|
134
134
|
io.out('');
|
|
135
|
+
io.out('add agent acepta cualquier agente del catálogo, no solo los del preset.');
|
|
136
|
+
io.out('Publicación: prepublishOnly ejecuta build, manifest:build y npm test (unit + integración).');
|
|
135
137
|
io.out('Compatibilidad: verify, judge (fuera del catálogo).');
|
|
136
138
|
}
|
|
137
139
|
export async function createContext(cwd, io) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ejecuta `fn` sobre `items` con un máximo de promesas en vuelo, preservando
|
|
3
|
+
* el orden de los resultados. Se usa para el I/O masivo de sync y materialización.
|
|
4
|
+
*/
|
|
5
|
+
export declare function mapLimit<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ejecuta `fn` sobre `items` con un máximo de promesas en vuelo, preservando
|
|
3
|
+
* el orden de los resultados. Se usa para el I/O masivo de sync y materialización.
|
|
4
|
+
*/
|
|
5
|
+
export async function mapLimit(items, limit, fn) {
|
|
6
|
+
if (items.length === 0) {
|
|
7
|
+
return [];
|
|
8
|
+
}
|
|
9
|
+
const results = new Array(items.length);
|
|
10
|
+
let next = 0;
|
|
11
|
+
const workers = Array.from({ length: Math.min(Math.max(limit, 1), items.length) }, async () => {
|
|
12
|
+
for (;;) {
|
|
13
|
+
const index = next;
|
|
14
|
+
next += 1;
|
|
15
|
+
if (index >= items.length) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
results[index] = await fn(items[index], index);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
await Promise.all(workers);
|
|
22
|
+
return results;
|
|
23
|
+
}
|
package/dist/engine/gates.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { isEngineCheckout } from '../source-checkout.js';
|
|
4
|
+
import { ENGINE_AGENTS_DIR, catalogIndex } from '../catalog.js';
|
|
5
|
+
import { ENGINE_SKILLS_DIR } from './engine.js';
|
|
4
6
|
const LEGACY_SOURCE_DIRS = ['core/presets', 'core/skills', 'core/agents'];
|
|
5
7
|
const LEGACY_TARGET_DIRS = [
|
|
6
8
|
'.claude',
|
|
@@ -16,6 +18,23 @@ const TEXT_FILE = /\.(md|mdc|ya?ml|json|ts|js|mjs|sh|ps1|txt)$/i;
|
|
|
16
18
|
function isLegacyName(name) {
|
|
17
19
|
return name === 'opsx' || name === 'openspec' || name.startsWith('opsx-') || name.startsWith('openspec-');
|
|
18
20
|
}
|
|
21
|
+
async function scanFile(file, relative, findings) {
|
|
22
|
+
const name = path.basename(file);
|
|
23
|
+
if (isLegacyName(name)) {
|
|
24
|
+
findings.push(`${relative} (nombre legacy)`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!TEXT_FILE.test(name)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const text = await fs.readFile(file, 'utf8').catch(() => '');
|
|
31
|
+
for (const marker of LEGACY_CONTENT) {
|
|
32
|
+
if (text.includes(marker)) {
|
|
33
|
+
findings.push(`${relative} (contiene "${marker}")`);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
19
38
|
async function walk(dir, relative, findings) {
|
|
20
39
|
const entries = await fs.readdir(dir, { withFileTypes: true }).catch(() => null);
|
|
21
40
|
if (entries === null) {
|
|
@@ -31,27 +50,46 @@ async function walk(dir, relative, findings) {
|
|
|
31
50
|
await walk(path.join(dir, entry.name), childRelative, findings);
|
|
32
51
|
continue;
|
|
33
52
|
}
|
|
34
|
-
if (
|
|
35
|
-
|
|
53
|
+
if (entry.isFile()) {
|
|
54
|
+
await scanFile(path.join(dir, entry.name), childRelative, findings);
|
|
36
55
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
async function scanInstalledCatalog(findings) {
|
|
59
|
+
const index = await catalogIndex();
|
|
60
|
+
for (const id of index.skills) {
|
|
61
|
+
for (const file of index.skillFiles[id] ?? []) {
|
|
62
|
+
const relative = `@engine/skills/shared/${id}/${path
|
|
63
|
+
.relative(path.join(ENGINE_SKILLS_DIR, id), file)
|
|
64
|
+
.split(path.sep)
|
|
65
|
+
.join('/')}`;
|
|
66
|
+
await scanFile(file, relative, findings);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (const id of index.agents) {
|
|
70
|
+
await scanFile(path.join(ENGINE_AGENTS_DIR, `${id}.md`), `@engine/agents/${id}.md`, findings);
|
|
71
|
+
}
|
|
72
|
+
for (const id of Object.keys(index.ruleFiles)) {
|
|
73
|
+
for (const file of index.ruleFiles[id] ?? []) {
|
|
74
|
+
await scanFile(file, `@engine/rules/${id}.md`, findings);
|
|
43
75
|
}
|
|
44
76
|
}
|
|
45
77
|
}
|
|
46
78
|
export async function scanLegacyArtifacts(root) {
|
|
47
79
|
const findings = [];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
80
|
+
const engineCheckout = await isEngineCheckout(root);
|
|
81
|
+
if (engineCheckout) {
|
|
82
|
+
// En el checkout del motor los destinos de agente son el taller del mantenedor.
|
|
83
|
+
for (const relative of LEGACY_SOURCE_DIRS) {
|
|
84
|
+
await walk(path.join(root, ...relative.split('/')), relative, findings);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// En una instalación se escanean el proyecto, los destinos y el catálogo del paquete.
|
|
89
|
+
for (const relative of ['.ai-hub', ...LEGACY_TARGET_DIRS]) {
|
|
90
|
+
await walk(path.join(root, ...relative.split('/')), relative, findings);
|
|
91
|
+
}
|
|
92
|
+
await scanInstalledCatalog(findings);
|
|
55
93
|
}
|
|
56
94
|
return findings.sort();
|
|
57
95
|
}
|