@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.
@@ -5,15 +5,25 @@ 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, readScaffold, SCAFFOLD_RELATIVE, } from '../presets/materialize.js';
9
9
  import { ENGINE_SKILLS_DIR } from '../engine/engine.js';
10
+ import { 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
+ function markCustom(manifest, dimension) {
19
+ manifest[dimension].selection = 'custom';
20
+ if (hasFullDimension(manifest)) {
21
+ manifest.catalog = catalogSeal(manifest);
22
+ }
23
+ else {
24
+ delete manifest.catalog;
25
+ }
26
+ }
17
27
  export async function cmdList(args, ctx) {
18
28
  let context;
19
29
  try {
@@ -24,11 +34,18 @@ export async function cmdList(args, ctx) {
24
34
  return 1;
25
35
  }
26
36
  const { manifest, engine, preset } = context;
37
+ const mode = manifest.skills.selection ?? 'preset';
27
38
  const data = {
28
39
  project: manifest.project,
29
40
  preset: { id: preset.id, version: preset.version, chain: preset.chain },
30
41
  targets: manifest.targets,
42
+ selection: {
43
+ skills: manifest.skills.selection ?? 'preset',
44
+ agents: manifest.agents.selection ?? 'preset',
45
+ rules: manifest.rules.selection ?? 'preset',
46
+ },
31
47
  commands: engine.commands.map((command) => ({ id: command.id, lane: command.lane, role: command.role })),
48
+ nativeSkills: engine.commands.map((command) => command.skill),
32
49
  skills: manifest.skills.publish,
33
50
  agents: manifest.agents.publish,
34
51
  rules: manifest.rules.publish,
@@ -40,10 +57,11 @@ export async function cmdList(args, ctx) {
40
57
  ctx.io.out(`project: ${manifest.project.id} (${manifest.project.name})`);
41
58
  ctx.io.out(`preset: ${preset.id}@${preset.version} [${preset.chain.join(' -> ')}]`);
42
59
  ctx.io.out(`targets: ${manifest.targets.join(', ')}`);
60
+ ctx.io.out(`selection: skills=${data.selection.skills} agents=${data.selection.agents} rules=${data.selection.rules}`);
43
61
  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'}`);
62
+ ctx.io.out(`skills: 17 nativas + ${data.skills.length} de proyecto (${mode})`);
45
63
  ctx.io.out(`agents (${data.agents.length}): ${data.agents.join(', ')}`);
46
- ctx.io.out(`rules (${data.rules.length}): ${data.rules.join(', ')}`);
64
+ ctx.io.out(`rules: ${data.rules.length + 1} universales (${data.rules.length} materializadas + preflight)`);
47
65
  return 0;
48
66
  }
49
67
  export async function cmdAdd(args, ctx) {
@@ -52,6 +70,10 @@ export async function cmdAdd(args, ctx) {
52
70
  ctx.io.err('uso: ai-hub add <skill|agent> <id>');
53
71
  return 2;
54
72
  }
73
+ if (parsed.kind === 'rule') {
74
+ ctx.io.err('las reglas universales se seleccionan con --exclude rule:<id> en init; no se añaden con add');
75
+ return 2;
76
+ }
55
77
  let context;
56
78
  try {
57
79
  context = await loadManifestContext(ctx.root);
@@ -64,8 +86,17 @@ export async function cmdAdd(args, ctx) {
64
86
  const id = parsed.id;
65
87
  const engineSkill = context.engine.commands.some((command) => command.skill === id);
66
88
  const catalogSkill = await exists(path.join(ENGINE_SKILLS_DIR, id, 'SKILL.md'));
89
+ const catalogAgent = await exists(path.join(ENGINE_AGENTS, `${id}.md`));
67
90
  const kind = parsed.kind ??
68
- (preset.skills.includes(id) || engineSkill || catalogSkill ? 'skill' : 'agent');
91
+ (preset.skills.includes(id) || engineSkill || catalogSkill
92
+ ? 'skill'
93
+ : catalogAgent || preset.agents.includes(id)
94
+ ? 'agent'
95
+ : null);
96
+ if (kind === null) {
97
+ ctx.io.err(`no existe "${id}" en el preset ${preset.id} ni en el catálogo del motor`);
98
+ return 1;
99
+ }
69
100
  if (kind === 'skill') {
70
101
  if (engineSkill) {
71
102
  ctx.io.out(`"${id}" ya forma parte de las 17 skills del motor`);
@@ -84,29 +115,25 @@ export async function cmdAdd(args, ctx) {
84
115
  ctx.io.out(JSON.stringify({ dryRun: true, action: 'add', kind, id, destination }, null, 2));
85
116
  return 0;
86
117
  }
87
- if (!(await exists(destination))) {
88
- await fs.cp(source, destination, { recursive: true });
89
- }
118
+ await materializeSkill(ctx.root, id, source, fromPreset ? `@preset/${preset.id}/skills/${id}` : `@engine/skills/shared/${id}`);
90
119
  manifest.skills.publish = [...new Set([...manifest.skills.publish, id])];
91
120
  }
92
121
  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
122
  const fromPreset = await findPresetAsset(preset.chain, `agents/${id}.md`);
98
123
  const source = fromPreset ?? path.join(ENGINE_AGENTS, `${id}.md`);
124
+ if (!(await exists(source))) {
125
+ ctx.io.err(`no existe el agente "${id}" en el catálogo ${ENGINE_AGENTS}`);
126
+ return 1;
127
+ }
99
128
  const destination = path.join(ctx.root, manifest.agents.source, `${id}.md`);
100
129
  if (args.flags['dry-run']) {
101
130
  ctx.io.out(JSON.stringify({ dryRun: true, action: 'add', kind, id, destination }, null, 2));
102
131
  return 0;
103
132
  }
104
- if (!(await exists(destination))) {
105
- await fs.mkdir(path.dirname(destination), { recursive: true });
106
- await fs.copyFile(source, destination);
107
- }
133
+ await materializeAgent(ctx.root, id, source, fromPreset ? `@preset/${preset.id}/agents/${id}.md` : `@engine/agents/${id}.md`);
108
134
  manifest.agents.publish = [...new Set([...manifest.agents.publish, id])];
109
135
  }
136
+ markCustom(manifest, kind === 'agent' ? 'agents' : 'skills');
110
137
  await writeManifest(ctx.root, manifest);
111
138
  const sync = await runSync(ctx.root, { check: false });
112
139
  await writeInventory(ctx.root);
@@ -116,7 +143,7 @@ export async function cmdAdd(args, ctx) {
116
143
  export async function cmdUninstall(args, ctx) {
117
144
  const parsed = splitItem(args);
118
145
  if (!parsed.id) {
119
- ctx.io.err('uso: ai-hub uninstall <skill|agent> <id>');
146
+ ctx.io.err('uso: ai-hub uninstall <skill|agent|rule> <id>');
120
147
  return 2;
121
148
  }
122
149
  let context;
@@ -129,11 +156,22 @@ export async function cmdUninstall(args, ctx) {
129
156
  }
130
157
  const { manifest } = context;
131
158
  const id = parsed.id;
132
- const kind = parsed.kind ?? (manifest.skills.publish.includes(id) ? 'skill' : 'agent');
159
+ const kind = parsed.kind ??
160
+ (manifest.skills.publish.includes(id)
161
+ ? 'skill'
162
+ : manifest.agents.publish.includes(id)
163
+ ? 'agent'
164
+ : manifest.rules.publish.includes(id) || id === 'skill-quality'
165
+ ? 'rule'
166
+ : 'agent');
133
167
  if (kind === 'skill' && context.engine.commands.some((command) => command.skill === id)) {
134
168
  ctx.io.err(`"${id}" es una skill del motor y no se puede desinstalar`);
135
169
  return 1;
136
170
  }
171
+ if (kind === 'rule' && (id === 'core' || id === 'preflight')) {
172
+ ctx.io.err(`la regla base "${id}" no se puede desinstalar`);
173
+ return 1;
174
+ }
137
175
  if (args.flags['dry-run']) {
138
176
  ctx.io.out(JSON.stringify({ dryRun: true, action: 'uninstall', kind, id }, null, 2));
139
177
  return 0;
@@ -142,10 +180,22 @@ export async function cmdUninstall(args, ctx) {
142
180
  if (kind === 'skill') {
143
181
  manifest.skills.publish = manifest.skills.publish.filter((skill) => skill !== id);
144
182
  await removeOwned(ctx.root, path.join(manifest.skills.source, id), true);
183
+ markCustom(manifest, 'skills');
145
184
  }
146
- else {
185
+ else if (kind === 'agent') {
147
186
  manifest.agents.publish = manifest.agents.publish.filter((agent) => agent !== id);
148
187
  await removeOwned(ctx.root, path.join(manifest.agents.source, `${id}.md`), false);
188
+ markCustom(manifest, 'agents');
189
+ }
190
+ else {
191
+ const removed = id === 'skill-quality'
192
+ ? manifest.rules.publish.filter((rule) => rule.startsWith('skill-quality/'))
193
+ : manifest.rules.publish.filter((rule) => rule === id);
194
+ manifest.rules.publish = manifest.rules.publish.filter((rule) => !removed.includes(rule));
195
+ for (const rule of removed) {
196
+ await removeOwned(ctx.root, path.join(manifest.rules.source, `${rule}.md`), false);
197
+ }
198
+ markCustom(manifest, 'rules');
149
199
  }
150
200
  await writeManifest(ctx.root, manifest);
151
201
  const sync = await runSync(ctx.root, { check: false });
@@ -159,9 +209,10 @@ async function removeOwned(root, relative, directory) {
159
209
  return;
160
210
  }
161
211
  const record = await readScaffold(root);
212
+ const relativePosix = relative.split(path.sep).join('/');
162
213
  if (directory) {
163
214
  const files = record.files;
164
- const prefix = `${relative.split(path.sep).join('/')}/`;
215
+ const prefix = `${relativePosix}/`;
165
216
  const owned = Object.entries(files).filter(([name]) => name.startsWith(prefix));
166
217
  if (owned.length === 0) {
167
218
  const marker = await fs.readFile(path.join(absolute, 'SKILL.md'), 'utf8').catch(() => '');
@@ -173,7 +224,8 @@ async function removeOwned(root, relative, directory) {
173
224
  return;
174
225
  }
175
226
  const text = await fs.readFile(absolute, 'utf8').catch(() => null);
176
- if (text === null || text.includes('GENERADO por ai-hub')) {
227
+ const owned = record.files[relativePosix] !== undefined;
228
+ if (text === null || owned || text.includes('GENERADO por ai-hub')) {
177
229
  await fs.rm(absolute, { force: true });
178
230
  }
179
231
  }
@@ -256,6 +308,26 @@ export async function cmdUpgrade(args, ctx) {
256
308
  return 1;
257
309
  }
258
310
  }
311
+ let context;
312
+ try {
313
+ context = await loadManifestContext(ctx.root);
314
+ }
315
+ catch (error) {
316
+ ctx.io.err(error.message);
317
+ return 1;
318
+ }
319
+ const refreshFull = Boolean(args.flags.full) ||
320
+ (hasFullDimension(context.manifest) && !args.flags['preset-only']);
321
+ if (refreshFull) {
322
+ const code = await cmdInstall({
323
+ positionals: [],
324
+ flags: args.flags.full ? { full: true } : {},
325
+ }, ctx);
326
+ if (code === 0) {
327
+ ctx.io.out('upgrade: dimensiones full actualizadas y sello refrescado');
328
+ }
329
+ return code;
330
+ }
259
331
  const result = await runSync(ctx.root, { check: false });
260
332
  ctx.io.out(`upgrade: ${result.written.length} archivos regenerados`);
261
333
  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';
@@ -1,11 +1,30 @@
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
+ agentCount: number;
26
+ ruleCount: number;
27
+ mcp: boolean;
9
28
  migrate: boolean;
10
29
  write: string[];
11
30
  hash: string;