@anytio/pspm 0.0.5 → 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
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFileSync } from 'fs';
3
- import { dirname, join, relative } from 'path';
4
- import { fileURLToPath, URL } from 'url';
3
+ import { dirname, join, basename, relative } from 'path';
4
+ import { fileURLToPath, URL as URL$1 } from 'url';
5
5
  import { Command } from 'commander';
6
- import { stat, writeFile, mkdir, rm, access, readFile, readdir, unlink } from 'fs/promises';
7
6
  import { createHash, randomBytes } from 'crypto';
8
7
  import * as semver from 'semver';
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';
@@ -18,6 +20,36 @@ function calculateIntegrity(data) {
18
20
  return `sha256-${hash}`;
19
21
  }
20
22
 
23
+ // ../../packages/shared/pspm-types/src/manifest.ts
24
+ var DEFAULT_SKILL_FILES = [
25
+ "SKILL.md",
26
+ "runtime",
27
+ "scripts",
28
+ "data"
29
+ ];
30
+ var PSPM_SCHEMA_URL = "https://pspm.dev/schema/v1/pspm.json";
31
+ function validateManifest(manifest) {
32
+ if (!manifest.name) {
33
+ return { valid: false, error: "Manifest must have a 'name' field" };
34
+ }
35
+ if (!manifest.version) {
36
+ return { valid: false, error: "Manifest must have a 'version' field" };
37
+ }
38
+ if (!/^[a-z][a-z0-9_-]*$/.test(manifest.name)) {
39
+ return {
40
+ valid: false,
41
+ error: "Name must start with a lowercase letter and contain only lowercase letters, numbers, hyphens, and underscores"
42
+ };
43
+ }
44
+ if (!/^\d+\.\d+\.\d+/.test(manifest.version)) {
45
+ return {
46
+ valid: false,
47
+ error: "Version must be a valid semantic version (e.g., 1.0.0)"
48
+ };
49
+ }
50
+ return { valid: true };
51
+ }
52
+
21
53
  // ../../packages/shared/pspm-types/src/specifier.ts
22
54
  var SPECIFIER_PATTERN = /^@user\/([a-zA-Z0-9_-]+)\/([a-z][a-z0-9_-]*)(?:@(.+))?$/;
23
55
  function parseSkillSpecifier(specifier) {
@@ -31,6 +63,41 @@ function parseSkillSpecifier(specifier) {
31
63
  versionRange: match[3]
32
64
  };
33
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
+ }
34
101
  function resolveVersion(range, availableVersions) {
35
102
  const sorted = availableVersions.filter((v) => semver.valid(v)).sort((a, b) => semver.rcompare(a, b));
36
103
  if (!range || range === "latest" || range === "*") {
@@ -53,13 +120,16 @@ function getConfig() {
53
120
  async function customFetch(url, options) {
54
121
  const { baseUrl, apiKey } = getConfig();
55
122
  const fullUrl = `${baseUrl}${url}`;
123
+ const headers = {
124
+ ...options.headers ?? {},
125
+ "Content-Type": "application/json"
126
+ };
127
+ if (apiKey) {
128
+ headers.Authorization = `Bearer ${apiKey}`;
129
+ }
56
130
  const response = await fetch(fullUrl, {
57
131
  ...options,
58
- headers: {
59
- ...options.headers,
60
- Authorization: `Bearer ${apiKey}`,
61
- "Content-Type": "application/json"
62
- }
132
+ headers
63
133
  });
64
134
  const text = await response.text();
65
135
  let data = null;
@@ -179,6 +249,102 @@ async function whoamiRequest(registryUrl, apiKey) {
179
249
  return null;
180
250
  }
181
251
  }
252
+ async function deprecateSkillVersion(skillName, version2, message) {
253
+ const config2 = getConfig();
254
+ if (!config2) {
255
+ return { status: 401, error: "SDK not configured" };
256
+ }
257
+ try {
258
+ const response = await fetch(
259
+ `${config2.baseUrl}/api/skills/${skillName}/${version2}/deprecate`,
260
+ {
261
+ method: "POST",
262
+ headers: {
263
+ "Content-Type": "application/json",
264
+ Authorization: `Bearer ${config2.apiKey}`
265
+ },
266
+ body: JSON.stringify({ message })
267
+ }
268
+ );
269
+ if (!response.ok) {
270
+ const error = await response.text();
271
+ return { status: response.status, error };
272
+ }
273
+ const data = await response.json();
274
+ return { status: response.status, data };
275
+ } catch (error) {
276
+ return {
277
+ status: 500,
278
+ error: error instanceof Error ? error.message : "Unknown error"
279
+ };
280
+ }
281
+ }
282
+ async function undeprecateSkillVersion(skillName, version2) {
283
+ const config2 = getConfig();
284
+ if (!config2) {
285
+ return { status: 401, error: "SDK not configured" };
286
+ }
287
+ try {
288
+ const response = await fetch(
289
+ `${config2.baseUrl}/api/skills/${skillName}/${version2}/deprecate`,
290
+ {
291
+ method: "DELETE",
292
+ headers: {
293
+ Authorization: `Bearer ${config2.apiKey}`
294
+ }
295
+ }
296
+ );
297
+ if (!response.ok) {
298
+ const error = await response.text();
299
+ return { status: response.status, error };
300
+ }
301
+ const data = await response.json();
302
+ return { status: response.status, data };
303
+ } catch (error) {
304
+ return {
305
+ status: 500,
306
+ error: error instanceof Error ? error.message : "Unknown error"
307
+ };
308
+ }
309
+ }
310
+ async function changeSkillAccess(skillName, input) {
311
+ const config2 = getConfig();
312
+ if (!config2) {
313
+ return { status: 401, error: "SDK not configured" };
314
+ }
315
+ try {
316
+ const response = await fetch(
317
+ `${config2.baseUrl}/api/skills/${skillName}/access`,
318
+ {
319
+ method: "POST",
320
+ headers: {
321
+ "Content-Type": "application/json",
322
+ Authorization: `Bearer ${config2.apiKey}`
323
+ },
324
+ body: JSON.stringify(input)
325
+ }
326
+ );
327
+ if (!response.ok) {
328
+ const error = await response.text();
329
+ try {
330
+ const errorJson = JSON.parse(error);
331
+ return {
332
+ status: response.status,
333
+ error: errorJson.message || errorJson.error || error
334
+ };
335
+ } catch {
336
+ return { status: response.status, error };
337
+ }
338
+ }
339
+ const data = await response.json();
340
+ return { status: response.status, data };
341
+ } catch (error) {
342
+ return {
343
+ status: 500,
344
+ error: error instanceof Error ? error.message : "Unknown error"
345
+ };
346
+ }
347
+ }
182
348
 
183
349
  // src/errors.ts
184
350
  var ConfigError = class extends Error {
@@ -238,12 +404,24 @@ function getConfigPath() {
238
404
  function getLegacyConfigPath() {
239
405
  return join(homedir(), ".pspm", "config.json");
240
406
  }
407
+ function getPspmDir() {
408
+ return join(process.cwd(), ".pspm");
409
+ }
241
410
  function getSkillsDir() {
242
- return join(process.cwd(), ".skills");
411
+ return join(process.cwd(), ".pspm", "skills");
412
+ }
413
+ function getCacheDir() {
414
+ return join(process.cwd(), ".pspm", "cache");
243
415
  }
244
416
  function getLockfilePath() {
417
+ return join(process.cwd(), "pspm-lock.json");
418
+ }
419
+ function getLegacyLockfilePath() {
245
420
  return join(process.cwd(), "skill-lock.json");
246
421
  }
422
+ function getLegacySkillsDir() {
423
+ return join(process.cwd(), ".skills");
424
+ }
247
425
  async function readUserConfig() {
248
426
  const configPath = getConfigPath();
249
427
  if (process.env.PSPM_DEBUG) {
@@ -255,10 +433,35 @@ async function readUserConfig() {
255
433
  if (process.env.PSPM_DEBUG) {
256
434
  console.log(`[config] Parsed config:`, JSON.stringify(parsed, null, 2));
257
435
  }
436
+ const scopedRegistries = {};
437
+ for (const key of Object.keys(parsed)) {
438
+ const scopeMatch = key.match(/^(@[^:]+):registry$/);
439
+ if (scopeMatch) {
440
+ const scope = scopeMatch[1];
441
+ scopedRegistries[scope] = parsed[key];
442
+ }
443
+ }
444
+ const registryTokens = {};
445
+ for (const key of Object.keys(parsed)) {
446
+ const tokenMatch = key.match(/^\/\/([^:]+):authToken$/);
447
+ if (tokenMatch) {
448
+ const host = tokenMatch[1];
449
+ registryTokens[host] = parsed[key];
450
+ }
451
+ if (key.startsWith("//") && typeof parsed[key] === "object") {
452
+ const host = key.slice(2);
453
+ const section = parsed[key];
454
+ if (section.authToken) {
455
+ registryTokens[host] = section.authToken;
456
+ }
457
+ }
458
+ }
258
459
  return {
259
460
  registry: parsed.registry,
260
461
  authToken: parsed.authToken,
261
- username: parsed.username
462
+ username: parsed.username,
463
+ scopedRegistries: Object.keys(scopedRegistries).length > 0 ? scopedRegistries : void 0,
464
+ registryTokens: Object.keys(registryTokens).length > 0 ? registryTokens : void 0
262
465
  };
263
466
  } catch (error) {
264
467
  if (process.env.PSPM_DEBUG) {
@@ -374,6 +577,8 @@ async function resolveConfig() {
374
577
  let registryUrl = DEFAULT_REGISTRY_URL;
375
578
  let apiKey = userConfig.authToken;
376
579
  const username = userConfig.username;
580
+ const scopedRegistries = userConfig.scopedRegistries ?? {};
581
+ const registryTokens = userConfig.registryTokens ?? {};
377
582
  if (userConfig.registry) {
378
583
  registryUrl = userConfig.registry;
379
584
  }
@@ -391,13 +596,33 @@ async function resolveConfig() {
391
596
  console.log(`[config] registryUrl: ${registryUrl}`);
392
597
  console.log(`[config] apiKey: ${apiKey ? "***" : "(not set)"}`);
393
598
  console.log(`[config] username: ${username || "(not set)"}`);
599
+ console.log(
600
+ `[config] scopedRegistries: ${JSON.stringify(scopedRegistries)}`
601
+ );
602
+ console.log(
603
+ `[config] registryTokens: ${Object.keys(registryTokens).length} configured`
604
+ );
394
605
  }
395
606
  return {
396
607
  registryUrl,
397
608
  apiKey,
398
- username
609
+ username,
610
+ scopedRegistries,
611
+ registryTokens
399
612
  };
400
613
  }
614
+ function getTokenForRegistry(config2, registryUrl) {
615
+ try {
616
+ const url = new URL(registryUrl);
617
+ const host = url.host;
618
+ if (config2.registryTokens[host]) {
619
+ return config2.registryTokens[host];
620
+ }
621
+ return config2.apiKey;
622
+ } catch {
623
+ return config2.apiKey;
624
+ }
625
+ }
401
626
  async function setCredentials(authToken, username, registry) {
402
627
  const config2 = await readUserConfig();
403
628
  config2.authToken = authToken;
@@ -442,43 +667,438 @@ async function getRegistryUrl() {
442
667
  const resolved = await resolveConfig();
443
668
  return resolved.registryUrl;
444
669
  }
670
+
671
+ // src/commands/access.ts
672
+ async function access(specifier, options) {
673
+ try {
674
+ const apiKey = await requireApiKey();
675
+ const registryUrl = await getRegistryUrl();
676
+ if (options.public && options.private) {
677
+ console.error("Error: Cannot specify both --public and --private");
678
+ process.exit(1);
679
+ }
680
+ if (!options.public && !options.private) {
681
+ console.error("Error: Must specify either --public or --private");
682
+ process.exit(1);
683
+ }
684
+ const visibility = options.public ? "public" : "private";
685
+ let packageName;
686
+ if (specifier) {
687
+ const parsed = parseSkillSpecifier(specifier);
688
+ if (!parsed) {
689
+ console.error(
690
+ `Error: Invalid skill specifier "${specifier}". Use format: @user/{username}/{name}`
691
+ );
692
+ process.exit(1);
693
+ }
694
+ packageName = parsed.name;
695
+ } else {
696
+ const { readFile: readFile7 } = await import('fs/promises');
697
+ const { join: join13 } = await import('path');
698
+ let manifest = null;
699
+ try {
700
+ const content = await readFile7(
701
+ join13(process.cwd(), "pspm.json"),
702
+ "utf-8"
703
+ );
704
+ manifest = JSON.parse(content);
705
+ } catch {
706
+ try {
707
+ const content = await readFile7(
708
+ join13(process.cwd(), "package.json"),
709
+ "utf-8"
710
+ );
711
+ manifest = JSON.parse(content);
712
+ } catch {
713
+ console.error(
714
+ "Error: No pspm.json or package.json found in current directory"
715
+ );
716
+ console.error(
717
+ "Either run this command in a package directory or specify a package name"
718
+ );
719
+ process.exit(1);
720
+ }
721
+ }
722
+ if (!manifest?.name) {
723
+ console.error("Error: Package manifest is missing 'name' field");
724
+ process.exit(1);
725
+ }
726
+ packageName = manifest.name;
727
+ }
728
+ configure2({ registryUrl, apiKey });
729
+ console.log(`Setting ${packageName} to ${visibility}...`);
730
+ const response = await changeSkillAccess(packageName, { visibility });
731
+ if (response.status !== 200 || !response.data) {
732
+ const errorMessage = response.error ?? "Failed to change visibility";
733
+ console.error(`Error: ${errorMessage}`);
734
+ process.exit(1);
735
+ }
736
+ const result = response.data;
737
+ console.log(
738
+ `+ @user/${result.username}/${result.name} is now ${result.visibility}`
739
+ );
740
+ if (visibility === "public") {
741
+ console.log("");
742
+ console.log(
743
+ "Note: This action is irreversible. Public packages cannot be made private."
744
+ );
745
+ }
746
+ } catch (error) {
747
+ const message = error instanceof Error ? error.message : "Unknown error";
748
+ console.error(`Error: ${message}`);
749
+ process.exit(1);
750
+ }
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
+ }
989
+ async function hasLegacyLockfile() {
990
+ try {
991
+ await stat(getLegacyLockfilePath());
992
+ return true;
993
+ } catch {
994
+ return false;
995
+ }
996
+ }
997
+ async function migrateLockfileIfNeeded() {
998
+ const legacyPath = getLegacyLockfilePath();
999
+ const newPath = getLockfilePath();
1000
+ try {
1001
+ await stat(legacyPath);
1002
+ } catch {
1003
+ return false;
1004
+ }
1005
+ try {
1006
+ await stat(newPath);
1007
+ return false;
1008
+ } catch {
1009
+ }
1010
+ try {
1011
+ const content = await readFile(legacyPath, "utf-8");
1012
+ const oldLockfile = JSON.parse(content);
1013
+ const newLockfile = {
1014
+ lockfileVersion: 2,
1015
+ registryUrl: oldLockfile.registryUrl,
1016
+ packages: oldLockfile.skills ?? {}
1017
+ };
1018
+ await writeFile(newPath, `${JSON.stringify(newLockfile, null, 2)}
1019
+ `);
1020
+ console.log(`Migrated lockfile: skill-lock.json \u2192 pspm-lock.json`);
1021
+ return true;
1022
+ } catch {
1023
+ return false;
1024
+ }
1025
+ }
445
1026
  async function readLockfile() {
446
1027
  const lockfilePath = getLockfilePath();
447
1028
  try {
448
1029
  const content = await readFile(lockfilePath, "utf-8");
449
- return JSON.parse(content);
1030
+ const lockfile = JSON.parse(content);
1031
+ if (lockfile.lockfileVersion === 1 && lockfile.skills && !lockfile.packages) {
1032
+ return {
1033
+ ...lockfile,
1034
+ lockfileVersion: 2,
1035
+ packages: lockfile.skills
1036
+ };
1037
+ }
1038
+ return lockfile;
450
1039
  } catch {
1040
+ if (await hasLegacyLockfile()) {
1041
+ try {
1042
+ const content = await readFile(getLegacyLockfilePath(), "utf-8");
1043
+ const legacyLockfile = JSON.parse(content);
1044
+ return {
1045
+ lockfileVersion: 2,
1046
+ registryUrl: legacyLockfile.registryUrl,
1047
+ packages: legacyLockfile.skills ?? {}
1048
+ };
1049
+ } catch {
1050
+ return null;
1051
+ }
1052
+ }
451
1053
  return null;
452
1054
  }
453
1055
  }
454
1056
  async function writeLockfile(lockfile) {
455
1057
  const lockfilePath = getLockfilePath();
456
1058
  await mkdir(dirname(lockfilePath), { recursive: true });
457
- await writeFile(lockfilePath, `${JSON.stringify(lockfile, null, 2)}
1059
+ const normalized = {
1060
+ lockfileVersion: 3,
1061
+ registryUrl: lockfile.registryUrl,
1062
+ packages: lockfile.packages ?? lockfile.skills ?? {}
1063
+ };
1064
+ if (lockfile.githubPackages && Object.keys(lockfile.githubPackages).length > 0) {
1065
+ normalized.githubPackages = lockfile.githubPackages;
1066
+ }
1067
+ await writeFile(lockfilePath, `${JSON.stringify(normalized, null, 2)}
458
1068
  `);
459
1069
  }
460
1070
  async function createEmptyLockfile() {
461
1071
  const registryUrl = await getRegistryUrl();
462
1072
  return {
463
- lockfileVersion: 1,
1073
+ lockfileVersion: 3,
464
1074
  registryUrl,
465
- skills: {}
1075
+ packages: {}
466
1076
  };
467
1077
  }
1078
+ function getPackages(lockfile) {
1079
+ return lockfile.packages ?? lockfile.skills ?? {};
1080
+ }
468
1081
  async function addToLockfile(fullName, entry) {
469
1082
  let lockfile = await readLockfile();
470
1083
  if (!lockfile) {
471
1084
  lockfile = await createEmptyLockfile();
472
1085
  }
473
- lockfile.skills[fullName] = entry;
1086
+ const packages = getPackages(lockfile);
1087
+ packages[fullName] = entry;
1088
+ lockfile.packages = packages;
474
1089
  await writeLockfile(lockfile);
475
1090
  }
476
1091
  async function removeFromLockfile(fullName) {
477
1092
  const lockfile = await readLockfile();
478
- if (!lockfile || !lockfile.skills[fullName]) {
1093
+ if (!lockfile) {
479
1094
  return false;
480
1095
  }
481
- delete lockfile.skills[fullName];
1096
+ const packages = getPackages(lockfile);
1097
+ if (!packages[fullName]) {
1098
+ return false;
1099
+ }
1100
+ delete packages[fullName];
1101
+ lockfile.packages = packages;
482
1102
  await writeLockfile(lockfile);
483
1103
  return true;
484
1104
  }
@@ -487,29 +1107,255 @@ async function listLockfileSkills() {
487
1107
  if (!lockfile) {
488
1108
  return [];
489
1109
  }
490
- return Object.entries(lockfile.skills).map(([name, entry]) => ({
1110
+ const packages = getPackages(lockfile);
1111
+ return Object.entries(packages).map(([name, entry]) => ({
491
1112
  name,
492
1113
  entry
493
1114
  }));
494
1115
  }
495
-
496
- // src/commands/add.ts
497
- async function add(specifier, _options) {
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() {
498
1150
  try {
499
- const apiKey = await requireApiKey();
500
- const registryUrl = await getRegistryUrl();
501
- const parsed = parseSkillSpecifier(specifier);
502
- if (!parsed) {
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
+ }
1308
+
1309
+ // src/commands/add.ts
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) {
1330
+ try {
1331
+ const config2 = await resolveConfig();
1332
+ const registryUrl = config2.registryUrl;
1333
+ const apiKey = getTokenForRegistry(config2, registryUrl);
1334
+ const parsed = parseSkillSpecifier(specifier);
1335
+ if (!parsed) {
503
1336
  console.error(
504
1337
  `Error: Invalid skill specifier "${specifier}". Use format: @user/{username}/{name}[@{version}]`
505
1338
  );
506
1339
  process.exit(1);
507
1340
  }
508
1341
  const { username, name, versionRange } = parsed;
509
- configure2({ registryUrl, apiKey });
1342
+ configure2({ registryUrl, apiKey: apiKey ?? "" });
510
1343
  console.log(`Resolving ${specifier}...`);
511
1344
  const versionsResponse = await listSkillVersions(username, name);
512
1345
  if (versionsResponse.status !== 200) {
1346
+ if (versionsResponse.status === 401) {
1347
+ if (!apiKey) {
1348
+ console.error(
1349
+ `Error: Package @user/${username}/${name} requires authentication`
1350
+ );
1351
+ console.error("Please run 'pspm login' to authenticate");
1352
+ } else {
1353
+ console.error(
1354
+ `Error: Access denied to @user/${username}/${name}. You may not have permission to access this private package.`
1355
+ );
1356
+ }
1357
+ process.exit(1);
1358
+ }
513
1359
  const errorMessage = extractApiErrorMessage(
514
1360
  versionsResponse,
515
1361
  `Skill @user/${username}/${name} not found`
@@ -543,10 +1389,12 @@ async function add(specifier, _options) {
543
1389
  }
544
1390
  const versionInfo = versionResponse.data;
545
1391
  const isPresignedUrl = versionInfo.downloadUrl.includes(".r2.cloudflarestorage.com") || versionInfo.downloadUrl.includes("X-Amz-Signature");
1392
+ const downloadHeaders = {};
1393
+ if (!isPresignedUrl && apiKey) {
1394
+ downloadHeaders.Authorization = `Bearer ${apiKey}`;
1395
+ }
546
1396
  const tarballResponse = await fetch(versionInfo.downloadUrl, {
547
- headers: isPresignedUrl ? {} : {
548
- Authorization: `Bearer ${apiKey}`
549
- },
1397
+ headers: downloadHeaders,
550
1398
  redirect: "follow"
551
1399
  });
552
1400
  if (!tarballResponse.ok) {
@@ -565,60 +1413,842 @@ async function add(specifier, _options) {
565
1413
  const skillsDir = getSkillsDir();
566
1414
  const destDir = join(skillsDir, username, name);
567
1415
  await mkdir(destDir, { recursive: true });
568
- const { writeFile: writeFile4 } = await import('fs/promises');
1416
+ const { writeFile: writeFile8 } = await import('fs/promises');
569
1417
  const tempFile = join(destDir, ".temp.tgz");
570
- await writeFile4(tempFile, tarballBuffer);
1418
+ await writeFile8(tempFile, tarballBuffer);
571
1419
  const { exec: exec2 } = await import('child_process');
572
1420
  const { promisify: promisify2 } = await import('util');
573
1421
  const execAsync = promisify2(exec2);
574
1422
  try {
575
1423
  await rm(destDir, { recursive: true, force: true });
576
1424
  await mkdir(destDir, { recursive: true });
577
- await writeFile4(tempFile, tarballBuffer);
1425
+ await writeFile8(tempFile, tarballBuffer);
578
1426
  await execAsync(
579
1427
  `tar -xzf "${tempFile}" -C "${destDir}" --strip-components=1`
580
1428
  );
581
1429
  } finally {
582
1430
  await rm(tempFile, { force: true });
583
1431
  }
584
- const fullName = `@user/${username}/${name}`;
585
- await addToLockfile(fullName, {
586
- version: resolved,
587
- resolved: versionInfo.downloadUrl,
588
- integrity
589
- });
590
- console.log(`Installed @user/${username}/${name}@${resolved}`);
591
- console.log(`Location: ${destDir}`);
592
- } catch (error) {
593
- const message = error instanceof Error ? error.message : "Unknown error";
594
- console.error(`Error: ${message}`);
595
- process.exit(1);
596
- }
597
- }
598
- async function configInit(options) {
599
- try {
600
- const configPath = join(process.cwd(), ".pspmrc");
601
- try {
602
- await stat(configPath);
603
- console.error("Error: .pspmrc already exists in this directory.");
604
- process.exit(1);
605
- } catch {
1432
+ const fullName = `@user/${username}/${name}`;
1433
+ await addToLockfile(fullName, {
1434
+ version: resolved,
1435
+ resolved: versionInfo.downloadUrl,
1436
+ integrity
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
+ }
1453
+ console.log(`Installed @user/${username}/${name}@${resolved}`);
1454
+ console.log(`Location: ${destDir}`);
1455
+ } catch (error) {
1456
+ const message = error instanceof Error ? error.message : "Unknown error";
1457
+ console.error(`Error: ${message}`);
1458
+ process.exit(1);
1459
+ }
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
+ }
1527
+ async function configInit(options) {
1528
+ try {
1529
+ const configPath = join(process.cwd(), ".pspmrc");
1530
+ try {
1531
+ await stat(configPath);
1532
+ console.error("Error: .pspmrc already exists in this directory.");
1533
+ process.exit(1);
1534
+ } catch {
1535
+ }
1536
+ const lines = ["; Project-specific PSPM configuration", ""];
1537
+ if (options.registry) {
1538
+ lines.push(`registry = ${options.registry}`);
1539
+ } else {
1540
+ lines.push("; Uncomment to use a custom registry:");
1541
+ lines.push("; registry = https://custom-registry.example.com");
1542
+ }
1543
+ lines.push("");
1544
+ await writeFile(configPath, lines.join("\n"));
1545
+ console.log("Created .pspmrc");
1546
+ console.log("");
1547
+ console.log("Contents:");
1548
+ console.log(lines.join("\n"));
1549
+ console.log("Note: .pspmrc should be committed to version control.");
1550
+ console.log("API keys should NOT be stored here - use pspm login instead.");
1551
+ } catch (error) {
1552
+ const message = error instanceof Error ? error.message : "Unknown error";
1553
+ console.error(`Error: ${message}`);
1554
+ process.exit(1);
1555
+ }
1556
+ }
1557
+
1558
+ // src/commands/config/show.ts
1559
+ async function configShow() {
1560
+ try {
1561
+ const resolved = await resolveConfig();
1562
+ const projectConfig = await findProjectConfig();
1563
+ const configPath = getConfigPath();
1564
+ console.log("Resolved Configuration:\n");
1565
+ console.log(` Registry URL: ${resolved.registryUrl}`);
1566
+ console.log(` API Key: ${resolved.apiKey ? "***" : "(not set)"}`);
1567
+ console.log(` Username: ${resolved.username || "(not set)"}`);
1568
+ console.log("");
1569
+ console.log("Config Locations:");
1570
+ console.log(` User config: ${configPath}`);
1571
+ console.log(` Project config: ${projectConfig ? ".pspmrc" : "(none)"}`);
1572
+ console.log("");
1573
+ console.log("Environment Variables:");
1574
+ console.log(
1575
+ ` PSPM_REGISTRY_URL: ${process.env.PSPM_REGISTRY_URL || "(not set)"}`
1576
+ );
1577
+ console.log(
1578
+ ` PSPM_API_KEY: ${process.env.PSPM_API_KEY ? "***" : "(not set)"}`
1579
+ );
1580
+ } catch (error) {
1581
+ const message = error instanceof Error ? error.message : "Unknown error";
1582
+ console.error(`Error: ${message}`);
1583
+ process.exit(1);
1584
+ }
1585
+ }
1586
+
1587
+ // src/commands/deprecate.ts
1588
+ async function deprecate(specifier, message, options) {
1589
+ try {
1590
+ const apiKey = await requireApiKey();
1591
+ const registryUrl = await getRegistryUrl();
1592
+ const parsed = parseSkillSpecifier(specifier);
1593
+ if (!parsed) {
1594
+ console.error(
1595
+ `Error: Invalid skill specifier "${specifier}". Use format: @user/{username}/{name}@{version}`
1596
+ );
1597
+ process.exit(1);
1598
+ }
1599
+ const { username, name, versionRange } = parsed;
1600
+ if (!versionRange) {
1601
+ console.error(
1602
+ "Error: Version is required for deprecation. Use format: @user/{username}/{name}@{version}"
1603
+ );
1604
+ process.exit(1);
1605
+ }
1606
+ configure2({ registryUrl, apiKey });
1607
+ if (options.undo) {
1608
+ console.log(
1609
+ `Removing deprecation from @user/${username}/${name}@${versionRange}...`
1610
+ );
1611
+ const response = await undeprecateSkillVersion(name, versionRange);
1612
+ if (response.status !== 200) {
1613
+ console.error(
1614
+ `Error: ${response.error || "Failed to remove deprecation"}`
1615
+ );
1616
+ process.exit(1);
1617
+ }
1618
+ console.log(
1619
+ `Removed deprecation from @user/${username}/${name}@${versionRange}`
1620
+ );
1621
+ } else {
1622
+ if (!message) {
1623
+ console.error(
1624
+ "Error: Deprecation message is required. Usage: pspm deprecate <specifier> <message>"
1625
+ );
1626
+ process.exit(1);
1627
+ }
1628
+ console.log(`Deprecating @user/${username}/${name}@${versionRange}...`);
1629
+ const response = await deprecateSkillVersion(name, versionRange, message);
1630
+ if (response.status !== 200) {
1631
+ console.error(
1632
+ `Error: ${response.error || "Failed to deprecate version"}`
1633
+ );
1634
+ process.exit(1);
1635
+ }
1636
+ console.log(`Deprecated @user/${username}/${name}@${versionRange}`);
1637
+ console.log(`Message: ${message}`);
1638
+ console.log("");
1639
+ console.log(
1640
+ "Users installing this version will see a deprecation warning."
1641
+ );
1642
+ console.log("The package is still available for download.");
1643
+ }
1644
+ } catch (error) {
1645
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1646
+ console.error(`Error: ${errorMessage}`);
1647
+ process.exit(1);
1648
+ }
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
+ }
1658
+ async function readExistingPackageJson() {
1659
+ try {
1660
+ const content = await readFile(
1661
+ join(process.cwd(), "package.json"),
1662
+ "utf-8"
1663
+ );
1664
+ const pkg = JSON.parse(content);
1665
+ return {
1666
+ name: pkg.name,
1667
+ version: pkg.version,
1668
+ description: pkg.description,
1669
+ author: typeof pkg.author === "string" ? pkg.author : pkg.author?.name,
1670
+ license: pkg.license
1671
+ };
1672
+ } catch {
1673
+ return null;
1674
+ }
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
+ }
1698
+ function sanitizeName(name) {
1699
+ const withoutScope = name.replace(/^@[^/]+\//, "");
1700
+ return withoutScope.toLowerCase().replace(/[^a-z0-9_-]/g, "-").replace(/^-+|-+$/g, "").replace(/-+/g, "-");
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
+ }
1708
+ async function init(options) {
1709
+ try {
1710
+ const pspmJsonPath = join(process.cwd(), "pspm.json");
1711
+ let exists = false;
1712
+ try {
1713
+ await stat(pspmJsonPath);
1714
+ exists = true;
1715
+ } catch {
1716
+ }
1717
+ if (exists && !options.force) {
1718
+ console.error("Error: pspm.json already exists in this directory.");
1719
+ console.error("Use --force to overwrite.");
1720
+ process.exit(1);
1721
+ }
1722
+ const existingPkg = await readExistingPackageJson();
1723
+ const gitAuthor = await getGitAuthor();
1724
+ const defaultName = sanitizeName(
1725
+ options.name || existingPkg?.name || basename(process.cwd())
1726
+ );
1727
+ const defaultVersion = existingPkg?.version || "0.1.0";
1728
+ const defaultDescription = options.description || existingPkg?.description || "";
1729
+ const defaultAuthor = options.author || existingPkg?.author || gitAuthor || "";
1730
+ const defaultLicense = existingPkg?.license || "MIT";
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
+ }
1824
+ if (!manifest.description) delete manifest.description;
1825
+ if (!manifest.author) delete manifest.author;
1826
+ if (manifest.capabilities?.length === 0) delete manifest.capabilities;
1827
+ const content = JSON.stringify(manifest, null, 2);
1828
+ console.log("");
1829
+ console.log(`About to write to ${pspmJsonPath}:`);
1830
+ console.log("");
1831
+ console.log(content);
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
+ `);
1847
+ try {
1848
+ await stat(join(process.cwd(), "SKILL.md"));
1849
+ } catch {
1850
+ console.log(
1851
+ "Note: Create a SKILL.md file with your skill's prompt content."
1852
+ );
1853
+ }
1854
+ if (existingPkg) {
1855
+ console.log("Note: Values were derived from existing package.json.");
1856
+ console.log(" pspm.json is for publishing to PSPM registry.");
1857
+ console.log(" package.json can still be used for npm dependencies.");
1858
+ }
1859
+ } catch (error) {
1860
+ const message = error instanceof Error ? error.message : "Unknown error";
1861
+ console.error(`Error: ${message}`);
1862
+ process.exit(1);
1863
+ }
1864
+ }
1865
+ function getCacheFilePath(cacheDir, integrity) {
1866
+ const match = integrity.match(/^sha256-(.+)$/);
1867
+ if (!match) {
1868
+ throw new Error(`Invalid integrity format: ${integrity}`);
1869
+ }
1870
+ const base64Hash = match[1];
1871
+ const hexHash = Buffer.from(base64Hash, "base64").toString("hex");
1872
+ return join(cacheDir, `sha256-${hexHash}.tgz`);
1873
+ }
1874
+ async function readFromCache(cacheDir, integrity) {
1875
+ try {
1876
+ const cachePath = getCacheFilePath(cacheDir, integrity);
1877
+ const data = await readFile(cachePath);
1878
+ const actualIntegrity = `sha256-${createHash("sha256").update(data).digest("base64")}`;
1879
+ if (actualIntegrity !== integrity) {
1880
+ await rm(cachePath, { force: true });
1881
+ return null;
1882
+ }
1883
+ return data;
1884
+ } catch {
1885
+ return null;
1886
+ }
1887
+ }
1888
+ async function writeToCache(cacheDir, integrity, data) {
1889
+ try {
1890
+ await mkdir(cacheDir, { recursive: true });
1891
+ const cachePath = getCacheFilePath(cacheDir, integrity);
1892
+ await writeFile(cachePath, data);
1893
+ } catch {
1894
+ }
1895
+ }
1896
+ async function install(options) {
1897
+ try {
1898
+ const config2 = await resolveConfig();
1899
+ const registryUrl = config2.registryUrl;
1900
+ const apiKey = getTokenForRegistry(config2, registryUrl);
1901
+ const skillsDir = options.dir || getSkillsDir();
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
+ }
1916
+ await migrateLockfileIfNeeded();
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) {
1930
+ if (options.frozenLockfile) {
1931
+ console.error(
1932
+ "Error: Dependencies in pspm.json are not in lockfile. Cannot install with --frozen-lockfile"
1933
+ );
1934
+ console.error("Missing dependencies:");
1935
+ for (const dep of missingDeps) {
1936
+ console.error(` - ${dep.fullName}@${dep.versionRange}`);
1937
+ }
1938
+ process.exit(1);
1939
+ }
1940
+ console.log(`Resolving ${missingDeps.length} new dependency(ies)...
1941
+ `);
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");
1986
+ const downloadHeaders = {};
1987
+ if (!isPresignedUrl && apiKey) {
1988
+ downloadHeaders.Authorization = `Bearer ${apiKey}`;
1989
+ }
1990
+ const tarballResponse = await fetch(versionInfo.downloadUrl, {
1991
+ headers: downloadHeaders,
1992
+ redirect: "follow"
1993
+ });
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
+ }
2114
+ } else {
2115
+ console.error(
2116
+ ` Error: Failed to download ${fullName} (${response.status})`
2117
+ );
2118
+ }
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) {
2124
+ console.error(
2125
+ ` Error: Checksum verification failed for ${fullName}`
2126
+ );
2127
+ if (options.frozenLockfile) {
2128
+ process.exit(1);
2129
+ }
2130
+ continue;
2131
+ }
2132
+ await writeToCache(cacheDir, entry.integrity, tarballBuffer);
2133
+ }
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}`
2169
+ );
2170
+ continue;
2171
+ }
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
+ }
2233
+ }
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.");
606
2244
  }
607
- const lines = ["; Project-specific PSPM configuration", ""];
608
- if (options.registry) {
609
- lines.push(`registry = ${options.registry}`);
2245
+ const totalCount = packageCount + githubCount;
2246
+ if (totalCount === 0) {
2247
+ console.log("No skills to install.");
610
2248
  } else {
611
- lines.push("; Uncomment to use a custom registry:");
612
- lines.push("; registry = https://custom-registry.example.com");
2249
+ console.log(`
2250
+ All ${totalCount} skill(s) installed.`);
613
2251
  }
614
- lines.push("");
615
- await writeFile(configPath, lines.join("\n"));
616
- console.log("Created .pspmrc");
617
- console.log("");
618
- console.log("Contents:");
619
- console.log(lines.join("\n"));
620
- console.log("Note: .pspmrc should be committed to version control.");
621
- console.log("API keys should NOT be stored here - use pspm login instead.");
622
2252
  } catch (error) {
623
2253
  const message = error instanceof Error ? error.message : "Unknown error";
624
2254
  console.error(`Error: ${message}`);
@@ -626,109 +2256,71 @@ async function configInit(options) {
626
2256
  }
627
2257
  }
628
2258
 
629
- // src/commands/config/show.ts
630
- async function configShow() {
631
- try {
632
- const resolved = await resolveConfig();
633
- const projectConfig = await findProjectConfig();
634
- const configPath = getConfigPath();
635
- console.log("Resolved Configuration:\n");
636
- console.log(` Registry URL: ${resolved.registryUrl}`);
637
- console.log(` API Key: ${resolved.apiKey ? "***" : "(not set)"}`);
638
- console.log(` Username: ${resolved.username || "(not set)"}`);
639
- console.log("");
640
- console.log("Config Locations:");
641
- console.log(` User config: ${configPath}`);
642
- console.log(` Project config: ${projectConfig ? ".pspmrc" : "(none)"}`);
643
- console.log("");
644
- console.log("Environment Variables:");
645
- console.log(
646
- ` PSPM_REGISTRY_URL: ${process.env.PSPM_REGISTRY_URL || "(not set)"}`
647
- );
648
- console.log(
649
- ` PSPM_API_KEY: ${process.env.PSPM_API_KEY ? "***" : "(not set)"}`
650
- );
651
- } catch (error) {
652
- const message = error instanceof Error ? error.message : "Unknown error";
653
- console.error(`Error: ${message}`);
654
- process.exit(1);
655
- }
656
- }
657
- async function install(options) {
2259
+ // src/commands/link.ts
2260
+ async function link(options) {
658
2261
  try {
659
- const apiKey = await requireApiKey();
660
- await getRegistryUrl();
661
- const skillsDir = options.dir || getSkillsDir();
662
- const lockfile = await readLockfile();
663
- if (!lockfile) {
664
- if (options.frozenLockfile) {
665
- console.error(
666
- "Error: No lockfile found. Cannot install with --frozen-lockfile"
667
- );
668
- process.exit(1);
669
- }
670
- console.log("No lockfile found. Nothing to install.");
671
- return;
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();
672
2274
  }
673
- const skillCount = Object.keys(lockfile.skills).length;
674
- if (skillCount === 0) {
675
- console.log("No skills in lockfile. Nothing to install.");
2275
+ if (agents.length === 1 && agents[0] === "none") {
2276
+ console.log("Skipping symlink creation (--agent none)");
676
2277
  return;
677
2278
  }
678
- console.log(`Installing ${skillCount} skill(s)...
679
- `);
680
- const entries = Object.entries(lockfile.skills);
681
- for (const [fullName, entry] of entries) {
682
- const match = fullName.match(/^@user\/([^/]+)\/([^/]+)$/);
683
- if (!match) {
684
- console.warn(`Warning: Invalid skill name in lockfile: ${fullName}`);
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}`);
685
2285
  continue;
686
2286
  }
687
- const [, username, name] = match;
688
- console.log(`Installing ${fullName}@${entry.version}...`);
689
- const isPresignedUrl = entry.resolved.includes(".r2.cloudflarestorage.com") || entry.resolved.includes("X-Amz-Signature");
690
- const response = await fetch(entry.resolved, {
691
- headers: isPresignedUrl ? {} : {
692
- Authorization: `Bearer ${apiKey}`
693
- },
694
- redirect: "follow"
2287
+ skills.push({
2288
+ name: parsed.name,
2289
+ sourcePath: getRegistrySkillPath(parsed.username, parsed.name)
695
2290
  });
696
- if (!response.ok) {
697
- console.error(
698
- ` Error: Failed to download ${fullName} (${response.status})`
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}`
699
2298
  );
700
2299
  continue;
701
2300
  }
702
- const tarballBuffer = Buffer.from(await response.arrayBuffer());
703
- const { createHash: createHash3 } = await import('crypto');
704
- const actualIntegrity = `sha256-${createHash3("sha256").update(tarballBuffer).digest("base64")}`;
705
- if (actualIntegrity !== entry.integrity) {
706
- console.error(` Error: Checksum verification failed for ${fullName}`);
707
- if (options.frozenLockfile) {
708
- process.exit(1);
709
- }
710
- continue;
711
- }
712
- const destDir = join(skillsDir, username, name);
713
- await rm(destDir, { recursive: true, force: true });
714
- await mkdir(destDir, { recursive: true });
715
- const tempFile = join(destDir, ".temp.tgz");
716
- const { writeFile: writeFile4 } = await import('fs/promises');
717
- await writeFile4(tempFile, tarballBuffer);
718
- const { exec: exec2 } = await import('child_process');
719
- const { promisify: promisify2 } = await import('util');
720
- const execAsync = promisify2(exec2);
721
- try {
722
- await execAsync(
723
- `tar -xzf "${tempFile}" -C "${destDir}" --strip-components=1`
724
- );
725
- } finally {
726
- await rm(tempFile, { force: true });
727
- }
728
- console.log(` Installed to ${destDir}`);
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}`);
729
2323
  }
730
- console.log(`
731
- All ${skillCount} skill(s) installed.`);
732
2324
  } catch (error) {
733
2325
  const message = error instanceof Error ? error.message : "Unknown error";
734
2326
  console.error(`Error: ${message}`);
@@ -737,7 +2329,76 @@ All ${skillCount} skill(s) installed.`);
737
2329
  }
738
2330
  async function list(options) {
739
2331
  try {
740
- 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
+ }
741
2402
  if (skills.length === 0) {
742
2403
  console.log("No skills installed.");
743
2404
  return;
@@ -746,26 +2407,33 @@ async function list(options) {
746
2407
  console.log(JSON.stringify(skills, null, 2));
747
2408
  return;
748
2409
  }
749
- const skillsDir = getSkillsDir();
750
2410
  console.log("Installed skills:\n");
751
- for (const { name, entry } of skills) {
752
- const match = name.match(/^@user\/([^/]+)\/([^/]+)$/);
753
- if (!match) continue;
754
- const [, username, skillName] = match;
755
- const skillPath = join(skillsDir, username, skillName);
756
- let status = "installed";
757
- try {
758
- await access(skillPath);
759
- } catch {
760
- 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})`);
761
2417
  }
762
- console.log(` ${name}@${entry.version}`);
763
- if (status === "missing") {
2418
+ if (skill.status === "missing") {
764
2419
  console.log(` Status: MISSING (run 'pspm install' to restore)`);
765
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
+ }
766
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`);
767
2435
  console.log(`
768
- Total: ${skills.length} skill(s)`);
2436
+ Total: ${skills.length} skill(s) (${parts.join(", ")})`);
769
2437
  } catch (error) {
770
2438
  const message = error instanceof Error ? error.message : "Unknown error";
771
2439
  console.error(`Error: ${message}`);
@@ -778,7 +2446,7 @@ function getWebAppUrl(registryUrl) {
778
2446
  return process.env.PSPM_WEB_URL.replace(/\/$/, "");
779
2447
  }
780
2448
  try {
781
- const url = new URL(registryUrl);
2449
+ const url = new URL$1(registryUrl);
782
2450
  return `${url.protocol}//${url.host}`;
783
2451
  } catch {
784
2452
  return DEFAULT_WEB_APP_URL;
@@ -786,7 +2454,7 @@ function getWebAppUrl(registryUrl) {
786
2454
  }
787
2455
  function getServerUrl(registryUrl) {
788
2456
  try {
789
- const url = new URL(registryUrl);
2457
+ const url = new URL$1(registryUrl);
790
2458
  return `${url.protocol}//${url.host}`;
791
2459
  } catch {
792
2460
  return DEFAULT_WEB_APP_URL;
@@ -818,7 +2486,7 @@ function startCallbackServer(expectedState) {
818
2486
  rejectToken = reject;
819
2487
  });
820
2488
  const server = http.createServer((req, res) => {
821
- const url = new URL(req.url || "/", `http://localhost`);
2489
+ const url = new URL$1(req.url || "/", `http://localhost`);
822
2490
  if (url.pathname === "/callback") {
823
2491
  const token = url.searchParams.get("token");
824
2492
  const state = url.searchParams.get("state");
@@ -964,7 +2632,132 @@ async function logout() {
964
2632
  process.exit(1);
965
2633
  }
966
2634
  }
2635
+ async function migrate(options) {
2636
+ try {
2637
+ const legacySkillsDir = getLegacySkillsDir();
2638
+ const newSkillsDir = getSkillsDir();
2639
+ const legacyLockfilePath = getLegacyLockfilePath();
2640
+ const newLockfilePath = getLockfilePath();
2641
+ const pspmDir = getPspmDir();
2642
+ let migrationNeeded = false;
2643
+ const actions = [];
2644
+ try {
2645
+ const legacyStats = await stat(legacySkillsDir);
2646
+ if (legacyStats.isDirectory()) {
2647
+ const contents = await readdir(legacySkillsDir);
2648
+ if (contents.length > 0) {
2649
+ migrationNeeded = true;
2650
+ actions.push(`Move .skills/ \u2192 .pspm/skills/`);
2651
+ }
2652
+ }
2653
+ } catch {
2654
+ }
2655
+ try {
2656
+ await stat(legacyLockfilePath);
2657
+ try {
2658
+ await stat(newLockfilePath);
2659
+ actions.push(
2660
+ `Note: Both skill-lock.json and pspm-lock.json exist. Manual merge may be needed.`
2661
+ );
2662
+ } catch {
2663
+ migrationNeeded = true;
2664
+ actions.push(`Migrate skill-lock.json \u2192 pspm-lock.json`);
2665
+ }
2666
+ } catch {
2667
+ }
2668
+ if (!migrationNeeded && actions.length === 0) {
2669
+ console.log(
2670
+ "No migration needed. Project is already using the new structure."
2671
+ );
2672
+ return;
2673
+ }
2674
+ if (options.dryRun) {
2675
+ console.log("Migration plan (dry run):");
2676
+ console.log("");
2677
+ for (const action of actions) {
2678
+ console.log(` - ${action}`);
2679
+ }
2680
+ console.log("");
2681
+ console.log("Run without --dry-run to perform migration.");
2682
+ return;
2683
+ }
2684
+ console.log("Migrating project structure...\n");
2685
+ const lockfileMigrated = await migrateLockfileIfNeeded();
2686
+ if (lockfileMigrated) {
2687
+ console.log(" \u2713 Migrated skill-lock.json \u2192 pspm-lock.json");
2688
+ }
2689
+ try {
2690
+ const legacyStats = await stat(legacySkillsDir);
2691
+ if (legacyStats.isDirectory()) {
2692
+ const contents = await readdir(legacySkillsDir);
2693
+ if (contents.length > 0) {
2694
+ await mkdir(pspmDir, { recursive: true });
2695
+ try {
2696
+ const newStats = await stat(newSkillsDir);
2697
+ if (newStats.isDirectory()) {
2698
+ const newContents = await readdir(newSkillsDir);
2699
+ if (newContents.length > 0) {
2700
+ console.log(
2701
+ " ! Both .skills/ and .pspm/skills/ have content. Manual merge required."
2702
+ );
2703
+ } else {
2704
+ await rm(newSkillsDir, { recursive: true, force: true });
2705
+ await rename(legacySkillsDir, newSkillsDir);
2706
+ console.log(" \u2713 Moved .skills/ \u2192 .pspm/skills/");
2707
+ }
2708
+ }
2709
+ } catch {
2710
+ await rename(legacySkillsDir, newSkillsDir);
2711
+ console.log(" \u2713 Moved .skills/ \u2192 .pspm/skills/");
2712
+ }
2713
+ }
2714
+ }
2715
+ } catch {
2716
+ }
2717
+ console.log("");
2718
+ console.log("Migration complete!");
2719
+ console.log("");
2720
+ console.log(
2721
+ "You can safely delete these legacy files if they still exist:"
2722
+ );
2723
+ console.log(" - skill-lock.json (replaced by pspm-lock.json)");
2724
+ console.log(" - .skills/ (replaced by .pspm/skills/)");
2725
+ console.log("");
2726
+ console.log("Update your .gitignore to include:");
2727
+ console.log(" .pspm/cache/");
2728
+ } catch (error) {
2729
+ const message = error instanceof Error ? error.message : "Unknown error";
2730
+ console.error(`Error: ${message}`);
2731
+ process.exit(1);
2732
+ }
2733
+ }
967
2734
  var exec = promisify(exec$1);
2735
+ async function detectManifest() {
2736
+ const cwd = process.cwd();
2737
+ const pspmJsonPath = join(cwd, "pspm.json");
2738
+ try {
2739
+ const content = await readFile(pspmJsonPath, "utf-8");
2740
+ const manifest = JSON.parse(content);
2741
+ return { type: "pspm.json", manifest, path: pspmJsonPath };
2742
+ } catch {
2743
+ }
2744
+ const packageJsonPath = join(cwd, "package.json");
2745
+ try {
2746
+ const content = await readFile(packageJsonPath, "utf-8");
2747
+ const packageJson2 = JSON.parse(content);
2748
+ const manifest = {
2749
+ name: packageJson2.name,
2750
+ version: packageJson2.version,
2751
+ description: packageJson2.description,
2752
+ author: typeof packageJson2.author === "string" ? packageJson2.author : packageJson2.author?.name,
2753
+ license: packageJson2.license,
2754
+ files: packageJson2.files
2755
+ };
2756
+ return { type: "package.json", manifest, path: packageJsonPath };
2757
+ } catch {
2758
+ throw new Error("No pspm.json or package.json found in current directory");
2759
+ }
2760
+ }
968
2761
  function formatBytes(bytes) {
969
2762
  if (bytes < 1024) return `${bytes}B`;
970
2763
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}kB`;
@@ -996,23 +2789,26 @@ async function publishCommand(options) {
996
2789
  try {
997
2790
  const apiKey = await requireApiKey();
998
2791
  const registryUrl = await getRegistryUrl();
999
- const packageJsonPath = join(process.cwd(), "package.json");
1000
- let packageJson2;
1001
- try {
1002
- const content = await readFile(packageJsonPath, "utf-8");
1003
- packageJson2 = JSON.parse(content);
1004
- } catch {
1005
- console.error("Error: No package.json found in current directory");
1006
- process.exit(1);
1007
- }
1008
- if (!packageJson2.name) {
1009
- console.error("Error: package.json must have a 'name' field");
1010
- process.exit(1);
2792
+ const detection = await detectManifest();
2793
+ const manifest = detection.manifest;
2794
+ if (detection.type === "package.json") {
2795
+ console.log("pspm warn Using package.json instead of pspm.json");
2796
+ console.log(
2797
+ "pspm warn Run 'pspm init' to create a dedicated pspm.json manifest"
2798
+ );
2799
+ console.log("");
1011
2800
  }
1012
- if (!packageJson2.version) {
1013
- console.error("Error: package.json must have a 'version' field");
2801
+ const validation = validateManifest(manifest);
2802
+ if (!validation.valid) {
2803
+ console.error(`Error: ${validation.error}`);
1014
2804
  process.exit(1);
1015
2805
  }
2806
+ const packageJson2 = {
2807
+ name: manifest.name,
2808
+ version: manifest.version,
2809
+ description: manifest.description,
2810
+ files: manifest.files
2811
+ };
1016
2812
  if (options.bump) {
1017
2813
  const semver2 = await import('semver');
1018
2814
  const newVersion = semver2.default.inc(packageJson2.version, options.bump);
@@ -1030,13 +2826,7 @@ async function publishCommand(options) {
1030
2826
  const tempDir = join(process.cwd(), ".pspm-publish");
1031
2827
  try {
1032
2828
  await exec(`rm -rf "${tempDir}" && mkdir -p "${tempDir}"`);
1033
- const files = packageJson2.files || [
1034
- "package.json",
1035
- "SKILL.md",
1036
- "runtime",
1037
- "scripts",
1038
- "data"
1039
- ];
2829
+ const files = packageJson2.files || [...DEFAULT_SKILL_FILES];
1040
2830
  await exec(`mkdir -p "${tempDir}/package"`);
1041
2831
  for (const file of files) {
1042
2832
  try {
@@ -1046,7 +2836,18 @@ async function publishCommand(options) {
1046
2836
  } catch {
1047
2837
  }
1048
2838
  }
1049
- await exec(`cp package.json "${tempDir}/package/"`);
2839
+ if (detection.type === "pspm.json") {
2840
+ await exec(`cp pspm.json "${tempDir}/package/"`);
2841
+ try {
2842
+ await stat(join(process.cwd(), "package.json"));
2843
+ await exec(
2844
+ `cp package.json "${tempDir}/package/" 2>/dev/null || true`
2845
+ );
2846
+ } catch {
2847
+ }
2848
+ } else {
2849
+ await exec(`cp package.json "${tempDir}/package/"`);
2850
+ }
1050
2851
  const packageDir = join(tempDir, "package");
1051
2852
  const tarballContents = await getFilesWithSizes(packageDir, packageDir);
1052
2853
  const unpackedSize = tarballContents.reduce((acc, f) => acc + f.size, 0);
@@ -1103,6 +2904,25 @@ async function publishCommand(options) {
1103
2904
  `+ @user/${result.skill.username}/${result.skill.name}@${result.version.version}`
1104
2905
  );
1105
2906
  console.log(`Checksum: ${result.version.checksum}`);
2907
+ if (options.access) {
2908
+ console.log(`
2909
+ Setting visibility to ${options.access}...`);
2910
+ const accessResponse = await changeSkillAccess(packageJson2.name, {
2911
+ visibility: options.access
2912
+ });
2913
+ if (accessResponse.status !== 200 || !accessResponse.data) {
2914
+ console.warn(
2915
+ `Warning: Failed to set visibility: ${accessResponse.error ?? "Unknown error"}`
2916
+ );
2917
+ } else {
2918
+ console.log(`Package is now ${accessResponse.data.visibility}`);
2919
+ if (options.access === "public") {
2920
+ console.log(
2921
+ "Note: This action is irreversible. Public packages cannot be made private."
2922
+ );
2923
+ }
2924
+ }
2925
+ }
1106
2926
  } finally {
1107
2927
  await exec(`rm -rf "${tempDir}"`).catch(() => {
1108
2928
  });
@@ -1118,59 +2938,103 @@ async function publishCommand(options) {
1118
2938
  }
1119
2939
  async function remove(nameOrSpecifier) {
1120
2940
  try {
1121
- await requireApiKey();
1122
- let fullName;
1123
- let username;
1124
- let name;
1125
- if (nameOrSpecifier.startsWith("@user/")) {
1126
- const match = nameOrSpecifier.match(/^@user\/([^/]+)\/([^@/]+)/);
1127
- if (!match) {
1128
- console.error(`Error: Invalid skill specifier: ${nameOrSpecifier}`);
1129
- process.exit(1);
1130
- }
1131
- fullName = `@user/${match[1]}/${match[2]}`;
1132
- username = match[1];
1133
- 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);
1134
2948
  } else {
1135
- const skills = await listLockfileSkills();
1136
- const found = skills.find((s) => {
1137
- const match2 = s.name.match(/^@user\/([^/]+)\/([^/]+)$/);
1138
- return match2 && match2[2] === nameOrSpecifier;
1139
- });
1140
- if (!found) {
1141
- console.error(
1142
- `Error: Skill "${nameOrSpecifier}" not found in lockfile`
1143
- );
1144
- process.exit(1);
1145
- }
1146
- fullName = found.name;
1147
- const match = fullName.match(/^@user\/([^/]+)\/([^/]+)$/);
1148
- if (!match) {
1149
- console.error(`Error: Invalid skill name in lockfile: ${fullName}`);
1150
- process.exit(1);
1151
- }
1152
- username = match[1];
1153
- name = match[2];
1154
- }
1155
- console.log(`Removing ${fullName}...`);
1156
- const removed = await removeFromLockfile(fullName);
1157
- if (!removed) {
1158
- console.error(`Error: ${fullName} not found in lockfile`);
1159
- process.exit(1);
1160
- }
1161
- const skillsDir = getSkillsDir();
1162
- const destDir = join(skillsDir, username, name);
1163
- try {
1164
- await rm(destDir, { recursive: true, force: true });
1165
- } catch {
2949
+ await removeByShortName(nameOrSpecifier, agents, agentConfigs);
1166
2950
  }
1167
- console.log(`Removed ${fullName}`);
1168
2951
  } catch (error) {
1169
2952
  const message = error instanceof Error ? error.message : "Unknown error";
1170
2953
  console.error(`Error: ${message}`);
1171
2954
  process.exit(1);
1172
2955
  }
1173
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
+ }
1174
3038
 
1175
3039
  // src/commands/unpublish.ts
1176
3040
  async function unpublish(specifier, options) {
@@ -1356,8 +3220,31 @@ program.command("logout").description("Log out and clear stored credentials").ac
1356
3220
  program.command("whoami").description("Show current user information").action(async () => {
1357
3221
  await whoami();
1358
3222
  });
1359
- 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) => {
1360
- await add(specifier, { save: options.save ?? true });
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) => {
3224
+ await init({
3225
+ name: options.name,
3226
+ description: options.description,
3227
+ author: options.author,
3228
+ yes: options.yes,
3229
+ force: options.force
3230
+ });
3231
+ });
3232
+ program.command("migrate").description(
3233
+ "Migrate from old directory structure (.skills/, skill-lock.json)"
3234
+ ).option("--dry-run", "Show what would be migrated without making changes").action(async (options) => {
3235
+ await migrate({ dryRun: options.dryRun });
3236
+ });
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
+ });
1361
3248
  });
1362
3249
  program.command("remove <name>").alias("rm").description("Remove an installed skill").action(async (name) => {
1363
3250
  await remove(name);
@@ -1365,24 +3252,49 @@ program.command("remove <name>").alias("rm").description("Remove an installed sk
1365
3252
  program.command("list").alias("ls").description("List installed skills").option("--json", "Output as JSON").action(async (options) => {
1366
3253
  await list({ json: options.json });
1367
3254
  });
1368
- 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) => {
1369
3259
  await install({
1370
3260
  frozenLockfile: options.frozenLockfile,
1371
- dir: options.dir
3261
+ dir: options.dir,
3262
+ agent: options.agent,
3263
+ yes: options.yes
1372
3264
  });
1373
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
+ });
1374
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) => {
1375
3273
  await update({ dryRun: options.dryRun });
1376
3274
  });
1377
- program.command("publish").description("Publish current directory as a skill").option("--bump <level>", "Bump version (major, minor, patch)").option("--tag <tag>", "Tag for the release").action(async (options) => {
3275
+ program.command("publish").description("Publish current directory as a skill").option("--bump <level>", "Bump version (major, minor, patch)").option("--tag <tag>", "Tag for the release").option("--access <level>", "Set package visibility (public or private)").action(async (options) => {
1378
3276
  await publishCommand({
1379
3277
  bump: options.bump,
1380
- tag: options.tag
3278
+ tag: options.tag,
3279
+ access: options.access
1381
3280
  });
1382
3281
  });
1383
- program.command("unpublish <specifier>").description("Remove a published skill version").option("--force", "Confirm destructive action").action(async (specifier, options) => {
3282
+ program.command("unpublish <specifier>").description(
3283
+ "Remove a published skill version (only within 72 hours of publishing)"
3284
+ ).option("--force", "Confirm destructive action").action(async (specifier, options) => {
1384
3285
  await unpublish(specifier, { force: options.force });
1385
3286
  });
3287
+ program.command("access [specifier]").description("Change package visibility (public/private)").option("--public", "Make the package public (irreversible)").option("--private", "Make the package private (only for private packages)").action(async (specifier, options) => {
3288
+ await access(specifier, {
3289
+ public: options.public,
3290
+ private: options.private
3291
+ });
3292
+ });
3293
+ program.command("deprecate <specifier> [message]").description(
3294
+ "Mark a skill version as deprecated (alternative to unpublish after 72 hours)"
3295
+ ).option("--undo", "Remove deprecation status").action(async (specifier, message, options) => {
3296
+ await deprecate(specifier, message, { undo: options.undo });
3297
+ });
1386
3298
  program.parse();
1387
3299
  //# sourceMappingURL=index.js.map
1388
3300
  //# sourceMappingURL=index.js.map