@supa-magic/spm 0.3.0 ā 0.4.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/bin/spm.js +1315 -1110
- package/package.json +6 -7
package/dist/bin/spm.js
CHANGED
|
@@ -1,1171 +1,1376 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from "commander";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync } from "node:fs";
|
|
4
|
-
import { join, dirname, relative, posix, resolve, normalize } from "node:path";
|
|
5
|
-
import { parse, stringify } from "yaml";
|
|
6
3
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
7
|
-
import {
|
|
4
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join, normalize, posix, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { parse, stringify } from "yaml";
|
|
8
7
|
import { tmpdir } from "node:os";
|
|
8
|
+
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
9
9
|
import { createHash } from "node:crypto";
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
//#region src/commands/doctor.ts
|
|
11
|
+
var registerDoctorCommand = (program) => {
|
|
12
|
+
program.command("doctor").description("Check project health and compatibility").action(() => {
|
|
13
|
+
console.log("Not implemented yet");
|
|
14
|
+
});
|
|
14
15
|
};
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/core/config/detect-providers.ts
|
|
18
|
+
var knownProviders = {
|
|
19
|
+
claude: ".claude",
|
|
20
|
+
cursor: ".cursor/rules",
|
|
21
|
+
copilot: ".copilot",
|
|
22
|
+
aider: ".aider",
|
|
23
|
+
codeium: ".codeium",
|
|
24
|
+
cody: ".cody"
|
|
22
25
|
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
var cliCommands = {
|
|
27
|
+
claude: "claude",
|
|
28
|
+
aider: "aider"
|
|
26
29
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
var hasCliInstalled = (command) => {
|
|
31
|
+
try {
|
|
32
|
+
execFileSync(command, ["--version"], {
|
|
33
|
+
stdio: "ignore",
|
|
34
|
+
timeout: 5e3,
|
|
35
|
+
shell: process.platform === "win32"
|
|
36
|
+
});
|
|
37
|
+
return true;
|
|
38
|
+
} catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
38
41
|
};
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
encoding: "utf-8"
|
|
55
|
-
}).trim();
|
|
56
|
-
} catch {
|
|
57
|
-
return void 0;
|
|
58
|
-
}
|
|
42
|
+
var detectProviders = (root) => Object.entries(knownProviders).reduce((providers, [name, path]) => {
|
|
43
|
+
const hasDir = existsSync(join(root, path));
|
|
44
|
+
const cli = cliCommands[name];
|
|
45
|
+
const hasCli = cli ? hasCliInstalled(cli) : false;
|
|
46
|
+
if (hasDir || hasCli) providers[name] = { path };
|
|
47
|
+
return providers;
|
|
48
|
+
}, {});
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/core/config/project-root.ts
|
|
51
|
+
var getGitRoot = () => {
|
|
52
|
+
try {
|
|
53
|
+
return execSync("git rev-parse --show-toplevel", { encoding: "utf-8" }).trim();
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
59
57
|
};
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
58
|
+
var getProjectRoot = () => getGitRoot() ?? process.cwd();
|
|
59
|
+
//#endregion
|
|
60
|
+
//#region src/core/config/spmrc.ts
|
|
61
|
+
var CONFIG_FILE = ".spmrc.yml";
|
|
62
|
+
var getConfigPath = (root) => join(root ?? getProjectRoot(), CONFIG_FILE);
|
|
63
|
+
var createDefaultConfig = (root) => ({
|
|
64
|
+
version: 1,
|
|
65
|
+
providers: detectProviders(root)
|
|
66
66
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
67
|
+
var readConfig = (root) => {
|
|
68
|
+
const resolvedRoot = root ?? getProjectRoot();
|
|
69
|
+
const configPath = getConfigPath(resolvedRoot);
|
|
70
|
+
if (!existsSync(configPath)) {
|
|
71
|
+
const config = createDefaultConfig(resolvedRoot);
|
|
72
|
+
writeConfig(config, resolvedRoot);
|
|
73
|
+
return {
|
|
74
|
+
config,
|
|
75
|
+
created: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
config: parse(readFileSync(configPath, "utf-8")),
|
|
80
|
+
created: false
|
|
81
|
+
};
|
|
77
82
|
};
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
writeFileSync(configPath, stringify(config), "utf-8");
|
|
83
|
+
var writeConfig = (config, root) => {
|
|
84
|
+
writeFileSync(getConfigPath(root), stringify(config), "utf-8");
|
|
81
85
|
};
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const frames = ["ā ", "ā ", "ā ¹", "ā ø", "ā ¼", "ā “", "ā ¦", "ā §", "ā ", "ā "];
|
|
90
|
-
const funMessages = {
|
|
91
|
-
packages: [
|
|
92
|
-
"Resolving dependency tree...",
|
|
93
|
-
"Negotiating versions...",
|
|
94
|
-
"Cross-referencing package manifests...",
|
|
95
|
-
"Validating checksums...",
|
|
96
|
-
"Flattening nested dependencies...",
|
|
97
|
-
"Fetching metadata from the registry...",
|
|
98
|
-
"Comparing lockfile with remote...",
|
|
99
|
-
"Deduplicating shared packages...",
|
|
100
|
-
// ~20% worried
|
|
101
|
-
"This package has 47 peer deps. Deep breath...",
|
|
102
|
-
"The lockfile disagrees. The lockfile is wrong..."
|
|
103
|
-
],
|
|
104
|
-
skills: [
|
|
105
|
-
"Loading skill into working memory...",
|
|
106
|
-
"Parsing skill metadata...",
|
|
107
|
-
"Checking for skill conflicts...",
|
|
108
|
-
"Mapping skill dependencies...",
|
|
109
|
-
"Indexing knowledge base...",
|
|
110
|
-
"Verifying skill signatures...",
|
|
111
|
-
"Wiring skills into a blockchain...",
|
|
112
|
-
"Calibrating expertise level...",
|
|
113
|
-
"Cross-referencing existing skills...",
|
|
114
|
-
"Analyzing project conventions...",
|
|
115
|
-
"Merging skill definitions...",
|
|
116
|
-
"Resolving naming conflicts...",
|
|
117
|
-
"Adapting skills to project rules...",
|
|
118
|
-
"Aligning intentions with context...",
|
|
119
|
-
"Validating output structure...",
|
|
120
|
-
"Making sure skills actually work...",
|
|
121
|
-
"Mapping file dependencies...",
|
|
122
|
-
"Warming up the tubes and transistors...",
|
|
123
|
-
"Reading configuration files...",
|
|
124
|
-
"Scanning project structure...",
|
|
125
|
-
"Writing changes to disk...",
|
|
126
|
-
// ~20% worried
|
|
127
|
-
"Something looks off in the skill graph...",
|
|
128
|
-
"Skill definition is ambiguous. Guessing...",
|
|
129
|
-
"Agent returned something weird. Reading it charitably...",
|
|
130
|
-
"The agent went off-script. Gently correcting..."
|
|
131
|
-
],
|
|
132
|
-
generic: [
|
|
133
|
-
"Warming up the tubes and transistors...",
|
|
134
|
-
"Reading configuration files...",
|
|
135
|
-
"Scanning project structure...",
|
|
136
|
-
"Writing changes to disk...",
|
|
137
|
-
"Tidying up temporary files...",
|
|
138
|
-
"Verifying file integrity...",
|
|
139
|
-
"Syncing state...",
|
|
140
|
-
"Finishing up loose ends...",
|
|
141
|
-
// ~20% worried
|
|
142
|
-
"This should've worked. Looking into it...",
|
|
143
|
-
"That took longer than expected. Moving on..."
|
|
144
|
-
]
|
|
86
|
+
var addConfigEntry = ({ providerPath, kind, name, source }) => {
|
|
87
|
+
const { config } = readConfig();
|
|
88
|
+
const provider = Object.values(config.providers).find((p) => p.path === providerPath);
|
|
89
|
+
if (!provider) throw new Error(`Provider with path "${providerPath}" not found in config`);
|
|
90
|
+
if (!provider[kind]) provider[kind] = {};
|
|
91
|
+
provider[kind][name] = source;
|
|
92
|
+
writeConfig(config);
|
|
145
93
|
};
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
let prevKeptCount = 0;
|
|
156
|
-
let spinnerRendered = false;
|
|
157
|
-
let startTime = 0;
|
|
158
|
-
let funIndex = 0;
|
|
159
|
-
let lastFunSwap = 0;
|
|
160
|
-
let cursorHidden = false;
|
|
161
|
-
let funLineRendered = false;
|
|
162
|
-
let currentFunText = "";
|
|
163
|
-
const writeCursorHide = () => {
|
|
164
|
-
if (!cursorHidden) {
|
|
165
|
-
write(hideCursor);
|
|
166
|
-
cursorHidden = true;
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
const writeCursorShow = () => {
|
|
170
|
-
if (cursorHidden) {
|
|
171
|
-
write(showCursor);
|
|
172
|
-
cursorHidden = false;
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
const clearRendered = () => {
|
|
176
|
-
if (renderedLineCount > 0) {
|
|
177
|
-
write(`\x1B[${renderedLineCount}A\r\x1B[J`);
|
|
178
|
-
renderedLineCount = 0;
|
|
179
|
-
renderedItemCount = 0;
|
|
180
|
-
prevKeptCount = 0;
|
|
181
|
-
spinnerRendered = false;
|
|
182
|
-
funLineRendered = false;
|
|
183
|
-
currentFunText = "";
|
|
184
|
-
}
|
|
185
|
-
};
|
|
186
|
-
const donePrefix = ` ${green}ā¢${reset$2} `;
|
|
187
|
-
const pendingPrefix = ` ${dim$1}ā¢${reset$2} `;
|
|
188
|
-
const render = () => {
|
|
189
|
-
const elapsed = Date.now() - startTime;
|
|
190
|
-
const frame = frames[frameIndex % frames.length];
|
|
191
|
-
if (!spinnerRendered) {
|
|
192
|
-
write(`${cyan}${frame}${reset$2} ${currentText}\x1B[K
|
|
193
|
-
`);
|
|
194
|
-
renderedLineCount++;
|
|
195
|
-
spinnerRendered = true;
|
|
196
|
-
} else {
|
|
197
|
-
const dist = renderedLineCount - prevKeptCount;
|
|
198
|
-
write(
|
|
199
|
-
`\x1B[${dist}A\r${cyan}${frame}${reset$2} ${currentText}\x1B[K\x1B[${dist}B\r`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
202
|
-
items.forEach((it, i) => {
|
|
203
|
-
if (i < renderedItemCount && it.done && !it.renderedDone) {
|
|
204
|
-
const linesUp = renderedLineCount - prevKeptCount - 1 - i;
|
|
205
|
-
write(
|
|
206
|
-
`\x1B[${linesUp}A\r${donePrefix}${it.text}\x1B[K\x1B[${linesUp}B\r`
|
|
207
|
-
);
|
|
208
|
-
it.renderedDone = true;
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
if (renderedItemCount < items.length && funLineRendered) {
|
|
212
|
-
write("\x1B[1A\r\x1B[K");
|
|
213
|
-
renderedLineCount--;
|
|
214
|
-
funLineRendered = false;
|
|
215
|
-
}
|
|
216
|
-
while (renderedItemCount < items.length) {
|
|
217
|
-
const it = items[renderedItemCount];
|
|
218
|
-
const prefix = it.done ? donePrefix : pendingPrefix;
|
|
219
|
-
write(`${prefix}${it.text}\x1B[K
|
|
220
|
-
`);
|
|
221
|
-
if (it.done) it.renderedDone = true;
|
|
222
|
-
renderedItemCount++;
|
|
223
|
-
renderedLineCount++;
|
|
224
|
-
}
|
|
225
|
-
if (elapsed > 1e4) {
|
|
226
|
-
const msgs = funMessages[currentCategory];
|
|
227
|
-
if (lastFunSwap === 0 || Date.now() - lastFunSwap > 1e4) {
|
|
228
|
-
funIndex = (funIndex + 1) % msgs.length;
|
|
229
|
-
lastFunSwap = Date.now();
|
|
230
|
-
}
|
|
231
|
-
const newFunText = msgs[funIndex];
|
|
232
|
-
const funLine = ` ā ${dim$1}${newFunText}${reset$2}`;
|
|
233
|
-
if (!funLineRendered) {
|
|
234
|
-
write(`${funLine}\x1B[K
|
|
235
|
-
`);
|
|
236
|
-
renderedLineCount++;
|
|
237
|
-
funLineRendered = true;
|
|
238
|
-
currentFunText = newFunText;
|
|
239
|
-
} else if (newFunText !== currentFunText) {
|
|
240
|
-
write(`\x1B[1A\r${funLine}\x1B[K\x1B[1B\r`);
|
|
241
|
-
currentFunText = newFunText;
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
frameIndex++;
|
|
245
|
-
};
|
|
246
|
-
const stopInterval = () => {
|
|
247
|
-
if (interval) {
|
|
248
|
-
clearInterval(interval);
|
|
249
|
-
interval = void 0;
|
|
250
|
-
}
|
|
251
|
-
};
|
|
252
|
-
const startInterval = () => {
|
|
253
|
-
stopInterval();
|
|
254
|
-
interval = setInterval(render, 80);
|
|
255
|
-
render();
|
|
256
|
-
};
|
|
257
|
-
const onExit = () => writeCursorShow();
|
|
258
|
-
process.on("exit", onExit);
|
|
259
|
-
return {
|
|
260
|
-
start: (text, category) => {
|
|
261
|
-
writeCursorHide();
|
|
262
|
-
currentText = text;
|
|
263
|
-
currentCategory = category ?? "generic";
|
|
264
|
-
items = [];
|
|
265
|
-
renderedItemCount = 0;
|
|
266
|
-
spinnerRendered = false;
|
|
267
|
-
funLineRendered = false;
|
|
268
|
-
currentFunText = "";
|
|
269
|
-
startTime = Date.now();
|
|
270
|
-
lastFunSwap = 0;
|
|
271
|
-
funIndex = Math.floor(Math.random() * funMessages[currentCategory].length);
|
|
272
|
-
startInterval();
|
|
273
|
-
},
|
|
274
|
-
item: (text) => {
|
|
275
|
-
const last = items.at(-1);
|
|
276
|
-
if (last && !last.done) last.done = true;
|
|
277
|
-
items.push({
|
|
278
|
-
text: text.replace(/^⢠/, ""),
|
|
279
|
-
done: false,
|
|
280
|
-
renderedDone: false
|
|
281
|
-
});
|
|
282
|
-
},
|
|
283
|
-
succeed: (text, dimText) => {
|
|
284
|
-
stopInterval();
|
|
285
|
-
clearRendered();
|
|
286
|
-
const dimPart = dimText ? ` ${dim$1}${dimText}${reset$2}` : "";
|
|
287
|
-
write(`${green}ā${reset$2} ${text}${dimPart}
|
|
288
|
-
`);
|
|
289
|
-
const keptItems = items.length <= 1 ? [] : items;
|
|
290
|
-
keptItems.forEach((it) => {
|
|
291
|
-
write(`${donePrefix}${it.text}\x1B[K
|
|
292
|
-
`);
|
|
293
|
-
});
|
|
294
|
-
prevKeptCount = keptItems.length;
|
|
295
|
-
renderedLineCount = prevKeptCount;
|
|
296
|
-
renderedItemCount = 0;
|
|
297
|
-
items = [];
|
|
298
|
-
spinnerRendered = false;
|
|
299
|
-
},
|
|
300
|
-
fail: (text) => {
|
|
301
|
-
stopInterval();
|
|
302
|
-
clearRendered();
|
|
303
|
-
write(`${red}ā${reset$2} ${text}
|
|
304
|
-
`);
|
|
305
|
-
writeCursorShow();
|
|
306
|
-
},
|
|
307
|
-
stop: () => {
|
|
308
|
-
stopInterval();
|
|
309
|
-
clearRendered();
|
|
310
|
-
writeCursorShow();
|
|
311
|
-
process.removeListener("exit", onExit);
|
|
312
|
-
}
|
|
313
|
-
};
|
|
94
|
+
var removeConfigEntry = ({ providerPath, kind, name }) => {
|
|
95
|
+
const { config } = readConfig();
|
|
96
|
+
const provider = Object.values(config.providers).find((p) => p.path === providerPath);
|
|
97
|
+
if (!provider) throw new Error(`Provider with path "${providerPath}" not found in config`);
|
|
98
|
+
const entries = provider[kind];
|
|
99
|
+
if (!entries || !(name in entries)) return;
|
|
100
|
+
delete entries[name];
|
|
101
|
+
if (Object.keys(entries).length === 0) delete provider[kind];
|
|
102
|
+
writeConfig(config);
|
|
314
103
|
};
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/utils/ansi.ts
|
|
106
|
+
var green = "\x1B[32m";
|
|
107
|
+
var red = "\x1B[31m";
|
|
108
|
+
var cyan = "\x1B[36m";
|
|
109
|
+
var dim$1 = "\x1B[2m";
|
|
110
|
+
var reset$2 = "\x1B[0m";
|
|
111
|
+
var hideCursor = "\x1B[?25l";
|
|
112
|
+
var showCursor = "\x1B[?25h";
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/utils/stepper.ts
|
|
115
|
+
var frames = [
|
|
116
|
+
"ā ",
|
|
117
|
+
"ā ",
|
|
118
|
+
"ā ¹",
|
|
119
|
+
"ā ø",
|
|
120
|
+
"ā ¼",
|
|
121
|
+
"ā “",
|
|
122
|
+
"ā ¦",
|
|
123
|
+
"ā §",
|
|
124
|
+
"ā ",
|
|
125
|
+
"ā "
|
|
126
|
+
];
|
|
127
|
+
var funMessages = {
|
|
128
|
+
packages: [
|
|
129
|
+
"Resolving dependency tree...",
|
|
130
|
+
"Negotiating versions...",
|
|
131
|
+
"Cross-referencing package manifests...",
|
|
132
|
+
"Validating checksums...",
|
|
133
|
+
"Flattening nested dependencies...",
|
|
134
|
+
"Fetching metadata from the registry...",
|
|
135
|
+
"Comparing lockfile with remote...",
|
|
136
|
+
"Deduplicating shared packages...",
|
|
137
|
+
"This package has 47 peer deps. Deep breath...",
|
|
138
|
+
"The lockfile disagrees. The lockfile is wrong..."
|
|
139
|
+
],
|
|
140
|
+
skills: [
|
|
141
|
+
"Loading skill into working memory...",
|
|
142
|
+
"Parsing skill metadata...",
|
|
143
|
+
"Checking for skill conflicts...",
|
|
144
|
+
"Mapping skill dependencies...",
|
|
145
|
+
"Indexing knowledge base...",
|
|
146
|
+
"Verifying skill signatures...",
|
|
147
|
+
"Wiring skills into a blockchain...",
|
|
148
|
+
"Calibrating expertise level...",
|
|
149
|
+
"Cross-referencing existing skills...",
|
|
150
|
+
"Analyzing project conventions...",
|
|
151
|
+
"Merging skill definitions...",
|
|
152
|
+
"Resolving naming conflicts...",
|
|
153
|
+
"Adapting skills to project rules...",
|
|
154
|
+
"Aligning intentions with context...",
|
|
155
|
+
"Validating output structure...",
|
|
156
|
+
"Making sure skills actually work...",
|
|
157
|
+
"Mapping file dependencies...",
|
|
158
|
+
"Warming up the tubes and transistors...",
|
|
159
|
+
"Reading configuration files...",
|
|
160
|
+
"Scanning project structure...",
|
|
161
|
+
"Writing changes to disk...",
|
|
162
|
+
"Something looks off in the skill graph...",
|
|
163
|
+
"Skill definition is ambiguous. Guessing...",
|
|
164
|
+
"Agent returned something weird. Reading it charitably...",
|
|
165
|
+
"The agent went off-script. Gently correcting..."
|
|
166
|
+
],
|
|
167
|
+
generic: [
|
|
168
|
+
"Warming up the tubes and transistors...",
|
|
169
|
+
"Reading configuration files...",
|
|
170
|
+
"Scanning project structure...",
|
|
171
|
+
"Writing changes to disk...",
|
|
172
|
+
"Tidying up temporary files...",
|
|
173
|
+
"Verifying file integrity...",
|
|
174
|
+
"Syncing state...",
|
|
175
|
+
"Finishing up loose ends...",
|
|
176
|
+
"This should've worked. Looking into it...",
|
|
177
|
+
"That took longer than expected. Moving on..."
|
|
178
|
+
]
|
|
334
179
|
};
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
180
|
+
var write = (s) => process.stdout.write(s);
|
|
181
|
+
var createStepper = () => {
|
|
182
|
+
let frameIndex = 0;
|
|
183
|
+
let currentText = "";
|
|
184
|
+
let currentCategory = "generic";
|
|
185
|
+
let items = [];
|
|
186
|
+
let interval;
|
|
187
|
+
let renderedLineCount = 0;
|
|
188
|
+
let renderedItemCount = 0;
|
|
189
|
+
let prevKeptCount = 0;
|
|
190
|
+
let spinnerRendered = false;
|
|
191
|
+
let startTime = 0;
|
|
192
|
+
let funIndex = 0;
|
|
193
|
+
let lastFunSwap = 0;
|
|
194
|
+
let cursorHidden = false;
|
|
195
|
+
let funLineRendered = false;
|
|
196
|
+
let currentFunText = "";
|
|
197
|
+
const writeCursorHide = () => {
|
|
198
|
+
if (!cursorHidden) {
|
|
199
|
+
write(hideCursor);
|
|
200
|
+
cursorHidden = true;
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
const writeCursorShow = () => {
|
|
204
|
+
if (cursorHidden) {
|
|
205
|
+
write(showCursor);
|
|
206
|
+
cursorHidden = false;
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
const clearRendered = () => {
|
|
210
|
+
if (renderedLineCount > 0) {
|
|
211
|
+
write(`\x1b[${renderedLineCount}A\r\x1b[J`);
|
|
212
|
+
renderedLineCount = 0;
|
|
213
|
+
renderedItemCount = 0;
|
|
214
|
+
prevKeptCount = 0;
|
|
215
|
+
spinnerRendered = false;
|
|
216
|
+
funLineRendered = false;
|
|
217
|
+
currentFunText = "";
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const donePrefix = ` ${green}ā¢${reset$2} `;
|
|
221
|
+
const pendingPrefix = ` ${dim$1}ā¢${reset$2} `;
|
|
222
|
+
const render = () => {
|
|
223
|
+
const elapsed = Date.now() - startTime;
|
|
224
|
+
const frame = frames[frameIndex % frames.length];
|
|
225
|
+
if (!spinnerRendered) {
|
|
226
|
+
write(`${cyan}${frame}${reset$2} ${currentText}\x1b[K\n`);
|
|
227
|
+
renderedLineCount++;
|
|
228
|
+
spinnerRendered = true;
|
|
229
|
+
} else {
|
|
230
|
+
const dist = renderedLineCount - prevKeptCount;
|
|
231
|
+
write(`\x1b[${dist}A\r${cyan}${frame}${reset$2} ${currentText}\x1b[K\x1b[${dist}B\r`);
|
|
232
|
+
}
|
|
233
|
+
items.forEach((it, i) => {
|
|
234
|
+
if (i < renderedItemCount && it.done && !it.renderedDone) {
|
|
235
|
+
const linesUp = renderedLineCount - prevKeptCount - 1 - i;
|
|
236
|
+
write(`\x1b[${linesUp}A\r${donePrefix}${it.text}\x1b[K\x1b[${linesUp}B\r`);
|
|
237
|
+
it.renderedDone = true;
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
if (renderedItemCount < items.length && funLineRendered) {
|
|
241
|
+
write("\x1B[1A\r\x1B[K");
|
|
242
|
+
renderedLineCount--;
|
|
243
|
+
funLineRendered = false;
|
|
244
|
+
}
|
|
245
|
+
while (renderedItemCount < items.length) {
|
|
246
|
+
const it = items[renderedItemCount];
|
|
247
|
+
write(`${it.done ? donePrefix : pendingPrefix}${it.text}\x1b[K\n`);
|
|
248
|
+
if (it.done) it.renderedDone = true;
|
|
249
|
+
renderedItemCount++;
|
|
250
|
+
renderedLineCount++;
|
|
251
|
+
}
|
|
252
|
+
if (elapsed > 1e4) {
|
|
253
|
+
const msgs = funMessages[currentCategory];
|
|
254
|
+
if (lastFunSwap === 0 || Date.now() - lastFunSwap > 1e4) {
|
|
255
|
+
funIndex = (funIndex + 1) % msgs.length;
|
|
256
|
+
lastFunSwap = Date.now();
|
|
257
|
+
}
|
|
258
|
+
const newFunText = msgs[funIndex];
|
|
259
|
+
const funLine = ` ā ${dim$1}${newFunText}${reset$2}`;
|
|
260
|
+
if (!funLineRendered) {
|
|
261
|
+
write(`${funLine}\x1b[K\n`);
|
|
262
|
+
renderedLineCount++;
|
|
263
|
+
funLineRendered = true;
|
|
264
|
+
currentFunText = newFunText;
|
|
265
|
+
} else if (newFunText !== currentFunText) {
|
|
266
|
+
write(`\x1b[1A\r${funLine}\x1b[K\x1b[1B\r`);
|
|
267
|
+
currentFunText = newFunText;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
frameIndex++;
|
|
271
|
+
};
|
|
272
|
+
const stopInterval = () => {
|
|
273
|
+
if (interval) {
|
|
274
|
+
clearInterval(interval);
|
|
275
|
+
interval = void 0;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const startInterval = () => {
|
|
279
|
+
stopInterval();
|
|
280
|
+
interval = setInterval(render, 80);
|
|
281
|
+
render();
|
|
282
|
+
};
|
|
283
|
+
const onExit = () => writeCursorShow();
|
|
284
|
+
process.on("exit", onExit);
|
|
285
|
+
return {
|
|
286
|
+
start: (text, category) => {
|
|
287
|
+
writeCursorHide();
|
|
288
|
+
currentText = text;
|
|
289
|
+
currentCategory = category ?? "generic";
|
|
290
|
+
items = [];
|
|
291
|
+
renderedItemCount = 0;
|
|
292
|
+
spinnerRendered = false;
|
|
293
|
+
funLineRendered = false;
|
|
294
|
+
currentFunText = "";
|
|
295
|
+
startTime = Date.now();
|
|
296
|
+
lastFunSwap = 0;
|
|
297
|
+
funIndex = Math.floor(Math.random() * funMessages[currentCategory].length);
|
|
298
|
+
startInterval();
|
|
299
|
+
},
|
|
300
|
+
item: (text) => {
|
|
301
|
+
const last = items.at(-1);
|
|
302
|
+
if (last && !last.done) last.done = true;
|
|
303
|
+
items.push({
|
|
304
|
+
text: text.replace(/^⢠/, ""),
|
|
305
|
+
done: false,
|
|
306
|
+
renderedDone: false
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
succeed: (text, dimText) => {
|
|
310
|
+
stopInterval();
|
|
311
|
+
clearRendered();
|
|
312
|
+
write(`${green}ā${reset$2} ${text}${dimText ? ` ${dim$1}${dimText}${reset$2}` : ""}\n`);
|
|
313
|
+
const keptItems = items.length <= 1 ? [] : items;
|
|
314
|
+
keptItems.forEach((it) => {
|
|
315
|
+
write(`${donePrefix}${it.text}\x1b[K\n`);
|
|
316
|
+
});
|
|
317
|
+
prevKeptCount = keptItems.length;
|
|
318
|
+
renderedLineCount = prevKeptCount;
|
|
319
|
+
renderedItemCount = 0;
|
|
320
|
+
items = [];
|
|
321
|
+
spinnerRendered = false;
|
|
322
|
+
},
|
|
323
|
+
fail: (text) => {
|
|
324
|
+
stopInterval();
|
|
325
|
+
clearRendered();
|
|
326
|
+
write(`${red}ā${reset$2} ${text}\n`);
|
|
327
|
+
writeCursorShow();
|
|
328
|
+
},
|
|
329
|
+
stop: () => {
|
|
330
|
+
stopInterval();
|
|
331
|
+
clearRendered();
|
|
332
|
+
writeCursorShow();
|
|
333
|
+
process.removeListener("exit", onExit);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
344
336
|
};
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
337
|
+
//#endregion
|
|
338
|
+
//#region src/commands/init.ts
|
|
339
|
+
var registerInitCommand = (program) => {
|
|
340
|
+
program.command("init").description("Initialize project configuration (.spmrc.yml)").action(() => {
|
|
341
|
+
const stepper = createStepper();
|
|
342
|
+
const { config, created } = readConfig();
|
|
343
|
+
const providers = Object.keys(config.providers);
|
|
344
|
+
if (created) stepper.succeed(`Created ${CONFIG_FILE}`);
|
|
345
|
+
else stepper.succeed(`${CONFIG_FILE} already exists`);
|
|
346
|
+
stepper.start("Detecting providers...", "generic");
|
|
347
|
+
providers.forEach((name) => stepper.item(name));
|
|
348
|
+
if (providers.length > 0) stepper.succeed(`Detected ${providers.length} provider(s)`);
|
|
349
|
+
else stepper.fail("No providers detected");
|
|
350
|
+
stepper.stop();
|
|
351
|
+
});
|
|
353
352
|
};
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
353
|
+
//#endregion
|
|
354
|
+
//#region src/core/installer/install-skill/install-skill.md?raw
|
|
355
|
+
var install_skill_default = "# Skill Integration\n\nYou are integrating a single skill into the project. Analyze the existing project setup and intelligently integrate the new files.\n\n## Context\n\n- **Provider directory**: `{{providerDir}}`\n- **Skill**: `{{skillName}}`\n\nIdentical files have already been removed. Only files that need action are listed below. If no downloaded files are listed, output `Done` immediately.\n\n## Output Format\n\nCRITICAL ā follow exactly:\n- NEVER wrap output in code blocks or backticks\n- NEVER write conversational text (\"Now let me...\", \"Let me check...\", \"The files are...\")\n- NEVER use emojis\n- Output ONLY structured log lines\n- Step headers are plain text ending with `...` ā these are parsed by the CLI to drive spinner states\n- Items below headers are indented with 2 spaces and use `⢠` prefix\n- File lists use ASCII tree: `āā`, `āā`, `ā`\n- End with `Done` on its own line\n\nStep headers (use these exactly):\n\n1. `Analyzing existing setup...`\n2. `Analyzing downloaded files...`\n3. `Detecting conflicts...`\n4. `Integrating...`\n5. `Done`\n\nExample output:\n\nAnalyzing existing setup...\n ⢠4 skills, 2 rules, 1 hook\n\nAnalyzing downloaded files...\n ⢠skill: git (3 files)\n\nDetecting conflicts...\n ⢠No conflicts\n\nIntegrating...\n ⢠skills/git/SKILL.md\n ⢠skills/git/branch.md\n\nDone\n\n{{embeddedSection}}\n\n## Step 1: Analyzing existing setup\n\nReview the existing files list above and catalog what is already in place: skills, rules, agents, hooks, mcp servers files.\nDo NOT use Read or Glob to scan the provider directory ā all information is provided above.\n\n## Step 2: Analyzing downloaded files\n\nReview the downloaded files listed above. These are the only files that need to be installed.\nDo NOT use Read or Glob to scan the download directory ā all file contents are provided above.\n\n## Step 3: Detecting conflicts\n\nUsing the existing files list and downloaded files above, check if any downloaded file conflicts with an existing file.\n\n- **No existing file** ā install as new\n- **Different content** ā replace silently\n- **Same content** ā skip (should already be pruned)\n\nIf there is a real conflict (same path, user has customized the existing file):\n\nDetecting conflicts...\n ⢠rules/coding.md ā local has custom rules\n ⢠Choose: (r)eplace / (s)kip / (m)erge\n\nWait for response before proceeding.\n\n## Step 4: Integrating\n\nWrite each downloaded file to `{{providerDir}}/skills/{{skillName}}/` using the Write tool. Use the exact content provided above.\n\n**Integrate, don't copy.** When a new skill can leverage existing project conventions, adapt it:\n\n- If the project has `rules/coding.md` with specific conventions ā make new skills follow those rules instead of their own defaults\n- If existing skills handle branching or committing ā reference them instead of duplicating instructions\n- If the project has naming conventions, testing patterns, or architectural rules ā align new skills with them\n\n{{unresolvedSection}}\n\n## Rules\n\n- Do not delete or modify existing files in the provider directory unless resolving a conflict\n- Do NOT read files from disk ā all content is embedded in this prompt\n- Do NOT delete the download folder ā cleanup is handled externally\n";
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/core/installer/install-skillset/install.md?raw
|
|
358
|
+
var install_default = "# Skillset Integration\n\nYou are integrating a new skillset into the project. Analyze the existing project setup and intelligently integrate the new files.\n\n## Context\n\n- **Provider directory**: `{{providerDir}}`\n- **Skillset**: `{{skillsetName}}` v`{{skillsetVersion}}`\n\nIdentical files have already been removed. Only files that need action are listed below. If no downloaded files are listed, output `Done` immediately.\n\n## Output Format\n\nCRITICAL ā follow exactly:\n- NEVER wrap output in code blocks or backticks\n- NEVER write conversational text (\"Now let me...\", \"Let me check...\", \"The files are...\")\n- NEVER use emojis\n- Output ONLY structured log lines\n- Step headers are plain text ending with `...` ā these are parsed by the CLI to drive spinner states\n- Items below headers are indented with 2 spaces and use `⢠` prefix\n- File lists use ASCII tree: `āā`, `āā`, `ā`\n- End with `Done` on its own line\n\nStep headers (use these exactly):\n\n1. `Analyzing existing setup...`\n2. `Analyzing downloaded files...`\n3. `Detecting conflicts...`\n4. `Integrating...`\n5. `Running setup...` (only if a setup file exists)\n6. `Done`\n\nExample output:\n\nAnalyzing existing setup...\n ⢠4 skills, 2 rules, 1 hook\n\nAnalyzing downloaded files...\n ⢠skills: git, github, implement\n ⢠rules: coding\n ⢠hook: biome-format\n\nDetecting conflicts...\n ⢠No conflicts\n\nIntegrating...\n ⢠skills/git/SKILL.md\n ⢠skills/git/branch.md\n ⢠skills/github/SKILL.md\n\nRunning setup...\n ⢠Configured MCP server: biome\n\nDone\n\n{{embeddedSection}}\n\n## Step 1: Analyzing existing setup\n\nReview the existing files list above and catalog what is already in place: skills, rules, agents, hooks, mcp servers files.\nDo NOT use Read or Glob to scan the provider directory ā all information is provided above.\n\n## Step 2: Analyzing downloaded files\n\nReview the downloaded files listed above. These are the only files that need to be installed.\nDo NOT use Read or Glob to scan the download directory ā all file contents are provided above.\n\n## Step 3: Detecting conflicts\n\nUsing the existing files list and downloaded files above, check if any downloaded file conflicts with an existing file.\n\n- **No existing file** ā install as new\n- **Different skillset version** ā replace silently (new version supersedes)\n- **Same version or no version info** ā conflict ā ask the user:\n\nDetecting conflicts...\n ⢠rules/coding.md ā local has custom rules\n ⢠Choose: (r)eplace / (s)kip / (m)erge\n\nWait for response before proceeding.\n\n## Step 4: Integrating\n\nWrite each downloaded file to `{{providerDir}}` using the Write tool, following the directory structure from the file paths. Use the exact content provided above.\n\n**Integrate, don't copy.** When a new skill can leverage existing project conventions, adapt it:\n\n- If the project has `rules/coding.md` with specific conventions ā make new skills follow those rules instead of their own defaults\n- If existing skills handle branching or committing ā reference them instead of duplicating instructions\n- If the project has naming conventions, testing patterns, or architectural rules ā align new skills with them\n\n{{setupSection}}\n\n## Rules\n\n- Setup files are NOT installed ā they contain instructions to configure the project\n- Do not delete or modify existing files in the provider directory unless resolving a conflict\n- Do NOT read files from disk ā all content is embedded in this prompt\n- Do NOT delete the download folder ā cleanup is handled externally\n";
|
|
359
|
+
//#endregion
|
|
360
|
+
//#region src/core/installer/shared/build-prompt.ts
|
|
361
|
+
var buildSetupSection = (setupContent) => setupContent ? [
|
|
362
|
+
"## Step 6: Running setup",
|
|
363
|
+
"",
|
|
364
|
+
"Follow the setup instructions below. Setup files configure the project environment (e.g. MCP servers, LSP, tooling).",
|
|
365
|
+
"Do NOT copy the setup content into the provider directory ā only execute its instructions.",
|
|
366
|
+
"",
|
|
367
|
+
"```",
|
|
368
|
+
setupContent,
|
|
369
|
+
"```"
|
|
370
|
+
].join("\n") : "";
|
|
371
|
+
var buildUnresolvedSection = (refs) => refs && refs.length > 0 ? [
|
|
372
|
+
"## Unresolved references",
|
|
373
|
+
"",
|
|
374
|
+
"The following files are referenced in the skill but do not exist in the source repository.",
|
|
375
|
+
"They may be runtime-generated paths (e.g. template outputs, reports). Review each reference",
|
|
376
|
+
"in context and decide:",
|
|
377
|
+
"",
|
|
378
|
+
"- **Runtime path** ā keep the reference as-is, the file will be created when the skill runs",
|
|
379
|
+
"- **Missing required file** ā warn the user that this file could not be found",
|
|
380
|
+
"",
|
|
381
|
+
...refs.map((r) => `- \`${r}\``)
|
|
382
|
+
].join("\n") : "";
|
|
383
|
+
var buildEmbeddedSection = (embedded) => {
|
|
384
|
+
const parts = [];
|
|
385
|
+
parts.push("## Existing files in provider directory");
|
|
386
|
+
parts.push("");
|
|
387
|
+
if (embedded.existingFiles.length === 0) parts.push("Provider directory is empty.");
|
|
388
|
+
else embedded.existingFiles.forEach((f) => parts.push(`- ${f}`));
|
|
389
|
+
parts.push("");
|
|
390
|
+
parts.push("## Downloaded files (to be installed)");
|
|
391
|
+
parts.push("");
|
|
392
|
+
if (embedded.downloadedFiles.length === 0) parts.push("No files to install (all unchanged).");
|
|
393
|
+
else embedded.downloadedFiles.forEach((f) => {
|
|
394
|
+
parts.push(`### ${f.path}`);
|
|
395
|
+
parts.push("");
|
|
396
|
+
parts.push("```");
|
|
397
|
+
parts.push(f.content);
|
|
398
|
+
parts.push("```");
|
|
399
|
+
parts.push("");
|
|
400
|
+
});
|
|
401
|
+
return parts.join("\n");
|
|
363
402
|
};
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
403
|
+
var buildInstructions = (input) => install_default.replace(/\{\{providerDir\}\}/g, input.providerDir).replace(/\{\{skillsetName\}\}/g, input.skillsetName).replace(/\{\{skillsetVersion\}\}/g, input.skillsetVersion).replace("{{setupSection}}", buildSetupSection(input.setupContent)).replace("{{embeddedSection}}", buildEmbeddedSection(input.embedded));
|
|
404
|
+
var writeInstructionsFile = (input) => {
|
|
405
|
+
const filePath = join(tmpdir(), "spm", `install-${input.skillsetName}.md`);
|
|
406
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
407
|
+
writeFileSync(filePath, buildInstructions(input), "utf-8");
|
|
408
|
+
return filePath;
|
|
368
409
|
};
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
410
|
+
var buildSkillInstructions = (input) => install_skill_default.replace(/\{\{providerDir\}\}/g, input.providerDir).replace(/\{\{skillName\}\}/g, input.skillName).replace("{{unresolvedSection}}", buildUnresolvedSection(input.unresolvedRefs)).replace("{{embeddedSection}}", buildEmbeddedSection(input.embedded));
|
|
411
|
+
var writeSkillInstructionsFile = (input) => {
|
|
412
|
+
const filePath = join(tmpdir(), "spm", `install-skill-${input.skillName}.md`);
|
|
413
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
414
|
+
writeFileSync(filePath, buildSkillInstructions(input), "utf-8");
|
|
415
|
+
return filePath;
|
|
372
416
|
};
|
|
373
|
-
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
417
|
+
var writeSetupInstructionsFile = (setupContent, skillsetName) => {
|
|
418
|
+
const instructions = [
|
|
419
|
+
"# Skillset Setup",
|
|
420
|
+
"",
|
|
421
|
+
`You are running setup for the **${skillsetName}** skillset.`,
|
|
422
|
+
"Follow the instructions below to configure the project environment.",
|
|
423
|
+
"",
|
|
424
|
+
"## Output Format",
|
|
425
|
+
"",
|
|
426
|
+
"CRITICAL ā follow exactly:",
|
|
427
|
+
"- NEVER wrap output in code blocks or backticks",
|
|
428
|
+
"- NEVER write conversational text",
|
|
429
|
+
"- NEVER use emojis",
|
|
430
|
+
"- Output ONLY structured log lines",
|
|
431
|
+
"- End with `Done` on its own line",
|
|
432
|
+
"",
|
|
433
|
+
"Step header: `Running setup...`",
|
|
434
|
+
"",
|
|
435
|
+
"## Instructions",
|
|
436
|
+
"",
|
|
437
|
+
setupContent,
|
|
438
|
+
"",
|
|
439
|
+
"Done"
|
|
440
|
+
].join("\n");
|
|
441
|
+
const filePath = join(tmpdir(), "spm", `setup-${skillsetName}.md`);
|
|
442
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
443
|
+
writeFileSync(filePath, instructions, "utf-8");
|
|
444
|
+
return filePath;
|
|
445
|
+
};
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/core/installer/shared/cleanup.ts
|
|
448
|
+
var cleanupDownloadDir = async (projectRoot, downloadDir) => {
|
|
449
|
+
await rm(downloadDir, {
|
|
450
|
+
recursive: true,
|
|
451
|
+
force: true
|
|
452
|
+
});
|
|
453
|
+
const spmDir = join(projectRoot, ".spm");
|
|
454
|
+
if ((await readdir(spmDir).catch(() => [])).length === 0) await rm(spmDir, {
|
|
455
|
+
recursive: true,
|
|
456
|
+
force: true
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
//#endregion
|
|
460
|
+
//#region src/core/installer/shared/collect-files.ts
|
|
461
|
+
var walkDir = (dir) => readdirSync(dir).flatMap((entry) => {
|
|
462
|
+
const fullPath = join(dir, entry);
|
|
463
|
+
return statSync(fullPath).isDirectory() ? walkDir(fullPath) : [fullPath];
|
|
464
|
+
});
|
|
465
|
+
var collectRemainingFiles = (downloadDir) => {
|
|
466
|
+
if (!existsSync(downloadDir)) return [];
|
|
467
|
+
return walkDir(downloadDir).filter((f) => !f.endsWith("SETUP.md")).map((fullPath) => ({
|
|
468
|
+
path: relative(downloadDir, fullPath).replace(/\\/g, "/"),
|
|
469
|
+
content: readFileSync(fullPath, "utf-8")
|
|
470
|
+
}));
|
|
471
|
+
};
|
|
472
|
+
var listExistingFiles = (providerDir) => {
|
|
473
|
+
if (!existsSync(providerDir)) return [];
|
|
474
|
+
return walkDir(providerDir).map((fullPath) => relative(providerDir, fullPath).replace(/\\/g, "/"));
|
|
475
|
+
};
|
|
476
|
+
//#endregion
|
|
477
|
+
//#region src/core/installer/shared/path-utils.ts
|
|
478
|
+
var safePath = (baseDir, filePath) => {
|
|
479
|
+
const resolvedBase = resolve(baseDir);
|
|
480
|
+
const resolved = resolve(resolvedBase, normalize(filePath));
|
|
481
|
+
if (resolved !== resolvedBase && !resolved.startsWith(`${resolvedBase}${sep}`)) throw new Error(`Path traversal detected: "${filePath}"`);
|
|
482
|
+
return resolved;
|
|
483
|
+
};
|
|
484
|
+
var stripProviderPrefix = (file, prefix) => {
|
|
485
|
+
const norm = prefix.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
486
|
+
if (file.startsWith(`${norm}/`)) return file.slice(norm.length + 1);
|
|
487
|
+
const bare = norm.replace(/^\./, "");
|
|
488
|
+
if (bare && file.startsWith(`${bare}/`)) return file.slice(bare.length + 1);
|
|
489
|
+
return file;
|
|
490
|
+
};
|
|
491
|
+
//#endregion
|
|
492
|
+
//#region src/core/installer/shared/copy-files.ts
|
|
493
|
+
var copyFilesToProvider = (files, targetDir, stepper, entityLabel) => {
|
|
494
|
+
if (files.length === 0) {
|
|
495
|
+
stepper.succeed(`${entityLabel} is up to date`);
|
|
496
|
+
return {
|
|
497
|
+
success: true,
|
|
498
|
+
output: "",
|
|
499
|
+
files: []
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
stepper.start("Integrating...", "skills");
|
|
503
|
+
const writtenFiles = files.map((file) => {
|
|
504
|
+
const targetPath = safePath(targetDir, file.path);
|
|
505
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
506
|
+
writeFileSync(targetPath, file.content, "utf-8");
|
|
507
|
+
stepper.item(file.path);
|
|
508
|
+
return file.path;
|
|
509
|
+
});
|
|
510
|
+
stepper.succeed(`${entityLabel} was integrated (${writtenFiles.length} file(s))`);
|
|
511
|
+
return {
|
|
512
|
+
success: true,
|
|
513
|
+
output: "",
|
|
514
|
+
files: writtenFiles
|
|
515
|
+
};
|
|
388
516
|
};
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/core/installer/shared/detect-conflicts.ts
|
|
519
|
+
var detectConflicts = (files, providerDir) => {
|
|
520
|
+
const newFiles = [];
|
|
521
|
+
const conflictFiles = [];
|
|
522
|
+
files.forEach((file) => {
|
|
523
|
+
if (existsSync(join(providerDir, file.path))) conflictFiles.push(file);
|
|
524
|
+
else newFiles.push(file);
|
|
525
|
+
});
|
|
526
|
+
return {
|
|
527
|
+
newFiles,
|
|
528
|
+
conflictFiles
|
|
529
|
+
};
|
|
395
530
|
};
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/core/downloader/download-from-github.ts
|
|
533
|
+
var downloadFromGitHub = async (source) => {
|
|
534
|
+
const url = `https://raw.githubusercontent.com/${source.owner}/${source.repository}/${source.ref}/${source.path}`;
|
|
535
|
+
const response = await fetch(url);
|
|
536
|
+
if (!response.ok) throw new Error(`Failed to download from GitHub: ${url} (${response.status} ${response.statusText})`);
|
|
537
|
+
return response.text();
|
|
402
538
|
};
|
|
403
|
-
|
|
404
|
-
|
|
539
|
+
//#endregion
|
|
540
|
+
//#region src/core/downloader/download-from-url.ts
|
|
541
|
+
var downloadFromUrl = async (source) => {
|
|
542
|
+
const response = await fetch(source.url);
|
|
543
|
+
if (!response.ok) throw new Error(`Failed to download from URL: ${source.url} (${response.status} ${response.statusText})`);
|
|
544
|
+
return response.text();
|
|
405
545
|
};
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
546
|
+
//#endregion
|
|
547
|
+
//#region src/core/downloader/read-local-file.ts
|
|
548
|
+
var readLocalFile = async (source) => {
|
|
549
|
+
try {
|
|
550
|
+
return await readFile(source.filePath, "utf-8");
|
|
551
|
+
} catch (error) {
|
|
552
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
553
|
+
throw new Error(`Failed to read local file: ${source.filePath} (${message})`);
|
|
554
|
+
}
|
|
413
555
|
};
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
return normalized.split("/").pop() ?? normalized;
|
|
556
|
+
//#endregion
|
|
557
|
+
//#region src/core/downloader/download-file.ts
|
|
558
|
+
var getContent = (source) => {
|
|
559
|
+
if (source.kind === "github") return downloadFromGitHub(source);
|
|
560
|
+
if (source.kind === "url") return downloadFromUrl(source);
|
|
561
|
+
return readLocalFile(source);
|
|
421
562
|
};
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const child = spawn("claude", args, {
|
|
437
|
-
shell: isWindows,
|
|
438
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
439
|
-
});
|
|
440
|
-
let resultText = "";
|
|
441
|
-
let lineBuffer = "";
|
|
442
|
-
let currentStepHeader = "Analyzing existing setup...";
|
|
443
|
-
let stepItems = [];
|
|
444
|
-
let stepFileCount = 0;
|
|
445
|
-
const state = { doneReceived: false, setupReached: false };
|
|
446
|
-
const writtenFiles = [];
|
|
447
|
-
const succeedCurrentStep = () => {
|
|
448
|
-
if (!currentStepHeader) return;
|
|
449
|
-
const base = toSuccessText(currentStepHeader);
|
|
450
|
-
if (currentStepHeader.startsWith("Detecting")) {
|
|
451
|
-
const noConflicts = stepItems.some((i) => /no conflict/i.test(i));
|
|
452
|
-
stepper.succeed(noConflicts ? "No conflicts" : base);
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
if (currentStepHeader.startsWith("Integrating")) {
|
|
456
|
-
stepper.succeed(
|
|
457
|
-
stepFileCount > 0 ? `${entityLabel} was integrated (${stepFileCount} file(s))` : `${entityLabel} was integrated`
|
|
458
|
-
);
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
if (currentStepHeader.startsWith("Running setup")) {
|
|
462
|
-
stepper.succeed(`${entityLabel} setup completed`);
|
|
463
|
-
currentStepHeader = "";
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (state.setupReached || currentStepHeader.startsWith("Cleaning")) return;
|
|
467
|
-
const context = stepItems.length > 0 ? stepItems.join(", ") : void 0;
|
|
468
|
-
stepper.succeed(base, context);
|
|
469
|
-
};
|
|
470
|
-
stepper.start("Analyzing existing setup...", "skills");
|
|
471
|
-
child.stdout.on("data", (chunk) => {
|
|
472
|
-
lineBuffer += chunk.toString();
|
|
473
|
-
const lines = lineBuffer.split("\n");
|
|
474
|
-
lineBuffer = lines.pop() ?? "";
|
|
475
|
-
lines.filter(Boolean).forEach((line) => {
|
|
476
|
-
try {
|
|
477
|
-
const event = JSON.parse(line);
|
|
478
|
-
if (event.type === "result" && event.result) {
|
|
479
|
-
resultText = event.result;
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
if (event.type !== "assistant") return;
|
|
483
|
-
const blocks = event.message?.content;
|
|
484
|
-
if (!blocks) return;
|
|
485
|
-
blocks.forEach((block) => {
|
|
486
|
-
if (block.type === "text" && block.text) {
|
|
487
|
-
block.text.split("\n").forEach((ln) => {
|
|
488
|
-
const trimmed = ln.trim();
|
|
489
|
-
if (!trimmed || isNoise(trimmed)) return;
|
|
490
|
-
if (trimmed === "Done") {
|
|
491
|
-
succeedCurrentStep();
|
|
492
|
-
currentStepHeader = "";
|
|
493
|
-
state.doneReceived = true;
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
if (isStepHeader(ln, trimmed)) {
|
|
497
|
-
if (trimmed === currentStepHeader) return;
|
|
498
|
-
succeedCurrentStep();
|
|
499
|
-
if (state.setupReached || trimmed.startsWith("Cleaning"))
|
|
500
|
-
return;
|
|
501
|
-
currentStepHeader = trimmed;
|
|
502
|
-
stepItems = [];
|
|
503
|
-
stepFileCount = 0;
|
|
504
|
-
if (trimmed.startsWith("Running setup")) {
|
|
505
|
-
state.setupReached = true;
|
|
506
|
-
}
|
|
507
|
-
stepper.start(trimmed, stepCategory(trimmed));
|
|
508
|
-
} else {
|
|
509
|
-
stepItems.push(trimmed);
|
|
510
|
-
if (currentStepHeader.startsWith("Integrating")) {
|
|
511
|
-
stepper.item(trimmed);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
});
|
|
515
|
-
}
|
|
516
|
-
if (block.type === "tool_use" && (block.name === "Write" || block.name === "Edit")) {
|
|
517
|
-
const filePath = block.input?.file_path;
|
|
518
|
-
if (typeof filePath === "string") {
|
|
519
|
-
const normalized = filePath.replace(/\\/g, "/");
|
|
520
|
-
const base = providerDir.replace(/\\/g, "/");
|
|
521
|
-
const isProviderFile = normalized.includes(`${base}/`);
|
|
522
|
-
const relative2 = toRelativePath(filePath, providerDir);
|
|
523
|
-
if (isProviderFile) writtenFiles.push(relative2);
|
|
524
|
-
stepFileCount++;
|
|
525
|
-
if (currentStepHeader.startsWith("Integrating")) {
|
|
526
|
-
stepper.item(relative2);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
});
|
|
531
|
-
} catch {
|
|
532
|
-
}
|
|
533
|
-
});
|
|
534
|
-
});
|
|
535
|
-
let stderrBuffer = "";
|
|
536
|
-
child.stderr.on("data", (chunk) => {
|
|
537
|
-
stderrBuffer += chunk.toString();
|
|
538
|
-
});
|
|
539
|
-
child.on("error", (err) => {
|
|
540
|
-
if ("code" in err && err.code === "ENOENT") {
|
|
541
|
-
reject(
|
|
542
|
-
new Error(
|
|
543
|
-
"Claude CLI not found. Install it: npm i -g @anthropic-ai/claude-code"
|
|
544
|
-
)
|
|
545
|
-
);
|
|
546
|
-
return;
|
|
547
|
-
}
|
|
548
|
-
reject(err);
|
|
549
|
-
});
|
|
550
|
-
child.on("close", (code) => {
|
|
551
|
-
if (currentStepHeader) {
|
|
552
|
-
succeedCurrentStep();
|
|
553
|
-
}
|
|
554
|
-
state.doneReceived || resultText.length > 0;
|
|
555
|
-
const stderr = stderrBuffer.trim();
|
|
556
|
-
if (code !== 0 && code !== null) {
|
|
557
|
-
const detail = stderr ? `
|
|
558
|
-
${stderr}` : "";
|
|
559
|
-
reject(new Error(`Claude CLI exited with code ${code}${detail}`));
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
if (code === null) {
|
|
563
|
-
const detail = stderr ? `
|
|
564
|
-
${stderr}` : "";
|
|
565
|
-
reject(
|
|
566
|
-
new Error(`Claude CLI was interrupted before completing${detail}`)
|
|
567
|
-
);
|
|
568
|
-
return;
|
|
569
|
-
}
|
|
570
|
-
resolve2({
|
|
571
|
-
success: true,
|
|
572
|
-
output: resultText,
|
|
573
|
-
files: writtenFiles
|
|
574
|
-
});
|
|
575
|
-
});
|
|
563
|
+
var downloadFile = async (source) => {
|
|
564
|
+
return {
|
|
565
|
+
content: await getContent(source),
|
|
566
|
+
source
|
|
567
|
+
};
|
|
568
|
+
};
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/core/installer/shared/download-entries.ts
|
|
571
|
+
var toGitHubSource = (entry, ref) => ({
|
|
572
|
+
kind: "github",
|
|
573
|
+
owner: entry.owner,
|
|
574
|
+
repository: entry.repository,
|
|
575
|
+
path: entry.path,
|
|
576
|
+
ref
|
|
576
577
|
});
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
578
|
+
var downloadEntries = async (entries, location, onFile) => {
|
|
579
|
+
return await Promise.all(entries.map((entry) => {
|
|
580
|
+
return downloadFile(toGitHubSource(entry, location.ref)).then((result) => {
|
|
581
|
+
onFile(entry.type, entry.path);
|
|
582
|
+
return {
|
|
583
|
+
...result,
|
|
584
|
+
type: entry.type,
|
|
585
|
+
path: entry.path,
|
|
586
|
+
skillName: entry.skillName
|
|
587
|
+
};
|
|
588
|
+
});
|
|
589
|
+
}));
|
|
590
|
+
};
|
|
591
|
+
//#endregion
|
|
592
|
+
//#region src/core/installer/shared/summary.ts
|
|
593
|
+
var buildFileTree = (paths) => {
|
|
594
|
+
const root = {
|
|
595
|
+
name: "",
|
|
596
|
+
children: []
|
|
597
|
+
};
|
|
598
|
+
paths.forEach((p) => {
|
|
599
|
+
const parts = p.split("/");
|
|
600
|
+
let current = root;
|
|
601
|
+
parts.forEach((part) => {
|
|
602
|
+
const existing = current.children.find((c) => c.name === part);
|
|
603
|
+
if (existing) current = existing;
|
|
604
|
+
else {
|
|
605
|
+
const node = {
|
|
606
|
+
name: part,
|
|
607
|
+
children: []
|
|
608
|
+
};
|
|
609
|
+
current.children.push(node);
|
|
610
|
+
current = node;
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
return root;
|
|
615
|
+
};
|
|
616
|
+
var renderTree = (node, prefix = "") => node.children.flatMap((child, i) => {
|
|
617
|
+
const isLast = i === node.children.length - 1;
|
|
618
|
+
const connector = isLast ? "āā" : "āā";
|
|
619
|
+
const childPrefix = prefix + (isLast ? " " : "ā ");
|
|
620
|
+
return [`${prefix}${connector} ${child.children.length > 0 ? `${child.name}/` : child.name}`, ...renderTree(child, childPrefix)];
|
|
594
621
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
const downloadedHash = hash(readFileSync(downloadedFile, "utf-8"));
|
|
603
|
-
const existingHash = hash(readFileSync(existingFile, "utf-8"));
|
|
604
|
-
if (downloadedHash === existingHash) {
|
|
605
|
-
unlinkSync(downloadedFile);
|
|
606
|
-
removed++;
|
|
607
|
-
}
|
|
608
|
-
});
|
|
609
|
-
return removed;
|
|
622
|
+
var printSummary = (files, providerPath) => {
|
|
623
|
+
if (files.length === 0) return;
|
|
624
|
+
const tree = buildFileTree([...new Set(files)].map((f) => stripProviderPrefix(f, providerPath)));
|
|
625
|
+
process.stdout.write(`\nš${providerPath}\n`);
|
|
626
|
+
renderTree(tree).forEach((line) => {
|
|
627
|
+
process.stdout.write(` ${dim$1}${line}${reset$2}\n`);
|
|
628
|
+
});
|
|
610
629
|
};
|
|
611
|
-
|
|
612
|
-
const
|
|
613
|
-
const
|
|
614
|
-
|
|
615
|
-
if (match) {
|
|
616
|
-
const frontmatter = parse(match[1]);
|
|
617
|
-
if (hasStringName(frontmatter)) return frontmatter.name;
|
|
618
|
-
}
|
|
619
|
-
return fileName.replace(/\.md$/i, "").toLowerCase();
|
|
630
|
+
var printCompleted = (startedAt) => {
|
|
631
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1e3);
|
|
632
|
+
const timeStr = elapsed >= 60 ? `${Math.floor(elapsed / 60)}m${(elapsed % 60).toString().padStart(2, "0")}s` : `${elapsed}s`;
|
|
633
|
+
process.stdout.write(`${green}ā${reset$2} Installation completed ${dim$1}in ${timeStr}${reset$2}\n`);
|
|
620
634
|
};
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
return hasDefaultBranch(data) ? data.default_branch : "main";
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region src/core/installer/shared/write-to-temp.ts
|
|
637
|
+
var writeFilesToTemp = async (downloadDir, files, onFile) => {
|
|
638
|
+
await Promise.all(files.map(async ({ relativePath, content }) => {
|
|
639
|
+
const filePath = safePath(downloadDir, relativePath);
|
|
640
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
641
|
+
await writeFile(filePath, content, "utf-8");
|
|
642
|
+
onFile?.(relativePath);
|
|
643
|
+
}));
|
|
631
644
|
};
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
645
|
+
//#endregion
|
|
646
|
+
//#region src/core/installer/spawn-claude/parse-stream.ts
|
|
647
|
+
var createStreamParser = (onEvent) => {
|
|
648
|
+
let lineBuffer = "";
|
|
649
|
+
let resultText = "";
|
|
650
|
+
const onChunk = (chunk) => {
|
|
651
|
+
lineBuffer += chunk.toString();
|
|
652
|
+
const lines = lineBuffer.split("\n");
|
|
653
|
+
lineBuffer = lines.pop() ?? "";
|
|
654
|
+
lines.filter(Boolean).forEach((line) => {
|
|
655
|
+
try {
|
|
656
|
+
const event = JSON.parse(line);
|
|
657
|
+
if (event.type === "result" && event.result) {
|
|
658
|
+
resultText = event.result;
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
onEvent(event);
|
|
662
|
+
} catch {}
|
|
663
|
+
});
|
|
664
|
+
};
|
|
665
|
+
return {
|
|
666
|
+
onChunk,
|
|
667
|
+
getResultText: () => resultText
|
|
668
|
+
};
|
|
647
669
|
};
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
}
|
|
656
|
-
const text = await response.text();
|
|
657
|
-
return validateSkillset(parse(text));
|
|
670
|
+
//#endregion
|
|
671
|
+
//#region src/core/installer/spawn-claude/text-utils.ts
|
|
672
|
+
var isNoise = (line) => line === "```" || line.startsWith("```") || /^(now |let me |the files? |i'll |i will |looking |checking )/i.test(line);
|
|
673
|
+
var isStepHeader = (raw, trimmed) => !raw.startsWith(" ") && !raw.startsWith(" ") && trimmed.endsWith("...");
|
|
674
|
+
var stepCategory = (header) => {
|
|
675
|
+
if (header.startsWith("Integrating") || header.startsWith("Analyzing") || header.startsWith("Detecting")) return "skills";
|
|
676
|
+
return "generic";
|
|
658
677
|
};
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
const identifier = {
|
|
666
|
-
owner: blobOrTree[1],
|
|
667
|
-
repository: blobOrTree[2],
|
|
668
|
-
path: blobOrTree[4],
|
|
669
|
-
ref: blobOrTree[3]
|
|
670
|
-
};
|
|
671
|
-
return { kind: detectKind(identifier.path), identifier };
|
|
672
|
-
}
|
|
673
|
-
const raw = url.match(
|
|
674
|
-
/^https?:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/
|
|
675
|
-
);
|
|
676
|
-
if (raw) {
|
|
677
|
-
const identifier = {
|
|
678
|
-
owner: raw[1],
|
|
679
|
-
repository: raw[2],
|
|
680
|
-
path: raw[4],
|
|
681
|
-
ref: raw[3]
|
|
682
|
-
};
|
|
683
|
-
return { kind: detectKind(identifier.path), identifier };
|
|
684
|
-
}
|
|
685
|
-
return void 0;
|
|
678
|
+
var irregulars = { Running: "Ran" };
|
|
679
|
+
var toSuccessText = (header) => {
|
|
680
|
+
const text = header.replace(/\.{3}$/, "").trim();
|
|
681
|
+
const firstWord = text.split(" ")[0];
|
|
682
|
+
if (irregulars[firstWord]) return text.replace(firstWord, irregulars[firstWord]);
|
|
683
|
+
return text.replace(/^(\w+)ing\b/, "$1ed");
|
|
686
684
|
};
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
685
|
+
var toRelativePath = (filePath, providerDir) => {
|
|
686
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
687
|
+
const marker = `${providerDir.replace(/\\/g, "/")}/`;
|
|
688
|
+
const idx = normalized.lastIndexOf(marker);
|
|
689
|
+
if (idx !== -1) return normalized.slice(idx + marker.length);
|
|
690
|
+
return normalized.split("/").pop() ?? normalized;
|
|
691
|
+
};
|
|
692
|
+
//#endregion
|
|
693
|
+
//#region src/core/installer/spawn-claude/step-tracker.ts
|
|
694
|
+
var createStepTracker = (stepper, providerDir, entityLabel) => {
|
|
695
|
+
let currentStepHeader = "Analyzing existing setup...";
|
|
696
|
+
let stepItems = [];
|
|
697
|
+
let stepFileCount = 0;
|
|
698
|
+
const state = { setupReached: false };
|
|
699
|
+
const writtenFiles = /* @__PURE__ */ new Set();
|
|
700
|
+
const succeedCurrentStep = () => {
|
|
701
|
+
if (!currentStepHeader) return;
|
|
702
|
+
const base = toSuccessText(currentStepHeader);
|
|
703
|
+
if (currentStepHeader.startsWith("Detecting")) {
|
|
704
|
+
const noConflicts = stepItems.some((i) => /no conflict/i.test(i));
|
|
705
|
+
stepper.succeed(noConflicts ? "No conflicts" : base);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (currentStepHeader.startsWith("Integrating")) {
|
|
709
|
+
stepper.succeed(stepFileCount > 0 ? `${entityLabel} was integrated (${stepFileCount} file(s))` : `${entityLabel} was integrated`);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
if (currentStepHeader.startsWith("Running setup")) {
|
|
713
|
+
stepper.succeed(`${entityLabel} setup completed`);
|
|
714
|
+
currentStepHeader = "";
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
if (state.setupReached || currentStepHeader.startsWith("Cleaning")) return;
|
|
718
|
+
const context = stepItems.length > 0 ? stepItems.join(", ") : void 0;
|
|
719
|
+
stepper.succeed(base, context);
|
|
720
|
+
};
|
|
721
|
+
const processTextLine = (raw) => {
|
|
722
|
+
const trimmed = raw.trim();
|
|
723
|
+
if (!trimmed || isNoise(trimmed)) return;
|
|
724
|
+
if (trimmed === "Done") {
|
|
725
|
+
succeedCurrentStep();
|
|
726
|
+
currentStepHeader = "";
|
|
727
|
+
return;
|
|
728
|
+
}
|
|
729
|
+
if (isStepHeader(raw, trimmed)) {
|
|
730
|
+
if (trimmed === currentStepHeader) return;
|
|
731
|
+
succeedCurrentStep();
|
|
732
|
+
if (state.setupReached || trimmed.startsWith("Cleaning")) return;
|
|
733
|
+
currentStepHeader = trimmed;
|
|
734
|
+
stepItems = [];
|
|
735
|
+
stepFileCount = 0;
|
|
736
|
+
if (trimmed.startsWith("Running setup")) state.setupReached = true;
|
|
737
|
+
stepper.start(trimmed, stepCategory(trimmed));
|
|
738
|
+
} else stepItems.push(trimmed);
|
|
739
|
+
};
|
|
740
|
+
const processToolUse = (block) => {
|
|
741
|
+
if (block.name !== "Write" && block.name !== "Edit") return;
|
|
742
|
+
const filePath = block.input?.file_path;
|
|
743
|
+
if (typeof filePath !== "string") return;
|
|
744
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
745
|
+
const base = providerDir.replace(/\\/g, "/");
|
|
746
|
+
const isProviderFile = normalized.includes(`${base}/`);
|
|
747
|
+
const relative = toRelativePath(filePath, providerDir);
|
|
748
|
+
if (isProviderFile && !writtenFiles.has(relative)) {
|
|
749
|
+
writtenFiles.add(relative);
|
|
750
|
+
stepFileCount++;
|
|
751
|
+
if (currentStepHeader.startsWith("Integrating")) stepper.item(relative);
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
const processBlock = (block) => {
|
|
755
|
+
if (block.type === "text" && block.text) block.text.split("\n").forEach(processTextLine);
|
|
756
|
+
if (block.type === "tool_use") processToolUse(block);
|
|
757
|
+
};
|
|
758
|
+
return {
|
|
759
|
+
processBlock,
|
|
760
|
+
getWrittenFiles: () => [...writtenFiles],
|
|
761
|
+
finalize: () => {
|
|
762
|
+
if (currentStepHeader) succeedCurrentStep();
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
};
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/core/installer/spawn-claude/spawn-claude.ts
|
|
768
|
+
var debug = (() => {
|
|
769
|
+
const logPath = process.env.SPM_DEBUG;
|
|
770
|
+
if (!logPath) return void 0;
|
|
771
|
+
mkdirSync(dirname(logPath), { recursive: true });
|
|
772
|
+
return (label, data) => appendFileSync(logPath, `[${label}] ${data}\n`, "utf-8");
|
|
773
|
+
})();
|
|
774
|
+
var spawnClaude = (instructionsFilePath, stepper, providerDir, model, entityLabel = "Skillset") => new Promise((resolve, reject) => {
|
|
775
|
+
const args = [
|
|
776
|
+
"-p",
|
|
777
|
+
"Install the skill as instructed.",
|
|
778
|
+
"--append-system-prompt-file",
|
|
779
|
+
instructionsFilePath,
|
|
780
|
+
"--verbose",
|
|
781
|
+
"--output-format",
|
|
782
|
+
"stream-json",
|
|
783
|
+
"--permission-mode",
|
|
784
|
+
"acceptEdits",
|
|
785
|
+
"--allowedTools",
|
|
786
|
+
"Read,Write,Edit,Bash,Glob,Grep",
|
|
787
|
+
...model ? ["--model", model] : []
|
|
788
|
+
];
|
|
789
|
+
debug?.("spawn", `claude ${args.join(" ")}`);
|
|
790
|
+
debug?.("config", `providerDir=${providerDir} instructions=${instructionsFilePath}`);
|
|
791
|
+
const child = spawn("claude", args, {
|
|
792
|
+
shell: process.platform === "win32",
|
|
793
|
+
stdio: [
|
|
794
|
+
"ignore",
|
|
795
|
+
"pipe",
|
|
796
|
+
"pipe"
|
|
797
|
+
]
|
|
798
|
+
});
|
|
799
|
+
const tracker = createStepTracker(stepper, providerDir, entityLabel);
|
|
800
|
+
const stream = createStreamParser((event) => {
|
|
801
|
+
debug?.("event", JSON.stringify(event));
|
|
802
|
+
if (event.type !== "assistant") return;
|
|
803
|
+
const blocks = event.message?.content;
|
|
804
|
+
if (!blocks) return;
|
|
805
|
+
blocks.forEach((block) => tracker.processBlock(block));
|
|
806
|
+
});
|
|
807
|
+
stepper.start("Analyzing existing setup...", "skills");
|
|
808
|
+
child.stdout.on("data", (chunk) => {
|
|
809
|
+
debug?.("stdout", chunk.toString());
|
|
810
|
+
stream.onChunk(chunk);
|
|
811
|
+
});
|
|
812
|
+
let stderrBuffer = "";
|
|
813
|
+
child.stderr.on("data", (chunk) => {
|
|
814
|
+
const text = chunk.toString();
|
|
815
|
+
debug?.("stderr", text);
|
|
816
|
+
stderrBuffer += text;
|
|
817
|
+
});
|
|
818
|
+
child.on("error", (err) => {
|
|
819
|
+
debug?.("error", err.message);
|
|
820
|
+
if ("code" in err && err.code === "ENOENT") {
|
|
821
|
+
reject(/* @__PURE__ */ new Error("Claude CLI not found. Install it: npm i -g @anthropic-ai/claude-code"));
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
reject(err);
|
|
825
|
+
});
|
|
826
|
+
child.on("close", (code) => {
|
|
827
|
+
tracker.finalize();
|
|
828
|
+
const stderr = stderrBuffer.trim();
|
|
829
|
+
const writtenFiles = tracker.getWrittenFiles();
|
|
830
|
+
debug?.("close", `code=${code} stderr=${stderr}`);
|
|
831
|
+
debug?.("files", JSON.stringify(writtenFiles));
|
|
832
|
+
debug?.("result", stream.getResultText());
|
|
833
|
+
if (code !== 0 && code !== null) {
|
|
834
|
+
const detail = stderr ? `\n${stderr}` : "";
|
|
835
|
+
reject(/* @__PURE__ */ new Error(`Claude CLI exited with code ${code}${detail}`));
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
if (code === null) {
|
|
839
|
+
const detail = stderr ? `\n${stderr}` : "";
|
|
840
|
+
reject(/* @__PURE__ */ new Error(`Claude CLI was interrupted before completing${detail}`));
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
resolve({
|
|
844
|
+
success: true,
|
|
845
|
+
output: stream.getResultText(),
|
|
846
|
+
files: writtenFiles
|
|
847
|
+
});
|
|
848
|
+
});
|
|
849
|
+
});
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region src/core/resolver/derive-skill-name.ts
|
|
852
|
+
var frontmatterPattern = /^---\r?\n([\s\S]*?)\r?\n---/;
|
|
853
|
+
var hasStringName = (value) => typeof value === "object" && value !== null && "name" in value && typeof value.name === "string";
|
|
854
|
+
var deriveSkillName = (content, fileName) => {
|
|
855
|
+
const match = content.match(frontmatterPattern);
|
|
856
|
+
if (match) {
|
|
857
|
+
const frontmatter = parse(match[1]);
|
|
858
|
+
if (hasStringName(frontmatter)) return frontmatter.name;
|
|
859
|
+
}
|
|
860
|
+
return fileName.replace(/\.md$/i, "").toLowerCase();
|
|
700
861
|
};
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
}
|
|
710
|
-
return result;
|
|
711
|
-
}
|
|
712
|
-
if (!trimmed.startsWith("@")) {
|
|
713
|
-
throw new Error(
|
|
714
|
-
`Invalid identifier "${trimmed}". Must start with @ or be a GitHub URL.`
|
|
715
|
-
);
|
|
716
|
-
}
|
|
717
|
-
return parseAtIdentifier(trimmed);
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/core/resolver/fetch-default-branch.ts
|
|
864
|
+
var hasDefaultBranch = (value) => typeof value === "object" && value !== null && "default_branch" in value && typeof value.default_branch === "string";
|
|
865
|
+
var fetchDefaultBranch = async (owner, repository) => {
|
|
866
|
+
const response = await fetch(`https://api.github.com/repos/${owner}/${repository}`);
|
|
867
|
+
if (!response.ok) return "main";
|
|
868
|
+
const data = await response.json();
|
|
869
|
+
return hasDefaultBranch(data) ? data.default_branch : "main";
|
|
718
870
|
};
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
".codeium/",
|
|
727
|
-
".cody/"
|
|
871
|
+
//#endregion
|
|
872
|
+
//#region src/core/resolver/fetch-skillset.ts
|
|
873
|
+
var requiredFields = [
|
|
874
|
+
"name",
|
|
875
|
+
"version",
|
|
876
|
+
"description",
|
|
877
|
+
"provider"
|
|
728
878
|
];
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
879
|
+
var validateSkillset = (data) => {
|
|
880
|
+
if (!data || typeof data !== "object") throw new Error("Invalid skillset: expected a YAML object.");
|
|
881
|
+
const record = data;
|
|
882
|
+
const missing = requiredFields.filter((field) => typeof record[field] !== "string");
|
|
883
|
+
if (missing.length > 0) throw new Error(`Invalid skillset: missing required fields: ${missing.join(", ")}.`);
|
|
884
|
+
return record;
|
|
732
885
|
};
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (!isExcluded(ref)) {
|
|
739
|
-
const resolved = posix.normalize(posix.join(fileDir, ref));
|
|
740
|
-
refs.add(resolved);
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
};
|
|
744
|
-
collect(markdownLinkPattern);
|
|
745
|
-
collect(inlinePathPattern);
|
|
746
|
-
return [...refs];
|
|
886
|
+
var fetchSkillset = async (location) => {
|
|
887
|
+
const url = `https://raw.githubusercontent.com/${location.owner}/${location.repository}/${location.ref}/${location.path}`;
|
|
888
|
+
const response = await fetch(url);
|
|
889
|
+
if (!response.ok) throw new Error(`Failed to fetch skillset from ${url} (${response.status} ${response.statusText})`);
|
|
890
|
+
return validateSkillset(parse(await response.text()));
|
|
747
891
|
};
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
892
|
+
//#endregion
|
|
893
|
+
//#region src/core/resolver/parse-identifier.ts
|
|
894
|
+
var detectKind = (path) => /\.md$/i.test(path) ? "skill" : "skillset";
|
|
895
|
+
var parseGitHubUrl = (url) => {
|
|
896
|
+
const blobOrTree = url.match(/^https?:\/\/github\.com\/([^/]+)\/([^/]+)\/(?:tree|blob)\/([^/]+)\/(.+)$/);
|
|
897
|
+
if (blobOrTree) {
|
|
898
|
+
const identifier = {
|
|
899
|
+
owner: blobOrTree[1],
|
|
900
|
+
repository: blobOrTree[2],
|
|
901
|
+
path: blobOrTree[4],
|
|
902
|
+
ref: blobOrTree[3]
|
|
903
|
+
};
|
|
904
|
+
return {
|
|
905
|
+
kind: detectKind(identifier.path),
|
|
906
|
+
identifier
|
|
907
|
+
};
|
|
908
|
+
}
|
|
909
|
+
const raw = url.match(/^https?:\/\/raw\.githubusercontent\.com\/([^/]+)\/([^/]+)\/([^/]+)\/(.+)$/);
|
|
910
|
+
if (raw) {
|
|
911
|
+
const identifier = {
|
|
912
|
+
owner: raw[1],
|
|
913
|
+
repository: raw[2],
|
|
914
|
+
path: raw[4],
|
|
915
|
+
ref: raw[3]
|
|
916
|
+
};
|
|
917
|
+
return {
|
|
918
|
+
kind: detectKind(identifier.path),
|
|
919
|
+
identifier
|
|
920
|
+
};
|
|
921
|
+
}
|
|
758
922
|
};
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
ref
|
|
772
|
-
});
|
|
773
|
-
files.push({ path: normalized, content });
|
|
774
|
-
const fileDir = posix.dirname(normalized);
|
|
775
|
-
const refs = parseSkillRefs(content, fileDir);
|
|
776
|
-
await Promise.all(refs.map((refPath) => fetchRecursive(refPath)));
|
|
777
|
-
};
|
|
778
|
-
await fetchRecursive(identifier.path);
|
|
779
|
-
const mainFile = files[0];
|
|
780
|
-
const fileName = posix.basename(identifier.path);
|
|
781
|
-
const name = mainFile ? deriveSkillName(mainFile.content, fileName) : fileName.replace(/\.md$/i, "");
|
|
782
|
-
return {
|
|
783
|
-
name,
|
|
784
|
-
location: {
|
|
785
|
-
owner: identifier.owner,
|
|
786
|
-
repository: identifier.repository,
|
|
787
|
-
path: identifier.path,
|
|
788
|
-
ref
|
|
789
|
-
},
|
|
790
|
-
files
|
|
791
|
-
};
|
|
923
|
+
var parseAtIdentifier = (input) => {
|
|
924
|
+
const parts = input.slice(1).split("/").filter(Boolean);
|
|
925
|
+
if (parts.length < 3) throw new Error(`Invalid identifier "${input}". Expected format: @owner/repo/path (e.g., @supa-magic/skillbox/claude/fsd).`);
|
|
926
|
+
const identifier = {
|
|
927
|
+
owner: parts[0],
|
|
928
|
+
repository: parts[1],
|
|
929
|
+
path: parts.slice(2).join("/")
|
|
930
|
+
};
|
|
931
|
+
return {
|
|
932
|
+
kind: detectKind(identifier.path),
|
|
933
|
+
identifier
|
|
934
|
+
};
|
|
792
935
|
};
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
}
|
|
803
|
-
if (source.startsWith("@")) {
|
|
804
|
-
const parts = source.slice(1).split("/").filter(Boolean);
|
|
805
|
-
if (parts.length < 3) {
|
|
806
|
-
throw new Error(
|
|
807
|
-
`Invalid cross-repo source "${source}". Expected format: @owner/repo/path.`
|
|
808
|
-
);
|
|
809
|
-
}
|
|
810
|
-
return {
|
|
811
|
-
owner: parts[0],
|
|
812
|
-
repository: parts[1],
|
|
813
|
-
basePath: parts.slice(2).join("/")
|
|
814
|
-
};
|
|
815
|
-
}
|
|
816
|
-
const normalized = source.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
817
|
-
if (normalized.includes("..")) {
|
|
818
|
-
throw new Error(
|
|
819
|
-
`Invalid skill source "${source}". Path traversal ("..") is not allowed.`
|
|
820
|
-
);
|
|
821
|
-
}
|
|
822
|
-
return {
|
|
823
|
-
owner: location.owner,
|
|
824
|
-
repository: location.repository,
|
|
825
|
-
basePath: normalized
|
|
826
|
-
};
|
|
936
|
+
var parseIdentifier = (input) => {
|
|
937
|
+
const trimmed = input.trim();
|
|
938
|
+
if (trimmed.startsWith("https://") || trimmed.startsWith("http://")) {
|
|
939
|
+
const result = parseGitHubUrl(trimmed);
|
|
940
|
+
if (!result) throw new Error(`Invalid GitHub URL "${trimmed}". Expected format: https://github.com/owner/repo/tree|blob/branch/path or https://raw.githubusercontent.com/owner/repo/ref/path.`);
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
if (!trimmed.startsWith("@")) throw new Error(`Invalid identifier "${trimmed}". Must start with @ or be a GitHub URL.`);
|
|
944
|
+
return parseAtIdentifier(trimmed);
|
|
827
945
|
};
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
files.forEach((file) => {
|
|
844
|
-
entries.push({
|
|
845
|
-
owner: resolved.owner,
|
|
846
|
-
repository: resolved.repository,
|
|
847
|
-
path: `${resolved.basePath}/${file}`,
|
|
848
|
-
type: "skill",
|
|
849
|
-
skillName
|
|
850
|
-
});
|
|
851
|
-
});
|
|
852
|
-
}
|
|
853
|
-
);
|
|
854
|
-
}
|
|
855
|
-
const localFileTypes = ["agents", "hooks", "mcp", "memory", "rules"];
|
|
856
|
-
const typeMap = {
|
|
857
|
-
agents: "agent",
|
|
858
|
-
hooks: "hook",
|
|
859
|
-
mcp: "mcp",
|
|
860
|
-
memory: "memory",
|
|
861
|
-
rules: "rule"
|
|
862
|
-
};
|
|
863
|
-
localFileTypes.forEach((key) => {
|
|
864
|
-
const files = skillset[key];
|
|
865
|
-
if (files) {
|
|
866
|
-
files.forEach((file) => {
|
|
867
|
-
entries.push({
|
|
868
|
-
owner: location.owner,
|
|
869
|
-
repository: location.repository,
|
|
870
|
-
path: `${skillsetDir}/${file}`,
|
|
871
|
-
type: typeMap[key]
|
|
872
|
-
});
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
});
|
|
876
|
-
return entries;
|
|
946
|
+
//#endregion
|
|
947
|
+
//#region src/core/resolver/parse-skill-refs.ts
|
|
948
|
+
var markdownLinkPattern = /\[[^\]]*\]\(([^)]+\.md)\)/g;
|
|
949
|
+
var inlinePathPattern = /(?:`|(?:^|\s))(\.[^\s`]*\.md)/g;
|
|
950
|
+
var providerPrefixes = [
|
|
951
|
+
".claude/",
|
|
952
|
+
".cursor/",
|
|
953
|
+
".copilot/",
|
|
954
|
+
".aider/",
|
|
955
|
+
".codeium/",
|
|
956
|
+
".cody/"
|
|
957
|
+
];
|
|
958
|
+
var isExcluded = (ref) => {
|
|
959
|
+
const normalized = ref.replace(/^\.\//, "");
|
|
960
|
+
return ref.startsWith("http://") || ref.startsWith("https://") || ref.startsWith("/") || ref.includes("..") || /\{[^}]+\}/.test(ref) || providerPrefixes.some((prefix) => normalized.startsWith(prefix));
|
|
877
961
|
};
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
962
|
+
var stripCodeBlocks = (text) => text.replace(/^(`{3,})[^\n]*\n[\s\S]*?\n\1\s*$/gm, "");
|
|
963
|
+
var parseSkillRefs = (content, fileDir) => {
|
|
964
|
+
const refs = /* @__PURE__ */ new Set();
|
|
965
|
+
const stripped = stripCodeBlocks(content);
|
|
966
|
+
const collect = (pattern) => {
|
|
967
|
+
for (const match of stripped.matchAll(pattern)) {
|
|
968
|
+
const ref = match[1];
|
|
969
|
+
if (!isExcluded(ref)) {
|
|
970
|
+
const resolved = posix.normalize(posix.join(fileDir, ref));
|
|
971
|
+
refs.add(resolved);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
collect(markdownLinkPattern);
|
|
976
|
+
collect(inlinePathPattern);
|
|
977
|
+
return [...refs];
|
|
978
|
+
};
|
|
979
|
+
//#endregion
|
|
980
|
+
//#region src/core/resolver/resolve-identifier.ts
|
|
981
|
+
var resolveIdentifier = async (input) => {
|
|
982
|
+
const { identifier } = parseIdentifier(input);
|
|
983
|
+
const ref = identifier.ref ?? await fetchDefaultBranch(identifier.owner, identifier.repository);
|
|
984
|
+
return {
|
|
985
|
+
owner: identifier.owner,
|
|
986
|
+
repository: identifier.repository,
|
|
987
|
+
path: `${identifier.path}/skillset.yml`,
|
|
988
|
+
ref
|
|
989
|
+
};
|
|
901
990
|
};
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
991
|
+
//#endregion
|
|
992
|
+
//#region src/core/resolver/resolve-skill.ts
|
|
993
|
+
var resolveSkill = async (identifier) => {
|
|
994
|
+
const ref = identifier.ref ?? await fetchDefaultBranch(identifier.owner, identifier.repository);
|
|
995
|
+
const visited = /* @__PURE__ */ new Set();
|
|
996
|
+
const files = [];
|
|
997
|
+
const unresolvedRefs = [];
|
|
998
|
+
const fetchRecursive = async (repoPath, isRoot) => {
|
|
999
|
+
const normalized = posix.normalize(repoPath);
|
|
1000
|
+
if (visited.has(normalized)) return;
|
|
1001
|
+
visited.add(normalized);
|
|
1002
|
+
let content;
|
|
1003
|
+
try {
|
|
1004
|
+
content = await downloadFromGitHub({
|
|
1005
|
+
kind: "github",
|
|
1006
|
+
owner: identifier.owner,
|
|
1007
|
+
repository: identifier.repository,
|
|
1008
|
+
path: normalized,
|
|
1009
|
+
ref
|
|
1010
|
+
});
|
|
1011
|
+
} catch (err) {
|
|
1012
|
+
if (isRoot) throw err;
|
|
1013
|
+
unresolvedRefs.push(normalized);
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
files.push({
|
|
1017
|
+
path: normalized,
|
|
1018
|
+
content
|
|
1019
|
+
});
|
|
1020
|
+
const fileDir = posix.dirname(normalized);
|
|
1021
|
+
const refs = parseSkillRefs(content, fileDir);
|
|
1022
|
+
await Promise.all(refs.map((refPath) => fetchRecursive(refPath, false)));
|
|
1023
|
+
};
|
|
1024
|
+
await fetchRecursive(identifier.path, true);
|
|
1025
|
+
const mainFile = files[0];
|
|
1026
|
+
const fileName = posix.basename(identifier.path);
|
|
1027
|
+
return {
|
|
1028
|
+
name: mainFile ? deriveSkillName(mainFile.content, fileName) : fileName.replace(/\.md$/i, ""),
|
|
1029
|
+
location: {
|
|
1030
|
+
owner: identifier.owner,
|
|
1031
|
+
repository: identifier.repository,
|
|
1032
|
+
path: identifier.path,
|
|
1033
|
+
ref
|
|
1034
|
+
},
|
|
1035
|
+
files,
|
|
1036
|
+
unresolvedRefs
|
|
1037
|
+
};
|
|
908
1038
|
};
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
1039
|
+
//#endregion
|
|
1040
|
+
//#region src/core/resolver/resolve-skillset.ts
|
|
1041
|
+
var resolveSkillSource = (source, location) => {
|
|
1042
|
+
if (source.startsWith("./")) {
|
|
1043
|
+
const skillsetDir = location.path.replace(/\/skillset\.yml$/, "");
|
|
1044
|
+
const relativePath = source.slice(2).replace(/\/+$/, "");
|
|
1045
|
+
return {
|
|
1046
|
+
owner: location.owner,
|
|
1047
|
+
repository: location.repository,
|
|
1048
|
+
basePath: `${skillsetDir}/${relativePath}`
|
|
1049
|
+
};
|
|
1050
|
+
}
|
|
1051
|
+
if (source.startsWith("@")) {
|
|
1052
|
+
const parts = source.slice(1).split("/").filter(Boolean);
|
|
1053
|
+
if (parts.length < 3) throw new Error(`Invalid cross-repo source "${source}". Expected format: @owner/repo/path.`);
|
|
1054
|
+
return {
|
|
1055
|
+
owner: parts[0],
|
|
1056
|
+
repository: parts[1],
|
|
1057
|
+
basePath: parts.slice(2).join("/")
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
const normalized = source.replace(/^\/+/, "").replace(/\/+$/, "");
|
|
1061
|
+
if (normalized.includes("..")) throw new Error(`Invalid skill source "${source}". Path traversal ("..") is not allowed.`);
|
|
1062
|
+
return {
|
|
1063
|
+
owner: location.owner,
|
|
1064
|
+
repository: location.repository,
|
|
1065
|
+
basePath: normalized
|
|
1066
|
+
};
|
|
926
1067
|
};
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
1068
|
+
var resolveSkillset = (skillset, location) => {
|
|
1069
|
+
const entries = [];
|
|
1070
|
+
const skillsetDir = location.path.replace(/\/skillset\.yml$/, "");
|
|
1071
|
+
if (skillset.setup) entries.push({
|
|
1072
|
+
owner: location.owner,
|
|
1073
|
+
repository: location.repository,
|
|
1074
|
+
path: `${skillsetDir}/${skillset.setup}`,
|
|
1075
|
+
type: "setup"
|
|
1076
|
+
});
|
|
1077
|
+
if (skillset.skills) Object.entries(skillset.skills).forEach(([skillName, { source, files }]) => {
|
|
1078
|
+
const resolved = resolveSkillSource(source, location);
|
|
1079
|
+
files.forEach((file) => {
|
|
1080
|
+
entries.push({
|
|
1081
|
+
owner: resolved.owner,
|
|
1082
|
+
repository: resolved.repository,
|
|
1083
|
+
path: `${resolved.basePath}/${file}`,
|
|
1084
|
+
type: "skill",
|
|
1085
|
+
skillName
|
|
1086
|
+
});
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
const localFileTypes = [
|
|
1090
|
+
"agents",
|
|
1091
|
+
"hooks",
|
|
1092
|
+
"mcp",
|
|
1093
|
+
"memory",
|
|
1094
|
+
"rules"
|
|
1095
|
+
];
|
|
1096
|
+
const typeMap = {
|
|
1097
|
+
agents: "agent",
|
|
1098
|
+
hooks: "hook",
|
|
1099
|
+
mcp: "mcp",
|
|
1100
|
+
memory: "memory",
|
|
1101
|
+
rules: "rule"
|
|
1102
|
+
};
|
|
1103
|
+
localFileTypes.forEach((key) => {
|
|
1104
|
+
const files = skillset[key];
|
|
1105
|
+
if (files) files.forEach((file) => {
|
|
1106
|
+
entries.push({
|
|
1107
|
+
owner: location.owner,
|
|
1108
|
+
repository: location.repository,
|
|
1109
|
+
path: `${skillsetDir}/${file}`,
|
|
1110
|
+
type: typeMap[key]
|
|
1111
|
+
});
|
|
1112
|
+
});
|
|
1113
|
+
});
|
|
1114
|
+
return entries;
|
|
1115
|
+
};
|
|
1116
|
+
//#endregion
|
|
1117
|
+
//#region src/core/installer/prune-unchanged.ts
|
|
1118
|
+
var hash = (content) => createHash("sha256").update(content).digest("hex");
|
|
1119
|
+
var collectFiles = (dir) => readdirSync(dir).flatMap((entry) => {
|
|
1120
|
+
const fullPath = join(dir, entry);
|
|
1121
|
+
return statSync(fullPath).isDirectory() ? collectFiles(fullPath) : [fullPath];
|
|
934
1122
|
});
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1123
|
+
var pruneUnchanged = (downloadDir, providerDir) => {
|
|
1124
|
+
const files = collectFiles(downloadDir);
|
|
1125
|
+
let removed = 0;
|
|
1126
|
+
files.forEach((downloadedFile) => {
|
|
1127
|
+
const existingFile = join(providerDir, relative(downloadDir, downloadedFile));
|
|
1128
|
+
if (!existsSync(existingFile)) return;
|
|
1129
|
+
if (hash(readFileSync(downloadedFile, "utf-8")) === hash(readFileSync(existingFile, "utf-8"))) {
|
|
1130
|
+
unlinkSync(downloadedFile);
|
|
1131
|
+
removed++;
|
|
1132
|
+
}
|
|
1133
|
+
});
|
|
1134
|
+
return removed;
|
|
1135
|
+
};
|
|
1136
|
+
//#endregion
|
|
1137
|
+
//#region src/core/installer/install-skill/install-skill-flow.ts
|
|
1138
|
+
var installSkillFlow = async (identifier, stepper, startedAt) => {
|
|
1139
|
+
const { config } = readConfig();
|
|
1140
|
+
stepper.start("Resolving skill...", "packages");
|
|
1141
|
+
const resolved = await resolveSkill(identifier);
|
|
1142
|
+
const locationRef = `${resolved.location.owner}/${resolved.location.repository}@${resolved.location.ref}`;
|
|
1143
|
+
stepper.succeed("Resolved", `${resolved.name} ${dim$1}${locationRef}${reset$2}`);
|
|
1144
|
+
const firstProvider = Object.entries(config.providers)[0];
|
|
1145
|
+
if (!firstProvider) throw new Error("No provider detected. Initialize a provider directory first (e.g., .claude/).");
|
|
1146
|
+
const [providerName, providerConfig] = firstProvider;
|
|
1147
|
+
const providerPath = providerConfig.path;
|
|
1148
|
+
const projectRoot = getProjectRoot();
|
|
1149
|
+
const downloadDir = join(projectRoot, ".spm", resolved.name);
|
|
1150
|
+
const skillDir = posix.dirname(resolved.location.path);
|
|
1151
|
+
stepper.start(`Downloading ${resolved.files.length} file(s)...`, "packages");
|
|
1152
|
+
await writeFilesToTemp(downloadDir, resolved.files.map((file) => ({
|
|
1153
|
+
relativePath: file.path.startsWith(`${skillDir}/`) ? file.path.slice(skillDir.length + 1) : file.path.split("/").pop() ?? file.path,
|
|
1154
|
+
content: file.content
|
|
1155
|
+
})), (relativePath) => stepper.item(relativePath));
|
|
1156
|
+
stepper.succeed(`Downloaded ${resolved.files.length} file(s)`);
|
|
1157
|
+
if (resolved.unresolvedRefs.length > 0) stepper.succeed(`Skipped ${resolved.unresolvedRefs.length} unresolved reference(s)`, resolved.unresolvedRefs.map((r) => r.split("/").pop()).join(", "));
|
|
1158
|
+
const providerFullPath = join(projectRoot, providerPath);
|
|
1159
|
+
const skillsBase = resolve(providerFullPath, "skills");
|
|
1160
|
+
const skillProviderDir = join(skillsBase, resolved.name);
|
|
1161
|
+
if (!resolve(skillProviderDir).startsWith(`${skillsBase}${sep}`)) throw new Error(`Invalid skill name: "${resolved.name}"`);
|
|
1162
|
+
const pruned = pruneUnchanged(downloadDir, skillProviderDir);
|
|
1163
|
+
if (pruned > 0) stepper.succeed(`Skipped ${pruned} unchanged file(s)`);
|
|
1164
|
+
const source = `https://github.com/${resolved.location.owner}/${resolved.location.repository}/blob/${resolved.location.ref}/${resolved.location.path}`;
|
|
1165
|
+
const remainingFiles = collectRemainingFiles(downloadDir);
|
|
1166
|
+
const { newFiles, conflictFiles } = detectConflicts(remainingFiles, skillProviderDir);
|
|
1167
|
+
let result;
|
|
1168
|
+
if (conflictFiles.length === 0) result = copyFilesToProvider(newFiles, skillProviderDir, stepper, "Skill");
|
|
1169
|
+
else {
|
|
1170
|
+
const model = providerName === "claude" ? "sonnet" : void 0;
|
|
1171
|
+
const embedded = {
|
|
1172
|
+
downloadedFiles: remainingFiles,
|
|
1173
|
+
existingFiles: listExistingFiles(providerFullPath)
|
|
1174
|
+
};
|
|
1175
|
+
result = await spawnClaude(writeSkillInstructionsFile({
|
|
1176
|
+
providerDir: providerFullPath,
|
|
1177
|
+
skillName: resolved.name,
|
|
1178
|
+
source,
|
|
1179
|
+
configPath: getConfigPath(),
|
|
1180
|
+
model,
|
|
1181
|
+
unresolvedRefs: resolved.unresolvedRefs,
|
|
1182
|
+
embedded
|
|
1183
|
+
}), stepper, providerFullPath, model, "Skill");
|
|
1184
|
+
}
|
|
1185
|
+
addConfigEntry({
|
|
1186
|
+
providerPath,
|
|
1187
|
+
kind: "skills",
|
|
1188
|
+
name: resolved.name,
|
|
1189
|
+
source
|
|
1190
|
+
});
|
|
1191
|
+
await cleanupDownloadDir(projectRoot, downloadDir);
|
|
1192
|
+
stepper.stop();
|
|
1193
|
+
printCompleted(startedAt);
|
|
1194
|
+
printSummary(result.files, providerPath);
|
|
1195
|
+
process.stdout.write(`\nšŖ ${cyan}Restart your AI agent to apply the new skills.${reset$2}\n`);
|
|
941
1196
|
};
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
`);
|
|
951
|
-
renderTree(tree).forEach((line) => {
|
|
952
|
-
process.stdout.write(` ${dim$1}${line}${reset$2}
|
|
953
|
-
`);
|
|
954
|
-
});
|
|
1197
|
+
//#endregion
|
|
1198
|
+
//#region src/core/installer/install-skillset/install-skillset-flow.ts
|
|
1199
|
+
var toTargetPath = (entry, skillsetDir) => {
|
|
1200
|
+
if (entry.type === "skill" && entry.skillName) {
|
|
1201
|
+
const fileName = entry.path.split("/").pop() ?? entry.path;
|
|
1202
|
+
return `skills/${entry.skillName}/${fileName}`;
|
|
1203
|
+
}
|
|
1204
|
+
return entry.path.startsWith(`${skillsetDir}/`) ? entry.path.slice(skillsetDir.length + 1) : entry.path;
|
|
955
1205
|
};
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1206
|
+
var installSkillsetFlow = async (input, stepper, startedAt) => {
|
|
1207
|
+
readConfig();
|
|
1208
|
+
stepper.start("Resolving endpoint...", "packages");
|
|
1209
|
+
const location = await resolveIdentifier(input);
|
|
1210
|
+
const locationRef = `${location.owner}/${location.repository}@${location.ref}`;
|
|
1211
|
+
stepper.succeed("Resolved", locationRef);
|
|
1212
|
+
stepper.start("Fetching skillset manifest...", "packages");
|
|
1213
|
+
const skillset = await fetchSkillset(location);
|
|
1214
|
+
stepper.succeed("Fetched skillset manifest", `${skillset.name} v${skillset.version}`);
|
|
1215
|
+
const providerPath = knownProviders[skillset.provider];
|
|
1216
|
+
if (!providerPath) throw new Error(`Unknown provider "${skillset.provider}". Known providers: ${Object.keys(knownProviders).join(", ")}`);
|
|
1217
|
+
const entries = resolveSkillset(skillset, location);
|
|
1218
|
+
if (entries.length === 0) {
|
|
1219
|
+
stepper.succeed("No files to download");
|
|
1220
|
+
stepper.stop();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
const projectRoot = getProjectRoot();
|
|
1224
|
+
const downloadDir = join(projectRoot, ".spm", skillset.name);
|
|
1225
|
+
const skillsetDir = location.path.replace(/\/[^/]+$/, "");
|
|
1226
|
+
stepper.start(`Downloading ${entries.length} file(s)...`, "packages");
|
|
1227
|
+
const results = await downloadEntries(entries, location, (type, path) => stepper.item(`${type} ${path.split("/").pop() ?? path}`));
|
|
1228
|
+
const setupResults = results.filter((r) => r.type === "setup");
|
|
1229
|
+
await writeFilesToTemp(downloadDir, results.filter((r) => r.type !== "setup").map((r) => ({
|
|
1230
|
+
relativePath: toTargetPath(r, skillsetDir),
|
|
1231
|
+
content: r.content
|
|
1232
|
+
})));
|
|
1233
|
+
const setupContent = setupResults.length > 0 ? setupResults[0].content : void 0;
|
|
1234
|
+
stepper.succeed(`Downloaded ${results.length} file(s)`);
|
|
1235
|
+
const providerFullPath = join(projectRoot, providerPath);
|
|
1236
|
+
const pruned = pruneUnchanged(downloadDir, providerFullPath);
|
|
1237
|
+
if (pruned > 0) stepper.succeed(`Skipped ${pruned} unchanged file(s)`);
|
|
1238
|
+
const source = `https://github.com/${location.owner}/${location.repository}/blob/${location.ref}/${location.path}`;
|
|
1239
|
+
const remainingFiles = collectRemainingFiles(downloadDir);
|
|
1240
|
+
const { newFiles, conflictFiles } = detectConflicts(remainingFiles, providerFullPath);
|
|
1241
|
+
let result;
|
|
1242
|
+
if (conflictFiles.length === 0) result = copyFilesToProvider(newFiles, providerFullPath, stepper, "Skillset");
|
|
1243
|
+
else {
|
|
1244
|
+
const model = skillset.provider === "claude" ? "sonnet" : void 0;
|
|
1245
|
+
const embedded = {
|
|
1246
|
+
downloadedFiles: remainingFiles,
|
|
1247
|
+
existingFiles: listExistingFiles(providerFullPath)
|
|
1248
|
+
};
|
|
1249
|
+
result = await spawnClaude(writeInstructionsFile({
|
|
1250
|
+
setupContent,
|
|
1251
|
+
providerDir: providerFullPath,
|
|
1252
|
+
skillsetName: skillset.name,
|
|
1253
|
+
skillsetVersion: skillset.version,
|
|
1254
|
+
source,
|
|
1255
|
+
configPath: getConfigPath(),
|
|
1256
|
+
model,
|
|
1257
|
+
embedded
|
|
1258
|
+
}), stepper, providerFullPath, model);
|
|
1259
|
+
}
|
|
1260
|
+
if (setupContent && conflictFiles.length === 0) {
|
|
1261
|
+
const model = skillset.provider === "claude" ? "sonnet" : void 0;
|
|
1262
|
+
await spawnClaude(writeSetupInstructionsFile(setupContent, skillset.name), stepper, providerFullPath, model);
|
|
1263
|
+
}
|
|
1264
|
+
addConfigEntry({
|
|
1265
|
+
providerPath,
|
|
1266
|
+
kind: "skillsets",
|
|
1267
|
+
name: skillset.name,
|
|
1268
|
+
source
|
|
1269
|
+
});
|
|
1270
|
+
await cleanupDownloadDir(projectRoot, downloadDir);
|
|
1271
|
+
stepper.stop();
|
|
1272
|
+
printCompleted(startedAt);
|
|
1273
|
+
printSummary(result.files, providerPath);
|
|
1274
|
+
process.stdout.write(`\nšŖ ${cyan}Restart your AI agent to apply the new skills.${reset$2}\n`);
|
|
963
1275
|
};
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
}
|
|
982
|
-
const provider = { path: providerPath };
|
|
983
|
-
const entries = resolveSkillset(skillset, location);
|
|
984
|
-
if (entries.length === 0) {
|
|
985
|
-
stepper.succeed("No files to download");
|
|
986
|
-
stepper.stop();
|
|
987
|
-
return;
|
|
988
|
-
}
|
|
989
|
-
const projectRoot = getProjectRoot();
|
|
990
|
-
const downloadDir = join(projectRoot, ".spm", skillset.name);
|
|
991
|
-
const toTargetPath = (entry) => {
|
|
992
|
-
if (entry.type === "skill" && entry.skillName) {
|
|
993
|
-
const fileName = entry.path.split("/").pop() ?? entry.path;
|
|
994
|
-
return `skills/${entry.skillName}/${fileName}`;
|
|
995
|
-
}
|
|
996
|
-
const skillsetDir = location.path.replace(/\/[^/]+$/, "");
|
|
997
|
-
return entry.path.startsWith(`${skillsetDir}/`) ? entry.path.slice(skillsetDir.length + 1) : entry.path;
|
|
998
|
-
};
|
|
999
|
-
stepper.start(`Downloading ${entries.length} file(s)...`, "packages");
|
|
1000
|
-
const results = await downloadEntries(
|
|
1001
|
-
entries,
|
|
1002
|
-
location,
|
|
1003
|
-
(type, path) => stepper.item(`${type} ${path.split("/").pop() ?? path}`)
|
|
1004
|
-
);
|
|
1005
|
-
const setupResults = results.filter((r) => r.type === "setup");
|
|
1006
|
-
const installResults = results.filter((r) => r.type !== "setup");
|
|
1007
|
-
await Promise.all(
|
|
1008
|
-
installResults.map(async (result2) => {
|
|
1009
|
-
const target = toTargetPath(result2);
|
|
1010
|
-
const filePath = safePath(downloadDir, target);
|
|
1011
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
1012
|
-
await writeFile(filePath, result2.content, "utf-8");
|
|
1013
|
-
})
|
|
1014
|
-
);
|
|
1015
|
-
let setupFile;
|
|
1016
|
-
if (setupResults.length > 0) {
|
|
1017
|
-
const setup = setupResults[0];
|
|
1018
|
-
setupFile = join(downloadDir, "SETUP.md");
|
|
1019
|
-
await mkdir(dirname(setupFile), { recursive: true });
|
|
1020
|
-
await writeFile(setupFile, setup.content, "utf-8");
|
|
1021
|
-
}
|
|
1022
|
-
stepper.succeed(`Downloaded ${results.length} file(s)`);
|
|
1023
|
-
const downloadedPaths = installResults.map((r) => toTargetPath(r));
|
|
1024
|
-
const providerFullPath = join(projectRoot, provider.path);
|
|
1025
|
-
const pruned = pruneUnchanged(downloadDir, providerFullPath);
|
|
1026
|
-
if (pruned > 0) {
|
|
1027
|
-
stepper.succeed(`Skipped ${pruned} unchanged file(s)`);
|
|
1028
|
-
}
|
|
1029
|
-
const model = skillset.provider === "claude" ? "haiku" : void 0;
|
|
1030
|
-
const result = await installSkillset(
|
|
1031
|
-
{
|
|
1032
|
-
downloadDir,
|
|
1033
|
-
setupFile,
|
|
1034
|
-
providerDir: providerFullPath,
|
|
1035
|
-
skillsetName: skillset.name,
|
|
1036
|
-
skillsetVersion: skillset.version,
|
|
1037
|
-
source: `@${location.owner}/${location.repository}`,
|
|
1038
|
-
configPath: getConfigPath(),
|
|
1039
|
-
model
|
|
1040
|
-
},
|
|
1041
|
-
stepper
|
|
1042
|
-
);
|
|
1043
|
-
await rm(downloadDir, { recursive: true, force: true });
|
|
1044
|
-
const spmDir = join(projectRoot, ".spm");
|
|
1045
|
-
const remaining = await readdir(spmDir).catch(() => []);
|
|
1046
|
-
if (remaining.length === 0) await rm(spmDir, { recursive: true, force: true });
|
|
1047
|
-
stepper.stop();
|
|
1048
|
-
printCompleted(startedAt);
|
|
1049
|
-
const summaryFiles = result.files.length > 0 ? result.files : downloadedPaths;
|
|
1050
|
-
printSummary(summaryFiles, provider.path);
|
|
1051
|
-
process.stdout.write(
|
|
1052
|
-
`
|
|
1053
|
-
šŖ ${cyan}Restart your AI agent to apply the new skills.${reset$2}
|
|
1054
|
-
`
|
|
1055
|
-
);
|
|
1276
|
+
//#endregion
|
|
1277
|
+
//#region src/commands/install.ts
|
|
1278
|
+
var registerInstallCommand = (program) => {
|
|
1279
|
+
program.command("install <target>").alias("i").description("Install a skillset or skill into the project").action(async (input) => {
|
|
1280
|
+
const stepper = createStepper();
|
|
1281
|
+
const startedAt = Date.now();
|
|
1282
|
+
try {
|
|
1283
|
+
const parsed = parseIdentifier(input);
|
|
1284
|
+
if (parsed.kind === "skill") await installSkillFlow(parsed.identifier, stepper, startedAt);
|
|
1285
|
+
else await installSkillsetFlow(input, stepper, startedAt);
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1288
|
+
stepper.fail(message);
|
|
1289
|
+
stepper.stop();
|
|
1290
|
+
process.exitCode = 1;
|
|
1291
|
+
}
|
|
1292
|
+
});
|
|
1056
1293
|
};
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const firstProvider = Object.entries(config.providers)[0];
|
|
1064
|
-
if (!firstProvider) {
|
|
1065
|
-
throw new Error(
|
|
1066
|
-
"No provider detected. Initialize a provider directory first (e.g., .claude/)."
|
|
1067
|
-
);
|
|
1068
|
-
}
|
|
1069
|
-
const [providerName, providerConfig] = firstProvider;
|
|
1070
|
-
const providerPath = providerConfig.path;
|
|
1071
|
-
const projectRoot = getProjectRoot();
|
|
1072
|
-
const downloadDir = join(projectRoot, ".spm", resolved.name);
|
|
1073
|
-
const skillDir = posix.dirname(resolved.location.path);
|
|
1074
|
-
stepper.start(`Downloading ${resolved.files.length} file(s)...`, "packages");
|
|
1075
|
-
await Promise.all(
|
|
1076
|
-
resolved.files.map(async (file) => {
|
|
1077
|
-
const relativePath = file.path.startsWith(`${skillDir}/`) ? file.path.slice(skillDir.length + 1) : file.path.split("/").pop() ?? file.path;
|
|
1078
|
-
const filePath = safePath(downloadDir, relativePath);
|
|
1079
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
1080
|
-
await writeFile(filePath, file.content, "utf-8");
|
|
1081
|
-
stepper.item(relativePath);
|
|
1082
|
-
})
|
|
1083
|
-
);
|
|
1084
|
-
stepper.succeed(`Downloaded ${resolved.files.length} file(s)`);
|
|
1085
|
-
const providerFullPath = join(projectRoot, providerPath);
|
|
1086
|
-
const skillProviderDir = join(providerFullPath, "skills", resolved.name);
|
|
1087
|
-
const pruned = pruneUnchanged(downloadDir, skillProviderDir);
|
|
1088
|
-
if (pruned > 0) {
|
|
1089
|
-
stepper.succeed(`Skipped ${pruned} unchanged file(s)`);
|
|
1090
|
-
}
|
|
1091
|
-
const model = providerName === "claude" ? "haiku" : void 0;
|
|
1092
|
-
const source = `@${resolved.location.owner}/${resolved.location.repository}`;
|
|
1093
|
-
const result = await installSingleSkill(
|
|
1094
|
-
{
|
|
1095
|
-
downloadDir,
|
|
1096
|
-
providerDir: providerFullPath,
|
|
1097
|
-
skillName: resolved.name,
|
|
1098
|
-
source,
|
|
1099
|
-
configPath: getConfigPath(),
|
|
1100
|
-
model
|
|
1101
|
-
},
|
|
1102
|
-
stepper
|
|
1103
|
-
);
|
|
1104
|
-
await rm(downloadDir, { recursive: true, force: true });
|
|
1105
|
-
const spmDir = join(projectRoot, ".spm");
|
|
1106
|
-
const remaining = await readdir(spmDir).catch(() => []);
|
|
1107
|
-
if (remaining.length === 0) await rm(spmDir, { recursive: true, force: true });
|
|
1108
|
-
stepper.stop();
|
|
1109
|
-
printCompleted(startedAt);
|
|
1110
|
-
const downloadedPaths = resolved.files.map((f) => {
|
|
1111
|
-
const skillDirPrefix = `${skillDir}/`;
|
|
1112
|
-
return f.path.startsWith(skillDirPrefix) ? `skills/${resolved.name}/${f.path.slice(skillDirPrefix.length)}` : `skills/${resolved.name}/${f.path.split("/").pop() ?? f.path}`;
|
|
1113
|
-
});
|
|
1114
|
-
const summaryFiles = result.files.length > 0 ? result.files : downloadedPaths;
|
|
1115
|
-
printSummary(summaryFiles, providerPath);
|
|
1116
|
-
process.stdout.write(
|
|
1117
|
-
`
|
|
1118
|
-
šŖ ${cyan}Restart your AI agent to apply the new skills.${reset$2}
|
|
1119
|
-
`
|
|
1120
|
-
);
|
|
1294
|
+
//#endregion
|
|
1295
|
+
//#region src/commands/list.ts
|
|
1296
|
+
var registerListCommand = (program) => {
|
|
1297
|
+
program.command("list").alias("ls").description("List installed skillsets").action(() => {
|
|
1298
|
+
console.log("Not implemented yet");
|
|
1299
|
+
});
|
|
1121
1300
|
};
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
}
|
|
1133
|
-
} catch (err) {
|
|
1134
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
1135
|
-
stepper.fail(message);
|
|
1136
|
-
stepper.stop();
|
|
1137
|
-
process.exitCode = 1;
|
|
1138
|
-
}
|
|
1139
|
-
});
|
|
1301
|
+
//#endregion
|
|
1302
|
+
//#region src/commands/uninstall.ts
|
|
1303
|
+
var findSkillProvider = (config, skillName) => {
|
|
1304
|
+
const entry = Object.entries(config.providers).find(([, provider]) => provider.skills != null && Object.hasOwn(provider.skills, skillName));
|
|
1305
|
+
if (!entry) return void 0;
|
|
1306
|
+
const [, provider] = entry;
|
|
1307
|
+
return {
|
|
1308
|
+
providerPath: provider.path,
|
|
1309
|
+
source: provider.skills?.[skillName] ?? ""
|
|
1310
|
+
};
|
|
1140
1311
|
};
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1312
|
+
var registerUninstallCommand = (program) => {
|
|
1313
|
+
program.command("uninstall <skill>").alias("un").description("Uninstall a skill from the project").action(async (skillName) => {
|
|
1314
|
+
const stepper = createStepper();
|
|
1315
|
+
try {
|
|
1316
|
+
stepper.start("Removing skill...", "packages");
|
|
1317
|
+
const { config } = readConfig();
|
|
1318
|
+
const match = findSkillProvider(config, skillName);
|
|
1319
|
+
if (!match) throw new Error(`Skill "${skillName}" is not installed`);
|
|
1320
|
+
const skillsParentDir = join(getProjectRoot(), match.providerPath, "skills");
|
|
1321
|
+
const skillDir = join(skillsParentDir, skillName);
|
|
1322
|
+
if (!resolve(skillDir).startsWith(`${resolve(skillsParentDir)}${sep}`)) throw new Error(`Invalid skill name: "${skillName}"`);
|
|
1323
|
+
await rm(skillDir, {
|
|
1324
|
+
recursive: true,
|
|
1325
|
+
force: true
|
|
1326
|
+
});
|
|
1327
|
+
if ((await readdir(skillsParentDir).catch(() => [])).length === 0) await rm(skillsParentDir, {
|
|
1328
|
+
recursive: true,
|
|
1329
|
+
force: true
|
|
1330
|
+
});
|
|
1331
|
+
removeConfigEntry({
|
|
1332
|
+
providerPath: match.providerPath,
|
|
1333
|
+
kind: "skills",
|
|
1334
|
+
name: skillName
|
|
1335
|
+
});
|
|
1336
|
+
stepper.succeed(`${green}Removed${reset$2}`, `${skillName} ${dim$1}from ${match.providerPath}${reset$2}`);
|
|
1337
|
+
stepper.stop();
|
|
1338
|
+
process.stdout.write(`\nšŖ ${cyan}Restart your AI agent to apply the changes.${reset$2}\n`);
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1341
|
+
stepper.fail(message);
|
|
1342
|
+
stepper.stop();
|
|
1343
|
+
process.exitCode = 1;
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1145
1346
|
};
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1347
|
+
//#endregion
|
|
1348
|
+
//#region src/utils/banner.ts
|
|
1349
|
+
var light = "\x1B[96m";
|
|
1350
|
+
var shade = "\x1B[36m";
|
|
1351
|
+
var dim = "\x1B[2m";
|
|
1352
|
+
var reset$1 = "\x1B[0m";
|
|
1353
|
+
var banner = (version) => [
|
|
1354
|
+
`${shade} ā${light}ā āā ${reset$1}AI Skill Package Manager`,
|
|
1355
|
+
`${shade} ā${light}āāāāā ${reset$1}install ⢠compose ⢠share`,
|
|
1356
|
+
`${shade}āā${light}āāāāāā ${reset$1}${dim}v${version} ⨠supa-magic${reset$1}`,
|
|
1357
|
+
`${shade} ā${light} ā`
|
|
1155
1358
|
].join("\n");
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
});
|
|
1359
|
+
//#endregion
|
|
1360
|
+
//#region src/bin/spm.ts
|
|
1361
|
+
var version = "0.4.0";
|
|
1362
|
+
var program = new Command();
|
|
1363
|
+
var gray = "\x1B[90m";
|
|
1364
|
+
var reset = "\x1B[0m";
|
|
1365
|
+
program.name("spm").description(banner(version)).version(version, "-v, --version").configureHelp({ formatHelp: (cmd, helper) => {
|
|
1366
|
+
const [description, ...rest] = Command.prototype.createHelp().formatHelp(cmd, helper).split("\n\n");
|
|
1367
|
+
return [description, `${gray}${rest.join("\n\n")}${reset}`].join("\n\n");
|
|
1368
|
+
} });
|
|
1167
1369
|
registerInitCommand(program);
|
|
1168
1370
|
registerInstallCommand(program);
|
|
1371
|
+
registerUninstallCommand(program);
|
|
1169
1372
|
registerListCommand(program);
|
|
1170
1373
|
registerDoctorCommand(program);
|
|
1171
1374
|
program.parse();
|
|
1375
|
+
//#endregion
|
|
1376
|
+
export {};
|