@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.
- package/dist/auth-Bzss8zOv.mjs +995 -0
- package/dist/auth-Bzss8zOv.mjs.map +1 -0
- package/dist/auth-DXam3c4S.js +1078 -0
- package/dist/auth-DXam3c4S.js.map +1 -0
- package/dist/auth.d.mts +2 -2
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +14 -1029
- package/dist/auth.mjs +2 -991
- package/dist/device-CDDcwDY6.mjs +758 -0
- package/dist/device-CDDcwDY6.mjs.map +1 -0
- package/dist/device-DuAYH8bk.js +839 -0
- package/dist/device-DuAYH8bk.js.map +1 -0
- package/dist/device.d.mts +2 -455
- package/dist/device.d.ts +2 -455
- package/dist/device.js +15 -728
- package/dist/device.mjs +2 -679
- package/dist/index-2rX86yMq.d.mts +331 -0
- package/dist/index-BI11AfMH.d.mts +377 -0
- package/dist/index-BI11AfMH.d.ts +377 -0
- package/dist/index-CoA6y0MP.d.mts +498 -0
- package/dist/index-CoA6y0MP.d.ts +498 -0
- package/dist/index-DaW7IYA-.d.mts +185 -0
- package/dist/index-DaW7IYA-.d.ts +185 -0
- package/dist/index-Dp6FwWKh.d.ts +331 -0
- package/dist/index.d.mts +2340 -2101
- package/dist/index.d.ts +2340 -2101
- package/dist/index.js +8117 -10538
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +7895 -10318
- package/dist/index.mjs.map +1 -1
- package/dist/rolldown-runtime-VH7oDXx4.js +28 -0
- package/dist/skill-linker.d.mts +82 -81
- package/dist/skill-linker.d.ts +82 -81
- package/dist/skill-linker.js +359 -402
- package/dist/skill-linker.js.map +1 -1
- package/dist/skill-linker.mjs +345 -357
- package/dist/skill-linker.mjs.map +1 -1
- package/dist/skill-store-Bym2yinU.mjs +388 -0
- package/dist/skill-store-Bym2yinU.mjs.map +1 -0
- package/dist/skill-store-CJdYyyxu.js +408 -0
- package/dist/skill-store-CJdYyyxu.js.map +1 -0
- package/dist/skill-store.d.mts +41 -42
- package/dist/skill-store.d.ts +41 -42
- package/dist/skill-store.js +5 -338
- package/dist/skill-store.mjs +2 -301
- package/dist/submit-Cg7wSa3x.mjs +84 -0
- package/dist/submit-Cg7wSa3x.mjs.map +1 -0
- package/dist/submit-CltE6YOZ.js +98 -0
- package/dist/submit-CltE6YOZ.js.map +1 -0
- package/dist/submit.d.mts +2 -3
- package/dist/submit.d.ts +2 -3
- package/dist/submit.js +4 -166
- package/dist/submit.mjs +2 -130
- package/dist/toolbox-CbfxpNg-.mjs +540 -0
- package/dist/toolbox-CbfxpNg-.mjs.map +1 -0
- package/dist/toolbox-CxnOr-FY.js +614 -0
- package/dist/toolbox-CxnOr-FY.js.map +1 -0
- package/dist/toolbox.d.mts +2 -244
- package/dist/toolbox.d.ts +2 -244
- package/dist/toolbox.js +14 -584
- package/dist/toolbox.mjs +2 -536
- package/dist/{types-B9gk3dXH.d.ts → types-BCV3IedE.d.mts} +25 -23
- package/dist/{types-B9gk3dXH.d.mts → types-BCV3IedE.d.ts} +25 -23
- package/dist/userHome-BquDeFVE.js +526 -0
- package/dist/userHome-BquDeFVE.js.map +1 -0
- package/dist/userHome-CcnAQPG1.mjs +212 -0
- package/dist/userHome-CcnAQPG1.mjs.map +1 -0
- package/package.json +10 -10
- package/dist/auth.js.map +0 -1
- package/dist/auth.mjs.map +0 -1
- package/dist/device.js.map +0 -1
- package/dist/device.mjs.map +0 -1
- package/dist/index-BMk4tqIT.d.mts +0 -328
- package/dist/index-CymN0x9Z.d.ts +0 -328
- package/dist/index-PD135hlB.d.mts +0 -626
- package/dist/index-PD135hlB.d.ts +0 -626
- package/dist/skill-store.js.map +0 -1
- package/dist/skill-store.mjs.map +0 -1
- package/dist/submit.js.map +0 -1
- package/dist/submit.mjs.map +0 -1
- package/dist/toolbox.js.map +0 -1
- package/dist/toolbox.mjs.map +0 -1
package/dist/skill-linker.mjs
CHANGED
|
@@ -1,396 +1,384 @@
|
|
|
1
|
-
|
|
2
|
-
import * as fs from "fs/promises";
|
|
3
|
-
import * as
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
+
return path.join(getHomeDir(), ".copilot", ENTRY_SUBDIR[kind], skillName);
|
|
92
57
|
}
|
|
93
58
|
function resolveLinkPath(opts) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
193
|
+
try {
|
|
194
|
+
await fs.lstat(p);
|
|
195
|
+
return true;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
223
199
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
200
|
+
const MANIFEST_FILENAME = {
|
|
201
|
+
skill: "SKILL.md",
|
|
202
|
+
agent: "AGENT.md"
|
|
227
203
|
};
|
|
228
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
|
|
360
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|