@soleri/cli 9.2.0 → 9.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/hooks.js +36 -7
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/install.js.map +1 -1
- package/dist/hook-packs/installer.js +7 -2
- package/dist/hook-packs/installer.js.map +1 -1
- package/dist/hook-packs/installer.ts +98 -26
- package/dist/hook-packs/registry.js +12 -4
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +21 -7
- package/package.json +1 -1
- package/src/__tests__/hook-packs.test.ts +23 -3
- package/src/__tests__/wizard-e2e.mjs +148 -58
- package/src/commands/hooks.ts +186 -82
- package/src/commands/install.ts +7 -4
- package/src/hook-packs/installer.ts +98 -26
- package/src/hook-packs/registry.ts +21 -7
package/src/commands/install.ts
CHANGED
|
@@ -145,9 +145,10 @@ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean)
|
|
|
145
145
|
const engine = resolveEngineBin();
|
|
146
146
|
servers[agentId] = {
|
|
147
147
|
type: 'local',
|
|
148
|
-
command:
|
|
149
|
-
|
|
150
|
-
|
|
148
|
+
command:
|
|
149
|
+
engine.command === 'node'
|
|
150
|
+
? ['node', engine.bin, '--agent', join(agentDir, 'agent.yaml')]
|
|
151
|
+
: ['npx', '-y', '@soleri/engine', '--agent', join(agentDir, 'agent.yaml')],
|
|
151
152
|
};
|
|
152
153
|
} else {
|
|
153
154
|
servers[agentId] = {
|
|
@@ -234,7 +235,9 @@ export function registerInstall(program: Command): void {
|
|
|
234
235
|
if (engine.command === 'node') {
|
|
235
236
|
p.log.info(`Detected file-tree agent (v7) — using resolved engine at ${engine.bin}`);
|
|
236
237
|
} else {
|
|
237
|
-
p.log.warn(
|
|
238
|
+
p.log.warn(
|
|
239
|
+
`Could not resolve @soleri/core locally — falling back to npx (slower startup)`,
|
|
240
|
+
);
|
|
238
241
|
p.log.info(`For instant startup: npm install -g @soleri/cli`);
|
|
239
242
|
}
|
|
240
243
|
}
|
|
@@ -30,25 +30,33 @@ function resolveHookFiles(packName: string): Map<string, string> {
|
|
|
30
30
|
if (pack.manifest.composedFrom) {
|
|
31
31
|
for (const subPackName of pack.manifest.composedFrom) {
|
|
32
32
|
const subFiles = resolveHookFiles(subPackName);
|
|
33
|
-
for (const [hook, path] of subFiles) {
|
|
33
|
+
for (const [hook, path] of subFiles) {
|
|
34
|
+
files.set(hook, path);
|
|
35
|
+
}
|
|
34
36
|
}
|
|
35
37
|
} else {
|
|
36
38
|
for (const hook of pack.manifest.hooks) {
|
|
37
39
|
const filePath = join(pack.dir, `hookify.${hook}.local.md`);
|
|
38
|
-
if (existsSync(filePath)) {
|
|
40
|
+
if (existsSync(filePath)) {
|
|
41
|
+
files.set(hook, filePath);
|
|
42
|
+
}
|
|
39
43
|
}
|
|
40
44
|
}
|
|
41
45
|
return files;
|
|
42
46
|
}
|
|
43
47
|
|
|
44
|
-
function resolveScripts(
|
|
48
|
+
function resolveScripts(
|
|
49
|
+
packName: string,
|
|
50
|
+
): Map<string, { sourcePath: string; targetDir: string; file: string }> {
|
|
45
51
|
const pack = getPack(packName);
|
|
46
52
|
if (!pack) return new Map();
|
|
47
53
|
const scripts = new Map<string, { sourcePath: string; targetDir: string; file: string }>();
|
|
48
54
|
if (pack.manifest.composedFrom) {
|
|
49
55
|
for (const subPackName of pack.manifest.composedFrom) {
|
|
50
56
|
const subScripts = resolveScripts(subPackName);
|
|
51
|
-
for (const [name, info] of subScripts) {
|
|
57
|
+
for (const [name, info] of subScripts) {
|
|
58
|
+
scripts.set(name, info);
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
} else if (pack.manifest.scripts) {
|
|
54
62
|
for (const script of pack.manifest.scripts) {
|
|
@@ -61,24 +69,39 @@ function resolveScripts(packName: string): Map<string, { sourcePath: string; tar
|
|
|
61
69
|
return scripts;
|
|
62
70
|
}
|
|
63
71
|
|
|
64
|
-
function resolveLifecycleHooks(
|
|
72
|
+
function resolveLifecycleHooks(
|
|
73
|
+
packName: string,
|
|
74
|
+
): { packName: string; hook: HookPackLifecycleHook }[] {
|
|
65
75
|
const pack = getPack(packName);
|
|
66
76
|
if (!pack) return [];
|
|
67
77
|
const hooks: { packName: string; hook: HookPackLifecycleHook }[] = [];
|
|
68
78
|
if (pack.manifest.composedFrom) {
|
|
69
|
-
for (const subPackName of pack.manifest.composedFrom) {
|
|
79
|
+
for (const subPackName of pack.manifest.composedFrom) {
|
|
80
|
+
hooks.push(...resolveLifecycleHooks(subPackName));
|
|
81
|
+
}
|
|
70
82
|
} else if (pack.manifest.lifecycleHooks) {
|
|
71
|
-
for (const hook of pack.manifest.lifecycleHooks) {
|
|
83
|
+
for (const hook of pack.manifest.lifecycleHooks) {
|
|
84
|
+
hooks.push({ packName: pack.manifest.name, hook });
|
|
85
|
+
}
|
|
72
86
|
}
|
|
73
87
|
return hooks;
|
|
74
88
|
}
|
|
75
89
|
|
|
76
|
-
interface SettingsHookEntry {
|
|
90
|
+
interface SettingsHookEntry {
|
|
91
|
+
type: 'command';
|
|
92
|
+
command: string;
|
|
93
|
+
timeout?: number;
|
|
94
|
+
[key: string]: unknown;
|
|
95
|
+
}
|
|
77
96
|
|
|
78
97
|
function readClaudeSettings(claudeDir: string): Record<string, unknown> {
|
|
79
98
|
const settingsPath = join(claudeDir, 'settings.json');
|
|
80
99
|
if (!existsSync(settingsPath)) return {};
|
|
81
|
-
try {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
102
|
+
} catch {
|
|
103
|
+
return {};
|
|
104
|
+
}
|
|
82
105
|
}
|
|
83
106
|
|
|
84
107
|
function writeClaudeSettings(claudeDir: string, settings: Record<string, unknown>): void {
|
|
@@ -86,7 +109,10 @@ function writeClaudeSettings(claudeDir: string, settings: Record<string, unknown
|
|
|
86
109
|
writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
87
110
|
}
|
|
88
111
|
|
|
89
|
-
function addLifecycleHooks(
|
|
112
|
+
function addLifecycleHooks(
|
|
113
|
+
claudeDir: string,
|
|
114
|
+
lifecycleHooks: { packName: string; hook: HookPackLifecycleHook }[],
|
|
115
|
+
): string[] {
|
|
90
116
|
if (lifecycleHooks.length === 0) return [];
|
|
91
117
|
const settings = readClaudeSettings(claudeDir);
|
|
92
118
|
const hooks = (settings['hooks'] ?? {}) as Record<string, unknown>;
|
|
@@ -94,10 +120,18 @@ function addLifecycleHooks(claudeDir: string, lifecycleHooks: { packName: string
|
|
|
94
120
|
for (const { packName: sourcePack, hook } of lifecycleHooks) {
|
|
95
121
|
const eventKey = hook.event;
|
|
96
122
|
const eventHooks = (hooks[eventKey] ?? []) as SettingsHookEntry[];
|
|
97
|
-
const alreadyExists = eventHooks.some(
|
|
123
|
+
const alreadyExists = eventHooks.some(
|
|
124
|
+
(h) => h.command === hook.command && h[PACK_MARKER] === sourcePack,
|
|
125
|
+
);
|
|
98
126
|
if (!alreadyExists) {
|
|
99
|
-
const entry: SettingsHookEntry = {
|
|
100
|
-
|
|
127
|
+
const entry: SettingsHookEntry = {
|
|
128
|
+
type: hook.type,
|
|
129
|
+
command: hook.command,
|
|
130
|
+
[PACK_MARKER]: sourcePack,
|
|
131
|
+
};
|
|
132
|
+
if (hook.timeout) {
|
|
133
|
+
entry.timeout = hook.timeout;
|
|
134
|
+
}
|
|
101
135
|
eventHooks.push(entry);
|
|
102
136
|
hooks[eventKey] = eventHooks;
|
|
103
137
|
added.push(`${eventKey}:${hook.matcher}`);
|
|
@@ -118,10 +152,17 @@ function removeLifecycleHooks(claudeDir: string, packName: string): string[] {
|
|
|
118
152
|
const filtered = eventHooks.filter((h) => h[PACK_MARKER] !== packName);
|
|
119
153
|
if (filtered.length < before) {
|
|
120
154
|
removed.push(eventKey);
|
|
121
|
-
if (filtered.length === 0) {
|
|
155
|
+
if (filtered.length === 0) {
|
|
156
|
+
delete hooks[eventKey];
|
|
157
|
+
} else {
|
|
158
|
+
hooks[eventKey] = filtered;
|
|
159
|
+
}
|
|
122
160
|
}
|
|
123
161
|
}
|
|
124
|
-
if (removed.length > 0) {
|
|
162
|
+
if (removed.length > 0) {
|
|
163
|
+
settings['hooks'] = hooks;
|
|
164
|
+
writeClaudeSettings(claudeDir, settings);
|
|
165
|
+
}
|
|
125
166
|
return removed;
|
|
126
167
|
}
|
|
127
168
|
|
|
@@ -130,7 +171,9 @@ export function installPack(
|
|
|
130
171
|
options?: { projectDir?: string },
|
|
131
172
|
): { installed: string[]; skipped: string[]; scripts: string[]; lifecycleHooks: string[] } {
|
|
132
173
|
const pack = getPack(packName);
|
|
133
|
-
if (!pack) {
|
|
174
|
+
if (!pack) {
|
|
175
|
+
throw new Error(`Unknown hook pack: "${packName}"`);
|
|
176
|
+
}
|
|
134
177
|
const claudeDir = resolveClaudeDir(options?.projectDir);
|
|
135
178
|
mkdirSync(claudeDir, { recursive: true });
|
|
136
179
|
const hookFiles = resolveHookFiles(packName);
|
|
@@ -138,7 +181,12 @@ export function installPack(
|
|
|
138
181
|
const skipped: string[] = [];
|
|
139
182
|
for (const [hook, sourcePath] of hookFiles) {
|
|
140
183
|
const destPath = join(claudeDir, `hookify.${hook}.local.md`);
|
|
141
|
-
if (existsSync(destPath)) {
|
|
184
|
+
if (existsSync(destPath)) {
|
|
185
|
+
skipped.push(hook);
|
|
186
|
+
} else {
|
|
187
|
+
copyFileSync(sourcePath, destPath);
|
|
188
|
+
installed.push(hook);
|
|
189
|
+
}
|
|
142
190
|
}
|
|
143
191
|
const scriptFiles = resolveScripts(packName);
|
|
144
192
|
const installedScripts: string[] = [];
|
|
@@ -160,18 +208,26 @@ export function removePack(
|
|
|
160
208
|
options?: { projectDir?: string },
|
|
161
209
|
): { removed: string[]; scripts: string[]; lifecycleHooks: string[] } {
|
|
162
210
|
const pack = getPack(packName);
|
|
163
|
-
if (!pack) {
|
|
211
|
+
if (!pack) {
|
|
212
|
+
throw new Error(`Unknown hook pack: "${packName}"`);
|
|
213
|
+
}
|
|
164
214
|
const claudeDir = resolveClaudeDir(options?.projectDir);
|
|
165
215
|
const removed: string[] = [];
|
|
166
216
|
for (const hook of pack.manifest.hooks) {
|
|
167
217
|
const filePath = join(claudeDir, `hookify.${hook}.local.md`);
|
|
168
|
-
if (existsSync(filePath)) {
|
|
218
|
+
if (existsSync(filePath)) {
|
|
219
|
+
unlinkSync(filePath);
|
|
220
|
+
removed.push(hook);
|
|
221
|
+
}
|
|
169
222
|
}
|
|
170
223
|
const removedScripts: string[] = [];
|
|
171
224
|
if (pack.manifest.scripts) {
|
|
172
225
|
for (const script of pack.manifest.scripts) {
|
|
173
226
|
const filePath = join(claudeDir, script.targetDir, script.file);
|
|
174
|
-
if (existsSync(filePath)) {
|
|
227
|
+
if (existsSync(filePath)) {
|
|
228
|
+
unlinkSync(filePath);
|
|
229
|
+
removedScripts.push(`${script.targetDir}/${script.file}`);
|
|
230
|
+
}
|
|
175
231
|
}
|
|
176
232
|
}
|
|
177
233
|
if (pack.manifest.composedFrom) {
|
|
@@ -180,14 +236,19 @@ export function removePack(
|
|
|
180
236
|
if (subPack?.manifest.scripts) {
|
|
181
237
|
for (const script of subPack.manifest.scripts) {
|
|
182
238
|
const filePath = join(claudeDir, script.targetDir, script.file);
|
|
183
|
-
if (existsSync(filePath)) {
|
|
239
|
+
if (existsSync(filePath)) {
|
|
240
|
+
unlinkSync(filePath);
|
|
241
|
+
removedScripts.push(`${script.targetDir}/${script.file}`);
|
|
242
|
+
}
|
|
184
243
|
}
|
|
185
244
|
}
|
|
186
245
|
}
|
|
187
246
|
}
|
|
188
247
|
const removedHooks = removeLifecycleHooks(claudeDir, packName);
|
|
189
248
|
if (pack.manifest.composedFrom) {
|
|
190
|
-
for (const subPackName of pack.manifest.composedFrom) {
|
|
249
|
+
for (const subPackName of pack.manifest.composedFrom) {
|
|
250
|
+
removedHooks.push(...removeLifecycleHooks(claudeDir, subPackName));
|
|
251
|
+
}
|
|
191
252
|
}
|
|
192
253
|
return { removed, scripts: removedScripts, lifecycleHooks: removedHooks };
|
|
193
254
|
}
|
|
@@ -203,12 +264,16 @@ export function isPackInstalled(
|
|
|
203
264
|
let present = 0;
|
|
204
265
|
for (const hook of pack.manifest.hooks) {
|
|
205
266
|
total++;
|
|
206
|
-
if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) {
|
|
267
|
+
if (existsSync(join(claudeDir, `hookify.${hook}.local.md`))) {
|
|
268
|
+
present++;
|
|
269
|
+
}
|
|
207
270
|
}
|
|
208
271
|
if (pack.manifest.scripts) {
|
|
209
272
|
for (const script of pack.manifest.scripts) {
|
|
210
273
|
total++;
|
|
211
|
-
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
274
|
+
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
275
|
+
present++;
|
|
276
|
+
}
|
|
212
277
|
}
|
|
213
278
|
}
|
|
214
279
|
if (pack.manifest.composedFrom) {
|
|
@@ -217,7 +282,9 @@ export function isPackInstalled(
|
|
|
217
282
|
if (subPack?.manifest.scripts) {
|
|
218
283
|
for (const script of subPack.manifest.scripts) {
|
|
219
284
|
total++;
|
|
220
|
-
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
285
|
+
if (existsSync(join(claudeDir, script.targetDir, script.file))) {
|
|
286
|
+
present++;
|
|
287
|
+
}
|
|
221
288
|
}
|
|
222
289
|
}
|
|
223
290
|
}
|
|
@@ -230,7 +297,12 @@ export function isPackInstalled(
|
|
|
230
297
|
for (const { packName: sourcePack, hook } of lcHooks) {
|
|
231
298
|
total++;
|
|
232
299
|
const eventHooks = hooksObj[hook.event];
|
|
233
|
-
if (
|
|
300
|
+
if (
|
|
301
|
+
Array.isArray(eventHooks) &&
|
|
302
|
+
eventHooks.some((h) => h.command === hook.command && h[PACK_MARKER] === sourcePack)
|
|
303
|
+
) {
|
|
304
|
+
present++;
|
|
305
|
+
}
|
|
234
306
|
}
|
|
235
307
|
}
|
|
236
308
|
}
|
|
@@ -35,8 +35,12 @@ export interface HookPackManifest {
|
|
|
35
35
|
const __filename = fileURLToPath(import.meta.url);
|
|
36
36
|
const __dirname = dirname(__filename);
|
|
37
37
|
|
|
38
|
-
function getBuiltinRoot(): string {
|
|
39
|
-
|
|
38
|
+
function getBuiltinRoot(): string {
|
|
39
|
+
return __dirname;
|
|
40
|
+
}
|
|
41
|
+
function getLocalRoot(): string {
|
|
42
|
+
return join(process.cwd(), '.soleri', 'hook-packs');
|
|
43
|
+
}
|
|
40
44
|
|
|
41
45
|
function scanPacksDir(root: string, source: 'built-in' | 'local'): HookPackManifest[] {
|
|
42
46
|
if (!existsSync(root)) return [];
|
|
@@ -50,7 +54,9 @@ function scanPacksDir(root: string, source: 'built-in' | 'local'): HookPackManif
|
|
|
50
54
|
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8')) as HookPackManifest;
|
|
51
55
|
manifest.source = source;
|
|
52
56
|
packs.push(manifest);
|
|
53
|
-
} catch {
|
|
57
|
+
} catch {
|
|
58
|
+
/* Skip malformed manifests */
|
|
59
|
+
}
|
|
54
60
|
}
|
|
55
61
|
return packs;
|
|
56
62
|
}
|
|
@@ -72,7 +78,9 @@ export function getPack(name: string): { manifest: HookPackManifest; dir: string
|
|
|
72
78
|
const manifest = JSON.parse(readFileSync(localManifest, 'utf-8')) as HookPackManifest;
|
|
73
79
|
manifest.source = 'local';
|
|
74
80
|
return { manifest, dir: localDir };
|
|
75
|
-
} catch {
|
|
81
|
+
} catch {
|
|
82
|
+
/* Fall through */
|
|
83
|
+
}
|
|
76
84
|
}
|
|
77
85
|
const builtinDir = join(getBuiltinRoot(), name);
|
|
78
86
|
const builtinManifest = join(builtinDir, 'manifest.json');
|
|
@@ -81,7 +89,9 @@ export function getPack(name: string): { manifest: HookPackManifest; dir: string
|
|
|
81
89
|
const manifest = JSON.parse(readFileSync(builtinManifest, 'utf-8')) as HookPackManifest;
|
|
82
90
|
manifest.source = 'built-in';
|
|
83
91
|
return { manifest, dir: builtinDir };
|
|
84
|
-
} catch {
|
|
92
|
+
} catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
export function getInstalledPacks(): string[] {
|
|
@@ -94,14 +104,18 @@ export function getInstalledPacks(): string[] {
|
|
|
94
104
|
const allScripts = pack.scripts.every((script) =>
|
|
95
105
|
existsSync(join(claudeDir, script.targetDir, script.file)),
|
|
96
106
|
);
|
|
97
|
-
if (allScripts) {
|
|
107
|
+
if (allScripts) {
|
|
108
|
+
installed.push(pack.name);
|
|
109
|
+
}
|
|
98
110
|
}
|
|
99
111
|
continue;
|
|
100
112
|
}
|
|
101
113
|
const allPresent = pack.hooks.every((hook) =>
|
|
102
114
|
existsSync(join(claudeDir, `hookify.${hook}.local.md`)),
|
|
103
115
|
);
|
|
104
|
-
if (allPresent) {
|
|
116
|
+
if (allPresent) {
|
|
117
|
+
installed.push(pack.name);
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
return installed;
|
|
107
121
|
}
|