skissue 0.1.17 → 0.1.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/entry.js CHANGED
@@ -9,7 +9,7 @@ import { resolve as resolve4 } from "node:path";
9
9
  // src/commands/init.ts
10
10
  import * as p2 from "@clack/prompts";
11
11
  import chalk2 from "chalk";
12
- import { existsSync as existsSync4 } from "node:fs";
12
+ import { existsSync as existsSync4, statSync as statSync3 } from "node:fs";
13
13
  import { mkdir as mkdir4 } from "node:fs/promises";
14
14
  import { relative, resolve as resolve3 } from "node:path";
15
15
 
@@ -350,6 +350,13 @@ import chalk from "chalk";
350
350
  import { existsSync as existsSync3, statSync as statSync2 } from "node:fs";
351
351
  import { mkdir as mkdir3, writeFile as writeFile2 } from "node:fs/promises";
352
352
  import { join as join3, resolve as resolve2 } from "node:path";
353
+
354
+ // src/commands/prompt-validators.ts
355
+ function requiredTrimmed(v) {
356
+ return String(v ?? "").trim() ? void 0 : "Required";
357
+ }
358
+
359
+ // src/commands/init-registry.ts
353
360
  function validateSkillId(raw) {
354
361
  const id = raw.trim();
355
362
  if (!id) return "Skill id is required";
@@ -357,6 +364,18 @@ function validateSkillId(raw) {
357
364
  if (id === "." || id === ".." || id.includes("..")) return "Invalid skill id";
358
365
  return void 0;
359
366
  }
367
+ function validateSkillIdPrompt(v) {
368
+ return validateSkillId(String(v ?? ""));
369
+ }
370
+ function registryDirectoryExists(root) {
371
+ const regDir = join3(root, "registry");
372
+ if (!existsSync3(regDir)) return false;
373
+ try {
374
+ return statSync2(regDir).isDirectory();
375
+ } catch {
376
+ return false;
377
+ }
378
+ }
360
379
  function registryLayoutExists(root) {
361
380
  const jsonPath = join3(root, "registry.json");
362
381
  const regDir = join3(root, "registry");
@@ -440,7 +459,7 @@ async function promptMinimalRegistryScaffold(root) {
440
459
  const idRaw = await p.text({
441
460
  message: "Sample skill id (folder name under registry/)",
442
461
  initialValue: "sample-skill",
443
- validate: (v) => validateSkillId(String(v ?? ""))
462
+ validate: validateSkillIdPrompt
444
463
  });
445
464
  if (p.isCancel(idRaw)) {
446
465
  return null;
@@ -494,6 +513,7 @@ async function runInitRegistry(cwd) {
494
513
  if (p.isCancel(where)) {
495
514
  p.cancel("Aborted.");
496
515
  process.exit(0);
516
+ return;
497
517
  }
498
518
  let root;
499
519
  if (where === "here") {
@@ -502,11 +522,12 @@ async function runInitRegistry(cwd) {
502
522
  const raw = await p.text({
503
523
  message: "Path to registry root (absolute or relative to current directory)",
504
524
  placeholder: "../my-skill-registry",
505
- validate: (v) => v?.trim() ? void 0 : "Required"
525
+ validate: requiredTrimmed
506
526
  });
507
527
  if (p.isCancel(raw)) {
508
528
  p.cancel("Aborted.");
509
529
  process.exit(0);
530
+ return;
510
531
  }
511
532
  root = resolve2(cwd, String(raw).trim());
512
533
  }
@@ -514,6 +535,7 @@ async function runInitRegistry(cwd) {
514
535
  if (!prompted) {
515
536
  p.cancel("Aborted. No files were changed.");
516
537
  process.exit(0);
538
+ return;
517
539
  }
518
540
  const { skillId, runGitInit, hadGit } = prompted;
519
541
  try {
@@ -561,6 +583,59 @@ Transport: ${mode}
561
583
  Branch: ${r.branch}
562
584
  skillsRoot: ${cfg.skillsRoot}`;
563
585
  }
586
+ async function runLocalBootstrapFlow(cwd, resolvedRoot) {
587
+ const atConsumerRoot = resolvedRoot === resolve3(cwd);
588
+ if (atConsumerRoot && !registryLayoutExists(resolvedRoot)) {
589
+ const okRoot = await p2.confirm({
590
+ message: "This adds registry.json and registry/ at your project root. Continue?",
591
+ initialValue: false
592
+ });
593
+ if (p2.isCancel(okRoot) || !okRoot) {
594
+ p2.cancel("Aborted.");
595
+ return { ok: false, exitCode: 0 };
596
+ }
597
+ }
598
+ const prompted = await promptMinimalRegistryScaffold(resolvedRoot);
599
+ if (!prompted) {
600
+ p2.cancel("Aborted. No files were changed.");
601
+ return { ok: false, exitCode: 0 };
602
+ }
603
+ let result;
604
+ try {
605
+ result = await scaffoldMinimalRegistry({
606
+ root: resolvedRoot,
607
+ skillId: prompted.skillId,
608
+ runGitInit: prompted.runGitInit
609
+ });
610
+ } catch (e) {
611
+ p2.cancel(e instanceof Error ? e.message : String(e));
612
+ return { ok: false, exitCode: 1 };
613
+ }
614
+ if (!prompted.hadGit && !result.gitInitRan) {
615
+ p2.note(
616
+ "This folder is not a git repository. Run `git init` before using it as a local skissue registry.",
617
+ chalk2.yellow("Heads up")
618
+ );
619
+ }
620
+ const branchDefault = await gitBranchShowCurrent(resolvedRoot) ?? "main";
621
+ const branch = await p2.text({
622
+ message: "Branch label (stored in config; installs use git HEAD from that directory)",
623
+ initialValue: branchDefault,
624
+ validate: requiredTrimmed
625
+ });
626
+ if (p2.isCancel(branch)) {
627
+ p2.cancel("Aborted.");
628
+ return { ok: false, exitCode: 0 };
629
+ }
630
+ const cfg = ConfigSchema.parse({
631
+ registry: {
632
+ path: localRegistryPathForConfig(cwd, resolvedRoot),
633
+ branch: String(branch).trim()
634
+ },
635
+ skillsRoot: ".agents/skills"
636
+ });
637
+ return { ok: true, cfg, bootstrappedSkillId: result.skillId };
638
+ }
564
639
  async function runInit(cwd) {
565
640
  p2.intro(chalk2.bold("skissue init"));
566
641
  const existingPath = configPath(cwd);
@@ -626,100 +701,93 @@ async function runInit(cwd) {
626
701
  message: "Path to registry repo root (relative to this project or absolute). Must contain registry/ and be a git repository.",
627
702
  placeholder: ".",
628
703
  initialValue: ".",
629
- validate: (v) => v?.trim() ? void 0 : "Required"
704
+ validate: requiredTrimmed
630
705
  });
631
706
  if (p2.isCancel(regPath)) {
632
707
  p2.cancel("Aborted.");
633
708
  process.exit(0);
634
709
  }
635
710
  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);
711
+ if (!existsSync4(resolvedExisting) || !statSync3(resolvedExisting).isDirectory()) {
712
+ p2.cancel(`Path does not exist or is not a directory: ${resolvedExisting}`);
713
+ process.exitCode = 1;
714
+ return;
715
+ }
716
+ if (!registryDirectoryExists(resolvedExisting)) {
717
+ const pathLabel = String(regPath).trim() || ".";
718
+ const bootstrapHere = await p2.confirm({
719
+ message: `${chalk2.yellow("No registry/ directory")} at ${chalk2.cyan(pathLabel)}. Bootstrap a minimal sample registry (registry.json + registry/<skill>) here?`,
720
+ initialValue: true
721
+ });
722
+ if (p2.isCancel(bootstrapHere)) {
723
+ p2.cancel("Aborted.");
724
+ process.exit(0);
725
+ }
726
+ if (!bootstrapHere) {
727
+ p2.cancel(
728
+ "Aborted. Create a registry/ folder in that directory, or run skissue init and choose Bootstrap a minimal sample registry."
729
+ );
730
+ process.exit(0);
731
+ }
732
+ const outcome = await runLocalBootstrapFlow(cwd, resolvedExisting);
733
+ if (!outcome.ok) {
734
+ if (outcome.exitCode === 1) {
735
+ process.exitCode = 1;
736
+ } else {
737
+ process.exit(0);
738
+ }
739
+ return;
740
+ }
741
+ cfg = outcome.cfg;
742
+ bootstrappedSkillId = outcome.bootstrappedSkillId;
743
+ } else {
744
+ const branchDefault = await gitBranchShowCurrent(resolvedExisting) ?? "main";
745
+ const branch = await p2.text({
746
+ message: "Branch label (stored in config; installs use git HEAD from that directory)",
747
+ initialValue: branchDefault,
748
+ validate: requiredTrimmed
749
+ });
750
+ if (p2.isCancel(branch)) {
751
+ p2.cancel("Aborted.");
752
+ process.exit(0);
753
+ }
754
+ cfg = ConfigSchema.parse({
755
+ registry: {
756
+ path: String(regPath).trim(),
757
+ branch: String(branch).trim()
758
+ },
759
+ skillsRoot: ".agents/skills"
760
+ });
645
761
  }
646
- cfg = ConfigSchema.parse({
647
- registry: {
648
- path: String(regPath).trim(),
649
- branch: String(branch).trim()
650
- },
651
- skillsRoot: ".agents/skills"
652
- });
653
762
  } else {
654
763
  const pathRaw = await p2.text({
655
764
  message: "Path for the new registry root (relative to this project or absolute)",
656
765
  placeholder: "./skill-registry",
657
766
  initialValue: "./skill-registry",
658
- validate: (v) => v?.trim() ? void 0 : "Required"
767
+ validate: requiredTrimmed
659
768
  });
660
769
  if (p2.isCancel(pathRaw)) {
661
770
  p2.cancel("Aborted.");
662
771
  process.exit(0);
663
772
  }
664
773
  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.");
774
+ const outcome = await runLocalBootstrapFlow(cwd, resolvedRoot);
775
+ if (!outcome.ok) {
776
+ if (outcome.exitCode === 1) {
777
+ process.exitCode = 1;
778
+ } else {
673
779
  process.exit(0);
674
780
  }
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
781
  return;
692
782
  }
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
- });
783
+ cfg = outcome.cfg;
784
+ bootstrappedSkillId = outcome.bootstrappedSkillId;
717
785
  }
718
786
  } else {
719
787
  const owner = await p2.text({
720
788
  message: "GitHub registry owner (org or user)",
721
789
  placeholder: "acme",
722
- validate: (v) => v?.trim() ? void 0 : "Required"
790
+ validate: requiredTrimmed
723
791
  });
724
792
  if (p2.isCancel(owner)) {
725
793
  p2.cancel("Aborted.");
@@ -728,7 +796,7 @@ async function runInit(cwd) {
728
796
  const repo = await p2.text({
729
797
  message: "Registry repository name",
730
798
  placeholder: "skill-registry",
731
- validate: (v) => v?.trim() ? void 0 : "Required"
799
+ validate: requiredTrimmed
732
800
  });
733
801
  if (p2.isCancel(repo)) {
734
802
  p2.cancel("Aborted.");
@@ -737,7 +805,7 @@ async function runInit(cwd) {
737
805
  const branch = await p2.text({
738
806
  message: "Branch to track",
739
807
  initialValue: "main",
740
- validate: (v) => v?.trim() ? void 0 : "Required"
808
+ validate: requiredTrimmed
741
809
  });
742
810
  if (p2.isCancel(branch)) {
743
811
  p2.cancel("Aborted.");
@@ -755,7 +823,7 @@ async function runInit(cwd) {
755
823
  const skillsRoot = await p2.text({
756
824
  message: "Install skills under (relative to project root)",
757
825
  initialValue: cfg.skillsRoot,
758
- validate: (v) => v?.trim() ? void 0 : "Required"
826
+ validate: requiredTrimmed
759
827
  });
760
828
  if (p2.isCancel(skillsRoot)) {
761
829
  p2.cancel("Aborted.");
@@ -1054,7 +1122,7 @@ async function runUpdate(cwd, skillId) {
1054
1122
  // src/commands/manage.ts
1055
1123
  import * as p3 from "@clack/prompts";
1056
1124
  import chalk9 from "chalk";
1057
- import readline from "node:readline";
1125
+ import { emitKeypressEvents } from "node:readline";
1058
1126
 
1059
1127
  // src/commands/banner.ts
1060
1128
  import chalk8 from "chalk";
@@ -1156,7 +1224,7 @@ async function promptMainMenuSelect(availableCount, installedCount) {
1156
1224
  };
1157
1225
  const { stdin } = process;
1158
1226
  if (stdin.isTTY) {
1159
- readline.emitKeypressEvents(stdin);
1227
+ emitKeypressEvents(stdin);
1160
1228
  stdin.prependListener("keypress", onCtrlC);
1161
1229
  }
1162
1230
  try {
@@ -1461,8 +1529,9 @@ async function runDoctor(cwd) {
1461
1529
  process.exitCode = 1;
1462
1530
  return;
1463
1531
  }
1464
- if (!isLocalRegistry(config)) {
1465
- console.log(chalk10.dim(`\u2022 ${authHint(config)}`));
1532
+ const hint = authHint(config);
1533
+ if (hint) {
1534
+ console.log(chalk10.dim(`\u2022 ${hint}`));
1466
1535
  }
1467
1536
  try {
1468
1537
  const { path: repoPath, head } = await ensureRegistryCheckout(cwd, config);
@@ -1545,7 +1614,7 @@ program.command("update").description("Re-fetch and overwrite installed skill(s)
1545
1614
  process.exitCode = 1;
1546
1615
  }
1547
1616
  });
1548
- program.command("doctor").description("Check Node, config, and sync registry checkout").option("-C, --cwd <path>", "Project root", process.cwd()).action(async (opts) => {
1617
+ program.command("doctor").description("Check Node, config, and sync registry checkout").option("-C, --cwd <path>", "Project root").action(async (opts) => {
1549
1618
  const cwd = resolve4(opts.cwd ?? process.cwd());
1550
1619
  try {
1551
1620
  await runDoctor(cwd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skissue",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -10,23 +10,29 @@ import { chmodSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
10
10
  import { dirname, join, sep } from "node:path";
11
11
  import { fileURLToPath } from "node:url";
12
12
 
13
- const scriptPath = fileURLToPath(import.meta.url);
14
- if (scriptPath.split(sep).includes("node_modules")) {
15
- process.exit(0);
16
- }
13
+ /**
14
+ * @param {string} scriptPath Absolute path to this file (or a test fixture path with the same layout).
15
+ */
16
+ export function runEnsureLocalBin(scriptPath) {
17
+ if (scriptPath.split(sep).includes("node_modules")) {
18
+ return;
19
+ }
17
20
 
18
- const root = dirname(dirname(scriptPath));
19
- const dist = join(root, "dist", "entry.js");
20
- const binDir = join(root, "node_modules", ".bin");
21
- const out = join(binDir, "skissue");
21
+ const root = dirname(dirname(scriptPath));
22
+ const dist = join(root, "dist", "entry.js");
23
+ const binDir = join(root, "node_modules", ".bin");
24
+ const out = join(binDir, "skissue");
22
25
 
23
- if (!existsSync(dist)) {
24
- process.exit(0);
25
- }
26
+ if (!existsSync(dist)) {
27
+ return;
28
+ }
26
29
 
27
- mkdirSync(binDir, { recursive: true });
28
- const content = `#!/usr/bin/env sh
30
+ mkdirSync(binDir, { recursive: true });
31
+ const content = `#!/usr/bin/env sh
29
32
  exec node "${dist}" "$@"
30
33
  `;
31
- writeFileSync(out, content, { mode: 0o755 });
32
- chmodSync(out, 0o755);
34
+ writeFileSync(out, content, { mode: 0o755 });
35
+ chmodSync(out, 0o755);
36
+ }
37
+
38
+ runEnsureLocalBin(fileURLToPath(import.meta.url));