@soleri/cli 1.9.0 → 1.10.1
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 +4 -0
- package/dist/commands/agent.d.ts +8 -0
- package/dist/commands/agent.js +150 -0
- package/dist/commands/agent.js.map +1 -0
- package/dist/commands/create.js +38 -6
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/install-knowledge.js +65 -3
- package/dist/commands/install-knowledge.js.map +1 -1
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +80 -0
- package/dist/commands/install.js.map +1 -0
- package/dist/commands/pack.d.ts +10 -0
- package/dist/commands/pack.js +512 -0
- package/dist/commands/pack.js.map +1 -0
- package/dist/commands/skills.d.ts +8 -0
- package/dist/commands/skills.js +167 -0
- package/dist/commands/skills.js.map +1 -0
- package/dist/commands/uninstall.d.ts +2 -0
- package/dist/commands/uninstall.js +74 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/hook-packs/installer.d.ts +0 -7
- package/dist/hook-packs/installer.js +1 -14
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +1 -18
- package/dist/hook-packs/registry.d.ts +2 -1
- package/dist/hook-packs/registry.ts +1 -1
- package/dist/main.js +40 -1
- package/dist/main.js.map +1 -1
- package/dist/prompts/archetypes.d.ts +1 -0
- package/dist/prompts/archetypes.js +177 -62
- package/dist/prompts/archetypes.js.map +1 -1
- package/dist/prompts/create-wizard.d.ts +3 -3
- package/dist/prompts/create-wizard.js +99 -50
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/prompts/playbook.d.ts +8 -7
- package/dist/prompts/playbook.js +201 -15
- package/dist/prompts/playbook.js.map +1 -1
- package/dist/utils/checks.d.ts +0 -1
- package/dist/utils/checks.js +1 -1
- package/dist/utils/checks.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/archetypes.test.ts +84 -0
- package/src/__tests__/doctor.test.ts +2 -2
- package/src/__tests__/wizard-e2e.mjs +508 -0
- package/src/commands/agent.ts +181 -0
- package/src/commands/create.ts +152 -104
- package/src/commands/install-knowledge.ts +75 -4
- package/src/commands/install.ts +101 -0
- package/src/commands/pack.ts +585 -0
- package/src/commands/skills.ts +191 -0
- package/src/commands/uninstall.ts +93 -0
- package/src/hook-packs/installer.ts +1 -18
- package/src/hook-packs/registry.ts +1 -1
- package/src/main.ts +42 -1
- package/src/prompts/archetypes.ts +193 -62
- package/src/prompts/create-wizard.ts +117 -61
- package/src/prompts/playbook.ts +207 -21
- package/src/utils/checks.ts +1 -1
- package/code-reviewer/.claude/hookify.focus-ring-required.local.md +0 -21
- package/code-reviewer/.claude/hookify.no-ai-attribution.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-any-types.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-console-log.local.md +0 -21
- package/code-reviewer/.claude/hookify.no-important.local.md +0 -18
- package/code-reviewer/.claude/hookify.no-inline-styles.local.md +0 -21
- package/code-reviewer/.claude/hookify.semantic-html.local.md +0 -18
- package/code-reviewer/.claude/hookify.ux-touch-targets.local.md +0 -18
- package/code-reviewer/.mcp.json +0 -11
- package/code-reviewer/README.md +0 -346
- package/code-reviewer/package-lock.json +0 -4484
- package/code-reviewer/package.json +0 -45
- package/code-reviewer/scripts/copy-assets.js +0 -15
- package/code-reviewer/scripts/setup.sh +0 -130
- package/code-reviewer/skills/brainstorming/SKILL.md +0 -170
- package/code-reviewer/skills/code-patrol/SKILL.md +0 -176
- package/code-reviewer/skills/context-resume/SKILL.md +0 -143
- package/code-reviewer/skills/executing-plans/SKILL.md +0 -201
- package/code-reviewer/skills/fix-and-learn/SKILL.md +0 -164
- package/code-reviewer/skills/health-check/SKILL.md +0 -225
- package/code-reviewer/skills/second-opinion/SKILL.md +0 -142
- package/code-reviewer/skills/systematic-debugging/SKILL.md +0 -230
- package/code-reviewer/skills/verification-before-completion/SKILL.md +0 -170
- package/code-reviewer/skills/writing-plans/SKILL.md +0 -207
- package/code-reviewer/src/__tests__/facades.test.ts +0 -598
- package/code-reviewer/src/activation/activate.ts +0 -125
- package/code-reviewer/src/activation/claude-md-content.ts +0 -217
- package/code-reviewer/src/activation/inject-claude-md.ts +0 -113
- package/code-reviewer/src/extensions/index.ts +0 -47
- package/code-reviewer/src/extensions/ops/example.ts +0 -28
- package/code-reviewer/src/identity/persona.ts +0 -62
- package/code-reviewer/src/index.ts +0 -278
- package/code-reviewer/src/intelligence/data/architecture.json +0 -5
- package/code-reviewer/src/intelligence/data/code-review.json +0 -5
- package/code-reviewer/tsconfig.json +0 -30
- package/code-reviewer/vitest.config.ts +0 -23
|
@@ -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
|
+
}
|