@danieljvdm/dev-kit 0.4.0 → 0.5.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/README.md +99 -30
- package/dev-kit.example.jsonc +1 -0
- package/package.json +1 -1
- package/schema/dev-kit.schema.json +18 -4
- package/skills/dev-kit/SKILL.md +12 -2
- package/src/bin/dev-kit.ts +5 -5
- package/src/catalog.ts +72 -15
- package/src/index.ts +8 -0
- package/src/manifest.ts +16 -2
- package/src/package-skill-source.ts +272 -0
- package/src/project-state.ts +33 -8
- package/src/skill-manager.ts +66 -30
- package/src/skill-selector.ts +43 -0
- package/src/sync.ts +175 -38
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Effect, FileSystem, Path, Result, Schema } from "effect";
|
|
2
|
+
|
|
3
|
+
import { observeSymbolicLink } from "./node-symbolic-link.ts";
|
|
4
|
+
import { isSkillName, parseSkillSelector } from "./skill-selector.ts";
|
|
5
|
+
import { isTypeScriptPackageName } from "./typescript-package-name.ts";
|
|
6
|
+
|
|
7
|
+
export class PackageSkillSourceError extends Schema.TaggedErrorClass<PackageSkillSourceError>()(
|
|
8
|
+
"PackageSkillSourceError",
|
|
9
|
+
{ message: Schema.String },
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
export type PackageSkillDiagnostic = {
|
|
13
|
+
readonly package: string;
|
|
14
|
+
readonly message: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type DiscoveredPackageSkill = {
|
|
18
|
+
readonly selector: string;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly description: string;
|
|
21
|
+
readonly package: string;
|
|
22
|
+
readonly version: string;
|
|
23
|
+
readonly path: string;
|
|
24
|
+
readonly linkPath: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const ProjectPackageSchema = Schema.fromJsonString(Schema.Struct({
|
|
28
|
+
dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
29
|
+
devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
30
|
+
optionalDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
31
|
+
peerDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
const PackageMetadataSchema = Schema.fromJsonString(Schema.Struct({
|
|
35
|
+
name: Schema.String,
|
|
36
|
+
version: Schema.String,
|
|
37
|
+
intent: Schema.optional(Schema.Unknown),
|
|
38
|
+
repository: Schema.optional(Schema.Unknown),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const nonEmptyString = (value: unknown): value is string =>
|
|
42
|
+
typeof value === "string" && value.trim().length > 0;
|
|
43
|
+
|
|
44
|
+
const hasIntentDiscoveryMetadata = (metadata: typeof PackageMetadataSchema.Type): boolean => {
|
|
45
|
+
const intent = metadata.intent;
|
|
46
|
+
if (typeof intent === "object" && intent !== null &&
|
|
47
|
+
"version" in intent && intent.version === 1 &&
|
|
48
|
+
"repo" in intent && nonEmptyString(intent.repo) &&
|
|
49
|
+
"docs" in intent && nonEmptyString(intent.docs)) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const repository = metadata.repository;
|
|
53
|
+
return nonEmptyString(repository) ||
|
|
54
|
+
(typeof repository === "object" && repository !== null &&
|
|
55
|
+
"url" in repository && nonEmptyString(repository.url));
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const isSafePackageVersion = (value: string): boolean =>
|
|
59
|
+
value.length > 0 && value.trim() === value && ![...value].some((character) => {
|
|
60
|
+
const code = character.charCodeAt(0);
|
|
61
|
+
return code <= 32 || (code >= 127 && code <= 159);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const isContained = (path: Path.Path, root: string, candidate: string): boolean => {
|
|
65
|
+
const relative = path.relative(root, candidate);
|
|
66
|
+
return relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const frontmatterScalar = (document: string, key: string): string | undefined => {
|
|
70
|
+
const body = document.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/)?.[1];
|
|
71
|
+
if (body === undefined) return undefined;
|
|
72
|
+
const lines = body.split(/\r?\n/);
|
|
73
|
+
const index = lines.findIndex((line) => line.startsWith(`${key}:`));
|
|
74
|
+
if (index < 0) return undefined;
|
|
75
|
+
const raw = lines[index]?.slice(key.length + 1).trim() ?? "";
|
|
76
|
+
const block = raw.match(/^([|>])(?:[1-9][+-]?|[+-][1-9]?)?$/)?.[1];
|
|
77
|
+
if (block !== undefined) {
|
|
78
|
+
const values: Array<string> = [];
|
|
79
|
+
for (const line of lines.slice(index + 1)) {
|
|
80
|
+
if (line.length > 0 && !/^\s/.test(line)) break;
|
|
81
|
+
values.push(line.trim());
|
|
82
|
+
}
|
|
83
|
+
const value = block === "|" ? values.join("\n").trim() : values.join(" ").trim();
|
|
84
|
+
return value.length > 0 ? value : undefined;
|
|
85
|
+
}
|
|
86
|
+
const quoted = raw.match(/^(['"])([\s\S]*?)\1(?:\s+#.*)?$/)?.[2];
|
|
87
|
+
const value = (quoted ?? raw.replace(/\s+#.*$/, "")).trim();
|
|
88
|
+
return value.length > 0 ? value : undefined;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const skillName = (document: string): string | undefined =>
|
|
92
|
+
frontmatterScalar(document, "name");
|
|
93
|
+
|
|
94
|
+
const skillDescription = (document: string): string | undefined =>
|
|
95
|
+
frontmatterScalar(document, "description");
|
|
96
|
+
|
|
97
|
+
const rejectNestedSymlinks = Effect.fn("rejectPackageSkillSymlinks")(function* (skillRoot: string) {
|
|
98
|
+
const fs = yield* FileSystem.FileSystem;
|
|
99
|
+
const path = yield* Path.Path;
|
|
100
|
+
const pending = [skillRoot];
|
|
101
|
+
while (pending.length > 0) {
|
|
102
|
+
const current = pending.pop();
|
|
103
|
+
if (current === undefined) continue;
|
|
104
|
+
if ((yield* observeSymbolicLink(current)).kind === "symlink") {
|
|
105
|
+
return yield* new PackageSkillSourceError({ message: `package skill contains a symlink: ${current}` });
|
|
106
|
+
}
|
|
107
|
+
const info = yield* fs.stat(current).pipe(
|
|
108
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `could not inspect package skill: ${current}` })),
|
|
109
|
+
);
|
|
110
|
+
if (info.type !== "Directory") continue;
|
|
111
|
+
for (const entry of yield* fs.readDirectory(current).pipe(
|
|
112
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `could not read package skill: ${current}` })),
|
|
113
|
+
)) pending.push(path.join(current, entry));
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const readDirectDependencyNames = Effect.fn("readDirectPackageSkillDependencyNames")(function* (projectDir: string) {
|
|
118
|
+
const fs = yield* FileSystem.FileSystem;
|
|
119
|
+
const path = yield* Path.Path;
|
|
120
|
+
const manifestPath = path.join(projectDir, "package.json");
|
|
121
|
+
const manifest = yield* fs.readFileString(manifestPath).pipe(
|
|
122
|
+
Effect.flatMap(Schema.decodeUnknownEffect(ProjectPackageSchema)),
|
|
123
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `invalid project package.json: ${manifestPath}` })),
|
|
124
|
+
);
|
|
125
|
+
return [...new Set([
|
|
126
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
127
|
+
...Object.keys(manifest.devDependencies ?? {}),
|
|
128
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
129
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
130
|
+
])].sort();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
type InstalledPackageSkills = {
|
|
134
|
+
readonly package: string;
|
|
135
|
+
readonly version: string;
|
|
136
|
+
readonly packageLink: string;
|
|
137
|
+
readonly skillsRoot: string;
|
|
138
|
+
readonly names: ReadonlyArray<string>;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const loadInstalledPackageSkills = Effect.fn("loadInstalledPackageSkills")(function* (
|
|
142
|
+
projectDir: string,
|
|
143
|
+
packageName: string,
|
|
144
|
+
) {
|
|
145
|
+
const fs = yield* FileSystem.FileSystem;
|
|
146
|
+
const path = yield* Path.Path;
|
|
147
|
+
const packageLink = path.join(projectDir, "node_modules", ...packageName.split("/"));
|
|
148
|
+
const packageRoot = yield* fs.realPath(packageLink).pipe(
|
|
149
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `package skill package is not installed: ${packageName}` })),
|
|
150
|
+
);
|
|
151
|
+
const packageInfo = yield* fs.stat(packageRoot).pipe(
|
|
152
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `could not inspect package skill package: ${packageName}` })),
|
|
153
|
+
);
|
|
154
|
+
if (packageInfo.type !== "Directory") return yield* new PackageSkillSourceError({ message: `package skill package is not a directory: ${packageName}` });
|
|
155
|
+
const metadata = yield* fs.readFileString(path.join(packageRoot, "package.json")).pipe(
|
|
156
|
+
Effect.flatMap(Schema.decodeUnknownEffect(PackageMetadataSchema)),
|
|
157
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `invalid package.json for package skill package: ${packageName}` })),
|
|
158
|
+
);
|
|
159
|
+
if (metadata.name !== packageName) return yield* new PackageSkillSourceError({ message: `package.json name does not match package skill package: ${packageName}` });
|
|
160
|
+
if (!isSafePackageVersion(metadata.version)) return yield* new PackageSkillSourceError({ message: `package.json has an invalid version for package skill package: ${packageName}` });
|
|
161
|
+
if (!hasIntentDiscoveryMetadata(metadata)) return yield* new PackageSkillSourceError({ message: `package does not declare Intent-compatible discovery metadata: ${packageName}` });
|
|
162
|
+
const skillsPath = "skills";
|
|
163
|
+
const skillsLink = path.join(packageLink, skillsPath);
|
|
164
|
+
if ((yield* observeSymbolicLink(skillsLink)).kind === "symlink") return yield* new PackageSkillSourceError({ message: `package skills path is a symlink: ${packageName}/${skillsPath}` });
|
|
165
|
+
const skillsRoot = yield* fs.realPath(skillsLink).pipe(
|
|
166
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `package skill package has no skills directory: ${packageName}` })),
|
|
167
|
+
);
|
|
168
|
+
if (!isContained(path, packageRoot, skillsRoot)) return yield* new PackageSkillSourceError({ message: `package skills path resolves outside package root: ${packageName}/${skillsPath}` });
|
|
169
|
+
const skillsInfo = yield* fs.stat(skillsRoot);
|
|
170
|
+
if (skillsInfo.type !== "Directory") return yield* new PackageSkillSourceError({ message: `package skills path is not a directory: ${packageName}/${skillsPath}` });
|
|
171
|
+
const names = (yield* fs.readDirectory(skillsRoot).pipe(
|
|
172
|
+
Effect.mapError(() => new PackageSkillSourceError({
|
|
173
|
+
message: `package skill package has no readable skills directory: ${packageName}`,
|
|
174
|
+
})),
|
|
175
|
+
)).filter(isSkillName).sort();
|
|
176
|
+
return {
|
|
177
|
+
package: packageName,
|
|
178
|
+
version: metadata.version,
|
|
179
|
+
packageLink,
|
|
180
|
+
skillsRoot,
|
|
181
|
+
names,
|
|
182
|
+
} satisfies InstalledPackageSkills;
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
const inspectPackageSkill = Effect.fn("inspectInstalledPackageSkill")(function* (
|
|
186
|
+
installed: InstalledPackageSkills,
|
|
187
|
+
name: string,
|
|
188
|
+
) {
|
|
189
|
+
const fs = yield* FileSystem.FileSystem;
|
|
190
|
+
const path = yield* Path.Path;
|
|
191
|
+
if (!isSkillName(name)) {
|
|
192
|
+
return yield* new PackageSkillSourceError({ message: `invalid package skill name: ${name}` });
|
|
193
|
+
}
|
|
194
|
+
const selector = `${installed.package}#${name}`;
|
|
195
|
+
const linkPath = path.join(installed.packageLink, "skills", name);
|
|
196
|
+
if ((yield* observeSymbolicLink(linkPath)).kind === "symlink") {
|
|
197
|
+
return yield* new PackageSkillSourceError({ message: `package skill contains a symlink: ${selector}` });
|
|
198
|
+
}
|
|
199
|
+
const skillRoot = yield* fs.realPath(linkPath).pipe(
|
|
200
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `package skill does not exist: ${selector}` })),
|
|
201
|
+
);
|
|
202
|
+
if (!isContained(path, installed.skillsRoot, skillRoot) ||
|
|
203
|
+
(yield* fs.stat(skillRoot)).type !== "Directory") {
|
|
204
|
+
return yield* new PackageSkillSourceError({ message: `package skill is not a contained directory: ${selector}` });
|
|
205
|
+
}
|
|
206
|
+
yield* rejectNestedSymlinks(skillRoot);
|
|
207
|
+
const document = yield* fs.readFileString(path.join(skillRoot, "SKILL.md")).pipe(
|
|
208
|
+
Effect.mapError(() => new PackageSkillSourceError({ message: `package skill is missing SKILL.md: ${selector}` })),
|
|
209
|
+
);
|
|
210
|
+
if (skillName(document) !== name) {
|
|
211
|
+
return yield* new PackageSkillSourceError({
|
|
212
|
+
message: `package skill SKILL.md name must match directory: ${selector}`,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const description = skillDescription(document);
|
|
216
|
+
if (description === undefined) {
|
|
217
|
+
return yield* new PackageSkillSourceError({
|
|
218
|
+
message: `package skill SKILL.md must declare a description: ${selector}`,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
selector,
|
|
223
|
+
name,
|
|
224
|
+
description,
|
|
225
|
+
package: installed.package,
|
|
226
|
+
version: installed.version,
|
|
227
|
+
path: skillRoot,
|
|
228
|
+
linkPath,
|
|
229
|
+
} satisfies DiscoveredPackageSkill;
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
/** Read direct project dependencies only; malformed packages are returned as diagnostics, never executed. */
|
|
233
|
+
export const discoverPackageSkills = Effect.fn("discoverInstalledPackageSkills")(function* (projectDir: string) {
|
|
234
|
+
const fs = yield* FileSystem.FileSystem;
|
|
235
|
+
const path = yield* Path.Path;
|
|
236
|
+
const candidates: Array<DiscoveredPackageSkill> = [];
|
|
237
|
+
const diagnostics: Array<PackageSkillDiagnostic> = [];
|
|
238
|
+
if (!(yield* fs.exists(path.join(projectDir, "package.json")))) {
|
|
239
|
+
return { candidates, diagnostics };
|
|
240
|
+
}
|
|
241
|
+
for (const packageName of yield* readDirectDependencyNames(projectDir)) {
|
|
242
|
+
if (!isTypeScriptPackageName(packageName)) {
|
|
243
|
+
diagnostics.push({ package: packageName, message: `invalid direct dependency package name: ${packageName}` });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const skillsLink = path.join(projectDir, "node_modules", ...packageName.split("/"), "skills");
|
|
247
|
+
if (!(yield* fs.exists(skillsLink))) continue;
|
|
248
|
+
const installed = yield* Effect.result(loadInstalledPackageSkills(projectDir, packageName));
|
|
249
|
+
if (Result.isFailure(installed)) {
|
|
250
|
+
diagnostics.push({ package: packageName, message: installed.failure.message });
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
for (const name of installed.success.names) {
|
|
254
|
+
const inspected = yield* Effect.result(inspectPackageSkill(installed.success, name));
|
|
255
|
+
if (Result.isSuccess(inspected)) candidates.push(inspected.success);
|
|
256
|
+
else diagnostics.push({ package: packageName, message: inspected.failure.message });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { candidates: candidates.sort((left, right) => left.selector.localeCompare(right.selector)), diagnostics };
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
/** Resolve one explicitly selected package skill. Unlike browsing, every malformed or missing part is an error. */
|
|
263
|
+
export const resolvePackageSkillSelector = Effect.fn("resolvePackageSkillSelector")(function* (projectDir: string, selector: string) {
|
|
264
|
+
const parsed = parseSkillSelector(selector);
|
|
265
|
+
if (parsed?.type !== "package") return yield* new PackageSkillSourceError({ message: `invalid package skill selector: ${selector}` });
|
|
266
|
+
const directDependencies = yield* readDirectDependencyNames(projectDir);
|
|
267
|
+
if (!directDependencies.includes(parsed.package)) return yield* new PackageSkillSourceError({ message: `package skill package is not a direct dependency: ${parsed.package}` });
|
|
268
|
+
return yield* inspectPackageSkill(
|
|
269
|
+
yield* loadInstalledPackageSkills(projectDir, parsed.package),
|
|
270
|
+
parsed.skill,
|
|
271
|
+
);
|
|
272
|
+
});
|
package/src/project-state.ts
CHANGED
|
@@ -2,6 +2,21 @@ import { Schema } from "effect";
|
|
|
2
2
|
|
|
3
3
|
import { DigestSchema } from "./path-digest.ts";
|
|
4
4
|
|
|
5
|
+
export const CatalogProvenanceSchema = Schema.Union([
|
|
6
|
+
Schema.Struct({
|
|
7
|
+
source: Schema.String,
|
|
8
|
+
repository: Schema.String,
|
|
9
|
+
resolved: Schema.String,
|
|
10
|
+
}),
|
|
11
|
+
Schema.Struct({
|
|
12
|
+
package: Schema.String,
|
|
13
|
+
version: Schema.String,
|
|
14
|
+
skill: Schema.String,
|
|
15
|
+
digest: DigestSchema,
|
|
16
|
+
}),
|
|
17
|
+
]);
|
|
18
|
+
export type CatalogProvenance = typeof CatalogProvenanceSchema.Type;
|
|
19
|
+
|
|
5
20
|
export const ManagedSkillOutputSchema = Schema.Struct({
|
|
6
21
|
resourceId: Schema.String,
|
|
7
22
|
path: Schema.String,
|
|
@@ -10,16 +25,26 @@ export const ManagedSkillOutputSchema = Schema.Struct({
|
|
|
10
25
|
mode: Schema.Literals(["copy", "symlink"]),
|
|
11
26
|
kind: Schema.Literals(["directory", "symlink"]),
|
|
12
27
|
digest: DigestSchema,
|
|
13
|
-
catalog: Schema.optional(
|
|
14
|
-
Schema.Struct({
|
|
15
|
-
source: Schema.String,
|
|
16
|
-
repository: Schema.String,
|
|
17
|
-
resolved: Schema.String,
|
|
18
|
-
}),
|
|
19
|
-
),
|
|
28
|
+
catalog: Schema.optional(CatalogProvenanceSchema),
|
|
20
29
|
});
|
|
21
30
|
export type ManagedSkillOutput = typeof ManagedSkillOutputSchema.Type;
|
|
22
31
|
|
|
32
|
+
export const ManagedInstructionOutputSchema = Schema.Struct({
|
|
33
|
+
resourceId: Schema.Literal("setup:claude-instructions"),
|
|
34
|
+
path: Schema.String,
|
|
35
|
+
sourcePath: Schema.String,
|
|
36
|
+
mode: Schema.Literal("symlink"),
|
|
37
|
+
kind: Schema.Literal("symlink"),
|
|
38
|
+
digest: DigestSchema,
|
|
39
|
+
});
|
|
40
|
+
export type ManagedInstructionOutput = typeof ManagedInstructionOutputSchema.Type;
|
|
41
|
+
|
|
42
|
+
export const ManagedOutputSchema = Schema.Union([
|
|
43
|
+
ManagedSkillOutputSchema,
|
|
44
|
+
ManagedInstructionOutputSchema,
|
|
45
|
+
]);
|
|
46
|
+
export type ManagedOutput = typeof ManagedOutputSchema.Type;
|
|
47
|
+
|
|
23
48
|
export const EffectTsgoLockSchema = Schema.Struct({
|
|
24
49
|
effectTsgoVersion: Schema.String,
|
|
25
50
|
typescriptPackage: Schema.String,
|
|
@@ -46,7 +71,7 @@ export const DevKitLockSchema = Schema.Struct({
|
|
|
46
71
|
effectTsgo: Schema.optional(EffectTsgoLockSchema),
|
|
47
72
|
}),
|
|
48
73
|
),
|
|
49
|
-
outputs: Schema.Array(
|
|
74
|
+
outputs: Schema.Array(ManagedOutputSchema),
|
|
50
75
|
});
|
|
51
76
|
export type DevKitLock = typeof DevKitLockSchema.Type;
|
|
52
77
|
|
package/src/skill-manager.ts
CHANGED
|
@@ -188,8 +188,14 @@ const selectedNames = (
|
|
|
188
188
|
return selected;
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
-
const
|
|
192
|
-
|
|
191
|
+
const displayValue = (value: string): string =>
|
|
192
|
+
[...value].map((character) => {
|
|
193
|
+
const code = character.charCodeAt(0);
|
|
194
|
+
return code <= 31 || (code >= 127 && code <= 159) ? " " : character;
|
|
195
|
+
}).join("").replace(/\s+/g, " ").trim();
|
|
196
|
+
|
|
197
|
+
const summary = (description: string, defaultDescription: string): string => {
|
|
198
|
+
const text = displayValue(description || defaultDescription);
|
|
193
199
|
const firstSentence = text.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim() ?? text;
|
|
194
200
|
return firstSentence.length > 96 ? `${firstSentence.slice(0, 93).trimEnd()}…` : firstSentence;
|
|
195
201
|
};
|
|
@@ -219,15 +225,16 @@ export const addSkills = Effect.fn("addManagedSkills")(function* (
|
|
|
219
225
|
options: ManagerOptions,
|
|
220
226
|
) {
|
|
221
227
|
const current = yield* readManifest(options, true);
|
|
222
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
223
|
-
const known = new Set([...catalog.skills.map((skill) => skill.
|
|
228
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
229
|
+
const known = new Set([...catalog.skills.map((skill) => skill.selector), ...Object.keys(catalog.families)]);
|
|
224
230
|
const unknown = names.filter((name) => !known.has(name));
|
|
225
231
|
if (unknown.length > 0) {
|
|
226
232
|
return yield* new SkillManagerError({
|
|
227
233
|
message: `unknown skill${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}. Try \`dev-kit search ${unknown[0]}\`.`,
|
|
228
234
|
});
|
|
229
235
|
}
|
|
230
|
-
|
|
236
|
+
const sourceFamilies = catalog.lock?.sources ?? [];
|
|
237
|
+
for (const source of sourceFamilies) {
|
|
231
238
|
if (!names.includes(source.id)) continue;
|
|
232
239
|
yield* printStatus(
|
|
233
240
|
"info",
|
|
@@ -255,7 +262,7 @@ export const removeSkills = Effect.fn("removeManagedSkills")(function* (
|
|
|
255
262
|
options: ManagerOptions,
|
|
256
263
|
) {
|
|
257
264
|
const current = yield* readManifest(options);
|
|
258
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
265
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
259
266
|
const before = selectedNames(
|
|
260
267
|
current.manifest.include,
|
|
261
268
|
current.manifest.exclude ?? [],
|
|
@@ -287,43 +294,63 @@ export const listSkills = Effect.fn("listManagedSkills")(function* (
|
|
|
287
294
|
) {
|
|
288
295
|
const fs = yield* FileSystem.FileSystem;
|
|
289
296
|
const paths = yield* resolvePaths(options);
|
|
290
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
297
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
|
|
291
298
|
const manifest = (yield* fs.exists(paths.manifestPath))
|
|
292
299
|
? (yield* readManifest(options)).manifest
|
|
293
300
|
: { include: [], exclude: [] };
|
|
294
301
|
const selected = selectedNames(manifest.include, manifest.exclude ?? [], catalog.families);
|
|
295
302
|
const query = options.query?.toLowerCase();
|
|
296
303
|
const visible = catalog.skills.filter((skill) =>
|
|
297
|
-
(options.all || selected.has(skill.
|
|
298
|
-
(!query || `${skill.
|
|
304
|
+
(options.all || selected.has(skill.selector)) &&
|
|
305
|
+
(!query || `${skill.selector} ${skill.description} ${skill.source}`.toLowerCase().includes(query)),
|
|
306
|
+
);
|
|
307
|
+
const catalogSelectors = new Set(catalog.skills.map((skill) => skill.selector));
|
|
308
|
+
const unavailable = [...selected].filter((selector) =>
|
|
309
|
+
!catalogSelectors.has(selector) && (!query || selector.toLowerCase().includes(query))
|
|
299
310
|
);
|
|
300
|
-
if (visible.length === 0) {
|
|
311
|
+
if (visible.length === 0 && unavailable.length === 0) {
|
|
301
312
|
yield* printStatus("info", query ? "No matching skills" : "No skills selected");
|
|
302
313
|
if (!query && !options.all) yield* printDetail("Browse with: dev-kit list --all");
|
|
303
314
|
return;
|
|
304
315
|
}
|
|
305
316
|
for (const skill of visible) {
|
|
306
|
-
const marker = selected.has(skill.
|
|
317
|
+
const marker = selected.has(skill.selector) ? "✓" : " ";
|
|
307
318
|
const origin = skill.bundled ? "built in" : skill.source;
|
|
308
|
-
const provenance = skill.
|
|
309
|
-
|
|
319
|
+
const provenance = skill.package
|
|
320
|
+
? ` [installed ${displayValue(skill.package.version)}]`
|
|
321
|
+
: skill.bundled ? "" : ` [${skill.source}]`;
|
|
322
|
+
yield* printLine(`${marker} ${skill.selector}${provenance} ${summary(skill.description, origin)}`);
|
|
323
|
+
}
|
|
324
|
+
for (const selector of unavailable) {
|
|
325
|
+
yield* printLine(`! ${selector} [unavailable] install or repair the selected direct dependency`);
|
|
310
326
|
}
|
|
311
327
|
yield* printLine();
|
|
312
|
-
yield* printLine(`${selected.size} selected · ${catalog.skills.length}
|
|
328
|
+
yield* printLine(`${selected.size} selected · ${catalog.skills.length} available`);
|
|
313
329
|
});
|
|
314
330
|
|
|
315
|
-
export const showSkill = Effect.fn("showCatalogSkill")(function* (
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
export const showSkill = Effect.fn("showCatalogSkill")(function* (
|
|
332
|
+
name: string,
|
|
333
|
+
options: ManagerOptions,
|
|
334
|
+
) {
|
|
335
|
+
const paths = yield* resolvePaths(options);
|
|
336
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
|
|
337
|
+
const skill = catalog.skills.find((candidate) => candidate.selector === name);
|
|
318
338
|
if (!skill) return yield* new SkillManagerError({ message: `unknown skill: ${name}` });
|
|
319
|
-
yield* printLine(skill.
|
|
320
|
-
if (skill.description) yield* printLine(skill.description);
|
|
339
|
+
yield* printLine(skill.selector);
|
|
340
|
+
if (skill.description) yield* printLine(displayValue(skill.description));
|
|
341
|
+
if (skill.package) {
|
|
342
|
+
yield* printLine(`Source: installed package`);
|
|
343
|
+
yield* printLine(`Package: ${skill.package.name}`);
|
|
344
|
+
yield* printLine(`Version: ${displayValue(skill.package.version)}`);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
321
347
|
yield* printLine(`Source: ${skill.bundled ? "dev-kit (built in)" : skill.source}`);
|
|
322
348
|
if (!skill.bundled) {
|
|
323
349
|
const source = catalog.lock?.sources.find((candidate) => candidate.id === skill.source);
|
|
324
350
|
if (source) {
|
|
325
351
|
yield* printLine(`Repository: ${source.repository}`);
|
|
326
352
|
yield* printLine(`Approved commit: ${source.resolved}`);
|
|
353
|
+
return;
|
|
327
354
|
}
|
|
328
355
|
}
|
|
329
356
|
});
|
|
@@ -345,22 +372,22 @@ export const chooseSkillsToAdd = Effect.fn("chooseSkillsToAdd")(function* (
|
|
|
345
372
|
return yield* new SkillManagerError({ message: "pass one or more skill names, or run this command in a terminal" });
|
|
346
373
|
}
|
|
347
374
|
const current = yield* readManifest(options, true);
|
|
348
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
375
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
349
376
|
const selected = selectedNames(
|
|
350
377
|
current.manifest.include,
|
|
351
378
|
current.manifest.exclude ?? [],
|
|
352
379
|
catalog.families,
|
|
353
380
|
);
|
|
354
|
-
const available = catalog.skills.filter((skill) => !selected.has(skill.
|
|
381
|
+
const available = catalog.skills.filter((skill) => !selected.has(skill.selector));
|
|
355
382
|
if (available.length === 0) {
|
|
356
|
-
yield* printStatus("success", "All
|
|
383
|
+
yield* printStatus("success", "All available skills are selected");
|
|
357
384
|
return;
|
|
358
385
|
}
|
|
359
386
|
const names = yield* Prompt.multiSelect({
|
|
360
387
|
message: "Choose skills to add",
|
|
361
388
|
choices: available.map((skill) => ({
|
|
362
|
-
title: skill.
|
|
363
|
-
value: skill.
|
|
389
|
+
title: skill.selector,
|
|
390
|
+
value: skill.selector,
|
|
364
391
|
description: summary(skill.description, skill.source),
|
|
365
392
|
})),
|
|
366
393
|
min: 1,
|
|
@@ -375,7 +402,7 @@ export const chooseSkillsToRemove = Effect.fn("chooseSkillsToRemove")(function*
|
|
|
375
402
|
return yield* new SkillManagerError({ message: "pass one or more skill names, or run this command in a terminal" });
|
|
376
403
|
}
|
|
377
404
|
const current = yield* readManifest(options);
|
|
378
|
-
const catalog = yield* loadSkillCatalog(yield* packageRoot());
|
|
405
|
+
const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
|
|
379
406
|
const selected = selectedNames(
|
|
380
407
|
current.manifest.include,
|
|
381
408
|
current.manifest.exclude ?? [],
|
|
@@ -387,11 +414,20 @@ export const chooseSkillsToRemove = Effect.fn("chooseSkillsToRemove")(function*
|
|
|
387
414
|
}
|
|
388
415
|
const names = yield* Prompt.multiSelect({
|
|
389
416
|
message: "Choose skills to remove",
|
|
390
|
-
choices:
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
417
|
+
choices: [
|
|
418
|
+
...catalog.skills.filter((skill) => selected.has(skill.selector)).map((skill) => ({
|
|
419
|
+
title: skill.selector,
|
|
420
|
+
value: skill.selector,
|
|
421
|
+
description: summary(skill.description, skill.source),
|
|
422
|
+
})),
|
|
423
|
+
...[...selected]
|
|
424
|
+
.filter((selector) => !catalog.skills.some((skill) => skill.selector === selector))
|
|
425
|
+
.map((selector) => ({
|
|
426
|
+
title: selector,
|
|
427
|
+
value: selector,
|
|
428
|
+
description: "Selected but currently unavailable",
|
|
429
|
+
})),
|
|
430
|
+
],
|
|
395
431
|
min: 1,
|
|
396
432
|
});
|
|
397
433
|
yield* removeSkills(names, options);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { isTypeScriptPackageName } from "./typescript-package-name.ts";
|
|
2
|
+
|
|
3
|
+
/** An immediate Agent Skill directory name. */
|
|
4
|
+
export const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
5
|
+
|
|
6
|
+
const PACKAGE_SKILL_SELECTOR_PATTERN = /^(?<package>[^#]+)#(?<skill>[^#]+)$/;
|
|
7
|
+
|
|
8
|
+
type StaticSkillSelector = {
|
|
9
|
+
readonly type: "static";
|
|
10
|
+
readonly name: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type PackageSkillSelector = {
|
|
14
|
+
readonly type: "package";
|
|
15
|
+
readonly package: string;
|
|
16
|
+
readonly skill: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type SkillSelector = StaticSkillSelector | PackageSkillSelector;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The JSON-schema-compatible pattern for static selectors and exact package
|
|
23
|
+
* selectors. Package names intentionally use the same rules as package-backed
|
|
24
|
+
* skill sources.
|
|
25
|
+
*/
|
|
26
|
+
export const SKILL_SELECTOR_PATTERN =
|
|
27
|
+
/^(?:[a-z0-9]+(?:-[a-z0-9]+)*|(?:[a-z0-9][a-z0-9._-]*|@[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*)#[a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
28
|
+
|
|
29
|
+
export const isSkillName = (value: string): boolean => SKILL_NAME_PATTERN.test(value);
|
|
30
|
+
|
|
31
|
+
/** Parse an exact, canonical manifest skill selector. */
|
|
32
|
+
export const parseSkillSelector = (value: string): SkillSelector | undefined => {
|
|
33
|
+
if (isSkillName(value)) return { type: "static", name: value };
|
|
34
|
+
|
|
35
|
+
const match = PACKAGE_SKILL_SELECTOR_PATTERN.exec(value);
|
|
36
|
+
const packageName = match?.groups?.package;
|
|
37
|
+
const skill = match?.groups?.skill;
|
|
38
|
+
if (packageName === undefined || skill === undefined ||
|
|
39
|
+
!isTypeScriptPackageName(packageName) || !isSkillName(skill)) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
return { type: "package", package: packageName, skill };
|
|
43
|
+
};
|