skissue 0.1.11
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 +132 -0
- package/dist/entry.js +1127 -0
- package/package.json +75 -0
- package/scripts/INDEX.md +10 -0
- package/scripts/ensure-local-bin.mjs +32 -0
- package/scripts/install.sh +33 -0
package/dist/entry.js
ADDED
|
@@ -0,0 +1,1127 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
// src/entry.ts
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
import { resolve as resolve2 } from "node:path";
|
|
8
|
+
|
|
9
|
+
// src/commands/init.ts
|
|
10
|
+
import * as p from "@clack/prompts";
|
|
11
|
+
import chalk from "chalk";
|
|
12
|
+
import { existsSync as existsSync3 } from "node:fs";
|
|
13
|
+
import { mkdir as mkdir3 } from "node:fs/promises";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
import { existsSync } from "node:fs";
|
|
17
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
18
|
+
import { dirname } from "node:path";
|
|
19
|
+
import YAML from "yaml";
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
|
|
22
|
+
// src/paths.ts
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
function skillIssueDir(cwd) {
|
|
25
|
+
return join(cwd, ".skill-issue");
|
|
26
|
+
}
|
|
27
|
+
function configPath(cwd) {
|
|
28
|
+
return join(skillIssueDir(cwd), "config.yaml");
|
|
29
|
+
}
|
|
30
|
+
function lockPath(cwd) {
|
|
31
|
+
return join(skillIssueDir(cwd), "lock.json");
|
|
32
|
+
}
|
|
33
|
+
function skillInstallPath(cwd, skillsRoot, skillId) {
|
|
34
|
+
const root = skillsRoot.startsWith("/") ? skillsRoot : join(cwd, skillsRoot);
|
|
35
|
+
return join(root, skillId);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/config.ts
|
|
39
|
+
var RegistryConfigSchema = z.object({
|
|
40
|
+
owner: z.string().optional(),
|
|
41
|
+
repo: z.string().optional(),
|
|
42
|
+
branch: z.string().min(1).default("main"),
|
|
43
|
+
/**
|
|
44
|
+
* When true: always use SSH. When false: always use HTTPS (with token if set).
|
|
45
|
+
* When omitted: use HTTPS if GITHUB_TOKEN/GH_TOKEN is set, else SSH (typical local dev).
|
|
46
|
+
*/
|
|
47
|
+
useSsh: z.boolean().optional(),
|
|
48
|
+
path: z.string().optional()
|
|
49
|
+
}).superRefine((data, ctx) => {
|
|
50
|
+
const local = data.path != null && data.path.trim().length > 0;
|
|
51
|
+
const remote = Boolean(data.owner?.trim() && data.repo?.trim());
|
|
52
|
+
if (local && remote) {
|
|
53
|
+
ctx.addIssue({
|
|
54
|
+
code: z.ZodIssueCode.custom,
|
|
55
|
+
message: "Use either registry.path (local) or registry.owner + registry.repo (remote), not both.",
|
|
56
|
+
path: ["path"]
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
if (!local && !remote) {
|
|
60
|
+
ctx.addIssue({
|
|
61
|
+
code: z.ZodIssueCode.custom,
|
|
62
|
+
message: "Set registry.path to a local directory, or registry.owner and registry.repo for a GitHub registry."
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
var ConfigSchema = z.object({
|
|
67
|
+
registry: RegistryConfigSchema,
|
|
68
|
+
skillsRoot: z.string().min(1).default(".agents/skills")
|
|
69
|
+
});
|
|
70
|
+
var DEFAULT_CONFIG_YAML = `# Remote (GitHub) \u2014 omit useSsh for auto (SSH if no token, else HTTPS+token):
|
|
71
|
+
# registry:
|
|
72
|
+
# owner: your-org
|
|
73
|
+
# repo: skill-registry
|
|
74
|
+
# branch: main
|
|
75
|
+
# useSsh: false # optional: force HTTPS; true forces SSH
|
|
76
|
+
#
|
|
77
|
+
# Local (path is relative to the consumer project root):
|
|
78
|
+
# registry:
|
|
79
|
+
# path: ../skill-registry
|
|
80
|
+
# branch: main
|
|
81
|
+
#
|
|
82
|
+
skillsRoot: .agents/skills
|
|
83
|
+
`;
|
|
84
|
+
function isLocalRegistry(config) {
|
|
85
|
+
return Boolean(config.registry.path?.trim());
|
|
86
|
+
}
|
|
87
|
+
async function needsSetup(cwd) {
|
|
88
|
+
const path = configPath(cwd);
|
|
89
|
+
if (!existsSync(path)) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
try {
|
|
93
|
+
const raw = await readFile(path, "utf8");
|
|
94
|
+
parseConfigYaml(raw);
|
|
95
|
+
return false;
|
|
96
|
+
} catch {
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async function loadConfig(cwd) {
|
|
101
|
+
const path = configPath(cwd);
|
|
102
|
+
const raw = await readFile(path, "utf8");
|
|
103
|
+
const data = YAML.parse(raw);
|
|
104
|
+
return ConfigSchema.parse(data);
|
|
105
|
+
}
|
|
106
|
+
async function writeConfig(cwd, config) {
|
|
107
|
+
const path = configPath(cwd);
|
|
108
|
+
await mkdir(dirname(path), { recursive: true });
|
|
109
|
+
const obj = ConfigSchema.parse(config);
|
|
110
|
+
await writeFile(path, YAML.stringify(obj, { lineWidth: 0 }), "utf8");
|
|
111
|
+
}
|
|
112
|
+
function parseConfigYaml(raw) {
|
|
113
|
+
return ConfigSchema.parse(YAML.parse(raw));
|
|
114
|
+
}
|
|
115
|
+
function defaultConfigTemplate() {
|
|
116
|
+
return DEFAULT_CONFIG_YAML;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/git/registry-repo.ts
|
|
120
|
+
import { createHash } from "node:crypto";
|
|
121
|
+
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
122
|
+
import { mkdir as mkdir2, access, rm } from "node:fs/promises";
|
|
123
|
+
import { homedir } from "node:os";
|
|
124
|
+
import { join as join2, resolve } from "node:path";
|
|
125
|
+
|
|
126
|
+
// src/git/exec.ts
|
|
127
|
+
import { spawn } from "node:child_process";
|
|
128
|
+
function execGit(args, options = {}) {
|
|
129
|
+
return new Promise((resolve3) => {
|
|
130
|
+
const env = { ...process.env, ...options.env };
|
|
131
|
+
if (env.GIT_TERMINAL_PROMPT === void 0) {
|
|
132
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
133
|
+
}
|
|
134
|
+
const child = spawn("git", args, {
|
|
135
|
+
cwd: options.cwd,
|
|
136
|
+
env,
|
|
137
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
138
|
+
});
|
|
139
|
+
let stdout = "";
|
|
140
|
+
let stderr = "";
|
|
141
|
+
const { timeoutMs } = options;
|
|
142
|
+
const timeout = timeoutMs != null && timeoutMs > 0 ? setTimeout(() => {
|
|
143
|
+
child.kill("SIGTERM");
|
|
144
|
+
}, timeoutMs) : void 0;
|
|
145
|
+
child.stdout?.on("data", (d) => {
|
|
146
|
+
stdout += d.toString();
|
|
147
|
+
});
|
|
148
|
+
child.stderr?.on("data", (d) => {
|
|
149
|
+
stderr += d.toString();
|
|
150
|
+
});
|
|
151
|
+
child.on("close", (code, signal) => {
|
|
152
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
153
|
+
if (signal === "SIGTERM") {
|
|
154
|
+
resolve3({
|
|
155
|
+
code: 124,
|
|
156
|
+
stdout,
|
|
157
|
+
stderr: `${stderr}
|
|
158
|
+
skissue: git timed out after ${timeoutMs}ms`.trim()
|
|
159
|
+
});
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
resolve3({ code: code ?? 1, stdout, stderr });
|
|
163
|
+
});
|
|
164
|
+
child.on("error", (err) => {
|
|
165
|
+
if (timeout !== void 0) clearTimeout(timeout);
|
|
166
|
+
resolve3({ code: 1, stdout, stderr: String(err) });
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/git/registry-repo.ts
|
|
172
|
+
function githubTokenFromEnv() {
|
|
173
|
+
const raw = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
174
|
+
const t = typeof raw === "string" ? raw.trim() : "";
|
|
175
|
+
return t.length > 0 ? t : void 0;
|
|
176
|
+
}
|
|
177
|
+
function resolveRegistryTransport(config) {
|
|
178
|
+
if (isLocalRegistry(config)) {
|
|
179
|
+
return "https";
|
|
180
|
+
}
|
|
181
|
+
const { useSsh } = config.registry;
|
|
182
|
+
if (useSsh === true) {
|
|
183
|
+
return "ssh";
|
|
184
|
+
}
|
|
185
|
+
if (useSsh === false) {
|
|
186
|
+
return "https";
|
|
187
|
+
}
|
|
188
|
+
return githubTokenFromEnv() ? "https" : "ssh";
|
|
189
|
+
}
|
|
190
|
+
function registryCacheDir(config) {
|
|
191
|
+
const owner = config.registry.owner ?? "";
|
|
192
|
+
const repo = config.registry.repo ?? "";
|
|
193
|
+
const transport = resolveRegistryTransport(config);
|
|
194
|
+
const key = `${owner}/${repo}/${transport}`;
|
|
195
|
+
const hash = createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
196
|
+
return join2(homedir(), ".cache", "skissue", "registries", hash);
|
|
197
|
+
}
|
|
198
|
+
function remoteFailureHint(transport, url, exitCode) {
|
|
199
|
+
if (exitCode === 124) {
|
|
200
|
+
return "\n\nTimed out. Check the network or set registry.path to a local clone.";
|
|
201
|
+
}
|
|
202
|
+
if (transport === "ssh") {
|
|
203
|
+
return [
|
|
204
|
+
"",
|
|
205
|
+
`
|
|
206
|
+
|
|
207
|
+
Used SSH: ${url}`,
|
|
208
|
+
'GitHub often reports "Repository not found" when the repo is private and your SSH key has no access, or when owner/repo is wrong.',
|
|
209
|
+
"Check registry.owner and registry.repo in .skill-issue/config.yaml, run `ssh -T git@github.com`, and confirm your GitHub user can access that repository.",
|
|
210
|
+
"Alternatives: clone the registry repo locally and set registry.path, or set GITHUB_TOKEN and registry.useSsh: false to use HTTPS."
|
|
211
|
+
].join("\n");
|
|
212
|
+
}
|
|
213
|
+
return [
|
|
214
|
+
"",
|
|
215
|
+
`
|
|
216
|
+
|
|
217
|
+
Used HTTPS: ${url}`,
|
|
218
|
+
"Private repos need GITHUB_TOKEN or GH_TOKEN (or omit registry.useSsh with no token to use SSH).",
|
|
219
|
+
"Or set registry.path to a local clone."
|
|
220
|
+
].join("\n");
|
|
221
|
+
}
|
|
222
|
+
function registryGitUrl(config) {
|
|
223
|
+
const owner = config.registry.owner;
|
|
224
|
+
const repo = config.registry.repo;
|
|
225
|
+
const transport = resolveRegistryTransport(config);
|
|
226
|
+
if (transport === "ssh") {
|
|
227
|
+
return `git@github.com:${owner}/${repo}.git`;
|
|
228
|
+
}
|
|
229
|
+
const token = githubTokenFromEnv();
|
|
230
|
+
if (token) {
|
|
231
|
+
return `https://x-access-token:${token}@github.com/${owner}/${repo}.git`;
|
|
232
|
+
}
|
|
233
|
+
return `https://github.com/${owner}/${repo}.git`;
|
|
234
|
+
}
|
|
235
|
+
async function ensureRegistryCheckout(cwd, config) {
|
|
236
|
+
if (isLocalRegistry(config)) {
|
|
237
|
+
const root = resolve(cwd, config.registry.path.trim());
|
|
238
|
+
if (!existsSync2(root) || !statSync(root).isDirectory()) {
|
|
239
|
+
throw new Error(`Local registry path does not exist or is not a directory: ${root}`);
|
|
240
|
+
}
|
|
241
|
+
const hasRegistry = existsSync2(join2(root, "registry.json")) || existsSync2(join2(root, "registry"));
|
|
242
|
+
if (!hasRegistry) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Local registry must contain registry.json or a registry/ directory: ${root}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
const before = await execGit(["rev-parse", "HEAD"], { cwd: root });
|
|
248
|
+
if (before.code !== 0) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Local registry must be a git repository root (with commits) so installs can be locked: ${root}`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
const pull = await execGit(["pull", "--ff-only"], { cwd: root, timeoutMs: 12e4 });
|
|
254
|
+
if (pull.code !== 0) {
|
|
255
|
+
process.stderr.write(
|
|
256
|
+
`skissue: could not fast-forward local registry (using current checkout): ${(pull.stderr || pull.stdout || "unknown").trim()}
|
|
257
|
+
`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
const head2 = await execGit(["rev-parse", "HEAD"], { cwd: root });
|
|
261
|
+
if (head2.code !== 0) {
|
|
262
|
+
throw new Error(`Local registry git rev-parse failed: ${root}`);
|
|
263
|
+
}
|
|
264
|
+
return { path: root, head: head2.stdout.trim() };
|
|
265
|
+
}
|
|
266
|
+
const dir = registryCacheDir(config);
|
|
267
|
+
const transport = resolveRegistryTransport(config);
|
|
268
|
+
const url = registryGitUrl(config);
|
|
269
|
+
const branch = config.registry.branch;
|
|
270
|
+
const parent = join2(homedir(), ".cache", "skissue", "registries");
|
|
271
|
+
await mkdir2(parent, { recursive: true });
|
|
272
|
+
let hasGit = false;
|
|
273
|
+
try {
|
|
274
|
+
await access(join2(dir, ".git"));
|
|
275
|
+
hasGit = true;
|
|
276
|
+
} catch {
|
|
277
|
+
hasGit = false;
|
|
278
|
+
}
|
|
279
|
+
if (!hasGit) {
|
|
280
|
+
await rm(dir, { recursive: true, force: true }).catch(() => void 0);
|
|
281
|
+
const clone = await execGit(["clone", "--depth", "1", "--branch", branch, url, dir], {
|
|
282
|
+
timeoutMs: 6e5
|
|
283
|
+
});
|
|
284
|
+
if (clone.code !== 0) {
|
|
285
|
+
const base = `git clone failed (${clone.code}): ${clone.stderr || clone.stdout}`.trim();
|
|
286
|
+
throw new Error(base + remoteFailureHint(transport, url, clone.code));
|
|
287
|
+
}
|
|
288
|
+
} else {
|
|
289
|
+
const fetch = await execGit(
|
|
290
|
+
["fetch", "origin", `refs/heads/${branch}:refs/remotes/origin/${branch}`, "--depth", "1"],
|
|
291
|
+
{ cwd: dir, timeoutMs: 3e5 }
|
|
292
|
+
);
|
|
293
|
+
if (fetch.code !== 0) {
|
|
294
|
+
const base = `git fetch failed (${fetch.code}): ${fetch.stderr || fetch.stdout}`.trim();
|
|
295
|
+
throw new Error(base + remoteFailureHint(transport, url, fetch.code));
|
|
296
|
+
}
|
|
297
|
+
const reset = await execGit(["reset", "--hard", `origin/${branch}`], {
|
|
298
|
+
cwd: dir,
|
|
299
|
+
timeoutMs: 6e4
|
|
300
|
+
});
|
|
301
|
+
if (reset.code !== 0) {
|
|
302
|
+
throw new Error(`git reset failed (${reset.code}): ${reset.stderr || reset.stdout}`.trim());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const head = await execGit(["rev-parse", "HEAD"], { cwd: dir });
|
|
306
|
+
if (head.code !== 0) {
|
|
307
|
+
throw new Error(`git rev-parse failed: ${head.stderr}`);
|
|
308
|
+
}
|
|
309
|
+
return { path: dir, head: head.stdout.trim() };
|
|
310
|
+
}
|
|
311
|
+
async function ensureCommit(repoPath, sha) {
|
|
312
|
+
const have = await execGit(["cat-file", "-e", `${sha}^{commit}`], { cwd: repoPath });
|
|
313
|
+
if (have.code === 0) return;
|
|
314
|
+
const fetch = await execGit(["fetch", "origin", sha, "--depth", "1"], {
|
|
315
|
+
cwd: repoPath,
|
|
316
|
+
timeoutMs: 3e5
|
|
317
|
+
});
|
|
318
|
+
if (fetch.code !== 0) {
|
|
319
|
+
const fetch2 = await execGit(["fetch", "origin", `${sha}:${sha}`, "--depth", "1"], {
|
|
320
|
+
cwd: repoPath,
|
|
321
|
+
timeoutMs: 3e5
|
|
322
|
+
});
|
|
323
|
+
if (fetch2.code !== 0) {
|
|
324
|
+
throw new Error(
|
|
325
|
+
`Could not fetch commit ${sha}: ${fetch.stderr || fetch2.stderr || fetch.stdout}`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
async function diffPath(repoPath, fromCommit, toCommit, pathInRepo) {
|
|
331
|
+
await ensureCommit(repoPath, fromCommit);
|
|
332
|
+
await ensureCommit(repoPath, toCommit);
|
|
333
|
+
const p3 = pathInRepo.replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
334
|
+
const diff = await execGit(["diff", fromCommit, toCommit, "--", p3], { cwd: repoPath });
|
|
335
|
+
if (diff.code !== 0) {
|
|
336
|
+
throw new Error(`git diff failed: ${diff.stderr}`);
|
|
337
|
+
}
|
|
338
|
+
return diff.stdout;
|
|
339
|
+
}
|
|
340
|
+
function isPathStale(diffStdout) {
|
|
341
|
+
return diffStdout.trim().length > 0;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// src/commands/init.ts
|
|
345
|
+
function summarizeConfig(cfg) {
|
|
346
|
+
const r = cfg.registry;
|
|
347
|
+
if (r.path?.trim()) {
|
|
348
|
+
return `Local registry: ${r.path}
|
|
349
|
+
Branch label: ${r.branch}
|
|
350
|
+
skillsRoot: ${cfg.skillsRoot}`;
|
|
351
|
+
}
|
|
352
|
+
const t = resolveRegistryTransport(cfg);
|
|
353
|
+
const mode = r.useSsh === void 0 ? `${t.toUpperCase()} (auto)` : `${t.toUpperCase()} (registry.useSsh: ${r.useSsh})`;
|
|
354
|
+
return `Remote registry: ${r.owner}/${r.repo}
|
|
355
|
+
Transport: ${mode}
|
|
356
|
+
Branch: ${r.branch}
|
|
357
|
+
skillsRoot: ${cfg.skillsRoot}`;
|
|
358
|
+
}
|
|
359
|
+
async function runInit(cwd) {
|
|
360
|
+
p.intro(chalk.bold("skissue init"));
|
|
361
|
+
const existingPath = configPath(cwd);
|
|
362
|
+
let hasExistingConfig = false;
|
|
363
|
+
if (existsSync3(existingPath)) {
|
|
364
|
+
hasExistingConfig = true;
|
|
365
|
+
try {
|
|
366
|
+
const existing = await loadConfig(cwd);
|
|
367
|
+
p.note(summarizeConfig(existing), "Current config");
|
|
368
|
+
} catch (err) {
|
|
369
|
+
p.note(err instanceof Error ? err.message : String(err), "Existing config (could not parse)");
|
|
370
|
+
}
|
|
371
|
+
const overwrite = await p.confirm({
|
|
372
|
+
message: "Overwrite existing .skill-issue/config.yaml?",
|
|
373
|
+
initialValue: false
|
|
374
|
+
});
|
|
375
|
+
if (p.isCancel(overwrite) || !overwrite) {
|
|
376
|
+
p.cancel("Aborted. Existing config unchanged.");
|
|
377
|
+
process.exit(0);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
const source = await p.select({
|
|
381
|
+
message: "Where does the skill registry live?",
|
|
382
|
+
options: [
|
|
383
|
+
{
|
|
384
|
+
value: "local",
|
|
385
|
+
label: "Local directory \u2014 skill-registry repo root (registry.json + registry/)"
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
value: "remote",
|
|
389
|
+
label: "GitHub \u2014 clone registry from owner/repo"
|
|
390
|
+
}
|
|
391
|
+
],
|
|
392
|
+
initialValue: "local"
|
|
393
|
+
});
|
|
394
|
+
if (p.isCancel(source)) {
|
|
395
|
+
p.cancel("Aborted.");
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
let cfg;
|
|
399
|
+
if (source === "local") {
|
|
400
|
+
const regPath = await p.text({
|
|
401
|
+
message: "Path to registry repo root (relative to this project or absolute). Must contain registry/ and be a git repository.",
|
|
402
|
+
placeholder: ".",
|
|
403
|
+
initialValue: ".",
|
|
404
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
405
|
+
});
|
|
406
|
+
if (p.isCancel(regPath)) {
|
|
407
|
+
p.cancel("Aborted.");
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
const branch = await p.text({
|
|
411
|
+
message: "Branch label (stored in config; installs use git HEAD from that directory)",
|
|
412
|
+
initialValue: "main",
|
|
413
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
414
|
+
});
|
|
415
|
+
if (p.isCancel(branch)) {
|
|
416
|
+
p.cancel("Aborted.");
|
|
417
|
+
process.exit(0);
|
|
418
|
+
}
|
|
419
|
+
cfg = ConfigSchema.parse({
|
|
420
|
+
registry: {
|
|
421
|
+
path: String(regPath).trim(),
|
|
422
|
+
branch: String(branch).trim()
|
|
423
|
+
},
|
|
424
|
+
skillsRoot: ".agents/skills"
|
|
425
|
+
});
|
|
426
|
+
} else {
|
|
427
|
+
const owner = await p.text({
|
|
428
|
+
message: "GitHub registry owner (org or user)",
|
|
429
|
+
placeholder: "acme",
|
|
430
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
431
|
+
});
|
|
432
|
+
if (p.isCancel(owner)) {
|
|
433
|
+
p.cancel("Aborted.");
|
|
434
|
+
process.exit(0);
|
|
435
|
+
}
|
|
436
|
+
const repo = await p.text({
|
|
437
|
+
message: "Registry repository name",
|
|
438
|
+
placeholder: "skill-registry",
|
|
439
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
440
|
+
});
|
|
441
|
+
if (p.isCancel(repo)) {
|
|
442
|
+
p.cancel("Aborted.");
|
|
443
|
+
process.exit(0);
|
|
444
|
+
}
|
|
445
|
+
const branch = await p.text({
|
|
446
|
+
message: "Branch to track",
|
|
447
|
+
initialValue: "main",
|
|
448
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
449
|
+
});
|
|
450
|
+
if (p.isCancel(branch)) {
|
|
451
|
+
p.cancel("Aborted.");
|
|
452
|
+
process.exit(0);
|
|
453
|
+
}
|
|
454
|
+
cfg = ConfigSchema.parse({
|
|
455
|
+
registry: {
|
|
456
|
+
owner: String(owner).trim(),
|
|
457
|
+
repo: String(repo).trim(),
|
|
458
|
+
branch: String(branch).trim()
|
|
459
|
+
},
|
|
460
|
+
skillsRoot: ".agents/skills"
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
const skillsRoot = await p.text({
|
|
464
|
+
message: "Install skills under (relative to project root)",
|
|
465
|
+
initialValue: cfg.skillsRoot,
|
|
466
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
467
|
+
});
|
|
468
|
+
if (p.isCancel(skillsRoot)) {
|
|
469
|
+
p.cancel("Aborted.");
|
|
470
|
+
process.exit(0);
|
|
471
|
+
}
|
|
472
|
+
cfg = ConfigSchema.parse({ ...cfg, skillsRoot: String(skillsRoot).trim() });
|
|
473
|
+
const confirm3 = await p.confirm({
|
|
474
|
+
message: hasExistingConfig ? `Overwrite ${chalk.cyan(".skill-issue/config.yaml")} and use ${chalk.cyan(cfg.skillsRoot)} for installs?` : `Create ${chalk.cyan(".skill-issue/config.yaml")} and use ${chalk.cyan(cfg.skillsRoot)} for installs?`,
|
|
475
|
+
initialValue: true
|
|
476
|
+
});
|
|
477
|
+
if (p.isCancel(confirm3) || !confirm3) {
|
|
478
|
+
p.cancel("Aborted.");
|
|
479
|
+
process.exit(0);
|
|
480
|
+
}
|
|
481
|
+
const dir = skillIssueDir(cwd);
|
|
482
|
+
await mkdir3(dir, { recursive: true });
|
|
483
|
+
await writeConfig(cwd, cfg);
|
|
484
|
+
p.note(defaultConfigTemplate().trim(), "Template reference");
|
|
485
|
+
p.outro(
|
|
486
|
+
chalk.green(
|
|
487
|
+
"Wrote .skill-issue/config.yaml. Run skissue install <id>, or skissue for the manage menu."
|
|
488
|
+
)
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// src/commands/install.ts
|
|
493
|
+
import chalk2 from "chalk";
|
|
494
|
+
import ora from "ora";
|
|
495
|
+
import { join as join5 } from "node:path";
|
|
496
|
+
|
|
497
|
+
// src/lockfile.ts
|
|
498
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir4 } from "node:fs/promises";
|
|
499
|
+
import { dirname as dirname2 } from "node:path";
|
|
500
|
+
import { z as z2 } from "zod";
|
|
501
|
+
var LockSkillEntrySchema = z2.object({
|
|
502
|
+
registryCommit: z2.string().min(1),
|
|
503
|
+
skillPath: z2.string().min(1),
|
|
504
|
+
ref: z2.string().min(1)
|
|
505
|
+
});
|
|
506
|
+
var LockSchema = z2.object({
|
|
507
|
+
version: z2.literal(1),
|
|
508
|
+
skills: z2.record(z2.string(), LockSkillEntrySchema)
|
|
509
|
+
});
|
|
510
|
+
function emptyLock() {
|
|
511
|
+
return { version: 1, skills: {} };
|
|
512
|
+
}
|
|
513
|
+
async function readLock(cwd) {
|
|
514
|
+
const path = lockPath(cwd);
|
|
515
|
+
const raw = await readFile2(path, "utf8");
|
|
516
|
+
const data = JSON.parse(raw);
|
|
517
|
+
return LockSchema.parse(data);
|
|
518
|
+
}
|
|
519
|
+
async function writeLock(cwd, lock) {
|
|
520
|
+
const path = lockPath(cwd);
|
|
521
|
+
await mkdir4(dirname2(path), { recursive: true });
|
|
522
|
+
const parsed = LockSchema.parse(lock);
|
|
523
|
+
await writeFile2(path, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
524
|
+
}
|
|
525
|
+
async function readLockOrEmpty(cwd) {
|
|
526
|
+
try {
|
|
527
|
+
return await readLock(cwd);
|
|
528
|
+
} catch {
|
|
529
|
+
return emptyLock();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
function upsertSkillLock(lock, skillId, entry) {
|
|
533
|
+
return {
|
|
534
|
+
...lock,
|
|
535
|
+
skills: { ...lock.skills, [skillId]: entry }
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function removeSkillLock(lock, skillId) {
|
|
539
|
+
const skills = { ...lock.skills };
|
|
540
|
+
delete skills[skillId];
|
|
541
|
+
return { ...lock, skills };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/registry/resolve.ts
|
|
545
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
546
|
+
import { join as join3 } from "node:path";
|
|
547
|
+
import { z as z3 } from "zod";
|
|
548
|
+
var RegistryJsonSchema = z3.object({
|
|
549
|
+
skills: z3.record(z3.string(), z3.string()).optional()
|
|
550
|
+
}).passthrough();
|
|
551
|
+
async function resolveSkillPath(registryRepoRoot, skillId) {
|
|
552
|
+
const registryFile = join3(registryRepoRoot, "registry.json");
|
|
553
|
+
let raw;
|
|
554
|
+
try {
|
|
555
|
+
raw = await readFile3(registryFile, "utf8");
|
|
556
|
+
} catch {
|
|
557
|
+
return { skillPath: join3("registry", skillId).replace(/\\/g, "/"), source: "convention" };
|
|
558
|
+
}
|
|
559
|
+
const parsed = JSON.parse(raw);
|
|
560
|
+
const reg = RegistryJsonSchema.safeParse(parsed);
|
|
561
|
+
if (!reg.success || !reg.data.skills) {
|
|
562
|
+
return { skillPath: join3("registry", skillId).replace(/\\/g, "/"), source: "convention" };
|
|
563
|
+
}
|
|
564
|
+
const mapped = reg.data.skills[skillId];
|
|
565
|
+
if (mapped !== void 0 && mapped.length > 0) {
|
|
566
|
+
return { skillPath: normalizeRelPath(mapped), source: "registry.json" };
|
|
567
|
+
}
|
|
568
|
+
return { skillPath: join3("registry", skillId).replace(/\\/g, "/"), source: "convention" };
|
|
569
|
+
}
|
|
570
|
+
function normalizeRelPath(p3) {
|
|
571
|
+
return p3.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/io.ts
|
|
575
|
+
import { access as access2, cp, rm as rm2 } from "node:fs/promises";
|
|
576
|
+
import { constants } from "node:fs";
|
|
577
|
+
import { join as join4 } from "node:path";
|
|
578
|
+
async function assertSkillMdPresent(skillSourceDir) {
|
|
579
|
+
const p3 = join4(skillSourceDir, "SKILL.md");
|
|
580
|
+
try {
|
|
581
|
+
await access2(p3, constants.R_OK);
|
|
582
|
+
} catch {
|
|
583
|
+
throw new Error(`Expected SKILL.md in skill path: ${skillSourceDir}`);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async function copySkillTree(fromDir, toDir) {
|
|
587
|
+
await rm2(toDir, { recursive: true, force: true }).catch(() => void 0);
|
|
588
|
+
await cp(fromDir, toDir, { recursive: true, force: true });
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// src/commands/install.ts
|
|
592
|
+
async function installSkillFromCheckout(cwd, config, repoPath, head, skillId) {
|
|
593
|
+
const { skillPath } = await resolveSkillPath(repoPath, skillId);
|
|
594
|
+
const src = join5(repoPath, skillPath);
|
|
595
|
+
await assertSkillMdPresent(src);
|
|
596
|
+
const dest = skillInstallPath(cwd, config.skillsRoot, skillId);
|
|
597
|
+
await copySkillTree(src, dest);
|
|
598
|
+
const lock = await readLockOrEmpty(cwd);
|
|
599
|
+
const ref = isLocalRegistry(config) ? "local" : `refs/heads/${config.registry.branch}`;
|
|
600
|
+
const next = upsertSkillLock(lock, skillId, {
|
|
601
|
+
registryCommit: head,
|
|
602
|
+
skillPath,
|
|
603
|
+
ref
|
|
604
|
+
});
|
|
605
|
+
await writeLock(cwd, next);
|
|
606
|
+
return dest;
|
|
607
|
+
}
|
|
608
|
+
async function runInstall(cwd, skillId) {
|
|
609
|
+
const config = await loadConfig(cwd);
|
|
610
|
+
const spin = ora(`Resolving registry and installing ${skillId}`).start();
|
|
611
|
+
try {
|
|
612
|
+
const { path: repoPath, head } = await ensureRegistryCheckout(cwd, config);
|
|
613
|
+
const dest = await installSkillFromCheckout(cwd, config, repoPath, head, skillId);
|
|
614
|
+
spin.succeed(chalk2.green(`Installed ${skillId} \u2192 ${dest}`));
|
|
615
|
+
} catch (err) {
|
|
616
|
+
spin.fail(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
617
|
+
throw err;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
async function runInstallMany(cwd, skillIds, options) {
|
|
621
|
+
const config = await loadConfig(cwd);
|
|
622
|
+
let repoPath;
|
|
623
|
+
let head;
|
|
624
|
+
if (options?.checkout) {
|
|
625
|
+
({ path: repoPath, head } = options.checkout);
|
|
626
|
+
} else {
|
|
627
|
+
const prep = ora("Preparing registry\u2026").start();
|
|
628
|
+
try {
|
|
629
|
+
const c = await ensureRegistryCheckout(cwd, config);
|
|
630
|
+
repoPath = c.path;
|
|
631
|
+
head = c.head;
|
|
632
|
+
prep.succeed(chalk2.green("Registry ready."));
|
|
633
|
+
} catch (err) {
|
|
634
|
+
prep.fail(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
635
|
+
throw err;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
for (const skillId of skillIds) {
|
|
639
|
+
const spin = ora(`Installing ${skillId}\u2026`).start();
|
|
640
|
+
try {
|
|
641
|
+
const dest = await installSkillFromCheckout(cwd, config, repoPath, head, skillId);
|
|
642
|
+
spin.succeed(chalk2.green(`Installed ${skillId} \u2192 ${dest}`));
|
|
643
|
+
} catch (err) {
|
|
644
|
+
spin.fail(chalk2.red(err instanceof Error ? err.message : String(err)));
|
|
645
|
+
throw err;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/commands/uninstall.ts
|
|
651
|
+
import chalk3 from "chalk";
|
|
652
|
+
import ora2 from "ora";
|
|
653
|
+
import { rm as rm3 } from "node:fs/promises";
|
|
654
|
+
async function runUninstall(cwd, skillId) {
|
|
655
|
+
const config = await loadConfig(cwd);
|
|
656
|
+
const spin = ora2(`Removing ${skillId}`).start();
|
|
657
|
+
try {
|
|
658
|
+
const dest = skillInstallPath(cwd, config.skillsRoot, skillId);
|
|
659
|
+
await rm3(dest, { recursive: true, force: true });
|
|
660
|
+
const lock = await readLockOrEmpty(cwd);
|
|
661
|
+
if (!lock.skills[skillId]) {
|
|
662
|
+
spin.stopAndPersist({
|
|
663
|
+
symbol: chalk3.yellow("\u26A0"),
|
|
664
|
+
text: chalk3.yellow(`No lock entry for ${skillId}; removed directory if present.`)
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
await writeLock(cwd, removeSkillLock(lock, skillId));
|
|
668
|
+
spin.succeed(chalk3.green(`Uninstalled ${skillId}`));
|
|
669
|
+
}
|
|
670
|
+
} catch (err) {
|
|
671
|
+
spin.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
|
|
672
|
+
throw err;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/commands/list.ts
|
|
677
|
+
import chalk4 from "chalk";
|
|
678
|
+
async function runList(cwd) {
|
|
679
|
+
const config = await loadConfig(cwd);
|
|
680
|
+
const lock = await readLockOrEmpty(cwd);
|
|
681
|
+
const ids = Object.keys(lock.skills).sort();
|
|
682
|
+
if (ids.length === 0) {
|
|
683
|
+
console.log(chalk4.dim("No skills installed (lock empty)."));
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
for (const id of ids) {
|
|
687
|
+
const e = lock.skills[id];
|
|
688
|
+
const dest = skillInstallPath(cwd, config.skillsRoot, id);
|
|
689
|
+
console.log(
|
|
690
|
+
[
|
|
691
|
+
chalk4.bold(id),
|
|
692
|
+
chalk4.dim("\u2192"),
|
|
693
|
+
dest,
|
|
694
|
+
chalk4.dim(`commit=${e.registryCommit.slice(0, 7)} path=${e.skillPath}`)
|
|
695
|
+
].join(" ")
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// src/commands/outdated.ts
|
|
701
|
+
import chalk5 from "chalk";
|
|
702
|
+
import ora3 from "ora";
|
|
703
|
+
async function runOutdated(cwd) {
|
|
704
|
+
const config = await loadConfig(cwd);
|
|
705
|
+
const lock = await readLockOrEmpty(cwd);
|
|
706
|
+
const ids = Object.keys(lock.skills).sort();
|
|
707
|
+
if (ids.length === 0) {
|
|
708
|
+
console.log(chalk5.dim("Nothing installed."));
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
const spin = ora3("Fetching registry and comparing paths").start();
|
|
712
|
+
try {
|
|
713
|
+
const { path: repoPath, head } = await ensureRegistryCheckout(cwd, config);
|
|
714
|
+
spin.stop();
|
|
715
|
+
for (const id of ids) {
|
|
716
|
+
const e = lock.skills[id];
|
|
717
|
+
const d = await diffPath(repoPath, e.registryCommit, head, e.skillPath);
|
|
718
|
+
const stale = isPathStale(d);
|
|
719
|
+
if (stale) {
|
|
720
|
+
console.log(
|
|
721
|
+
chalk5.yellow(`${id}`) + chalk5.dim(
|
|
722
|
+
` outdated (path changed ${e.registryCommit.slice(0, 7)} \u2192 ${head.slice(0, 7)})`
|
|
723
|
+
)
|
|
724
|
+
);
|
|
725
|
+
} else {
|
|
726
|
+
console.log(chalk5.green(`${id}`) + chalk5.dim(` up to date @ ${head.slice(0, 7)}`));
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
} catch (err) {
|
|
730
|
+
spin.fail(chalk5.red(err instanceof Error ? err.message : String(err)));
|
|
731
|
+
process.exitCode = 1;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// src/commands/update.ts
|
|
736
|
+
import chalk6 from "chalk";
|
|
737
|
+
async function runUpdate(cwd, skillId) {
|
|
738
|
+
await loadConfig(cwd);
|
|
739
|
+
const lock = await readLockOrEmpty(cwd);
|
|
740
|
+
if (skillId) {
|
|
741
|
+
if (!lock.skills[skillId]) {
|
|
742
|
+
console.error(chalk6.red(`Unknown skill in lock: ${skillId}`));
|
|
743
|
+
process.exitCode = 1;
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
await runInstall(cwd, skillId);
|
|
748
|
+
} catch {
|
|
749
|
+
process.exitCode = 1;
|
|
750
|
+
}
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
const ids = Object.keys(lock.skills).sort();
|
|
754
|
+
if (ids.length === 0) {
|
|
755
|
+
console.log(chalk6.dim("Nothing to update (lock empty)."));
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
await runInstallMany(cwd, ids);
|
|
760
|
+
} catch {
|
|
761
|
+
process.exitCode = 1;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// src/commands/manage.ts
|
|
766
|
+
import * as p2 from "@clack/prompts";
|
|
767
|
+
import chalk8 from "chalk";
|
|
768
|
+
|
|
769
|
+
// src/commands/banner.ts
|
|
770
|
+
import chalk7 from "chalk";
|
|
771
|
+
var LOGO = [
|
|
772
|
+
" \u2597\u2584\u2584\u2596\u2597\u2596 \u2597\u2596\u2597\u2584\u2584\u2584\u2596\u2597\u2596 \u2597\u2596 ",
|
|
773
|
+
" \u2590\u258C \u2590\u258C\u2597\u259E\u2598 \u2588 \u2590\u258C \u2590\u258C ",
|
|
774
|
+
" \u259D\u2580\u259A\u2596 \u2590\u259B\u259A\u2596 \u2588 \u2590\u258C \u2590\u258C ",
|
|
775
|
+
" \u2597\u2584\u2584\u259E\u2598\u2590\u258C \u2590\u258C\u2597\u2584\u2588\u2584\u2596\u2590\u2599\u2584\u2584\u2596\u2590\u2599\u2584\u2584\u2596",
|
|
776
|
+
" \u2597\u2584\u2584\u2584\u2596\u2597\u2584\u2584\u2596\u2597\u2584\u2584\u2596\u2597\u2596 \u2597\u2596\u2597\u2584\u2584\u2584\u2596 ",
|
|
777
|
+
" \u2588 \u2590\u258C \u2590\u258C \u2590\u258C \u2590\u258C\u2590\u258C ",
|
|
778
|
+
" \u2588 \u259D\u2580\u259A\u2596 \u259D\u2580\u259A\u2596\u2590\u258C \u2590\u258C\u2590\u259B\u2580\u2580\u2598 ",
|
|
779
|
+
" \u2597\u2584\u2588\u2584\u2596\u2597\u2584\u2584\u259E\u2598\u2597\u2584\u2584\u259E\u2598\u259D\u259A\u2584\u259E\u2598\u2590\u2599\u2584\u2584\u2596 "
|
|
780
|
+
];
|
|
781
|
+
var GRADIENT_STOPS = [
|
|
782
|
+
[99, 102, 241],
|
|
783
|
+
[139, 92, 246],
|
|
784
|
+
[192, 132, 252],
|
|
785
|
+
[233, 213, 255],
|
|
786
|
+
[192, 132, 252],
|
|
787
|
+
[139, 92, 246],
|
|
788
|
+
[99, 102, 241],
|
|
789
|
+
[79, 70, 229]
|
|
790
|
+
];
|
|
791
|
+
function lerpColor(a, b, t) {
|
|
792
|
+
return [
|
|
793
|
+
Math.round(a[0] + (b[0] - a[0]) * t),
|
|
794
|
+
Math.round(a[1] + (b[1] - a[1]) * t),
|
|
795
|
+
Math.round(a[2] + (b[2] - a[2]) * t)
|
|
796
|
+
];
|
|
797
|
+
}
|
|
798
|
+
function gradientLine(line, row, totalRows) {
|
|
799
|
+
const stopIdx = row / Math.max(1, totalRows - 1) * (GRADIENT_STOPS.length - 1);
|
|
800
|
+
const lo = Math.floor(stopIdx);
|
|
801
|
+
const hi = Math.min(lo + 1, GRADIENT_STOPS.length - 1);
|
|
802
|
+
const t = stopIdx - lo;
|
|
803
|
+
const [r, g, b] = lerpColor(GRADIENT_STOPS[lo], GRADIENT_STOPS[hi], t);
|
|
804
|
+
return chalk7.rgb(r, g, b)(line);
|
|
805
|
+
}
|
|
806
|
+
function printSkillIssueBanner(version) {
|
|
807
|
+
console.log("");
|
|
808
|
+
for (let i = 0; i < LOGO.length; i++) {
|
|
809
|
+
console.log(gradientLine(LOGO[i], i, LOGO.length));
|
|
810
|
+
}
|
|
811
|
+
if (version) {
|
|
812
|
+
console.log(chalk7.dim(` v${version}`));
|
|
813
|
+
}
|
|
814
|
+
console.log("");
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// src/registry/catalog.ts
|
|
818
|
+
import { access as access3, readFile as readFile4, readdir } from "node:fs/promises";
|
|
819
|
+
import { constants as constants2 } from "node:fs";
|
|
820
|
+
import { join as join6 } from "node:path";
|
|
821
|
+
import { z as z4 } from "zod";
|
|
822
|
+
var RegistryJsonSchema2 = z4.object({
|
|
823
|
+
skills: z4.record(z4.string(), z4.string()).optional()
|
|
824
|
+
}).passthrough();
|
|
825
|
+
async function listRegistrySkillIds(registryRepoRoot) {
|
|
826
|
+
const ids = /* @__PURE__ */ new Set();
|
|
827
|
+
const registryFile = join6(registryRepoRoot, "registry.json");
|
|
828
|
+
try {
|
|
829
|
+
const raw = await readFile4(registryFile, "utf8");
|
|
830
|
+
const parsed = JSON.parse(raw);
|
|
831
|
+
const reg = RegistryJsonSchema2.safeParse(parsed);
|
|
832
|
+
if (reg.success && reg.data.skills) {
|
|
833
|
+
for (const id of Object.keys(reg.data.skills)) {
|
|
834
|
+
if (id.trim()) ids.add(id);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
} catch {
|
|
838
|
+
}
|
|
839
|
+
const registryDir = join6(registryRepoRoot, "registry");
|
|
840
|
+
try {
|
|
841
|
+
const entries = await readdir(registryDir, { withFileTypes: true });
|
|
842
|
+
for (const e of entries) {
|
|
843
|
+
if (!e.isDirectory()) continue;
|
|
844
|
+
const id = e.name;
|
|
845
|
+
if (!id.trim()) continue;
|
|
846
|
+
const skillRoot = join6(registryDir, id);
|
|
847
|
+
try {
|
|
848
|
+
await access3(join6(skillRoot, "SKILL.md"), constants2.R_OK);
|
|
849
|
+
ids.add(id);
|
|
850
|
+
} catch {
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
} catch {
|
|
854
|
+
}
|
|
855
|
+
return [...ids].sort((a, b) => a.localeCompare(b));
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/commands/manage.ts
|
|
859
|
+
function printRegistryStats(catalogLen, installed, canInstall) {
|
|
860
|
+
const sep = chalk8.dim(" \xB7 ");
|
|
861
|
+
const line = [
|
|
862
|
+
chalk8.dim("Registry"),
|
|
863
|
+
chalk8.cyan(catalogLen),
|
|
864
|
+
chalk8.dim("skills"),
|
|
865
|
+
sep,
|
|
866
|
+
chalk8.dim("installed"),
|
|
867
|
+
chalk8.cyan(installed),
|
|
868
|
+
sep,
|
|
869
|
+
chalk8.dim("not installed"),
|
|
870
|
+
chalk8.cyan(canInstall)
|
|
871
|
+
].join(" ");
|
|
872
|
+
p2.log.info(line);
|
|
873
|
+
}
|
|
874
|
+
async function runManage(cwd) {
|
|
875
|
+
printSkillIssueBanner();
|
|
876
|
+
p2.intro(chalk8.dim("manage"));
|
|
877
|
+
const config = await loadConfig(cwd);
|
|
878
|
+
const spin = p2.spinner();
|
|
879
|
+
spin.start("Preparing registry\u2026");
|
|
880
|
+
let checkout;
|
|
881
|
+
try {
|
|
882
|
+
checkout = await ensureRegistryCheckout(cwd, config);
|
|
883
|
+
spin.stop(chalk8.green("Registry ready."));
|
|
884
|
+
} catch (err) {
|
|
885
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
886
|
+
spin.stop(chalk8.red(msg));
|
|
887
|
+
throw err;
|
|
888
|
+
}
|
|
889
|
+
const catalog = await listRegistrySkillIds(checkout.path);
|
|
890
|
+
let lock = await readLockOrEmpty(cwd);
|
|
891
|
+
while (true) {
|
|
892
|
+
const installedIds = Object.keys(lock.skills).sort();
|
|
893
|
+
const available = catalog.filter((id) => !lock.skills[id]);
|
|
894
|
+
printRegistryStats(catalog.length, installedIds.length, available.length);
|
|
895
|
+
const action = await p2.select({
|
|
896
|
+
message: chalk8.bold("Next step"),
|
|
897
|
+
initialValue: "install",
|
|
898
|
+
options: [
|
|
899
|
+
{
|
|
900
|
+
value: "install",
|
|
901
|
+
label: "Install",
|
|
902
|
+
hint: chalk8.dim(`${available.length} available`)
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
value: "uninstall",
|
|
906
|
+
label: "Uninstall",
|
|
907
|
+
hint: chalk8.dim(`${installedIds.length} installed`)
|
|
908
|
+
},
|
|
909
|
+
{ value: "done", label: "Exit", hint: chalk8.dim("back to shell") }
|
|
910
|
+
]
|
|
911
|
+
});
|
|
912
|
+
if (p2.isCancel(action)) {
|
|
913
|
+
p2.cancel("Aborted.");
|
|
914
|
+
process.exit(0);
|
|
915
|
+
}
|
|
916
|
+
if (action === "done") {
|
|
917
|
+
p2.outro(chalk8.green("Finished."));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
if (action === "install") {
|
|
921
|
+
if (available.length === 0) {
|
|
922
|
+
p2.log.warn(
|
|
923
|
+
chalk8.dim("Nothing left to install \u2014 everything in the registry is already installed.")
|
|
924
|
+
);
|
|
925
|
+
continue;
|
|
926
|
+
}
|
|
927
|
+
p2.log.message(chalk8.dim("Space toggle \xB7 Enter confirm \xB7 Esc cancel"));
|
|
928
|
+
const selected2 = await p2.multiselect({
|
|
929
|
+
message: "Skills to install",
|
|
930
|
+
options: available.map((id) => ({ value: id, label: id })),
|
|
931
|
+
required: false
|
|
932
|
+
});
|
|
933
|
+
if (p2.isCancel(selected2)) {
|
|
934
|
+
p2.cancel("Aborted.");
|
|
935
|
+
process.exit(0);
|
|
936
|
+
}
|
|
937
|
+
if (selected2.length === 0) {
|
|
938
|
+
p2.log.warn(chalk8.dim("No selection \u2014 back to the menu."));
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
const ok2 = await p2.confirm({
|
|
942
|
+
message: `Install ${chalk8.cyan(selected2.join(", "))}?`,
|
|
943
|
+
initialValue: true
|
|
944
|
+
});
|
|
945
|
+
if (p2.isCancel(ok2)) {
|
|
946
|
+
p2.cancel("Aborted.");
|
|
947
|
+
process.exit(0);
|
|
948
|
+
}
|
|
949
|
+
if (!ok2) continue;
|
|
950
|
+
await runInstallMany(cwd, selected2, { checkout });
|
|
951
|
+
lock = await readLockOrEmpty(cwd);
|
|
952
|
+
continue;
|
|
953
|
+
}
|
|
954
|
+
if (installedIds.length === 0) {
|
|
955
|
+
p2.log.warn(chalk8.dim("Nothing installed yet."));
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
p2.log.message(chalk8.dim("Space toggle \xB7 Enter confirm \xB7 Esc cancel"));
|
|
959
|
+
const selected = await p2.multiselect({
|
|
960
|
+
message: "Skills to remove",
|
|
961
|
+
options: installedIds.map((id) => ({ value: id, label: id })),
|
|
962
|
+
required: false
|
|
963
|
+
});
|
|
964
|
+
if (p2.isCancel(selected)) {
|
|
965
|
+
p2.cancel("Aborted.");
|
|
966
|
+
process.exit(0);
|
|
967
|
+
}
|
|
968
|
+
if (selected.length === 0) {
|
|
969
|
+
p2.log.warn(chalk8.dim("No selection \u2014 back to the menu."));
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
const ok = await p2.confirm({
|
|
973
|
+
message: `Remove ${chalk8.cyan(selected.join(", "))}?`,
|
|
974
|
+
initialValue: false
|
|
975
|
+
});
|
|
976
|
+
if (p2.isCancel(ok)) {
|
|
977
|
+
p2.cancel("Aborted.");
|
|
978
|
+
process.exit(0);
|
|
979
|
+
}
|
|
980
|
+
if (!ok) continue;
|
|
981
|
+
const toRemove = [...selected].sort((a, b) => a.localeCompare(b));
|
|
982
|
+
for (const id of toRemove) {
|
|
983
|
+
await runUninstall(cwd, id);
|
|
984
|
+
}
|
|
985
|
+
lock = await readLockOrEmpty(cwd);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// src/commands/default.ts
|
|
990
|
+
async function runDefault(cwd) {
|
|
991
|
+
if (await needsSetup(cwd)) {
|
|
992
|
+
await runInit(cwd);
|
|
993
|
+
await runManage(cwd);
|
|
994
|
+
} else {
|
|
995
|
+
await runManage(cwd);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// src/commands/doctor.ts
|
|
1000
|
+
import chalk9 from "chalk";
|
|
1001
|
+
function nodeOk() {
|
|
1002
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
1003
|
+
if (Number.isFinite(major) && major >= 24) {
|
|
1004
|
+
return { ok: true, detail: `Node ${process.version}` };
|
|
1005
|
+
}
|
|
1006
|
+
return { ok: false, detail: `Node ${process.version} (need >=24)` };
|
|
1007
|
+
}
|
|
1008
|
+
function authHint(config) {
|
|
1009
|
+
if (isLocalRegistry(config)) {
|
|
1010
|
+
return "";
|
|
1011
|
+
}
|
|
1012
|
+
const t = resolveRegistryTransport(config);
|
|
1013
|
+
if (t === "ssh") {
|
|
1014
|
+
return "Registry uses SSH (or auto: no token in env); ensure `ssh -T git@github.com` succeeds.";
|
|
1015
|
+
}
|
|
1016
|
+
if (process.env.GITHUB_TOKEN || process.env.GH_TOKEN) {
|
|
1017
|
+
return "Registry uses HTTPS with GITHUB_TOKEN or GH_TOKEN.";
|
|
1018
|
+
}
|
|
1019
|
+
return "HTTPS without a token (registry.useSsh: false). Public repos only, or set a token / omit registry.useSsh for auto SSH when no token.";
|
|
1020
|
+
}
|
|
1021
|
+
async function runDoctor(cwd) {
|
|
1022
|
+
const n = nodeOk();
|
|
1023
|
+
console.log(n.ok ? chalk9.green(`\u2713 ${n.detail}`) : chalk9.red(`\u2717 ${n.detail}`));
|
|
1024
|
+
let config;
|
|
1025
|
+
try {
|
|
1026
|
+
config = await loadConfig(cwd);
|
|
1027
|
+
console.log(chalk9.green("\u2713 .skill-issue/config.yaml valid"));
|
|
1028
|
+
} catch (e) {
|
|
1029
|
+
console.log(chalk9.red(`\u2717 config: ${e instanceof Error ? e.message : String(e)}`));
|
|
1030
|
+
process.exitCode = 1;
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (!isLocalRegistry(config)) {
|
|
1034
|
+
console.log(chalk9.dim(`\u2022 ${authHint(config)}`));
|
|
1035
|
+
}
|
|
1036
|
+
try {
|
|
1037
|
+
const { path: repoPath, head } = await ensureRegistryCheckout(cwd, config);
|
|
1038
|
+
const label = isLocalRegistry(config) ? "Local registry" : "Registry cache";
|
|
1039
|
+
console.log(chalk9.green(`\u2713 ${label} synced (${repoPath}) @ ${head.slice(0, 7)}`));
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
console.log(chalk9.red(`\u2717 Registry: ${e instanceof Error ? e.message : String(e)}`));
|
|
1042
|
+
process.exitCode = 1;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/entry.ts
|
|
1047
|
+
var require2 = createRequire(import.meta.url);
|
|
1048
|
+
var pkg = require2("../package.json");
|
|
1049
|
+
var program = new Command();
|
|
1050
|
+
program.name("skissue").description("Install and sync agent skills from a GitHub registry or a local registry path").version(pkg.version);
|
|
1051
|
+
program.command("init").description("Create .skill-issue/config and confirm skills install path").action(async () => {
|
|
1052
|
+
const cwd = process.cwd();
|
|
1053
|
+
try {
|
|
1054
|
+
await runInit(cwd);
|
|
1055
|
+
} catch (e) {
|
|
1056
|
+
console.error(e);
|
|
1057
|
+
process.exitCode = 1;
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
program.command("install").description("Install a skill by id from the registry").argument("<id>", "Skill id").action(async (id) => {
|
|
1061
|
+
const cwd = process.cwd();
|
|
1062
|
+
try {
|
|
1063
|
+
await runInstall(cwd, id);
|
|
1064
|
+
} catch {
|
|
1065
|
+
process.exitCode = 1;
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
program.command("uninstall").description("Remove an installed skill and lock entry").argument("<id>", "Skill id").action(async (id) => {
|
|
1069
|
+
const cwd = process.cwd();
|
|
1070
|
+
try {
|
|
1071
|
+
await runUninstall(cwd, id);
|
|
1072
|
+
} catch {
|
|
1073
|
+
process.exitCode = 1;
|
|
1074
|
+
}
|
|
1075
|
+
});
|
|
1076
|
+
program.command("list").description("List installed skills and lock info").action(async () => {
|
|
1077
|
+
const cwd = process.cwd();
|
|
1078
|
+
try {
|
|
1079
|
+
await runList(cwd);
|
|
1080
|
+
} catch {
|
|
1081
|
+
process.exitCode = 1;
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
program.command("manage").alias("browse").description("Interactive menu: install or uninstall skills from the registry").action(async () => {
|
|
1085
|
+
const cwd = process.cwd();
|
|
1086
|
+
try {
|
|
1087
|
+
await runManage(cwd);
|
|
1088
|
+
} catch {
|
|
1089
|
+
process.exitCode = 1;
|
|
1090
|
+
}
|
|
1091
|
+
});
|
|
1092
|
+
program.command("outdated").description("Show skills whose registry path changed since lock").action(async () => {
|
|
1093
|
+
const cwd = process.cwd();
|
|
1094
|
+
try {
|
|
1095
|
+
await runOutdated(cwd);
|
|
1096
|
+
} catch {
|
|
1097
|
+
process.exitCode = 1;
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
program.command("update").description("Re-fetch and overwrite installed skill(s); refresh lock").argument("[id]", "Optional skill id (default: all locked skills)").action(async (id) => {
|
|
1101
|
+
const cwd = process.cwd();
|
|
1102
|
+
try {
|
|
1103
|
+
await runUpdate(cwd, id);
|
|
1104
|
+
} catch {
|
|
1105
|
+
process.exitCode = 1;
|
|
1106
|
+
}
|
|
1107
|
+
});
|
|
1108
|
+
program.command("doctor").description("Check Node, config, and sync registry checkout").option("-C, --cwd <path>", "Project root", process.cwd()).action(async (opts) => {
|
|
1109
|
+
const cwd = resolve2(opts.cwd ?? process.cwd());
|
|
1110
|
+
try {
|
|
1111
|
+
await runDoctor(cwd);
|
|
1112
|
+
} catch {
|
|
1113
|
+
process.exitCode = 1;
|
|
1114
|
+
}
|
|
1115
|
+
});
|
|
1116
|
+
program.action(async () => {
|
|
1117
|
+
const cwd = process.cwd();
|
|
1118
|
+
try {
|
|
1119
|
+
await runDefault(cwd);
|
|
1120
|
+
} catch {
|
|
1121
|
+
process.exitCode = 1;
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
program.parseAsync(process.argv).catch((e) => {
|
|
1125
|
+
console.error(e);
|
|
1126
|
+
process.exit(1);
|
|
1127
|
+
});
|