pi-agent-toolkit 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +375 -229
  2. package/package.json +5 -3
package/dist/index.js CHANGED
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+ import { dirname as dirname3, resolve as resolve5 } from "path";
6
+ import { fileURLToPath as fileURLToPath2 } from "url";
4
7
  import { defineCommand, runMain } from "citty";
5
8
 
6
9
  // src/commands/install.ts
@@ -185,8 +188,7 @@ var extensions = [
185
188
  group: "tools",
186
189
  description: "Intercepts pip/python calls and redirects to uv",
187
190
  method: "copy",
188
- source: "extensions/uv.ts",
189
- recommends: ["intercepted-commands"]
191
+ source: "extensions/uv.ts"
190
192
  },
191
193
  {
192
194
  name: "execute-command",
@@ -541,13 +543,15 @@ function getByCategory(category) {
541
543
  return registry.filter((c) => c.category === category);
542
544
  }
543
545
  function getExtensionGroups() {
544
- const groups = {};
545
- for (const ext of getByCategory("extensions")) {
546
- const group = ext.group ?? "tools";
547
- if (!groups[group]) groups[group] = [];
548
- groups[group].push(ext);
549
- }
550
- return groups;
546
+ return getByCategory("extensions").reduce(
547
+ (acc, ext) => {
548
+ const key = ext.group ?? "tools";
549
+ if (!acc[key]) acc[key] = [];
550
+ acc[key].push(ext);
551
+ return acc;
552
+ },
553
+ {}
554
+ );
551
555
  }
552
556
  var GROUP_LABELS = {
553
557
  safety: "Safety",
@@ -561,19 +565,21 @@ var GROUP_LABELS = {
561
565
 
562
566
  // src/lib/warnings.ts
563
567
  import * as p from "@clack/prompts";
564
- async function checkRecommendations(selected) {
568
+ function findMissingRecommendations(selected) {
565
569
  const selectedNames = new Set(selected.map((c) => c.name));
566
570
  const warnings = [];
567
571
  for (const component of selected) {
568
572
  if (!component.recommends) continue;
569
573
  for (const rec of component.recommends) {
570
574
  if (!selectedNames.has(rec)) {
571
- warnings.push(
572
- `${component.name} works best with ${rec}, which you didn't select.`
573
- );
575
+ warnings.push(`${component.name} works best with ${rec}, which you didn't select.`);
574
576
  }
575
577
  }
576
578
  }
579
+ return warnings;
580
+ }
581
+ async function checkRecommendations(selected) {
582
+ const warnings = findMissingRecommendations(selected);
577
583
  if (warnings.length === 0) return true;
578
584
  p.log.warn("Some selected components have recommendations:");
579
585
  for (const warning of warnings) {
@@ -594,12 +600,11 @@ async function checkRecommendations(selected) {
594
600
  import {
595
601
  cpSync,
596
602
  existsSync as existsSync2,
597
- mkdirSync as mkdirSync2,
598
- symlinkSync,
599
603
  lstatSync,
600
- readlinkSync,
604
+ mkdirSync as mkdirSync2,
601
605
  readdirSync,
602
606
  statSync,
607
+ symlinkSync,
603
608
  unlinkSync
604
609
  } from "fs";
605
610
  import { resolve as resolve2, basename } from "path";
@@ -635,51 +640,64 @@ function emptyManifest() {
635
640
  updatedAt: ""
636
641
  };
637
642
  }
638
- function readManifest() {
639
- if (!existsSync(MANIFEST_PATH)) return emptyManifest();
643
+ function readManifest(path = MANIFEST_PATH) {
644
+ if (!existsSync(path)) return emptyManifest();
640
645
  try {
641
- const raw = readFileSync(MANIFEST_PATH, "utf-8");
642
- return JSON.parse(raw);
646
+ const raw = readFileSync(path, "utf-8");
647
+ const parsed = JSON.parse(raw);
648
+ const defaults = emptyManifest();
649
+ const installed = parsed.installed ?? {};
650
+ return {
651
+ version: parsed.version ?? defaults.version,
652
+ installed: {
653
+ extensions: installed.extensions ?? defaults.installed.extensions,
654
+ skills: {
655
+ bundled: installed.skills?.bundled ?? defaults.installed.skills.bundled,
656
+ external: installed.skills?.external ?? defaults.installed.skills.external
657
+ },
658
+ packages: installed.packages ?? defaults.installed.packages,
659
+ configs: installed.configs ?? defaults.installed.configs
660
+ },
661
+ installedAt: parsed.installedAt ?? defaults.installedAt,
662
+ updatedAt: parsed.updatedAt ?? defaults.updatedAt
663
+ };
643
664
  } catch {
644
665
  return emptyManifest();
645
666
  }
646
667
  }
647
- function writeManifest(manifest) {
648
- const dir = dirname2(MANIFEST_PATH);
649
- mkdirSync(dir, { recursive: true });
650
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + "\n");
668
+ function writeManifest(manifest, path = MANIFEST_PATH) {
669
+ mkdirSync(dirname2(path), { recursive: true });
670
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
651
671
  }
652
- function recordInstall(names, category, cliVersion) {
653
- const manifest = readManifest();
672
+ function getList(manifest, category) {
673
+ switch (category) {
674
+ case "extensions":
675
+ return manifest.installed.extensions;
676
+ case "skills-bundled":
677
+ return manifest.installed.skills.bundled;
678
+ case "skills-external":
679
+ return manifest.installed.skills.external;
680
+ case "packages":
681
+ return manifest.installed.packages;
682
+ case "configs":
683
+ return manifest.installed.configs;
684
+ default:
685
+ return null;
686
+ }
687
+ }
688
+ function recordInstall(names, category, cliVersion, path = MANIFEST_PATH) {
689
+ const manifest = readManifest(path);
654
690
  const now = (/* @__PURE__ */ new Date()).toISOString();
655
691
  if (!manifest.installedAt) manifest.installedAt = now;
656
692
  manifest.updatedAt = now;
657
693
  manifest.version = cliVersion;
658
- for (const name of names) {
659
- switch (category) {
660
- case "extensions":
661
- if (!manifest.installed.extensions.includes(name))
662
- manifest.installed.extensions.push(name);
663
- break;
664
- case "skills-bundled":
665
- if (!manifest.installed.skills.bundled.includes(name))
666
- manifest.installed.skills.bundled.push(name);
667
- break;
668
- case "skills-external":
669
- if (!manifest.installed.skills.external.includes(name))
670
- manifest.installed.skills.external.push(name);
671
- break;
672
- case "packages":
673
- if (!manifest.installed.packages.includes(name))
674
- manifest.installed.packages.push(name);
675
- break;
676
- case "configs":
677
- if (!manifest.installed.configs.includes(name))
678
- manifest.installed.configs.push(name);
679
- break;
694
+ const list2 = getList(manifest, category);
695
+ if (list2) {
696
+ for (const name of names) {
697
+ if (!list2.includes(name)) list2.push(name);
680
698
  }
681
699
  }
682
- writeManifest(manifest);
700
+ writeManifest(manifest, path);
683
701
  }
684
702
 
685
703
  // src/lib/installer.ts
@@ -712,42 +730,24 @@ function resolveTarget(component) {
712
730
  return resolve2(PI_AGENT_DIR, component.name);
713
731
  }
714
732
  }
715
- function copyComponent(source, target, isDirectory) {
716
- const targetDir = isDirectory ? resolve2(target, "..") : resolve2(target, "..");
717
- mkdirSync2(targetDir, { recursive: true });
718
- if (isDirectory) {
719
- cpSync(source, target, { recursive: true });
720
- } else {
721
- cpSync(source, target);
722
- }
733
+ function copyComponent(source, target) {
734
+ mkdirSync2(resolve2(target, ".."), { recursive: true });
735
+ cpSync(source, target, { recursive: true });
723
736
  }
724
737
  function linkComponent(source, target) {
725
738
  const targetDir = resolve2(target, "..");
726
739
  mkdirSync2(targetDir, { recursive: true });
727
- if (existsSync2(target) || isSymlink(target)) {
728
- unlinkSync(target);
729
- }
740
+ if (targetExists(target)) unlinkSync(target);
730
741
  symlinkSync(source, target);
731
742
  }
732
- function isSymlink(path) {
743
+ function targetExists(path) {
733
744
  try {
734
745
  lstatSync(path);
735
- return lstatSync(path).isSymbolicLink();
746
+ return true;
736
747
  } catch {
737
748
  return false;
738
749
  }
739
750
  }
740
- function targetExists(target) {
741
- if (isSymlink(target)) {
742
- try {
743
- readlinkSync(target);
744
- return true;
745
- } catch {
746
- return false;
747
- }
748
- }
749
- return existsSync2(target);
750
- }
751
751
  function installLocal(component, options) {
752
752
  const source = resolveSource(component, options);
753
753
  const target = resolveTarget(component);
@@ -762,7 +762,7 @@ function installLocal(component, options) {
762
762
  linkComponent(source, target);
763
763
  return { success: true, message: "linked" };
764
764
  } else {
765
- copyComponent(source, target, component.isDirectory ?? false);
765
+ copyComponent(source, target);
766
766
  return { success: true, message: "copied" };
767
767
  }
768
768
  } catch (err) {
@@ -810,10 +810,10 @@ async function installComponents(components, options) {
810
810
  mkdirSync2(PI_EXTENSIONS_DIR, { recursive: true });
811
811
  mkdirSync2(PI_SKILLS_DIR, { recursive: true });
812
812
  mkdirSync2(AGENTS_SKILLS_DIR, { recursive: true });
813
- const spinner2 = p2.spinner();
813
+ const spinner3 = p2.spinner();
814
814
  const results = [];
815
815
  for (const component of components) {
816
- spinner2.start(`Installing ${component.name}...`);
816
+ spinner3.start(`Installing ${component.name}...`);
817
817
  let result;
818
818
  switch (component.method) {
819
819
  case "copy":
@@ -831,9 +831,9 @@ async function installComponents(components, options) {
831
831
  }
832
832
  results.push({ name: component.name, ...result });
833
833
  if (result.success) {
834
- spinner2.stop(`${component.name}: ${result.message}`);
834
+ spinner3.stop(`${component.name}: ${result.message}`);
835
835
  } else {
836
- spinner2.stop(`${component.name}: FAILED - ${result.message}`);
836
+ spinner3.stop(`${component.name}: FAILED - ${result.message}`);
837
837
  }
838
838
  }
839
839
  const successByCategory = /* @__PURE__ */ new Map();
@@ -841,19 +841,17 @@ async function installComponents(components, options) {
841
841
  if (!r.success) continue;
842
842
  const component = components.find((c) => c.name === r.name);
843
843
  if (!component) continue;
844
- const cat = component.category;
845
- if (!successByCategory.has(cat)) successByCategory.set(cat, []);
846
- successByCategory.get(cat).push(r.name);
844
+ const list2 = successByCategory.get(component.category) ?? [];
845
+ list2.push(r.name);
846
+ successByCategory.set(component.category, list2);
847
847
  }
848
848
  for (const [category, names] of successByCategory) {
849
849
  recordInstall(names, category, options.cliVersion);
850
850
  }
851
851
  const succeeded = results.filter((r) => r.success).length;
852
- const failed = results.filter((r) => !r.success).length;
852
+ const failed = results.length - succeeded;
853
853
  if (failed > 0) {
854
- p2.log.warn(
855
- `Installed ${succeeded}/${results.length} components. ${failed} failed:`
856
- );
854
+ p2.log.warn(`Installed ${succeeded}/${results.length} components. ${failed} failed:`);
857
855
  for (const r of results.filter((r2) => !r2.success)) {
858
856
  p2.log.error(` ${r.name}: ${r.message}`);
859
857
  }
@@ -868,130 +866,82 @@ function installExtensionDeps() {
868
866
  for (const entry of entries) {
869
867
  const fullPath = resolve2(extDir, entry);
870
868
  try {
871
- if (statSync(fullPath).isDirectory() && existsSync2(resolve2(fullPath, "package.json"))) {
872
- p2.log.info(`Installing dependencies for ${entry}...`);
873
- execSync("npm install --silent", { cwd: fullPath, stdio: "pipe" });
869
+ if (!statSync(fullPath).isDirectory() || !existsSync2(resolve2(fullPath, "package.json"))) {
870
+ continue;
874
871
  }
875
872
  } catch {
873
+ continue;
874
+ }
875
+ try {
876
+ p2.log.info(`Installing dependencies for ${entry}...`);
877
+ execSync("npm install --silent", { cwd: fullPath, stdio: "pipe" });
878
+ } catch (err) {
879
+ const msg = err instanceof Error ? err.message : String(err);
880
+ p2.log.error(`Failed to install dependencies for ${entry}: ${msg}`);
876
881
  }
877
882
  }
878
883
  }
879
884
 
880
885
  // src/commands/install.ts
881
- function componentOptions(components) {
882
- return components.map((c) => ({
883
- value: c.name,
884
- label: c.name,
885
- hint: c.description
886
- }));
887
- }
888
- async function interactivePicker() {
889
- const selected = [];
890
- const groups = getExtensionGroups();
891
- const extensionOptions = [];
892
- for (const group of Object.keys(GROUP_LABELS)) {
893
- const components = groups[group];
894
- if (!components?.length) continue;
895
- extensionOptions.push({
896
- value: `__separator_${group}`,
897
- label: pc.dim(`--- ${GROUP_LABELS[group]} ---`),
898
- hint: ""
899
- });
900
- extensionOptions.push(...componentOptions(components));
901
- }
902
- const extResult = await p3.multiselect({
903
- message: "Select extensions to install:",
904
- options: extensionOptions.filter((o) => !o.value.startsWith("__separator")),
886
+ async function selectComponents(message, components) {
887
+ if (components.length === 0) return [];
888
+ const result = await p3.multiselect({
889
+ message,
890
+ options: components.map((c) => ({
891
+ value: c.name,
892
+ label: c.name,
893
+ hint: c.description
894
+ })),
905
895
  required: false
906
896
  });
907
- if (p3.isCancel(extResult)) {
897
+ if (p3.isCancel(result)) {
908
898
  p3.cancel("Installation cancelled.");
909
899
  process.exit(0);
910
900
  }
911
- selected.push(...extResult);
912
- const bundledSkills2 = getByCategory("skills-bundled");
913
- if (bundledSkills2.length > 0) {
914
- const skillResult = await p3.multiselect({
915
- message: "Select bundled skills to install:",
916
- options: componentOptions(bundledSkills2),
917
- required: false
918
- });
919
- if (p3.isCancel(skillResult)) {
920
- p3.cancel("Installation cancelled.");
921
- process.exit(0);
922
- }
923
- selected.push(...skillResult);
924
- }
925
- const externalSkills2 = getByCategory("skills-external");
926
- if (externalSkills2.length > 0) {
927
- const extSkillResult = await p3.multiselect({
901
+ return result;
902
+ }
903
+ async function interactivePicker() {
904
+ const selected = [];
905
+ const steps = [
906
+ { message: "Select extensions to install:", components: getByCategory("extensions") },
907
+ { message: "Select bundled skills to install:", components: getByCategory("skills-bundled") },
908
+ {
928
909
  message: "Select external skills to install (fetched from source repos):",
929
- options: componentOptions(externalSkills2),
930
- required: false
931
- });
932
- if (p3.isCancel(extSkillResult)) {
933
- p3.cancel("Installation cancelled.");
934
- process.exit(0);
935
- }
936
- selected.push(...extSkillResult);
937
- }
938
- const pkgs = getByCategory("packages");
939
- if (pkgs.length > 0) {
940
- const pkgResult = await p3.multiselect({
941
- message: "Select pi packages to install:",
942
- options: componentOptions(pkgs),
943
- required: false
944
- });
945
- if (p3.isCancel(pkgResult)) {
946
- p3.cancel("Installation cancelled.");
947
- process.exit(0);
948
- }
949
- selected.push(...pkgResult);
950
- }
951
- const configs2 = getByCategory("configs");
952
- if (configs2.length > 0) {
953
- const configResult = await p3.multiselect({
954
- message: "Select starter configs to install (copied as templates, won't overwrite existing):",
955
- options: componentOptions(configs2),
956
- required: false
957
- });
958
- if (p3.isCancel(configResult)) {
959
- p3.cancel("Installation cancelled.");
960
- process.exit(0);
910
+ components: getByCategory("skills-external")
911
+ },
912
+ { message: "Select pi packages to install:", components: getByCategory("packages") },
913
+ {
914
+ message: "Select starter configs (copied as templates, won't overwrite existing):",
915
+ components: getByCategory("configs")
961
916
  }
962
- selected.push(...configResult);
917
+ ];
918
+ for (const step of steps) {
919
+ const names = await selectComponents(step.message, step.components);
920
+ selected.push(...names);
963
921
  }
964
922
  return registry.filter((c) => selected.includes(c.name));
965
923
  }
966
924
  function resolveFromFlags(args) {
967
- const names = /* @__PURE__ */ new Set();
968
- if (args.extensions) {
969
- for (const name of args.extensions) names.add(name);
970
- }
971
- if (args.skills) {
972
- for (const name of args.skills) names.add(name);
973
- }
974
- if (args.packages) {
975
- for (const name of args.packages) names.add(name);
976
- }
925
+ const names = /* @__PURE__ */ new Set([
926
+ ...args.extensions ?? [],
927
+ ...args.skills ?? [],
928
+ ...args.packages ?? []
929
+ ]);
977
930
  const resolved = [];
978
931
  const notFound = [];
979
932
  for (const name of names) {
980
933
  const component = registry.find((c) => c.name === name);
981
- if (component) {
982
- resolved.push(component);
983
- } else {
984
- notFound.push(name);
985
- }
934
+ if (component) resolved.push(component);
935
+ else notFound.push(name);
986
936
  }
987
937
  if (notFound.length > 0) {
988
938
  p3.log.warn(`Unknown components: ${notFound.join(", ")}`);
989
- p3.log.info('Run "pi-toolkit list" to see available components.');
939
+ p3.log.info('Run "pi-agent-toolkit list" to see available components.');
990
940
  }
991
941
  return resolved;
992
942
  }
993
943
  async function runInstall(args) {
994
- p3.intro(pc.bold("pi-toolkit install"));
944
+ p3.intro(pc.bold("pi-agent-toolkit install"));
995
945
  if (args.link && !args.repoPath) {
996
946
  p3.log.error("--link requires --repo-path to be set.");
997
947
  p3.log.info("Example: pi-toolkit install --link --repo-path ~/Code/pi-toolkit");
@@ -1011,18 +961,14 @@ async function runInstall(args) {
1011
961
  p3.outro("Done.");
1012
962
  return;
1013
963
  }
1014
- const extCount = components.filter((c) => c.category === "extensions").length;
1015
- const skillCount = components.filter(
1016
- (c) => c.category === "skills-bundled" || c.category === "skills-external"
1017
- ).length;
1018
- const pkgCount = components.filter((c) => c.category === "packages").length;
1019
- const cfgCount = components.filter((c) => c.category === "configs").length;
1020
- const parts = [];
1021
- if (extCount) parts.push(`${extCount} extension${extCount > 1 ? "s" : ""}`);
1022
- if (skillCount) parts.push(`${skillCount} skill${skillCount > 1 ? "s" : ""}`);
1023
- if (pkgCount) parts.push(`${pkgCount} package${pkgCount > 1 ? "s" : ""}`);
1024
- if (cfgCount) parts.push(`${cfgCount} config${cfgCount > 1 ? "s" : ""}`);
1025
- p3.log.info(`Will install: ${parts.join(", ")}`);
964
+ const counts = [
965
+ ["extension", components.filter((c) => c.category === "extensions").length],
966
+ ["skill", components.filter((c) => c.category.startsWith("skills-")).length],
967
+ ["package", components.filter((c) => c.category === "packages").length],
968
+ ["config", components.filter((c) => c.category === "configs").length]
969
+ ];
970
+ const summary = counts.filter(([, n]) => n > 0).map(([label, n]) => `${n} ${label}${n > 1 ? "s" : ""}`).join(", ");
971
+ p3.log.info(`Will install: ${summary}`);
1026
972
  if (args.link) {
1027
973
  p3.log.info(`Mode: symlink (repo: ${args.repoPath})`);
1028
974
  }
@@ -1054,7 +1000,7 @@ async function runInstall(args) {
1054
1000
  import pc2 from "picocolors";
1055
1001
  function runList() {
1056
1002
  console.log();
1057
- console.log(pc2.bold("pi-toolkit: available components"));
1003
+ console.log(pc2.bold("pi-agent-toolkit: available components"));
1058
1004
  console.log();
1059
1005
  console.log(pc2.bold(pc2.cyan("Extensions")));
1060
1006
  const groups = getExtensionGroups();
@@ -1103,7 +1049,7 @@ function runList() {
1103
1049
  }
1104
1050
 
1105
1051
  // src/commands/status.ts
1106
- import { existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
1052
+ import { existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync } from "fs";
1107
1053
  import { resolve as resolve3, basename as basename2 } from "path";
1108
1054
  import pc3 from "picocolors";
1109
1055
  function expectedPath(component) {
@@ -1121,8 +1067,7 @@ function expectedPath(component) {
1121
1067
  case "skills-external": {
1122
1068
  const piPath = resolve3(PI_SKILLS_DIR, component.name);
1123
1069
  const agentsPath = resolve3(AGENTS_SKILLS_DIR, component.name);
1124
- if (existsSync3(piPath) || isSymlink2(piPath)) return piPath;
1125
- if (existsSync3(agentsPath) || isSymlink2(agentsPath)) return agentsPath;
1070
+ if (existsSync3(piPath)) return piPath;
1126
1071
  return agentsPath;
1127
1072
  }
1128
1073
  case "packages":
@@ -1136,44 +1081,36 @@ function expectedPath(component) {
1136
1081
  return null;
1137
1082
  }
1138
1083
  }
1139
- function isSymlink2(path) {
1084
+ function checkFile(path) {
1140
1085
  try {
1141
- return lstatSync2(path).isSymbolicLink();
1086
+ const stats = lstatSync2(path);
1087
+ if (stats.isSymbolicLink()) {
1088
+ const target = readlinkSync(path);
1089
+ const dangling = !existsSync3(path);
1090
+ return {
1091
+ exists: !dangling,
1092
+ detail: dangling ? `dangling symlink -> ${target}` : `symlink -> ${target}`
1093
+ };
1094
+ }
1095
+ return { exists: true };
1142
1096
  } catch {
1143
- return false;
1097
+ return { exists: false };
1144
1098
  }
1145
1099
  }
1146
- function checkFile(path) {
1147
- if (isSymlink2(path)) {
1148
- const target = readlinkSync2(path);
1149
- const dangling = !existsSync3(path);
1150
- return {
1151
- exists: !dangling,
1152
- detail: dangling ? `dangling symlink -> ${target}` : `symlink -> ${target}`
1153
- };
1154
- }
1155
- return { exists: existsSync3(path) };
1156
- }
1157
1100
  function runStatus() {
1158
1101
  const manifest = readManifest();
1159
1102
  console.log();
1160
- console.log(pc3.bold("pi-toolkit status"));
1103
+ console.log(pc3.bold("pi-agent-toolkit status"));
1161
1104
  console.log();
1162
1105
  if (!manifest.installedAt) {
1163
1106
  console.log(pc3.dim("No pi-toolkit manifest found. Nothing has been installed yet."));
1164
- console.log(pc3.dim('Run "pi-toolkit install" to get started.'));
1107
+ console.log(pc3.dim('Run "pi-agent-toolkit install" to get started.'));
1165
1108
  console.log();
1166
1109
  return;
1167
1110
  }
1168
- console.log(
1169
- `${pc3.dim("CLI version:")} ${manifest.version || "unknown"}`
1170
- );
1171
- console.log(
1172
- `${pc3.dim("Installed at:")} ${manifest.installedAt}`
1173
- );
1174
- console.log(
1175
- `${pc3.dim("Updated at:")} ${manifest.updatedAt}`
1176
- );
1111
+ console.log(`${pc3.dim("CLI version:")} ${manifest.version || "unknown"}`);
1112
+ console.log(`${pc3.dim("Installed at:")} ${manifest.installedAt}`);
1113
+ console.log(`${pc3.dim("Updated at:")} ${manifest.updatedAt}`);
1177
1114
  console.log();
1178
1115
  const installedNames = /* @__PURE__ */ new Set([
1179
1116
  ...manifest.installed.extensions,
@@ -1241,21 +1178,205 @@ function runStatus() {
1241
1178
  }
1242
1179
  const missing = entries.filter((e) => e.status === "missing");
1243
1180
  if (missing.length > 0) {
1244
- console.log(
1245
- pc3.yellow(
1246
- `${missing.length} component(s) in manifest but missing from disk:`
1247
- )
1248
- );
1181
+ console.log(pc3.yellow(`${missing.length} component(s) in manifest but missing from disk:`));
1249
1182
  for (const m of missing) {
1250
1183
  console.log(pc3.yellow(` - ${m.name}`));
1251
1184
  }
1252
- console.log(pc3.dim('Re-run "pi-toolkit install" to restore them.'));
1185
+ console.log(pc3.dim('Re-run "pi-agent-toolkit install" to restore them.'));
1253
1186
  console.log();
1254
1187
  }
1255
1188
  }
1256
1189
 
1190
+ // src/commands/sync.ts
1191
+ import {
1192
+ cpSync as cpSync2,
1193
+ existsSync as existsSync4,
1194
+ lstatSync as lstatSync3,
1195
+ mkdirSync as mkdirSync3,
1196
+ readdirSync as readdirSync2,
1197
+ rmSync,
1198
+ statSync as statSync2,
1199
+ symlinkSync as symlinkSync2,
1200
+ unlinkSync as unlinkSync2
1201
+ } from "fs";
1202
+ import { resolve as resolve4 } from "path";
1203
+ import * as p4 from "@clack/prompts";
1204
+ import pc4 from "picocolors";
1205
+ function getExternalSkillNames() {
1206
+ return new Set(getByCategory("skills-external").map((c) => c.name));
1207
+ }
1208
+ function isSymlink(path) {
1209
+ try {
1210
+ return lstatSync3(path).isSymbolicLink();
1211
+ } catch {
1212
+ return false;
1213
+ }
1214
+ }
1215
+ function findUnmanaged(scanDir, targetDir, category, skipNames) {
1216
+ if (!existsSync4(scanDir)) return [];
1217
+ const items = [];
1218
+ const entries = readdirSync2(scanDir);
1219
+ for (const entry of entries) {
1220
+ const fullPath = resolve4(scanDir, entry);
1221
+ if (isSymlink(fullPath)) continue;
1222
+ if (skipNames.has(entry) || skipNames.has(entry.replace(/\.ts$/, ""))) continue;
1223
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
1224
+ let isDir;
1225
+ try {
1226
+ isDir = statSync2(fullPath).isDirectory();
1227
+ } catch {
1228
+ continue;
1229
+ }
1230
+ if (category === "extensions") {
1231
+ if (!isDir && !entry.endsWith(".ts")) continue;
1232
+ } else {
1233
+ if (!isDir) continue;
1234
+ }
1235
+ items.push({
1236
+ name: entry,
1237
+ sourcePath: fullPath,
1238
+ targetDir,
1239
+ category,
1240
+ isDirectory: isDir
1241
+ });
1242
+ }
1243
+ return items;
1244
+ }
1245
+ function absorbItem(item) {
1246
+ const targetPath = resolve4(item.targetDir, item.name);
1247
+ try {
1248
+ mkdirSync3(item.targetDir, { recursive: true });
1249
+ if (item.isDirectory) {
1250
+ cpSync2(item.sourcePath, targetPath, { recursive: true });
1251
+ rmSync(item.sourcePath, { recursive: true, force: true });
1252
+ } else {
1253
+ cpSync2(item.sourcePath, targetPath);
1254
+ unlinkSync2(item.sourcePath);
1255
+ }
1256
+ symlinkSync2(targetPath, item.sourcePath);
1257
+ return { success: true, message: "absorbed and symlinked" };
1258
+ } catch (err) {
1259
+ const msg = err instanceof Error ? err.message : String(err);
1260
+ return { success: false, message: msg };
1261
+ }
1262
+ }
1263
+ async function runSync(options) {
1264
+ p4.intro(pc4.bold("pi-agent-toolkit sync"));
1265
+ const repoPath = resolve4(options.repoPath);
1266
+ const dotfilesPath = resolve4(repoPath, "dotfiles");
1267
+ if (!existsSync4(dotfilesPath)) {
1268
+ p4.log.error(`dotfiles/ not found at ${dotfilesPath}`);
1269
+ p4.log.info("Make sure --repo-path points to your pi-toolkit repo clone.");
1270
+ process.exit(1);
1271
+ }
1272
+ p4.log.info(`Repo: ${repoPath}`);
1273
+ p4.log.info("Scanning for unmanaged extensions and skills...");
1274
+ const externalSkills2 = getExternalSkillNames();
1275
+ const knownExtensions = /* @__PURE__ */ new Set();
1276
+ const extDir = resolve4(dotfilesPath, "extensions");
1277
+ if (existsSync4(extDir)) {
1278
+ for (const entry of readdirSync2(extDir)) {
1279
+ knownExtensions.add(entry);
1280
+ }
1281
+ }
1282
+ const knownAgentSkills = /* @__PURE__ */ new Set();
1283
+ const agentSkillsDir = resolve4(dotfilesPath, "agent-skills");
1284
+ if (existsSync4(agentSkillsDir)) {
1285
+ for (const entry of readdirSync2(agentSkillsDir)) {
1286
+ knownAgentSkills.add(entry);
1287
+ }
1288
+ }
1289
+ const knownGlobalSkills = /* @__PURE__ */ new Set();
1290
+ const globalSkillsDir = resolve4(dotfilesPath, "global-skills");
1291
+ if (existsSync4(globalSkillsDir)) {
1292
+ for (const entry of readdirSync2(globalSkillsDir)) {
1293
+ knownGlobalSkills.add(entry);
1294
+ }
1295
+ }
1296
+ const found = [
1297
+ ...findUnmanaged(
1298
+ PI_EXTENSIONS_DIR,
1299
+ resolve4(dotfilesPath, "extensions"),
1300
+ "extensions",
1301
+ knownExtensions
1302
+ ),
1303
+ ...findUnmanaged(
1304
+ PI_SKILLS_DIR,
1305
+ resolve4(dotfilesPath, "agent-skills"),
1306
+ "agent-skills",
1307
+ /* @__PURE__ */ new Set([...knownAgentSkills, ...externalSkills2])
1308
+ ),
1309
+ ...findUnmanaged(
1310
+ AGENTS_SKILLS_DIR,
1311
+ resolve4(dotfilesPath, "global-skills"),
1312
+ "global-skills",
1313
+ /* @__PURE__ */ new Set([...knownGlobalSkills, ...externalSkills2])
1314
+ )
1315
+ ];
1316
+ if (found.length === 0) {
1317
+ p4.log.success("No unmanaged extensions or skills found. Everything is in sync.");
1318
+ p4.outro("Done.");
1319
+ return;
1320
+ }
1321
+ p4.log.info(`Found ${found.length} unmanaged item(s):`);
1322
+ for (const item of found) {
1323
+ const suffix = item.isDirectory ? "/" : "";
1324
+ p4.log.message(` ${pc4.yellow(item.name + suffix)} ${pc4.dim(`(${item.category})`)}`);
1325
+ }
1326
+ let toAbsorb;
1327
+ if (options.all) {
1328
+ toAbsorb = found;
1329
+ } else {
1330
+ const selected = await p4.multiselect({
1331
+ message: "Select items to absorb into the repo:",
1332
+ options: found.map((item) => ({
1333
+ value: item.name,
1334
+ label: item.name + (item.isDirectory ? "/" : ""),
1335
+ hint: `${item.category} -> dotfiles/${item.category}/${item.name}`
1336
+ })),
1337
+ required: false
1338
+ });
1339
+ if (p4.isCancel(selected)) {
1340
+ p4.cancel("Sync cancelled.");
1341
+ process.exit(0);
1342
+ }
1343
+ const selectedNames = new Set(selected);
1344
+ toAbsorb = found.filter((item) => selectedNames.has(item.name));
1345
+ }
1346
+ if (toAbsorb.length === 0) {
1347
+ p4.log.warn("Nothing selected to absorb.");
1348
+ p4.outro("Done.");
1349
+ return;
1350
+ }
1351
+ const spinner3 = p4.spinner();
1352
+ let succeeded = 0;
1353
+ let failed = 0;
1354
+ for (const item of toAbsorb) {
1355
+ spinner3.start(`Absorbing ${item.name}...`);
1356
+ const result = absorbItem(item);
1357
+ if (result.success) {
1358
+ spinner3.stop(`${item.name}: ${result.message}`);
1359
+ succeeded++;
1360
+ } else {
1361
+ spinner3.stop(`${item.name}: FAILED - ${result.message}`);
1362
+ failed++;
1363
+ }
1364
+ }
1365
+ if (failed > 0) {
1366
+ p4.log.warn(`Absorbed ${succeeded}/${toAbsorb.length}. ${failed} failed.`);
1367
+ } else {
1368
+ p4.log.success(`All ${succeeded} item(s) absorbed into the repo.`);
1369
+ }
1370
+ p4.outro(
1371
+ pc4.green("Next steps: review the new files in dotfiles/, add to registry.ts, then commit.")
1372
+ );
1373
+ }
1374
+
1257
1375
  // src/index.ts
1258
- var CLI_VERSION = "0.1.0";
1376
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1377
+ var CLI_VERSION = JSON.parse(
1378
+ readFileSync2(resolve5(__dirname2, "..", "package.json"), "utf-8")
1379
+ ).version;
1259
1380
  var install = defineCommand({
1260
1381
  meta: {
1261
1382
  name: "install",
@@ -1329,6 +1450,30 @@ var status = defineCommand({
1329
1450
  runStatus();
1330
1451
  }
1331
1452
  });
1453
+ var sync = defineCommand({
1454
+ meta: {
1455
+ name: "sync",
1456
+ description: "Absorb unmanaged extensions and skills from pi into the repo"
1457
+ },
1458
+ args: {
1459
+ "repo-path": {
1460
+ type: "string",
1461
+ description: "Path to local pi-toolkit repo clone",
1462
+ required: true
1463
+ },
1464
+ all: {
1465
+ type: "boolean",
1466
+ description: "Absorb all unmanaged items without prompting",
1467
+ default: false
1468
+ }
1469
+ },
1470
+ run({ args }) {
1471
+ return runSync({
1472
+ repoPath: args["repo-path"],
1473
+ all: args.all
1474
+ });
1475
+ }
1476
+ });
1332
1477
  var main = defineCommand({
1333
1478
  meta: {
1334
1479
  name: "pi-agent-toolkit",
@@ -1338,7 +1483,8 @@ var main = defineCommand({
1338
1483
  subCommands: {
1339
1484
  install,
1340
1485
  list,
1341
- status
1486
+ status,
1487
+ sync
1342
1488
  }
1343
1489
  });
1344
1490
  runMain(main);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-agent-toolkit",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "CLI to selectively install curated extensions, skills, and configs for the pi coding agent",
5
5
  "keywords": [
6
6
  "pi",
@@ -27,11 +27,13 @@
27
27
  "scripts": {
28
28
  "build": "tsup",
29
29
  "dev": "tsup --watch",
30
+ "test": "node --experimental-strip-types --test src/**/*.test.ts",
31
+ "typecheck": "tsc",
30
32
  "prepack": "npm run build"
31
33
  },
32
34
  "dependencies": {
33
- "@clack/prompts": "^0.10.0",
34
- "citty": "^0.1.6",
35
+ "@clack/prompts": "^0.11.0",
36
+ "citty": "^0.2.1",
35
37
  "picocolors": "^1.1.1"
36
38
  },
37
39
  "devDependencies": {