skissue 0.1.15 → 0.1.17
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 +9 -12
- package/dist/entry.js +643 -359
- package/package.json +1 -1
package/dist/entry.js
CHANGED
|
@@ -4,13 +4,14 @@
|
|
|
4
4
|
// src/entry.ts
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { createRequire } from "node:module";
|
|
7
|
-
import { resolve as
|
|
7
|
+
import { resolve as resolve4 } from "node:path";
|
|
8
8
|
|
|
9
9
|
// src/commands/init.ts
|
|
10
|
-
import * as
|
|
11
|
-
import
|
|
12
|
-
import { existsSync as
|
|
13
|
-
import { mkdir as
|
|
10
|
+
import * as p2 from "@clack/prompts";
|
|
11
|
+
import chalk2 from "chalk";
|
|
12
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
13
|
+
import { mkdir as mkdir4 } from "node:fs/promises";
|
|
14
|
+
import { relative, resolve as resolve3 } from "node:path";
|
|
14
15
|
|
|
15
16
|
// src/config.ts
|
|
16
17
|
import { existsSync } from "node:fs";
|
|
@@ -116,17 +117,10 @@ function defaultConfigTemplate() {
|
|
|
116
117
|
return DEFAULT_CONFIG_YAML;
|
|
117
118
|
}
|
|
118
119
|
|
|
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
120
|
// src/git/exec.ts
|
|
127
121
|
import { spawn } from "node:child_process";
|
|
128
122
|
function execGit(args, options = {}) {
|
|
129
|
-
return new Promise((
|
|
123
|
+
return new Promise((resolve5) => {
|
|
130
124
|
const env = { ...process.env, ...options.env };
|
|
131
125
|
if (env.GIT_TERMINAL_PROMPT === void 0) {
|
|
132
126
|
env.GIT_TERMINAL_PROMPT = "0";
|
|
@@ -151,7 +145,7 @@ function execGit(args, options = {}) {
|
|
|
151
145
|
child.on("close", (code, signal) => {
|
|
152
146
|
if (timeout !== void 0) clearTimeout(timeout);
|
|
153
147
|
if (signal === "SIGTERM") {
|
|
154
|
-
|
|
148
|
+
resolve5({
|
|
155
149
|
code: 124,
|
|
156
150
|
stdout,
|
|
157
151
|
stderr: `${stderr}
|
|
@@ -159,16 +153,21 @@ skissue: git timed out after ${timeoutMs}ms`.trim()
|
|
|
159
153
|
});
|
|
160
154
|
return;
|
|
161
155
|
}
|
|
162
|
-
|
|
156
|
+
resolve5({ code: code ?? 1, stdout, stderr });
|
|
163
157
|
});
|
|
164
158
|
child.on("error", (err) => {
|
|
165
159
|
if (timeout !== void 0) clearTimeout(timeout);
|
|
166
|
-
|
|
160
|
+
resolve5({ code: 1, stdout, stderr: String(err) });
|
|
167
161
|
});
|
|
168
162
|
});
|
|
169
163
|
}
|
|
170
164
|
|
|
171
165
|
// src/git/registry-repo.ts
|
|
166
|
+
import { createHash } from "node:crypto";
|
|
167
|
+
import { existsSync as existsSync2, statSync } from "node:fs";
|
|
168
|
+
import { mkdir as mkdir2, access, rm } from "node:fs/promises";
|
|
169
|
+
import { homedir } from "node:os";
|
|
170
|
+
import { join as join2, resolve } from "node:path";
|
|
172
171
|
function githubTokenFromEnv() {
|
|
173
172
|
const raw = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN;
|
|
174
173
|
const t = typeof raw === "string" ? raw.trim() : "";
|
|
@@ -287,7 +286,7 @@ async function ensureRegistryCheckout(cwd, config) {
|
|
|
287
286
|
}
|
|
288
287
|
} else {
|
|
289
288
|
const fetch = await execGit(
|
|
290
|
-
["fetch", "origin",
|
|
289
|
+
["fetch", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`, "--depth", "1"],
|
|
291
290
|
{ cwd: dir, timeoutMs: 3e5 }
|
|
292
291
|
);
|
|
293
292
|
if (fetch.code !== 0) {
|
|
@@ -340,8 +339,214 @@ async function diffPath(repoPath, fromCommit, toCommit, pathInRepo) {
|
|
|
340
339
|
function isPathStale(diffStdout) {
|
|
341
340
|
return diffStdout.trim().length > 0;
|
|
342
341
|
}
|
|
342
|
+
async function isSkillPathStaleAtHead(repoPath, head, entry) {
|
|
343
|
+
const d = await diffPath(repoPath, entry.registryCommit, head, entry.skillPath);
|
|
344
|
+
return isPathStale(d);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// src/commands/init-registry.ts
|
|
348
|
+
import * as p from "@clack/prompts";
|
|
349
|
+
import chalk from "chalk";
|
|
350
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
|
|
351
|
+
import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
|
|
352
|
+
import { join as join3, resolve as resolve2 } from "node:path";
|
|
353
|
+
function validateSkillId(raw) {
|
|
354
|
+
const id = raw.trim();
|
|
355
|
+
if (!id) return "Skill id is required";
|
|
356
|
+
if (id.includes("/") || id.includes("\\")) return "Use a single segment (no path separators)";
|
|
357
|
+
if (id === "." || id === ".." || id.includes("..")) return "Invalid skill id";
|
|
358
|
+
return void 0;
|
|
359
|
+
}
|
|
360
|
+
function registryLayoutExists(root) {
|
|
361
|
+
const jsonPath = join3(root, "registry.json");
|
|
362
|
+
const regDir = join3(root, "registry");
|
|
363
|
+
if (existsSync3(jsonPath)) return true;
|
|
364
|
+
if (!existsSync3(regDir)) return false;
|
|
365
|
+
try {
|
|
366
|
+
return statSync2(regDir).isDirectory();
|
|
367
|
+
} catch {
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function skillMarkdown(skillId) {
|
|
372
|
+
const title = skillId.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
373
|
+
return `---
|
|
374
|
+
name: ${skillId}
|
|
375
|
+
description: Sample skill scaffolded by skissue init-registry. Replace this description.
|
|
376
|
+
---
|
|
377
|
+
|
|
378
|
+
# ${title}
|
|
379
|
+
|
|
380
|
+
A minimal starter skill for your registry.
|
|
381
|
+
|
|
382
|
+
## When to use
|
|
383
|
+
|
|
384
|
+
Use when the user works with this sample capability (customize this section).
|
|
385
|
+
|
|
386
|
+
## Instructions
|
|
387
|
+
|
|
388
|
+
1. Edit this file to describe what the agent should do.
|
|
389
|
+
2. Optionally add \`hard/\` with executable checks.
|
|
390
|
+
`;
|
|
391
|
+
}
|
|
392
|
+
async function commitScaffoldedRegistry(root, skillId) {
|
|
393
|
+
const gitDir = join3(root, ".git");
|
|
394
|
+
if (!existsSync3(gitDir)) {
|
|
395
|
+
return { committed: false };
|
|
396
|
+
}
|
|
397
|
+
const regSkillPath = `registry/${skillId}`;
|
|
398
|
+
const addJson = await execGit(["add", "registry.json"], { cwd: root });
|
|
399
|
+
if (addJson.code !== 0) {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`git add registry.json failed: ${addJson.stderr || addJson.stdout || `exit ${addJson.code}`}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const addSkill = await execGit(["add", regSkillPath], { cwd: root });
|
|
405
|
+
if (addSkill.code !== 0) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`git add ${regSkillPath} failed: ${addSkill.stderr || addSkill.stdout || `exit ${addSkill.code}`}`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
const diff = await execGit(["diff", "--cached", "--quiet"], { cwd: root });
|
|
411
|
+
const hasStaged = diff.code === 1;
|
|
412
|
+
if (hasStaged) {
|
|
413
|
+
const commit = await execGit(["commit", "-m", "chore: scaffold skill registry"], { cwd: root });
|
|
414
|
+
if (commit.code !== 0) {
|
|
415
|
+
throw new Error(
|
|
416
|
+
`git commit failed. Configure git user.name and user.email in this repo or globally. ${(commit.stderr || commit.stdout || "").trim()}`
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
return { committed: true };
|
|
420
|
+
}
|
|
421
|
+
const head = await execGit(["rev-parse", "HEAD"], { cwd: root });
|
|
422
|
+
if (head.code !== 0) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"Local registry must have at least one git commit so installs can resolve HEAD. Stage and commit the scaffolded files, or run git commit."
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
return { committed: false };
|
|
428
|
+
}
|
|
429
|
+
async function promptMinimalRegistryScaffold(root) {
|
|
430
|
+
const exists = registryLayoutExists(root);
|
|
431
|
+
if (exists) {
|
|
432
|
+
const ok = await p.confirm({
|
|
433
|
+
message: `${chalk.yellow("registry.json and/or registry/ already exist here.")} Replace with a minimal single-skill layout?`,
|
|
434
|
+
initialValue: false
|
|
435
|
+
});
|
|
436
|
+
if (p.isCancel(ok) || !ok) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const idRaw = await p.text({
|
|
441
|
+
message: "Sample skill id (folder name under registry/)",
|
|
442
|
+
initialValue: "sample-skill",
|
|
443
|
+
validate: (v) => validateSkillId(String(v ?? ""))
|
|
444
|
+
});
|
|
445
|
+
if (p.isCancel(idRaw)) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
const skillId = String(idRaw).trim();
|
|
449
|
+
const gitDir = join3(root, ".git");
|
|
450
|
+
const hadGit = existsSync3(gitDir);
|
|
451
|
+
let runGitInit = false;
|
|
452
|
+
if (!hadGit) {
|
|
453
|
+
const initGit = await p.confirm({
|
|
454
|
+
message: "No git repository here yet. Run `git init`? (recommended \u2014 skissue local registry mode needs a git repo)",
|
|
455
|
+
initialValue: true
|
|
456
|
+
});
|
|
457
|
+
if (p.isCancel(initGit)) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
runGitInit = Boolean(initGit);
|
|
461
|
+
}
|
|
462
|
+
return { skillId, runGitInit, hadGit };
|
|
463
|
+
}
|
|
464
|
+
async function scaffoldMinimalRegistry(params) {
|
|
465
|
+
const { root, skillId, runGitInit } = params;
|
|
466
|
+
const regPath = `registry/${skillId}`;
|
|
467
|
+
const manifest = { skills: { [skillId]: regPath } };
|
|
468
|
+
await mkdir3(join3(root, regPath), { recursive: true });
|
|
469
|
+
await writeFile2(join3(root, "registry.json"), `${JSON.stringify(manifest, null, 2)}
|
|
470
|
+
`, "utf8");
|
|
471
|
+
await writeFile2(join3(root, regPath, "SKILL.md"), skillMarkdown(skillId), "utf8");
|
|
472
|
+
let gitInitRan = false;
|
|
473
|
+
if (runGitInit) {
|
|
474
|
+
const r = await execGit(["init"], { cwd: root });
|
|
475
|
+
if (r.code === 0) {
|
|
476
|
+
gitInitRan = true;
|
|
477
|
+
} else {
|
|
478
|
+
throw new Error(`git init failed: ${r.stderr || r.stdout || `exit ${r.code}`}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const { committed } = await commitScaffoldedRegistry(root, skillId);
|
|
482
|
+
return { root, skillId, gitInitRan, committed };
|
|
483
|
+
}
|
|
484
|
+
async function runInitRegistry(cwd) {
|
|
485
|
+
p.intro(chalk.bold("skissue init-registry"));
|
|
486
|
+
const where = await p.select({
|
|
487
|
+
message: "Where should the skill registry live?",
|
|
488
|
+
options: [
|
|
489
|
+
{ value: "here", label: "Current directory \u2014 create registry files here" },
|
|
490
|
+
{ value: "other", label: "Another directory \u2014 I will enter the path" }
|
|
491
|
+
],
|
|
492
|
+
initialValue: "here"
|
|
493
|
+
});
|
|
494
|
+
if (p.isCancel(where)) {
|
|
495
|
+
p.cancel("Aborted.");
|
|
496
|
+
process.exit(0);
|
|
497
|
+
}
|
|
498
|
+
let root;
|
|
499
|
+
if (where === "here") {
|
|
500
|
+
root = resolve2(cwd);
|
|
501
|
+
} else {
|
|
502
|
+
const raw = await p.text({
|
|
503
|
+
message: "Path to registry root (absolute or relative to current directory)",
|
|
504
|
+
placeholder: "../my-skill-registry",
|
|
505
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
506
|
+
});
|
|
507
|
+
if (p.isCancel(raw)) {
|
|
508
|
+
p.cancel("Aborted.");
|
|
509
|
+
process.exit(0);
|
|
510
|
+
}
|
|
511
|
+
root = resolve2(cwd, String(raw).trim());
|
|
512
|
+
}
|
|
513
|
+
const prompted = await promptMinimalRegistryScaffold(root);
|
|
514
|
+
if (!prompted) {
|
|
515
|
+
p.cancel("Aborted. No files were changed.");
|
|
516
|
+
process.exit(0);
|
|
517
|
+
}
|
|
518
|
+
const { skillId, runGitInit, hadGit } = prompted;
|
|
519
|
+
try {
|
|
520
|
+
const result = await scaffoldMinimalRegistry({ root, skillId, runGitInit });
|
|
521
|
+
if (!hadGit && !result.gitInitRan) {
|
|
522
|
+
p.note(
|
|
523
|
+
"This folder is not a git repository. Run `git init` before using it as a local skissue registry.",
|
|
524
|
+
chalk.yellow("Heads up")
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
p.outro(
|
|
528
|
+
chalk.green(
|
|
529
|
+
`Wrote ${chalk.cyan("registry.json")} and ${chalk.cyan(`registry/${skillId}/SKILL.md`)} under ${chalk.cyan(result.root)}`
|
|
530
|
+
)
|
|
531
|
+
);
|
|
532
|
+
} catch (e) {
|
|
533
|
+
p.cancel(e instanceof Error ? e.message : String(e));
|
|
534
|
+
process.exitCode = 1;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
343
537
|
|
|
344
538
|
// src/commands/init.ts
|
|
539
|
+
async function gitBranchShowCurrent(root) {
|
|
540
|
+
const r = await execGit(["branch", "--show-current"], { cwd: root });
|
|
541
|
+
if (r.code !== 0) return void 0;
|
|
542
|
+
const b = r.stdout.trim();
|
|
543
|
+
return b.length > 0 ? b : void 0;
|
|
544
|
+
}
|
|
545
|
+
function localRegistryPathForConfig(cwd, resolvedRoot) {
|
|
546
|
+
const rel = relative(cwd, resolvedRoot);
|
|
547
|
+
if (!rel || rel === "") return ".";
|
|
548
|
+
return rel;
|
|
549
|
+
}
|
|
345
550
|
function summarizeConfig(cfg) {
|
|
346
551
|
const r = cfg.registry;
|
|
347
552
|
if (r.path?.trim()) {
|
|
@@ -357,27 +562,27 @@ Branch: ${r.branch}
|
|
|
357
562
|
skillsRoot: ${cfg.skillsRoot}`;
|
|
358
563
|
}
|
|
359
564
|
async function runInit(cwd) {
|
|
360
|
-
|
|
565
|
+
p2.intro(chalk2.bold("skissue init"));
|
|
361
566
|
const existingPath = configPath(cwd);
|
|
362
567
|
let hasExistingConfig = false;
|
|
363
|
-
if (
|
|
568
|
+
if (existsSync4(existingPath)) {
|
|
364
569
|
hasExistingConfig = true;
|
|
365
570
|
try {
|
|
366
571
|
const existing = await loadConfig(cwd);
|
|
367
|
-
|
|
572
|
+
p2.note(summarizeConfig(existing), "Current config");
|
|
368
573
|
} catch (err) {
|
|
369
|
-
|
|
574
|
+
p2.note(err instanceof Error ? err.message : String(err), "Existing config (could not parse)");
|
|
370
575
|
}
|
|
371
|
-
const overwrite = await
|
|
576
|
+
const overwrite = await p2.confirm({
|
|
372
577
|
message: "Overwrite existing .skill-issue/config.yaml?",
|
|
373
578
|
initialValue: false
|
|
374
579
|
});
|
|
375
|
-
if (
|
|
376
|
-
|
|
580
|
+
if (p2.isCancel(overwrite) || !overwrite) {
|
|
581
|
+
p2.cancel("Aborted. Existing config unchanged.");
|
|
377
582
|
process.exit(0);
|
|
378
583
|
}
|
|
379
584
|
}
|
|
380
|
-
const source = await
|
|
585
|
+
const source = await p2.select({
|
|
381
586
|
message: "Where does the skill registry live?",
|
|
382
587
|
options: [
|
|
383
588
|
{
|
|
@@ -391,64 +596,151 @@ async function runInit(cwd) {
|
|
|
391
596
|
],
|
|
392
597
|
initialValue: "local"
|
|
393
598
|
});
|
|
394
|
-
if (
|
|
395
|
-
|
|
599
|
+
if (p2.isCancel(source)) {
|
|
600
|
+
p2.cancel("Aborted.");
|
|
396
601
|
process.exit(0);
|
|
397
602
|
}
|
|
398
603
|
let cfg;
|
|
604
|
+
let bootstrappedSkillId;
|
|
399
605
|
if (source === "local") {
|
|
400
|
-
const
|
|
401
|
-
message: "
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
606
|
+
const localMode = await p2.select({
|
|
607
|
+
message: "Local registry setup",
|
|
608
|
+
options: [
|
|
609
|
+
{
|
|
610
|
+
value: "existing",
|
|
611
|
+
label: "Use an existing registry folder (registry.json + registry/)"
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
value: "bootstrap",
|
|
615
|
+
label: "Bootstrap a minimal sample registry"
|
|
616
|
+
}
|
|
617
|
+
],
|
|
618
|
+
initialValue: "existing"
|
|
405
619
|
});
|
|
406
|
-
if (
|
|
407
|
-
|
|
620
|
+
if (p2.isCancel(localMode)) {
|
|
621
|
+
p2.cancel("Aborted.");
|
|
408
622
|
process.exit(0);
|
|
409
623
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
624
|
+
if (localMode === "existing") {
|
|
625
|
+
const regPath = await p2.text({
|
|
626
|
+
message: "Path to registry repo root (relative to this project or absolute). Must contain registry/ and be a git repository.",
|
|
627
|
+
placeholder: ".",
|
|
628
|
+
initialValue: ".",
|
|
629
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
630
|
+
});
|
|
631
|
+
if (p2.isCancel(regPath)) {
|
|
632
|
+
p2.cancel("Aborted.");
|
|
633
|
+
process.exit(0);
|
|
634
|
+
}
|
|
635
|
+
const resolvedExisting = resolve3(cwd, String(regPath).trim());
|
|
636
|
+
const branchDefault = await gitBranchShowCurrent(resolvedExisting) ?? "main";
|
|
637
|
+
const branch = await p2.text({
|
|
638
|
+
message: "Branch label (stored in config; installs use git HEAD from that directory)",
|
|
639
|
+
initialValue: branchDefault,
|
|
640
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
641
|
+
});
|
|
642
|
+
if (p2.isCancel(branch)) {
|
|
643
|
+
p2.cancel("Aborted.");
|
|
644
|
+
process.exit(0);
|
|
645
|
+
}
|
|
646
|
+
cfg = ConfigSchema.parse({
|
|
647
|
+
registry: {
|
|
648
|
+
path: String(regPath).trim(),
|
|
649
|
+
branch: String(branch).trim()
|
|
650
|
+
},
|
|
651
|
+
skillsRoot: ".agents/skills"
|
|
652
|
+
});
|
|
653
|
+
} else {
|
|
654
|
+
const pathRaw = await p2.text({
|
|
655
|
+
message: "Path for the new registry root (relative to this project or absolute)",
|
|
656
|
+
placeholder: "./skill-registry",
|
|
657
|
+
initialValue: "./skill-registry",
|
|
658
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
659
|
+
});
|
|
660
|
+
if (p2.isCancel(pathRaw)) {
|
|
661
|
+
p2.cancel("Aborted.");
|
|
662
|
+
process.exit(0);
|
|
663
|
+
}
|
|
664
|
+
const resolvedRoot = resolve3(cwd, String(pathRaw).trim());
|
|
665
|
+
const atConsumerRoot = resolvedRoot === resolve3(cwd);
|
|
666
|
+
if (atConsumerRoot && !registryLayoutExists(resolvedRoot)) {
|
|
667
|
+
const okRoot = await p2.confirm({
|
|
668
|
+
message: "This adds registry.json and registry/ at your project root. Continue?",
|
|
669
|
+
initialValue: false
|
|
670
|
+
});
|
|
671
|
+
if (p2.isCancel(okRoot) || !okRoot) {
|
|
672
|
+
p2.cancel("Aborted.");
|
|
673
|
+
process.exit(0);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const prompted = await promptMinimalRegistryScaffold(resolvedRoot);
|
|
677
|
+
if (!prompted) {
|
|
678
|
+
p2.cancel("Aborted. No files were changed.");
|
|
679
|
+
process.exit(0);
|
|
680
|
+
}
|
|
681
|
+
let result;
|
|
682
|
+
try {
|
|
683
|
+
result = await scaffoldMinimalRegistry({
|
|
684
|
+
root: resolvedRoot,
|
|
685
|
+
skillId: prompted.skillId,
|
|
686
|
+
runGitInit: prompted.runGitInit
|
|
687
|
+
});
|
|
688
|
+
} catch (e) {
|
|
689
|
+
p2.cancel(e instanceof Error ? e.message : String(e));
|
|
690
|
+
process.exitCode = 1;
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
bootstrappedSkillId = result.skillId;
|
|
694
|
+
if (!prompted.hadGit && !result.gitInitRan) {
|
|
695
|
+
p2.note(
|
|
696
|
+
"This folder is not a git repository. Run `git init` before using it as a local skissue registry.",
|
|
697
|
+
chalk2.yellow("Heads up")
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
const branchDefault = await gitBranchShowCurrent(resolvedRoot) ?? "main";
|
|
701
|
+
const branch = await p2.text({
|
|
702
|
+
message: "Branch label (stored in config; installs use git HEAD from that directory)",
|
|
703
|
+
initialValue: branchDefault,
|
|
704
|
+
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
705
|
+
});
|
|
706
|
+
if (p2.isCancel(branch)) {
|
|
707
|
+
p2.cancel("Aborted.");
|
|
708
|
+
process.exit(0);
|
|
709
|
+
}
|
|
710
|
+
cfg = ConfigSchema.parse({
|
|
711
|
+
registry: {
|
|
712
|
+
path: localRegistryPathForConfig(cwd, resolvedRoot),
|
|
713
|
+
branch: String(branch).trim()
|
|
714
|
+
},
|
|
715
|
+
skillsRoot: ".agents/skills"
|
|
716
|
+
});
|
|
418
717
|
}
|
|
419
|
-
cfg = ConfigSchema.parse({
|
|
420
|
-
registry: {
|
|
421
|
-
path: String(regPath).trim(),
|
|
422
|
-
branch: String(branch).trim()
|
|
423
|
-
},
|
|
424
|
-
skillsRoot: ".agents/skills"
|
|
425
|
-
});
|
|
426
718
|
} else {
|
|
427
|
-
const owner = await
|
|
719
|
+
const owner = await p2.text({
|
|
428
720
|
message: "GitHub registry owner (org or user)",
|
|
429
721
|
placeholder: "acme",
|
|
430
722
|
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
431
723
|
});
|
|
432
|
-
if (
|
|
433
|
-
|
|
724
|
+
if (p2.isCancel(owner)) {
|
|
725
|
+
p2.cancel("Aborted.");
|
|
434
726
|
process.exit(0);
|
|
435
727
|
}
|
|
436
|
-
const repo = await
|
|
728
|
+
const repo = await p2.text({
|
|
437
729
|
message: "Registry repository name",
|
|
438
730
|
placeholder: "skill-registry",
|
|
439
731
|
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
440
732
|
});
|
|
441
|
-
if (
|
|
442
|
-
|
|
733
|
+
if (p2.isCancel(repo)) {
|
|
734
|
+
p2.cancel("Aborted.");
|
|
443
735
|
process.exit(0);
|
|
444
736
|
}
|
|
445
|
-
const branch = await
|
|
737
|
+
const branch = await p2.text({
|
|
446
738
|
message: "Branch to track",
|
|
447
739
|
initialValue: "main",
|
|
448
740
|
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
449
741
|
});
|
|
450
|
-
if (
|
|
451
|
-
|
|
742
|
+
if (p2.isCancel(branch)) {
|
|
743
|
+
p2.cancel("Aborted.");
|
|
452
744
|
process.exit(0);
|
|
453
745
|
}
|
|
454
746
|
cfg = ConfigSchema.parse({
|
|
@@ -460,42 +752,42 @@ async function runInit(cwd) {
|
|
|
460
752
|
skillsRoot: ".agents/skills"
|
|
461
753
|
});
|
|
462
754
|
}
|
|
463
|
-
const skillsRoot = await
|
|
755
|
+
const skillsRoot = await p2.text({
|
|
464
756
|
message: "Install skills under (relative to project root)",
|
|
465
757
|
initialValue: cfg.skillsRoot,
|
|
466
758
|
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
467
759
|
});
|
|
468
|
-
if (
|
|
469
|
-
|
|
760
|
+
if (p2.isCancel(skillsRoot)) {
|
|
761
|
+
p2.cancel("Aborted.");
|
|
470
762
|
process.exit(0);
|
|
471
763
|
}
|
|
472
764
|
cfg = ConfigSchema.parse({ ...cfg, skillsRoot: String(skillsRoot).trim() });
|
|
473
|
-
const confirm4 = await
|
|
474
|
-
message: hasExistingConfig ? `Overwrite ${
|
|
765
|
+
const confirm4 = await p2.confirm({
|
|
766
|
+
message: hasExistingConfig ? `Overwrite ${chalk2.cyan(".skill-issue/config.yaml")} and use ${chalk2.cyan(cfg.skillsRoot)} for installs?` : `Create ${chalk2.cyan(".skill-issue/config.yaml")} and use ${chalk2.cyan(cfg.skillsRoot)} for installs?`,
|
|
475
767
|
initialValue: true
|
|
476
768
|
});
|
|
477
|
-
if (
|
|
478
|
-
|
|
769
|
+
if (p2.isCancel(confirm4) || !confirm4) {
|
|
770
|
+
p2.cancel("Aborted.");
|
|
479
771
|
process.exit(0);
|
|
480
772
|
}
|
|
481
773
|
const dir = skillIssueDir(cwd);
|
|
482
|
-
await
|
|
774
|
+
await mkdir4(dir, { recursive: true });
|
|
483
775
|
await writeConfig(cwd, cfg);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
);
|
|
776
|
+
p2.note(defaultConfigTemplate().trim(), "Template reference");
|
|
777
|
+
const outroLines = bootstrappedSkillId ? [
|
|
778
|
+
"Wrote .skill-issue/config.yaml.",
|
|
779
|
+
`Try ${chalk2.cyan(`skissue install ${bootstrappedSkillId}`)}, or run skissue for the manage menu.`
|
|
780
|
+
] : ["Wrote .skill-issue/config.yaml. Run skissue install <id>, or skissue for the manage menu."];
|
|
781
|
+
p2.outro(chalk2.green(outroLines.join(" ")));
|
|
490
782
|
}
|
|
491
783
|
|
|
492
784
|
// src/commands/install.ts
|
|
493
|
-
import
|
|
785
|
+
import chalk3 from "chalk";
|
|
494
786
|
import ora from "ora";
|
|
495
|
-
import { join as
|
|
787
|
+
import { join as join6 } from "node:path";
|
|
496
788
|
|
|
497
789
|
// src/lockfile.ts
|
|
498
|
-
import { readFile as readFile2, writeFile as
|
|
790
|
+
import { readFile as readFile2, writeFile as writeFile3, mkdir as mkdir5 } from "node:fs/promises";
|
|
499
791
|
import { dirname as dirname2 } from "node:path";
|
|
500
792
|
import { z as z2 } from "zod";
|
|
501
793
|
var LockSkillEntrySchema = z2.object({
|
|
@@ -518,9 +810,9 @@ async function readLock(cwd) {
|
|
|
518
810
|
}
|
|
519
811
|
async function writeLock(cwd, lock) {
|
|
520
812
|
const path = lockPath(cwd);
|
|
521
|
-
await
|
|
813
|
+
await mkdir5(dirname2(path), { recursive: true });
|
|
522
814
|
const parsed = LockSchema.parse(lock);
|
|
523
|
-
await
|
|
815
|
+
await writeFile3(path, JSON.stringify(parsed, null, 2) + "\n", "utf8");
|
|
524
816
|
}
|
|
525
817
|
async function readLockOrEmpty(cwd) {
|
|
526
818
|
try {
|
|
@@ -543,29 +835,29 @@ function removeSkillLock(lock, skillId) {
|
|
|
543
835
|
|
|
544
836
|
// src/registry/resolve.ts
|
|
545
837
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
546
|
-
import { join as
|
|
838
|
+
import { join as join4 } from "node:path";
|
|
547
839
|
import { z as z3 } from "zod";
|
|
548
840
|
var RegistryJsonSchema = z3.object({
|
|
549
841
|
skills: z3.record(z3.string(), z3.string()).optional()
|
|
550
842
|
}).passthrough();
|
|
551
843
|
async function resolveSkillPath(registryRepoRoot, skillId) {
|
|
552
|
-
const registryFile =
|
|
844
|
+
const registryFile = join4(registryRepoRoot, "registry.json");
|
|
553
845
|
let raw;
|
|
554
846
|
try {
|
|
555
847
|
raw = await readFile3(registryFile, "utf8");
|
|
556
848
|
} catch {
|
|
557
|
-
return { skillPath:
|
|
849
|
+
return { skillPath: join4("registry", skillId).replace(/\\/g, "/"), source: "convention" };
|
|
558
850
|
}
|
|
559
851
|
const parsed = JSON.parse(raw);
|
|
560
852
|
const reg = RegistryJsonSchema.safeParse(parsed);
|
|
561
853
|
if (!reg.success || !reg.data.skills) {
|
|
562
|
-
return { skillPath:
|
|
854
|
+
return { skillPath: join4("registry", skillId).replace(/\\/g, "/"), source: "convention" };
|
|
563
855
|
}
|
|
564
856
|
const mapped = reg.data.skills[skillId];
|
|
565
857
|
if (mapped !== void 0 && mapped.length > 0) {
|
|
566
858
|
return { skillPath: normalizeRelPath(mapped), source: "registry.json" };
|
|
567
859
|
}
|
|
568
|
-
return { skillPath:
|
|
860
|
+
return { skillPath: join4("registry", skillId).replace(/\\/g, "/"), source: "convention" };
|
|
569
861
|
}
|
|
570
862
|
function normalizeRelPath(p4) {
|
|
571
863
|
return p4.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/\/+$/, "");
|
|
@@ -574,9 +866,9 @@ function normalizeRelPath(p4) {
|
|
|
574
866
|
// src/io.ts
|
|
575
867
|
import { access as access2, cp, rm as rm2 } from "node:fs/promises";
|
|
576
868
|
import { constants } from "node:fs";
|
|
577
|
-
import { join as
|
|
869
|
+
import { join as join5 } from "node:path";
|
|
578
870
|
async function assertSkillMdPresent(skillSourceDir) {
|
|
579
|
-
const p4 =
|
|
871
|
+
const p4 = join5(skillSourceDir, "SKILL.md");
|
|
580
872
|
try {
|
|
581
873
|
await access2(p4, constants.R_OK);
|
|
582
874
|
} catch {
|
|
@@ -591,7 +883,7 @@ async function copySkillTree(fromDir, toDir) {
|
|
|
591
883
|
// src/commands/install.ts
|
|
592
884
|
async function installSkillFromCheckout(cwd, config, repoPath, head, skillId) {
|
|
593
885
|
const { skillPath } = await resolveSkillPath(repoPath, skillId);
|
|
594
|
-
const src =
|
|
886
|
+
const src = join6(repoPath, skillPath);
|
|
595
887
|
await assertSkillMdPresent(src);
|
|
596
888
|
const dest = skillInstallPath(cwd, config.skillsRoot, skillId);
|
|
597
889
|
await copySkillTree(src, dest);
|
|
@@ -611,9 +903,9 @@ async function runInstall(cwd, skillId) {
|
|
|
611
903
|
try {
|
|
612
904
|
const { path: repoPath, head } = await ensureRegistryCheckout(cwd, config);
|
|
613
905
|
const dest = await installSkillFromCheckout(cwd, config, repoPath, head, skillId);
|
|
614
|
-
spin.succeed(
|
|
906
|
+
spin.succeed(chalk3.green(`Installed ${skillId} \u2192 ${dest}`));
|
|
615
907
|
} catch (err) {
|
|
616
|
-
spin.fail(
|
|
908
|
+
spin.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
|
|
617
909
|
throw err;
|
|
618
910
|
}
|
|
619
911
|
}
|
|
@@ -629,9 +921,9 @@ async function runInstallMany(cwd, skillIds, options) {
|
|
|
629
921
|
const c = await ensureRegistryCheckout(cwd, config);
|
|
630
922
|
repoPath = c.path;
|
|
631
923
|
head = c.head;
|
|
632
|
-
prep.succeed(
|
|
924
|
+
prep.succeed(chalk3.green("Registry ready."));
|
|
633
925
|
} catch (err) {
|
|
634
|
-
prep.fail(
|
|
926
|
+
prep.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
|
|
635
927
|
throw err;
|
|
636
928
|
}
|
|
637
929
|
}
|
|
@@ -639,16 +931,16 @@ async function runInstallMany(cwd, skillIds, options) {
|
|
|
639
931
|
const spin = ora(`Installing ${skillId}\u2026`).start();
|
|
640
932
|
try {
|
|
641
933
|
const dest = await installSkillFromCheckout(cwd, config, repoPath, head, skillId);
|
|
642
|
-
spin.succeed(
|
|
934
|
+
spin.succeed(chalk3.green(`Installed ${skillId} \u2192 ${dest}`));
|
|
643
935
|
} catch (err) {
|
|
644
|
-
spin.fail(
|
|
936
|
+
spin.fail(chalk3.red(err instanceof Error ? err.message : String(err)));
|
|
645
937
|
throw err;
|
|
646
938
|
}
|
|
647
939
|
}
|
|
648
940
|
}
|
|
649
941
|
|
|
650
942
|
// src/commands/uninstall.ts
|
|
651
|
-
import
|
|
943
|
+
import chalk4 from "chalk";
|
|
652
944
|
import ora2 from "ora";
|
|
653
945
|
import { rm as rm3 } from "node:fs/promises";
|
|
654
946
|
async function runUninstall(cwd, skillId) {
|
|
@@ -660,27 +952,27 @@ async function runUninstall(cwd, skillId) {
|
|
|
660
952
|
const lock = await readLockOrEmpty(cwd);
|
|
661
953
|
if (!lock.skills[skillId]) {
|
|
662
954
|
spin.stopAndPersist({
|
|
663
|
-
symbol:
|
|
664
|
-
text:
|
|
955
|
+
symbol: chalk4.yellow("\u26A0"),
|
|
956
|
+
text: chalk4.yellow(`No lock entry for ${skillId}; removed directory if present.`)
|
|
665
957
|
});
|
|
666
958
|
} else {
|
|
667
959
|
await writeLock(cwd, removeSkillLock(lock, skillId));
|
|
668
|
-
spin.succeed(
|
|
960
|
+
spin.succeed(chalk4.green(`Uninstalled ${skillId}`));
|
|
669
961
|
}
|
|
670
962
|
} catch (err) {
|
|
671
|
-
spin.fail(
|
|
963
|
+
spin.fail(chalk4.red(err instanceof Error ? err.message : String(err)));
|
|
672
964
|
throw err;
|
|
673
965
|
}
|
|
674
966
|
}
|
|
675
967
|
|
|
676
968
|
// src/commands/list.ts
|
|
677
|
-
import
|
|
969
|
+
import chalk5 from "chalk";
|
|
678
970
|
async function runList(cwd) {
|
|
679
971
|
const config = await loadConfig(cwd);
|
|
680
972
|
const lock = await readLockOrEmpty(cwd);
|
|
681
973
|
const ids = Object.keys(lock.skills).sort();
|
|
682
974
|
if (ids.length === 0) {
|
|
683
|
-
console.log(
|
|
975
|
+
console.log(chalk5.dim("No skills installed (lock empty)."));
|
|
684
976
|
return;
|
|
685
977
|
}
|
|
686
978
|
for (const id of ids) {
|
|
@@ -688,24 +980,24 @@ async function runList(cwd) {
|
|
|
688
980
|
const dest = skillInstallPath(cwd, config.skillsRoot, id);
|
|
689
981
|
console.log(
|
|
690
982
|
[
|
|
691
|
-
|
|
692
|
-
|
|
983
|
+
chalk5.bold(id),
|
|
984
|
+
chalk5.dim("\u2192"),
|
|
693
985
|
dest,
|
|
694
|
-
|
|
986
|
+
chalk5.dim(`commit=${e.registryCommit.slice(0, 7)} path=${e.skillPath}`)
|
|
695
987
|
].join(" ")
|
|
696
988
|
);
|
|
697
989
|
}
|
|
698
990
|
}
|
|
699
991
|
|
|
700
992
|
// src/commands/outdated.ts
|
|
701
|
-
import
|
|
993
|
+
import chalk6 from "chalk";
|
|
702
994
|
import ora3 from "ora";
|
|
703
995
|
async function runOutdated(cwd) {
|
|
704
996
|
const config = await loadConfig(cwd);
|
|
705
997
|
const lock = await readLockOrEmpty(cwd);
|
|
706
998
|
const ids = Object.keys(lock.skills).sort();
|
|
707
999
|
if (ids.length === 0) {
|
|
708
|
-
console.log(
|
|
1000
|
+
console.log(chalk6.dim("Nothing installed."));
|
|
709
1001
|
return;
|
|
710
1002
|
}
|
|
711
1003
|
const spin = ora3("Fetching registry and comparing paths").start();
|
|
@@ -714,32 +1006,29 @@ async function runOutdated(cwd) {
|
|
|
714
1006
|
spin.stop();
|
|
715
1007
|
for (const id of ids) {
|
|
716
1008
|
const e = lock.skills[id];
|
|
717
|
-
const
|
|
718
|
-
const stale = isPathStale(d);
|
|
1009
|
+
const stale = await isSkillPathStaleAtHead(repoPath, head, e);
|
|
719
1010
|
if (stale) {
|
|
720
1011
|
console.log(
|
|
721
|
-
|
|
722
|
-
` outdated (path changed ${e.registryCommit.slice(0, 7)} \u2192 ${head.slice(0, 7)})`
|
|
723
|
-
)
|
|
1012
|
+
chalk6.bold.yellowBright("OUTDATED") + " " + chalk6.yellow(id) + chalk6.dim(` \u2014 path changed ${e.registryCommit.slice(0, 7)} \u2192 ${head.slice(0, 7)}`)
|
|
724
1013
|
);
|
|
725
1014
|
} else {
|
|
726
|
-
console.log(
|
|
1015
|
+
console.log(chalk6.green(`${id}`) + chalk6.dim(` up to date @ ${head.slice(0, 7)}`));
|
|
727
1016
|
}
|
|
728
1017
|
}
|
|
729
1018
|
} catch (err) {
|
|
730
|
-
spin.fail(
|
|
1019
|
+
spin.fail(chalk6.red(err instanceof Error ? err.message : String(err)));
|
|
731
1020
|
process.exitCode = 1;
|
|
732
1021
|
}
|
|
733
1022
|
}
|
|
734
1023
|
|
|
735
1024
|
// src/commands/update.ts
|
|
736
|
-
import
|
|
1025
|
+
import chalk7 from "chalk";
|
|
737
1026
|
async function runUpdate(cwd, skillId) {
|
|
738
1027
|
await loadConfig(cwd);
|
|
739
1028
|
const lock = await readLockOrEmpty(cwd);
|
|
740
1029
|
if (skillId) {
|
|
741
1030
|
if (!lock.skills[skillId]) {
|
|
742
|
-
console.error(
|
|
1031
|
+
console.error(chalk7.red(`Unknown skill in lock: ${skillId}`));
|
|
743
1032
|
process.exitCode = 1;
|
|
744
1033
|
return;
|
|
745
1034
|
}
|
|
@@ -752,7 +1041,7 @@ async function runUpdate(cwd, skillId) {
|
|
|
752
1041
|
}
|
|
753
1042
|
const ids = Object.keys(lock.skills).sort();
|
|
754
1043
|
if (ids.length === 0) {
|
|
755
|
-
console.log(
|
|
1044
|
+
console.log(chalk7.dim("Nothing to update (lock empty)."));
|
|
756
1045
|
return;
|
|
757
1046
|
}
|
|
758
1047
|
try {
|
|
@@ -763,11 +1052,12 @@ async function runUpdate(cwd, skillId) {
|
|
|
763
1052
|
}
|
|
764
1053
|
|
|
765
1054
|
// src/commands/manage.ts
|
|
766
|
-
import * as
|
|
767
|
-
import
|
|
1055
|
+
import * as p3 from "@clack/prompts";
|
|
1056
|
+
import chalk9 from "chalk";
|
|
1057
|
+
import readline from "node:readline";
|
|
768
1058
|
|
|
769
1059
|
// src/commands/banner.ts
|
|
770
|
-
import
|
|
1060
|
+
import chalk8 from "chalk";
|
|
771
1061
|
var LOGO = [
|
|
772
1062
|
" \u2597\u2584\u2584\u2596\u2597\u2596 \u2597\u2596\u2597\u2584\u2584\u2584\u2596\u2597\u2596 \u2597\u2596 ",
|
|
773
1063
|
" \u2590\u258C \u2590\u258C\u2597\u259E\u2598 \u2588 \u2590\u258C \u2590\u258C ",
|
|
@@ -801,7 +1091,7 @@ function gradientLine(line, row, totalRows) {
|
|
|
801
1091
|
const hi = Math.min(lo + 1, GRADIENT_STOPS.length - 1);
|
|
802
1092
|
const t = stopIdx - lo;
|
|
803
1093
|
const [r, g, b] = lerpColor(GRADIENT_STOPS[lo], GRADIENT_STOPS[hi], t);
|
|
804
|
-
return
|
|
1094
|
+
return chalk8.rgb(r, g, b)(line);
|
|
805
1095
|
}
|
|
806
1096
|
function printSkillIssueBanner(version) {
|
|
807
1097
|
console.log("");
|
|
@@ -809,7 +1099,7 @@ function printSkillIssueBanner(version) {
|
|
|
809
1099
|
console.log(gradientLine(LOGO[i], i, LOGO.length));
|
|
810
1100
|
}
|
|
811
1101
|
if (version) {
|
|
812
|
-
console.log(
|
|
1102
|
+
console.log(chalk8.dim(` v${version}`));
|
|
813
1103
|
}
|
|
814
1104
|
console.log("");
|
|
815
1105
|
}
|
|
@@ -817,14 +1107,14 @@ function printSkillIssueBanner(version) {
|
|
|
817
1107
|
// src/registry/catalog.ts
|
|
818
1108
|
import { access as access3, readFile as readFile4, readdir } from "node:fs/promises";
|
|
819
1109
|
import { constants as constants2 } from "node:fs";
|
|
820
|
-
import { join as
|
|
1110
|
+
import { join as join7 } from "node:path";
|
|
821
1111
|
import { z as z4 } from "zod";
|
|
822
1112
|
var RegistryJsonSchema2 = z4.object({
|
|
823
1113
|
skills: z4.record(z4.string(), z4.string()).optional()
|
|
824
1114
|
}).passthrough();
|
|
825
1115
|
async function listRegistrySkillIds(registryRepoRoot) {
|
|
826
1116
|
const ids = /* @__PURE__ */ new Set();
|
|
827
|
-
const registryFile =
|
|
1117
|
+
const registryFile = join7(registryRepoRoot, "registry.json");
|
|
828
1118
|
try {
|
|
829
1119
|
const raw = await readFile4(registryFile, "utf8");
|
|
830
1120
|
const parsed = JSON.parse(raw);
|
|
@@ -836,16 +1126,16 @@ async function listRegistrySkillIds(registryRepoRoot) {
|
|
|
836
1126
|
}
|
|
837
1127
|
} catch {
|
|
838
1128
|
}
|
|
839
|
-
const registryDir =
|
|
1129
|
+
const registryDir = join7(registryRepoRoot, "registry");
|
|
840
1130
|
try {
|
|
841
1131
|
const entries = await readdir(registryDir, { withFileTypes: true });
|
|
842
1132
|
for (const e of entries) {
|
|
843
1133
|
if (!e.isDirectory()) continue;
|
|
844
1134
|
const id = e.name;
|
|
845
1135
|
if (!id.trim()) continue;
|
|
846
|
-
const skillRoot =
|
|
1136
|
+
const skillRoot = join7(registryDir, id);
|
|
847
1137
|
try {
|
|
848
|
-
await access3(
|
|
1138
|
+
await access3(join7(skillRoot, "SKILL.md"), constants2.R_OK);
|
|
849
1139
|
ids.add(id);
|
|
850
1140
|
} catch {
|
|
851
1141
|
}
|
|
@@ -856,133 +1146,274 @@ async function listRegistrySkillIds(registryRepoRoot) {
|
|
|
856
1146
|
}
|
|
857
1147
|
|
|
858
1148
|
// src/commands/manage.ts
|
|
1149
|
+
var MAIN_MENU_DOUBLE_ESCAPE_MS = 1200;
|
|
1150
|
+
async function promptMainMenuSelect(availableCount, installedCount) {
|
|
1151
|
+
const onCtrlC = (_s, key) => {
|
|
1152
|
+
if (key?.ctrl && key?.name === "c") {
|
|
1153
|
+
p3.cancel("Aborted.");
|
|
1154
|
+
process.exit(0);
|
|
1155
|
+
}
|
|
1156
|
+
};
|
|
1157
|
+
const { stdin } = process;
|
|
1158
|
+
if (stdin.isTTY) {
|
|
1159
|
+
readline.emitKeypressEvents(stdin);
|
|
1160
|
+
stdin.prependListener("keypress", onCtrlC);
|
|
1161
|
+
}
|
|
1162
|
+
try {
|
|
1163
|
+
return await p3.select({
|
|
1164
|
+
message: chalk9.bold("Next step"),
|
|
1165
|
+
initialValue: "install",
|
|
1166
|
+
options: [
|
|
1167
|
+
{
|
|
1168
|
+
value: "install",
|
|
1169
|
+
label: "Install",
|
|
1170
|
+
hint: chalk9.dim(`${availableCount} available`)
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
value: "uninstall",
|
|
1174
|
+
label: "Uninstall",
|
|
1175
|
+
hint: chalk9.dim(`${installedCount} installed`)
|
|
1176
|
+
},
|
|
1177
|
+
{
|
|
1178
|
+
value: "update",
|
|
1179
|
+
label: "Update",
|
|
1180
|
+
hint: chalk9.dim("refresh from registry checkout")
|
|
1181
|
+
},
|
|
1182
|
+
{ value: "done", label: "Exit", hint: chalk9.dim("back to shell") }
|
|
1183
|
+
]
|
|
1184
|
+
});
|
|
1185
|
+
} finally {
|
|
1186
|
+
if (stdin.isTTY) {
|
|
1187
|
+
stdin.removeListener("keypress", onCtrlC);
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
859
1191
|
function printRegistryStats(catalogLen, installed, canInstall) {
|
|
860
|
-
const sep =
|
|
1192
|
+
const sep = chalk9.dim(" \xB7 ");
|
|
861
1193
|
const line = [
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1194
|
+
chalk9.dim("Registry"),
|
|
1195
|
+
chalk9.cyan(catalogLen),
|
|
1196
|
+
chalk9.dim("skills"),
|
|
865
1197
|
sep,
|
|
866
|
-
|
|
867
|
-
|
|
1198
|
+
chalk9.dim("installed"),
|
|
1199
|
+
chalk9.cyan(installed),
|
|
868
1200
|
sep,
|
|
869
|
-
|
|
870
|
-
|
|
1201
|
+
chalk9.dim("not installed"),
|
|
1202
|
+
chalk9.cyan(canInstall)
|
|
871
1203
|
].join(" ");
|
|
872
|
-
|
|
1204
|
+
p3.log.info(line);
|
|
873
1205
|
}
|
|
874
1206
|
async function runManage(cwd) {
|
|
875
1207
|
printSkillIssueBanner();
|
|
876
|
-
|
|
1208
|
+
p3.intro(chalk9.dim("manage"));
|
|
877
1209
|
const config = await loadConfig(cwd);
|
|
878
|
-
const spin =
|
|
1210
|
+
const spin = p3.spinner();
|
|
879
1211
|
spin.start("Preparing registry\u2026");
|
|
880
1212
|
let checkout;
|
|
881
1213
|
try {
|
|
882
1214
|
checkout = await ensureRegistryCheckout(cwd, config);
|
|
883
|
-
spin.stop(
|
|
1215
|
+
spin.stop(chalk9.green("Registry ready."));
|
|
884
1216
|
} catch (err) {
|
|
885
1217
|
const msg = err instanceof Error ? err.message : String(err);
|
|
886
|
-
spin.stop(
|
|
1218
|
+
spin.stop(chalk9.red(msg));
|
|
887
1219
|
throw err;
|
|
888
1220
|
}
|
|
889
1221
|
const catalog = await listRegistrySkillIds(checkout.path);
|
|
890
1222
|
let lock = await readLockOrEmpty(cwd);
|
|
1223
|
+
let lastMainMenuEscapeAt = 0;
|
|
891
1224
|
while (true) {
|
|
892
1225
|
const installedIds = Object.keys(lock.skills).sort();
|
|
893
1226
|
const available = catalog.filter((id) => !lock.skills[id]);
|
|
894
1227
|
printRegistryStats(catalog.length, installedIds.length, available.length);
|
|
895
|
-
const action = await
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
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);
|
|
1228
|
+
const action = await promptMainMenuSelect(available.length, installedIds.length);
|
|
1229
|
+
if (p3.isCancel(action)) {
|
|
1230
|
+
const now = Date.now();
|
|
1231
|
+
if (now - lastMainMenuEscapeAt < MAIN_MENU_DOUBLE_ESCAPE_MS) {
|
|
1232
|
+
p3.cancel("Aborted.");
|
|
1233
|
+
process.exit(0);
|
|
1234
|
+
}
|
|
1235
|
+
lastMainMenuEscapeAt = now;
|
|
1236
|
+
p3.log.message(
|
|
1237
|
+
chalk9.dim(
|
|
1238
|
+
`Press Esc twice within ${MAIN_MENU_DOUBLE_ESCAPE_MS / 1e3}s to exit, or choose a step.`
|
|
1239
|
+
)
|
|
1240
|
+
);
|
|
1241
|
+
continue;
|
|
915
1242
|
}
|
|
1243
|
+
lastMainMenuEscapeAt = 0;
|
|
916
1244
|
if (action === "done") {
|
|
917
|
-
|
|
1245
|
+
p3.outro(chalk9.green("Finished."));
|
|
918
1246
|
return;
|
|
919
1247
|
}
|
|
1248
|
+
if (action === "update") {
|
|
1249
|
+
if (installedIds.length === 0) {
|
|
1250
|
+
p3.log.warn(chalk9.dim("Nothing installed yet."));
|
|
1251
|
+
continue;
|
|
1252
|
+
}
|
|
1253
|
+
const checkSpin = p3.spinner();
|
|
1254
|
+
checkSpin.start("Checking installed skills against registry\u2026");
|
|
1255
|
+
let staleById;
|
|
1256
|
+
try {
|
|
1257
|
+
staleById = {};
|
|
1258
|
+
for (const id of installedIds) {
|
|
1259
|
+
staleById[id] = await isSkillPathStaleAtHead(
|
|
1260
|
+
checkout.path,
|
|
1261
|
+
checkout.head,
|
|
1262
|
+
lock.skills[id]
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
checkSpin.stop(chalk9.green("Ready."));
|
|
1266
|
+
} catch (err) {
|
|
1267
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1268
|
+
checkSpin.stop(chalk9.red(msg));
|
|
1269
|
+
p3.log.error(chalk9.red(`Could not compare skills: ${msg}`));
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
const staleIds = installedIds.filter((id) => staleById[id]);
|
|
1273
|
+
const updateMode = await p3.select({
|
|
1274
|
+
message: chalk9.bold("Update skills"),
|
|
1275
|
+
initialValue: "pick",
|
|
1276
|
+
options: [
|
|
1277
|
+
{
|
|
1278
|
+
value: "pick",
|
|
1279
|
+
label: "Choose which to update",
|
|
1280
|
+
hint: staleIds.length > 0 ? chalk9.bold.yellowBright(
|
|
1281
|
+
`${staleIds.length} OUTDATED \u2014 differs from registry head`
|
|
1282
|
+
) : chalk9.dim("all match registry")
|
|
1283
|
+
},
|
|
1284
|
+
{
|
|
1285
|
+
value: "all",
|
|
1286
|
+
label: "Update all",
|
|
1287
|
+
hint: chalk9.dim(`${installedIds.length} installed`)
|
|
1288
|
+
}
|
|
1289
|
+
]
|
|
1290
|
+
});
|
|
1291
|
+
if (p3.isCancel(updateMode)) {
|
|
1292
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1293
|
+
continue;
|
|
1294
|
+
}
|
|
1295
|
+
if (updateMode === "all") {
|
|
1296
|
+
const ok2 = await p3.confirm({
|
|
1297
|
+
message: `Update all ${chalk9.cyan(String(installedIds.length))} installed skills?`,
|
|
1298
|
+
initialValue: true
|
|
1299
|
+
});
|
|
1300
|
+
if (p3.isCancel(ok2)) {
|
|
1301
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
if (!ok2) continue;
|
|
1305
|
+
try {
|
|
1306
|
+
await runInstallMany(cwd, installedIds, { checkout });
|
|
1307
|
+
} catch {
|
|
1308
|
+
p3.log.error(chalk9.red("Update failed."));
|
|
1309
|
+
}
|
|
1310
|
+
lock = await readLockOrEmpty(cwd);
|
|
1311
|
+
continue;
|
|
1312
|
+
}
|
|
1313
|
+
p3.log.message(chalk9.dim("Space toggle \xB7 Enter confirm \xB7 Esc back"));
|
|
1314
|
+
const selected = await p3.multiselect({
|
|
1315
|
+
message: "Skills to update",
|
|
1316
|
+
options: installedIds.map((id) => ({
|
|
1317
|
+
value: id,
|
|
1318
|
+
label: id,
|
|
1319
|
+
hint: staleById[id] ? chalk9.bold.yellowBright("OUTDATED") + chalk9.dim(" \xB7 update to sync") : chalk9.dim("up to date")
|
|
1320
|
+
})),
|
|
1321
|
+
initialValues: staleIds,
|
|
1322
|
+
required: false
|
|
1323
|
+
});
|
|
1324
|
+
if (p3.isCancel(selected)) {
|
|
1325
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
if (selected.length === 0) {
|
|
1329
|
+
p3.log.warn(chalk9.dim("No selection \u2014 back to the menu."));
|
|
1330
|
+
continue;
|
|
1331
|
+
}
|
|
1332
|
+
const ok = await p3.confirm({
|
|
1333
|
+
message: `Update ${chalk9.cyan(selected.join(", "))}?`,
|
|
1334
|
+
initialValue: true
|
|
1335
|
+
});
|
|
1336
|
+
if (p3.isCancel(ok)) {
|
|
1337
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (!ok) continue;
|
|
1341
|
+
try {
|
|
1342
|
+
await runInstallMany(cwd, selected, { checkout });
|
|
1343
|
+
} catch {
|
|
1344
|
+
p3.log.error(chalk9.red("Update failed."));
|
|
1345
|
+
}
|
|
1346
|
+
lock = await readLockOrEmpty(cwd);
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
920
1349
|
if (action === "install") {
|
|
921
1350
|
if (available.length === 0) {
|
|
922
|
-
|
|
923
|
-
|
|
1351
|
+
p3.log.warn(
|
|
1352
|
+
chalk9.dim("Nothing left to install \u2014 everything in the registry is already installed.")
|
|
924
1353
|
);
|
|
925
1354
|
continue;
|
|
926
1355
|
}
|
|
927
|
-
|
|
928
|
-
const
|
|
1356
|
+
p3.log.message(chalk9.dim("Space toggle \xB7 Enter confirm \xB7 Esc back"));
|
|
1357
|
+
const selected = await p3.multiselect({
|
|
929
1358
|
message: "Skills to install",
|
|
930
1359
|
options: available.map((id) => ({ value: id, label: id })),
|
|
931
1360
|
required: false
|
|
932
1361
|
});
|
|
933
|
-
if (
|
|
934
|
-
|
|
935
|
-
|
|
1362
|
+
if (p3.isCancel(selected)) {
|
|
1363
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1364
|
+
continue;
|
|
936
1365
|
}
|
|
937
|
-
if (
|
|
938
|
-
|
|
1366
|
+
if (selected.length === 0) {
|
|
1367
|
+
p3.log.warn(chalk9.dim("No selection \u2014 back to the menu."));
|
|
939
1368
|
continue;
|
|
940
1369
|
}
|
|
941
|
-
const
|
|
942
|
-
message: `Install ${
|
|
1370
|
+
const ok = await p3.confirm({
|
|
1371
|
+
message: `Install ${chalk9.cyan(selected.join(", "))}?`,
|
|
943
1372
|
initialValue: true
|
|
944
1373
|
});
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
|
|
1374
|
+
if (p3.isCancel(ok)) {
|
|
1375
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1376
|
+
continue;
|
|
948
1377
|
}
|
|
949
|
-
if (!
|
|
950
|
-
await runInstallMany(cwd,
|
|
1378
|
+
if (!ok) continue;
|
|
1379
|
+
await runInstallMany(cwd, selected, { checkout });
|
|
951
1380
|
lock = await readLockOrEmpty(cwd);
|
|
952
1381
|
continue;
|
|
953
1382
|
}
|
|
954
|
-
if (
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1383
|
+
if (action === "uninstall") {
|
|
1384
|
+
if (installedIds.length === 0) {
|
|
1385
|
+
p3.log.warn(chalk9.dim("Nothing installed yet."));
|
|
1386
|
+
continue;
|
|
1387
|
+
}
|
|
1388
|
+
p3.log.message(chalk9.dim("Space toggle \xB7 Enter confirm \xB7 Esc back"));
|
|
1389
|
+
const selected = await p3.multiselect({
|
|
1390
|
+
message: "Skills to remove",
|
|
1391
|
+
options: installedIds.map((id) => ({ value: id, label: id })),
|
|
1392
|
+
required: false
|
|
1393
|
+
});
|
|
1394
|
+
if (p3.isCancel(selected)) {
|
|
1395
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1396
|
+
continue;
|
|
1397
|
+
}
|
|
1398
|
+
if (selected.length === 0) {
|
|
1399
|
+
p3.log.warn(chalk9.dim("No selection \u2014 back to the menu."));
|
|
1400
|
+
continue;
|
|
1401
|
+
}
|
|
1402
|
+
const ok = await p3.confirm({
|
|
1403
|
+
message: `Remove ${chalk9.cyan(selected.join(", "))}?`,
|
|
1404
|
+
initialValue: false
|
|
1405
|
+
});
|
|
1406
|
+
if (p3.isCancel(ok)) {
|
|
1407
|
+
p3.log.message(chalk9.dim("Back to the menu."));
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
if (!ok) continue;
|
|
1411
|
+
const toRemove = [...selected].sort((a, b) => a.localeCompare(b));
|
|
1412
|
+
for (const id of toRemove) {
|
|
1413
|
+
await runUninstall(cwd, id);
|
|
1414
|
+
}
|
|
1415
|
+
lock = await readLockOrEmpty(cwd);
|
|
984
1416
|
}
|
|
985
|
-
lock = await readLockOrEmpty(cwd);
|
|
986
1417
|
}
|
|
987
1418
|
}
|
|
988
1419
|
|
|
@@ -997,7 +1428,7 @@ async function runDefault(cwd) {
|
|
|
997
1428
|
}
|
|
998
1429
|
|
|
999
1430
|
// src/commands/doctor.ts
|
|
1000
|
-
import
|
|
1431
|
+
import chalk10 from "chalk";
|
|
1001
1432
|
function nodeOk() {
|
|
1002
1433
|
const major = Number(process.versions.node.split(".")[0]);
|
|
1003
1434
|
if (Number.isFinite(major) && major >= 24) {
|
|
@@ -1020,172 +1451,25 @@ function authHint(config) {
|
|
|
1020
1451
|
}
|
|
1021
1452
|
async function runDoctor(cwd) {
|
|
1022
1453
|
const n = nodeOk();
|
|
1023
|
-
console.log(n.ok ?
|
|
1454
|
+
console.log(n.ok ? chalk10.green(`\u2713 ${n.detail}`) : chalk10.red(`\u2717 ${n.detail}`));
|
|
1024
1455
|
let config;
|
|
1025
1456
|
try {
|
|
1026
1457
|
config = await loadConfig(cwd);
|
|
1027
|
-
console.log(
|
|
1458
|
+
console.log(chalk10.green("\u2713 .skill-issue/config.yaml valid"));
|
|
1028
1459
|
} catch (e) {
|
|
1029
|
-
console.log(
|
|
1460
|
+
console.log(chalk10.red(`\u2717 config: ${e instanceof Error ? e.message : String(e)}`));
|
|
1030
1461
|
process.exitCode = 1;
|
|
1031
1462
|
return;
|
|
1032
1463
|
}
|
|
1033
1464
|
if (!isLocalRegistry(config)) {
|
|
1034
|
-
console.log(
|
|
1465
|
+
console.log(chalk10.dim(`\u2022 ${authHint(config)}`));
|
|
1035
1466
|
}
|
|
1036
1467
|
try {
|
|
1037
1468
|
const { path: repoPath, head } = await ensureRegistryCheckout(cwd, config);
|
|
1038
1469
|
const label = isLocalRegistry(config) ? "Local registry" : "Registry cache";
|
|
1039
|
-
console.log(
|
|
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/commands/init-registry.ts
|
|
1047
|
-
import * as p3 from "@clack/prompts";
|
|
1048
|
-
import chalk10 from "chalk";
|
|
1049
|
-
import { existsSync as existsSync4, statSync as statSync2 } from "node:fs";
|
|
1050
|
-
import { mkdir as mkdir5, writeFile as writeFile3 } from "node:fs/promises";
|
|
1051
|
-
import { join as join7, resolve as resolve2 } from "node:path";
|
|
1052
|
-
function validateSkillId(raw) {
|
|
1053
|
-
const id = raw.trim();
|
|
1054
|
-
if (!id) return "Skill id is required";
|
|
1055
|
-
if (id.includes("/") || id.includes("\\")) return "Use a single segment (no path separators)";
|
|
1056
|
-
if (id === "." || id === ".." || id.includes("..")) return "Invalid skill id";
|
|
1057
|
-
return void 0;
|
|
1058
|
-
}
|
|
1059
|
-
function registryLayoutExists(root) {
|
|
1060
|
-
const jsonPath = join7(root, "registry.json");
|
|
1061
|
-
const regDir = join7(root, "registry");
|
|
1062
|
-
if (existsSync4(jsonPath)) return true;
|
|
1063
|
-
if (!existsSync4(regDir)) return false;
|
|
1064
|
-
try {
|
|
1065
|
-
return statSync2(regDir).isDirectory();
|
|
1066
|
-
} catch {
|
|
1067
|
-
return false;
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
function skillMarkdown(skillId) {
|
|
1071
|
-
const title = skillId.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
1072
|
-
return `---
|
|
1073
|
-
name: ${skillId}
|
|
1074
|
-
description: Sample skill scaffolded by skissue init-registry. Replace this description.
|
|
1075
|
-
---
|
|
1076
|
-
|
|
1077
|
-
# ${title}
|
|
1078
|
-
|
|
1079
|
-
A minimal starter skill for your registry.
|
|
1080
|
-
|
|
1081
|
-
## When to use
|
|
1082
|
-
|
|
1083
|
-
Use when the user works with this sample capability (customize this section).
|
|
1084
|
-
|
|
1085
|
-
## Instructions
|
|
1086
|
-
|
|
1087
|
-
1. Edit this file to describe what the agent should do.
|
|
1088
|
-
2. Optionally add \`hard/\` with executable checks.
|
|
1089
|
-
`;
|
|
1090
|
-
}
|
|
1091
|
-
async function scaffoldMinimalRegistry(params) {
|
|
1092
|
-
const { root, skillId, runGitInit } = params;
|
|
1093
|
-
const regPath = `registry/${skillId}`;
|
|
1094
|
-
const manifest = { skills: { [skillId]: regPath } };
|
|
1095
|
-
await mkdir5(join7(root, regPath), { recursive: true });
|
|
1096
|
-
await writeFile3(join7(root, "registry.json"), `${JSON.stringify(manifest, null, 2)}
|
|
1097
|
-
`, "utf8");
|
|
1098
|
-
await writeFile3(join7(root, regPath, "SKILL.md"), skillMarkdown(skillId), "utf8");
|
|
1099
|
-
let gitInitRan = false;
|
|
1100
|
-
if (runGitInit) {
|
|
1101
|
-
const r = await execGit(["init"], { cwd: root });
|
|
1102
|
-
if (r.code === 0) {
|
|
1103
|
-
gitInitRan = true;
|
|
1104
|
-
} else {
|
|
1105
|
-
throw new Error(`git init failed: ${r.stderr || r.stdout || `exit ${r.code}`}`);
|
|
1106
|
-
}
|
|
1107
|
-
}
|
|
1108
|
-
return { root, skillId, gitInitRan };
|
|
1109
|
-
}
|
|
1110
|
-
async function runInitRegistry(cwd) {
|
|
1111
|
-
p3.intro(chalk10.bold("skissue init-registry"));
|
|
1112
|
-
const where = await p3.select({
|
|
1113
|
-
message: "Where should the skill registry live?",
|
|
1114
|
-
options: [
|
|
1115
|
-
{ value: "here", label: "Current directory \u2014 create registry files here" },
|
|
1116
|
-
{ value: "other", label: "Another directory \u2014 I will enter the path" }
|
|
1117
|
-
],
|
|
1118
|
-
initialValue: "here"
|
|
1119
|
-
});
|
|
1120
|
-
if (p3.isCancel(where)) {
|
|
1121
|
-
p3.cancel("Aborted.");
|
|
1122
|
-
process.exit(0);
|
|
1123
|
-
}
|
|
1124
|
-
let root;
|
|
1125
|
-
if (where === "here") {
|
|
1126
|
-
root = resolve2(cwd);
|
|
1127
|
-
} else {
|
|
1128
|
-
const raw = await p3.text({
|
|
1129
|
-
message: "Path to registry root (absolute or relative to current directory)",
|
|
1130
|
-
placeholder: "../my-skill-registry",
|
|
1131
|
-
validate: (v) => v?.trim() ? void 0 : "Required"
|
|
1132
|
-
});
|
|
1133
|
-
if (p3.isCancel(raw)) {
|
|
1134
|
-
p3.cancel("Aborted.");
|
|
1135
|
-
process.exit(0);
|
|
1136
|
-
}
|
|
1137
|
-
root = resolve2(cwd, String(raw).trim());
|
|
1138
|
-
}
|
|
1139
|
-
const exists = registryLayoutExists(root);
|
|
1140
|
-
if (exists) {
|
|
1141
|
-
const ok = await p3.confirm({
|
|
1142
|
-
message: `${chalk10.yellow("registry.json and/or registry/ already exist here.")} Replace with a minimal single-skill layout?`,
|
|
1143
|
-
initialValue: false
|
|
1144
|
-
});
|
|
1145
|
-
if (p3.isCancel(ok) || !ok) {
|
|
1146
|
-
p3.cancel("Aborted. No files were changed.");
|
|
1147
|
-
process.exit(0);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
const idRaw = await p3.text({
|
|
1151
|
-
message: "Sample skill id (folder name under registry/)",
|
|
1152
|
-
initialValue: "sample-skill",
|
|
1153
|
-
validate: (v) => validateSkillId(String(v ?? ""))
|
|
1154
|
-
});
|
|
1155
|
-
if (p3.isCancel(idRaw)) {
|
|
1156
|
-
p3.cancel("Aborted.");
|
|
1157
|
-
process.exit(0);
|
|
1158
|
-
}
|
|
1159
|
-
const skillId = String(idRaw).trim();
|
|
1160
|
-
const gitDir = join7(root, ".git");
|
|
1161
|
-
const hadGit = existsSync4(gitDir);
|
|
1162
|
-
let runGitInit = false;
|
|
1163
|
-
if (!hadGit) {
|
|
1164
|
-
const initGit = await p3.confirm({
|
|
1165
|
-
message: "No git repository here yet. Run `git init`? (recommended \u2014 skissue local registry mode needs a git repo)",
|
|
1166
|
-
initialValue: true
|
|
1167
|
-
});
|
|
1168
|
-
if (p3.isCancel(initGit)) {
|
|
1169
|
-
p3.cancel("Aborted.");
|
|
1170
|
-
process.exit(0);
|
|
1171
|
-
}
|
|
1172
|
-
runGitInit = Boolean(initGit);
|
|
1173
|
-
}
|
|
1174
|
-
try {
|
|
1175
|
-
const result = await scaffoldMinimalRegistry({ root, skillId, runGitInit });
|
|
1176
|
-
if (!hadGit && !result.gitInitRan) {
|
|
1177
|
-
p3.note(
|
|
1178
|
-
"This folder is not a git repository. Run `git init` before using it as a local skissue registry.",
|
|
1179
|
-
chalk10.yellow("Heads up")
|
|
1180
|
-
);
|
|
1181
|
-
}
|
|
1182
|
-
p3.outro(
|
|
1183
|
-
chalk10.green(
|
|
1184
|
-
`Wrote ${chalk10.cyan("registry.json")} and ${chalk10.cyan(`registry/${skillId}/SKILL.md`)} under ${chalk10.cyan(result.root)}`
|
|
1185
|
-
)
|
|
1186
|
-
);
|
|
1470
|
+
console.log(chalk10.green(`\u2713 ${label} synced (${repoPath}) @ ${head.slice(0, 7)}`));
|
|
1187
1471
|
} catch (e) {
|
|
1188
|
-
|
|
1472
|
+
console.log(chalk10.red(`\u2717 Registry: ${e instanceof Error ? e.message : String(e)}`));
|
|
1189
1473
|
process.exitCode = 1;
|
|
1190
1474
|
}
|
|
1191
1475
|
}
|
|
@@ -1262,7 +1546,7 @@ program.command("update").description("Re-fetch and overwrite installed skill(s)
|
|
|
1262
1546
|
}
|
|
1263
1547
|
});
|
|
1264
1548
|
program.command("doctor").description("Check Node, config, and sync registry checkout").option("-C, --cwd <path>", "Project root", process.cwd()).action(async (opts) => {
|
|
1265
|
-
const cwd =
|
|
1549
|
+
const cwd = resolve4(opts.cwd ?? process.cwd());
|
|
1266
1550
|
try {
|
|
1267
1551
|
await runDoctor(cwd);
|
|
1268
1552
|
} catch {
|