@serviceme/devtools-core 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/auth-Bzss8zOv.mjs +995 -0
  2. package/dist/auth-Bzss8zOv.mjs.map +1 -0
  3. package/dist/auth-DXam3c4S.js +1078 -0
  4. package/dist/auth-DXam3c4S.js.map +1 -0
  5. package/dist/auth.d.mts +2 -2
  6. package/dist/auth.d.ts +2 -2
  7. package/dist/auth.js +14 -1029
  8. package/dist/auth.mjs +2 -991
  9. package/dist/device-CDDcwDY6.mjs +758 -0
  10. package/dist/device-CDDcwDY6.mjs.map +1 -0
  11. package/dist/device-DuAYH8bk.js +839 -0
  12. package/dist/device-DuAYH8bk.js.map +1 -0
  13. package/dist/device.d.mts +2 -455
  14. package/dist/device.d.ts +2 -455
  15. package/dist/device.js +15 -728
  16. package/dist/device.mjs +2 -679
  17. package/dist/index-2rX86yMq.d.mts +331 -0
  18. package/dist/index-BI11AfMH.d.mts +377 -0
  19. package/dist/index-BI11AfMH.d.ts +377 -0
  20. package/dist/index-CoA6y0MP.d.mts +498 -0
  21. package/dist/index-CoA6y0MP.d.ts +498 -0
  22. package/dist/index-DaW7IYA-.d.mts +185 -0
  23. package/dist/index-DaW7IYA-.d.ts +185 -0
  24. package/dist/index-Dp6FwWKh.d.ts +331 -0
  25. package/dist/index.d.mts +2340 -2101
  26. package/dist/index.d.ts +2340 -2101
  27. package/dist/index.js +8117 -10538
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +7895 -10318
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/rolldown-runtime-VH7oDXx4.js +28 -0
  32. package/dist/skill-linker.d.mts +82 -81
  33. package/dist/skill-linker.d.ts +82 -81
  34. package/dist/skill-linker.js +359 -402
  35. package/dist/skill-linker.js.map +1 -1
  36. package/dist/skill-linker.mjs +345 -357
  37. package/dist/skill-linker.mjs.map +1 -1
  38. package/dist/skill-store-Bym2yinU.mjs +388 -0
  39. package/dist/skill-store-Bym2yinU.mjs.map +1 -0
  40. package/dist/skill-store-CJdYyyxu.js +408 -0
  41. package/dist/skill-store-CJdYyyxu.js.map +1 -0
  42. package/dist/skill-store.d.mts +41 -42
  43. package/dist/skill-store.d.ts +41 -42
  44. package/dist/skill-store.js +5 -338
  45. package/dist/skill-store.mjs +2 -301
  46. package/dist/submit-Cg7wSa3x.mjs +84 -0
  47. package/dist/submit-Cg7wSa3x.mjs.map +1 -0
  48. package/dist/submit-CltE6YOZ.js +98 -0
  49. package/dist/submit-CltE6YOZ.js.map +1 -0
  50. package/dist/submit.d.mts +2 -3
  51. package/dist/submit.d.ts +2 -3
  52. package/dist/submit.js +4 -166
  53. package/dist/submit.mjs +2 -130
  54. package/dist/toolbox-CbfxpNg-.mjs +540 -0
  55. package/dist/toolbox-CbfxpNg-.mjs.map +1 -0
  56. package/dist/toolbox-CxnOr-FY.js +614 -0
  57. package/dist/toolbox-CxnOr-FY.js.map +1 -0
  58. package/dist/toolbox.d.mts +2 -244
  59. package/dist/toolbox.d.ts +2 -244
  60. package/dist/toolbox.js +14 -584
  61. package/dist/toolbox.mjs +2 -536
  62. package/dist/{types-B9gk3dXH.d.ts → types-BCV3IedE.d.mts} +25 -23
  63. package/dist/{types-B9gk3dXH.d.mts → types-BCV3IedE.d.ts} +25 -23
  64. package/dist/userHome-BquDeFVE.js +526 -0
  65. package/dist/userHome-BquDeFVE.js.map +1 -0
  66. package/dist/userHome-CcnAQPG1.mjs +212 -0
  67. package/dist/userHome-CcnAQPG1.mjs.map +1 -0
  68. package/package.json +10 -10
  69. package/dist/auth.js.map +0 -1
  70. package/dist/auth.mjs.map +0 -1
  71. package/dist/device.js.map +0 -1
  72. package/dist/device.mjs.map +0 -1
  73. package/dist/index-BMk4tqIT.d.mts +0 -328
  74. package/dist/index-CymN0x9Z.d.ts +0 -328
  75. package/dist/index-PD135hlB.d.mts +0 -626
  76. package/dist/index-PD135hlB.d.ts +0 -626
  77. package/dist/skill-store.js.map +0 -1
  78. package/dist/skill-store.mjs.map +0 -1
  79. package/dist/submit.js.map +0 -1
  80. package/dist/submit.mjs.map +0 -1
  81. package/dist/toolbox.js.map +0 -1
  82. package/dist/toolbox.mjs.map +0 -1
@@ -1,396 +1,384 @@
1
- // src/skill-linker/index.ts
2
- import * as fs from "fs/promises";
3
- import * as path2 from "path";
4
-
5
- // src/paths/userHome.ts
6
- import * as os from "os";
7
- import * as path from "path";
8
- var SERVICEME_DIR_NAME = ".serviceme";
9
- var REPOS_SUBDIR = "repos";
10
- var SAFE_REPO_ID_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
11
- var SERVICEME_HOME_ENV = "SERVICEME_HOME";
12
- var activeOverrides = {};
13
- function resolveHomeDir() {
14
- const injected = activeOverrides.homeDir;
15
- if (injected !== void 0) {
16
- return injected;
17
- }
18
- return os.homedir();
19
- }
20
- function resolveServicemeHomeEnv() {
21
- const injected = activeOverrides.servicemeHomeEnv;
22
- if (injected !== void 0) {
23
- return injected.length > 0 ? injected : void 0;
24
- }
25
- const envValue = process.env[SERVICEME_HOME_ENV];
26
- return envValue && envValue.length > 0 ? envValue : void 0;
27
- }
28
- function assertSafeRepoId(repoId) {
29
- if (typeof repoId !== "string" || repoId.length === 0 || !SAFE_REPO_ID_PATTERN.test(repoId)) {
30
- throw new Error(
31
- `Invalid repo id: ${JSON.stringify(repoId)}. Must match ${SAFE_REPO_ID_PATTERN} (alphanumeric start, then alphanumerics / underscores / hyphens, \u2264 64 chars).`
32
- );
33
- }
34
- return repoId;
35
- }
36
- function getHomeDir() {
37
- return resolveHomeDir();
38
- }
39
- function getServicemeHome() {
40
- const override = resolveServicemeHomeEnv();
41
- if (override !== void 0) {
42
- return path.resolve(override);
43
- }
44
- return path.join(resolveHomeDir(), SERVICEME_DIR_NAME);
45
- }
46
- function getReposDir() {
47
- return path.join(getServicemeHome(), REPOS_SUBDIR);
48
- }
49
- function getRepoDir(repoId) {
50
- return path.join(getReposDir(), assertSafeRepoId(repoId));
51
- }
52
-
53
- // src/skill-linker/index.ts
54
- var ENTRY_SUBDIR = {
55
- skill: "skills",
56
- agent: "agents"
1
+ import { R as getRepoDir, j as getHomeDir } from "./userHome-CcnAQPG1.mjs";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ //#region src/skill-linker/index.ts
5
+ const ENTRY_SUBDIR = {
6
+ skill: "skills",
7
+ agent: "agents"
57
8
  };
9
+ /**
10
+ * Sentinel error for link failures. Carries the chosen mode + platform
11
+ * hint so the UI can render an actionable message.
12
+ */
58
13
  var LinkError = class extends Error {
59
- constructor(opts) {
60
- super(
61
- `failed to ${opts.mode} link ${opts.linkPath} \u2192 ${opts.target} on ${opts.platform}: ${opts.cause instanceof Error ? opts.cause.message : String(opts.cause)}`
62
- );
63
- this.name = "LinkError";
64
- this.mode = opts.mode;
65
- this.target = opts.target;
66
- this.linkPath = opts.linkPath;
67
- this.platform = opts.platform;
68
- this.cause = opts.cause;
69
- }
70
- /** A user-facing hint for the platform that failed. */
71
- hint() {
72
- return getPlatformHint(this.platform);
73
- }
14
+ constructor(opts) {
15
+ super(`failed to ${opts.mode} link ${opts.linkPath} → ${opts.target} on ${opts.platform}: ${opts.cause instanceof Error ? opts.cause.message : String(opts.cause)}`);
16
+ this.name = "LinkError";
17
+ this.mode = opts.mode;
18
+ this.target = opts.target;
19
+ this.linkPath = opts.linkPath;
20
+ this.platform = opts.platform;
21
+ this.cause = opts.cause;
22
+ }
23
+ /** A user-facing hint for the platform that failed. */
24
+ hint() {
25
+ return getPlatformHint(this.platform);
26
+ }
74
27
  };
28
+ /** Choose the platform-optimal link mode. */
75
29
  function pickMode(platform = process.platform) {
76
- return platform === "win32" ? "junction" : "symlink";
30
+ return platform === "win32" ? "junction" : "symlink";
77
31
  }
32
+ /** A short hint string for the platform where the link failed. */
78
33
  function getPlatformHint(platform) {
79
- if (platform === "win32") {
80
- return "On Windows, enable Developer Mode in Settings \u2192 Privacy & Security \u2192 For developers, or run as Administrator. NTFS junction also requires the target to be on a local NTFS volume (OneDrive / network shares are not supported).";
81
- }
82
- return "Symlink creation failed. Check filesystem permissions and that the source directory exists. On some filesystems (FAT32, exFAT, network mounts) symlinks are not supported.";
34
+ if (platform === "win32") return "On Windows, enable Developer Mode in Settings → Privacy & Security → For developers, or run as Administrator. NTFS junction also requires the target to be on a local NTFS volume (OneDrive / network shares are not supported).";
35
+ return "Symlink creation failed. Check filesystem permissions and that the source directory exists. On some filesystems (FAT32, exFAT, network mounts) symlinks are not supported.";
83
36
  }
37
+ /**
38
+ * Resolve the source path inside `~/.serviceme/repos/<repoId>/{skills,agents}/<name>`.
39
+ * Exported for callers that need to inspect the target without installing.
40
+ */
84
41
  function resolveSkillTarget(repoId, skillName, kind = "skill") {
85
- return path2.join(getRepoDir(repoId), ENTRY_SUBDIR[kind], skillName);
42
+ return path.join(getRepoDir(repoId), ENTRY_SUBDIR[kind], skillName);
86
43
  }
44
+ /**
45
+ * Resolve the workspace link path — `<workspaceDir>/.github/{skills,agents}/<name>` —
46
+ * the same directory Copilot/Claude actually scan.
47
+ */
87
48
  function resolveSkillLinkPath(workspaceDir, skillName, kind = "skill") {
88
- return path2.join(workspaceDir, ".github", ENTRY_SUBDIR[kind], skillName);
49
+ return path.join(workspaceDir, ".github", ENTRY_SUBDIR[kind], skillName);
89
50
  }
51
+ /**
52
+ * Resolve the user-scope link path — `~/.copilot/{skills,agents}/<name>` —
53
+ * visible to every workspace.
54
+ */
90
55
  function resolveUserSkillLinkPath(skillName, kind = "skill") {
91
- return path2.join(getHomeDir(), ".copilot", ENTRY_SUBDIR[kind], skillName);
56
+ return path.join(getHomeDir(), ".copilot", ENTRY_SUBDIR[kind], skillName);
92
57
  }
93
58
  function resolveLinkPath(opts) {
94
- if (opts.scope === "workspace" && !opts.workspaceDir) {
95
- throw new Error(
96
- "workspaceDir is required for scope: 'workspace' (pass scope: 'user' for a global install instead)."
97
- );
98
- }
99
- const base = opts.scope === "user" ? resolveUserSkillLinkPath(opts.skillName, opts.kind) : resolveSkillLinkPath(opts.workspaceDir, opts.skillName, opts.kind);
100
- return opts.linkBasename ? path2.join(path2.dirname(base), opts.linkBasename) : base;
59
+ if (opts.scope === "workspace" && !opts.workspaceDir) throw new Error("workspaceDir is required for scope: 'workspace' (pass scope: 'user' for a global install instead).");
60
+ const base = opts.scope === "user" ? resolveUserSkillLinkPath(opts.skillName, opts.kind) : resolveSkillLinkPath(opts.workspaceDir, opts.skillName, opts.kind);
61
+ return opts.linkBasename ? path.join(path.dirname(base), opts.linkBasename) : base;
101
62
  }
63
+ /**
64
+ * Install a skill into a workspace by creating a link at the standard
65
+ * path. Throws `LinkError` on failure (no silent copy fallback per
66
+ * §11.8).
67
+ *
68
+ * Idempotent: if the correct link already exists, returns the existing
69
+ * record without touching the filesystem. If a *wrong* link exists at
70
+ * the target path, it is replaced.
71
+ */
102
72
  async function installSkillToWorkspace(opts) {
103
- const kind = opts.kind ?? "skill";
104
- const scope = opts.scope ?? "workspace";
105
- const isFile = opts.sourceIsFile ?? false;
106
- const target = opts.sourcePath ?? resolveSkillTarget(opts.repoId, opts.skillName, kind);
107
- const linkPath = resolveLinkPath({
108
- workspaceDir: opts.workspaceDir,
109
- skillName: opts.skillName,
110
- kind,
111
- scope,
112
- linkBasename: isFile ? path2.basename(target) : void 0
113
- });
114
- await fs.access(target).catch(() => {
115
- throw new LinkError({
116
- mode: "symlink",
117
- // placeholder — mode not chosen yet
118
- target,
119
- linkPath,
120
- platform: process.platform,
121
- cause: new Error(`skill source not found: ${target}`)
122
- });
123
- });
124
- if (await isCorrectLink(linkPath, target)) {
125
- return { mode: "symlink", linkPath, targetPath: target };
126
- }
127
- await removeIfExists(linkPath);
128
- const requestedMode = opts.mode === void 0 || opts.mode === "auto" ? pickMode() : opts.mode;
129
- const mode = isFile && requestedMode === "junction" ? "symlink" : requestedMode;
130
- await fs.mkdir(path2.dirname(linkPath), { recursive: true });
131
- try {
132
- if (mode === "junction") {
133
- await fs.symlink(target, linkPath, "junction");
134
- } else if (mode === "symlink") {
135
- await fs.symlink(target, linkPath, isFile ? "file" : "dir");
136
- } else {
137
- throw new LinkError({
138
- mode: "copy",
139
- target,
140
- linkPath,
141
- platform: process.platform,
142
- cause: new Error(
143
- "copy mode is not supported in v1 \u2014 see spec \xA711.8 (throw, not copy fallback)"
144
- )
145
- });
146
- }
147
- } catch (err) {
148
- if (err instanceof LinkError) throw err;
149
- throw new LinkError({
150
- mode,
151
- target,
152
- linkPath,
153
- platform: process.platform,
154
- cause: err
155
- });
156
- }
157
- return { mode, linkPath, targetPath: target };
73
+ const kind = opts.kind ?? "skill";
74
+ const scope = opts.scope ?? "workspace";
75
+ const isFile = opts.sourceIsFile ?? false;
76
+ const target = opts.sourcePath ?? resolveSkillTarget(opts.repoId, opts.skillName, kind);
77
+ const linkPath = resolveLinkPath({
78
+ workspaceDir: opts.workspaceDir,
79
+ skillName: opts.skillName,
80
+ kind,
81
+ scope,
82
+ linkBasename: isFile ? path.basename(target) : void 0
83
+ });
84
+ await fs.access(target).catch(() => {
85
+ throw new LinkError({
86
+ mode: "symlink",
87
+ target,
88
+ linkPath,
89
+ platform: process.platform,
90
+ cause: /* @__PURE__ */ new Error(`skill source not found: ${target}`)
91
+ });
92
+ });
93
+ if (await isCorrectLink(linkPath, target)) return {
94
+ mode: "symlink",
95
+ linkPath,
96
+ targetPath: target
97
+ };
98
+ await removeIfExists(linkPath);
99
+ const requestedMode = opts.mode === void 0 || opts.mode === "auto" ? pickMode() : opts.mode;
100
+ const mode = isFile && requestedMode === "junction" ? "symlink" : requestedMode;
101
+ await fs.mkdir(path.dirname(linkPath), { recursive: true });
102
+ try {
103
+ if (mode === "junction") await fs.symlink(target, linkPath, "junction");
104
+ else if (mode === "symlink") await fs.symlink(target, linkPath, isFile ? "file" : "dir");
105
+ else throw new LinkError({
106
+ mode: "copy",
107
+ target,
108
+ linkPath,
109
+ platform: process.platform,
110
+ cause: /* @__PURE__ */ new Error("copy mode is not supported in v1 — see spec §11.8 (throw, not copy fallback)")
111
+ });
112
+ } catch (err) {
113
+ if (err instanceof LinkError) throw err;
114
+ throw new LinkError({
115
+ mode,
116
+ target,
117
+ linkPath,
118
+ platform: process.platform,
119
+ cause: err
120
+ });
121
+ }
122
+ return {
123
+ mode,
124
+ linkPath,
125
+ targetPath: target
126
+ };
158
127
  }
128
+ /**
129
+ * Convert an existing, real (non-symlink) skill/agent directory at the
130
+ * standard scan path into a symlink pointing at the matching global
131
+ * repo entry. Refuses to run when there's nothing real to convert —
132
+ * callers should only surface this action when a marketplace/scope
133
+ * state check already confirmed a non-symlink install is present.
134
+ */
159
135
  async function convertToSymlink(opts) {
160
- const kind = opts.kind ?? "skill";
161
- const scope = opts.scope ?? "workspace";
162
- const linkPath = resolveLinkPath({
163
- workspaceDir: opts.workspaceDir,
164
- skillName: opts.skillName,
165
- kind,
166
- scope,
167
- linkBasename: opts.sourceIsFile && opts.sourcePath ? path2.basename(opts.sourcePath) : void 0
168
- });
169
- const stat2 = await fs.lstat(linkPath).catch(() => null);
170
- if (!stat2) {
171
- throw new Error(`Nothing installed at ${linkPath} to convert.`);
172
- }
173
- if (stat2.isSymbolicLink()) {
174
- throw new Error(`${linkPath} is already a symlink.`);
175
- }
176
- return installSkillToWorkspace(opts);
136
+ const kind = opts.kind ?? "skill";
137
+ const scope = opts.scope ?? "workspace";
138
+ const linkPath = resolveLinkPath({
139
+ workspaceDir: opts.workspaceDir,
140
+ skillName: opts.skillName,
141
+ kind,
142
+ scope,
143
+ linkBasename: opts.sourceIsFile && opts.sourcePath ? path.basename(opts.sourcePath) : void 0
144
+ });
145
+ const stat = await fs.lstat(linkPath).catch(() => null);
146
+ if (!stat) throw new Error(`Nothing installed at ${linkPath} to convert.`);
147
+ if (stat.isSymbolicLink()) throw new Error(`${linkPath} is already a symlink.`);
148
+ return installSkillToWorkspace(opts);
177
149
  }
150
+ /**
151
+ * Remove a skill/agent link from a workspace/user scope. Idempotent:
152
+ * succeeds when the link does not exist (no error).
153
+ *
154
+ * Tries the bare `<name>` path first (directory-based entries), then
155
+ * falls back to scanning the scope dir for a `<name>.<ext>` file (flat
156
+ * manifest entries, e.g. `CSharpExpert.agent.md`) — the caller doesn't
157
+ * necessarily still have the source repo entry to know which shape was
158
+ * installed (e.g. the repo may have been removed since).
159
+ */
178
160
  async function uninstallSkillFromWorkspace(opts) {
179
- const kind = opts.kind ?? "skill";
180
- const scope = opts.scope ?? "workspace";
181
- const linkPath = resolveLinkPath({
182
- workspaceDir: opts.workspaceDir,
183
- skillName: opts.skillName,
184
- kind,
185
- scope
186
- });
187
- if (await pathExists(linkPath)) {
188
- await removeIfExists(linkPath);
189
- return;
190
- }
191
- const scopeDir = path2.dirname(linkPath);
192
- let entries;
193
- try {
194
- entries = await fs.readdir(scopeDir, { withFileTypes: true });
195
- } catch {
196
- entries = [];
197
- }
198
- const flatMatch = entries.find(
199
- (e) => e.name === opts.skillName || e.name.startsWith(`${opts.skillName}.`)
200
- );
201
- if (flatMatch) {
202
- await removeIfExists(path2.join(scopeDir, flatMatch.name));
203
- return;
204
- }
205
- if (scope === "user") {
206
- const legacyDir = path2.join(getHomeDir(), LEGACY_USER_SCOPE_ROOT, ENTRY_SUBDIR[kind]);
207
- const legacyEntries = await fs.readdir(legacyDir, { withFileTypes: true }).catch(() => []);
208
- const legacyMatch = legacyEntries.find(
209
- (e) => e.name === opts.skillName || e.name.startsWith(`${opts.skillName}.`)
210
- );
211
- if (legacyMatch) {
212
- await removeIfExists(path2.join(legacyDir, legacyMatch.name));
213
- }
214
- }
161
+ const kind = opts.kind ?? "skill";
162
+ const scope = opts.scope ?? "workspace";
163
+ const linkPath = resolveLinkPath({
164
+ workspaceDir: opts.workspaceDir,
165
+ skillName: opts.skillName,
166
+ kind,
167
+ scope
168
+ });
169
+ if (await pathExists(linkPath)) {
170
+ await removeIfExists(linkPath);
171
+ return;
172
+ }
173
+ const scopeDir = path.dirname(linkPath);
174
+ let entries;
175
+ try {
176
+ entries = await fs.readdir(scopeDir, { withFileTypes: true });
177
+ } catch {
178
+ entries = [];
179
+ }
180
+ const flatMatch = entries.find((e) => e.name === opts.skillName || e.name.startsWith(`${opts.skillName}.`));
181
+ if (flatMatch) {
182
+ await removeIfExists(path.join(scopeDir, flatMatch.name));
183
+ return;
184
+ }
185
+ if (scope === "user") {
186
+ const legacyDir = path.join(getHomeDir(), LEGACY_USER_SCOPE_ROOT, ENTRY_SUBDIR[kind]);
187
+ const legacyMatch = (await fs.readdir(legacyDir, { withFileTypes: true }).catch(() => [])).find((e) => e.name === opts.skillName || e.name.startsWith(`${opts.skillName}.`));
188
+ if (legacyMatch) await removeIfExists(path.join(legacyDir, legacyMatch.name));
189
+ }
215
190
  }
191
+ /** `true` when `p` exists (following symlinks is irrelevant — `lstat` is enough). */
216
192
  async function pathExists(p) {
217
- try {
218
- await fs.lstat(p);
219
- return true;
220
- } catch {
221
- return false;
222
- }
193
+ try {
194
+ await fs.lstat(p);
195
+ return true;
196
+ } catch {
197
+ return false;
198
+ }
223
199
  }
224
- var MANIFEST_FILENAME = {
225
- skill: "SKILL.md",
226
- agent: "AGENT.md"
200
+ const MANIFEST_FILENAME = {
201
+ skill: "SKILL.md",
202
+ agent: "AGENT.md"
227
203
  };
228
- var LEGACY_USER_SCOPE_ROOT = ".agents";
204
+ /**
205
+ * Legacy user-scope root used before the migration to `~/.copilot`.
206
+ * Kept readable so user-scope listing can surface old links instead of
207
+ * silently hiding content the user still has installed.
208
+ */
209
+ const LEGACY_USER_SCOPE_ROOT = ".agents";
210
+ /**
211
+ * Enumerate all skill/agent entries currently present in a workspace/user
212
+ * scope — both symlinked installs (`mode: "symlink"`/`"junction"`) AND
213
+ * real, non-symlink directories/files that already carry a valid
214
+ * manifest (`mode: "copy"`). Used by the UI's "Manage installed skills"
215
+ * view so it can show a real (copy-installed) entry as installed too —
216
+ * this is common for the user (global) scope, where many skills are
217
+ * placed by other tools/installers as plain copies rather than via this
218
+ * package's symlink-only install flow. `repoId` is `""` for `"copy"`
219
+ * entries since there's no symlink target to resolve it from; callers
220
+ * match these by name only. Returns an empty array when the scan
221
+ * directory doesn't exist.
222
+ */
229
223
  async function listLinkedSkills(workspaceDir, kind = "skill", scope = "workspace") {
230
- if (scope === "workspace" && !workspaceDir) {
231
- return [];
232
- }
233
- const userRoot = path2.join(getHomeDir(), ".copilot", ENTRY_SUBDIR[kind]);
234
- const root = scope === "user" ? userRoot : path2.join(workspaceDir, ".github", ENTRY_SUBDIR[kind]);
235
- let entries;
236
- try {
237
- entries = await fs.readdir(root, { withFileTypes: true });
238
- } catch (err) {
239
- if (err.code === "ENOENT") return [];
240
- throw err;
241
- }
242
- const results = [];
243
- for (const entry of entries) {
244
- const entryPath = path2.join(root, entry.name);
245
- if (entry.isSymbolicLink()) {
246
- const skillName = entry.name.replace(/\.(agent|skill)\.md$/, "");
247
- const rawTarget = await readLinkTarget(entryPath);
248
- if (!rawTarget) continue;
249
- const resolvedTarget = resolveTargetPath(entryPath, rawTarget);
250
- const targetStat = await fs.stat(resolvedTarget).catch(() => null);
251
- if (!targetStat) continue;
252
- results.push({
253
- repoId: extractRepoIdFromTarget(resolvedTarget) ?? "",
254
- skillName,
255
- linkPath: entryPath,
256
- targetPath: resolvedTarget,
257
- mode: process.platform === "win32" ? "junction" : "symlink",
258
- scope
259
- });
260
- continue;
261
- }
262
- if (entry.isDirectory()) {
263
- const hasManifest = await fs.access(path2.join(entryPath, MANIFEST_FILENAME[kind])).then(() => true).catch(() => false);
264
- if (!hasManifest) continue;
265
- results.push({
266
- repoId: "",
267
- skillName: entry.name,
268
- linkPath: entryPath,
269
- targetPath: entryPath,
270
- mode: "copy",
271
- scope
272
- });
273
- continue;
274
- }
275
- if (entry.isFile() && entry.name.endsWith(`.${kind}.md`)) {
276
- results.push({
277
- repoId: "",
278
- skillName: entry.name.replace(/\.(agent|skill)\.md$/, ""),
279
- linkPath: entryPath,
280
- targetPath: entryPath,
281
- mode: "copy",
282
- scope
283
- });
284
- }
285
- }
286
- if (scope === "user") {
287
- const seen = new Set(results.map((r) => r.skillName));
288
- for (const legacy of await listLegacyUserScopeSkills(kind)) {
289
- if (!seen.has(legacy.skillName)) {
290
- results.push(legacy);
291
- }
292
- }
293
- }
294
- return results;
224
+ if (scope === "workspace" && !workspaceDir) return [];
225
+ const userRoot = path.join(getHomeDir(), ".copilot", ENTRY_SUBDIR[kind]);
226
+ const root = scope === "user" ? userRoot : path.join(workspaceDir, ".github", ENTRY_SUBDIR[kind]);
227
+ let entries;
228
+ try {
229
+ entries = await fs.readdir(root, { withFileTypes: true });
230
+ } catch (err) {
231
+ if (err.code === "ENOENT") return [];
232
+ throw err;
233
+ }
234
+ const results = [];
235
+ for (const entry of entries) {
236
+ const entryPath = path.join(root, entry.name);
237
+ if (entry.isSymbolicLink()) {
238
+ const skillName = entry.name.replace(/\.(agent|skill)\.md$/, "");
239
+ const rawTarget = await readLinkTarget(entryPath);
240
+ if (!rawTarget) continue;
241
+ const resolvedTarget = resolveTargetPath(entryPath, rawTarget);
242
+ if (!await fs.stat(resolvedTarget).catch(() => null)) continue;
243
+ results.push({
244
+ repoId: extractRepoIdFromTarget(resolvedTarget) ?? "",
245
+ skillName,
246
+ linkPath: entryPath,
247
+ targetPath: resolvedTarget,
248
+ mode: process.platform === "win32" ? "junction" : "symlink",
249
+ scope
250
+ });
251
+ continue;
252
+ }
253
+ if (entry.isDirectory()) {
254
+ if (!await fs.access(path.join(entryPath, MANIFEST_FILENAME[kind])).then(() => true).catch(() => false)) continue;
255
+ results.push({
256
+ repoId: "",
257
+ skillName: entry.name,
258
+ linkPath: entryPath,
259
+ targetPath: entryPath,
260
+ mode: "copy",
261
+ scope
262
+ });
263
+ continue;
264
+ }
265
+ if (entry.isFile() && entry.name.endsWith(`.${kind}.md`)) results.push({
266
+ repoId: "",
267
+ skillName: entry.name.replace(/\.(agent|skill)\.md$/, ""),
268
+ linkPath: entryPath,
269
+ targetPath: entryPath,
270
+ mode: "copy",
271
+ scope
272
+ });
273
+ }
274
+ if (scope === "user") {
275
+ const seen = new Set(results.map((r) => r.skillName));
276
+ for (const legacy of await listLegacyUserScopeSkills(kind)) if (!seen.has(legacy.skillName)) results.push(legacy);
277
+ }
278
+ return results;
295
279
  }
280
+ /**
281
+ * List entries still living in the legacy `~/.agents` user scope.
282
+ * Read-only: migration is an explicit user action elsewhere.
283
+ */
296
284
  async function listLegacyUserScopeSkills(kind = "skill") {
297
- const root = path2.join(getHomeDir(), LEGACY_USER_SCOPE_ROOT, ENTRY_SUBDIR[kind]);
298
- let entries;
299
- try {
300
- entries = await fs.readdir(root, { withFileTypes: true });
301
- } catch (err) {
302
- if (err.code === "ENOENT") return [];
303
- throw err;
304
- }
305
- const results = [];
306
- for (const entry of entries) {
307
- const entryPath = path2.join(root, entry.name);
308
- if (entry.isSymbolicLink()) {
309
- const skillName = entry.name.replace(/\.(agent|skill)\.md$/, "");
310
- const rawTarget = await readLinkTarget(entryPath);
311
- if (!rawTarget) continue;
312
- const resolvedTarget = resolveTargetPath(entryPath, rawTarget);
313
- const targetStat = await fs.stat(resolvedTarget).catch(() => null);
314
- if (!targetStat) continue;
315
- results.push({
316
- repoId: extractRepoIdFromTarget(resolvedTarget) ?? "",
317
- skillName,
318
- linkPath: entryPath,
319
- targetPath: resolvedTarget,
320
- mode: process.platform === "win32" ? "junction" : "symlink",
321
- scope: "user"
322
- });
323
- } else if (entry.isDirectory()) {
324
- const manifestPath = path2.join(entryPath, MANIFEST_FILENAME[kind]);
325
- const stat2 = await fs.stat(manifestPath).catch(() => null);
326
- if (stat2?.isFile()) {
327
- results.push({
328
- repoId: "",
329
- skillName: entry.name,
330
- linkPath: entryPath,
331
- targetPath: manifestPath,
332
- mode: "copy",
333
- scope: "user"
334
- });
335
- }
336
- }
337
- }
338
- return results;
285
+ const root = path.join(getHomeDir(), LEGACY_USER_SCOPE_ROOT, ENTRY_SUBDIR[kind]);
286
+ let entries;
287
+ try {
288
+ entries = await fs.readdir(root, { withFileTypes: true });
289
+ } catch (err) {
290
+ if (err.code === "ENOENT") return [];
291
+ throw err;
292
+ }
293
+ const results = [];
294
+ for (const entry of entries) {
295
+ const entryPath = path.join(root, entry.name);
296
+ if (entry.isSymbolicLink()) {
297
+ const skillName = entry.name.replace(/\.(agent|skill)\.md$/, "");
298
+ const rawTarget = await readLinkTarget(entryPath);
299
+ if (!rawTarget) continue;
300
+ const resolvedTarget = resolveTargetPath(entryPath, rawTarget);
301
+ if (!await fs.stat(resolvedTarget).catch(() => null)) continue;
302
+ results.push({
303
+ repoId: extractRepoIdFromTarget(resolvedTarget) ?? "",
304
+ skillName,
305
+ linkPath: entryPath,
306
+ targetPath: resolvedTarget,
307
+ mode: process.platform === "win32" ? "junction" : "symlink",
308
+ scope: "user"
309
+ });
310
+ } else if (entry.isDirectory()) {
311
+ const manifestPath = path.join(entryPath, MANIFEST_FILENAME[kind]);
312
+ if ((await fs.stat(manifestPath).catch(() => null))?.isFile()) results.push({
313
+ repoId: "",
314
+ skillName: entry.name,
315
+ linkPath: entryPath,
316
+ targetPath: manifestPath,
317
+ mode: "copy",
318
+ scope: "user"
319
+ });
320
+ }
321
+ }
322
+ return results;
339
323
  }
324
+ /** Returns `true` when `linkPath` is a symlink/junction pointing at `expectedTarget`. */
340
325
  async function isCorrectLink(linkPath, expectedTarget) {
341
- let actual;
342
- try {
343
- actual = await readLinkTarget(linkPath);
344
- } catch {
345
- return false;
346
- }
347
- if (!actual) return false;
348
- return resolveTargetPath(linkPath, actual) === resolveTargetPath(linkPath, expectedTarget);
326
+ let actual;
327
+ try {
328
+ actual = await readLinkTarget(linkPath);
329
+ } catch {
330
+ return false;
331
+ }
332
+ if (!actual) return false;
333
+ return resolveTargetPath(linkPath, actual) === resolveTargetPath(linkPath, expectedTarget);
349
334
  }
335
+ /** Read a link's target without throwing — returns undefined on ENOENT. */
350
336
  async function readLinkTarget(linkPath) {
351
- try {
352
- return await fs.readlink(linkPath);
353
- } catch (err) {
354
- if (err.code === "ENOENT") return void 0;
355
- throw err;
356
- }
337
+ try {
338
+ return await fs.readlink(linkPath);
339
+ } catch (err) {
340
+ if (err.code === "ENOENT") return void 0;
341
+ throw err;
342
+ }
357
343
  }
344
+ /**
345
+ * Resolve a relative symlink target against its parent directory so a
346
+ * relative link reads back as an absolute path comparable to another
347
+ * absolute path.
348
+ */
358
349
  function resolveTargetPath(linkPath, target) {
359
- if (path2.isAbsolute(target)) return path2.normalize(target);
360
- return path2.normalize(path2.resolve(path2.dirname(linkPath), target));
350
+ if (path.isAbsolute(target)) return path.normalize(target);
351
+ return path.normalize(path.resolve(path.dirname(linkPath), target));
361
352
  }
353
+ /**
354
+ * Parse the `<repoId>` segment back out of a resolved link target of
355
+ * the shape `.../.serviceme/repos/<repoId>/{skills,agents}/<name>`.
356
+ * Returns `undefined` when the target doesn't match that shape (e.g. a
357
+ * hand-authored local skill under `~/.agents/skills/<name>` with no
358
+ * backing repo).
359
+ */
362
360
  function extractRepoIdFromTarget(target) {
363
- const marker = `${path2.sep}repos${path2.sep}`;
364
- const idx = target.indexOf(marker);
365
- if (idx === -1) return void 0;
366
- const rest = target.slice(idx + marker.length);
367
- const [repoId] = rest.split(path2.sep);
368
- return repoId || void 0;
361
+ const marker = `${path.sep}repos${path.sep}`;
362
+ const idx = target.indexOf(marker);
363
+ if (idx === -1) return void 0;
364
+ const [repoId] = target.slice(idx + marker.length).split(path.sep);
365
+ return repoId || void 0;
369
366
  }
367
+ /** Best-effort removal of a path. ENOENT is silently ignored. */
370
368
  async function removeIfExists(p) {
371
- try {
372
- const stat2 = await fs.lstat(p);
373
- if (stat2.isDirectory() && !stat2.isSymbolicLink()) {
374
- await fs.rm(p, { recursive: true, force: true });
375
- } else {
376
- await fs.unlink(p);
377
- }
378
- } catch (err) {
379
- if (err.code === "ENOENT") return;
380
- throw err;
381
- }
369
+ try {
370
+ const stat = await fs.lstat(p);
371
+ if (stat.isDirectory() && !stat.isSymbolicLink()) await fs.rm(p, {
372
+ recursive: true,
373
+ force: true
374
+ });
375
+ else await fs.unlink(p);
376
+ } catch (err) {
377
+ if (err.code === "ENOENT") return;
378
+ throw err;
379
+ }
382
380
  }
383
- export {
384
- LinkError,
385
- convertToSymlink,
386
- getPlatformHint,
387
- installSkillToWorkspace,
388
- listLegacyUserScopeSkills,
389
- listLinkedSkills,
390
- pickMode,
391
- resolveSkillLinkPath,
392
- resolveSkillTarget,
393
- resolveUserSkillLinkPath,
394
- uninstallSkillFromWorkspace
395
- };
381
+ //#endregion
382
+ export { LinkError, convertToSymlink, getPlatformHint, installSkillToWorkspace, listLegacyUserScopeSkills, listLinkedSkills, pickMode, resolveSkillLinkPath, resolveSkillTarget, resolveUserSkillLinkPath, uninstallSkillFromWorkspace };
383
+
396
384
  //# sourceMappingURL=skill-linker.mjs.map