@danieljvdm/dev-kit 0.5.0 → 0.7.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.
Files changed (56) hide show
  1. package/README.md +161 -68
  2. package/dev-kit.example.jsonc +8 -3
  3. package/package.json +19 -15
  4. package/schema/dev-kit.schema.json +52 -0
  5. package/skill-sources.jsonc +8 -12
  6. package/skill-sources.lock.json +3 -9
  7. package/skills/dev-kit/SKILL.md +74 -24
  8. package/skills/effect-atom-data-fetching/SKILL.md +40 -0
  9. package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
  10. package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
  11. package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
  12. package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
  13. package/skills/effect-atom-data-fetching/references/testing.md +63 -0
  14. package/skills/effect-ts/agents/openai.yaml +0 -1
  15. package/skills/effect-ts/references/audit-services.md +11 -11
  16. package/skills/effect-ts/references/guide-effect.md +56 -69
  17. package/skills/effect-ts/references/guide-error-handling.md +64 -73
  18. package/skills/effect-ts/references/guide-layers.md +187 -215
  19. package/skills/effect-ts/references/guide-observability.md +91 -116
  20. package/skills/effect-ts/references/guide-retries.md +32 -44
  21. package/skills/effect-ts/references/guide-schedule.md +26 -40
  22. package/skills/effect-ts/references/guide-schema.md +50 -57
  23. package/skills/effect-ts/references/guide-sql.md +47 -50
  24. package/skills/effect-ts/references/guide-testing.md +96 -98
  25. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
  26. package/skills/effect-ts/references/version-and-source.md +0 -1
  27. package/src/bin/dev-kit.ts +61 -28
  28. package/src/catalog-manager.ts +86 -34
  29. package/src/catalog.ts +72 -34
  30. package/src/cli-ui.ts +20 -16
  31. package/src/effect-source.ts +49 -19
  32. package/src/effect-tsgo.ts +66 -35
  33. package/src/gitignore.ts +19 -6
  34. package/src/index.ts +12 -0
  35. package/src/manifest.ts +51 -3
  36. package/src/node-symbolic-link.ts +3 -0
  37. package/src/oxlint-plugin-effect.js +3 -0
  38. package/src/oxlint-plugin-style.d.ts +8 -0
  39. package/src/oxlint-plugin-style.js +8 -0
  40. package/src/oxlint.js +14 -0
  41. package/src/oxlint.ts +14 -0
  42. package/src/package-skill-source.ts +190 -75
  43. package/src/path-digest.ts +37 -10
  44. package/src/project-package.ts +59 -0
  45. package/src/project-process-lock.ts +19 -12
  46. package/src/project-state.ts +29 -2
  47. package/src/skill-manager.ts +134 -55
  48. package/src/skill-selector.ts +8 -2
  49. package/src/source-manifest.ts +2 -6
  50. package/src/sync.ts +491 -121
  51. package/src/vendor.ts +112 -42
  52. package/src/vite-plus-hooks.ts +174 -0
  53. package/src/vite-plus-quality.ts +49 -0
  54. package/templates/AGENTS.md +9 -0
  55. package/templates/vite-plus/github-actions-check.yml +44 -0
  56. package/templates/vite-plus/vite.config.ts +22 -0
@@ -1,18 +1,17 @@
1
- import { applyEdits, modify, parse as parseJsonc, type ParseError } from "jsonc-parser";
2
1
  import { Effect, FileSystem, Path, Schema } from "effect";
3
2
  import { Prompt } from "effect/unstable/cli";
3
+ import { applyEdits, modify, parse as parseJsonc, type ParseError } from "jsonc-parser";
4
4
 
5
5
  import { loadSkillCatalog } from "./catalog.ts";
6
6
  import { isInteractiveTerminal, printDetail, printLine, printStatus } from "./cli-ui.ts";
7
+ import { patchProjectGitignore } from "./gitignore.ts";
7
8
  import { DevKitManifestSchema } from "./manifest.ts";
8
9
  import { observeSymbolicLink } from "./node-symbolic-link.ts";
9
10
  import { runProjectSkillPlan } from "./sync.ts";
10
- import { patchProjectGitignore } from "./gitignore.ts";
11
11
 
12
- class SkillManagerError extends Schema.TaggedErrorClass<SkillManagerError>()(
13
- "SkillManagerError",
14
- { message: Schema.String },
15
- ) {}
12
+ class SkillManagerError extends Schema.TaggedErrorClass<SkillManagerError>()("SkillManagerError", {
13
+ message: Schema.String,
14
+ }) {}
16
15
 
17
16
  type ManagerOptions = {
18
17
  readonly projectDir?: string;
@@ -22,16 +21,16 @@ type ManagerOptions = {
22
21
 
23
22
  const packageRoot = Effect.fn("skillManagerPackageRoot")(function* () {
24
23
  const path = yield* Path.Path;
24
+
25
25
  return path.resolve(path.dirname(yield* path.fromFileUrl(new URL(import.meta.url))), "..");
26
26
  });
27
27
 
28
- const resolvePaths = Effect.fn("resolveSkillManagerPaths")(function* (
29
- options: ManagerOptions,
30
- ) {
28
+ const resolvePaths = Effect.fn("resolveSkillManagerPaths")(function* (options: ManagerOptions) {
31
29
  const fs = yield* FileSystem.FileSystem;
32
30
  const path = yield* Path.Path;
33
31
  const projectDir = path.resolve(options.projectDir ?? ".");
34
32
  const candidate = options.manifestPath ?? "dev-kit.jsonc";
33
+
35
34
  if (candidate.length === 0 || path.isAbsolute(candidate)) {
36
35
  return yield* new SkillManagerError({
37
36
  message: "--manifest must be a non-empty project-relative path",
@@ -39,16 +38,14 @@ const resolvePaths = Effect.fn("resolveSkillManagerPaths")(function* (
39
38
  }
40
39
  const manifestPath = path.resolve(projectDir, candidate);
41
40
  const relative = path.relative(projectDir, manifestPath);
42
- if (
43
- relative === ".." ||
44
- relative.startsWith(`..${path.sep}`) ||
45
- path.isAbsolute(relative)
46
- ) {
41
+
42
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
47
43
  return yield* new SkillManagerError({
48
44
  message: "--manifest must resolve inside the project",
49
45
  });
50
46
  }
51
47
  let ancestor = projectDir;
48
+
52
49
  for (const segment of relative.split(path.sep).slice(0, -1)) {
53
50
  ancestor = path.join(ancestor, segment);
54
51
  if ((yield* observeSymbolicLink(ancestor)).kind === "symlink") {
@@ -58,12 +55,14 @@ const resolvePaths = Effect.fn("resolveSkillManagerPaths")(function* (
58
55
  }
59
56
  }
60
57
  const destination = yield* observeSymbolicLink(manifestPath);
58
+
61
59
  if (destination.kind === "symlink") {
62
60
  return yield* new SkillManagerError({ message: `manifest is a symlink: ${relative}` });
63
61
  }
64
62
  if (destination.kind === "not-symlink" && (yield* fs.stat(manifestPath)).type !== "File") {
65
63
  return yield* new SkillManagerError({ message: `manifest is not a regular file: ${relative}` });
66
64
  }
65
+
67
66
  return {
68
67
  projectDir,
69
68
  manifestPath,
@@ -73,31 +72,45 @@ const resolvePaths = Effect.fn("resolveSkillManagerPaths")(function* (
73
72
  const renderDefaultManifest = (projectDir: string, manifestPath: string, path: Path.Path) => {
74
73
  const rawSchemaPath = path.relative(
75
74
  path.dirname(manifestPath),
76
- path.join(projectDir, "node_modules", "@danieljvdm", "dev-kit", "schema", "dev-kit.schema.json"),
75
+ path.join(
76
+ projectDir,
77
+ "node_modules",
78
+ "@danieljvdm",
79
+ "dev-kit",
80
+ "schema",
81
+ "dev-kit.schema.json",
82
+ ),
77
83
  );
78
- const portableSchemaPath = path.sep === "/"
79
- ? rawSchemaPath
80
- : rawSchemaPath.split(path.sep).join("/");
84
+ const portableSchemaPath =
85
+ path.sep === "/" ? rawSchemaPath : rawSchemaPath.split(path.sep).join("/");
81
86
  const schemaPath = portableSchemaPath.startsWith(".")
82
87
  ? portableSchemaPath
83
88
  : `./${portableSchemaPath}`;
84
- return `${JSON.stringify({
85
- $schema: schemaPath,
86
- include: [],
87
- targets: { agents: { enabled: true, mode: "copy" } },
88
- }, null, 2)}\n`;
89
+
90
+ return `${JSON.stringify(
91
+ {
92
+ $schema: schemaPath,
93
+ include: [],
94
+ targets: { agents: { enabled: true, mode: "copy" } },
95
+ },
96
+ null,
97
+ 2,
98
+ )}\n`;
89
99
  };
90
100
 
91
- const createDefaultManifest = Effect.fn("createDefaultSkillManifest")(function* (
92
- paths: { readonly projectDir: string; readonly manifestPath: string },
93
- ) {
101
+ const createDefaultManifest = Effect.fn("createDefaultSkillManifest")(function* (paths: {
102
+ readonly projectDir: string;
103
+ readonly manifestPath: string;
104
+ }) {
94
105
  const fs = yield* FileSystem.FileSystem;
95
106
  const path = yield* Path.Path;
107
+
96
108
  yield* fs.makeDirectory(path.dirname(paths.manifestPath), { recursive: true });
97
109
  const staged = yield* fs.makeTempFileScoped({
98
110
  directory: path.dirname(paths.manifestPath),
99
111
  prefix: ".dev-kit-init-",
100
112
  });
113
+
101
114
  yield* fs.writeFileString(
102
115
  staged,
103
116
  renderDefaultManifest(paths.projectDir, paths.manifestPath, path),
@@ -112,6 +125,7 @@ const readManifest = Effect.fn("readManagedSkillManifest")(function* (
112
125
  ) {
113
126
  const fs = yield* FileSystem.FileSystem;
114
127
  const paths = yield* resolvePaths(options);
128
+
115
129
  if (!(yield* fs.exists(paths.manifestPath))) {
116
130
  if (!create) {
117
131
  return yield* new SkillManagerError({
@@ -123,12 +137,14 @@ const readManifest = Effect.fn("readManagedSkillManifest")(function* (
123
137
  const raw = yield* fs.readFileString(paths.manifestPath);
124
138
  const errors: Array<ParseError> = [];
125
139
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
140
+
126
141
  if (errors.length > 0) {
127
142
  return yield* new SkillManagerError({ message: `could not parse ${paths.manifestPath}` });
128
143
  }
129
144
  const manifest = yield* Schema.decodeUnknownEffect(DevKitManifestSchema)(parsed).pipe(
130
145
  Effect.mapError((error) => new SkillManagerError({ message: error.message })),
131
146
  );
147
+
132
148
  return { ...paths, manifest, raw };
133
149
  });
134
150
 
@@ -141,33 +157,46 @@ const writeArray = Effect.fn("writeManifestArray")(function* (
141
157
  const fs = yield* FileSystem.FileSystem;
142
158
  const parsed = parseJsonc(raw) as Record<string, unknown>;
143
159
  const current = Array.isArray(parsed[property])
144
- ? (parsed[property] as Array<unknown>).filter((value): value is string => typeof value === "string")
160
+ ? (parsed[property] as Array<unknown>).filter(
161
+ (value): value is string => typeof value === "string",
162
+ )
145
163
  : undefined;
164
+
146
165
  if (current === undefined) {
147
166
  if (values.length === 0) return;
148
167
  const edits = modify(raw, [property], [...values], {
149
168
  formattingOptions: { insertSpaces: true, tabSize: 2 },
150
169
  });
170
+
151
171
  yield* fs.writeFileString(manifestPath, applyEdits(raw, edits));
172
+
152
173
  return;
153
174
  }
154
175
  let next = raw;
155
176
  const retained = [...current];
177
+
156
178
  for (let index = current.length - 1; index >= 0; index -= 1) {
157
179
  const currentValue = current[index];
180
+
158
181
  if (currentValue !== undefined && !values.includes(currentValue)) {
159
- next = applyEdits(next, modify(next, [property, index], undefined, {
160
- formattingOptions: { insertSpaces: true, tabSize: 2 },
161
- }));
182
+ next = applyEdits(
183
+ next,
184
+ modify(next, [property, index], undefined, {
185
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
186
+ }),
187
+ );
162
188
  retained.splice(index, 1);
163
189
  }
164
190
  }
165
191
  for (const value of values) {
166
192
  if (retained.includes(value)) continue;
167
- next = applyEdits(next, modify(next, [property, retained.length], value, {
168
- formattingOptions: { insertSpaces: true, tabSize: 2 },
169
- isArrayInsertion: true,
170
- }));
193
+ next = applyEdits(
194
+ next,
195
+ modify(next, [property, retained.length], value, {
196
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
197
+ isArrayInsertion: true,
198
+ }),
199
+ );
171
200
  retained.push(value);
172
201
  }
173
202
  if (next !== raw) yield* fs.writeFileString(manifestPath, next);
@@ -179,24 +208,32 @@ const selectedNames = (
179
208
  families: Readonly<Record<string, ReadonlyArray<string>>>,
180
209
  ) => {
181
210
  const selected = new Set<string>();
211
+
182
212
  for (const name of include) {
183
213
  for (const skill of families[name] ?? [name]) selected.add(skill);
184
214
  }
185
215
  for (const name of exclude) {
186
216
  for (const skill of families[name] ?? [name]) selected.delete(skill);
187
217
  }
218
+
188
219
  return selected;
189
220
  };
190
221
 
191
222
  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();
223
+ [...value]
224
+ .map((character) => {
225
+ const code = character.charCodeAt(0);
226
+
227
+ return code <= 31 || (code >= 127 && code <= 159) ? " " : character;
228
+ })
229
+ .join("")
230
+ .replace(/\s+/g, " ")
231
+ .trim();
196
232
 
197
233
  const summary = (description: string, defaultDescription: string): string => {
198
234
  const text = displayValue(description || defaultDescription);
199
235
  const firstSentence = text.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim() ?? text;
236
+
200
237
  return firstSentence.length > 96 ? `${firstSentence.slice(0, 93).trimEnd()}…` : firstSentence;
201
238
  };
202
239
 
@@ -211,8 +248,10 @@ const applyIfRequested = (options: ManagerOptions) =>
211
248
  export const initProject = Effect.fn("initDevKitProject")(function* (options: ManagerOptions) {
212
249
  const fs = yield* FileSystem.FileSystem;
213
250
  const paths = yield* resolvePaths(options);
251
+
214
252
  if (yield* fs.exists(paths.manifestPath)) {
215
253
  yield* printStatus("info", "Already initialized", paths.manifestPath);
254
+
216
255
  return;
217
256
  }
218
257
  yield* createDefaultManifest(paths);
@@ -226,14 +265,19 @@ export const addSkills = Effect.fn("addManagedSkills")(function* (
226
265
  ) {
227
266
  const current = yield* readManifest(options, true);
228
267
  const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
229
- const known = new Set([...catalog.skills.map((skill) => skill.selector), ...Object.keys(catalog.families)]);
268
+ const known = new Set([
269
+ ...catalog.skills.map((skill) => skill.selector),
270
+ ...Object.keys(catalog.families),
271
+ ]);
230
272
  const unknown = names.filter((name) => !known.has(name));
273
+
231
274
  if (unknown.length > 0) {
232
275
  return yield* new SkillManagerError({
233
276
  message: `unknown skill${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}. Try \`dev-kit search ${unknown[0]}\`.`,
234
277
  });
235
278
  }
236
279
  const sourceFamilies = catalog.lock?.sources ?? [];
280
+
237
281
  for (const source of sourceFamilies) {
238
282
  if (!names.includes(source.id)) continue;
239
283
  yield* printStatus(
@@ -246,8 +290,10 @@ export const addSkills = Effect.fn("addManagedSkills")(function* (
246
290
  }
247
291
  const include = [...new Set([...current.manifest.include, ...names])];
248
292
  const exclude = (current.manifest.exclude ?? []).filter((name) => !names.includes(name));
293
+
249
294
  yield* writeArray(current.manifestPath, current.raw, "include", include);
250
295
  const reread = yield* FileSystem.FileSystem;
296
+
251
297
  yield* writeArray(
252
298
  current.manifestPath,
253
299
  yield* reread.readFileString(current.manifestPath),
@@ -268,18 +314,23 @@ export const removeSkills = Effect.fn("removeManagedSkills")(function* (
268
314
  current.manifest.exclude ?? [],
269
315
  catalog.families,
270
316
  );
271
- const absent = names.filter((name) => !before.has(name) && !current.manifest.include.includes(name));
317
+ const absent = names.filter(
318
+ (name) => !before.has(name) && !current.manifest.include.includes(name),
319
+ );
320
+
272
321
  if (absent.length > 0) {
273
322
  return yield* new SkillManagerError({ message: `not selected: ${absent.join(", ")}` });
274
323
  }
275
324
  const include = current.manifest.include.filter((name) => !names.includes(name));
276
325
  const excluded = new Set(current.manifest.exclude ?? []);
326
+
277
327
  for (const name of names) {
278
328
  if (before.has(name) && !current.manifest.include.includes(name)) excluded.add(name);
279
329
  else excluded.delete(name);
280
330
  }
281
331
  yield* writeArray(current.manifestPath, current.raw, "include", include);
282
332
  const fs = yield* FileSystem.FileSystem;
333
+
283
334
  yield* writeArray(
284
335
  current.manifestPath,
285
336
  yield* fs.readFileString(current.manifestPath),
@@ -300,17 +351,22 @@ export const listSkills = Effect.fn("listManagedSkills")(function* (
300
351
  : { include: [], exclude: [] };
301
352
  const selected = selectedNames(manifest.include, manifest.exclude ?? [], catalog.families);
302
353
  const query = options.query?.toLowerCase();
303
- const visible = catalog.skills.filter((skill) =>
304
- (options.all || selected.has(skill.selector)) &&
305
- (!query || `${skill.selector} ${skill.description} ${skill.source}`.toLowerCase().includes(query)),
354
+ const visible = catalog.skills.filter(
355
+ (skill) =>
356
+ (options.all || selected.has(skill.selector)) &&
357
+ (!query ||
358
+ `${skill.selector} ${skill.description} ${skill.source}`.toLowerCase().includes(query)),
306
359
  );
307
360
  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))
361
+ const unavailable = [...selected].filter(
362
+ (selector) =>
363
+ !catalogSelectors.has(selector) && (!query || selector.toLowerCase().includes(query)),
310
364
  );
365
+
311
366
  if (visible.length === 0 && unavailable.length === 0) {
312
367
  yield* printStatus("info", query ? "No matching skills" : "No skills selected");
313
368
  if (!query && !options.all) yield* printDetail("Browse with: dev-kit list --all");
369
+
314
370
  return;
315
371
  }
316
372
  for (const skill of visible) {
@@ -318,11 +374,18 @@ export const listSkills = Effect.fn("listManagedSkills")(function* (
318
374
  const origin = skill.bundled ? "built in" : skill.source;
319
375
  const provenance = skill.package
320
376
  ? ` [installed ${displayValue(skill.package.version)}]`
321
- : skill.bundled ? "" : ` [${skill.source}]`;
322
- yield* printLine(`${marker} ${skill.selector}${provenance} ${summary(skill.description, origin)}`);
377
+ : skill.bundled
378
+ ? ""
379
+ : ` [${skill.source}]`;
380
+
381
+ yield* printLine(
382
+ `${marker} ${skill.selector}${provenance} ${summary(skill.description, origin)}`,
383
+ );
323
384
  }
324
385
  for (const selector of unavailable) {
325
- yield* printLine(`! ${selector} [unavailable] install or repair the selected direct dependency`);
386
+ yield* printLine(
387
+ `! ${selector} [unavailable] install or repair the selected direct dependency`,
388
+ );
326
389
  }
327
390
  yield* printLine();
328
391
  yield* printLine(`${selected.size} selected · ${catalog.skills.length} available`);
@@ -335,6 +398,7 @@ export const showSkill = Effect.fn("showCatalogSkill")(function* (
335
398
  const paths = yield* resolvePaths(options);
336
399
  const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
337
400
  const skill = catalog.skills.find((candidate) => candidate.selector === name);
401
+
338
402
  if (!skill) return yield* new SkillManagerError({ message: `unknown skill: ${name}` });
339
403
  yield* printLine(skill.selector);
340
404
  if (skill.description) yield* printLine(displayValue(skill.description));
@@ -342,14 +406,17 @@ export const showSkill = Effect.fn("showCatalogSkill")(function* (
342
406
  yield* printLine(`Source: installed package`);
343
407
  yield* printLine(`Package: ${skill.package.name}`);
344
408
  yield* printLine(`Version: ${displayValue(skill.package.version)}`);
409
+
345
410
  return;
346
411
  }
347
412
  yield* printLine(`Source: ${skill.bundled ? "dev-kit (built in)" : skill.source}`);
348
413
  if (!skill.bundled) {
349
414
  const source = catalog.lock?.sources.find((candidate) => candidate.id === skill.source);
415
+
350
416
  if (source) {
351
417
  yield* printLine(`Repository: ${source.repository}`);
352
418
  yield* printLine(`Approved commit: ${source.resolved}`);
419
+
353
420
  return;
354
421
  }
355
422
  }
@@ -369,7 +436,9 @@ export const chooseSkillsToAdd = Effect.fn("chooseSkillsToAdd")(function* (
369
436
  options: ManagerOptions,
370
437
  ) {
371
438
  if (!(yield* isInteractiveTerminal)) {
372
- return yield* new SkillManagerError({ message: "pass one or more skill names, or run this command in a terminal" });
439
+ return yield* new SkillManagerError({
440
+ message: "pass one or more skill names, or run this command in a terminal",
441
+ });
373
442
  }
374
443
  const current = yield* readManifest(options, true);
375
444
  const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
@@ -379,8 +448,10 @@ export const chooseSkillsToAdd = Effect.fn("chooseSkillsToAdd")(function* (
379
448
  catalog.families,
380
449
  );
381
450
  const available = catalog.skills.filter((skill) => !selected.has(skill.selector));
451
+
382
452
  if (available.length === 0) {
383
453
  yield* printStatus("success", "All available skills are selected");
454
+
384
455
  return;
385
456
  }
386
457
  const names = yield* Prompt.multiSelect({
@@ -392,6 +463,7 @@ export const chooseSkillsToAdd = Effect.fn("chooseSkillsToAdd")(function* (
392
463
  })),
393
464
  min: 1,
394
465
  });
466
+
395
467
  yield* addSkills(names, options);
396
468
  });
397
469
 
@@ -399,7 +471,9 @@ export const chooseSkillsToRemove = Effect.fn("chooseSkillsToRemove")(function*
399
471
  options: ManagerOptions,
400
472
  ) {
401
473
  if (!(yield* isInteractiveTerminal)) {
402
- return yield* new SkillManagerError({ message: "pass one or more skill names, or run this command in a terminal" });
474
+ return yield* new SkillManagerError({
475
+ message: "pass one or more skill names, or run this command in a terminal",
476
+ });
403
477
  }
404
478
  const current = yield* readManifest(options);
405
479
  const catalog = yield* loadSkillCatalog(yield* packageRoot(), current.projectDir);
@@ -408,18 +482,22 @@ export const chooseSkillsToRemove = Effect.fn("chooseSkillsToRemove")(function*
408
482
  current.manifest.exclude ?? [],
409
483
  catalog.families,
410
484
  );
485
+
411
486
  if (selected.size === 0) {
412
487
  yield* printStatus("info", "No skills selected");
488
+
413
489
  return;
414
490
  }
415
491
  const names = yield* Prompt.multiSelect({
416
492
  message: "Choose skills to remove",
417
493
  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
- })),
494
+ ...catalog.skills
495
+ .filter((skill) => selected.has(skill.selector))
496
+ .map((skill) => ({
497
+ title: skill.selector,
498
+ value: skill.selector,
499
+ description: summary(skill.description, skill.source),
500
+ })),
423
501
  ...[...selected]
424
502
  .filter((selector) => !catalog.skills.some((skill) => skill.selector === selector))
425
503
  .map((selector) => ({
@@ -430,5 +508,6 @@ export const chooseSkillsToRemove = Effect.fn("chooseSkillsToRemove")(function*
430
508
  ],
431
509
  min: 1,
432
510
  });
511
+
433
512
  yield* removeSkills(names, options);
434
513
  });
@@ -35,9 +35,15 @@ export const parseSkillSelector = (value: string): SkillSelector | undefined =>
35
35
  const match = PACKAGE_SKILL_SELECTOR_PATTERN.exec(value);
36
36
  const packageName = match?.groups?.package;
37
37
  const skill = match?.groups?.skill;
38
- if (packageName === undefined || skill === undefined ||
39
- !isTypeScriptPackageName(packageName) || !isSkillName(skill)) {
38
+
39
+ if (
40
+ packageName === undefined ||
41
+ skill === undefined ||
42
+ !isTypeScriptPackageName(packageName) ||
43
+ !isSkillName(skill)
44
+ ) {
40
45
  return undefined;
41
46
  }
47
+
42
48
  return { type: "package", package: packageName, skill };
43
49
  };
@@ -31,12 +31,8 @@ export const LockedSkillSourceSchema = Schema.Struct({
31
31
  include: Schema.Array(Schema.String),
32
32
  exclude: Schema.optional(Schema.Array(Schema.String)),
33
33
  skills: Schema.Array(Schema.String),
34
- descriptions: Schema.optional(
35
- Schema.Record(Schema.String, Schema.String),
36
- ),
37
- digests: Schema.optional(
38
- Schema.Record(Schema.String, DigestSchema),
39
- ),
34
+ descriptions: Schema.optional(Schema.Record(Schema.String, Schema.String)),
35
+ digests: Schema.optional(Schema.Record(Schema.String, DigestSchema)),
40
36
  licensePath: Schema.optional(Schema.String),
41
37
  stripFrontmatter: Schema.optional(Schema.Array(Schema.String)),
42
38
  });