agentspec-cli 0.1.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/README.md +343 -0
- package/dist/index.js +1786 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1786 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command as Command9 } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/info.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import pc3 from "picocolors";
|
|
9
|
+
|
|
10
|
+
// src/lib/constants.ts
|
|
11
|
+
import { homedir } from "os";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
var AGENTSPEC_HOME = process.env["AGENTSPEC_HOME"] ?? join(homedir(), ".agentspec");
|
|
14
|
+
var AGENTSPEC_STORE = join(AGENTSPEC_HOME, "store");
|
|
15
|
+
var AGENTSPEC_BACKUPS = join(AGENTSPEC_HOME, "backups");
|
|
16
|
+
var AGENTSPEC_PROJECTS = join(AGENTSPEC_HOME, "projects");
|
|
17
|
+
var AGENTSPEC_GLOBAL_STATE = join(AGENTSPEC_HOME, "global-state.json");
|
|
18
|
+
var API_BASE = process.env["AGENTSPEC_API_URL"] ?? "https://api.agentspec.sh";
|
|
19
|
+
var FRAMEWORK_FILE_PATHS = {
|
|
20
|
+
Codex: "codex.toml",
|
|
21
|
+
OpenCode: "opencode.json",
|
|
22
|
+
ClaudeCode: ".claude/settings.json"
|
|
23
|
+
};
|
|
24
|
+
var SKILL_FRAMEWORK_DIRS = {
|
|
25
|
+
ClaudeCode: ".claude/skills",
|
|
26
|
+
Codex: ".codex/skills",
|
|
27
|
+
OpenCode: ".opencode/skills",
|
|
28
|
+
Windsurf: ".windsurf/skills",
|
|
29
|
+
Universal: "skills"
|
|
30
|
+
};
|
|
31
|
+
var RULE_FILE_PATHS = {
|
|
32
|
+
cursorrules: ".cursorrules",
|
|
33
|
+
"cursor-rules": ".cursor/rules/",
|
|
34
|
+
"claude-md": "CLAUDE.md",
|
|
35
|
+
"agents-md": "AGENTS.md",
|
|
36
|
+
"copilot-instructions": ".github/copilot-instructions.md",
|
|
37
|
+
windsurfrules: ".windsurfrules",
|
|
38
|
+
"windsurf-rules": ".windsurf/rules/",
|
|
39
|
+
clinerules: ".clinerules",
|
|
40
|
+
"gemini-md": "GEMINI.md"
|
|
41
|
+
};
|
|
42
|
+
var GLOBAL_FRAMEWORK_FILE_PATHS = {
|
|
43
|
+
ClaudeCode: join(homedir(), ".claude", "settings.json"),
|
|
44
|
+
OpenCode: join(homedir(), ".config", "opencode", "opencode.json"),
|
|
45
|
+
Codex: join(homedir(), ".codex", "codex.toml")
|
|
46
|
+
};
|
|
47
|
+
var GLOBAL_SKILL_FRAMEWORK_DIRS = {
|
|
48
|
+
ClaudeCode: join(homedir(), ".claude", "skills"),
|
|
49
|
+
Codex: join(homedir(), ".codex", "skills"),
|
|
50
|
+
OpenCode: join(homedir(), ".opencode", "skills")
|
|
51
|
+
};
|
|
52
|
+
var RULE_FILE_LABELS = {
|
|
53
|
+
cursorrules: "Cursor (.cursorrules)",
|
|
54
|
+
"cursor-rules": "Cursor (.cursor/rules/)",
|
|
55
|
+
"claude-md": "Claude Code (CLAUDE.md)",
|
|
56
|
+
"agents-md": "Codex / Claude Code (AGENTS.md)",
|
|
57
|
+
"copilot-instructions": "GitHub Copilot (.github/copilot-instructions.md)",
|
|
58
|
+
windsurfrules: "Windsurf (.windsurfrules)",
|
|
59
|
+
"windsurf-rules": "Windsurf (.windsurf/rules/)",
|
|
60
|
+
clinerules: "Cline (.clinerules)",
|
|
61
|
+
"gemini-md": "Gemini (GEMINI.md)"
|
|
62
|
+
};
|
|
63
|
+
var SKILL_FRAMEWORK_LABELS = {
|
|
64
|
+
ClaudeCode: "Claude Code (.claude/skills/)",
|
|
65
|
+
Codex: "Codex (.codex/skills/)",
|
|
66
|
+
OpenCode: "OpenCode (.opencode/skills/)",
|
|
67
|
+
Windsurf: "Windsurf (.windsurf/skills/)",
|
|
68
|
+
Universal: "Universal (skills/)"
|
|
69
|
+
};
|
|
70
|
+
var GLOBAL_SUPPORTED_FRAMEWORKS = /* @__PURE__ */ new Set(["ClaudeCode", "OpenCode", "Codex"]);
|
|
71
|
+
|
|
72
|
+
// src/lib/api.ts
|
|
73
|
+
var ApiError = class extends Error {
|
|
74
|
+
constructor(status, message) {
|
|
75
|
+
super(message);
|
|
76
|
+
this.status = status;
|
|
77
|
+
this.name = "ApiError";
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
async function apiFetch(path) {
|
|
81
|
+
const url = `${API_BASE}${path}`;
|
|
82
|
+
const res = await fetch(url, {
|
|
83
|
+
headers: { "User-Agent": "agentspec-cli/0.1.0", Accept: "application/json" }
|
|
84
|
+
});
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
let message = `HTTP ${res.status}`;
|
|
87
|
+
try {
|
|
88
|
+
const body = await res.json();
|
|
89
|
+
if (body.message) message = body.message;
|
|
90
|
+
} catch {
|
|
91
|
+
}
|
|
92
|
+
throw new ApiError(res.status, message);
|
|
93
|
+
}
|
|
94
|
+
return res.json();
|
|
95
|
+
}
|
|
96
|
+
async function resolve(id) {
|
|
97
|
+
const { data } = await apiFetch(`/api/resolve/${id}`);
|
|
98
|
+
return data;
|
|
99
|
+
}
|
|
100
|
+
async function getConfig(id) {
|
|
101
|
+
const { data } = await apiFetch(`/api/templates/${id}`);
|
|
102
|
+
return data;
|
|
103
|
+
}
|
|
104
|
+
async function getSkill(id) {
|
|
105
|
+
const { data } = await apiFetch(`/api/skills/${id}`);
|
|
106
|
+
return data;
|
|
107
|
+
}
|
|
108
|
+
async function getRule(id) {
|
|
109
|
+
const { data } = await apiFetch(`/api/rules/${id}`);
|
|
110
|
+
return data;
|
|
111
|
+
}
|
|
112
|
+
async function getSpec(id) {
|
|
113
|
+
const { data } = await apiFetch(`/api/specs/${id}`);
|
|
114
|
+
return data;
|
|
115
|
+
}
|
|
116
|
+
async function getPlugin(id) {
|
|
117
|
+
const { data } = await apiFetch(`/api/plugins/${id}`);
|
|
118
|
+
return data;
|
|
119
|
+
}
|
|
120
|
+
async function listItems(type, opts = {}) {
|
|
121
|
+
const endpoint = type === "configs" ? "templates" : type;
|
|
122
|
+
const params = new URLSearchParams();
|
|
123
|
+
if (opts.sort) params.set("sort", opts.sort);
|
|
124
|
+
if (opts.framework) params.set("framework", opts.framework);
|
|
125
|
+
if (opts.fileType) params.set("fileType", opts.fileType);
|
|
126
|
+
if (opts.limit != null) params.set("limit", String(opts.limit));
|
|
127
|
+
if (opts.offset != null) params.set("offset", String(opts.offset));
|
|
128
|
+
const qs = params.toString();
|
|
129
|
+
const raw = await apiFetch(
|
|
130
|
+
`/api/${endpoint}${qs ? `?${qs}` : ""}`
|
|
131
|
+
);
|
|
132
|
+
const data = raw.data.map((item) => ({
|
|
133
|
+
...item,
|
|
134
|
+
title: item.title ?? item.name ?? ""
|
|
135
|
+
}));
|
|
136
|
+
return { data, total: raw.total };
|
|
137
|
+
}
|
|
138
|
+
async function search(query, opts = {}) {
|
|
139
|
+
const params = new URLSearchParams({ q: query });
|
|
140
|
+
if (opts.type) params.set("type", opts.type);
|
|
141
|
+
if (opts.limit != null) params.set("limit", String(opts.limit));
|
|
142
|
+
return apiFetch(`/api/search?${params.toString()}`);
|
|
143
|
+
}
|
|
144
|
+
function trackInstall(type, id) {
|
|
145
|
+
const pathMap = { config: "templates", skill: "skills", rule: "rules", spec: "specs" };
|
|
146
|
+
const path = `/${pathMap[type]}/${id}/install`;
|
|
147
|
+
fetch(`${API_BASE}${path}`, { headers: { "User-Agent": "agentspec-cli/0.1.0" } }).catch(() => {
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// src/lib/prompts.ts
|
|
152
|
+
import { confirm, isCancel, select, spinner as clackSpinner } from "@clack/prompts";
|
|
153
|
+
import pc from "picocolors";
|
|
154
|
+
var IS_TTY = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
155
|
+
function assertNotCancel(value) {
|
|
156
|
+
if (isCancel(value)) {
|
|
157
|
+
console.log(pc.dim("\nCancelled."));
|
|
158
|
+
process.exit(0);
|
|
159
|
+
}
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
async function promptScope(opts) {
|
|
163
|
+
if (!IS_TTY) return "project";
|
|
164
|
+
const result = await select({
|
|
165
|
+
message: "Install to:",
|
|
166
|
+
options: [
|
|
167
|
+
{ value: "project", label: "This project", hint: opts.projectLabel },
|
|
168
|
+
{ value: "global", label: "Global (symlink)", hint: opts.globalLabel }
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
return assertNotCancel(result);
|
|
172
|
+
}
|
|
173
|
+
async function promptConfirm(message, initialValue = true) {
|
|
174
|
+
if (!IS_TTY) {
|
|
175
|
+
console.log(
|
|
176
|
+
pc.dim(
|
|
177
|
+
` ${message} ${initialValue ? "[Y/n]" : "[y/N]"} \u2192 ${initialValue ? "yes" : "no"} (non-interactive)`
|
|
178
|
+
)
|
|
179
|
+
);
|
|
180
|
+
return initialValue;
|
|
181
|
+
}
|
|
182
|
+
const result = await confirm({ message, initialValue });
|
|
183
|
+
return assertNotCancel(result);
|
|
184
|
+
}
|
|
185
|
+
async function promptFramework(frameworks, labels) {
|
|
186
|
+
if (!IS_TTY) return frameworks[0];
|
|
187
|
+
const result = await select({
|
|
188
|
+
message: "Select target framework:",
|
|
189
|
+
options: frameworks.map((fw) => ({ value: fw, label: labels[fw] ?? fw }))
|
|
190
|
+
});
|
|
191
|
+
return assertNotCancel(result);
|
|
192
|
+
}
|
|
193
|
+
async function promptFileType(fileTypes, labels) {
|
|
194
|
+
if (!IS_TTY) return fileTypes[0];
|
|
195
|
+
const result = await select({
|
|
196
|
+
message: "Select target file type:",
|
|
197
|
+
options: fileTypes.map((ft) => ({ value: ft, label: labels[ft] ?? ft }))
|
|
198
|
+
});
|
|
199
|
+
return assertNotCancel(result);
|
|
200
|
+
}
|
|
201
|
+
async function promptUninstallAction(targetPath) {
|
|
202
|
+
if (!IS_TTY) return "keep";
|
|
203
|
+
const result = await select({
|
|
204
|
+
message: `${pc.yellow(targetPath)} was installed as a copy and may have been modified.`,
|
|
205
|
+
options: [
|
|
206
|
+
{
|
|
207
|
+
value: "keep",
|
|
208
|
+
label: "Keep the file",
|
|
209
|
+
hint: "just stop tracking it with agentspec"
|
|
210
|
+
},
|
|
211
|
+
{ value: "delete", label: "Delete the file", hint: "removes it from disk" }
|
|
212
|
+
]
|
|
213
|
+
});
|
|
214
|
+
return assertNotCancel(result);
|
|
215
|
+
}
|
|
216
|
+
function createSpinner() {
|
|
217
|
+
if (!IS_TTY) {
|
|
218
|
+
return {
|
|
219
|
+
start(msg) {
|
|
220
|
+
process.stdout.write(`${msg}...
|
|
221
|
+
`);
|
|
222
|
+
},
|
|
223
|
+
stop(msg) {
|
|
224
|
+
process.stdout.write(`\u2713 ${msg}
|
|
225
|
+
`);
|
|
226
|
+
},
|
|
227
|
+
fail(msg) {
|
|
228
|
+
process.stderr.write(`\u2717 ${msg}
|
|
229
|
+
`);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const s = clackSpinner();
|
|
234
|
+
return {
|
|
235
|
+
start(msg) {
|
|
236
|
+
s.start(msg);
|
|
237
|
+
},
|
|
238
|
+
stop(msg) {
|
|
239
|
+
s.stop(msg);
|
|
240
|
+
},
|
|
241
|
+
fail(msg) {
|
|
242
|
+
s.stop(pc.red(`\u2717 ${msg}`));
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// src/lib/ui.ts
|
|
248
|
+
import pc2 from "picocolors";
|
|
249
|
+
var log = {
|
|
250
|
+
info: (msg) => console.log(msg),
|
|
251
|
+
success: (msg) => console.log(`${pc2.green("\u2713")} ${msg}`),
|
|
252
|
+
warn: (msg) => console.warn(`${pc2.yellow("!")} ${msg}`),
|
|
253
|
+
error: (msg) => console.error(`${pc2.red("\u2717")} ${msg}`),
|
|
254
|
+
step: (msg) => console.log(` ${pc2.dim("\u2192")} ${msg}`),
|
|
255
|
+
tip: (msg) => console.log(pc2.dim(` ${msg}`))
|
|
256
|
+
};
|
|
257
|
+
function printTable(headers, rows) {
|
|
258
|
+
if (rows.length === 0) return;
|
|
259
|
+
const colWidths = headers.map((h, i) => {
|
|
260
|
+
const maxRow = Math.max(...rows.map((r) => stripAnsi(r[i] ?? "").length));
|
|
261
|
+
return Math.max(h.length, maxRow);
|
|
262
|
+
});
|
|
263
|
+
const divider2 = colWidths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
|
|
264
|
+
const fmt = (cells, bold = false) => cells.map((c, i) => {
|
|
265
|
+
const visible = stripAnsi(c);
|
|
266
|
+
const padded = c + " ".repeat(Math.max(0, (colWidths[i] ?? 0) - visible.length));
|
|
267
|
+
return bold ? pc2.bold(padded) : padded;
|
|
268
|
+
}).join(" ");
|
|
269
|
+
console.log(fmt(headers, true));
|
|
270
|
+
console.log(pc2.dim(divider2));
|
|
271
|
+
for (const row of rows) {
|
|
272
|
+
console.log(fmt(row));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function stripAnsi(str) {
|
|
276
|
+
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
277
|
+
}
|
|
278
|
+
function truncate(str, maxLen) {
|
|
279
|
+
if (str.length <= maxLen) return str;
|
|
280
|
+
return str.slice(0, maxLen - 1) + "\u2026";
|
|
281
|
+
}
|
|
282
|
+
function formatDate(dateStr) {
|
|
283
|
+
const date = new Date(dateStr);
|
|
284
|
+
const now = Date.now();
|
|
285
|
+
const diffMs = now - date.getTime();
|
|
286
|
+
const diffDays = Math.floor(diffMs / 864e5);
|
|
287
|
+
if (diffDays === 0) return "today";
|
|
288
|
+
if (diffDays === 1) return "yesterday";
|
|
289
|
+
if (diffDays < 30) return `${diffDays}d ago`;
|
|
290
|
+
if (diffDays < 365) return `${Math.floor(diffDays / 30)}mo ago`;
|
|
291
|
+
return `${Math.floor(diffDays / 365)}y ago`;
|
|
292
|
+
}
|
|
293
|
+
function formatScore(up, down) {
|
|
294
|
+
const net = up - down;
|
|
295
|
+
const sign = net >= 0 ? "+" : "";
|
|
296
|
+
return `${sign}${net}`;
|
|
297
|
+
}
|
|
298
|
+
function typeLabel(type) {
|
|
299
|
+
const labels = {
|
|
300
|
+
config: "config",
|
|
301
|
+
skill: "skill",
|
|
302
|
+
rule: "rule",
|
|
303
|
+
spec: "spec",
|
|
304
|
+
plugin: "plugin"
|
|
305
|
+
};
|
|
306
|
+
return labels[type] ?? type;
|
|
307
|
+
}
|
|
308
|
+
function typeBadge(type) {
|
|
309
|
+
const colors = {
|
|
310
|
+
config: pc2.blue,
|
|
311
|
+
skill: pc2.magenta,
|
|
312
|
+
rule: pc2.yellow,
|
|
313
|
+
spec: pc2.cyan,
|
|
314
|
+
plugin: pc2.green
|
|
315
|
+
};
|
|
316
|
+
const color = colors[type] ?? pc2.white;
|
|
317
|
+
return color(typeLabel(type));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// src/commands/info.ts
|
|
321
|
+
var infoCommand = new Command("info").description("Show details for a config, skill, rule, spec, or plugin").argument("<id>", "Content UUID").option("--type <type>", "Skip auto-detect: config|skill|rule|spec|plugin").action(async (id, opts) => {
|
|
322
|
+
let contentType = opts.type;
|
|
323
|
+
if (!contentType) {
|
|
324
|
+
const spinner = createSpinner();
|
|
325
|
+
spinner.start(`Resolving ${pc3.cyan(id)}`);
|
|
326
|
+
try {
|
|
327
|
+
const resolved = await resolve(id);
|
|
328
|
+
spinner.stop(`${pc3.bold(resolved.title)} (${pc3.dim(resolved.type)})`);
|
|
329
|
+
contentType = resolved.type;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
spinner.fail(
|
|
332
|
+
`Could not resolve ${id}: ${err instanceof Error ? err.message : String(err)}`
|
|
333
|
+
);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
try {
|
|
338
|
+
switch (contentType) {
|
|
339
|
+
case "config":
|
|
340
|
+
await showConfig(id);
|
|
341
|
+
break;
|
|
342
|
+
case "skill":
|
|
343
|
+
await showSkill(id);
|
|
344
|
+
break;
|
|
345
|
+
case "rule":
|
|
346
|
+
await showRule(id);
|
|
347
|
+
break;
|
|
348
|
+
case "spec":
|
|
349
|
+
await showSpec(id);
|
|
350
|
+
break;
|
|
351
|
+
case "plugin":
|
|
352
|
+
await showPlugin(id);
|
|
353
|
+
break;
|
|
354
|
+
default:
|
|
355
|
+
log.error(`Unknown content type: ${contentType}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
log.error(err instanceof Error ? err.message : String(err));
|
|
360
|
+
process.exit(1);
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
function field(label, value) {
|
|
364
|
+
if (value == null || value === "") return;
|
|
365
|
+
const pad = 14;
|
|
366
|
+
console.log(` ${pc3.dim(label.padEnd(pad))} ${value}`);
|
|
367
|
+
}
|
|
368
|
+
function divider(title) {
|
|
369
|
+
if (title) {
|
|
370
|
+
console.log(`
|
|
371
|
+
${pc3.bold(title)}`);
|
|
372
|
+
console.log(pc3.dim("\u2500".repeat(title.length)));
|
|
373
|
+
} else {
|
|
374
|
+
console.log(pc3.dim("\u2500".repeat(48)));
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
function previewContent(content, lines = 12) {
|
|
378
|
+
const preview = content.split("\n").slice(0, lines).join("\n");
|
|
379
|
+
const hasMore = content.split("\n").length > lines;
|
|
380
|
+
for (const line of preview.split("\n")) {
|
|
381
|
+
console.log(` ${pc3.dim(line)}`);
|
|
382
|
+
}
|
|
383
|
+
if (hasMore) {
|
|
384
|
+
console.log(pc3.dim(" [...]"));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
async function showConfig(id) {
|
|
388
|
+
const spinner = createSpinner();
|
|
389
|
+
spinner.start("Fetching config...");
|
|
390
|
+
const config = await getConfig(id);
|
|
391
|
+
spinner.stop(pc3.bold(config.title));
|
|
392
|
+
divider();
|
|
393
|
+
field("Type", "config");
|
|
394
|
+
if (config.slug) field("Slug", pc3.cyan(config.slug));
|
|
395
|
+
field("Framework", config.framework);
|
|
396
|
+
field("Format", config.format);
|
|
397
|
+
field("Installs to", FRAMEWORK_FILE_PATHS[config.framework]);
|
|
398
|
+
field("Author", config.author.name);
|
|
399
|
+
field("Votes", formatScore(config.upvoteCount, config.downvoteCount));
|
|
400
|
+
field("Installs", String(config.installCount));
|
|
401
|
+
field("Created", formatDate(config.createdAt));
|
|
402
|
+
if (config.description) {
|
|
403
|
+
console.log();
|
|
404
|
+
console.log(` ${config.description}`);
|
|
405
|
+
}
|
|
406
|
+
divider("Preview");
|
|
407
|
+
previewContent(config.configuration);
|
|
408
|
+
console.log();
|
|
409
|
+
const configRef = config.slug ?? id;
|
|
410
|
+
log.tip(`Install with: agentspec install ${configRef}`);
|
|
411
|
+
}
|
|
412
|
+
async function showSkill(id) {
|
|
413
|
+
const spinner = createSpinner();
|
|
414
|
+
spinner.start("Fetching skill...");
|
|
415
|
+
const skill = await getSkill(id);
|
|
416
|
+
spinner.stop(pc3.bold(skill.name));
|
|
417
|
+
const dirsPreview = skill.frameworks.map((fw) => `${fw} \u2192 ${SKILL_FRAMEWORK_DIRS[fw] ?? "?"}/${skill.name}/SKILL.md`).join(", ");
|
|
418
|
+
divider();
|
|
419
|
+
field("Type", "skill");
|
|
420
|
+
if (skill.slug) field("Slug", pc3.cyan(skill.slug));
|
|
421
|
+
field("Frameworks", skill.frameworks.join(", "));
|
|
422
|
+
field("Installs to", dirsPreview);
|
|
423
|
+
field("Author", skill.author.name);
|
|
424
|
+
field("Votes", formatScore(skill.upvoteCount, skill.downvoteCount));
|
|
425
|
+
field("Installs", String(skill.installCount + skill.externalInstalls));
|
|
426
|
+
field("Created", formatDate(skill.createdAt));
|
|
427
|
+
if (skill.description) {
|
|
428
|
+
console.log();
|
|
429
|
+
console.log(` ${skill.description}`);
|
|
430
|
+
}
|
|
431
|
+
divider("Preview");
|
|
432
|
+
previewContent(skill.skillContent);
|
|
433
|
+
console.log();
|
|
434
|
+
const skillRef = skill.slug ?? id;
|
|
435
|
+
log.tip(`Install with: agentspec install ${skillRef}`);
|
|
436
|
+
if (skill.frameworks.length > 1) {
|
|
437
|
+
log.tip(`Pick framework: agentspec install ${skillRef} --framework <name>`);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async function showRule(id) {
|
|
441
|
+
const spinner = createSpinner();
|
|
442
|
+
spinner.start("Fetching rule...");
|
|
443
|
+
const rule = await getRule(id);
|
|
444
|
+
spinner.stop(pc3.bold(rule.title));
|
|
445
|
+
const pathsPreview = rule.fileTypes.map((ft) => `${ft} \u2192 ${RULE_FILE_PATHS[ft] ?? "?"}`).join(", ");
|
|
446
|
+
divider();
|
|
447
|
+
field("Type", "rule");
|
|
448
|
+
if (rule.slug) field("Slug", pc3.cyan(rule.slug));
|
|
449
|
+
field("File types", rule.fileTypes.join(", "));
|
|
450
|
+
field("Installs to", pathsPreview);
|
|
451
|
+
field("Author", rule.author.name);
|
|
452
|
+
field("Votes", formatScore(rule.upvoteCount, rule.downvoteCount));
|
|
453
|
+
field("Installs", String(rule.installCount));
|
|
454
|
+
field("Created", formatDate(rule.createdAt));
|
|
455
|
+
if (rule.description) {
|
|
456
|
+
console.log();
|
|
457
|
+
console.log(` ${rule.description}`);
|
|
458
|
+
}
|
|
459
|
+
divider("Preview");
|
|
460
|
+
previewContent(rule.content);
|
|
461
|
+
console.log();
|
|
462
|
+
const ruleRef = rule.slug ?? id;
|
|
463
|
+
log.tip(`Install with: agentspec install ${ruleRef}`);
|
|
464
|
+
if (rule.fileTypes.length > 1) {
|
|
465
|
+
log.tip(`Pick file type: agentspec install ${ruleRef} --file-type <type>`);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
async function showSpec(id) {
|
|
469
|
+
const spinner = createSpinner();
|
|
470
|
+
spinner.start("Fetching spec...");
|
|
471
|
+
const spec = await getSpec(id);
|
|
472
|
+
spinner.stop(pc3.bold(spec.title));
|
|
473
|
+
divider();
|
|
474
|
+
field("Type", "spec");
|
|
475
|
+
if (spec.slug) field("Slug", pc3.cyan(spec.slug));
|
|
476
|
+
field("Framework", spec.framework);
|
|
477
|
+
field("Items", String(spec.items.length));
|
|
478
|
+
field("Author", spec.author.name);
|
|
479
|
+
field("Votes", formatScore(spec.upvoteCount, spec.downvoteCount));
|
|
480
|
+
field("Installs", String(spec.installCount));
|
|
481
|
+
field("Created", formatDate(spec.createdAt));
|
|
482
|
+
if (spec.description) {
|
|
483
|
+
console.log();
|
|
484
|
+
console.log(` ${spec.description}`);
|
|
485
|
+
}
|
|
486
|
+
if (spec.items.length > 0) {
|
|
487
|
+
divider("Included items");
|
|
488
|
+
for (const item of spec.items) {
|
|
489
|
+
const data = item.data;
|
|
490
|
+
const itemTitle = data.title ?? data.name ?? item.data.id;
|
|
491
|
+
console.log(` ${pc3.dim(`[${item.targetType}]`)} ${itemTitle}`);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
console.log();
|
|
495
|
+
const specRef = spec.slug ?? id;
|
|
496
|
+
log.tip(`Install everything: agentspec install ${specRef}`);
|
|
497
|
+
}
|
|
498
|
+
async function showPlugin(id) {
|
|
499
|
+
const spinner = createSpinner();
|
|
500
|
+
spinner.start("Fetching plugin...");
|
|
501
|
+
const plugin = await getPlugin(id);
|
|
502
|
+
spinner.stop(pc3.bold(plugin.name));
|
|
503
|
+
divider();
|
|
504
|
+
field("Type", "plugin (MCP server)");
|
|
505
|
+
field("Slug", plugin.slug);
|
|
506
|
+
field("Frameworks", plugin.frameworks.join(", "));
|
|
507
|
+
field("Tags", plugin.tags.join(", "));
|
|
508
|
+
field("Author", plugin.author.name);
|
|
509
|
+
field("Votes", formatScore(plugin.upvoteCount, plugin.downvoteCount));
|
|
510
|
+
field("Repo", plugin.repoUrl ?? "\u2014");
|
|
511
|
+
field("Homepage", plugin.homepageUrl ?? "\u2014");
|
|
512
|
+
field("Created", formatDate(plugin.createdAt));
|
|
513
|
+
if (plugin.description) {
|
|
514
|
+
console.log();
|
|
515
|
+
console.log(` ${plugin.description}`);
|
|
516
|
+
}
|
|
517
|
+
if (plugin.installation) {
|
|
518
|
+
divider("Installation");
|
|
519
|
+
previewContent(plugin.installation);
|
|
520
|
+
}
|
|
521
|
+
console.log();
|
|
522
|
+
log.tip(`View full details: https://agentspec.sh/plugins/${plugin.slug}`);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// src/commands/install.ts
|
|
526
|
+
import { intro, outro } from "@clack/prompts";
|
|
527
|
+
import { Command as Command2 } from "commander";
|
|
528
|
+
import pc4 from "picocolors";
|
|
529
|
+
|
|
530
|
+
// src/lib/installer.ts
|
|
531
|
+
import { copyFile, mkdir as mkdir2, readlink, rm as rm2, stat as stat2, symlink, unlink, writeFile as writeFile2 } from "fs/promises";
|
|
532
|
+
import { dirname, join as join3, resolve as resolve2 } from "path";
|
|
533
|
+
|
|
534
|
+
// src/lib/store.ts
|
|
535
|
+
import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
|
|
536
|
+
import { join as join2 } from "path";
|
|
537
|
+
function storeDir(slug) {
|
|
538
|
+
return join2(AGENTSPEC_STORE, slug);
|
|
539
|
+
}
|
|
540
|
+
function storeContentPath(slug) {
|
|
541
|
+
return join2(storeDir(slug), "content");
|
|
542
|
+
}
|
|
543
|
+
function storeMetaPath(slug) {
|
|
544
|
+
return join2(storeDir(slug), "meta.json");
|
|
545
|
+
}
|
|
546
|
+
async function exists(p) {
|
|
547
|
+
return stat(p).then(
|
|
548
|
+
() => true,
|
|
549
|
+
() => false
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
async function hasStoreItem(slug) {
|
|
553
|
+
return exists(storeContentPath(slug));
|
|
554
|
+
}
|
|
555
|
+
async function storeItem(slug, content, meta) {
|
|
556
|
+
const dir = storeDir(slug);
|
|
557
|
+
await mkdir(dir, { recursive: true });
|
|
558
|
+
await Promise.all([
|
|
559
|
+
writeFile(storeContentPath(slug), content, "utf8"),
|
|
560
|
+
writeFile(storeMetaPath(slug), JSON.stringify(meta, null, 2), "utf8")
|
|
561
|
+
]);
|
|
562
|
+
}
|
|
563
|
+
async function readStoreMeta(slug) {
|
|
564
|
+
const raw = await readFile(storeMetaPath(slug), "utf8");
|
|
565
|
+
return JSON.parse(raw);
|
|
566
|
+
}
|
|
567
|
+
async function listStoreItems() {
|
|
568
|
+
const { readdir } = await import("fs/promises");
|
|
569
|
+
if (!await exists(AGENTSPEC_STORE)) return [];
|
|
570
|
+
const entries = await readdir(AGENTSPEC_STORE, { withFileTypes: true });
|
|
571
|
+
const metas = [];
|
|
572
|
+
for (const entry of entries) {
|
|
573
|
+
if (!entry.isDirectory()) continue;
|
|
574
|
+
const metaPath = join2(AGENTSPEC_STORE, entry.name, "meta.json");
|
|
575
|
+
if (await exists(metaPath)) {
|
|
576
|
+
try {
|
|
577
|
+
const raw = await readFile(metaPath, "utf8");
|
|
578
|
+
metas.push(JSON.parse(raw));
|
|
579
|
+
} catch {
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return metas;
|
|
584
|
+
}
|
|
585
|
+
function storeContentAbsPath(slug) {
|
|
586
|
+
return storeContentPath(slug);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/lib/installer.ts
|
|
590
|
+
async function fileExists(p) {
|
|
591
|
+
return stat2(p).then(
|
|
592
|
+
() => true,
|
|
593
|
+
() => false
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
async function isSymlink(p) {
|
|
597
|
+
return readlink(p).then(
|
|
598
|
+
() => true,
|
|
599
|
+
() => false
|
|
600
|
+
);
|
|
601
|
+
}
|
|
602
|
+
function globalBackupPath(targetAbsPath) {
|
|
603
|
+
const flat = targetAbsPath.replace(/^\//, "").replace(/\//g, "__");
|
|
604
|
+
return join3(AGENTSPEC_BACKUPS, flat);
|
|
605
|
+
}
|
|
606
|
+
async function ensureParentDir(absPath) {
|
|
607
|
+
await mkdir2(dirname(absPath), { recursive: true });
|
|
608
|
+
}
|
|
609
|
+
async function installToProject(dest, content, opts = {}) {
|
|
610
|
+
const absPath = resolve2(process.cwd(), dest);
|
|
611
|
+
let backupPath = null;
|
|
612
|
+
const exists2 = await fileExists(absPath);
|
|
613
|
+
const alreadyLink = exists2 && await isSymlink(absPath);
|
|
614
|
+
if (!opts.dryRun) {
|
|
615
|
+
await ensureParentDir(absPath);
|
|
616
|
+
if (exists2 && !alreadyLink) {
|
|
617
|
+
backupPath = `${absPath}.agentspec-orig`;
|
|
618
|
+
await copyFile(absPath, backupPath);
|
|
619
|
+
}
|
|
620
|
+
if (alreadyLink) {
|
|
621
|
+
await unlink(absPath);
|
|
622
|
+
}
|
|
623
|
+
await writeFile2(absPath, content, "utf8");
|
|
624
|
+
}
|
|
625
|
+
return { targetPath: absPath, backupPath, scope: "project", mode: "copy" };
|
|
626
|
+
}
|
|
627
|
+
async function installToProjectDir(destDir, title, content, opts = {}) {
|
|
628
|
+
const safeName = title.replace(/[^a-z0-9_-]/gi, "-").replace(/-+/g, "-").toLowerCase();
|
|
629
|
+
const dest = `${destDir.replace(/\/$/, "")}/${safeName}.md`;
|
|
630
|
+
return installToProject(dest, content, opts);
|
|
631
|
+
}
|
|
632
|
+
async function installToGlobal(slug, globalDest, opts = {}) {
|
|
633
|
+
const storePath = storeContentAbsPath(slug);
|
|
634
|
+
let backupPath = null;
|
|
635
|
+
const exists2 = await fileExists(globalDest);
|
|
636
|
+
const alreadyLink = exists2 && await isSymlink(globalDest);
|
|
637
|
+
if (!opts.dryRun) {
|
|
638
|
+
await ensureParentDir(globalDest);
|
|
639
|
+
if (exists2 && !alreadyLink) {
|
|
640
|
+
backupPath = globalBackupPath(globalDest);
|
|
641
|
+
await mkdir2(dirname(backupPath), { recursive: true });
|
|
642
|
+
await copyFile(globalDest, backupPath);
|
|
643
|
+
await unlink(globalDest);
|
|
644
|
+
} else if (alreadyLink) {
|
|
645
|
+
await unlink(globalDest);
|
|
646
|
+
}
|
|
647
|
+
await symlink(storePath, globalDest);
|
|
648
|
+
} else {
|
|
649
|
+
if (exists2 && !alreadyLink) {
|
|
650
|
+
backupPath = globalBackupPath(globalDest);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { targetPath: globalDest, backupPath, scope: "global", mode: "symlink" };
|
|
654
|
+
}
|
|
655
|
+
async function uninstallFromProject(targetPath, action) {
|
|
656
|
+
if (action === "delete" && await fileExists(targetPath)) {
|
|
657
|
+
await rm2(targetPath, { force: true });
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
async function uninstallFromGlobal(targetPath, backupPath) {
|
|
661
|
+
const isLink = await isSymlink(targetPath);
|
|
662
|
+
if (isLink) {
|
|
663
|
+
await unlink(targetPath);
|
|
664
|
+
}
|
|
665
|
+
if (backupPath && await fileExists(backupPath)) {
|
|
666
|
+
await ensureParentDir(targetPath);
|
|
667
|
+
await copyFile(backupPath, targetPath);
|
|
668
|
+
await rm2(backupPath, { force: true });
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// src/lib/manifest.ts
|
|
673
|
+
import { createHash } from "crypto";
|
|
674
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile3 } from "fs/promises";
|
|
675
|
+
import { dirname as dirname2, join as join4 } from "path";
|
|
676
|
+
function projectHash(dir) {
|
|
677
|
+
return createHash("sha256").update(dir).digest("hex").slice(0, 12);
|
|
678
|
+
}
|
|
679
|
+
function projectStatePath(dir) {
|
|
680
|
+
return join4(AGENTSPEC_PROJECTS, projectHash(dir), "state.json");
|
|
681
|
+
}
|
|
682
|
+
async function readJson(path, defaultValue) {
|
|
683
|
+
try {
|
|
684
|
+
const raw = await readFile2(path, "utf8");
|
|
685
|
+
return JSON.parse(raw);
|
|
686
|
+
} catch {
|
|
687
|
+
return defaultValue;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async function writeJson(path, data) {
|
|
691
|
+
await mkdir3(dirname2(path), { recursive: true });
|
|
692
|
+
await writeFile3(path, JSON.stringify(data, null, 2), "utf8");
|
|
693
|
+
}
|
|
694
|
+
var defaultGlobalState = () => ({ version: 1, activeSpec: null, links: {} });
|
|
695
|
+
var defaultProjectState = (dir) => ({
|
|
696
|
+
version: 1,
|
|
697
|
+
projectDir: dir,
|
|
698
|
+
activeSpec: null,
|
|
699
|
+
links: {}
|
|
700
|
+
});
|
|
701
|
+
async function readGlobalState() {
|
|
702
|
+
return readJson(AGENTSPEC_GLOBAL_STATE, defaultGlobalState());
|
|
703
|
+
}
|
|
704
|
+
async function writeGlobalState(state) {
|
|
705
|
+
await writeJson(AGENTSPEC_GLOBAL_STATE, state);
|
|
706
|
+
}
|
|
707
|
+
async function readProjectState(projectDir = process.cwd()) {
|
|
708
|
+
return readJson(projectStatePath(projectDir), defaultProjectState(projectDir));
|
|
709
|
+
}
|
|
710
|
+
async function writeProjectState(state, projectDir = process.cwd()) {
|
|
711
|
+
await writeJson(projectStatePath(projectDir), state);
|
|
712
|
+
}
|
|
713
|
+
async function registerLink(scope, slug, record, projectDir = process.cwd()) {
|
|
714
|
+
if (scope === "global") {
|
|
715
|
+
const state = await readGlobalState();
|
|
716
|
+
state.links[slug] = record;
|
|
717
|
+
await writeGlobalState(state);
|
|
718
|
+
} else {
|
|
719
|
+
const state = await readProjectState(projectDir);
|
|
720
|
+
state.links[slug] = record;
|
|
721
|
+
await writeProjectState(state, projectDir);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
async function unregisterLink(scope, slug, projectDir = process.cwd()) {
|
|
725
|
+
if (scope === "global") {
|
|
726
|
+
const state = await readGlobalState();
|
|
727
|
+
const record = state.links[slug] ?? null;
|
|
728
|
+
delete state.links[slug];
|
|
729
|
+
await writeGlobalState(state);
|
|
730
|
+
return record;
|
|
731
|
+
} else {
|
|
732
|
+
const state = await readProjectState(projectDir);
|
|
733
|
+
const record = state.links[slug] ?? null;
|
|
734
|
+
delete state.links[slug];
|
|
735
|
+
await writeProjectState(state, projectDir);
|
|
736
|
+
return record;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
async function findLink(slug, projectDir = process.cwd()) {
|
|
740
|
+
const projectState = await readProjectState(projectDir);
|
|
741
|
+
if (projectState.links[slug]) return projectState.links[slug];
|
|
742
|
+
const globalState = await readGlobalState();
|
|
743
|
+
return globalState.links[slug] ?? null;
|
|
744
|
+
}
|
|
745
|
+
async function getActiveSpec(scope, projectDir = process.cwd()) {
|
|
746
|
+
if (scope === "global") {
|
|
747
|
+
return (await readGlobalState()).activeSpec;
|
|
748
|
+
}
|
|
749
|
+
return (await readProjectState(projectDir)).activeSpec;
|
|
750
|
+
}
|
|
751
|
+
async function setActiveSpec(scope, specSlug, projectDir = process.cwd()) {
|
|
752
|
+
if (scope === "global") {
|
|
753
|
+
const state = await readGlobalState();
|
|
754
|
+
state.activeSpec = specSlug;
|
|
755
|
+
await writeGlobalState(state);
|
|
756
|
+
} else {
|
|
757
|
+
const state = await readProjectState(projectDir);
|
|
758
|
+
state.activeSpec = specSlug;
|
|
759
|
+
await writeProjectState(state, projectDir);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
// src/commands/install.ts
|
|
764
|
+
function die(msg) {
|
|
765
|
+
log.error(msg);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
throw new Error(msg);
|
|
768
|
+
}
|
|
769
|
+
var installCommand = new Command2("install").description("Install a config, skill, rule, or spec by ID or slug").argument("<id>", "Content UUID or slug").option("--type <type>", "Skip auto-detect: config|skill|rule|spec").option("--framework <name>", "Target framework for skills").option("--file-type <type>", "Target file type for rules").option("--global", "Install globally as symlink (default: prompt)", false).option("--project", "Install to this project as a file copy (default: prompt)", false).option("--dry-run", "Preview without writing files", false).action(
|
|
770
|
+
async (id, opts) => {
|
|
771
|
+
const { dryRun } = opts;
|
|
772
|
+
intro(pc4.bold("agentspec install"));
|
|
773
|
+
if (dryRun) log.warn("Dry run \u2014 no files will be written");
|
|
774
|
+
let contentType = opts.type;
|
|
775
|
+
let contentId = id;
|
|
776
|
+
let title = id;
|
|
777
|
+
if (!contentType) {
|
|
778
|
+
const spinner = createSpinner();
|
|
779
|
+
spinner.start(`Resolving ${pc4.cyan(id)}...`);
|
|
780
|
+
try {
|
|
781
|
+
const resolved = await resolve(id);
|
|
782
|
+
spinner.stop(`Found: ${pc4.bold(resolved.title)} ${pc4.dim(`(${resolved.type})`)}`);
|
|
783
|
+
contentType = resolved.type;
|
|
784
|
+
contentId = resolved.id;
|
|
785
|
+
title = resolved.title;
|
|
786
|
+
} catch (err) {
|
|
787
|
+
spinner.fail(
|
|
788
|
+
`Could not resolve ${id}: ${err instanceof Error ? err.message : String(err)}`
|
|
789
|
+
);
|
|
790
|
+
process.exit(1);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
try {
|
|
794
|
+
switch (contentType) {
|
|
795
|
+
case "config":
|
|
796
|
+
await installConfig(contentId, title, opts);
|
|
797
|
+
break;
|
|
798
|
+
case "skill":
|
|
799
|
+
await installSkill(contentId, title, opts);
|
|
800
|
+
break;
|
|
801
|
+
case "rule":
|
|
802
|
+
await installRule(contentId, title, opts);
|
|
803
|
+
break;
|
|
804
|
+
case "spec":
|
|
805
|
+
await installSpec(contentId, title, opts);
|
|
806
|
+
break;
|
|
807
|
+
case "plugin":
|
|
808
|
+
log.error("Plugins require manual MCP server setup.");
|
|
809
|
+
log.tip(`See: https://agentspec.sh/plugins/${id}`);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
break;
|
|
812
|
+
default:
|
|
813
|
+
die(`Unknown content type: ${contentType}`);
|
|
814
|
+
}
|
|
815
|
+
if (contentType !== "spec") {
|
|
816
|
+
trackInstall(contentType, contentId);
|
|
817
|
+
}
|
|
818
|
+
} catch (err) {
|
|
819
|
+
die(err instanceof Error ? err.message : String(err));
|
|
820
|
+
}
|
|
821
|
+
outro(pc4.green(`Done! Installed ${pc4.bold(title)}`));
|
|
822
|
+
}
|
|
823
|
+
);
|
|
824
|
+
async function installConfig(id, title, opts) {
|
|
825
|
+
const spinner = createSpinner();
|
|
826
|
+
spinner.start("Fetching config...");
|
|
827
|
+
let config;
|
|
828
|
+
try {
|
|
829
|
+
config = await getConfig(id);
|
|
830
|
+
} catch (err) {
|
|
831
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
832
|
+
process.exit(1);
|
|
833
|
+
}
|
|
834
|
+
spinner.stop(`${pc4.bold(config.title)} ${pc4.dim(`(${config.framework}, ${config.format})`)}`);
|
|
835
|
+
const projectDest = FRAMEWORK_FILE_PATHS[config.framework];
|
|
836
|
+
const globalDest = GLOBAL_FRAMEWORK_FILE_PATHS[config.framework];
|
|
837
|
+
if (!projectDest) die(`No install path for framework: ${config.framework}`);
|
|
838
|
+
const scope = await resolveScope(opts, {
|
|
839
|
+
hasGlobal: !!globalDest && GLOBAL_SUPPORTED_FRAMEWORKS.has(config.framework),
|
|
840
|
+
projectLabel: projectDest,
|
|
841
|
+
globalLabel: globalDest ? tilde(globalDest) : ""
|
|
842
|
+
});
|
|
843
|
+
const targetLabel = scope === "global" ? tilde(globalDest) : projectDest;
|
|
844
|
+
const ok = await promptConfirm(`Install "${config.title}" to ${pc4.cyan(targetLabel)}?`);
|
|
845
|
+
if (!ok) {
|
|
846
|
+
log.info("Aborted.");
|
|
847
|
+
process.exit(0);
|
|
848
|
+
}
|
|
849
|
+
const configSlug = config.slug ?? id;
|
|
850
|
+
const meta = {
|
|
851
|
+
id: config.id,
|
|
852
|
+
slug: configSlug,
|
|
853
|
+
type: "config",
|
|
854
|
+
title: config.title,
|
|
855
|
+
framework: config.framework,
|
|
856
|
+
contentHash: config.contentHash,
|
|
857
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
858
|
+
};
|
|
859
|
+
if (!opts.dryRun) await storeItem(configSlug, config.configuration, meta);
|
|
860
|
+
if (scope === "global") {
|
|
861
|
+
const result = await installToGlobal(configSlug, globalDest, { dryRun: opts.dryRun });
|
|
862
|
+
logResult(result, scope);
|
|
863
|
+
if (!opts.dryRun)
|
|
864
|
+
await registerLink(
|
|
865
|
+
"global",
|
|
866
|
+
configSlug,
|
|
867
|
+
makeLink(config.id, configSlug, "config", config.title, scope, result, config.contentHash)
|
|
868
|
+
);
|
|
869
|
+
} else {
|
|
870
|
+
const result = await installToProject(projectDest, config.configuration, {
|
|
871
|
+
dryRun: opts.dryRun
|
|
872
|
+
});
|
|
873
|
+
logResult(result, scope);
|
|
874
|
+
if (!opts.dryRun)
|
|
875
|
+
await registerLink(
|
|
876
|
+
"project",
|
|
877
|
+
configSlug,
|
|
878
|
+
makeLink(config.id, configSlug, "config", config.title, scope, result, config.contentHash)
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
async function installSkill(id, title, opts) {
|
|
883
|
+
const spinner = createSpinner();
|
|
884
|
+
spinner.start("Fetching skill...");
|
|
885
|
+
let skill;
|
|
886
|
+
try {
|
|
887
|
+
skill = await getSkill(id);
|
|
888
|
+
} catch (err) {
|
|
889
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
spinner.stop(`${pc4.bold(skill.name)} ${pc4.dim(`(${skill.frameworks.join(", ")})`)}`);
|
|
893
|
+
let fw = opts.framework;
|
|
894
|
+
if (!fw) {
|
|
895
|
+
if (skill.frameworks.length === 1) {
|
|
896
|
+
fw = skill.frameworks[0];
|
|
897
|
+
} else {
|
|
898
|
+
fw = await promptFramework(skill.frameworks, SKILL_FRAMEWORK_LABELS);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (!fw) die("Could not determine target framework");
|
|
902
|
+
const projectDir = SKILL_FRAMEWORK_DIRS[fw];
|
|
903
|
+
const globalDir = GLOBAL_SKILL_FRAMEWORK_DIRS[fw];
|
|
904
|
+
if (!projectDir) die(`Unknown framework: ${fw}`);
|
|
905
|
+
const projectDest = `${projectDir}/${skill.name}/SKILL.md`;
|
|
906
|
+
const globalDest = globalDir ? `${globalDir}/${skill.name}/SKILL.md` : void 0;
|
|
907
|
+
const scope = await resolveScope(opts, {
|
|
908
|
+
hasGlobal: !!globalDir && GLOBAL_SUPPORTED_FRAMEWORKS.has(fw),
|
|
909
|
+
projectLabel: projectDest,
|
|
910
|
+
globalLabel: globalDest ? tilde(globalDest) : ""
|
|
911
|
+
});
|
|
912
|
+
const targetLabel = scope === "global" ? tilde(globalDest) : projectDest;
|
|
913
|
+
const ok = await promptConfirm(`Install "${skill.name}" to ${pc4.cyan(targetLabel)}?`);
|
|
914
|
+
if (!ok) {
|
|
915
|
+
log.info("Aborted.");
|
|
916
|
+
process.exit(0);
|
|
917
|
+
}
|
|
918
|
+
const skillSlug = skill.slug ?? id;
|
|
919
|
+
const skillMeta = {
|
|
920
|
+
id: skill.id,
|
|
921
|
+
slug: skillSlug,
|
|
922
|
+
type: "skill",
|
|
923
|
+
title: skill.name,
|
|
924
|
+
framework: fw,
|
|
925
|
+
contentHash: skill.contentHash,
|
|
926
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
927
|
+
};
|
|
928
|
+
if (!opts.dryRun) await storeItem(skillSlug, skill.skillContent, skillMeta);
|
|
929
|
+
if (scope === "global") {
|
|
930
|
+
const result = await installToGlobal(skillSlug, globalDest, { dryRun: opts.dryRun });
|
|
931
|
+
logResult(result, scope);
|
|
932
|
+
if (!opts.dryRun)
|
|
933
|
+
await registerLink(
|
|
934
|
+
"global",
|
|
935
|
+
skillSlug,
|
|
936
|
+
makeLink(skill.id, skillSlug, "skill", skill.name, scope, result, skill.contentHash)
|
|
937
|
+
);
|
|
938
|
+
} else {
|
|
939
|
+
const result = await installToProject(projectDest, skill.skillContent, { dryRun: opts.dryRun });
|
|
940
|
+
logResult(result, scope);
|
|
941
|
+
if (!opts.dryRun)
|
|
942
|
+
await registerLink(
|
|
943
|
+
"project",
|
|
944
|
+
skillSlug,
|
|
945
|
+
makeLink(skill.id, skillSlug, "skill", skill.name, scope, result, skill.contentHash)
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async function installRule(id, title, opts) {
|
|
950
|
+
const spinner = createSpinner();
|
|
951
|
+
spinner.start("Fetching rule...");
|
|
952
|
+
let rule;
|
|
953
|
+
try {
|
|
954
|
+
rule = await getRule(id);
|
|
955
|
+
} catch (err) {
|
|
956
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
957
|
+
process.exit(1);
|
|
958
|
+
}
|
|
959
|
+
spinner.stop(`${pc4.bold(rule.title)} ${pc4.dim(`(${rule.fileTypes.join(", ")})`)}`);
|
|
960
|
+
let ft = opts.fileType;
|
|
961
|
+
if (!ft) {
|
|
962
|
+
if (rule.fileTypes.length === 1) {
|
|
963
|
+
ft = rule.fileTypes[0];
|
|
964
|
+
} else {
|
|
965
|
+
ft = await promptFileType(rule.fileTypes, RULE_FILE_LABELS);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
if (!ft) die("Could not determine target file type");
|
|
969
|
+
const destPath = RULE_FILE_PATHS[ft];
|
|
970
|
+
if (!destPath) die(`Unknown file type: ${ft}`);
|
|
971
|
+
const ok = await promptConfirm(`Install "${rule.title}" to ${pc4.cyan(destPath)}?`);
|
|
972
|
+
if (!ok) {
|
|
973
|
+
log.info("Aborted.");
|
|
974
|
+
process.exit(0);
|
|
975
|
+
}
|
|
976
|
+
const ruleSlug = rule.slug ?? id;
|
|
977
|
+
const ruleMeta = {
|
|
978
|
+
id: rule.id,
|
|
979
|
+
slug: ruleSlug,
|
|
980
|
+
type: "rule",
|
|
981
|
+
title: rule.title,
|
|
982
|
+
contentHash: rule.contentHash,
|
|
983
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
984
|
+
};
|
|
985
|
+
if (!opts.dryRun) await storeItem(ruleSlug, rule.content, ruleMeta);
|
|
986
|
+
let result;
|
|
987
|
+
if (destPath.endsWith("/")) {
|
|
988
|
+
result = await installToProjectDir(destPath, rule.title, rule.content, { dryRun: opts.dryRun });
|
|
989
|
+
} else {
|
|
990
|
+
result = await installToProject(destPath, rule.content, { dryRun: opts.dryRun });
|
|
991
|
+
}
|
|
992
|
+
logResult(result, "project");
|
|
993
|
+
if (!opts.dryRun)
|
|
994
|
+
await registerLink(
|
|
995
|
+
"project",
|
|
996
|
+
ruleSlug,
|
|
997
|
+
makeLink(rule.id, ruleSlug, "rule", rule.title, "project", result, rule.contentHash)
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
async function installSpec(id, title, opts) {
|
|
1001
|
+
const spinner = createSpinner();
|
|
1002
|
+
spinner.start("Fetching spec...");
|
|
1003
|
+
let spec;
|
|
1004
|
+
try {
|
|
1005
|
+
spec = await getSpec(id);
|
|
1006
|
+
} catch (err) {
|
|
1007
|
+
spinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1008
|
+
process.exit(1);
|
|
1009
|
+
}
|
|
1010
|
+
spinner.stop(
|
|
1011
|
+
`${pc4.bold(spec.title)} ${pc4.dim(`(${spec.framework}, ${spec.items.length} items)`)}`
|
|
1012
|
+
);
|
|
1013
|
+
const hasGlobal = GLOBAL_SUPPORTED_FRAMEWORKS.has(spec.framework);
|
|
1014
|
+
const scope = await resolveScope(opts, {
|
|
1015
|
+
hasGlobal,
|
|
1016
|
+
projectLabel: "this project",
|
|
1017
|
+
globalLabel: "~/.config/agentspec (global)"
|
|
1018
|
+
});
|
|
1019
|
+
const ok = await promptConfirm(
|
|
1020
|
+
`Install all ${spec.items.length} items from "${spec.title}" ${scope === "global" ? "globally" : "to this project"}?`
|
|
1021
|
+
);
|
|
1022
|
+
if (!ok) {
|
|
1023
|
+
log.info("Aborted.");
|
|
1024
|
+
process.exit(0);
|
|
1025
|
+
}
|
|
1026
|
+
const total = spec.items.length;
|
|
1027
|
+
let step = 0;
|
|
1028
|
+
for (const item of spec.items) {
|
|
1029
|
+
step++;
|
|
1030
|
+
const prefix = pc4.dim(`[${step}/${total}]`);
|
|
1031
|
+
if (item.targetType === "plugin") {
|
|
1032
|
+
log.warn(`${prefix} Skipping plugin \u2014 manual setup required`);
|
|
1033
|
+
continue;
|
|
1034
|
+
}
|
|
1035
|
+
console.log(`
|
|
1036
|
+
${prefix} Installing ${item.targetType}...`);
|
|
1037
|
+
if (item.targetType === "config") {
|
|
1038
|
+
const data = item.data;
|
|
1039
|
+
const itemSlug = data.slug ?? data.id;
|
|
1040
|
+
const projectDest = FRAMEWORK_FILE_PATHS[data.framework];
|
|
1041
|
+
const globalDest = scope === "global" ? GLOBAL_FRAMEWORK_FILE_PATHS[data.framework] : void 0;
|
|
1042
|
+
if (!projectDest) {
|
|
1043
|
+
log.warn(`Unknown framework ${data.framework} \u2014 skipping`);
|
|
1044
|
+
continue;
|
|
1045
|
+
}
|
|
1046
|
+
if (scope === "global" && globalDest) {
|
|
1047
|
+
const meta = {
|
|
1048
|
+
id: data.id,
|
|
1049
|
+
slug: itemSlug,
|
|
1050
|
+
type: "config",
|
|
1051
|
+
title: data.title,
|
|
1052
|
+
framework: data.framework,
|
|
1053
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1054
|
+
};
|
|
1055
|
+
if (!opts.dryRun) await storeItem(itemSlug, data.configuration, meta);
|
|
1056
|
+
const result = await installToGlobal(itemSlug, globalDest, { dryRun: opts.dryRun });
|
|
1057
|
+
logResult(result, scope);
|
|
1058
|
+
if (!opts.dryRun)
|
|
1059
|
+
await registerLink(
|
|
1060
|
+
"global",
|
|
1061
|
+
itemSlug,
|
|
1062
|
+
makeLink(data.id, itemSlug, "config", data.title, scope, result)
|
|
1063
|
+
);
|
|
1064
|
+
} else {
|
|
1065
|
+
const result = await installToProject(projectDest, data.configuration, {
|
|
1066
|
+
dryRun: opts.dryRun
|
|
1067
|
+
});
|
|
1068
|
+
logResult(result, "project");
|
|
1069
|
+
if (!opts.dryRun)
|
|
1070
|
+
await registerLink(
|
|
1071
|
+
"project",
|
|
1072
|
+
itemSlug,
|
|
1073
|
+
makeLink(data.id, itemSlug, "config", data.title, "project", result)
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
trackInstall("config", data.id);
|
|
1077
|
+
} else if (item.targetType === "skill") {
|
|
1078
|
+
const data = item.data;
|
|
1079
|
+
const itemSlug = data.slug ?? data.id;
|
|
1080
|
+
const fw = opts.framework ?? data.frameworks[0] ?? data.framework;
|
|
1081
|
+
const projectDest = `${SKILL_FRAMEWORK_DIRS[fw] ?? ".claude/skills"}/${data.name}/SKILL.md`;
|
|
1082
|
+
const globalDest = scope === "global" && GLOBAL_SKILL_FRAMEWORK_DIRS[fw] ? `${GLOBAL_SKILL_FRAMEWORK_DIRS[fw]}/${data.name}/SKILL.md` : void 0;
|
|
1083
|
+
if (scope === "global" && globalDest) {
|
|
1084
|
+
const meta = {
|
|
1085
|
+
id: data.id,
|
|
1086
|
+
slug: itemSlug,
|
|
1087
|
+
type: "skill",
|
|
1088
|
+
title: data.name,
|
|
1089
|
+
framework: fw,
|
|
1090
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1091
|
+
};
|
|
1092
|
+
if (!opts.dryRun) await storeItem(itemSlug, data.skillContent, meta);
|
|
1093
|
+
const result = await installToGlobal(itemSlug, globalDest, { dryRun: opts.dryRun });
|
|
1094
|
+
logResult(result, scope);
|
|
1095
|
+
if (!opts.dryRun)
|
|
1096
|
+
await registerLink(
|
|
1097
|
+
"global",
|
|
1098
|
+
itemSlug,
|
|
1099
|
+
makeLink(data.id, itemSlug, "skill", data.name, scope, result)
|
|
1100
|
+
);
|
|
1101
|
+
} else {
|
|
1102
|
+
const result = await installToProject(projectDest, data.skillContent, {
|
|
1103
|
+
dryRun: opts.dryRun
|
|
1104
|
+
});
|
|
1105
|
+
logResult(result, "project");
|
|
1106
|
+
if (!opts.dryRun)
|
|
1107
|
+
await registerLink(
|
|
1108
|
+
"project",
|
|
1109
|
+
itemSlug,
|
|
1110
|
+
makeLink(data.id, itemSlug, "skill", data.name, "project", result)
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
trackInstall("skill", data.id);
|
|
1114
|
+
} else if (item.targetType === "rule") {
|
|
1115
|
+
const data = item.data;
|
|
1116
|
+
const itemSlug = data.slug ?? data.id;
|
|
1117
|
+
const ft = opts.fileType ?? data.fileTypes[0] ?? data.fileType;
|
|
1118
|
+
const destPath = RULE_FILE_PATHS[ft];
|
|
1119
|
+
if (!destPath) {
|
|
1120
|
+
log.warn(`Unknown file type ${ft} \u2014 skipping`);
|
|
1121
|
+
continue;
|
|
1122
|
+
}
|
|
1123
|
+
let result;
|
|
1124
|
+
if (destPath.endsWith("/")) {
|
|
1125
|
+
result = await installToProjectDir(destPath, data.title, data.content, {
|
|
1126
|
+
dryRun: opts.dryRun
|
|
1127
|
+
});
|
|
1128
|
+
} else {
|
|
1129
|
+
result = await installToProject(destPath, data.content, { dryRun: opts.dryRun });
|
|
1130
|
+
}
|
|
1131
|
+
logResult(result, "project");
|
|
1132
|
+
if (!opts.dryRun)
|
|
1133
|
+
await registerLink(
|
|
1134
|
+
"project",
|
|
1135
|
+
itemSlug,
|
|
1136
|
+
makeLink(data.id, itemSlug, "rule", data.title, "project", result)
|
|
1137
|
+
);
|
|
1138
|
+
trackInstall("rule", data.id);
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
trackInstall("spec", id);
|
|
1142
|
+
}
|
|
1143
|
+
async function resolveScope(opts, config) {
|
|
1144
|
+
if (opts.global) return "global";
|
|
1145
|
+
if (opts.project) return "project";
|
|
1146
|
+
if (!config.hasGlobal) return "project";
|
|
1147
|
+
return promptScope({ projectLabel: config.projectLabel, globalLabel: config.globalLabel });
|
|
1148
|
+
}
|
|
1149
|
+
function tilde(p) {
|
|
1150
|
+
const home = process.env["HOME"] ?? "";
|
|
1151
|
+
return home ? p.replace(home, "~") : p;
|
|
1152
|
+
}
|
|
1153
|
+
function logResult(result, scope) {
|
|
1154
|
+
if (scope === "global") {
|
|
1155
|
+
log.step(`Symlinked ${pc4.cyan(tilde(result.targetPath))}`);
|
|
1156
|
+
} else {
|
|
1157
|
+
log.step(`Wrote ${pc4.cyan(result.targetPath)}`);
|
|
1158
|
+
}
|
|
1159
|
+
if (result.backupPath) {
|
|
1160
|
+
log.step(`Backed up original \u2192 ${pc4.dim(result.backupPath)}`);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function makeLink(id, slug, type, title, scope, result, contentHash) {
|
|
1164
|
+
return {
|
|
1165
|
+
id,
|
|
1166
|
+
slug,
|
|
1167
|
+
type,
|
|
1168
|
+
title,
|
|
1169
|
+
scope,
|
|
1170
|
+
targetPath: result.targetPath,
|
|
1171
|
+
backupPath: result.backupPath,
|
|
1172
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1173
|
+
contentHash
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// src/commands/list.ts
|
|
1178
|
+
import { Command as Command3 } from "commander";
|
|
1179
|
+
import pc5 from "picocolors";
|
|
1180
|
+
var LIST_TYPES = ["configs", "skills", "rules", "specs"];
|
|
1181
|
+
var listCommand = new Command3("list").description("Browse configs, skills, rules, or specs").argument("<type>", "Content type: configs|skills|rules|specs").option("--sort <sort>", "Sort order: hot|new|top|installs", "hot").option("--framework <name>", "Filter by framework (e.g. ClaudeCode, Codex)").option("--file-type <type>", "Filter rules by file type (e.g. claude-md)").option("--limit <n>", "Number of results to show", "20").option("--offset <n>", "Pagination offset", "0").action(
|
|
1182
|
+
async (type, opts) => {
|
|
1183
|
+
if (!LIST_TYPES.includes(type)) {
|
|
1184
|
+
log.error(`Invalid type "${type}". Choose from: ${LIST_TYPES.join(", ")}`);
|
|
1185
|
+
process.exit(1);
|
|
1186
|
+
}
|
|
1187
|
+
const limit = Math.min(parseInt(opts.limit, 10) || 20, 100);
|
|
1188
|
+
const offset = parseInt(opts.offset, 10) || 0;
|
|
1189
|
+
const spinner = createSpinner();
|
|
1190
|
+
spinner.start(`Loading ${type}...`);
|
|
1191
|
+
let result;
|
|
1192
|
+
try {
|
|
1193
|
+
result = await listItems(type, {
|
|
1194
|
+
sort: opts.sort,
|
|
1195
|
+
framework: opts.framework,
|
|
1196
|
+
fileType: opts.fileType,
|
|
1197
|
+
limit,
|
|
1198
|
+
offset
|
|
1199
|
+
});
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
spinner.fail(`Failed to load ${type}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1202
|
+
process.exit(1);
|
|
1203
|
+
}
|
|
1204
|
+
spinner.stop(
|
|
1205
|
+
`${pc5.bold(type)} \u2014 ${result.total} total, showing ${offset + 1}\u2013${Math.min(offset + limit, result.total)} (sorted by ${opts.sort})`
|
|
1206
|
+
);
|
|
1207
|
+
if (result.data.length === 0) {
|
|
1208
|
+
log.tip("No results found.");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
console.log();
|
|
1212
|
+
const typeLabel2 = type === "configs" ? "config" : type.slice(0, -1);
|
|
1213
|
+
printTable(
|
|
1214
|
+
["#", "ID", "TITLE", "FRAMEWORK", "SCORE", "INSTALLS", "CREATED"],
|
|
1215
|
+
result.data.map((item, i) => [
|
|
1216
|
+
pc5.dim(String(offset + i + 1)),
|
|
1217
|
+
pc5.dim(item.id),
|
|
1218
|
+
truncate(item.title, 36),
|
|
1219
|
+
pc5.dim(
|
|
1220
|
+
item.framework ?? item.fileType ?? item.frameworks?.join(", ") ?? item.fileTypes?.join(", ") ?? "\u2014"
|
|
1221
|
+
),
|
|
1222
|
+
pc5.dim(formatScore(item.upvoteCount, item.downvoteCount)),
|
|
1223
|
+
pc5.dim(String(item.installCount)),
|
|
1224
|
+
pc5.dim(formatDate(item.createdAt))
|
|
1225
|
+
])
|
|
1226
|
+
);
|
|
1227
|
+
console.log();
|
|
1228
|
+
if (offset + limit < result.total) {
|
|
1229
|
+
log.tip(
|
|
1230
|
+
`Next page: agentspec list ${type} --offset ${offset + limit} --limit ${limit}${opts.framework ? ` --framework ${opts.framework}` : ""}`
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
log.tip(`Install with: agentspec install <id>`);
|
|
1234
|
+
log.tip(`Details: agentspec info <id>`);
|
|
1235
|
+
if (typeLabel2 !== type) {
|
|
1236
|
+
console.log();
|
|
1237
|
+
console.log(
|
|
1238
|
+
` Type badges: ${typeBadge("config")} ${typeBadge("skill")} ${typeBadge("rule")} ${typeBadge("spec")}`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
);
|
|
1243
|
+
|
|
1244
|
+
// src/commands/search.ts
|
|
1245
|
+
import { Command as Command4 } from "commander";
|
|
1246
|
+
import pc6 from "picocolors";
|
|
1247
|
+
var searchCommand = new Command4("search").description("Semantic search across configs, skills, rules, specs, and plugins").argument("<query>", "Search query").option("--type <type>", "Filter by type: all|config|skill|rule|spec|plugin", "all").option("--limit <n>", "Max results to return", "10").action(async (query, opts) => {
|
|
1248
|
+
const limit = Math.min(parseInt(opts.limit, 10) || 10, 50);
|
|
1249
|
+
const spinner = createSpinner();
|
|
1250
|
+
spinner.start(`Searching for "${pc6.cyan(query)}"`);
|
|
1251
|
+
let result;
|
|
1252
|
+
try {
|
|
1253
|
+
result = await search(query, { type: opts.type, limit });
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
spinner.fail(`Search failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1256
|
+
process.exit(1);
|
|
1257
|
+
}
|
|
1258
|
+
spinner.stop(`Found ${result.data.length} results for "${pc6.cyan(query)}"`);
|
|
1259
|
+
if (result.data.length === 0) {
|
|
1260
|
+
log.tip("No results. Try a broader query or different type filter.");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
console.log();
|
|
1264
|
+
printTable(
|
|
1265
|
+
["TYPE", "ID", "TITLE", "SCORE"],
|
|
1266
|
+
result.data.map((item) => [
|
|
1267
|
+
typeBadge(item.itemType),
|
|
1268
|
+
pc6.dim(item.id),
|
|
1269
|
+
truncate(item.title, 40),
|
|
1270
|
+
pc6.dim(formatScore(item.score, 0))
|
|
1271
|
+
])
|
|
1272
|
+
);
|
|
1273
|
+
console.log();
|
|
1274
|
+
log.tip(`Install with: agentspec install <id>`);
|
|
1275
|
+
log.tip(`Get details: agentspec info <id>`);
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// src/commands/status.ts
|
|
1279
|
+
import { Command as Command5 } from "commander";
|
|
1280
|
+
import pc7 from "picocolors";
|
|
1281
|
+
import { homedir as homedir2 } from "os";
|
|
1282
|
+
function tilde2(p) {
|
|
1283
|
+
return p.replace(homedir2(), "~");
|
|
1284
|
+
}
|
|
1285
|
+
var statusCommand = new Command5("status").description("Show installed items and active spec").option("--global", "Show global installation state", false).option("--store", "List all items in the local store (~/.agentspec/store/)", false).action(async (opts) => {
|
|
1286
|
+
if (opts.store) {
|
|
1287
|
+
const items = await listStoreItems();
|
|
1288
|
+
if (items.length === 0) {
|
|
1289
|
+
log.info("Store is empty. Install something with: agentspec install <slug>");
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1292
|
+
console.log(pc7.bold(`
|
|
1293
|
+
~/.agentspec/store/ \u2014 ${items.length} item(s)
|
|
1294
|
+
`));
|
|
1295
|
+
printTable(
|
|
1296
|
+
["SLUG", "TYPE", "TITLE", "FETCHED"],
|
|
1297
|
+
items.map((m) => [
|
|
1298
|
+
pc7.cyan(m.slug),
|
|
1299
|
+
pc7.dim(m.type),
|
|
1300
|
+
truncate(m.title, 40),
|
|
1301
|
+
pc7.dim(new Date(m.fetchedAt).toLocaleDateString())
|
|
1302
|
+
])
|
|
1303
|
+
);
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
if (opts.global) {
|
|
1307
|
+
const state2 = await readGlobalState();
|
|
1308
|
+
const links2 = Object.values(state2.links);
|
|
1309
|
+
console.log();
|
|
1310
|
+
console.log(pc7.bold(" Global state") + pc7.dim(` (~/.agentspec/global-state.json)`));
|
|
1311
|
+
if (state2.activeSpec) {
|
|
1312
|
+
console.log(` Active spec: ${pc7.cyan(state2.activeSpec)}`);
|
|
1313
|
+
}
|
|
1314
|
+
console.log();
|
|
1315
|
+
if (links2.length === 0) {
|
|
1316
|
+
log.info("Nothing installed globally. Use: agentspec install <slug> --global");
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
printTable(
|
|
1320
|
+
["SLUG", "TYPE", "TARGET", "MODE"],
|
|
1321
|
+
links2.map((r) => [
|
|
1322
|
+
pc7.cyan(r.slug),
|
|
1323
|
+
pc7.dim(r.type),
|
|
1324
|
+
tilde2(r.targetPath),
|
|
1325
|
+
r.scope === "global" ? pc7.magenta("symlink") : pc7.dim("copy")
|
|
1326
|
+
])
|
|
1327
|
+
);
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
const state = await readProjectState();
|
|
1331
|
+
const links = Object.values(state.links);
|
|
1332
|
+
console.log();
|
|
1333
|
+
console.log(pc7.bold(" Project: ") + pc7.dim(process.cwd()));
|
|
1334
|
+
if (state.activeSpec) {
|
|
1335
|
+
console.log(` Active spec: ${pc7.cyan(state.activeSpec)}`);
|
|
1336
|
+
}
|
|
1337
|
+
console.log();
|
|
1338
|
+
if (links.length === 0) {
|
|
1339
|
+
log.info("Nothing tracked in this project. Install with: agentspec install <slug>");
|
|
1340
|
+
log.tip("Or check global installs with: agentspec status --global");
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
printTable(
|
|
1344
|
+
["SLUG", "TYPE", "TARGET", "MODE", "INSTALLED"],
|
|
1345
|
+
links.map((r) => [
|
|
1346
|
+
pc7.cyan(r.slug),
|
|
1347
|
+
pc7.dim(r.type),
|
|
1348
|
+
truncate(r.targetPath, 40),
|
|
1349
|
+
r.scope === "global" ? pc7.magenta("symlink") : pc7.dim("copy"),
|
|
1350
|
+
pc7.dim(new Date(r.installedAt).toLocaleDateString())
|
|
1351
|
+
])
|
|
1352
|
+
);
|
|
1353
|
+
console.log();
|
|
1354
|
+
log.tip("Remove an item: agentspec uninstall <slug>");
|
|
1355
|
+
log.tip("Switch agent setup: agentspec use <spec-slug>");
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
// src/commands/uninstall.ts
|
|
1359
|
+
import { intro as intro2, outro as outro2 } from "@clack/prompts";
|
|
1360
|
+
import { Command as Command6 } from "commander";
|
|
1361
|
+
import pc8 from "picocolors";
|
|
1362
|
+
function die2(msg) {
|
|
1363
|
+
log.error(msg);
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
throw new Error(msg);
|
|
1366
|
+
}
|
|
1367
|
+
var uninstallCommand = new Command6("uninstall").description("Remove an installed config, skill, or rule").argument("<id>", "Slug or UUID of the installed item").option("--global", "Uninstall from global scope", false).option("--project", "Uninstall from this project", false).action(async (id, opts) => {
|
|
1368
|
+
intro2(pc8.bold("agentspec uninstall"));
|
|
1369
|
+
const record = await findLink(id);
|
|
1370
|
+
if (!record) {
|
|
1371
|
+
die2(
|
|
1372
|
+
`"${id}" is not tracked by agentspec in this project or globally.
|
|
1373
|
+
If you installed it manually, remove the file directly.`
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
const scopeLabel = record.scope === "global" ? pc8.cyan("global") + pc8.dim(" (symlink)") : pc8.cyan("project") + pc8.dim(" (file copy)");
|
|
1377
|
+
console.log();
|
|
1378
|
+
console.log(` ${pc8.bold(record.title)}`);
|
|
1379
|
+
console.log(` ${pc8.dim("\u2192")} ${record.targetPath}`);
|
|
1380
|
+
console.log(` ${pc8.dim("scope:")} ${scopeLabel}`);
|
|
1381
|
+
console.log();
|
|
1382
|
+
if (record.scope === "global") {
|
|
1383
|
+
const ok = await promptConfirm(
|
|
1384
|
+
`Remove symlink and restore original${record.backupPath ? " from backup" : ""}?`
|
|
1385
|
+
);
|
|
1386
|
+
if (!ok) {
|
|
1387
|
+
log.info("Aborted.");
|
|
1388
|
+
process.exit(0);
|
|
1389
|
+
}
|
|
1390
|
+
await uninstallFromGlobal(record.targetPath, record.backupPath);
|
|
1391
|
+
await unregisterLink("global", id);
|
|
1392
|
+
log.step(`Removed symlink ${pc8.dim(record.targetPath)}`);
|
|
1393
|
+
if (record.backupPath) {
|
|
1394
|
+
log.step(`Restored original from backup`);
|
|
1395
|
+
}
|
|
1396
|
+
} else {
|
|
1397
|
+
const action = await promptUninstallAction(record.targetPath);
|
|
1398
|
+
await uninstallFromProject(record.targetPath, action);
|
|
1399
|
+
await unregisterLink("project", id);
|
|
1400
|
+
if (action === "delete") {
|
|
1401
|
+
log.step(`Deleted ${pc8.dim(record.targetPath)}`);
|
|
1402
|
+
} else {
|
|
1403
|
+
log.step(`Untracked \u2014 file kept at ${pc8.dim(record.targetPath)}`);
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
outro2(pc8.green(`Uninstalled ${pc8.bold(record.title)}`));
|
|
1407
|
+
});
|
|
1408
|
+
|
|
1409
|
+
// src/commands/update.ts
|
|
1410
|
+
import { intro as intro3, outro as outro3 } from "@clack/prompts";
|
|
1411
|
+
import { createHash as createHash2 } from "crypto";
|
|
1412
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1413
|
+
import { Command as Command7 } from "commander";
|
|
1414
|
+
import pc9 from "picocolors";
|
|
1415
|
+
function sha256(content) {
|
|
1416
|
+
return createHash2("sha256").update(content, "utf8").digest("hex");
|
|
1417
|
+
}
|
|
1418
|
+
async function fetchLatest(id, type) {
|
|
1419
|
+
switch (type) {
|
|
1420
|
+
case "config": {
|
|
1421
|
+
const d = await getConfig(id);
|
|
1422
|
+
return { content: d.configuration, contentHash: d.contentHash, title: d.title };
|
|
1423
|
+
}
|
|
1424
|
+
case "skill": {
|
|
1425
|
+
const d = await getSkill(id);
|
|
1426
|
+
return { content: d.skillContent, contentHash: d.contentHash, title: d.name };
|
|
1427
|
+
}
|
|
1428
|
+
case "rule": {
|
|
1429
|
+
const d = await getRule(id);
|
|
1430
|
+
return { content: d.content, contentHash: d.contentHash, title: d.title };
|
|
1431
|
+
}
|
|
1432
|
+
default:
|
|
1433
|
+
throw new Error(`Cannot update type "${type}" \u2014 only config, skill, rule supported`);
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async function updateOne(slug, opts) {
|
|
1437
|
+
if (!await hasStoreItem(slug)) return "skipped";
|
|
1438
|
+
const meta = await readStoreMeta(slug);
|
|
1439
|
+
if (!["config", "skill", "rule"].includes(meta.type)) return "skipped";
|
|
1440
|
+
const latest = await fetchLatest(meta.id, meta.type);
|
|
1441
|
+
const remoteHash = latest.contentHash;
|
|
1442
|
+
if (meta.contentHash && meta.contentHash === remoteHash) {
|
|
1443
|
+
return "up-to-date";
|
|
1444
|
+
}
|
|
1445
|
+
const storePath = storeContentAbsPath(slug);
|
|
1446
|
+
let localContent = null;
|
|
1447
|
+
try {
|
|
1448
|
+
localContent = await readFile3(storePath, "utf8");
|
|
1449
|
+
} catch {
|
|
1450
|
+
}
|
|
1451
|
+
if (localContent !== null && meta.contentHash) {
|
|
1452
|
+
const localHash = sha256(localContent);
|
|
1453
|
+
if (localHash !== meta.contentHash && !opts.force) {
|
|
1454
|
+
return "local-modified";
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (!opts.dryRun) {
|
|
1458
|
+
await storeItem(slug, latest.content, {
|
|
1459
|
+
...meta,
|
|
1460
|
+
contentHash: remoteHash,
|
|
1461
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1462
|
+
});
|
|
1463
|
+
}
|
|
1464
|
+
return "updated";
|
|
1465
|
+
}
|
|
1466
|
+
var updateCommand = new Command7("update").description("Update installed items to their latest version from agentspec.sh").argument("[slug]", "Slug of the item to update (omit to update all store items)").option("--all", "Update all items in the store", false).option("--check", "Only check for updates without applying them", false).option("--force", "Overwrite even if local store copy was manually modified", false).option("--dry-run", "Preview without writing anything", false).action(
|
|
1467
|
+
async (slug, opts) => {
|
|
1468
|
+
intro3(pc9.bold("agentspec update"));
|
|
1469
|
+
const dryRun = opts.dryRun || opts.check;
|
|
1470
|
+
if (dryRun)
|
|
1471
|
+
log.warn(
|
|
1472
|
+
opts.check ? "Check mode \u2014 not applying changes" : "Dry run \u2014 no files will be written"
|
|
1473
|
+
);
|
|
1474
|
+
let slugs;
|
|
1475
|
+
if (slug) {
|
|
1476
|
+
slugs = [slug];
|
|
1477
|
+
} else if (opts.all || !slug) {
|
|
1478
|
+
const items = await listStoreItems();
|
|
1479
|
+
slugs = items.map((m) => m.slug);
|
|
1480
|
+
if (slugs.length === 0) {
|
|
1481
|
+
log.info("Store is empty \u2014 nothing to update.");
|
|
1482
|
+
log.tip("Install something first: agentspec install <slug>");
|
|
1483
|
+
process.exit(0);
|
|
1484
|
+
}
|
|
1485
|
+
} else {
|
|
1486
|
+
slugs = [slug];
|
|
1487
|
+
}
|
|
1488
|
+
const results = [];
|
|
1489
|
+
const spinner = createSpinner();
|
|
1490
|
+
spinner.start(`Checking ${slugs.length} item(s)...`);
|
|
1491
|
+
let checked = 0;
|
|
1492
|
+
for (const s of slugs) {
|
|
1493
|
+
try {
|
|
1494
|
+
const result = await updateOne(s, { dryRun, force: opts.force });
|
|
1495
|
+
let title;
|
|
1496
|
+
try {
|
|
1497
|
+
title = (await readStoreMeta(s)).title;
|
|
1498
|
+
} catch {
|
|
1499
|
+
}
|
|
1500
|
+
results.push({ slug: s, result, title });
|
|
1501
|
+
} catch (err) {
|
|
1502
|
+
results.push({ slug: s, result: "skipped", title: void 0 });
|
|
1503
|
+
log.warn(` ${s}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1504
|
+
}
|
|
1505
|
+
checked++;
|
|
1506
|
+
}
|
|
1507
|
+
spinner.stop(`Checked ${checked} item(s)`);
|
|
1508
|
+
const updated = results.filter((r) => r.result === "updated");
|
|
1509
|
+
const upToDate = results.filter((r) => r.result === "up-to-date");
|
|
1510
|
+
const localModified = results.filter((r) => r.result === "local-modified");
|
|
1511
|
+
const skipped = results.filter((r) => r.result === "skipped");
|
|
1512
|
+
console.log();
|
|
1513
|
+
if (results.length > 1) {
|
|
1514
|
+
printTable(
|
|
1515
|
+
["SLUG", "STATUS", "TITLE"],
|
|
1516
|
+
results.map(({ slug: s, result, title }) => [
|
|
1517
|
+
pc9.cyan(s),
|
|
1518
|
+
result === "updated" ? pc9.green("updated") : result === "up-to-date" ? pc9.dim("up-to-date") : result === "local-modified" ? pc9.yellow("local modified") : pc9.dim("skipped"),
|
|
1519
|
+
pc9.dim(title ?? "\u2014")
|
|
1520
|
+
])
|
|
1521
|
+
);
|
|
1522
|
+
console.log();
|
|
1523
|
+
}
|
|
1524
|
+
if (updated.length > 0) {
|
|
1525
|
+
log.success(`${updated.length} item(s) updated`);
|
|
1526
|
+
if (updated.length <= 5) {
|
|
1527
|
+
for (const r of updated) log.step(r.slug);
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
if (localModified.length > 0) {
|
|
1531
|
+
log.warn(`${localModified.length} item(s) have local modifications \u2014 skipped`);
|
|
1532
|
+
for (const r of localModified) {
|
|
1533
|
+
log.step(`${pc9.yellow(r.slug)} \u2014 run with ${pc9.dim("--force")} to overwrite`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (upToDate.length > 0 && results.length === 1) {
|
|
1537
|
+
log.info(`${pc9.cyan(upToDate[0].slug)} is already up-to-date`);
|
|
1538
|
+
}
|
|
1539
|
+
if (updated.length > 0) {
|
|
1540
|
+
console.log();
|
|
1541
|
+
log.tip("Global installs (symlinks) already reflect the update.");
|
|
1542
|
+
log.tip("Project copies need re-installation: agentspec install <slug> --project");
|
|
1543
|
+
}
|
|
1544
|
+
outro3(pc9.green("Done"));
|
|
1545
|
+
}
|
|
1546
|
+
);
|
|
1547
|
+
|
|
1548
|
+
// src/commands/use.ts
|
|
1549
|
+
import { intro as intro4, outro as outro4 } from "@clack/prompts";
|
|
1550
|
+
import { Command as Command8 } from "commander";
|
|
1551
|
+
import pc10 from "picocolors";
|
|
1552
|
+
var useCommand = new Command8("use").description("Activate a spec \u2014 links all its items in one shot").argument("<spec>", 'Spec slug or UUID (use "--none" to deactivate)').option("--global", "Activate globally", false).option("--project", "Activate for this project only", false).option("--none", "Deactivate the current spec", false).option("--dry-run", "Preview without writing files", false).action(
|
|
1553
|
+
async (specId, opts) => {
|
|
1554
|
+
intro4(pc10.bold("agentspec use"));
|
|
1555
|
+
if (opts.dryRun) log.warn("Dry run \u2014 no files will be written");
|
|
1556
|
+
if (opts.none) {
|
|
1557
|
+
const scope2 = opts.global ? "global" : "project";
|
|
1558
|
+
const current = await getActiveSpec(scope2);
|
|
1559
|
+
if (!current) {
|
|
1560
|
+
log.info("No active spec to deactivate.");
|
|
1561
|
+
process.exit(0);
|
|
1562
|
+
}
|
|
1563
|
+
await deactivateSpec(current, scope2, opts.dryRun);
|
|
1564
|
+
await setActiveSpec(scope2, null);
|
|
1565
|
+
outro4(pc10.green("Deactivated."));
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
const spinner = createSpinner();
|
|
1569
|
+
spinner.start(`Resolving ${pc10.cyan(specId)}...`);
|
|
1570
|
+
let resolved;
|
|
1571
|
+
try {
|
|
1572
|
+
resolved = await resolve(specId);
|
|
1573
|
+
} catch (err) {
|
|
1574
|
+
spinner.fail(`Could not resolve: ${err instanceof Error ? err.message : String(err)}`);
|
|
1575
|
+
process.exit(1);
|
|
1576
|
+
}
|
|
1577
|
+
if (resolved.type !== "spec") {
|
|
1578
|
+
spinner.fail(`"${specId}" is a ${resolved.type}, not a spec`);
|
|
1579
|
+
process.exit(1);
|
|
1580
|
+
}
|
|
1581
|
+
spinner.stop(`${pc10.bold(resolved.title)} ${pc10.dim(`(${resolved.type})`)}`);
|
|
1582
|
+
const hasGlobal = GLOBAL_SUPPORTED_FRAMEWORKS.has(resolved.framework ?? "");
|
|
1583
|
+
let scope;
|
|
1584
|
+
if (opts.global) {
|
|
1585
|
+
scope = "global";
|
|
1586
|
+
} else if (opts.project) {
|
|
1587
|
+
scope = "project";
|
|
1588
|
+
} else if (!hasGlobal) {
|
|
1589
|
+
scope = "project";
|
|
1590
|
+
} else {
|
|
1591
|
+
scope = await promptScope({
|
|
1592
|
+
projectLabel: "this project only",
|
|
1593
|
+
globalLabel: "global (affects all projects)"
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
const scopeLabel = scope === "global" ? "globally" : "for this project";
|
|
1597
|
+
const ok = await promptConfirm(`Activate "${resolved.title}" ${scopeLabel}?`);
|
|
1598
|
+
if (!ok) {
|
|
1599
|
+
log.info("Aborted.");
|
|
1600
|
+
process.exit(0);
|
|
1601
|
+
}
|
|
1602
|
+
const specSpinner = createSpinner();
|
|
1603
|
+
specSpinner.start("Loading spec items...");
|
|
1604
|
+
let spec;
|
|
1605
|
+
try {
|
|
1606
|
+
spec = await getSpec(resolved.id);
|
|
1607
|
+
} catch (err) {
|
|
1608
|
+
specSpinner.fail(`Failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1609
|
+
process.exit(1);
|
|
1610
|
+
}
|
|
1611
|
+
specSpinner.stop(`${spec.items.length} items to activate`);
|
|
1612
|
+
const currentSpec = await getActiveSpec(scope);
|
|
1613
|
+
if (currentSpec && currentSpec !== resolved.id) {
|
|
1614
|
+
log.info(`
|
|
1615
|
+
Deactivating previous spec: ${pc10.dim(currentSpec)}`);
|
|
1616
|
+
await deactivateSpec(currentSpec, scope, opts.dryRun);
|
|
1617
|
+
}
|
|
1618
|
+
console.log();
|
|
1619
|
+
const total = spec.items.length;
|
|
1620
|
+
let step = 0;
|
|
1621
|
+
for (const item of spec.items) {
|
|
1622
|
+
step++;
|
|
1623
|
+
const prefix = pc10.dim(`[${step}/${total}]`);
|
|
1624
|
+
if (item.targetType === "plugin") {
|
|
1625
|
+
log.warn(`${prefix} Skipping plugin \u2014 manual setup required`);
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
if (item.targetType === "config") {
|
|
1629
|
+
const data = item.data;
|
|
1630
|
+
const itemSlug = data.slug ?? data.id;
|
|
1631
|
+
const projectDest = FRAMEWORK_FILE_PATHS[data.framework];
|
|
1632
|
+
const globalDest = GLOBAL_FRAMEWORK_FILE_PATHS[data.framework];
|
|
1633
|
+
if (!projectDest) {
|
|
1634
|
+
log.warn(`${prefix} Unknown framework ${data.framework} \u2014 skipping`);
|
|
1635
|
+
continue;
|
|
1636
|
+
}
|
|
1637
|
+
if (!opts.dryRun) {
|
|
1638
|
+
const meta = {
|
|
1639
|
+
id: data.id,
|
|
1640
|
+
slug: itemSlug,
|
|
1641
|
+
type: "config",
|
|
1642
|
+
title: data.title,
|
|
1643
|
+
framework: data.framework,
|
|
1644
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1645
|
+
};
|
|
1646
|
+
if (!await hasStoreItem(itemSlug))
|
|
1647
|
+
await storeItem(itemSlug, data.configuration, meta);
|
|
1648
|
+
}
|
|
1649
|
+
if (scope === "global" && globalDest) {
|
|
1650
|
+
const result = await installToGlobal(itemSlug, globalDest, { dryRun: opts.dryRun });
|
|
1651
|
+
log.step(`${prefix} ${pc10.cyan(tilde3(globalDest))} ${pc10.dim("(symlink)")}`);
|
|
1652
|
+
if (!opts.dryRun)
|
|
1653
|
+
await registerLink(
|
|
1654
|
+
scope,
|
|
1655
|
+
itemSlug,
|
|
1656
|
+
makeLink2(data.id, itemSlug, "config", data.title, scope, result, resolved.id)
|
|
1657
|
+
);
|
|
1658
|
+
} else {
|
|
1659
|
+
const result = await installToProject(projectDest, data.configuration, {
|
|
1660
|
+
dryRun: opts.dryRun
|
|
1661
|
+
});
|
|
1662
|
+
log.step(`${prefix} ${pc10.cyan(projectDest)} ${pc10.dim("(copy)")}`);
|
|
1663
|
+
if (!opts.dryRun)
|
|
1664
|
+
await registerLink(
|
|
1665
|
+
scope,
|
|
1666
|
+
itemSlug,
|
|
1667
|
+
makeLink2(data.id, itemSlug, "config", data.title, scope, result, resolved.id)
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
trackInstall("config", data.id);
|
|
1671
|
+
} else if (item.targetType === "skill") {
|
|
1672
|
+
const data = item.data;
|
|
1673
|
+
const itemSlug = data.slug ?? data.id;
|
|
1674
|
+
const fw = data.frameworks[0] ?? data.framework;
|
|
1675
|
+
const projectDest = `${SKILL_FRAMEWORK_DIRS[fw] ?? ".claude/skills"}/${data.name}/SKILL.md`;
|
|
1676
|
+
const globalDest = GLOBAL_SKILL_FRAMEWORK_DIRS[fw] ? `${GLOBAL_SKILL_FRAMEWORK_DIRS[fw]}/${data.name}/SKILL.md` : void 0;
|
|
1677
|
+
if (!opts.dryRun) {
|
|
1678
|
+
const meta = {
|
|
1679
|
+
id: data.id,
|
|
1680
|
+
slug: itemSlug,
|
|
1681
|
+
type: "skill",
|
|
1682
|
+
title: data.name,
|
|
1683
|
+
framework: fw,
|
|
1684
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1685
|
+
};
|
|
1686
|
+
if (!await hasStoreItem(itemSlug)) await storeItem(itemSlug, data.skillContent, meta);
|
|
1687
|
+
}
|
|
1688
|
+
if (scope === "global" && globalDest) {
|
|
1689
|
+
const result = await installToGlobal(itemSlug, globalDest, { dryRun: opts.dryRun });
|
|
1690
|
+
log.step(`${prefix} ${pc10.cyan(tilde3(globalDest))} ${pc10.dim("(symlink)")}`);
|
|
1691
|
+
if (!opts.dryRun)
|
|
1692
|
+
await registerLink(
|
|
1693
|
+
scope,
|
|
1694
|
+
itemSlug,
|
|
1695
|
+
makeLink2(data.id, itemSlug, "skill", data.name, scope, result, resolved.id)
|
|
1696
|
+
);
|
|
1697
|
+
} else {
|
|
1698
|
+
const result = await installToProject(projectDest, data.skillContent, {
|
|
1699
|
+
dryRun: opts.dryRun
|
|
1700
|
+
});
|
|
1701
|
+
log.step(`${prefix} ${pc10.cyan(projectDest)} ${pc10.dim("(copy)")}`);
|
|
1702
|
+
if (!opts.dryRun)
|
|
1703
|
+
await registerLink(
|
|
1704
|
+
scope,
|
|
1705
|
+
itemSlug,
|
|
1706
|
+
makeLink2(data.id, itemSlug, "skill", data.name, scope, result, resolved.id)
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
trackInstall("skill", data.id);
|
|
1710
|
+
} else if (item.targetType === "rule") {
|
|
1711
|
+
const data = item.data;
|
|
1712
|
+
const itemSlug = data.slug ?? data.id;
|
|
1713
|
+
const ft = data.fileTypes[0] ?? data.fileType;
|
|
1714
|
+
const destPath = RULE_FILE_PATHS[ft];
|
|
1715
|
+
if (!destPath) {
|
|
1716
|
+
log.warn(`${prefix} Unknown file type ${ft} \u2014 skipping`);
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
let result;
|
|
1720
|
+
if (destPath.endsWith("/")) {
|
|
1721
|
+
result = await installToProjectDir(destPath, data.title, data.content, {
|
|
1722
|
+
dryRun: opts.dryRun
|
|
1723
|
+
});
|
|
1724
|
+
} else {
|
|
1725
|
+
result = await installToProject(destPath, data.content, { dryRun: opts.dryRun });
|
|
1726
|
+
}
|
|
1727
|
+
log.step(`${prefix} ${pc10.cyan(destPath)} ${pc10.dim("(copy)")}`);
|
|
1728
|
+
if (!opts.dryRun)
|
|
1729
|
+
await registerLink(
|
|
1730
|
+
"project",
|
|
1731
|
+
itemSlug,
|
|
1732
|
+
makeLink2(data.id, itemSlug, "rule", data.title, "project", result, resolved.id)
|
|
1733
|
+
);
|
|
1734
|
+
trackInstall("rule", data.id);
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
if (!opts.dryRun) {
|
|
1738
|
+
await setActiveSpec(scope, resolved.id);
|
|
1739
|
+
}
|
|
1740
|
+
trackInstall("spec", resolved.id);
|
|
1741
|
+
outro4(pc10.green(`Activated ${pc10.bold(resolved.title)} ${pc10.dim(`(${scope})`)} `));
|
|
1742
|
+
}
|
|
1743
|
+
);
|
|
1744
|
+
async function deactivateSpec(specId, scope, dryRun) {
|
|
1745
|
+
const state = scope === "global" ? await readGlobalState() : await readProjectState();
|
|
1746
|
+
for (const [slug, record] of Object.entries(state.links)) {
|
|
1747
|
+
if (record.scope !== scope) continue;
|
|
1748
|
+
if (record.installedBySpec !== specId) continue;
|
|
1749
|
+
if (record.scope === "global") {
|
|
1750
|
+
if (!dryRun) await uninstallFromGlobal(record.targetPath, record.backupPath);
|
|
1751
|
+
log.step(`Unlinked ${pc10.dim(tilde3(record.targetPath))}`);
|
|
1752
|
+
} else {
|
|
1753
|
+
log.step(`Untracked ${pc10.dim(record.targetPath)}`);
|
|
1754
|
+
}
|
|
1755
|
+
if (!dryRun) await unregisterLink(scope, slug);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function tilde3(p) {
|
|
1759
|
+
const home = process.env["HOME"] ?? "";
|
|
1760
|
+
return home ? p.replace(home, "~") : p;
|
|
1761
|
+
}
|
|
1762
|
+
function makeLink2(id, slug, type, title, scope, result, specSlug) {
|
|
1763
|
+
return {
|
|
1764
|
+
id,
|
|
1765
|
+
slug,
|
|
1766
|
+
type,
|
|
1767
|
+
title,
|
|
1768
|
+
scope,
|
|
1769
|
+
targetPath: result.targetPath,
|
|
1770
|
+
backupPath: result.backupPath,
|
|
1771
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1772
|
+
installedBySpec: specSlug
|
|
1773
|
+
};
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
// src/index.ts
|
|
1777
|
+
var program = new Command9().name("agentspec").description("Install AI agent configs, skills, and rules from agentspec.sh").version("0.1.0");
|
|
1778
|
+
program.addCommand(installCommand);
|
|
1779
|
+
program.addCommand(uninstallCommand);
|
|
1780
|
+
program.addCommand(updateCommand);
|
|
1781
|
+
program.addCommand(useCommand);
|
|
1782
|
+
program.addCommand(statusCommand);
|
|
1783
|
+
program.addCommand(searchCommand);
|
|
1784
|
+
program.addCommand(listCommand);
|
|
1785
|
+
program.addCommand(infoCommand);
|
|
1786
|
+
program.parse(process.argv);
|