@danieljvdm/dev-kit 0.4.0 → 0.6.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.
@@ -135,6 +135,13 @@ export const digestText = Effect.fn("digestText")(function* (value: string) {
135
135
  return yield* digestFrames(["text-v1", value]);
136
136
  });
137
137
 
138
+ export const digestFileContent = Effect.fn("digestFileContent")(function* (
139
+ value: string,
140
+ mode = 0o644,
141
+ ) {
142
+ return yield* digestFrames(["file-v1", String(mode), value]);
143
+ });
144
+
138
145
  export const digestSymlinkTarget = Effect.fn("digestSymlinkTarget")(function* (target: string) {
139
146
  return yield* digestFrames(["symlink-v1", target]);
140
147
  });
@@ -0,0 +1,34 @@
1
+ import { Effect, FileSystem, Path, Schema } from "effect";
2
+
3
+ export class ProjectPackageError extends Schema.TaggedErrorClass<ProjectPackageError>()(
4
+ "ProjectPackageError",
5
+ { message: Schema.String },
6
+ ) {}
7
+
8
+ const ProjectPackageSchema = Schema.fromJsonString(Schema.Struct({
9
+ dependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
10
+ devDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
11
+ optionalDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
12
+ peerDependencies: Schema.optional(Schema.Record(Schema.String, Schema.String)),
13
+ }));
14
+
15
+ export const readDirectDependencyNames = Effect.fn("readDirectDependencyNames")(function* (
16
+ projectDir: string,
17
+ ) {
18
+ const fs = yield* FileSystem.FileSystem;
19
+ const path = yield* Path.Path;
20
+ const manifestPath = path.join(projectDir, "package.json");
21
+ if (!(yield* fs.exists(manifestPath))) return [];
22
+ const manifest = yield* fs.readFileString(manifestPath).pipe(
23
+ Effect.flatMap(Schema.decodeUnknownEffect(ProjectPackageSchema)),
24
+ Effect.mapError(() => new ProjectPackageError({
25
+ message: `invalid project package.json: ${manifestPath}`,
26
+ })),
27
+ );
28
+ return [...new Set([
29
+ ...Object.keys(manifest.dependencies ?? {}),
30
+ ...Object.keys(manifest.devDependencies ?? {}),
31
+ ...Object.keys(manifest.optionalDependencies ?? {}),
32
+ ...Object.keys(manifest.peerDependencies ?? {}),
33
+ ])].sort();
34
+ });
@@ -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,42 @@ 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 ManagedAgentInstructionsOutputSchema = Schema.Struct({
33
+ resourceId: Schema.Literal("setup:agent-instructions"),
34
+ path: Schema.String,
35
+ sourcePath: Schema.String,
36
+ mode: Schema.Literal("copy"),
37
+ kind: Schema.Literal("file"),
38
+ digest: DigestSchema,
39
+ });
40
+ export type ManagedAgentInstructionsOutput = typeof ManagedAgentInstructionsOutputSchema.Type;
41
+
42
+ export const ManagedClaudeInstructionsOutputSchema = Schema.Struct({
43
+ resourceId: Schema.Literal("setup:claude-instructions"),
44
+ path: Schema.String,
45
+ sourcePath: Schema.String,
46
+ mode: Schema.Literal("symlink"),
47
+ kind: Schema.Literal("symlink"),
48
+ digest: DigestSchema,
49
+ });
50
+ export type ManagedClaudeInstructionsOutput = typeof ManagedClaudeInstructionsOutputSchema.Type;
51
+
52
+ export const ManagedInstructionOutputSchema = Schema.Union([
53
+ ManagedAgentInstructionsOutputSchema,
54
+ ManagedClaudeInstructionsOutputSchema,
55
+ ]);
56
+ export type ManagedInstructionOutput = typeof ManagedInstructionOutputSchema.Type;
57
+
58
+ export const ManagedOutputSchema = Schema.Union([
59
+ ManagedSkillOutputSchema,
60
+ ManagedInstructionOutputSchema,
61
+ ]);
62
+ export type ManagedOutput = typeof ManagedOutputSchema.Type;
63
+
23
64
  export const EffectTsgoLockSchema = Schema.Struct({
24
65
  effectTsgoVersion: Schema.String,
25
66
  typescriptPackage: Schema.String,
@@ -46,7 +87,7 @@ export const DevKitLockSchema = Schema.Struct({
46
87
  effectTsgo: Schema.optional(EffectTsgoLockSchema),
47
88
  }),
48
89
  ),
49
- outputs: Schema.Array(ManagedSkillOutputSchema),
90
+ outputs: Schema.Array(ManagedOutputSchema),
50
91
  });
51
92
  export type DevKitLock = typeof DevKitLockSchema.Type;
52
93
 
@@ -54,7 +95,7 @@ export const OwnershipReceiptSchema = Schema.Struct({
54
95
  resourceId: Schema.String,
55
96
  path: Schema.String,
56
97
  mode: Schema.Literals(["copy", "symlink"]),
57
- kind: Schema.Literals(["directory", "symlink"]),
98
+ kind: Schema.Literals(["file", "directory", "symlink"]),
58
99
  digest: DigestSchema,
59
100
  });
60
101
  export type OwnershipReceipt = typeof OwnershipReceiptSchema.Type;
@@ -188,8 +188,14 @@ const selectedNames = (
188
188
  return selected;
189
189
  };
190
190
 
191
- const summary = (description: string, fallback: string): string => {
192
- const text = description || fallback;
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.name), ...Object.keys(catalog.families)]);
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
- for (const source of catalog.lock?.sources ?? []) {
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.name)) &&
298
- (!query || `${skill.name} ${skill.description} ${skill.source}`.toLowerCase().includes(query)),
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.name) ? "✓" : " ";
317
+ const marker = selected.has(skill.selector) ? "✓" : " ";
307
318
  const origin = skill.bundled ? "built in" : skill.source;
308
- const provenance = skill.bundled ? "" : ` [${skill.source}]`;
309
- yield* printLine(`${marker} ${skill.name}${provenance} ${summary(skill.description, origin)}`);
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} approved`);
328
+ yield* printLine(`${selected.size} selected · ${catalog.skills.length} available`);
313
329
  });
314
330
 
315
- export const showSkill = Effect.fn("showCatalogSkill")(function* (name: string) {
316
- const catalog = yield* loadSkillCatalog(yield* packageRoot());
317
- const skill = catalog.skills.find((candidate) => candidate.name === name);
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.name);
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.name));
381
+ const available = catalog.skills.filter((skill) => !selected.has(skill.selector));
355
382
  if (available.length === 0) {
356
- yield* printStatus("success", "All approved skills are selected");
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.name,
363
- value: skill.name,
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: catalog.skills.filter((skill) => selected.has(skill.name)).map((skill) => ({
391
- title: skill.name,
392
- value: skill.name,
393
- description: summary(skill.description, skill.source),
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
+ };