pi-agent-toolkit 0.1.2 → 0.3.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 +441 -265
  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",
@@ -536,18 +538,22 @@ var registry = [
536
538
  ...externalSkills,
537
539
  ...packages,
538
540
  ...configs
541
+ // Placeholders: no items yet, but the categories are ready for content.
542
+ // Add prompts, agents, and themes entries here as they're created.
539
543
  ];
540
544
  function getByCategory(category) {
541
545
  return registry.filter((c) => c.category === category);
542
546
  }
543
547
  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;
548
+ return getByCategory("extensions").reduce(
549
+ (acc, ext) => {
550
+ const key = ext.group ?? "tools";
551
+ if (!acc[key]) acc[key] = [];
552
+ acc[key].push(ext);
553
+ return acc;
554
+ },
555
+ {}
556
+ );
551
557
  }
552
558
  var GROUP_LABELS = {
553
559
  safety: "Safety",
@@ -561,19 +567,21 @@ var GROUP_LABELS = {
561
567
 
562
568
  // src/lib/warnings.ts
563
569
  import * as p from "@clack/prompts";
564
- async function checkRecommendations(selected) {
570
+ function findMissingRecommendations(selected) {
565
571
  const selectedNames = new Set(selected.map((c) => c.name));
566
572
  const warnings = [];
567
573
  for (const component of selected) {
568
574
  if (!component.recommends) continue;
569
575
  for (const rec of component.recommends) {
570
576
  if (!selectedNames.has(rec)) {
571
- warnings.push(
572
- `${component.name} works best with ${rec}, which you didn't select.`
573
- );
577
+ warnings.push(`${component.name} works best with ${rec}, which you didn't select.`);
574
578
  }
575
579
  }
576
580
  }
581
+ return warnings;
582
+ }
583
+ async function checkRecommendations(selected) {
584
+ const warnings = findMissingRecommendations(selected);
577
585
  if (warnings.length === 0) return true;
578
586
  p.log.warn("Some selected components have recommendations:");
579
587
  for (const warning of warnings) {
@@ -594,12 +602,11 @@ async function checkRecommendations(selected) {
594
602
  import {
595
603
  cpSync,
596
604
  existsSync as existsSync2,
597
- mkdirSync as mkdirSync2,
598
- symlinkSync,
599
605
  lstatSync,
600
- readlinkSync,
606
+ mkdirSync as mkdirSync2,
601
607
  readdirSync,
602
608
  statSync,
609
+ symlinkSync,
603
610
  unlinkSync
604
611
  } from "fs";
605
612
  import { resolve as resolve2, basename } from "path";
@@ -618,6 +625,9 @@ var AGENTS_SKILLS_DIR = resolve(homedir(), ".agents", "skills");
618
625
  var MANIFEST_PATH = resolve(PI_AGENT_DIR, ".pi-toolkit.json");
619
626
  var PI_EXTENSIONS_DIR = resolve(PI_AGENT_DIR, "extensions");
620
627
  var PI_SKILLS_DIR = resolve(PI_AGENT_DIR, "skills");
628
+ var PI_PROMPTS_DIR = resolve(PI_AGENT_DIR, "prompts");
629
+ var PI_AGENTS_DIR = resolve(PI_AGENT_DIR, "agents");
630
+ var PI_THEMES_DIR = resolve(PI_AGENT_DIR, "themes");
621
631
 
622
632
  // src/lib/manifest.ts
623
633
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
@@ -628,6 +638,9 @@ function emptyManifest() {
628
638
  installed: {
629
639
  extensions: [],
630
640
  skills: { bundled: [], external: [] },
641
+ prompts: [],
642
+ agents: [],
643
+ themes: [],
631
644
  packages: [],
632
645
  configs: []
633
646
  },
@@ -635,51 +648,73 @@ function emptyManifest() {
635
648
  updatedAt: ""
636
649
  };
637
650
  }
638
- function readManifest() {
639
- if (!existsSync(MANIFEST_PATH)) return emptyManifest();
651
+ function readManifest(path = MANIFEST_PATH) {
652
+ if (!existsSync(path)) return emptyManifest();
640
653
  try {
641
- const raw = readFileSync(MANIFEST_PATH, "utf-8");
642
- return JSON.parse(raw);
654
+ const raw = readFileSync(path, "utf-8");
655
+ const parsed = JSON.parse(raw);
656
+ const defaults = emptyManifest();
657
+ const installed = parsed.installed ?? {};
658
+ return {
659
+ version: parsed.version ?? defaults.version,
660
+ installed: {
661
+ extensions: installed.extensions ?? defaults.installed.extensions,
662
+ skills: {
663
+ bundled: installed.skills?.bundled ?? defaults.installed.skills.bundled,
664
+ external: installed.skills?.external ?? defaults.installed.skills.external
665
+ },
666
+ prompts: installed.prompts ?? defaults.installed.prompts,
667
+ agents: installed.agents ?? defaults.installed.agents,
668
+ themes: installed.themes ?? defaults.installed.themes,
669
+ packages: installed.packages ?? defaults.installed.packages,
670
+ configs: installed.configs ?? defaults.installed.configs
671
+ },
672
+ installedAt: parsed.installedAt ?? defaults.installedAt,
673
+ updatedAt: parsed.updatedAt ?? defaults.updatedAt
674
+ };
643
675
  } catch {
644
676
  return emptyManifest();
645
677
  }
646
678
  }
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");
679
+ function writeManifest(manifest, path = MANIFEST_PATH) {
680
+ mkdirSync(dirname2(path), { recursive: true });
681
+ writeFileSync(path, JSON.stringify(manifest, null, 2) + "\n");
651
682
  }
652
- function recordInstall(names, category, cliVersion) {
653
- const manifest = readManifest();
683
+ function getList(manifest, category) {
684
+ switch (category) {
685
+ case "extensions":
686
+ return manifest.installed.extensions;
687
+ case "skills-bundled":
688
+ return manifest.installed.skills.bundled;
689
+ case "skills-external":
690
+ return manifest.installed.skills.external;
691
+ case "packages":
692
+ return manifest.installed.packages;
693
+ case "prompts":
694
+ return manifest.installed.prompts;
695
+ case "agents":
696
+ return manifest.installed.agents;
697
+ case "themes":
698
+ return manifest.installed.themes;
699
+ case "configs":
700
+ return manifest.installed.configs;
701
+ default:
702
+ return null;
703
+ }
704
+ }
705
+ function recordInstall(names, category, cliVersion, path = MANIFEST_PATH) {
706
+ const manifest = readManifest(path);
654
707
  const now = (/* @__PURE__ */ new Date()).toISOString();
655
708
  if (!manifest.installedAt) manifest.installedAt = now;
656
709
  manifest.updatedAt = now;
657
710
  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;
711
+ const list2 = getList(manifest, category);
712
+ if (list2) {
713
+ for (const name of names) {
714
+ if (!list2.includes(name)) list2.push(name);
680
715
  }
681
716
  }
682
- writeManifest(manifest);
717
+ writeManifest(manifest, path);
683
718
  }
684
719
 
685
720
  // src/lib/installer.ts
@@ -704,6 +739,14 @@ function resolveTarget(component) {
704
739
  }
705
740
  return resolve2(PI_SKILLS_DIR, component.name);
706
741
  }
742
+ case "prompts":
743
+ return resolve2(PI_PROMPTS_DIR, component.name);
744
+ case "agents":
745
+ return resolve2(PI_AGENTS_DIR, component.name);
746
+ case "themes": {
747
+ const themeName = (component.source ?? component.name).endsWith(".json") ? basename(component.source ?? component.name) : component.name + ".json";
748
+ return resolve2(PI_THEMES_DIR, themeName);
749
+ }
707
750
  case "configs": {
708
751
  const name = (component.source ?? component.name).replace(".template", "");
709
752
  return resolve2(PI_AGENT_DIR, name);
@@ -712,42 +755,24 @@ function resolveTarget(component) {
712
755
  return resolve2(PI_AGENT_DIR, component.name);
713
756
  }
714
757
  }
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
- }
758
+ function copyComponent(source, target) {
759
+ mkdirSync2(resolve2(target, ".."), { recursive: true });
760
+ cpSync(source, target, { recursive: true });
723
761
  }
724
762
  function linkComponent(source, target) {
725
763
  const targetDir = resolve2(target, "..");
726
764
  mkdirSync2(targetDir, { recursive: true });
727
- if (existsSync2(target) || isSymlink(target)) {
728
- unlinkSync(target);
729
- }
765
+ if (targetExists(target)) unlinkSync(target);
730
766
  symlinkSync(source, target);
731
767
  }
732
- function isSymlink(path) {
768
+ function targetExists(path) {
733
769
  try {
734
770
  lstatSync(path);
735
- return lstatSync(path).isSymbolicLink();
771
+ return true;
736
772
  } catch {
737
773
  return false;
738
774
  }
739
775
  }
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
776
  function installLocal(component, options) {
752
777
  const source = resolveSource(component, options);
753
778
  const target = resolveTarget(component);
@@ -762,7 +787,7 @@ function installLocal(component, options) {
762
787
  linkComponent(source, target);
763
788
  return { success: true, message: "linked" };
764
789
  } else {
765
- copyComponent(source, target, component.isDirectory ?? false);
790
+ copyComponent(source, target);
766
791
  return { success: true, message: "copied" };
767
792
  }
768
793
  } catch (err) {
@@ -806,14 +831,21 @@ function installViaPi(component) {
806
831
  }
807
832
  async function installComponents(components, options) {
808
833
  if (components.length === 0) return;
809
- mkdirSync2(PI_AGENT_DIR, { recursive: true });
810
- mkdirSync2(PI_EXTENSIONS_DIR, { recursive: true });
811
- mkdirSync2(PI_SKILLS_DIR, { recursive: true });
812
- mkdirSync2(AGENTS_SKILLS_DIR, { recursive: true });
813
- const spinner2 = p2.spinner();
834
+ for (const dir of [
835
+ PI_AGENT_DIR,
836
+ PI_EXTENSIONS_DIR,
837
+ PI_SKILLS_DIR,
838
+ PI_PROMPTS_DIR,
839
+ PI_AGENTS_DIR,
840
+ PI_THEMES_DIR,
841
+ AGENTS_SKILLS_DIR
842
+ ]) {
843
+ mkdirSync2(dir, { recursive: true });
844
+ }
845
+ const spinner3 = p2.spinner();
814
846
  const results = [];
815
847
  for (const component of components) {
816
- spinner2.start(`Installing ${component.name}...`);
848
+ spinner3.start(`Installing ${component.name}...`);
817
849
  let result;
818
850
  switch (component.method) {
819
851
  case "copy":
@@ -831,9 +863,9 @@ async function installComponents(components, options) {
831
863
  }
832
864
  results.push({ name: component.name, ...result });
833
865
  if (result.success) {
834
- spinner2.stop(`${component.name}: ${result.message}`);
866
+ spinner3.stop(`${component.name}: ${result.message}`);
835
867
  } else {
836
- spinner2.stop(`${component.name}: FAILED - ${result.message}`);
868
+ spinner3.stop(`${component.name}: FAILED - ${result.message}`);
837
869
  }
838
870
  }
839
871
  const successByCategory = /* @__PURE__ */ new Map();
@@ -841,19 +873,17 @@ async function installComponents(components, options) {
841
873
  if (!r.success) continue;
842
874
  const component = components.find((c) => c.name === r.name);
843
875
  if (!component) continue;
844
- const cat = component.category;
845
- if (!successByCategory.has(cat)) successByCategory.set(cat, []);
846
- successByCategory.get(cat).push(r.name);
876
+ const list2 = successByCategory.get(component.category) ?? [];
877
+ list2.push(r.name);
878
+ successByCategory.set(component.category, list2);
847
879
  }
848
880
  for (const [category, names] of successByCategory) {
849
881
  recordInstall(names, category, options.cliVersion);
850
882
  }
851
883
  const succeeded = results.filter((r) => r.success).length;
852
- const failed = results.filter((r) => !r.success).length;
884
+ const failed = results.length - succeeded;
853
885
  if (failed > 0) {
854
- p2.log.warn(
855
- `Installed ${succeeded}/${results.length} components. ${failed} failed:`
856
- );
886
+ p2.log.warn(`Installed ${succeeded}/${results.length} components. ${failed} failed:`);
857
887
  for (const r of results.filter((r2) => !r2.success)) {
858
888
  p2.log.error(` ${r.name}: ${r.message}`);
859
889
  }
@@ -868,130 +898,93 @@ function installExtensionDeps() {
868
898
  for (const entry of entries) {
869
899
  const fullPath = resolve2(extDir, entry);
870
900
  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" });
901
+ if (!statSync(fullPath).isDirectory() || !existsSync2(resolve2(fullPath, "package.json"))) {
902
+ continue;
874
903
  }
875
904
  } catch {
905
+ continue;
906
+ }
907
+ try {
908
+ p2.log.info(`Installing dependencies for ${entry}...`);
909
+ execSync("npm install --silent", { cwd: fullPath, stdio: "pipe" });
910
+ } catch (err) {
911
+ const msg = err instanceof Error ? err.message : String(err);
912
+ p2.log.error(`Failed to install dependencies for ${entry}: ${msg}`);
876
913
  }
877
914
  }
878
915
  }
879
916
 
880
917
  // 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")),
918
+ var SELECT_ALL = "__all__";
919
+ async function selectComponents(message, components) {
920
+ if (components.length === 0) return [];
921
+ const result = await p3.multiselect({
922
+ message,
923
+ options: [
924
+ { value: SELECT_ALL, label: "* Select all", hint: `all ${components.length} items` },
925
+ ...components.map((c) => ({
926
+ value: c.name,
927
+ label: c.name,
928
+ hint: c.description
929
+ }))
930
+ ],
905
931
  required: false
906
932
  });
907
- if (p3.isCancel(extResult)) {
933
+ if (p3.isCancel(result)) {
908
934
  p3.cancel("Installation cancelled.");
909
935
  process.exit(0);
910
936
  }
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);
937
+ const selected = result;
938
+ if (selected.includes(SELECT_ALL)) {
939
+ return components.map((c) => c.name);
924
940
  }
925
- const externalSkills2 = getByCategory("skills-external");
926
- if (externalSkills2.length > 0) {
927
- const extSkillResult = await p3.multiselect({
941
+ return selected;
942
+ }
943
+ async function interactivePicker() {
944
+ const selected = [];
945
+ const steps = [
946
+ { message: "Select extensions to install:", components: getByCategory("extensions") },
947
+ { message: "Select bundled skills to install:", components: getByCategory("skills-bundled") },
948
+ {
928
949
  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);
950
+ components: getByCategory("skills-external")
951
+ },
952
+ { message: "Select prompts to install:", components: getByCategory("prompts") },
953
+ { message: "Select agents to install:", components: getByCategory("agents") },
954
+ { message: "Select themes to install:", components: getByCategory("themes") },
955
+ { message: "Select pi packages to install:", components: getByCategory("packages") },
956
+ {
957
+ message: "Select starter configs (copied as templates, won't overwrite existing):",
958
+ components: getByCategory("configs")
961
959
  }
962
- selected.push(...configResult);
960
+ ];
961
+ for (const step of steps) {
962
+ const names = await selectComponents(step.message, step.components);
963
+ selected.push(...names);
963
964
  }
964
965
  return registry.filter((c) => selected.includes(c.name));
965
966
  }
966
967
  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
- }
968
+ const names = /* @__PURE__ */ new Set([
969
+ ...args.extensions ?? [],
970
+ ...args.skills ?? [],
971
+ ...args.packages ?? []
972
+ ]);
977
973
  const resolved = [];
978
974
  const notFound = [];
979
975
  for (const name of names) {
980
976
  const component = registry.find((c) => c.name === name);
981
- if (component) {
982
- resolved.push(component);
983
- } else {
984
- notFound.push(name);
985
- }
977
+ if (component) resolved.push(component);
978
+ else notFound.push(name);
986
979
  }
987
980
  if (notFound.length > 0) {
988
981
  p3.log.warn(`Unknown components: ${notFound.join(", ")}`);
989
- p3.log.info('Run "pi-toolkit list" to see available components.');
982
+ p3.log.info('Run "pi-agent-toolkit list" to see available components.');
990
983
  }
991
984
  return resolved;
992
985
  }
993
986
  async function runInstall(args) {
994
- p3.intro(pc.bold("pi-toolkit install"));
987
+ p3.intro(pc.bold("pi-agent-toolkit install"));
995
988
  if (args.link && !args.repoPath) {
996
989
  p3.log.error("--link requires --repo-path to be set.");
997
990
  p3.log.info("Example: pi-toolkit install --link --repo-path ~/Code/pi-toolkit");
@@ -1011,18 +1004,17 @@ async function runInstall(args) {
1011
1004
  p3.outro("Done.");
1012
1005
  return;
1013
1006
  }
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(", ")}`);
1007
+ const counts = [
1008
+ ["extension", components.filter((c) => c.category === "extensions").length],
1009
+ ["skill", components.filter((c) => c.category.startsWith("skills-")).length],
1010
+ ["prompt", components.filter((c) => c.category === "prompts").length],
1011
+ ["agent", components.filter((c) => c.category === "agents").length],
1012
+ ["theme", components.filter((c) => c.category === "themes").length],
1013
+ ["package", components.filter((c) => c.category === "packages").length],
1014
+ ["config", components.filter((c) => c.category === "configs").length]
1015
+ ];
1016
+ const summary = counts.filter(([, n]) => n > 0).map(([label, n]) => `${n} ${label}${n > 1 ? "s" : ""}`).join(", ");
1017
+ p3.log.info(`Will install: ${summary}`);
1026
1018
  if (args.link) {
1027
1019
  p3.log.info(`Mode: symlink (repo: ${args.repoPath})`);
1028
1020
  }
@@ -1052,9 +1044,20 @@ async function runInstall(args) {
1052
1044
 
1053
1045
  // src/commands/list.ts
1054
1046
  import pc2 from "picocolors";
1047
+ function printSection(category, title, subtitle) {
1048
+ const items = getByCategory(category);
1049
+ if (items.length === 0) return;
1050
+ const header = subtitle ? pc2.bold(pc2.cyan(title)) + pc2.dim(` (${subtitle})`) : pc2.bold(pc2.cyan(title));
1051
+ console.log(header);
1052
+ for (const c of items) {
1053
+ const suffix = c.remote ? pc2.dim(` [${c.remote}]`) : "";
1054
+ console.log(` ${pc2.green(c.name.padEnd(38))} ${pc2.dim(c.description)}${suffix}`);
1055
+ }
1056
+ console.log();
1057
+ }
1055
1058
  function runList() {
1056
1059
  console.log();
1057
- console.log(pc2.bold("pi-toolkit: available components"));
1060
+ console.log(pc2.bold("pi-agent-toolkit: available components"));
1058
1061
  console.log();
1059
1062
  console.log(pc2.bold(pc2.cyan("Extensions")));
1060
1063
  const groups = getExtensionGroups();
@@ -1067,43 +1070,17 @@ function runList() {
1067
1070
  }
1068
1071
  }
1069
1072
  console.log();
1070
- const bundled = getByCategory("skills-bundled");
1071
- if (bundled.length > 0) {
1072
- console.log(pc2.bold(pc2.cyan("Bundled Skills")));
1073
- for (const c of bundled) {
1074
- console.log(` ${pc2.green(c.name.padEnd(38))} ${pc2.dim(c.description)}`);
1075
- }
1076
- console.log();
1077
- }
1078
- const external = getByCategory("skills-external");
1079
- if (external.length > 0) {
1080
- console.log(pc2.bold(pc2.cyan("External Skills")) + pc2.dim(" (installed from source repos)"));
1081
- for (const c of external) {
1082
- const source = c.remote ? pc2.dim(` [${c.remote}]`) : "";
1083
- console.log(` ${pc2.green(c.name.padEnd(38))} ${pc2.dim(c.description)}${source}`);
1084
- }
1085
- console.log();
1086
- }
1087
- const pkgs = getByCategory("packages");
1088
- if (pkgs.length > 0) {
1089
- console.log(pc2.bold(pc2.cyan("Packages")) + pc2.dim(" (installed via pi install)"));
1090
- for (const c of pkgs) {
1091
- console.log(` ${pc2.green(c.name.padEnd(38))} ${pc2.dim(c.description)}`);
1092
- }
1093
- console.log();
1094
- }
1095
- const configs2 = getByCategory("configs");
1096
- if (configs2.length > 0) {
1097
- console.log(pc2.bold(pc2.cyan("Starter Configs")) + pc2.dim(" (copied as templates)"));
1098
- for (const c of configs2) {
1099
- console.log(` ${pc2.green(c.name.padEnd(38))} ${pc2.dim(c.description)}`);
1100
- }
1101
- console.log();
1102
- }
1073
+ printSection("skills-bundled", "Bundled Skills");
1074
+ printSection("skills-external", "External Skills", "installed from source repos");
1075
+ printSection("prompts", "Prompts", "custom prompt templates");
1076
+ printSection("agents", "Agents", "custom agent definitions");
1077
+ printSection("themes", "Themes", "TUI color themes");
1078
+ printSection("packages", "Packages", "installed via pi install");
1079
+ printSection("configs", "Starter Configs", "copied as templates");
1103
1080
  }
1104
1081
 
1105
1082
  // src/commands/status.ts
1106
- import { existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync as readlinkSync2 } from "fs";
1083
+ import { existsSync as existsSync3, lstatSync as lstatSync2, readlinkSync } from "fs";
1107
1084
  import { resolve as resolve3, basename as basename2 } from "path";
1108
1085
  import pc3 from "picocolors";
1109
1086
  function expectedPath(component) {
@@ -1121,10 +1098,17 @@ function expectedPath(component) {
1121
1098
  case "skills-external": {
1122
1099
  const piPath = resolve3(PI_SKILLS_DIR, component.name);
1123
1100
  const agentsPath = resolve3(AGENTS_SKILLS_DIR, component.name);
1124
- if (existsSync3(piPath) || isSymlink2(piPath)) return piPath;
1125
- if (existsSync3(agentsPath) || isSymlink2(agentsPath)) return agentsPath;
1101
+ if (existsSync3(piPath)) return piPath;
1126
1102
  return agentsPath;
1127
1103
  }
1104
+ case "prompts":
1105
+ return resolve3(PI_PROMPTS_DIR, component.name);
1106
+ case "agents":
1107
+ return resolve3(PI_AGENTS_DIR, component.name);
1108
+ case "themes": {
1109
+ const themeName = (component.source ?? component.name).endsWith(".json") ? basename2(component.source ?? component.name) : component.name + ".json";
1110
+ return resolve3(PI_THEMES_DIR, themeName);
1111
+ }
1128
1112
  case "packages":
1129
1113
  return null;
1130
1114
  // Can't easily check pi packages on disk
@@ -1136,49 +1120,44 @@ function expectedPath(component) {
1136
1120
  return null;
1137
1121
  }
1138
1122
  }
1139
- function isSymlink2(path) {
1123
+ function checkFile(path) {
1140
1124
  try {
1141
- return lstatSync2(path).isSymbolicLink();
1125
+ const stats = lstatSync2(path);
1126
+ if (stats.isSymbolicLink()) {
1127
+ const target = readlinkSync(path);
1128
+ const dangling = !existsSync3(path);
1129
+ return {
1130
+ exists: !dangling,
1131
+ detail: dangling ? `dangling symlink -> ${target}` : `symlink -> ${target}`
1132
+ };
1133
+ }
1134
+ return { exists: true };
1142
1135
  } catch {
1143
- return false;
1144
- }
1145
- }
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
- };
1136
+ return { exists: false };
1154
1137
  }
1155
- return { exists: existsSync3(path) };
1156
1138
  }
1157
1139
  function runStatus() {
1158
1140
  const manifest = readManifest();
1159
1141
  console.log();
1160
- console.log(pc3.bold("pi-toolkit status"));
1142
+ console.log(pc3.bold("pi-agent-toolkit status"));
1161
1143
  console.log();
1162
1144
  if (!manifest.installedAt) {
1163
1145
  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.'));
1146
+ console.log(pc3.dim('Run "pi-agent-toolkit install" to get started.'));
1165
1147
  console.log();
1166
1148
  return;
1167
1149
  }
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
- );
1150
+ console.log(`${pc3.dim("CLI version:")} ${manifest.version || "unknown"}`);
1151
+ console.log(`${pc3.dim("Installed at:")} ${manifest.installedAt}`);
1152
+ console.log(`${pc3.dim("Updated at:")} ${manifest.updatedAt}`);
1177
1153
  console.log();
1178
1154
  const installedNames = /* @__PURE__ */ new Set([
1179
1155
  ...manifest.installed.extensions,
1180
1156
  ...manifest.installed.skills.bundled,
1181
1157
  ...manifest.installed.skills.external,
1158
+ ...manifest.installed.prompts,
1159
+ ...manifest.installed.agents,
1160
+ ...manifest.installed.themes,
1182
1161
  ...manifest.installed.packages,
1183
1162
  ...manifest.installed.configs
1184
1163
  ]);
@@ -1215,6 +1194,9 @@ function runStatus() {
1215
1194
  { key: "extensions", label: "Extensions" },
1216
1195
  { key: "skills-bundled", label: "Bundled Skills" },
1217
1196
  { key: "skills-external", label: "External Skills" },
1197
+ { key: "prompts", label: "Prompts" },
1198
+ { key: "agents", label: "Agents" },
1199
+ { key: "themes", label: "Themes" },
1218
1200
  { key: "packages", label: "Packages" },
1219
1201
  { key: "configs", label: "Configs" }
1220
1202
  ];
@@ -1241,21 +1223,190 @@ function runStatus() {
1241
1223
  }
1242
1224
  const missing = entries.filter((e) => e.status === "missing");
1243
1225
  if (missing.length > 0) {
1244
- console.log(
1245
- pc3.yellow(
1246
- `${missing.length} component(s) in manifest but missing from disk:`
1247
- )
1248
- );
1226
+ console.log(pc3.yellow(`${missing.length} component(s) in manifest but missing from disk:`));
1249
1227
  for (const m of missing) {
1250
1228
  console.log(pc3.yellow(` - ${m.name}`));
1251
1229
  }
1252
- console.log(pc3.dim('Re-run "pi-toolkit install" to restore them.'));
1230
+ console.log(pc3.dim('Re-run "pi-agent-toolkit install" to restore them.'));
1253
1231
  console.log();
1254
1232
  }
1255
1233
  }
1256
1234
 
1235
+ // src/commands/sync.ts
1236
+ import {
1237
+ cpSync as cpSync2,
1238
+ existsSync as existsSync4,
1239
+ lstatSync as lstatSync3,
1240
+ mkdirSync as mkdirSync3,
1241
+ readdirSync as readdirSync2,
1242
+ rmSync,
1243
+ statSync as statSync2,
1244
+ symlinkSync as symlinkSync2,
1245
+ unlinkSync as unlinkSync2
1246
+ } from "fs";
1247
+ import { resolve as resolve4 } from "path";
1248
+ import * as p4 from "@clack/prompts";
1249
+ import pc4 from "picocolors";
1250
+ function getExternalSkillNames() {
1251
+ return new Set(getByCategory("skills-external").map((c) => c.name));
1252
+ }
1253
+ function isSymlink(path) {
1254
+ try {
1255
+ return lstatSync3(path).isSymbolicLink();
1256
+ } catch {
1257
+ return false;
1258
+ }
1259
+ }
1260
+ function findUnmanaged(scanDir, targetDir, category, skipNames) {
1261
+ if (!existsSync4(scanDir)) return [];
1262
+ const items = [];
1263
+ const entries = readdirSync2(scanDir);
1264
+ for (const entry of entries) {
1265
+ const fullPath = resolve4(scanDir, entry);
1266
+ if (isSymlink(fullPath)) continue;
1267
+ if (skipNames.has(entry) || skipNames.has(entry.replace(/\.ts$/, ""))) continue;
1268
+ if (entry === "node_modules" || entry.startsWith(".")) continue;
1269
+ let isDir;
1270
+ try {
1271
+ isDir = statSync2(fullPath).isDirectory();
1272
+ } catch {
1273
+ continue;
1274
+ }
1275
+ if (category === "extensions") {
1276
+ if (!isDir && !entry.endsWith(".ts")) continue;
1277
+ } else {
1278
+ if (!isDir) continue;
1279
+ }
1280
+ items.push({
1281
+ name: entry,
1282
+ sourcePath: fullPath,
1283
+ targetDir,
1284
+ category,
1285
+ isDirectory: isDir
1286
+ });
1287
+ }
1288
+ return items;
1289
+ }
1290
+ function absorbItem(item) {
1291
+ const targetPath = resolve4(item.targetDir, item.name);
1292
+ try {
1293
+ mkdirSync3(item.targetDir, { recursive: true });
1294
+ if (item.isDirectory) {
1295
+ cpSync2(item.sourcePath, targetPath, { recursive: true });
1296
+ rmSync(item.sourcePath, { recursive: true, force: true });
1297
+ } else {
1298
+ cpSync2(item.sourcePath, targetPath);
1299
+ unlinkSync2(item.sourcePath);
1300
+ }
1301
+ symlinkSync2(targetPath, item.sourcePath);
1302
+ return { success: true, message: "absorbed and symlinked" };
1303
+ } catch (err) {
1304
+ const msg = err instanceof Error ? err.message : String(err);
1305
+ return { success: false, message: msg };
1306
+ }
1307
+ }
1308
+ async function runSync(options) {
1309
+ p4.intro(pc4.bold("pi-agent-toolkit sync"));
1310
+ const repoPath = resolve4(options.repoPath);
1311
+ const dotfilesPath = resolve4(repoPath, "dotfiles");
1312
+ if (!existsSync4(dotfilesPath)) {
1313
+ p4.log.error(`dotfiles/ not found at ${dotfilesPath}`);
1314
+ p4.log.info("Make sure --repo-path points to your pi-toolkit repo clone.");
1315
+ process.exit(1);
1316
+ }
1317
+ p4.log.info(`Repo: ${repoPath}`);
1318
+ p4.log.info("Scanning for unmanaged components...");
1319
+ const externalSkills2 = getExternalSkillNames();
1320
+ function knownNames(subdir) {
1321
+ const dir = resolve4(dotfilesPath, subdir);
1322
+ if (!existsSync4(dir)) return /* @__PURE__ */ new Set();
1323
+ return new Set(readdirSync2(dir));
1324
+ }
1325
+ const scanTargets = [
1326
+ [PI_EXTENSIONS_DIR, "extensions", "extensions", knownNames("extensions")],
1327
+ [
1328
+ PI_SKILLS_DIR,
1329
+ "agent-skills",
1330
+ "agent-skills",
1331
+ /* @__PURE__ */ new Set([...knownNames("agent-skills"), ...externalSkills2])
1332
+ ],
1333
+ [
1334
+ AGENTS_SKILLS_DIR,
1335
+ "global-skills",
1336
+ "global-skills",
1337
+ /* @__PURE__ */ new Set([...knownNames("global-skills"), ...externalSkills2])
1338
+ ],
1339
+ [PI_PROMPTS_DIR, "prompts", "prompts", knownNames("prompts")],
1340
+ [PI_AGENTS_DIR, "agents", "agents", knownNames("agents")],
1341
+ [PI_THEMES_DIR, "themes", "themes", knownNames("themes")]
1342
+ ];
1343
+ const found = scanTargets.flatMap(
1344
+ ([scanDir, dotfilesSub, category, skipNames]) => findUnmanaged(scanDir, resolve4(dotfilesPath, dotfilesSub), category, skipNames)
1345
+ );
1346
+ if (found.length === 0) {
1347
+ p4.log.success("No unmanaged components found. Everything is in sync.");
1348
+ p4.outro("Done.");
1349
+ return;
1350
+ }
1351
+ p4.log.info(`Found ${found.length} unmanaged item(s):`);
1352
+ for (const item of found) {
1353
+ const suffix = item.isDirectory ? "/" : "";
1354
+ p4.log.message(` ${pc4.yellow(item.name + suffix)} ${pc4.dim(`(${item.category})`)}`);
1355
+ }
1356
+ let toAbsorb;
1357
+ if (options.all) {
1358
+ toAbsorb = found;
1359
+ } else {
1360
+ const selected = await p4.multiselect({
1361
+ message: "Select items to absorb into the repo:",
1362
+ options: found.map((item) => ({
1363
+ value: item.name,
1364
+ label: item.name + (item.isDirectory ? "/" : ""),
1365
+ hint: `${item.category} -> dotfiles/${item.category}/${item.name}`
1366
+ })),
1367
+ required: false
1368
+ });
1369
+ if (p4.isCancel(selected)) {
1370
+ p4.cancel("Sync cancelled.");
1371
+ process.exit(0);
1372
+ }
1373
+ const selectedNames = new Set(selected);
1374
+ toAbsorb = found.filter((item) => selectedNames.has(item.name));
1375
+ }
1376
+ if (toAbsorb.length === 0) {
1377
+ p4.log.warn("Nothing selected to absorb.");
1378
+ p4.outro("Done.");
1379
+ return;
1380
+ }
1381
+ const spinner3 = p4.spinner();
1382
+ let succeeded = 0;
1383
+ let failed = 0;
1384
+ for (const item of toAbsorb) {
1385
+ spinner3.start(`Absorbing ${item.name}...`);
1386
+ const result = absorbItem(item);
1387
+ if (result.success) {
1388
+ spinner3.stop(`${item.name}: ${result.message}`);
1389
+ succeeded++;
1390
+ } else {
1391
+ spinner3.stop(`${item.name}: FAILED - ${result.message}`);
1392
+ failed++;
1393
+ }
1394
+ }
1395
+ if (failed > 0) {
1396
+ p4.log.warn(`Absorbed ${succeeded}/${toAbsorb.length}. ${failed} failed.`);
1397
+ } else {
1398
+ p4.log.success(`All ${succeeded} item(s) absorbed into the repo.`);
1399
+ }
1400
+ p4.outro(
1401
+ pc4.green("Next steps: review the new files in dotfiles/, add to registry.ts, then commit.")
1402
+ );
1403
+ }
1404
+
1257
1405
  // src/index.ts
1258
- var CLI_VERSION = "0.1.0";
1406
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1407
+ var CLI_VERSION = JSON.parse(
1408
+ readFileSync2(resolve5(__dirname2, "..", "package.json"), "utf-8")
1409
+ ).version;
1259
1410
  var install = defineCommand({
1260
1411
  meta: {
1261
1412
  name: "install",
@@ -1329,6 +1480,30 @@ var status = defineCommand({
1329
1480
  runStatus();
1330
1481
  }
1331
1482
  });
1483
+ var sync = defineCommand({
1484
+ meta: {
1485
+ name: "sync",
1486
+ description: "Absorb unmanaged extensions and skills from pi into the repo"
1487
+ },
1488
+ args: {
1489
+ "repo-path": {
1490
+ type: "string",
1491
+ description: "Path to local pi-toolkit repo clone",
1492
+ required: true
1493
+ },
1494
+ all: {
1495
+ type: "boolean",
1496
+ description: "Absorb all unmanaged items without prompting",
1497
+ default: false
1498
+ }
1499
+ },
1500
+ run({ args }) {
1501
+ return runSync({
1502
+ repoPath: args["repo-path"],
1503
+ all: args.all
1504
+ });
1505
+ }
1506
+ });
1332
1507
  var main = defineCommand({
1333
1508
  meta: {
1334
1509
  name: "pi-agent-toolkit",
@@ -1338,7 +1513,8 @@ var main = defineCommand({
1338
1513
  subCommands: {
1339
1514
  install,
1340
1515
  list,
1341
- status
1516
+ status,
1517
+ sync
1342
1518
  }
1343
1519
  });
1344
1520
  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.3.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": {