@danieljvdm/dev-kit 0.3.3 → 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 CHANGED
@@ -20,7 +20,8 @@ Install the published Dev Kit package:
20
20
  bun add -d @danieljvdm/dev-kit
21
21
  ```
22
22
 
23
- Initialize the project, browse the approved catalog, and add skills:
23
+ Initialize the project, browse available built-in, approved Git, and installed
24
+ package skills, then add the ones you want:
24
25
 
25
26
  ```bash
26
27
  bun x dev-kit init
@@ -107,7 +108,7 @@ the root package itself.
107
108
  | `dev-kit remove <skill...>` | Deselect and uninstall skills safely. |
108
109
  | `dev-kit list [--all]` | List selected skills or browse the catalog. |
109
110
  | `dev-kit search <words...>` | Search names and descriptions. |
110
- | `dev-kit info <skill>` | Show description, source, and approved commit. |
111
+ | `dev-kit info <skill>` | Show description and Git or installed-package provenance. |
111
112
  | `dev-kit status` | Check whether the project matches its selection. |
112
113
  | `dev-kit sync` | Apply the current manifest. |
113
114
  | `dev-kit plan` | Preview project changes without writing files. |
@@ -149,7 +150,8 @@ tool versions. A project-local process lock also prevents concurrent applies.
149
150
 
150
151
  ## Manifest
151
152
 
152
- `include` accepts individual skill names and skill families:
153
+ `include` accepts static skill names, skill families, and explicit
154
+ `<package>#<skill>` selectors:
153
155
 
154
156
  ```jsonc
155
157
  {
@@ -159,9 +161,13 @@ tool versions. A project-local process lock also prevents concurrent applies.
159
161
  "effect",
160
162
  "workers-best-practices",
161
163
  "wrangler",
162
- "serve-sim"
164
+ "serve-sim",
165
+ "@tanstack/ai#ai-core"
163
166
  ],
164
167
  "exclude": ["animation-vocabulary"],
168
+ "setup": {
169
+ "claudeInstructions": { "enabled": true }
170
+ },
165
171
  "targets": {
166
172
  "agents": { "enabled": true, "mode": "copy" },
167
173
  "claude": { "enabled": true, "mode": "symlink" },
@@ -175,6 +181,8 @@ tool versions. A project-local process lock also prevents concurrent applies.
175
181
  - Prefer individual external skills such as `workers-best-practices` and
176
182
  `wrangler`, selected after scanning the project for relevant technologies.
177
183
  - `serve-sim` selects the approved Evan Bacon simulator skill directly.
184
+ - `@tanstack/ai#ai-core` explicitly selects a skill discovered in that direct
185
+ project dependency; discovery alone never selects it.
178
186
  - An approved source ID is broad shorthand that selects every skill from that
179
187
  source. Use it only when the scan confirms that every member applies.
180
188
 
@@ -183,6 +191,25 @@ Dev Kit reserves `.repos/<source-id>` for project-local source checkouts. Run
183
191
  The patch is idempotent, preserves existing lines, and refuses symlinked
184
192
  `.gitignore` files.
185
193
 
194
+ ## Claude instructions
195
+
196
+ Enable a portable Claude Code instruction bridge in the manifest:
197
+
198
+ ```jsonc
199
+ {
200
+ "include": [],
201
+ "setup": {
202
+ "claudeInstructions": { "enabled": true }
203
+ }
204
+ }
205
+ ```
206
+
207
+ `dev-kit apply` requires a project-root `AGENTS.md`, then manages
208
+ `CLAUDE.md` as the relative symlink `CLAUDE.md → AGENTS.md`. The link is
209
+ recorded in the lockfile and local ownership state. Dev Kit refuses to replace
210
+ an unowned `CLAUDE.md` and removes the link when the task is disabled only if
211
+ the owned link is unchanged.
212
+
186
213
  ## Effect source checkout
187
214
 
188
215
  Enable a local checkout of the exact installed Effect release in the manifest:
@@ -248,10 +275,65 @@ troubleshooting the task directly.
248
275
  Package and tsconfig edits remain explicit until Dev Kit can safely own parts
249
276
  of shared JSONC files.
250
277
 
251
- ## Approved external skills
278
+ ## Installed package skills
279
+
280
+ Dev Kit generically discovers agent skills bundled by the project's installed
281
+ JavaScript packages. It reads the project's direct dependencies, checks the
282
+ package's Intent v1 discovery metadata (or Intent's repository-metadata
283
+ fallback), then looks for the layout
284
+ `node_modules/<package>/skills/<skill>/SKILL.md`.
285
+ TanStack is one publisher of this layout; no TanStack package names or skill
286
+ paths are hard-coded into Dev Kit.
287
+
288
+ Discovery is browse-only. These commands show an installed package skill but do
289
+ not select, copy, symlink, lock, or otherwise install it:
290
+
291
+ ```bash
292
+ bun x dev-kit list --all
293
+ bun x dev-kit search tanstack
294
+ bun x dev-kit info @tanstack/ai#ai-core
295
+ ```
296
+
297
+ Selection is explicit and package-qualified:
298
+
299
+ ```bash
300
+ bun x dev-kit add @tanstack/ai#ai-core
301
+ ```
302
+
303
+ That writes `@tanstack/ai#ai-core` to `dev-kit.jsonc` and, unless
304
+ `--no-apply` is passed, installs it through the normal ownership-safe sync
305
+ path. The qualifier prevents ambiguity when two dependencies publish the same
306
+ skill name. Two selected skills that would both write the same destination are
307
+ rejected before any output is changed.
308
+
309
+ The initial compatibility boundary is intentionally small and deterministic:
252
310
 
253
- This repository is an opinionated catalog, not a mirror of every upstream skill
254
- tree. `skill-sources.jsonc` declares sources Dan has approved:
311
+ - only packages named in the root project's `dependencies`,
312
+ `devDependencies`, `optionalDependencies`, or `peerDependencies` are
313
+ scanned;
314
+ - package code is never imported or executed;
315
+ - npm-style and pnpm/workspace symlinks under `node_modules` are supported;
316
+ - Yarn Plug'n'Play and transitive dependency traversal are not scanned; and
317
+ - immediate `skills/<name>/SKILL.md` roots are listed. Nested topic skills and
318
+ references remain part of that root and are copied with it.
319
+
320
+ The last rule adapts Intent's routed, nested skill trees to the immediate folder
321
+ and frontmatter-name invariants expected by Agent Skills targets. Dev Kit does
322
+ not rewrite nested names or ask Intent to manage agent configuration.
323
+
324
+ The project `dev-kit.lock.json` records the selected package name, installed
325
+ version, skill name, and content digest. `apply --locked` therefore rejects
326
+ package-version or skill-content drift. Dev Kit never downloads a missing
327
+ package or substitutes a registry version.
328
+
329
+ See TanStack's
330
+ [Agent Skills documentation](https://tanstack.com/ai/latest/docs/getting-started/agent-skills)
331
+ for a real package suite that uses this convention.
332
+
333
+ ## Approved external Git skills
334
+
335
+ This repository remains an opinionated catalog for Git-hosted skills.
336
+ `skill-sources.jsonc` contains only reviewed Git sources:
255
337
 
256
338
  ```jsonc
257
339
  {
@@ -276,49 +358,41 @@ bun run catalog:refresh
276
358
  bun run catalog:check
277
359
  ```
278
360
 
279
- Adding a source does not require editing JSONC:
361
+ Adding a Git source does not require editing JSONC:
280
362
 
281
363
  ```bash
282
- # Opens a skill picker in a terminal
283
364
  dev-kit catalog add https://github.com/owner/repository
284
-
285
- # Explicit and automation-friendly
286
365
  dev-kit catalog add https://github.com/owner/repository \
287
366
  --skill one --skill two
288
367
  dev-kit catalog add https://github.com/owner/repository --all
289
368
  ```
290
369
 
291
- GitHub tree URLs are accepted, so a URL such as
292
- `https://github.com/owner/repository/tree/main/skills` supplies the repository,
293
- ref, and skills path together. `--all` expands to the skills discovered at that
370
+ GitHub tree URLs are accepted. `--all` expands to the skills found at that
294
371
  exact snapshot; it never writes a wildcard that could silently approve a future
295
- upstream addition.
372
+ upstream addition. Catalog refresh resolves refs to exact commits, validates
373
+ names and paths, rejects symlinks and collisions, extracts descriptions, and
374
+ updates `skill-sources.lock.json`.
296
375
 
297
- The current catalog includes approved snapshots from Emil Kowalski,
298
- Cloudflare, and Evan Bacon. Inspect them with `dev-kit catalog list` and
299
- `dev-kit catalog info <source>`.
376
+ When a project selects one of these Git-backed skills, Dev Kit fetches the
377
+ approved commit into the ignored `.dev-kit/cache` and installs it through the
378
+ same ownership-safe sync path. Only a reviewed catalog refresh changes the
379
+ approved Git content.
300
380
 
301
- The refresh resolves refs to exact commits, validates names and paths, rejects
302
- symlinks and collisions, extracts short descriptions, and updates
303
- `skill-sources.lock.json`. It does not copy upstream skill trees into this
304
- repository.
381
+ ## Oxlint and Oxfmt configurations
305
382
 
306
- When a consuming project selects an external skill, Dev Kit fetches that exact
307
- approved commit into the ignored `.dev-kit/cache`, applies declared compatibility
308
- transforms, and installs the result through the ownership-safe sync path. Normal
309
- installs never float to a newer upstream commit; only a reviewed catalog refresh
310
- changes what is approved.
311
-
312
- ## Oxlint preset for Vite+
313
-
314
- Dev Kit exports a typed, composable set of high-signal Oxlint rules for Vite+
315
- projects:
383
+ Dev Kit exports one typed Oxlint ruleset and one typed Oxfmt configuration for
384
+ both standalone Oxc projects and Vite+ projects. A Vite+ project composes them
385
+ in `vite.config.ts`:
316
386
 
317
387
  ```ts
318
388
  import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
389
+ import { recommendedOxfmtConfig } from "@danieljvdm/dev-kit/oxfmt";
319
390
  import { defineConfig } from "vite-plus";
320
391
 
321
392
  export default defineConfig({
393
+ fmt: {
394
+ ...recommendedOxfmtConfig,
395
+ },
322
396
  lint: {
323
397
  extends: [recommendedOxlintConfig],
324
398
  rules: {
@@ -329,7 +403,38 @@ export default defineConfig({
329
403
  ```
330
404
 
331
405
  Use `lint.extends` rather than a shallow object spread so Vite+ composes the
332
- nested plugin and rule configuration correctly.
406
+ nested plugin and rule configuration correctly. Oxfmt has no `extends`, so
407
+ spread its configuration before project-local formatter options.
408
+
409
+ Standalone projects import the same objects from their native config files:
410
+
411
+ ```ts
412
+ // oxlint.config.ts
413
+ import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
414
+ import { defineConfig } from "oxlint";
415
+
416
+ export default defineConfig({
417
+ extends: [recommendedOxlintConfig],
418
+ });
419
+ ```
420
+
421
+ ```ts
422
+ // oxfmt.config.ts
423
+ import { recommendedOxfmtConfig } from "@danieljvdm/dev-kit/oxfmt";
424
+ import { defineConfig } from "oxfmt";
425
+
426
+ export default defineConfig({
427
+ ...recommendedOxfmtConfig,
428
+ });
429
+ ```
430
+
431
+ The Oxlint preset also registers the shared `effect` JavaScript plugin. Effect
432
+ projects opt into its rules in path-specific overrides, for example
433
+ `effect/no-effect-run`, `effect/no-unsafe-promise`, and
434
+ `effect/no-untyped-throw`. The package exports the plugin directly from
435
+ `@danieljvdm/dev-kit/oxlint-plugin-effect` for configurations that do not
436
+ extend the recommended preset. Strict workflow, Atom, and boundary rules remain
437
+ consumer-scoped because application and host boundaries differ by repository.
333
438
 
334
439
  ## Development
335
440
 
@@ -2,6 +2,7 @@
2
2
  "$schema": "./schema/dev-kit.schema.json",
3
3
  "include": ["dev-kit", "effect"],
4
4
  "setup": {
5
+ "claudeInstructions": { "enabled": true },
5
6
  "effectSource": { "enabled": true },
6
7
  "effectTsgo": { "enabled": true }
7
8
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danieljvdm/dev-kit",
3
- "version": "0.3.3",
3
+ "version": "0.5.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Declarative project development toolkit with portable agent skills.",
@@ -23,6 +23,14 @@
23
23
  "./oxlint": {
24
24
  "types": "./src/oxlint.ts",
25
25
  "default": "./src/oxlint.js"
26
+ },
27
+ "./oxfmt": {
28
+ "types": "./src/oxfmt.ts",
29
+ "default": "./src/oxfmt.js"
30
+ },
31
+ "./oxlint-plugin-effect": {
32
+ "types": "./src/oxlint-plugin-effect.d.ts",
33
+ "default": "./src/oxlint-plugin-effect.js"
26
34
  }
27
35
  },
28
36
  "files": [
@@ -64,14 +72,24 @@
64
72
  "@effect/tsgo": "0.24.3",
65
73
  "@effect/vitest": "4.0.0-beta.102",
66
74
  "@types/node": "25.9.1",
75
+ "oxfmt": "0.60.0",
76
+ "oxlint": "1.75.0",
67
77
  "typescript": "7.0.2",
68
78
  "vite-plus": "0.2.6",
69
79
  "vitest": "4.1.10"
70
80
  },
71
81
  "peerDependencies": {
82
+ "oxfmt": ">=0.60.0 <0.61.0",
83
+ "oxlint": ">=1.75.0 <2.0.0",
72
84
  "vite-plus": ">=0.2.6 <0.3.0"
73
85
  },
74
86
  "peerDependenciesMeta": {
87
+ "oxfmt": {
88
+ "optional": true
89
+ },
90
+ "oxlint": {
91
+ "optional": true
92
+ },
75
93
  "vite-plus": {
76
94
  "optional": true
77
95
  }
@@ -10,21 +10,21 @@
10
10
  "type": "string"
11
11
  },
12
12
  "include": {
13
- "description": "Skill names or family names to sync. Prefer individually relevant external skills; an external source id selects every approved skill from that source.",
13
+ "description": "Static skill names, family names, or exact <package>#<skill> selectors to sync. Installed package skills are discovered for browsing but selected only when explicitly included.",
14
14
  "type": "array",
15
15
  "items": {
16
16
  "type": "string",
17
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
17
+ "pattern": "^(?:[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]+)*)$"
18
18
  },
19
19
  "uniqueItems": true,
20
20
  "minItems": 0
21
21
  },
22
22
  "exclude": {
23
- "description": "Skill names or family names to remove from the selected set after expanding includes.",
23
+ "description": "Static skill names, family names, or exact <package>#<skill> selectors to remove after expanding includes.",
24
24
  "type": "array",
25
25
  "items": {
26
26
  "type": "string",
27
- "pattern": "^[a-z0-9]+(-[a-z0-9]+)*$"
27
+ "pattern": "^(?:[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
28
  },
29
29
  "uniqueItems": true,
30
30
  "default": []
@@ -34,6 +34,9 @@
34
34
  "type": "object",
35
35
  "additionalProperties": false,
36
36
  "properties": {
37
+ "claudeInstructions": {
38
+ "$ref": "#/$defs/claudeInstructionsSetup"
39
+ },
37
40
  "effectSource": {
38
41
  "$ref": "#/$defs/effectSourceSetup"
39
42
  },
@@ -58,6 +61,17 @@
58
61
  },
59
62
  "required": ["include"],
60
63
  "$defs": {
64
+ "claudeInstructionsSetup": {
65
+ "description": "Manage CLAUDE.md as a relative symlink to the project-root AGENTS.md file.",
66
+ "type": "object",
67
+ "additionalProperties": false,
68
+ "properties": {
69
+ "enabled": {
70
+ "type": "boolean",
71
+ "default": false
72
+ }
73
+ }
74
+ },
61
75
  "effectSourceSetup": {
62
76
  "type": "object",
63
77
  "additionalProperties": false,
@@ -67,6 +67,9 @@ skill as `dev-kit` when project agents should carry the toolkit procedure.
67
67
  "$schema": "./node_modules/@danieljvdm/dev-kit/schema/dev-kit.schema.json",
68
68
  "include": ["dev-kit", "effect"],
69
69
  "exclude": [],
70
+ "setup": {
71
+ "claudeInstructions": { "enabled": true }
72
+ },
70
73
  "targets": {
71
74
  "agents": { "enabled": true, "mode": "copy" },
72
75
  "claude": { "enabled": true, "mode": "symlink" },
@@ -80,6 +83,12 @@ use symlinks for additional harness discovery paths. Keep every target path
80
83
  project-relative and separate from the manifest, lock, state, and process-lock
81
84
  paths.
82
85
 
86
+ Enable `setup.claudeInstructions` when Claude Code should consume the same
87
+ project-root instructions as Codex. It manages `CLAUDE.md` as a relative
88
+ symlink to an existing `AGENTS.md`. Preserve any conflicting `CLAUDE.md`; when
89
+ disabled, dev-kit removes only an unchanged link recorded in local ownership
90
+ state.
91
+
83
92
  ## Ownership and conflicts
84
93
 
85
94
  Dev-kit adopts an existing destination only when its digest exactly matches a
@@ -156,16 +165,19 @@ dependencies; `dev-kit apply` patches once and then converges.
156
165
  Use `dev-kit tsgo patch --dry-run` for focused diagnosis. Use `--force` only
157
166
  after the user accepts a potentially commit-incompatible TypeScript binary.
158
167
 
159
- ## Oxlint preset for Vite+
168
+ ## Oxlint and Oxfmt configurations
160
169
 
161
- For Vite+ projects, compose the package's typed recommended Oxlint preset with
162
- project-specific lint configuration:
170
+ Use Dev Kit's canonical Oxlint and Oxfmt objects in Vite+ projects:
163
171
 
164
172
  ```ts
165
173
  import { recommendedOxlintConfig } from "@danieljvdm/dev-kit/oxlint";
174
+ import { recommendedOxfmtConfig } from "@danieljvdm/dev-kit/oxfmt";
166
175
  import { defineConfig } from "vite-plus";
167
176
 
168
177
  export default defineConfig({
178
+ fmt: {
179
+ ...recommendedOxfmtConfig,
180
+ },
169
181
  lint: {
170
182
  extends: [recommendedOxlintConfig],
171
183
  rules: {
@@ -176,14 +188,25 @@ export default defineConfig({
176
188
  ```
177
189
 
178
190
  Use `lint.extends` instead of spreading the object so Vite+ composes nested
179
- rule maps correctly. Keep repository-specific path conventions and
180
- platform-specific accessibility rules in the consuming project.
191
+ rule maps correctly. Oxfmt has no inheritance mechanism, so spread its object
192
+ before project-local options. Standalone `oxlint.config.ts` uses the same
193
+ `extends: [recommendedOxlintConfig]`; standalone `oxfmt.config.ts` spreads the
194
+ same `recommendedOxfmtConfig`.
195
+
196
+ The Oxlint preset registers Dev Kit's shared Effect plugin as `effect`, but
197
+ does not enable its scope-sensitive rules globally. Effect projects should
198
+ enable rules such as `effect/no-effect-run`, `effect/no-unsafe-promise`, and
199
+ `effect/no-untyped-throw` only in Effect-owned code, with explicit exceptions
200
+ for tests and host boundaries. The stricter `effect/no-async-workflow`,
201
+ `effect/no-promise-atom-mode`, and `effect/no-sync-boundary-decode` rules also
202
+ need consumer-owned scopes. Keep repository-specific paths and platform rules
203
+ in the consuming project.
181
204
 
182
205
  ## Current boundary
183
206
 
184
- Manage skill outputs, the `setup.effectSource` checkout, and the explicit
185
- `setup.effectTsgo` task. Edit shared `package.json` and `tsconfig.json`
186
- contributions deliberately. The Oxlint preset is a composable package export,
187
- not a manifest-managed output. Treat named bundles, Oxfmt presets, and broader
188
- setup tasks as future manifest capabilities until the installed CLI exposes
189
- them.
207
+ Manage skill outputs, the `setup.claudeInstructions` link, the
208
+ `setup.effectSource` checkout, and the explicit `setup.effectTsgo` task. Edit
209
+ shared `package.json` and `tsconfig.json`
210
+ contributions deliberately. The Oxlint and Oxfmt configurations are composable
211
+ package exports, not manifest-managed outputs. Treat broader setup tasks as
212
+ future manifest capabilities until the installed CLI exposes them.
@@ -57,7 +57,7 @@ const addCommand = CliCommand.make(
57
57
  skills.length === 0
58
58
  ? chooseSkillsToAdd({ apply: !noApply, manifestPath: manifest, projectDir })
59
59
  : addSkills(skills, { apply: !noApply, manifestPath: manifest, projectDir }),
60
- ).pipe(CliCommand.withDescription("Select and install one or more approved skills."));
60
+ ).pipe(CliCommand.withDescription("Select and install one or more available skills."));
61
61
 
62
62
  const removeCommand = CliCommand.make(
63
63
  "remove",
@@ -89,13 +89,13 @@ const searchCommand = CliCommand.make(
89
89
  { query: Argument.string("query").pipe(Argument.variadic({ min: 1 })), ...projectFlags },
90
90
  ({ query, manifest, projectDir }) =>
91
91
  listSkills({ all: true, query: query.join(" "), manifestPath: manifest, projectDir }),
92
- ).pipe(CliCommand.withDescription("Search approved skill names and descriptions."));
92
+ ).pipe(CliCommand.withDescription("Search available skill names and descriptions."));
93
93
 
94
94
  const infoCommand = CliCommand.make(
95
95
  "info",
96
- { skill: Argument.string("skill") },
97
- ({ skill }) => showSkill(skill),
98
- ).pipe(CliCommand.withDescription("Show provenance and details for an approved skill."));
96
+ { skill: Argument.string("skill"), ...projectFlags },
97
+ ({ skill, manifest, projectDir }) => showSkill(skill, { manifestPath: manifest, projectDir }),
98
+ ).pipe(CliCommand.withDescription("Show provenance and details for an available skill."));
99
99
 
100
100
  const planCommand = CliCommand.make(
101
101
  "plan",
package/src/catalog.ts CHANGED
@@ -2,7 +2,11 @@ import { parse as parseJsonc, type ParseError } from "jsonc-parser";
2
2
  import { Effect, FileSystem, Path, Schema, Stream } from "effect";
3
3
  import { ChildProcess } from "effect/unstable/process";
4
4
 
5
- import { observePath } from "./path-digest.ts";
5
+ import { observePath, type Digest } from "./path-digest.ts";
6
+ import {
7
+ discoverPackageSkills,
8
+ resolvePackageSkillSelector,
9
+ } from "./package-skill-source.ts";
6
10
  import {
7
11
  SkillSourcesLockSchema,
8
12
  type LockedSkillSource,
@@ -11,9 +15,14 @@ import {
11
15
 
12
16
  export type CatalogSkill = {
13
17
  readonly name: string;
18
+ readonly selector: string;
14
19
  readonly description: string;
15
20
  readonly source: string;
16
21
  readonly bundled: boolean;
22
+ readonly package?: {
23
+ readonly name: string;
24
+ readonly version: string;
25
+ };
17
26
  };
18
27
 
19
28
  export type SkillCatalog = {
@@ -24,11 +33,19 @@ export type SkillCatalog = {
24
33
 
25
34
  export type ResolvedSkillSource = {
26
35
  readonly path: string;
27
- readonly catalog?: {
28
- readonly source: string;
29
- readonly repository: string;
30
- readonly resolved: string;
31
- };
36
+ readonly linkPath?: string;
37
+ readonly catalog?:
38
+ | {
39
+ readonly source: string;
40
+ readonly repository: string;
41
+ readonly resolved: string;
42
+ }
43
+ | {
44
+ readonly package: string;
45
+ readonly version: string;
46
+ readonly skill: string;
47
+ readonly digest: Digest;
48
+ };
32
49
  };
33
50
 
34
51
  class CatalogError extends Schema.TaggedErrorClass<CatalogError>()("CatalogError", {
@@ -87,6 +104,7 @@ const readDescription = Effect.fn("readSkillDescription")(function* (skillPath:
87
104
 
88
105
  export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
89
106
  packageRoot: string,
107
+ projectDir: string,
90
108
  ) {
91
109
  const fs = yield* FileSystem.FileSystem;
92
110
  const path = yield* Path.Path;
@@ -98,6 +116,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
98
116
  if ((yield* fs.exists(path.join(skillPath, "SKILL.md")))) {
99
117
  skills.push({
100
118
  name,
119
+ selector: name,
101
120
  description: yield* readDescription(skillPath),
102
121
  source: "built-in",
103
122
  bundled: true,
@@ -110,28 +129,49 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
110
129
  for (const name of source.skills) {
111
130
  skills.push({
112
131
  name,
132
+ selector: name,
113
133
  description: source.descriptions?.[name] ?? "",
114
134
  source: source.id,
115
135
  bundled: false,
116
136
  });
117
137
  }
118
138
  }
139
+ const discovery = yield* discoverPackageSkills(projectDir);
140
+ for (const candidate of discovery.candidates) {
141
+ skills.push({
142
+ name: candidate.name,
143
+ selector: candidate.selector,
144
+ description: candidate.description,
145
+ source: candidate.package,
146
+ bundled: false,
147
+ package: { name: candidate.package, version: candidate.version },
148
+ });
149
+ }
119
150
  const duplicates = skills.filter(
120
- (skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
151
+ (skill, index) => skills.findIndex((candidate) => candidate.selector === skill.selector) !== index,
121
152
  );
122
153
  if (duplicates.length > 0) {
123
154
  return yield* new CatalogError({
124
- message: `duplicate catalog skill: ${duplicates[0]?.name ?? "unknown"}`,
155
+ message: `duplicate catalog skill selector: ${duplicates[0]?.selector ?? "unknown"}`,
156
+ });
157
+ }
158
+ const externalFamilies = (lock?.sources ?? []).map((source) =>
159
+ [source.id, source.skills] as const
160
+ );
161
+ const duplicateFamily = externalFamilies.find(
162
+ ([id], index) => externalFamilies.findIndex(([candidate]) => candidate === id) !== index,
163
+ );
164
+ if (duplicateFamily !== undefined) {
165
+ return yield* new CatalogError({
166
+ message: `duplicate catalog family: ${duplicateFamily[0]}`,
125
167
  });
126
168
  }
127
169
  const families: Readonly<Record<string, ReadonlyArray<string>>> = {
128
170
  effect: ["effect-ts"],
129
- ...Object.fromEntries(
130
- (lock?.sources ?? []).map((source) => [source.id, source.skills]),
131
- ),
171
+ ...Object.fromEntries(externalFamilies),
132
172
  };
133
173
  return {
134
- skills: skills.sort((left, right) => left.name.localeCompare(right.name)),
174
+ skills: skills.sort((left, right) => left.selector.localeCompare(right.selector)),
135
175
  families,
136
176
  ...(lock ? { lock } : {}),
137
177
  } satisfies SkillCatalog;
@@ -224,15 +264,15 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
224
264
  export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
225
265
  packageRoot: string,
226
266
  projectDir: string,
267
+ catalog: SkillCatalog,
227
268
  selected: ReadonlyArray<string>,
228
269
  cache = true,
229
270
  ) {
230
271
  const path = yield* Path.Path;
231
- const catalog = yield* loadSkillCatalog(packageRoot);
232
272
  const sources = new Map<string, ResolvedSkillSource>();
233
273
  for (const skill of catalog.skills.filter((skill) => skill.bundled)) {
234
- if (selected.includes(skill.name)) {
235
- sources.set(skill.name, { path: path.join(packageRoot, "skills", skill.name) });
274
+ if (selected.includes(skill.selector)) {
275
+ sources.set(skill.selector, { path: path.join(packageRoot, "skills", skill.name) });
236
276
  }
237
277
  }
238
278
  for (const source of catalog.lock?.sources ?? []) {
@@ -242,5 +282,22 @@ export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
242
282
  sources.set(name, sourcePath);
243
283
  }
244
284
  }
285
+ for (const selector of selected.filter((value) => value.includes("#"))) {
286
+ const resolved = yield* resolvePackageSkillSelector(projectDir, selector);
287
+ const observation = yield* observePath(resolved.path);
288
+ if (observation.kind !== "directory") {
289
+ return yield* new CatalogError({ message: `package skill is missing: ${selector}` });
290
+ }
291
+ sources.set(selector, {
292
+ path: resolved.path,
293
+ linkPath: resolved.linkPath,
294
+ catalog: {
295
+ package: resolved.package,
296
+ version: resolved.version,
297
+ skill: resolved.name,
298
+ digest: observation.digest,
299
+ },
300
+ });
301
+ }
245
302
  return sources;
246
303
  });
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export {
2
+ type ClaudeInstructionsSetup,
3
+ ClaudeInstructionsSetupSchema,
2
4
  type DevKitManifest,
3
5
  DevKitManifestSchema,
4
6
  type EffectSourceSetup,
@@ -58,15 +60,21 @@ export {
58
60
  } from "./sync.ts";
59
61
  export {
60
62
  AppliedStateSchema,
63
+ CatalogProvenanceSchema,
61
64
  DevKitLockSchema,
62
65
  EffectSourceLockSchema,
63
66
  EffectTsgoLockSchema,
67
+ ManagedInstructionOutputSchema,
68
+ ManagedOutputSchema,
64
69
  ManagedSkillOutputSchema,
65
70
  OwnershipReceiptSchema,
66
71
  type AppliedState,
72
+ type CatalogProvenance,
67
73
  type DevKitLock,
68
74
  type EffectSourceLock,
69
75
  type EffectTsgoLock,
76
+ type ManagedInstructionOutput,
77
+ type ManagedOutput,
70
78
  type ManagedSkillOutput,
71
79
  type OwnershipReceipt,
72
80
  } from "./project-state.ts";