@anytio/pspm 0.0.7 → 0.1.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.
package/dist/index.js CHANGED
@@ -5,9 +5,11 @@ import { fileURLToPath, URL as URL$1 } from 'url';
5
5
  import { Command } from 'commander';
6
6
  import { createHash, randomBytes } from 'crypto';
7
7
  import * as semver from 'semver';
8
- import { stat, writeFile, readdir, mkdir, rm, rename, access as access$1, readFile, unlink } from 'fs/promises';
8
+ import { stat, writeFile, readdir, mkdir, rm, rename, access as access$1, readFile, lstat, cp, unlink, readlink, symlink } from 'fs/promises';
9
9
  import { homedir } from 'os';
10
10
  import * as ini from 'ini';
11
+ import { checkbox } from '@inquirer/prompts';
12
+ import { createInterface } from 'readline';
11
13
  import http from 'http';
12
14
  import open from 'open';
13
15
  import { exec as exec$1 } from 'child_process';
@@ -25,7 +27,7 @@ var DEFAULT_SKILL_FILES = [
25
27
  "scripts",
26
28
  "data"
27
29
  ];
28
- var PSPM_SCHEMA_URL = "https://pspm.dev/schema/pspm.json";
30
+ var PSPM_SCHEMA_URL = "https://pspm.dev/schema/v1/pspm.json";
29
31
  function validateManifest(manifest) {
30
32
  if (!manifest.name) {
31
33
  return { valid: false, error: "Manifest must have a 'name' field" };
@@ -61,6 +63,41 @@ function parseSkillSpecifier(specifier) {
61
63
  versionRange: match[3]
62
64
  };
63
65
  }
66
+ var GITHUB_SPECIFIER_PATTERN = /^github:([a-zA-Z0-9_-]+)\/([a-zA-Z0-9_.-]+)(\/[^@]+)?(?:@(.+))?$/;
67
+ function parseGitHubSpecifier(specifier) {
68
+ const match = specifier.match(GITHUB_SPECIFIER_PATTERN);
69
+ if (!match) {
70
+ return null;
71
+ }
72
+ const [, owner, repo, pathWithSlash, ref] = match;
73
+ return {
74
+ owner,
75
+ repo,
76
+ // Remove leading slash from path
77
+ path: pathWithSlash ? pathWithSlash.slice(1) : void 0,
78
+ ref: ref || void 0
79
+ };
80
+ }
81
+ function formatGitHubSpecifier(spec) {
82
+ let result = `github:${spec.owner}/${spec.repo}`;
83
+ if (spec.path) {
84
+ result += `/${spec.path}`;
85
+ }
86
+ if (spec.ref) {
87
+ result += `@${spec.ref}`;
88
+ }
89
+ return result;
90
+ }
91
+ function getGitHubSkillName(spec) {
92
+ if (spec.path) {
93
+ const segments = spec.path.split("/").filter(Boolean);
94
+ return segments[segments.length - 1];
95
+ }
96
+ return spec.repo;
97
+ }
98
+ function isGitHubSpecifier(specifier) {
99
+ return specifier.startsWith("github:");
100
+ }
64
101
  function resolveVersion(range, availableVersions) {
65
102
  const sorted = availableVersions.filter((v) => semver.valid(v)).sort((a, b) => semver.rcompare(a, b));
66
103
  if (!range || range === "latest" || range === "*") {
@@ -656,19 +693,19 @@ async function access(specifier, options) {
656
693
  }
657
694
  packageName = parsed.name;
658
695
  } else {
659
- const { readFile: readFile6 } = await import('fs/promises');
660
- const { join: join10 } = await import('path');
696
+ const { readFile: readFile7 } = await import('fs/promises');
697
+ const { join: join13 } = await import('path');
661
698
  let manifest = null;
662
699
  try {
663
- const content = await readFile6(
664
- join10(process.cwd(), "pspm.json"),
700
+ const content = await readFile7(
701
+ join13(process.cwd(), "pspm.json"),
665
702
  "utf-8"
666
703
  );
667
704
  manifest = JSON.parse(content);
668
705
  } catch {
669
706
  try {
670
- const content = await readFile6(
671
- join10(process.cwd(), "package.json"),
707
+ const content = await readFile7(
708
+ join13(process.cwd(), "package.json"),
672
709
  "utf-8"
673
710
  );
674
711
  manifest = JSON.parse(content);
@@ -712,6 +749,243 @@ async function access(specifier, options) {
712
749
  process.exit(1);
713
750
  }
714
751
  }
752
+ var AGENT_INFO = {
753
+ "claude-code": {
754
+ displayName: "Claude Code",
755
+ skillsDir: ".claude/skills"
756
+ },
757
+ codex: {
758
+ displayName: "Codex",
759
+ skillsDir: ".codex/skills"
760
+ },
761
+ cursor: {
762
+ displayName: "Cursor",
763
+ skillsDir: ".cursor/skills"
764
+ },
765
+ gemini: {
766
+ displayName: "Gemini CLI",
767
+ skillsDir: ".gemini/skills"
768
+ },
769
+ kiro: {
770
+ displayName: "Kiro CLI",
771
+ skillsDir: ".kiro/skills"
772
+ },
773
+ opencode: {
774
+ displayName: "OpenCode",
775
+ skillsDir: ".opencode/skills"
776
+ }
777
+ };
778
+ var DEFAULT_AGENT_CONFIGS = {
779
+ "claude-code": { skillsDir: AGENT_INFO["claude-code"].skillsDir },
780
+ codex: { skillsDir: AGENT_INFO.codex.skillsDir },
781
+ cursor: { skillsDir: AGENT_INFO.cursor.skillsDir },
782
+ gemini: { skillsDir: AGENT_INFO.gemini.skillsDir },
783
+ kiro: { skillsDir: AGENT_INFO.kiro.skillsDir },
784
+ opencode: { skillsDir: AGENT_INFO.opencode.skillsDir }
785
+ };
786
+ var DEFAULT_AGENT = "claude-code";
787
+ var ALL_AGENTS = [
788
+ "claude-code",
789
+ "codex",
790
+ "cursor",
791
+ "gemini",
792
+ "kiro",
793
+ "opencode"
794
+ ];
795
+ function resolveAgentConfig(name, overrides) {
796
+ if (overrides?.[name]) {
797
+ return overrides[name];
798
+ }
799
+ if (name in DEFAULT_AGENT_CONFIGS) {
800
+ return DEFAULT_AGENT_CONFIGS[name];
801
+ }
802
+ return null;
803
+ }
804
+ function parseAgentArg(agentArg) {
805
+ if (!agentArg) {
806
+ return [DEFAULT_AGENT];
807
+ }
808
+ if (agentArg === "none") {
809
+ return ["none"];
810
+ }
811
+ return agentArg.split(",").map((a) => a.trim()).filter(Boolean);
812
+ }
813
+ function getAvailableAgents(overrides) {
814
+ const builtIn = Object.keys(DEFAULT_AGENT_CONFIGS);
815
+ const custom = overrides ? Object.keys(overrides) : [];
816
+ return [.../* @__PURE__ */ new Set([...builtIn, ...custom])];
817
+ }
818
+ async function promptForAgents() {
819
+ const choices = ALL_AGENTS.map((agent) => ({
820
+ name: `${AGENT_INFO[agent].displayName} (${AGENT_INFO[agent].skillsDir})`,
821
+ value: agent,
822
+ checked: true
823
+ // All selected by default
824
+ }));
825
+ const selected = await checkbox({
826
+ message: "Select agents to install skills to",
827
+ choices
828
+ });
829
+ if (selected.length === 0) {
830
+ return ["none"];
831
+ }
832
+ return selected;
833
+ }
834
+ var GitHubRateLimitError = class extends Error {
835
+ constructor() {
836
+ super(
837
+ "GitHub API rate limit exceeded. Set GITHUB_TOKEN environment variable for higher limits."
838
+ );
839
+ this.name = "GitHubRateLimitError";
840
+ }
841
+ };
842
+ var GitHubNotFoundError = class extends Error {
843
+ constructor(spec) {
844
+ const path = spec.path ? `/${spec.path}` : "";
845
+ const ref = spec.ref ? `@${spec.ref}` : "";
846
+ super(
847
+ `GitHub repository not found: ${spec.owner}/${spec.repo}${path}${ref}`
848
+ );
849
+ this.name = "GitHubNotFoundError";
850
+ }
851
+ };
852
+ var GitHubPathNotFoundError = class extends Error {
853
+ constructor(spec, availablePaths) {
854
+ const pathInfo = availablePaths?.length ? `
855
+ Available paths in repository root:
856
+ ${availablePaths.join("\n ")}` : "";
857
+ super(
858
+ `Path "${spec.path}" not found in ${spec.owner}/${spec.repo}${pathInfo}`
859
+ );
860
+ this.name = "GitHubPathNotFoundError";
861
+ }
862
+ };
863
+ function getGitHubHeaders() {
864
+ const headers = {
865
+ Accept: "application/vnd.github+json",
866
+ "X-GitHub-Api-Version": "2022-11-28",
867
+ "User-Agent": "pspm-cli"
868
+ };
869
+ const token = process.env.GITHUB_TOKEN;
870
+ if (token) {
871
+ headers.Authorization = `Bearer ${token}`;
872
+ }
873
+ return headers;
874
+ }
875
+ async function resolveGitHubRef(owner, repo, ref) {
876
+ const headers = getGitHubHeaders();
877
+ if (!ref || ref === "latest") {
878
+ const repoUrl = `https://api.github.com/repos/${owner}/${repo}`;
879
+ const repoResponse = await fetch(repoUrl, { headers });
880
+ if (repoResponse.status === 404) {
881
+ throw new GitHubNotFoundError({ owner, repo });
882
+ }
883
+ if (repoResponse.status === 403) {
884
+ const remaining = repoResponse.headers.get("x-ratelimit-remaining");
885
+ if (remaining === "0") {
886
+ throw new GitHubRateLimitError();
887
+ }
888
+ }
889
+ if (!repoResponse.ok) {
890
+ throw new Error(`GitHub API error: ${repoResponse.status}`);
891
+ }
892
+ const repoData = await repoResponse.json();
893
+ ref = repoData.default_branch;
894
+ }
895
+ const commitUrl = `https://api.github.com/repos/${owner}/${repo}/commits/${ref}`;
896
+ const commitResponse = await fetch(commitUrl, { headers });
897
+ if (commitResponse.status === 404) {
898
+ throw new GitHubNotFoundError({ owner, repo, ref });
899
+ }
900
+ if (commitResponse.status === 403) {
901
+ const remaining = commitResponse.headers.get("x-ratelimit-remaining");
902
+ if (remaining === "0") {
903
+ throw new GitHubRateLimitError();
904
+ }
905
+ }
906
+ if (!commitResponse.ok) {
907
+ throw new Error(`GitHub API error: ${commitResponse.status}`);
908
+ }
909
+ const commitData = await commitResponse.json();
910
+ return commitData.sha;
911
+ }
912
+ async function downloadGitHubPackage(spec) {
913
+ const headers = getGitHubHeaders();
914
+ const commit = await resolveGitHubRef(spec.owner, spec.repo, spec.ref);
915
+ const tarballUrl = `https://api.github.com/repos/${spec.owner}/${spec.repo}/tarball/${commit}`;
916
+ const response = await fetch(tarballUrl, {
917
+ headers,
918
+ redirect: "follow"
919
+ });
920
+ if (response.status === 404) {
921
+ throw new GitHubNotFoundError(spec);
922
+ }
923
+ if (response.status === 403) {
924
+ const remaining = response.headers.get("x-ratelimit-remaining");
925
+ if (remaining === "0") {
926
+ throw new GitHubRateLimitError();
927
+ }
928
+ }
929
+ if (!response.ok) {
930
+ throw new Error(`Failed to download GitHub tarball: ${response.status}`);
931
+ }
932
+ const buffer = Buffer.from(await response.arrayBuffer());
933
+ const integrity = calculateIntegrity(buffer);
934
+ return { buffer, commit, integrity };
935
+ }
936
+ async function extractGitHubPackage(spec, buffer, skillsDir) {
937
+ const destPath = spec.path ? join(skillsDir, "_github", spec.owner, spec.repo, spec.path) : join(skillsDir, "_github", spec.owner, spec.repo);
938
+ const tempDir = join(skillsDir, "_github", ".temp", `${Date.now()}`);
939
+ await mkdir(tempDir, { recursive: true });
940
+ const tempFile = join(tempDir, "archive.tgz");
941
+ try {
942
+ await writeFile(tempFile, buffer);
943
+ const { exec: exec2 } = await import('child_process');
944
+ const { promisify: promisify2 } = await import('util');
945
+ const execAsync = promisify2(exec2);
946
+ await execAsync(`tar -xzf "${tempFile}" -C "${tempDir}"`);
947
+ const entries = await readdir(tempDir);
948
+ const extractedDir = entries.find(
949
+ (e) => e !== "archive.tgz" && !e.startsWith(".")
950
+ );
951
+ if (!extractedDir) {
952
+ throw new Error("Failed to find extracted directory in tarball");
953
+ }
954
+ const sourcePath = join(tempDir, extractedDir);
955
+ const copySource = spec.path ? join(sourcePath, spec.path) : sourcePath;
956
+ if (spec.path) {
957
+ const pathExists = await lstat(copySource).catch(() => null);
958
+ if (!pathExists) {
959
+ const rootEntries = await readdir(sourcePath);
960
+ const dirs = [];
961
+ for (const entry of rootEntries) {
962
+ const stat7 = await lstat(join(sourcePath, entry)).catch(() => null);
963
+ if (stat7?.isDirectory() && !entry.startsWith(".")) {
964
+ dirs.push(entry);
965
+ }
966
+ }
967
+ throw new GitHubPathNotFoundError(spec, dirs);
968
+ }
969
+ }
970
+ await rm(destPath, { recursive: true, force: true });
971
+ await mkdir(destPath, { recursive: true });
972
+ await cp(copySource, destPath, { recursive: true });
973
+ return spec.path ? `.pspm/skills/_github/${spec.owner}/${spec.repo}/${spec.path}` : `.pspm/skills/_github/${spec.owner}/${spec.repo}`;
974
+ } finally {
975
+ await rm(tempDir, { recursive: true, force: true });
976
+ }
977
+ }
978
+ function getGitHubDisplayName(spec, commit) {
979
+ let name = `github:${spec.owner}/${spec.repo}`;
980
+ if (spec.path) {
981
+ name += `/${spec.path}`;
982
+ }
983
+ if (spec.ref || commit) {
984
+ const ref = spec.ref || "HEAD";
985
+ name += ` (${ref}${""})`;
986
+ }
987
+ return name;
988
+ }
715
989
  async function hasLegacyLockfile() {
716
990
  try {
717
991
  await stat(getLegacyLockfilePath());
@@ -783,17 +1057,20 @@ async function writeLockfile(lockfile) {
783
1057
  const lockfilePath = getLockfilePath();
784
1058
  await mkdir(dirname(lockfilePath), { recursive: true });
785
1059
  const normalized = {
786
- lockfileVersion: 2,
1060
+ lockfileVersion: 3,
787
1061
  registryUrl: lockfile.registryUrl,
788
1062
  packages: lockfile.packages ?? lockfile.skills ?? {}
789
1063
  };
1064
+ if (lockfile.githubPackages && Object.keys(lockfile.githubPackages).length > 0) {
1065
+ normalized.githubPackages = lockfile.githubPackages;
1066
+ }
790
1067
  await writeFile(lockfilePath, `${JSON.stringify(normalized, null, 2)}
791
1068
  `);
792
1069
  }
793
1070
  async function createEmptyLockfile() {
794
1071
  const registryUrl = await getRegistryUrl();
795
1072
  return {
796
- lockfileVersion: 2,
1073
+ lockfileVersion: 3,
797
1074
  registryUrl,
798
1075
  packages: {}
799
1076
  };
@@ -836,9 +1113,220 @@ async function listLockfileSkills() {
836
1113
  entry
837
1114
  }));
838
1115
  }
1116
+ async function addGitHubToLockfile(specifier, entry) {
1117
+ let lockfile = await readLockfile();
1118
+ if (!lockfile) {
1119
+ lockfile = await createEmptyLockfile();
1120
+ }
1121
+ if (!lockfile.githubPackages) {
1122
+ lockfile.githubPackages = {};
1123
+ }
1124
+ lockfile.githubPackages[specifier] = entry;
1125
+ await writeLockfile(lockfile);
1126
+ }
1127
+ async function removeGitHubFromLockfile(specifier) {
1128
+ const lockfile = await readLockfile();
1129
+ if (!lockfile?.githubPackages?.[specifier]) {
1130
+ return false;
1131
+ }
1132
+ delete lockfile.githubPackages[specifier];
1133
+ await writeLockfile(lockfile);
1134
+ return true;
1135
+ }
1136
+ async function listLockfileGitHubPackages() {
1137
+ const lockfile = await readLockfile();
1138
+ if (!lockfile?.githubPackages) {
1139
+ return [];
1140
+ }
1141
+ return Object.entries(lockfile.githubPackages).map(([specifier, entry]) => ({
1142
+ specifier,
1143
+ entry
1144
+ }));
1145
+ }
1146
+ function getManifestPath() {
1147
+ return join(process.cwd(), "pspm.json");
1148
+ }
1149
+ async function readManifest() {
1150
+ try {
1151
+ const content = await readFile(getManifestPath(), "utf-8");
1152
+ return JSON.parse(content);
1153
+ } catch {
1154
+ return null;
1155
+ }
1156
+ }
1157
+ async function writeManifest(manifest) {
1158
+ const content = JSON.stringify(manifest, null, 2);
1159
+ await writeFile(getManifestPath(), `${content}
1160
+ `);
1161
+ }
1162
+ async function createMinimalManifest() {
1163
+ return {
1164
+ dependencies: {}
1165
+ };
1166
+ }
1167
+ async function ensureManifest() {
1168
+ let manifest = await readManifest();
1169
+ if (!manifest) {
1170
+ manifest = await createMinimalManifest();
1171
+ await writeManifest(manifest);
1172
+ }
1173
+ return manifest;
1174
+ }
1175
+ async function addDependency(skillName, versionRange) {
1176
+ const manifest = await ensureManifest();
1177
+ if (!manifest.dependencies) {
1178
+ manifest.dependencies = {};
1179
+ }
1180
+ manifest.dependencies[skillName] = versionRange;
1181
+ await writeManifest(manifest);
1182
+ }
1183
+ async function removeDependency(skillName) {
1184
+ const manifest = await readManifest();
1185
+ if (!manifest?.dependencies?.[skillName]) {
1186
+ return false;
1187
+ }
1188
+ delete manifest.dependencies[skillName];
1189
+ await writeManifest(manifest);
1190
+ return true;
1191
+ }
1192
+ async function getDependencies() {
1193
+ const manifest = await readManifest();
1194
+ return manifest?.dependencies ?? {};
1195
+ }
1196
+ async function getGitHubDependencies() {
1197
+ const manifest = await readManifest();
1198
+ return manifest?.githubDependencies ?? {};
1199
+ }
1200
+ async function addGitHubDependency(specifier, ref) {
1201
+ const manifest = await ensureManifest();
1202
+ if (!manifest.githubDependencies) {
1203
+ manifest.githubDependencies = {};
1204
+ }
1205
+ manifest.githubDependencies[specifier] = ref;
1206
+ await writeManifest(manifest);
1207
+ }
1208
+ async function removeGitHubDependency(specifier) {
1209
+ const manifest = await readManifest();
1210
+ if (!manifest?.githubDependencies?.[specifier]) {
1211
+ return false;
1212
+ }
1213
+ delete manifest.githubDependencies[specifier];
1214
+ await writeManifest(manifest);
1215
+ return true;
1216
+ }
1217
+ async function createAgentSymlinks(skills, options) {
1218
+ const { agents, projectRoot, agentConfigs } = options;
1219
+ if (agents.length === 1 && agents[0] === "none") {
1220
+ return;
1221
+ }
1222
+ for (const agentName of agents) {
1223
+ const config2 = resolveAgentConfig(agentName, agentConfigs);
1224
+ if (!config2) {
1225
+ console.warn(`Warning: Unknown agent "${agentName}", skipping symlinks`);
1226
+ continue;
1227
+ }
1228
+ const agentSkillsDir = join(projectRoot, config2.skillsDir);
1229
+ await mkdir(agentSkillsDir, { recursive: true });
1230
+ for (const skill of skills) {
1231
+ const symlinkPath = join(agentSkillsDir, skill.name);
1232
+ const targetPath = join(projectRoot, skill.sourcePath);
1233
+ const relativeTarget = relative(dirname(symlinkPath), targetPath);
1234
+ await createSymlink(symlinkPath, relativeTarget, skill.name);
1235
+ }
1236
+ }
1237
+ }
1238
+ async function createSymlink(symlinkPath, target, skillName) {
1239
+ try {
1240
+ const stats = await lstat(symlinkPath).catch(() => null);
1241
+ if (stats) {
1242
+ if (stats.isSymbolicLink()) {
1243
+ const existingTarget = await readlink(symlinkPath);
1244
+ if (existingTarget === target) {
1245
+ return;
1246
+ }
1247
+ await rm(symlinkPath);
1248
+ } else {
1249
+ console.warn(
1250
+ `Warning: File exists at symlink path for "${skillName}", skipping: ${symlinkPath}`
1251
+ );
1252
+ return;
1253
+ }
1254
+ }
1255
+ await symlink(target, symlinkPath);
1256
+ } catch (error) {
1257
+ const message = error instanceof Error ? error.message : String(error);
1258
+ console.warn(
1259
+ `Warning: Failed to create symlink for "${skillName}": ${message}`
1260
+ );
1261
+ }
1262
+ }
1263
+ async function removeAgentSymlinks(skillName, options) {
1264
+ const { agents, projectRoot, agentConfigs } = options;
1265
+ if (agents.length === 1 && agents[0] === "none") {
1266
+ return;
1267
+ }
1268
+ for (const agentName of agents) {
1269
+ const config2 = resolveAgentConfig(agentName, agentConfigs);
1270
+ if (!config2) {
1271
+ continue;
1272
+ }
1273
+ const symlinkPath = join(projectRoot, config2.skillsDir, skillName);
1274
+ try {
1275
+ const stats = await lstat(symlinkPath).catch(() => null);
1276
+ if (stats?.isSymbolicLink()) {
1277
+ await rm(symlinkPath);
1278
+ }
1279
+ } catch {
1280
+ }
1281
+ }
1282
+ }
1283
+ function getRegistrySkillPath(username, skillName) {
1284
+ return `.pspm/skills/${username}/${skillName}`;
1285
+ }
1286
+ function getGitHubSkillPath(owner, repo, path) {
1287
+ if (path) {
1288
+ return `.pspm/skills/_github/${owner}/${repo}/${path}`;
1289
+ }
1290
+ return `.pspm/skills/_github/${owner}/${repo}`;
1291
+ }
1292
+ async function getLinkedAgents(skillName, agents, projectRoot, agentConfigs) {
1293
+ const linkedAgents = [];
1294
+ for (const agentName of agents) {
1295
+ const config2 = resolveAgentConfig(agentName, agentConfigs);
1296
+ if (!config2) continue;
1297
+ const symlinkPath = join(projectRoot, config2.skillsDir, skillName);
1298
+ try {
1299
+ const stats = await lstat(symlinkPath);
1300
+ if (stats.isSymbolicLink()) {
1301
+ linkedAgents.push(agentName);
1302
+ }
1303
+ } catch {
1304
+ }
1305
+ }
1306
+ return linkedAgents;
1307
+ }
839
1308
 
840
1309
  // src/commands/add.ts
841
- async function add(specifier, _options) {
1310
+ async function add(specifier, options) {
1311
+ let agents;
1312
+ const manifest = await readManifest();
1313
+ if (options.agent) {
1314
+ agents = parseAgentArg(options.agent);
1315
+ } else if (manifest) {
1316
+ agents = parseAgentArg(void 0);
1317
+ } else if (options.yes) {
1318
+ agents = parseAgentArg(void 0);
1319
+ } else {
1320
+ console.log("No pspm.json found. Let's set up your project.\n");
1321
+ agents = await promptForAgents();
1322
+ }
1323
+ if (isGitHubSpecifier(specifier)) {
1324
+ await addGitHub(specifier, { ...options, resolvedAgents: agents });
1325
+ } else {
1326
+ await addRegistry(specifier, { ...options, resolvedAgents: agents });
1327
+ }
1328
+ }
1329
+ async function addRegistry(specifier, options) {
842
1330
  try {
843
1331
  const config2 = await resolveConfig();
844
1332
  const registryUrl = config2.registryUrl;
@@ -925,16 +1413,16 @@ async function add(specifier, _options) {
925
1413
  const skillsDir = getSkillsDir();
926
1414
  const destDir = join(skillsDir, username, name);
927
1415
  await mkdir(destDir, { recursive: true });
928
- const { writeFile: writeFile6 } = await import('fs/promises');
1416
+ const { writeFile: writeFile8 } = await import('fs/promises');
929
1417
  const tempFile = join(destDir, ".temp.tgz");
930
- await writeFile6(tempFile, tarballBuffer);
1418
+ await writeFile8(tempFile, tarballBuffer);
931
1419
  const { exec: exec2 } = await import('child_process');
932
1420
  const { promisify: promisify2 } = await import('util');
933
1421
  const execAsync = promisify2(exec2);
934
1422
  try {
935
1423
  await rm(destDir, { recursive: true, force: true });
936
1424
  await mkdir(destDir, { recursive: true });
937
- await writeFile6(tempFile, tarballBuffer);
1425
+ await writeFile8(tempFile, tarballBuffer);
938
1426
  await execAsync(
939
1427
  `tar -xzf "${tempFile}" -C "${destDir}" --strip-components=1`
940
1428
  );
@@ -947,6 +1435,21 @@ async function add(specifier, _options) {
947
1435
  resolved: versionInfo.downloadUrl,
948
1436
  integrity
949
1437
  });
1438
+ const dependencyRange = versionRange || `^${resolved}`;
1439
+ await addDependency(fullName, dependencyRange);
1440
+ const agents = options.resolvedAgents;
1441
+ if (agents[0] !== "none") {
1442
+ const skillManifest = await readManifest();
1443
+ const skillInfo = {
1444
+ name,
1445
+ sourcePath: getRegistrySkillPath(username, name)
1446
+ };
1447
+ await createAgentSymlinks([skillInfo], {
1448
+ agents,
1449
+ projectRoot: process.cwd(),
1450
+ agentConfigs: skillManifest?.agents
1451
+ });
1452
+ }
950
1453
  console.log(`Installed @user/${username}/${name}@${resolved}`);
951
1454
  console.log(`Location: ${destDir}`);
952
1455
  } catch (error) {
@@ -955,6 +1458,72 @@ async function add(specifier, _options) {
955
1458
  process.exit(1);
956
1459
  }
957
1460
  }
1461
+ async function addGitHub(specifier, options) {
1462
+ try {
1463
+ const parsed = parseGitHubSpecifier(specifier);
1464
+ if (!parsed) {
1465
+ console.error(
1466
+ `Error: Invalid GitHub specifier "${specifier}". Use format: github:{owner}/{repo}[/{path}][@{ref}]`
1467
+ );
1468
+ process.exit(1);
1469
+ }
1470
+ const ref = parsed.ref || "HEAD";
1471
+ console.log(`Resolving ${getGitHubDisplayName(parsed)}...`);
1472
+ const result = await downloadGitHubPackage(parsed);
1473
+ console.log(
1474
+ `Installing ${specifier} (${ref}@${result.commit.slice(0, 7)})...`
1475
+ );
1476
+ const skillsDir = getSkillsDir();
1477
+ const destPath = await extractGitHubPackage(
1478
+ parsed,
1479
+ result.buffer,
1480
+ skillsDir
1481
+ );
1482
+ const lockfileSpecifier = formatGitHubSpecifier({
1483
+ owner: parsed.owner,
1484
+ repo: parsed.repo,
1485
+ path: parsed.path
1486
+ // Don't include ref in the specifier key, it's stored in gitRef
1487
+ });
1488
+ const entry = {
1489
+ version: result.commit.slice(0, 7),
1490
+ resolved: `https://github.com/${parsed.owner}/${parsed.repo}`,
1491
+ integrity: result.integrity,
1492
+ gitCommit: result.commit,
1493
+ gitRef: ref
1494
+ };
1495
+ await addGitHubToLockfile(lockfileSpecifier, entry);
1496
+ await addGitHubDependency(lockfileSpecifier, ref);
1497
+ const agents = options.resolvedAgents;
1498
+ if (agents[0] !== "none") {
1499
+ const manifest = await readManifest();
1500
+ const skillName = getGitHubSkillName(parsed);
1501
+ const skillInfo = {
1502
+ name: skillName,
1503
+ sourcePath: getGitHubSkillPath(parsed.owner, parsed.repo, parsed.path)
1504
+ };
1505
+ await createAgentSymlinks([skillInfo], {
1506
+ agents,
1507
+ projectRoot: process.cwd(),
1508
+ agentConfigs: manifest?.agents
1509
+ });
1510
+ }
1511
+ console.log(`Installed ${specifier} (${ref}@${result.commit.slice(0, 7)})`);
1512
+ console.log(`Location: ${destPath}`);
1513
+ } catch (error) {
1514
+ if (error instanceof GitHubRateLimitError) {
1515
+ console.error(`Error: ${error.message}`);
1516
+ } else if (error instanceof GitHubPathNotFoundError) {
1517
+ console.error(`Error: ${error.message}`);
1518
+ } else if (error instanceof GitHubNotFoundError) {
1519
+ console.error(`Error: ${error.message}`);
1520
+ } else {
1521
+ const message = error instanceof Error ? error.message : "Unknown error";
1522
+ console.error(`Error: ${message}`);
1523
+ }
1524
+ process.exit(1);
1525
+ }
1526
+ }
958
1527
  async function configInit(options) {
959
1528
  try {
960
1529
  const configPath = join(process.cwd(), ".pspmrc");
@@ -1078,6 +1647,14 @@ async function deprecate(specifier, message, options) {
1078
1647
  process.exit(1);
1079
1648
  }
1080
1649
  }
1650
+ function prompt(rl, question, defaultValue) {
1651
+ return new Promise((resolve) => {
1652
+ const displayDefault = defaultValue ? ` (${defaultValue})` : "";
1653
+ rl.question(`${question}${displayDefault} `, (answer) => {
1654
+ resolve(answer.trim() || defaultValue);
1655
+ });
1656
+ });
1657
+ }
1081
1658
  async function readExistingPackageJson() {
1082
1659
  try {
1083
1660
  const content = await readFile(
@@ -1096,52 +1673,177 @@ async function readExistingPackageJson() {
1096
1673
  return null;
1097
1674
  }
1098
1675
  }
1676
+ async function getGitAuthor() {
1677
+ try {
1678
+ const { exec: exec2 } = await import('child_process');
1679
+ const { promisify: promisify2 } = await import('util');
1680
+ const execAsync = promisify2(exec2);
1681
+ const [nameResult, emailResult] = await Promise.all([
1682
+ execAsync("git config user.name").catch(() => ({ stdout: "" })),
1683
+ execAsync("git config user.email").catch(() => ({ stdout: "" }))
1684
+ ]);
1685
+ const name = nameResult.stdout.trim();
1686
+ const email = emailResult.stdout.trim();
1687
+ if (name && email) {
1688
+ return `${name} <${email}>`;
1689
+ }
1690
+ if (name) {
1691
+ return name;
1692
+ }
1693
+ return null;
1694
+ } catch {
1695
+ return null;
1696
+ }
1697
+ }
1099
1698
  function sanitizeName(name) {
1100
1699
  const withoutScope = name.replace(/^@[^/]+\//, "");
1101
1700
  return withoutScope.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
1102
1701
  }
1702
+ function isValidName(name) {
1703
+ return /^[a-z][a-z0-9_-]*$/.test(name);
1704
+ }
1705
+ function isValidVersion(version2) {
1706
+ return /^\d+\.\d+\.\d+(-[a-zA-Z0-9.-]+)?(\+[a-zA-Z0-9.-]+)?$/.test(version2);
1707
+ }
1103
1708
  async function init(options) {
1104
1709
  try {
1105
1710
  const pspmJsonPath = join(process.cwd(), "pspm.json");
1711
+ let exists = false;
1106
1712
  try {
1107
1713
  await stat(pspmJsonPath);
1714
+ exists = true;
1715
+ } catch {
1716
+ }
1717
+ if (exists && !options.force) {
1108
1718
  console.error("Error: pspm.json already exists in this directory.");
1109
- console.error("Use --force to overwrite (not yet implemented).");
1719
+ console.error("Use --force to overwrite.");
1110
1720
  process.exit(1);
1111
- } catch {
1112
1721
  }
1113
1722
  const existingPkg = await readExistingPackageJson();
1114
- const defaultName = options.name || sanitizeName(existingPkg?.name || basename(process.cwd()));
1723
+ const gitAuthor = await getGitAuthor();
1724
+ const defaultName = sanitizeName(
1725
+ options.name || existingPkg?.name || basename(process.cwd())
1726
+ );
1115
1727
  const defaultVersion = existingPkg?.version || "0.1.0";
1116
1728
  const defaultDescription = options.description || existingPkg?.description || "";
1117
- const defaultAuthor = options.author || existingPkg?.author || "";
1729
+ const defaultAuthor = options.author || existingPkg?.author || gitAuthor || "";
1118
1730
  const defaultLicense = existingPkg?.license || "MIT";
1119
- const manifest = {
1120
- $schema: PSPM_SCHEMA_URL,
1121
- name: defaultName,
1122
- version: defaultVersion,
1123
- description: defaultDescription || void 0,
1124
- author: defaultAuthor || void 0,
1125
- license: defaultLicense,
1126
- type: "skill",
1127
- capabilities: [],
1128
- main: "SKILL.md",
1129
- requirements: {
1130
- pspm: ">=0.1.0"
1131
- },
1132
- files: [...DEFAULT_SKILL_FILES],
1133
- dependencies: {},
1134
- private: false
1135
- };
1731
+ const defaultMain = "SKILL.md";
1732
+ const defaultCapabilities = "";
1733
+ let manifest;
1734
+ if (options.yes) {
1735
+ manifest = {
1736
+ $schema: PSPM_SCHEMA_URL,
1737
+ name: defaultName,
1738
+ version: defaultVersion,
1739
+ description: defaultDescription || void 0,
1740
+ author: defaultAuthor || void 0,
1741
+ license: defaultLicense,
1742
+ type: "skill",
1743
+ capabilities: [],
1744
+ main: defaultMain,
1745
+ requirements: {
1746
+ pspm: ">=0.1.0"
1747
+ },
1748
+ files: [...DEFAULT_SKILL_FILES],
1749
+ dependencies: {},
1750
+ private: false
1751
+ };
1752
+ } else {
1753
+ console.log(
1754
+ "This utility will walk you through creating a pspm.json file."
1755
+ );
1756
+ console.log(
1757
+ "It only covers the most common items, and tries to guess sensible defaults."
1758
+ );
1759
+ console.log("");
1760
+ console.log(
1761
+ "See `pspm init --help` for definitive documentation on these fields"
1762
+ );
1763
+ console.log("and exactly what they do.");
1764
+ console.log("");
1765
+ console.log("Press ^C at any time to quit.");
1766
+ const rl = createInterface({
1767
+ input: process.stdin,
1768
+ output: process.stdout
1769
+ });
1770
+ try {
1771
+ let name = await prompt(rl, "skill name:", defaultName);
1772
+ while (!isValidName(name)) {
1773
+ console.log(
1774
+ " Name must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores."
1775
+ );
1776
+ name = await prompt(rl, "skill name:", sanitizeName(name));
1777
+ }
1778
+ let version2 = await prompt(rl, "version:", defaultVersion);
1779
+ while (!isValidVersion(version2)) {
1780
+ console.log(" Version must be valid semver (e.g., 1.0.0)");
1781
+ version2 = await prompt(rl, "version:", "0.1.0");
1782
+ }
1783
+ const description = await prompt(
1784
+ rl,
1785
+ "description:",
1786
+ defaultDescription
1787
+ );
1788
+ const main = await prompt(rl, "entry point:", defaultMain);
1789
+ const capabilitiesStr = await prompt(
1790
+ rl,
1791
+ "capabilities (comma-separated):",
1792
+ defaultCapabilities
1793
+ );
1794
+ const author = await prompt(rl, "author:", defaultAuthor);
1795
+ const license = await prompt(rl, "license:", defaultLicense);
1796
+ rl.close();
1797
+ const capabilities = capabilitiesStr ? capabilitiesStr.split(",").map((s) => s.trim()).filter(Boolean) : [];
1798
+ manifest = {
1799
+ $schema: PSPM_SCHEMA_URL,
1800
+ name,
1801
+ version: version2,
1802
+ description: description || void 0,
1803
+ author: author || void 0,
1804
+ license,
1805
+ type: "skill",
1806
+ capabilities,
1807
+ main,
1808
+ requirements: {
1809
+ pspm: ">=0.1.0"
1810
+ },
1811
+ files: [...DEFAULT_SKILL_FILES],
1812
+ dependencies: {},
1813
+ private: false
1814
+ };
1815
+ } catch (error) {
1816
+ rl.close();
1817
+ if (error instanceof Error && error.message.includes("readline was closed")) {
1818
+ console.log("\nAborted.");
1819
+ process.exit(0);
1820
+ }
1821
+ throw error;
1822
+ }
1823
+ }
1136
1824
  if (!manifest.description) delete manifest.description;
1137
1825
  if (!manifest.author) delete manifest.author;
1826
+ if (manifest.capabilities?.length === 0) delete manifest.capabilities;
1138
1827
  const content = JSON.stringify(manifest, null, 2);
1139
- await writeFile(pspmJsonPath, `${content}
1140
- `);
1141
- console.log("Created pspm.json:");
1828
+ console.log("");
1829
+ console.log(`About to write to ${pspmJsonPath}:`);
1142
1830
  console.log("");
1143
1831
  console.log(content);
1144
1832
  console.log("");
1833
+ if (!options.yes) {
1834
+ const rl = createInterface({
1835
+ input: process.stdin,
1836
+ output: process.stdout
1837
+ });
1838
+ const confirm = await prompt(rl, "Is this OK?", "yes");
1839
+ rl.close();
1840
+ if (confirm.toLowerCase() !== "yes" && confirm.toLowerCase() !== "y") {
1841
+ console.log("Aborted.");
1842
+ process.exit(0);
1843
+ }
1844
+ }
1845
+ await writeFile(pspmJsonPath, `${content}
1846
+ `);
1145
1847
  try {
1146
1848
  await stat(join(process.cwd(), "SKILL.md"));
1147
1849
  } catch {
@@ -1194,108 +1896,431 @@ async function writeToCache(cacheDir, integrity, data) {
1194
1896
  async function install(options) {
1195
1897
  try {
1196
1898
  const config2 = await resolveConfig();
1197
- const apiKey = getTokenForRegistry(config2, config2.registryUrl);
1899
+ const registryUrl = config2.registryUrl;
1900
+ const apiKey = getTokenForRegistry(config2, registryUrl);
1198
1901
  const skillsDir = options.dir || getSkillsDir();
1199
1902
  const cacheDir = getCacheDir();
1903
+ const manifest = await readManifest();
1904
+ const agentConfigs = manifest?.agents;
1905
+ let agents;
1906
+ if (options.agent) {
1907
+ agents = parseAgentArg(options.agent);
1908
+ } else if (manifest) {
1909
+ agents = parseAgentArg(void 0);
1910
+ } else if (options.yes) {
1911
+ agents = parseAgentArg(void 0);
1912
+ } else {
1913
+ console.log("No pspm.json found. Let's set up your project.\n");
1914
+ agents = await promptForAgents();
1915
+ }
1200
1916
  await migrateLockfileIfNeeded();
1201
- const lockfile = await readLockfile();
1202
- if (!lockfile) {
1917
+ let lockfile = await readLockfile();
1918
+ const manifestDeps = await getDependencies();
1919
+ const manifestGitHubDeps = await getGitHubDependencies();
1920
+ const lockfilePackages = lockfile?.packages ?? lockfile?.skills ?? {};
1921
+ const lockfileGitHubPackages = lockfile?.githubPackages ?? {};
1922
+ const installedSkills = [];
1923
+ const missingDeps = [];
1924
+ for (const [fullName, versionRange] of Object.entries(manifestDeps)) {
1925
+ if (!lockfilePackages[fullName]) {
1926
+ missingDeps.push({ fullName, versionRange });
1927
+ }
1928
+ }
1929
+ if (missingDeps.length > 0) {
1203
1930
  if (options.frozenLockfile) {
1204
1931
  console.error(
1205
- "Error: No lockfile found. Cannot install with --frozen-lockfile"
1932
+ "Error: Dependencies in pspm.json are not in lockfile. Cannot install with --frozen-lockfile"
1206
1933
  );
1934
+ console.error("Missing dependencies:");
1935
+ for (const dep of missingDeps) {
1936
+ console.error(` - ${dep.fullName}@${dep.versionRange}`);
1937
+ }
1207
1938
  process.exit(1);
1208
1939
  }
1209
- console.log("No lockfile found. Nothing to install.");
1210
- return;
1211
- }
1212
- const skillCount = Object.keys(
1213
- lockfile.packages ?? lockfile.skills ?? {}
1214
- ).length;
1215
- if (skillCount === 0) {
1216
- console.log("No skills in lockfile. Nothing to install.");
1217
- return;
1218
- }
1219
- console.log(`Installing ${skillCount} skill(s)...
1940
+ console.log(`Resolving ${missingDeps.length} new dependency(ies)...
1220
1941
  `);
1221
- const packages = lockfile.packages ?? lockfile.skills ?? {};
1222
- const entries = Object.entries(packages);
1223
- for (const [fullName, entry] of entries) {
1224
- const match = fullName.match(/^@user\/([^/]+)\/([^/]+)$/);
1225
- if (!match) {
1226
- console.warn(`Warning: Invalid skill name in lockfile: ${fullName}`);
1227
- continue;
1228
- }
1229
- const [, username, name] = match;
1230
- console.log(`Installing ${fullName}@${entry.version}...`);
1231
- let tarballBuffer;
1232
- let fromCache = false;
1233
- const cachedTarball = await readFromCache(cacheDir, entry.integrity);
1234
- if (cachedTarball) {
1235
- tarballBuffer = cachedTarball;
1236
- fromCache = true;
1237
- } else {
1238
- const isPresignedUrl = entry.resolved.includes(".r2.cloudflarestorage.com") || entry.resolved.includes("X-Amz-Signature");
1942
+ configure2({ registryUrl, apiKey: apiKey ?? "" });
1943
+ for (const { fullName, versionRange } of missingDeps) {
1944
+ const parsed = parseSkillSpecifier(fullName);
1945
+ if (!parsed) {
1946
+ console.error(`Error: Invalid dependency specifier: ${fullName}`);
1947
+ continue;
1948
+ }
1949
+ const { username, name } = parsed;
1950
+ console.log(`Resolving ${fullName}@${versionRange}...`);
1951
+ const versionsResponse = await listSkillVersions(username, name);
1952
+ if (versionsResponse.status !== 200) {
1953
+ const errorMessage = extractApiErrorMessage(
1954
+ versionsResponse,
1955
+ `Skill ${fullName} not found`
1956
+ );
1957
+ console.error(`Error: ${errorMessage}`);
1958
+ continue;
1959
+ }
1960
+ const versions = versionsResponse.data;
1961
+ if (versions.length === 0) {
1962
+ console.error(`Error: Skill ${fullName} not found`);
1963
+ continue;
1964
+ }
1965
+ const versionStrings = versions.map(
1966
+ (v) => v.version
1967
+ );
1968
+ const resolved = resolveVersion(versionRange || "*", versionStrings);
1969
+ if (!resolved) {
1970
+ console.error(
1971
+ `Error: No version matching "${versionRange}" for ${fullName}`
1972
+ );
1973
+ continue;
1974
+ }
1975
+ const versionResponse = await getSkillVersion(username, name, resolved);
1976
+ if (versionResponse.status !== 200 || !versionResponse.data) {
1977
+ const errorMessage = extractApiErrorMessage(
1978
+ versionResponse,
1979
+ `Version ${resolved} not found`
1980
+ );
1981
+ console.error(`Error: ${errorMessage}`);
1982
+ continue;
1983
+ }
1984
+ const versionInfo = versionResponse.data;
1985
+ const isPresignedUrl = versionInfo.downloadUrl.includes(".r2.cloudflarestorage.com") || versionInfo.downloadUrl.includes("X-Amz-Signature");
1239
1986
  const downloadHeaders = {};
1240
1987
  if (!isPresignedUrl && apiKey) {
1241
1988
  downloadHeaders.Authorization = `Bearer ${apiKey}`;
1242
1989
  }
1243
- const response = await fetch(entry.resolved, {
1990
+ const tarballResponse = await fetch(versionInfo.downloadUrl, {
1244
1991
  headers: downloadHeaders,
1245
1992
  redirect: "follow"
1246
1993
  });
1247
- if (!response.ok) {
1248
- if (response.status === 401) {
1249
- if (!apiKey) {
1250
- console.error(
1251
- ` Error: ${fullName} requires authentication. Run 'pspm login' first.`
1252
- );
1994
+ if (!tarballResponse.ok) {
1995
+ console.error(
1996
+ `Error: Failed to download tarball for ${fullName} (${tarballResponse.status})`
1997
+ );
1998
+ continue;
1999
+ }
2000
+ const tarballBuffer = Buffer.from(await tarballResponse.arrayBuffer());
2001
+ const integrity = calculateIntegrity(tarballBuffer);
2002
+ await addToLockfile(fullName, {
2003
+ version: resolved,
2004
+ resolved: versionInfo.downloadUrl,
2005
+ integrity
2006
+ });
2007
+ await writeToCache(cacheDir, integrity, tarballBuffer);
2008
+ console.log(` Resolved ${fullName}@${resolved}`);
2009
+ }
2010
+ lockfile = await readLockfile();
2011
+ }
2012
+ const missingGitHubDeps = [];
2013
+ for (const [specifier, ref] of Object.entries(manifestGitHubDeps)) {
2014
+ if (!lockfileGitHubPackages[specifier]) {
2015
+ missingGitHubDeps.push({ specifier, ref });
2016
+ }
2017
+ }
2018
+ if (missingGitHubDeps.length > 0) {
2019
+ if (options.frozenLockfile) {
2020
+ console.error(
2021
+ "Error: GitHub dependencies in pspm.json are not in lockfile. Cannot install with --frozen-lockfile"
2022
+ );
2023
+ console.error("Missing GitHub dependencies:");
2024
+ for (const dep of missingGitHubDeps) {
2025
+ console.error(` - ${dep.specifier}@${dep.ref}`);
2026
+ }
2027
+ process.exit(1);
2028
+ }
2029
+ console.log(
2030
+ `
2031
+ Resolving ${missingGitHubDeps.length} GitHub dependency(ies)...
2032
+ `
2033
+ );
2034
+ for (const { specifier, ref } of missingGitHubDeps) {
2035
+ const parsed = parseGitHubSpecifier(specifier);
2036
+ if (!parsed) {
2037
+ console.error(`Error: Invalid GitHub specifier: ${specifier}`);
2038
+ continue;
2039
+ }
2040
+ parsed.ref = parsed.ref || ref;
2041
+ console.log(`Resolving ${getGitHubDisplayName(parsed)}...`);
2042
+ try {
2043
+ const result = await downloadGitHubPackage(parsed);
2044
+ await extractGitHubPackage(parsed, result.buffer, skillsDir);
2045
+ const entry = {
2046
+ version: result.commit.slice(0, 7),
2047
+ resolved: `https://github.com/${parsed.owner}/${parsed.repo}`,
2048
+ integrity: result.integrity,
2049
+ gitCommit: result.commit,
2050
+ gitRef: ref || "HEAD"
2051
+ };
2052
+ await addGitHubToLockfile(specifier, entry);
2053
+ await writeToCache(cacheDir, result.integrity, result.buffer);
2054
+ console.log(
2055
+ ` Resolved ${specifier} (${ref}@${result.commit.slice(0, 7)})`
2056
+ );
2057
+ } catch (error) {
2058
+ if (error instanceof GitHubRateLimitError) {
2059
+ console.error(`Error: ${error.message}`);
2060
+ } else if (error instanceof GitHubPathNotFoundError) {
2061
+ console.error(`Error: ${error.message}`);
2062
+ } else if (error instanceof GitHubNotFoundError) {
2063
+ console.error(`Error: ${error.message}`);
2064
+ } else {
2065
+ const message = error instanceof Error ? error.message : String(error);
2066
+ console.error(`Error resolving ${specifier}: ${message}`);
2067
+ }
2068
+ }
2069
+ }
2070
+ lockfile = await readLockfile();
2071
+ }
2072
+ const packages = lockfile?.packages ?? lockfile?.skills ?? {};
2073
+ const packageCount = Object.keys(packages).length;
2074
+ if (packageCount > 0) {
2075
+ console.log(`
2076
+ Installing ${packageCount} registry skill(s)...
2077
+ `);
2078
+ const entries = Object.entries(packages);
2079
+ for (const [fullName, entry] of entries) {
2080
+ const match = fullName.match(/^@user\/([^/]+)\/([^/]+)$/);
2081
+ if (!match) {
2082
+ console.warn(`Warning: Invalid skill name in lockfile: ${fullName}`);
2083
+ continue;
2084
+ }
2085
+ const [, username, name] = match;
2086
+ console.log(`Installing ${fullName}@${entry.version}...`);
2087
+ let tarballBuffer;
2088
+ let fromCache = false;
2089
+ const cachedTarball = await readFromCache(cacheDir, entry.integrity);
2090
+ if (cachedTarball) {
2091
+ tarballBuffer = cachedTarball;
2092
+ fromCache = true;
2093
+ } else {
2094
+ const isPresignedUrl = entry.resolved.includes(".r2.cloudflarestorage.com") || entry.resolved.includes("X-Amz-Signature");
2095
+ const downloadHeaders = {};
2096
+ if (!isPresignedUrl && apiKey) {
2097
+ downloadHeaders.Authorization = `Bearer ${apiKey}`;
2098
+ }
2099
+ const response = await fetch(entry.resolved, {
2100
+ headers: downloadHeaders,
2101
+ redirect: "follow"
2102
+ });
2103
+ if (!response.ok) {
2104
+ if (response.status === 401) {
2105
+ if (!apiKey) {
2106
+ console.error(
2107
+ ` Error: ${fullName} requires authentication. Run 'pspm login' first.`
2108
+ );
2109
+ } else {
2110
+ console.error(
2111
+ ` Error: Access denied to ${fullName}. You may not have permission to access this private package.`
2112
+ );
2113
+ }
1253
2114
  } else {
1254
2115
  console.error(
1255
- ` Error: Access denied to ${fullName}. You may not have permission to access this private package.`
2116
+ ` Error: Failed to download ${fullName} (${response.status})`
1256
2117
  );
1257
2118
  }
1258
- } else {
2119
+ continue;
2120
+ }
2121
+ tarballBuffer = Buffer.from(await response.arrayBuffer());
2122
+ const actualIntegrity = `sha256-${createHash("sha256").update(tarballBuffer).digest("base64")}`;
2123
+ if (actualIntegrity !== entry.integrity) {
1259
2124
  console.error(
1260
- ` Error: Failed to download ${fullName} (${response.status})`
2125
+ ` Error: Checksum verification failed for ${fullName}`
1261
2126
  );
2127
+ if (options.frozenLockfile) {
2128
+ process.exit(1);
2129
+ }
2130
+ continue;
1262
2131
  }
1263
- continue;
2132
+ await writeToCache(cacheDir, entry.integrity, tarballBuffer);
1264
2133
  }
1265
- tarballBuffer = Buffer.from(await response.arrayBuffer());
1266
- const actualIntegrity = `sha256-${createHash("sha256").update(tarballBuffer).digest("base64")}`;
1267
- if (actualIntegrity !== entry.integrity) {
1268
- console.error(
1269
- ` Error: Checksum verification failed for ${fullName}`
2134
+ const destDir = join(skillsDir, username, name);
2135
+ await rm(destDir, { recursive: true, force: true });
2136
+ await mkdir(destDir, { recursive: true });
2137
+ const tempFile = join(destDir, ".temp.tgz");
2138
+ await writeFile(tempFile, tarballBuffer);
2139
+ const { exec: exec2 } = await import('child_process');
2140
+ const { promisify: promisify2 } = await import('util');
2141
+ const execAsync = promisify2(exec2);
2142
+ try {
2143
+ await execAsync(
2144
+ `tar -xzf "${tempFile}" -C "${destDir}" --strip-components=1`
2145
+ );
2146
+ } finally {
2147
+ await rm(tempFile, { force: true });
2148
+ }
2149
+ console.log(
2150
+ ` Installed to ${destDir}${fromCache ? " (from cache)" : ""}`
2151
+ );
2152
+ installedSkills.push({
2153
+ name,
2154
+ sourcePath: getRegistrySkillPath(username, name)
2155
+ });
2156
+ }
2157
+ }
2158
+ const githubPackages = lockfile?.githubPackages ?? {};
2159
+ const githubCount = Object.keys(githubPackages).length;
2160
+ if (githubCount > 0) {
2161
+ console.log(`
2162
+ Installing ${githubCount} GitHub skill(s)...
2163
+ `);
2164
+ for (const [specifier, entry] of Object.entries(githubPackages)) {
2165
+ const parsed = parseGitHubSpecifier(specifier);
2166
+ if (!parsed) {
2167
+ console.warn(
2168
+ `Warning: Invalid GitHub specifier in lockfile: ${specifier}`
1270
2169
  );
1271
- if (options.frozenLockfile) {
1272
- process.exit(1);
1273
- }
1274
2170
  continue;
1275
2171
  }
1276
- await writeToCache(cacheDir, entry.integrity, tarballBuffer);
2172
+ const ghEntry = entry;
2173
+ console.log(
2174
+ `Installing ${specifier} (${ghEntry.gitRef}@${ghEntry.gitCommit.slice(0, 7)})...`
2175
+ );
2176
+ let tarballBuffer;
2177
+ let fromCache = false;
2178
+ const cachedTarball = await readFromCache(cacheDir, ghEntry.integrity);
2179
+ if (cachedTarball) {
2180
+ tarballBuffer = cachedTarball;
2181
+ fromCache = true;
2182
+ } else {
2183
+ try {
2184
+ const specWithCommit = { ...parsed, ref: ghEntry.gitCommit };
2185
+ const result = await downloadGitHubPackage(specWithCommit);
2186
+ tarballBuffer = result.buffer;
2187
+ if (result.integrity !== ghEntry.integrity) {
2188
+ console.error(
2189
+ ` Error: Checksum verification failed for ${specifier}`
2190
+ );
2191
+ if (options.frozenLockfile) {
2192
+ process.exit(1);
2193
+ }
2194
+ continue;
2195
+ }
2196
+ await writeToCache(cacheDir, ghEntry.integrity, tarballBuffer);
2197
+ } catch (error) {
2198
+ if (error instanceof GitHubRateLimitError) {
2199
+ console.error(` Error: ${error.message}`);
2200
+ } else if (error instanceof GitHubPathNotFoundError) {
2201
+ console.error(` Error: ${error.message}`);
2202
+ } else if (error instanceof GitHubNotFoundError) {
2203
+ console.error(` Error: ${error.message}`);
2204
+ } else {
2205
+ const message = error instanceof Error ? error.message : String(error);
2206
+ console.error(` Error downloading ${specifier}: ${message}`);
2207
+ }
2208
+ continue;
2209
+ }
2210
+ }
2211
+ try {
2212
+ const destPath = await extractGitHubPackage(
2213
+ parsed,
2214
+ tarballBuffer,
2215
+ skillsDir
2216
+ );
2217
+ console.log(
2218
+ ` Installed to ${destPath}${fromCache ? " (from cache)" : ""}`
2219
+ );
2220
+ const skillName = getGitHubSkillName(parsed);
2221
+ installedSkills.push({
2222
+ name: skillName,
2223
+ sourcePath: getGitHubSkillPath(
2224
+ parsed.owner,
2225
+ parsed.repo,
2226
+ parsed.path
2227
+ )
2228
+ });
2229
+ } catch (error) {
2230
+ const message = error instanceof Error ? error.message : String(error);
2231
+ console.error(` Error extracting ${specifier}: ${message}`);
2232
+ }
1277
2233
  }
1278
- const destDir = join(skillsDir, username, name);
1279
- await rm(destDir, { recursive: true, force: true });
1280
- await mkdir(destDir, { recursive: true });
1281
- const tempFile = join(destDir, ".temp.tgz");
1282
- await writeFile(tempFile, tarballBuffer);
1283
- const { exec: exec2 } = await import('child_process');
1284
- const { promisify: promisify2 } = await import('util');
1285
- const execAsync = promisify2(exec2);
1286
- try {
1287
- await execAsync(
1288
- `tar -xzf "${tempFile}" -C "${destDir}" --strip-components=1`
2234
+ }
2235
+ if (installedSkills.length > 0 && agents[0] !== "none") {
2236
+ console.log(`
2237
+ Creating symlinks for agent(s): ${agents.join(", ")}...`);
2238
+ await createAgentSymlinks(installedSkills, {
2239
+ agents,
2240
+ projectRoot: process.cwd(),
2241
+ agentConfigs
2242
+ });
2243
+ console.log(" Symlinks created.");
2244
+ }
2245
+ const totalCount = packageCount + githubCount;
2246
+ if (totalCount === 0) {
2247
+ console.log("No skills to install.");
2248
+ } else {
2249
+ console.log(`
2250
+ All ${totalCount} skill(s) installed.`);
2251
+ }
2252
+ } catch (error) {
2253
+ const message = error instanceof Error ? error.message : "Unknown error";
2254
+ console.error(`Error: ${message}`);
2255
+ process.exit(1);
2256
+ }
2257
+ }
2258
+
2259
+ // src/commands/link.ts
2260
+ async function link(options) {
2261
+ try {
2262
+ const manifest = await readManifest();
2263
+ const agentConfigs = manifest?.agents;
2264
+ let agents;
2265
+ if (options.agent) {
2266
+ agents = parseAgentArg(options.agent);
2267
+ } else if (manifest) {
2268
+ agents = parseAgentArg(void 0);
2269
+ } else if (options.yes) {
2270
+ agents = parseAgentArg(void 0);
2271
+ } else {
2272
+ console.log("No pspm.json found. Let's set up your project.\n");
2273
+ agents = await promptForAgents();
2274
+ }
2275
+ if (agents.length === 1 && agents[0] === "none") {
2276
+ console.log("Skipping symlink creation (--agent none)");
2277
+ return;
2278
+ }
2279
+ const skills = [];
2280
+ const registrySkills = await listLockfileSkills();
2281
+ for (const { name } of registrySkills) {
2282
+ const parsed = parseSkillSpecifier(name);
2283
+ if (!parsed) {
2284
+ console.warn(`Warning: Invalid skill name in lockfile: ${name}`);
2285
+ continue;
2286
+ }
2287
+ skills.push({
2288
+ name: parsed.name,
2289
+ sourcePath: getRegistrySkillPath(parsed.username, parsed.name)
2290
+ });
2291
+ }
2292
+ const githubSkills = await listLockfileGitHubPackages();
2293
+ for (const { specifier } of githubSkills) {
2294
+ const parsed = parseGitHubSpecifier(specifier);
2295
+ if (!parsed) {
2296
+ console.warn(
2297
+ `Warning: Invalid GitHub specifier in lockfile: ${specifier}`
1289
2298
  );
1290
- } finally {
1291
- await rm(tempFile, { force: true });
2299
+ continue;
1292
2300
  }
1293
- console.log(
1294
- ` Installed to ${destDir}${fromCache ? " (from cache)" : ""}`
1295
- );
2301
+ const skillName = getGitHubSkillName(parsed);
2302
+ skills.push({
2303
+ name: skillName,
2304
+ sourcePath: getGitHubSkillPath(parsed.owner, parsed.repo, parsed.path)
2305
+ });
2306
+ }
2307
+ if (skills.length === 0) {
2308
+ console.log("No skills found in lockfile. Nothing to link.");
2309
+ return;
2310
+ }
2311
+ console.log(
2312
+ `Creating symlinks for ${skills.length} skill(s) to agent(s): ${agents.join(", ")}...`
2313
+ );
2314
+ await createAgentSymlinks(skills, {
2315
+ agents,
2316
+ projectRoot: process.cwd(),
2317
+ agentConfigs
2318
+ });
2319
+ console.log("Symlinks created successfully.");
2320
+ console.log("\nLinked skills:");
2321
+ for (const skill of skills) {
2322
+ console.log(` ${skill.name} -> ${skill.sourcePath}`);
1296
2323
  }
1297
- console.log(`
1298
- All ${skillCount} skill(s) installed.`);
1299
2324
  } catch (error) {
1300
2325
  const message = error instanceof Error ? error.message : "Unknown error";
1301
2326
  console.error(`Error: ${message}`);
@@ -1304,7 +2329,76 @@ All ${skillCount} skill(s) installed.`);
1304
2329
  }
1305
2330
  async function list(options) {
1306
2331
  try {
1307
- const skills = await listLockfileSkills();
2332
+ const registrySkills = await listLockfileSkills();
2333
+ const githubSkills = await listLockfileGitHubPackages();
2334
+ const manifest = await readManifest();
2335
+ const agentConfigs = manifest?.agents;
2336
+ const availableAgents = getAvailableAgents(agentConfigs);
2337
+ const projectRoot = process.cwd();
2338
+ const skills = [];
2339
+ for (const { name: fullName, entry } of registrySkills) {
2340
+ const match = fullName.match(/^@user\/([^/]+)\/([^/]+)$/);
2341
+ if (!match) continue;
2342
+ const [, username, skillName] = match;
2343
+ const sourcePath = getRegistrySkillPath(username, skillName);
2344
+ const absolutePath = join(projectRoot, sourcePath);
2345
+ let status = "installed";
2346
+ try {
2347
+ await access$1(absolutePath);
2348
+ } catch {
2349
+ status = "missing";
2350
+ }
2351
+ const linkedAgents = await getLinkedAgents(
2352
+ skillName,
2353
+ availableAgents,
2354
+ projectRoot,
2355
+ agentConfigs
2356
+ );
2357
+ skills.push({
2358
+ name: skillName,
2359
+ fullName,
2360
+ version: entry.version,
2361
+ source: "registry",
2362
+ sourcePath,
2363
+ status,
2364
+ linkedAgents
2365
+ });
2366
+ }
2367
+ for (const { specifier, entry } of githubSkills) {
2368
+ const parsed = parseGitHubSpecifier(specifier);
2369
+ if (!parsed) continue;
2370
+ const ghEntry = entry;
2371
+ const skillName = getGitHubSkillName(parsed);
2372
+ const sourcePath = getGitHubSkillPath(
2373
+ parsed.owner,
2374
+ parsed.repo,
2375
+ parsed.path
2376
+ );
2377
+ const absolutePath = join(projectRoot, sourcePath);
2378
+ let status = "installed";
2379
+ try {
2380
+ await access$1(absolutePath);
2381
+ } catch {
2382
+ status = "missing";
2383
+ }
2384
+ const linkedAgents = await getLinkedAgents(
2385
+ skillName,
2386
+ availableAgents,
2387
+ projectRoot,
2388
+ agentConfigs
2389
+ );
2390
+ skills.push({
2391
+ name: skillName,
2392
+ fullName: specifier,
2393
+ version: ghEntry.gitCommit.slice(0, 7),
2394
+ source: "github",
2395
+ sourcePath,
2396
+ status,
2397
+ linkedAgents,
2398
+ gitRef: ghEntry.gitRef,
2399
+ gitCommit: ghEntry.gitCommit
2400
+ });
2401
+ }
1308
2402
  if (skills.length === 0) {
1309
2403
  console.log("No skills installed.");
1310
2404
  return;
@@ -1313,26 +2407,33 @@ async function list(options) {
1313
2407
  console.log(JSON.stringify(skills, null, 2));
1314
2408
  return;
1315
2409
  }
1316
- const skillsDir = getSkillsDir();
1317
2410
  console.log("Installed skills:\n");
1318
- for (const { name, entry } of skills) {
1319
- const match = name.match(/^@user\/([^/]+)\/([^/]+)$/);
1320
- if (!match) continue;
1321
- const [, username, skillName] = match;
1322
- const skillPath = join(skillsDir, username, skillName);
1323
- let status = "installed";
1324
- try {
1325
- await access$1(skillPath);
1326
- } catch {
1327
- status = "missing";
2411
+ for (const skill of skills) {
2412
+ if (skill.source === "registry") {
2413
+ console.log(` ${skill.fullName}@${skill.version} (registry)`);
2414
+ } else {
2415
+ const refInfo = skill.gitRef ? `${skill.gitRef}@${skill.gitCommit?.slice(0, 7)}` : skill.version;
2416
+ console.log(` ${skill.fullName} (${refInfo})`);
1328
2417
  }
1329
- console.log(` ${name}@${entry.version}`);
1330
- if (status === "missing") {
2418
+ if (skill.status === "missing") {
1331
2419
  console.log(` Status: MISSING (run 'pspm install' to restore)`);
1332
2420
  }
2421
+ if (skill.linkedAgents.length > 0) {
2422
+ for (const agent of skill.linkedAgents) {
2423
+ const config2 = resolveAgentConfig(agent, agentConfigs);
2424
+ if (config2) {
2425
+ console.log(` -> ${config2.skillsDir}/${skill.name}`);
2426
+ }
2427
+ }
2428
+ }
1333
2429
  }
2430
+ const registryCount = skills.filter((s) => s.source === "registry").length;
2431
+ const githubCount = skills.filter((s) => s.source === "github").length;
2432
+ const parts = [];
2433
+ if (registryCount > 0) parts.push(`${registryCount} registry`);
2434
+ if (githubCount > 0) parts.push(`${githubCount} github`);
1334
2435
  console.log(`
1335
- Total: ${skills.length} skill(s)`);
2436
+ Total: ${skills.length} skill(s) (${parts.join(", ")})`);
1336
2437
  } catch (error) {
1337
2438
  const message = error instanceof Error ? error.message : "Unknown error";
1338
2439
  console.error(`Error: ${message}`);
@@ -1837,59 +2938,103 @@ Setting visibility to ${options.access}...`);
1837
2938
  }
1838
2939
  async function remove(nameOrSpecifier) {
1839
2940
  try {
1840
- await requireApiKey();
1841
- let fullName;
1842
- let username;
1843
- let name;
1844
- if (nameOrSpecifier.startsWith("@user/")) {
1845
- const match = nameOrSpecifier.match(/^@user\/([^/]+)\/([^@/]+)/);
1846
- if (!match) {
1847
- console.error(`Error: Invalid skill specifier: ${nameOrSpecifier}`);
1848
- process.exit(1);
1849
- }
1850
- fullName = `@user/${match[1]}/${match[2]}`;
1851
- username = match[1];
1852
- name = match[2];
2941
+ const manifest = await readManifest();
2942
+ const agentConfigs = manifest?.agents;
2943
+ const agents = getAvailableAgents(agentConfigs);
2944
+ if (isGitHubSpecifier(nameOrSpecifier)) {
2945
+ await removeGitHub(nameOrSpecifier, agents, agentConfigs);
2946
+ } else if (nameOrSpecifier.startsWith("@user/")) {
2947
+ await removeRegistry(nameOrSpecifier, agents, agentConfigs);
1853
2948
  } else {
1854
- const skills = await listLockfileSkills();
1855
- const found = skills.find((s) => {
1856
- const match2 = s.name.match(/^@user\/([^/]+)\/([^/]+)$/);
1857
- return match2 && match2[2] === nameOrSpecifier;
1858
- });
1859
- if (!found) {
1860
- console.error(
1861
- `Error: Skill "${nameOrSpecifier}" not found in lockfile`
1862
- );
1863
- process.exit(1);
1864
- }
1865
- fullName = found.name;
1866
- const match = fullName.match(/^@user\/([^/]+)\/([^/]+)$/);
1867
- if (!match) {
1868
- console.error(`Error: Invalid skill name in lockfile: ${fullName}`);
1869
- process.exit(1);
1870
- }
1871
- username = match[1];
1872
- name = match[2];
1873
- }
1874
- console.log(`Removing ${fullName}...`);
1875
- const removed = await removeFromLockfile(fullName);
1876
- if (!removed) {
1877
- console.error(`Error: ${fullName} not found in lockfile`);
1878
- process.exit(1);
1879
- }
1880
- const skillsDir = getSkillsDir();
1881
- const destDir = join(skillsDir, username, name);
1882
- try {
1883
- await rm(destDir, { recursive: true, force: true });
1884
- } catch {
2949
+ await removeByShortName(nameOrSpecifier, agents, agentConfigs);
1885
2950
  }
1886
- console.log(`Removed ${fullName}`);
1887
2951
  } catch (error) {
1888
2952
  const message = error instanceof Error ? error.message : "Unknown error";
1889
2953
  console.error(`Error: ${message}`);
1890
2954
  process.exit(1);
1891
2955
  }
1892
2956
  }
2957
+ async function removeRegistry(specifier, agents, agentConfigs) {
2958
+ const match = specifier.match(/^@user\/([^/]+)\/([^@/]+)/);
2959
+ if (!match) {
2960
+ console.error(`Error: Invalid skill specifier: ${specifier}`);
2961
+ process.exit(1);
2962
+ }
2963
+ const fullName = `@user/${match[1]}/${match[2]}`;
2964
+ const username = match[1];
2965
+ const name = match[2];
2966
+ console.log(`Removing ${fullName}...`);
2967
+ const removedFromLockfile = await removeFromLockfile(fullName);
2968
+ const removedFromManifest = await removeDependency(fullName);
2969
+ if (!removedFromLockfile && !removedFromManifest) {
2970
+ console.error(`Error: ${fullName} not found in lockfile or pspm.json`);
2971
+ process.exit(1);
2972
+ }
2973
+ await removeAgentSymlinks(name, {
2974
+ agents,
2975
+ projectRoot: process.cwd(),
2976
+ agentConfigs
2977
+ });
2978
+ const skillsDir = getSkillsDir();
2979
+ const destDir = join(skillsDir, username, name);
2980
+ try {
2981
+ await rm(destDir, { recursive: true, force: true });
2982
+ } catch {
2983
+ }
2984
+ console.log(`Removed ${fullName}`);
2985
+ }
2986
+ async function removeGitHub(specifier, agents, agentConfigs) {
2987
+ const parsed = parseGitHubSpecifier(specifier);
2988
+ if (!parsed) {
2989
+ console.error(`Error: Invalid GitHub specifier: ${specifier}`);
2990
+ process.exit(1);
2991
+ }
2992
+ const lockfileKey = parsed.path ? `github:${parsed.owner}/${parsed.repo}/${parsed.path}` : `github:${parsed.owner}/${parsed.repo}`;
2993
+ console.log(`Removing ${lockfileKey}...`);
2994
+ const removedFromLockfile = await removeGitHubFromLockfile(lockfileKey);
2995
+ const removedFromManifest = await removeGitHubDependency(lockfileKey);
2996
+ if (!removedFromLockfile && !removedFromManifest) {
2997
+ console.error(`Error: ${lockfileKey} not found in lockfile or pspm.json`);
2998
+ process.exit(1);
2999
+ }
3000
+ const skillName = getGitHubSkillName(parsed);
3001
+ await removeAgentSymlinks(skillName, {
3002
+ agents,
3003
+ projectRoot: process.cwd(),
3004
+ agentConfigs
3005
+ });
3006
+ const skillsDir = getSkillsDir();
3007
+ const destPath = getGitHubSkillPath(parsed.owner, parsed.repo, parsed.path);
3008
+ const destDir = join(skillsDir, "..", destPath);
3009
+ try {
3010
+ await rm(destDir, { recursive: true, force: true });
3011
+ } catch {
3012
+ }
3013
+ console.log(`Removed ${lockfileKey}`);
3014
+ }
3015
+ async function removeByShortName(shortName, agents, agentConfigs) {
3016
+ const registrySkills = await listLockfileSkills();
3017
+ const foundRegistry = registrySkills.find((s) => {
3018
+ const match = s.name.match(/^@user\/([^/]+)\/([^/]+)$/);
3019
+ return match && match[2] === shortName;
3020
+ });
3021
+ if (foundRegistry) {
3022
+ await removeRegistry(foundRegistry.name, agents, agentConfigs);
3023
+ return;
3024
+ }
3025
+ const githubSkills = await listLockfileGitHubPackages();
3026
+ const foundGitHub = githubSkills.find((s) => {
3027
+ const parsed = parseGitHubSpecifier(s.specifier);
3028
+ if (!parsed) return false;
3029
+ return getGitHubSkillName(parsed) === shortName;
3030
+ });
3031
+ if (foundGitHub) {
3032
+ await removeGitHub(foundGitHub.specifier, agents, agentConfigs);
3033
+ return;
3034
+ }
3035
+ console.error(`Error: Skill "${shortName}" not found in lockfile`);
3036
+ process.exit(1);
3037
+ }
1893
3038
 
1894
3039
  // src/commands/unpublish.ts
1895
3040
  async function unpublish(specifier, options) {
@@ -2075,12 +3220,13 @@ program.command("logout").description("Log out and clear stored credentials").ac
2075
3220
  program.command("whoami").description("Show current user information").action(async () => {
2076
3221
  await whoami();
2077
3222
  });
2078
- program.command("init").description("Create a new pspm.json manifest in the current directory").option("-n, --name <name>", "Skill name").option("-d, --description <desc>", "Skill description").option("-a, --author <author>", "Author name").option("-y, --yes", "Skip prompts and use defaults").action(async (options) => {
3223
+ program.command("init").description("Create a new pspm.json manifest in the current directory").option("-n, --name <name>", "Skill name").option("-d, --description <desc>", "Skill description").option("-a, --author <author>", "Author name").option("-y, --yes", "Skip prompts and use defaults").option("-f, --force", "Overwrite existing pspm.json").action(async (options) => {
2079
3224
  await init({
2080
3225
  name: options.name,
2081
3226
  description: options.description,
2082
3227
  author: options.author,
2083
- yes: options.yes
3228
+ yes: options.yes,
3229
+ force: options.force
2084
3230
  });
2085
3231
  });
2086
3232
  program.command("migrate").description(
@@ -2088,8 +3234,17 @@ program.command("migrate").description(
2088
3234
  ).option("--dry-run", "Show what would be migrated without making changes").action(async (options) => {
2089
3235
  await migrate({ dryRun: options.dryRun });
2090
3236
  });
2091
- program.command("add <specifier>").description("Add a skill (e.g., @user/bsheng/vite_slides@^2.0.0)").option("--save", "Save to lockfile (default)").action(async (specifier, options) => {
2092
- await add(specifier, { save: options.save ?? true });
3237
+ program.command("add <specifier>").description(
3238
+ "Add a skill (e.g., @user/bsheng/vite_slides@^2.0.0 or github:owner/repo/path@ref)"
3239
+ ).option("--save", "Save to lockfile (default)").option(
3240
+ "--agent <agents>",
3241
+ 'Comma-separated agents for symlinks (default: "claude-code", use "none" to skip)'
3242
+ ).option("-y, --yes", "Skip agent selection prompt and use defaults").action(async (specifier, options) => {
3243
+ await add(specifier, {
3244
+ save: options.save ?? true,
3245
+ agent: options.agent,
3246
+ yes: options.yes
3247
+ });
2093
3248
  });
2094
3249
  program.command("remove <name>").alias("rm").description("Remove an installed skill").action(async (name) => {
2095
3250
  await remove(name);
@@ -2097,12 +3252,23 @@ program.command("remove <name>").alias("rm").description("Remove an installed sk
2097
3252
  program.command("list").alias("ls").description("List installed skills").option("--json", "Output as JSON").action(async (options) => {
2098
3253
  await list({ json: options.json });
2099
3254
  });
2100
- program.command("install").alias("i").description("Install all skills from lockfile").option("--frozen-lockfile", "Fail if lockfile is missing or outdated").option("--dir <path>", "Install skills to a specific directory").action(async (options) => {
3255
+ program.command("install").alias("i").description("Install all skills from lockfile").option("--frozen-lockfile", "Fail if lockfile is missing or outdated").option("--dir <path>", "Install skills to a specific directory").option(
3256
+ "--agent <agents>",
3257
+ 'Comma-separated agents for symlinks (default: "claude-code", use "none" to skip)'
3258
+ ).option("-y, --yes", "Skip agent selection prompt and use defaults").action(async (options) => {
2101
3259
  await install({
2102
3260
  frozenLockfile: options.frozenLockfile,
2103
- dir: options.dir
3261
+ dir: options.dir,
3262
+ agent: options.agent,
3263
+ yes: options.yes
2104
3264
  });
2105
3265
  });
3266
+ program.command("link").description("Recreate agent symlinks without reinstalling").option(
3267
+ "--agent <agents>",
3268
+ 'Comma-separated agents for symlinks (default: "claude-code", use "none" to skip)'
3269
+ ).option("-y, --yes", "Skip agent selection prompt and use defaults").action(async (options) => {
3270
+ await link({ agent: options.agent, yes: options.yes });
3271
+ });
2106
3272
  program.command("update").description("Update all skills to latest compatible versions").option("--dry-run", "Show what would be updated without making changes").action(async (options) => {
2107
3273
  await update({ dryRun: options.dryRun });
2108
3274
  });