@soleri/cli 1.8.0 → 1.10.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 (57) hide show
  1. package/README.md +4 -0
  2. package/dist/commands/agent.d.ts +8 -0
  3. package/dist/commands/agent.js +150 -0
  4. package/dist/commands/agent.js.map +1 -0
  5. package/dist/commands/create.js +30 -4
  6. package/dist/commands/create.js.map +1 -1
  7. package/dist/commands/install-knowledge.js +65 -3
  8. package/dist/commands/install-knowledge.js.map +1 -1
  9. package/dist/commands/install.d.ts +2 -0
  10. package/dist/commands/install.js +80 -0
  11. package/dist/commands/install.js.map +1 -0
  12. package/dist/commands/pack.d.ts +10 -0
  13. package/dist/commands/pack.js +512 -0
  14. package/dist/commands/pack.js.map +1 -0
  15. package/dist/commands/skills.d.ts +8 -0
  16. package/dist/commands/skills.js +167 -0
  17. package/dist/commands/skills.js.map +1 -0
  18. package/dist/commands/uninstall.d.ts +2 -0
  19. package/dist/commands/uninstall.js +74 -0
  20. package/dist/commands/uninstall.js.map +1 -0
  21. package/dist/hook-packs/installer.d.ts +0 -7
  22. package/dist/hook-packs/installer.js +1 -14
  23. package/dist/hook-packs/installer.js.map +1 -1
  24. package/dist/hook-packs/installer.ts +1 -18
  25. package/dist/hook-packs/registry.d.ts +2 -1
  26. package/dist/hook-packs/registry.ts +1 -1
  27. package/dist/main.js +40 -1
  28. package/dist/main.js.map +1 -1
  29. package/dist/prompts/archetypes.d.ts +1 -0
  30. package/dist/prompts/archetypes.js +177 -32
  31. package/dist/prompts/archetypes.js.map +1 -1
  32. package/dist/prompts/create-wizard.js +105 -60
  33. package/dist/prompts/create-wizard.js.map +1 -1
  34. package/dist/prompts/playbook.d.ts +8 -7
  35. package/dist/prompts/playbook.js +312 -30
  36. package/dist/prompts/playbook.js.map +1 -1
  37. package/dist/utils/checks.d.ts +0 -1
  38. package/dist/utils/checks.js +1 -1
  39. package/dist/utils/checks.js.map +1 -1
  40. package/package.json +1 -1
  41. package/src/__tests__/archetypes.test.ts +84 -0
  42. package/src/__tests__/doctor.test.ts +2 -2
  43. package/src/__tests__/wizard-e2e.mjs +508 -0
  44. package/src/commands/agent.ts +181 -0
  45. package/src/commands/create.ts +146 -104
  46. package/src/commands/install-knowledge.ts +75 -4
  47. package/src/commands/install.ts +101 -0
  48. package/src/commands/pack.ts +585 -0
  49. package/src/commands/skills.ts +191 -0
  50. package/src/commands/uninstall.ts +93 -0
  51. package/src/hook-packs/installer.ts +1 -18
  52. package/src/hook-packs/registry.ts +1 -1
  53. package/src/main.ts +42 -1
  54. package/src/prompts/archetypes.ts +193 -62
  55. package/src/prompts/create-wizard.ts +114 -58
  56. package/src/prompts/playbook.ts +207 -21
  57. package/src/utils/checks.ts +1 -1
@@ -0,0 +1,585 @@
1
+ /**
2
+ * Unified pack CLI — install, list, remove, info, outdated for all pack types.
3
+ *
4
+ * Replaces separate `hooks add-pack`, `install-knowledge`, `skills install`
5
+ * with a single `soleri pack` command family.
6
+ *
7
+ * Resolution order: local path → built-in → npm registry.
8
+ */
9
+
10
+ import { join } from 'node:path';
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import type { Command } from 'commander';
13
+ import * as p from '@clack/prompts';
14
+ import { PackLockfile, inferPackType, resolvePack, checkNpmVersion } from '@soleri/core';
15
+ import type { LockEntry, PackSource } from '@soleri/core';
16
+ import { detectAgent } from '../utils/agent-context.js';
17
+
18
+ const LOCKFILE_NAME = 'soleri.lock';
19
+
20
+ function getLockfilePath(): string {
21
+ const ctx = detectAgent();
22
+ if (!ctx) {
23
+ p.log.error('No agent project detected in current directory.');
24
+ process.exit(1);
25
+ }
26
+ return join(ctx.agentPath, LOCKFILE_NAME);
27
+ }
28
+
29
+ function getBuiltinDirs(agentPath: string): string[] {
30
+ const dirs: string[] = [];
31
+ // Check for bundled packs in node_modules
32
+ const nmPacks = join(agentPath, 'node_modules', '@soleri');
33
+ if (existsSync(nmPacks)) {
34
+ dirs.push(nmPacks);
35
+ }
36
+ return dirs;
37
+ }
38
+
39
+ export function registerPack(program: Command): void {
40
+ const pack = program
41
+ .command('pack')
42
+ .description('Manage extension packs (hooks, skills, knowledge, domains)');
43
+
44
+ // ─── list ──────────────────────────────────────────────────
45
+ pack
46
+ .command('list')
47
+ .option('--type <type>', 'Filter by pack type (hooks, skills, knowledge, domain, bundle)')
48
+ .description('List installed packs')
49
+ .action((opts: { type?: string }) => {
50
+ const lockfile = new PackLockfile(getLockfilePath());
51
+ let entries = lockfile.list();
52
+
53
+ if (opts.type) {
54
+ entries = entries.filter((e) => e.type === opts.type);
55
+ }
56
+
57
+ if (entries.length === 0) {
58
+ p.log.info('No packs installed.');
59
+ return;
60
+ }
61
+
62
+ p.log.info(`${entries.length} pack(s) installed:\n`);
63
+ for (const entry of entries) {
64
+ const badge =
65
+ entry.source === 'built-in' ? ' [built-in]' : entry.source === 'npm' ? ' [npm]' : '';
66
+ console.log(` ${entry.id}@${entry.version} ${entry.type}${badge}`);
67
+ if (entry.vaultEntries > 0) console.log(` vault: ${entry.vaultEntries} entries`);
68
+ if (entry.skills.length > 0) console.log(` skills: ${entry.skills.join(', ')}`);
69
+ if (entry.hooks.length > 0) console.log(` hooks: ${entry.hooks.join(', ')}`);
70
+ }
71
+ });
72
+
73
+ // ─── install ───────────────────────────────────────────────
74
+ pack
75
+ .command('install')
76
+ .argument('<pack>', 'Pack name, path, or npm package')
77
+ .option('--type <type>', 'Expected pack type (hooks, skills, knowledge, domain)')
78
+ .option('--version <ver>', 'Specific version to install')
79
+ .option('--frozen', 'Fail if pack is not in lockfile (CI mode)')
80
+ .description('Install a pack from local path, built-in, or npm')
81
+ .action(
82
+ async (packName: string, opts: { type?: string; version?: string; frozen?: boolean }) => {
83
+ const lockfilePath = getLockfilePath();
84
+ const lockfile = new PackLockfile(lockfilePath);
85
+ const ctx = detectAgent();
86
+ if (!ctx) return;
87
+
88
+ // Frozen mode — only install from lockfile
89
+ if (opts.frozen) {
90
+ const entry = lockfile.get(packName);
91
+ if (!entry) {
92
+ p.log.error(`Pack "${packName}" not in lockfile. Cannot install in frozen mode.`);
93
+ process.exit(1);
94
+ }
95
+ p.log.info(`Frozen: ${entry.id}@${entry.version} (${entry.source})`);
96
+ return;
97
+ }
98
+
99
+ // Check if already installed
100
+ if (lockfile.has(packName)) {
101
+ p.log.warn(
102
+ `Pack "${packName}" is already installed. Use \`soleri pack update\` to upgrade.`,
103
+ );
104
+ return;
105
+ }
106
+
107
+ const s = p.spinner();
108
+ s.start(`Resolving pack: ${packName}...`);
109
+
110
+ try {
111
+ const resolved = resolvePack(packName, {
112
+ builtinDirs: getBuiltinDirs(ctx.agentPath),
113
+ version: opts.version,
114
+ });
115
+
116
+ s.message(`Installing from ${resolved.source}...`);
117
+
118
+ // Read manifest
119
+ const manifestPath = join(resolved.directory, 'soleri-pack.json');
120
+ if (!existsSync(manifestPath)) {
121
+ s.stop('Install failed');
122
+ p.log.error(`No soleri-pack.json found in ${resolved.directory}`);
123
+ process.exit(1);
124
+ return;
125
+ }
126
+
127
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
128
+ const packType = inferPackType(manifest);
129
+
130
+ // Type check if specified
131
+ if (opts.type && packType !== opts.type && packType !== 'bundle') {
132
+ s.stop('Install failed');
133
+ p.log.error(`Expected pack type "${opts.type}" but got "${packType}"`);
134
+ process.exit(1);
135
+ return;
136
+ }
137
+
138
+ // Count contents
139
+ const vaultDir = join(resolved.directory, manifest.vault?.dir ?? 'vault');
140
+ let vaultEntries = 0;
141
+ if (existsSync(vaultDir)) {
142
+ const { loadIntelligenceData } = await import('@soleri/core');
143
+ const entries = loadIntelligenceData(vaultDir);
144
+ vaultEntries = entries.length;
145
+ }
146
+
147
+ const skillsDir = join(resolved.directory, manifest.skills?.dir ?? 'skills');
148
+ const skills = existsSync(skillsDir) ? listMdFiles(skillsDir) : [];
149
+
150
+ const hooksDir = join(resolved.directory, manifest.hooks?.dir ?? 'hooks');
151
+ const hooks = existsSync(hooksDir) ? listMdFiles(hooksDir) : [];
152
+
153
+ // Create lock entry
154
+ const entry: LockEntry = {
155
+ id: manifest.id,
156
+ version: manifest.version,
157
+ type: packType,
158
+ source: resolved.source as PackSource,
159
+ directory: resolved.directory,
160
+ integrity: PackLockfile.computeIntegrity(manifestPath),
161
+ installedAt: new Date().toISOString(),
162
+ vaultEntries,
163
+ skills,
164
+ hooks,
165
+ facadesRegistered: (manifest.facades?.length ?? 0) > 0,
166
+ };
167
+
168
+ lockfile.set(entry);
169
+ lockfile.save();
170
+
171
+ s.stop(`Installed ${manifest.id}@${manifest.version} (${packType})`);
172
+
173
+ const parts: string[] = [];
174
+ if (vaultEntries > 0) parts.push(`${vaultEntries} vault entries`);
175
+ if (skills.length > 0) parts.push(`${skills.length} skills`);
176
+ if (hooks.length > 0) parts.push(`${hooks.length} hooks`);
177
+ if (parts.length > 0) {
178
+ p.log.info(` Contents: ${parts.join(', ')}`);
179
+ }
180
+ } catch (err) {
181
+ s.stop('Install failed');
182
+ p.log.error(err instanceof Error ? err.message : String(err));
183
+ process.exit(1);
184
+ }
185
+ },
186
+ );
187
+
188
+ // ─── remove ────────────────────────────────────────────────
189
+ pack
190
+ .command('remove')
191
+ .argument('<packId>', 'Pack ID to remove')
192
+ .description('Remove an installed pack')
193
+ .action((packId: string) => {
194
+ const lockfile = new PackLockfile(getLockfilePath());
195
+
196
+ if (!lockfile.has(packId)) {
197
+ p.log.error(`Pack "${packId}" is not installed.`);
198
+ process.exit(1);
199
+ }
200
+
201
+ lockfile.remove(packId);
202
+ lockfile.save();
203
+ p.log.success(`Removed ${packId}`);
204
+ p.log.info('Note: Vault entries from this pack are preserved in the knowledge base.');
205
+ });
206
+
207
+ // ─── info ──────────────────────────────────────────────────
208
+ pack
209
+ .command('info')
210
+ .argument('<packId>', 'Pack ID')
211
+ .description('Show detailed info about an installed pack')
212
+ .action((packId: string) => {
213
+ const lockfile = new PackLockfile(getLockfilePath());
214
+ const entry = lockfile.get(packId);
215
+
216
+ if (!entry) {
217
+ p.log.error(`Pack "${packId}" is not installed.`);
218
+ process.exit(1);
219
+ return;
220
+ }
221
+
222
+ console.log(`\n Pack: ${entry.id}`);
223
+ console.log(` Version: ${entry.version}`);
224
+ console.log(` Type: ${entry.type}`);
225
+ console.log(` Source: ${entry.source}`);
226
+ console.log(` Directory: ${entry.directory}`);
227
+ console.log(` Installed: ${entry.installedAt}`);
228
+ console.log(` Integrity: ${entry.integrity}`);
229
+ if (entry.vaultEntries > 0) console.log(` Vault: ${entry.vaultEntries} entries`);
230
+ if (entry.skills.length > 0) console.log(` Skills: ${entry.skills.join(', ')}`);
231
+ if (entry.hooks.length > 0) console.log(` Hooks: ${entry.hooks.join(', ')}`);
232
+ console.log('');
233
+ });
234
+
235
+ // ─── outdated ──────────────────────────────────────────────
236
+ pack
237
+ .command('outdated')
238
+ .description('Check for packs with available updates on npm')
239
+ .action(() => {
240
+ const lockfile = new PackLockfile(getLockfilePath());
241
+ const entries = lockfile.list().filter((e) => e.source === 'npm');
242
+
243
+ if (entries.length === 0) {
244
+ p.log.info('No npm-sourced packs installed.');
245
+ return;
246
+ }
247
+
248
+ const s = p.spinner();
249
+ s.start('Checking for updates...');
250
+
251
+ const outdated: Array<{ id: string; current: string; latest: string }> = [];
252
+ for (const entry of entries) {
253
+ const npmPkg = entry.id.startsWith('@') ? entry.id : `@soleri/pack-${entry.id}`;
254
+ const latest = checkNpmVersion(npmPkg);
255
+ if (latest && latest !== entry.version) {
256
+ outdated.push({ id: entry.id, current: entry.version, latest });
257
+ }
258
+ }
259
+
260
+ s.stop(
261
+ outdated.length > 0 ? `${outdated.length} update(s) available` : 'All packs up to date',
262
+ );
263
+
264
+ for (const item of outdated) {
265
+ console.log(` ${item.id} ${item.current} → ${item.latest}`);
266
+ }
267
+ });
268
+
269
+ // ─── update ─────────────────────────────────────────────────
270
+ pack
271
+ .command('update')
272
+ .argument('[packId]', 'Specific pack to update (or all)')
273
+ .option('--force', 'Force update even if version is incompatible')
274
+ .description('Update installed packs to latest compatible version')
275
+ .action((packId: string | undefined, _opts: { force?: boolean }) => {
276
+ const lockfilePath = getLockfilePath();
277
+ const lockfile = new PackLockfile(lockfilePath);
278
+ const ctx = detectAgent();
279
+ if (!ctx) return;
280
+
281
+ let entries = lockfile.list().filter((e) => e.source === 'npm');
282
+ if (packId) {
283
+ entries = entries.filter((e) => e.id === packId);
284
+ if (entries.length === 0) {
285
+ p.log.error(
286
+ lockfile.has(packId)
287
+ ? `Pack "${packId}" is local/built-in and cannot be updated from npm.`
288
+ : `Pack "${packId}" is not installed.`,
289
+ );
290
+ process.exit(1);
291
+ }
292
+ }
293
+
294
+ if (entries.length === 0) {
295
+ p.log.info('No npm-sourced packs to update.');
296
+ return;
297
+ }
298
+
299
+ const s = p.spinner();
300
+ s.start('Checking for updates...');
301
+
302
+ let updated = 0;
303
+ for (const entry of entries) {
304
+ const npmPkg = entry.id.startsWith('@') ? entry.id : `@soleri/pack-${entry.id}`;
305
+ const latest = checkNpmVersion(npmPkg);
306
+ if (!latest || latest === entry.version) continue;
307
+
308
+ // Update lockfile entry with new version
309
+ lockfile.set({ ...entry, version: latest, installedAt: new Date().toISOString() });
310
+ updated++;
311
+ p.log.info(` ${entry.id}: ${entry.version} → ${latest}`);
312
+ }
313
+
314
+ if (updated > 0) {
315
+ lockfile.save();
316
+ s.stop(`Updated ${updated} pack(s)`);
317
+ } else {
318
+ s.stop('All packs up to date');
319
+ }
320
+ });
321
+
322
+ // ─── search ─────────────────────────────────────────────────
323
+ pack
324
+ .command('search')
325
+ .argument('<query>', 'Search term')
326
+ .option('--type <type>', 'Filter by pack type')
327
+ .description('Search for packs on the npm registry')
328
+ .action((query: string) => {
329
+ const s = p.spinner();
330
+ s.start(`Searching npm for "${query}"...`);
331
+
332
+ try {
333
+ const { execFileSync } = require('node:child_process');
334
+ const searchTerm = `soleri-pack-${query}`;
335
+ const result = execFileSync('npm', ['search', searchTerm, '--json'], {
336
+ encoding: 'utf-8',
337
+ timeout: 15_000,
338
+ });
339
+
340
+ const packages = JSON.parse(result || '[]');
341
+ const filtered = packages.filter(
342
+ (pkg: { name: string }) =>
343
+ pkg.name.includes('soleri-pack') || pkg.name.startsWith('@soleri/pack-'),
344
+ );
345
+
346
+ s.stop(filtered.length > 0 ? `Found ${filtered.length} pack(s)` : 'No packs found');
347
+
348
+ for (const pkg of filtered) {
349
+ console.log(` ${pkg.name}@${pkg.version} ${pkg.description || ''}`);
350
+ }
351
+ } catch {
352
+ s.stop('Search failed');
353
+ p.log.warn('Could not search npm registry. Check your network connection.');
354
+ }
355
+ });
356
+
357
+ // ─── create ─────────────────────────────────────────────────
358
+ pack
359
+ .command('create')
360
+ .description('Scaffold a new pack project')
361
+ .action(async () => {
362
+ const name = await p.text({ message: 'Pack name:', placeholder: 'my-react-patterns' });
363
+ if (p.isCancel(name) || !name) return;
364
+
365
+ const packType = await p.select({
366
+ message: 'Pack type:',
367
+ options: [
368
+ { value: 'knowledge', label: 'Knowledge — vault entries, patterns, anti-patterns' },
369
+ { value: 'skills', label: 'Skills — workflow skill files' },
370
+ { value: 'hooks', label: 'Hooks — editor hook files' },
371
+ { value: 'bundle', label: 'Bundle — multiple content types' },
372
+ ],
373
+ });
374
+ if (p.isCancel(packType)) return;
375
+
376
+ const description = await p.text({
377
+ message: 'Description:',
378
+ placeholder: 'Patterns for React hooks and state management',
379
+ });
380
+ if (p.isCancel(description)) return;
381
+
382
+ const author = await p.text({ message: 'Author:', placeholder: '@username' });
383
+ if (p.isCancel(author)) return;
384
+
385
+ const dir = join(process.cwd(), String(name));
386
+ const { mkdirSync, writeFileSync } = require('node:fs');
387
+
388
+ mkdirSync(dir, { recursive: true });
389
+
390
+ // Scaffold manifest
391
+ const manifest: Record<string, unknown> = {
392
+ id: name,
393
+ version: '1.0.0',
394
+ description: description || '',
395
+ author: author || '',
396
+ license: 'MIT',
397
+ soleri: '>=2.0.0',
398
+ };
399
+
400
+ // Scaffold content directories based on type
401
+ if (packType === 'knowledge' || packType === 'bundle') {
402
+ const vaultDir = join(dir, 'vault');
403
+ mkdirSync(vaultDir, { recursive: true });
404
+ writeFileSync(join(vaultDir, 'patterns.json'), JSON.stringify([], null, 2) + '\n', 'utf-8');
405
+ manifest.vault = { dir: 'vault' };
406
+ }
407
+
408
+ if (packType === 'skills' || packType === 'bundle') {
409
+ const skillsDir = join(dir, 'skills');
410
+ mkdirSync(skillsDir, { recursive: true });
411
+ writeFileSync(
412
+ join(skillsDir, 'example.md'),
413
+ `# Example Skill\n\nReplace this with your skill content.\n`,
414
+ 'utf-8',
415
+ );
416
+ manifest.skills = { dir: 'skills' };
417
+ }
418
+
419
+ if (packType === 'hooks' || packType === 'bundle') {
420
+ const hooksDir = join(dir, 'hooks');
421
+ mkdirSync(hooksDir, { recursive: true });
422
+ writeFileSync(
423
+ join(hooksDir, 'example.md'),
424
+ `# Example Hook\n\nReplace this with your hook content.\n`,
425
+ 'utf-8',
426
+ );
427
+ manifest.hooks = { dir: 'hooks' };
428
+ }
429
+
430
+ writeFileSync(
431
+ join(dir, 'soleri-pack.json'),
432
+ JSON.stringify(manifest, null, 2) + '\n',
433
+ 'utf-8',
434
+ );
435
+
436
+ p.log.success(`Created ${name}/`);
437
+ p.log.info(` soleri-pack.json`);
438
+ if (manifest.vault) p.log.info(` vault/patterns.json`);
439
+ if (manifest.skills) p.log.info(` skills/example.md`);
440
+ if (manifest.hooks) p.log.info(` hooks/example.md`);
441
+ p.log.info(`\nNext: edit content, then \`soleri pack validate ${name}/\``);
442
+ });
443
+
444
+ // ─── validate ───────────────────────────────────────────────
445
+ pack
446
+ .command('validate')
447
+ .argument('<path>', 'Path to pack directory')
448
+ .description('Validate a pack before publishing')
449
+ .action((packPath: string) => {
450
+ const { resolve } = require('node:path');
451
+ const dir = resolve(packPath);
452
+ const errors: string[] = [];
453
+ const warnings: string[] = [];
454
+
455
+ // Check manifest exists
456
+ const manifestPath = join(dir, 'soleri-pack.json');
457
+ if (!existsSync(manifestPath)) {
458
+ p.log.error(`No soleri-pack.json found at ${dir}`);
459
+ process.exit(1);
460
+ }
461
+
462
+ let manifest: Record<string, unknown>;
463
+ try {
464
+ manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
465
+ } catch {
466
+ p.log.error('Invalid JSON in soleri-pack.json');
467
+ process.exit(1);
468
+ return;
469
+ }
470
+
471
+ // Required fields
472
+ if (!manifest.id || typeof manifest.id !== 'string')
473
+ errors.push('Missing or invalid "id" field');
474
+ if (!manifest.version || typeof manifest.version !== 'string')
475
+ errors.push('Missing or invalid "version" field');
476
+ if (manifest.version && !/^\d+\.\d+\.\d+/.test(manifest.version as string))
477
+ errors.push('Version must be valid semver (e.g., 1.0.0)');
478
+ if (!manifest.soleri) warnings.push('Missing "soleri" compatibility range');
479
+
480
+ // Naming convention
481
+ const id = manifest.id as string;
482
+ if (id && !id.match(/^[@a-z0-9][\w./-]*$/i)) {
483
+ errors.push(`Pack id "${id}" contains invalid characters`);
484
+ }
485
+
486
+ // Content directories exist
487
+ const packType = inferPackType(
488
+ manifest as { vault?: unknown; skills?: unknown; hooks?: unknown },
489
+ );
490
+ if (manifest.vault) {
491
+ const vaultDir = join(dir, (manifest.vault as { dir?: string }).dir || 'vault');
492
+ if (!existsSync(vaultDir)) errors.push(`Vault directory not found: ${vaultDir}`);
493
+ }
494
+ if (manifest.skills) {
495
+ const skillsDir = join(dir, (manifest.skills as { dir?: string }).dir || 'skills');
496
+ if (!existsSync(skillsDir)) errors.push(`Skills directory not found: ${skillsDir}`);
497
+ }
498
+ if (manifest.hooks) {
499
+ const hooksDir = join(dir, (manifest.hooks as { dir?: string }).dir || 'hooks');
500
+ if (!existsSync(hooksDir)) errors.push(`Hooks directory not found: ${hooksDir}`);
501
+ }
502
+
503
+ // Report
504
+ if (errors.length > 0) {
505
+ p.log.error(`Validation failed (${errors.length} error(s)):`);
506
+ for (const err of errors) console.log(` ✗ ${err}`);
507
+ if (warnings.length > 0) {
508
+ for (const warn of warnings) console.log(` ⚠ ${warn}`);
509
+ }
510
+ process.exit(1);
511
+ }
512
+
513
+ if (warnings.length > 0) {
514
+ for (const warn of warnings) p.log.warn(warn);
515
+ }
516
+
517
+ p.log.success(`Pack "${id}" v${manifest.version} (${packType}) is valid`);
518
+ });
519
+
520
+ // ─── publish ────────────────────────────────────────────────
521
+ pack
522
+ .command('publish')
523
+ .argument('[path]', 'Path to pack directory', '.')
524
+ .option('--dry-run', 'Show what would be published without publishing')
525
+ .description('Publish pack to npm registry')
526
+ .action((packPath: string, opts: { dryRun?: boolean }) => {
527
+ const { resolve } = require('node:path');
528
+ const { execFileSync } = require('node:child_process');
529
+ const dir = resolve(packPath);
530
+
531
+ // Validate first
532
+ const manifestPath = join(dir, 'soleri-pack.json');
533
+ if (!existsSync(manifestPath)) {
534
+ p.log.error(`No soleri-pack.json found at ${dir}`);
535
+ process.exit(1);
536
+ }
537
+
538
+ // Check for package.json (needed for npm publish)
539
+ const pkgPath = join(dir, 'package.json');
540
+ if (!existsSync(pkgPath)) {
541
+ // Auto-generate from manifest
542
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
543
+ const pkg = {
544
+ name: manifest.id.startsWith('@') ? manifest.id : `soleri-pack-${manifest.id}`,
545
+ version: manifest.version,
546
+ description: manifest.description || '',
547
+ keywords: ['soleri', 'soleri-pack', manifest.type || 'knowledge'].filter(Boolean),
548
+ files: ['soleri-pack.json', 'vault', 'skills', 'hooks'].filter((f) =>
549
+ existsSync(join(dir, f)),
550
+ ),
551
+ };
552
+ const { writeFileSync } = require('node:fs');
553
+ writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
554
+ p.log.info('Generated package.json from manifest');
555
+ }
556
+
557
+ const s = p.spinner();
558
+ s.start(opts.dryRun ? 'Dry run...' : 'Publishing to npm...');
559
+
560
+ try {
561
+ const args = ['publish', dir, '--access', 'public'];
562
+ if (opts.dryRun) args.push('--dry-run');
563
+ execFileSync('npm', args, { stdio: 'pipe', timeout: 60_000 });
564
+ s.stop(opts.dryRun ? 'Dry run complete' : 'Published successfully');
565
+ } catch (err) {
566
+ s.stop('Publish failed');
567
+ p.log.error(err instanceof Error ? err.message : String(err));
568
+ process.exit(1);
569
+ }
570
+ });
571
+ }
572
+
573
+ // ─── Helpers ──────────────────────────────────────────────────────────
574
+
575
+ function listMdFiles(dir: string): string[] {
576
+ try {
577
+ const { readdirSync } = require('node:fs');
578
+ const { basename } = require('node:path');
579
+ return readdirSync(dir)
580
+ .filter((f: string) => f.endsWith('.md'))
581
+ .map((f: string) => basename(f, '.md'));
582
+ } catch {
583
+ return [];
584
+ }
585
+ }