@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.
- package/README.md +130 -35
- package/dev-kit.example.jsonc +2 -0
- package/package.json +3 -2
- package/schema/dev-kit.schema.json +32 -4
- package/skills/dev-kit/SKILL.md +31 -6
- package/skills/effect-atom-data-fetching/SKILL.md +40 -0
- package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
- package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
- package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
- package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
- package/skills/effect-atom-data-fetching/references/testing.md +63 -0
- package/src/bin/dev-kit.ts +5 -5
- package/src/catalog.ts +73 -16
- package/src/index.ts +14 -0
- package/src/manifest.ts +29 -2
- package/src/package-skill-source.ts +250 -0
- package/src/path-digest.ts +7 -0
- package/src/project-package.ts +34 -0
- package/src/project-state.ts +50 -9
- package/src/skill-manager.ts +66 -30
- package/src/skill-selector.ts +43 -0
- package/src/sync.ts +279 -40
- package/templates/AGENTS.md +9 -0
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
|
|
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
|
|
@@ -72,18 +73,33 @@ bun x dev-kit plan
|
|
|
72
73
|
bun x dev-kit apply
|
|
73
74
|
```
|
|
74
75
|
|
|
75
|
-
Commit the generated `dev-kit.lock.json`, then
|
|
76
|
-
|
|
76
|
+
Commit the generated `dev-kit.lock.json`, then let the package lifecycle
|
|
77
|
+
converge owned outputs automatically when installed packages change:
|
|
77
78
|
|
|
78
79
|
```jsonc
|
|
79
80
|
{
|
|
80
81
|
"scripts": {
|
|
81
|
-
"postinstall": "dev-kit apply
|
|
82
|
+
"postinstall": "dev-kit apply"
|
|
82
83
|
}
|
|
83
84
|
}
|
|
84
85
|
```
|
|
85
86
|
|
|
86
|
-
That single postinstall applies every task enabled in `dev-kit.jsonc
|
|
87
|
+
That single postinstall applies every task enabled in `dev-kit.jsonc` and
|
|
88
|
+
regenerates `dev-kit.lock.json` when an intentional package upgrade changes a
|
|
89
|
+
bundled or package-provided skill. Ownership and conflict checks still prevent
|
|
90
|
+
unreviewed overwrites.
|
|
91
|
+
|
|
92
|
+
Keep strict verification in CI. Either install with lifecycle scripts disabled
|
|
93
|
+
before running locked mode:
|
|
94
|
+
|
|
95
|
+
```bash
|
|
96
|
+
bun install --ignore-scripts
|
|
97
|
+
bun x dev-kit apply --locked
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Or allow the normal postinstall and fail CI when it leaves tracked changes.
|
|
101
|
+
Do not run an unlocked apply before a locked verification because that would
|
|
102
|
+
regenerate the drift being checked.
|
|
87
103
|
|
|
88
104
|
This repository dogfoods the same flow with its committed `dev-kit.jsonc` and
|
|
89
105
|
`dev-kit.lock.json`. From this source checkout, invoke the local CLI with:
|
|
@@ -107,7 +123,7 @@ the root package itself.
|
|
|
107
123
|
| `dev-kit remove <skill...>` | Deselect and uninstall skills safely. |
|
|
108
124
|
| `dev-kit list [--all]` | List selected skills or browse the catalog. |
|
|
109
125
|
| `dev-kit search <words...>` | Search names and descriptions. |
|
|
110
|
-
| `dev-kit info <skill>` | Show description
|
|
126
|
+
| `dev-kit info <skill>` | Show description and Git or installed-package provenance. |
|
|
111
127
|
| `dev-kit status` | Check whether the project matches its selection. |
|
|
112
128
|
| `dev-kit sync` | Apply the current manifest. |
|
|
113
129
|
| `dev-kit plan` | Preview project changes without writing files. |
|
|
@@ -149,7 +165,8 @@ tool versions. A project-local process lock also prevents concurrent applies.
|
|
|
149
165
|
|
|
150
166
|
## Manifest
|
|
151
167
|
|
|
152
|
-
`include` accepts
|
|
168
|
+
`include` accepts static skill names, skill families, and explicit
|
|
169
|
+
`<package>#<skill>` selectors:
|
|
153
170
|
|
|
154
171
|
```jsonc
|
|
155
172
|
{
|
|
@@ -159,9 +176,14 @@ tool versions. A project-local process lock also prevents concurrent applies.
|
|
|
159
176
|
"effect",
|
|
160
177
|
"workers-best-practices",
|
|
161
178
|
"wrangler",
|
|
162
|
-
"serve-sim"
|
|
179
|
+
"serve-sim",
|
|
180
|
+
"@tanstack/ai#ai-core"
|
|
163
181
|
],
|
|
164
182
|
"exclude": ["animation-vocabulary"],
|
|
183
|
+
"setup": {
|
|
184
|
+
"agentInstructions": { "enabled": true },
|
|
185
|
+
"claudeInstructions": { "enabled": true }
|
|
186
|
+
},
|
|
165
187
|
"targets": {
|
|
166
188
|
"agents": { "enabled": true, "mode": "copy" },
|
|
167
189
|
"claude": { "enabled": true, "mode": "symlink" },
|
|
@@ -171,10 +193,13 @@ tool versions. A project-local process lock also prevents concurrent applies.
|
|
|
171
193
|
```
|
|
172
194
|
|
|
173
195
|
- `dev-kit` installs guidance for operating the toolkit itself.
|
|
174
|
-
- `effect` expands to
|
|
196
|
+
- `effect` expands to `effect-ts` plus focused Effect Atom HTTP data-fetching
|
|
197
|
+
guidance.
|
|
175
198
|
- Prefer individual external skills such as `workers-best-practices` and
|
|
176
199
|
`wrangler`, selected after scanning the project for relevant technologies.
|
|
177
200
|
- `serve-sim` selects the approved Evan Bacon simulator skill directly.
|
|
201
|
+
- `@tanstack/ai#ai-core` explicitly selects a skill discovered in that direct
|
|
202
|
+
project dependency; discovery alone never selects it.
|
|
178
203
|
- An approved source ID is broad shorthand that selects every skill from that
|
|
179
204
|
source. Use it only when the scan confirms that every member applies.
|
|
180
205
|
|
|
@@ -183,6 +208,34 @@ Dev Kit reserves `.repos/<source-id>` for project-local source checkouts. Run
|
|
|
183
208
|
The patch is idempotent, preserves existing lines, and refuses symlinked
|
|
184
209
|
`.gitignore` files.
|
|
185
210
|
|
|
211
|
+
## Agent instructions
|
|
212
|
+
|
|
213
|
+
Enable a managed project-root instruction wrapper and a portable Claude Code
|
|
214
|
+
bridge in the manifest:
|
|
215
|
+
|
|
216
|
+
```jsonc
|
|
217
|
+
{
|
|
218
|
+
"include": ["dev-kit"],
|
|
219
|
+
"setup": {
|
|
220
|
+
"agentInstructions": { "enabled": true },
|
|
221
|
+
"claudeInstructions": { "enabled": true }
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`setup.agentInstructions` manages `AGENTS.md` as a generated wrapper with a
|
|
227
|
+
short description of dev-kit and a pointer to the installed `dev-kit` skill.
|
|
228
|
+
When the root `package.json` declares `vite-plus` directly, the wrapper also
|
|
229
|
+
includes Vite+'s installed `node_modules/vite-plus/AGENTS.md` instructions.
|
|
230
|
+
Transitive installations do not opt a project in.
|
|
231
|
+
|
|
232
|
+
`setup.claudeInstructions` manages `CLAUDE.md` as the relative symlink
|
|
233
|
+
`CLAUDE.md → AGENTS.md`. It can link to the generated wrapper in the same apply,
|
|
234
|
+
or retain the older behavior of linking to an existing regular `AGENTS.md` when
|
|
235
|
+
the wrapper task is disabled. Both outputs are recorded independently in the
|
|
236
|
+
lockfile and local ownership state. Dev Kit refuses to replace unowned files
|
|
237
|
+
and removes only unchanged owned outputs.
|
|
238
|
+
|
|
186
239
|
## Effect source checkout
|
|
187
240
|
|
|
188
241
|
Enable a local checkout of the exact installed Effect release in the manifest:
|
|
@@ -248,10 +301,65 @@ troubleshooting the task directly.
|
|
|
248
301
|
Package and tsconfig edits remain explicit until Dev Kit can safely own parts
|
|
249
302
|
of shared JSONC files.
|
|
250
303
|
|
|
251
|
-
##
|
|
304
|
+
## Installed package skills
|
|
252
305
|
|
|
253
|
-
|
|
254
|
-
|
|
306
|
+
Dev Kit generically discovers agent skills bundled by the project's installed
|
|
307
|
+
JavaScript packages. It reads the project's direct dependencies, checks the
|
|
308
|
+
package's Intent v1 discovery metadata (or Intent's repository-metadata
|
|
309
|
+
fallback), then looks for the layout
|
|
310
|
+
`node_modules/<package>/skills/<skill>/SKILL.md`.
|
|
311
|
+
TanStack is one publisher of this layout; no TanStack package names or skill
|
|
312
|
+
paths are hard-coded into Dev Kit.
|
|
313
|
+
|
|
314
|
+
Discovery is browse-only. These commands show an installed package skill but do
|
|
315
|
+
not select, copy, symlink, lock, or otherwise install it:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
bun x dev-kit list --all
|
|
319
|
+
bun x dev-kit search tanstack
|
|
320
|
+
bun x dev-kit info @tanstack/ai#ai-core
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Selection is explicit and package-qualified:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
bun x dev-kit add @tanstack/ai#ai-core
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
That writes `@tanstack/ai#ai-core` to `dev-kit.jsonc` and, unless
|
|
330
|
+
`--no-apply` is passed, installs it through the normal ownership-safe sync
|
|
331
|
+
path. The qualifier prevents ambiguity when two dependencies publish the same
|
|
332
|
+
skill name. Two selected skills that would both write the same destination are
|
|
333
|
+
rejected before any output is changed.
|
|
334
|
+
|
|
335
|
+
The initial compatibility boundary is intentionally small and deterministic:
|
|
336
|
+
|
|
337
|
+
- only packages named in the root project's `dependencies`,
|
|
338
|
+
`devDependencies`, `optionalDependencies`, or `peerDependencies` are
|
|
339
|
+
scanned;
|
|
340
|
+
- package code is never imported or executed;
|
|
341
|
+
- npm-style and pnpm/workspace symlinks under `node_modules` are supported;
|
|
342
|
+
- Yarn Plug'n'Play and transitive dependency traversal are not scanned; and
|
|
343
|
+
- immediate `skills/<name>/SKILL.md` roots are listed. Nested topic skills and
|
|
344
|
+
references remain part of that root and are copied with it.
|
|
345
|
+
|
|
346
|
+
The last rule adapts Intent's routed, nested skill trees to the immediate folder
|
|
347
|
+
and frontmatter-name invariants expected by Agent Skills targets. Dev Kit does
|
|
348
|
+
not rewrite nested names or ask Intent to manage agent configuration.
|
|
349
|
+
|
|
350
|
+
The project `dev-kit.lock.json` records the selected package name, installed
|
|
351
|
+
version, skill name, and content digest. `apply --locked` therefore rejects
|
|
352
|
+
package-version or skill-content drift. Dev Kit never downloads a missing
|
|
353
|
+
package or substitutes a registry version.
|
|
354
|
+
|
|
355
|
+
See TanStack's
|
|
356
|
+
[Agent Skills documentation](https://tanstack.com/ai/latest/docs/getting-started/agent-skills)
|
|
357
|
+
for a real package suite that uses this convention.
|
|
358
|
+
|
|
359
|
+
## Approved external Git skills
|
|
360
|
+
|
|
361
|
+
This repository remains an opinionated catalog for Git-hosted skills.
|
|
362
|
+
`skill-sources.jsonc` contains only reviewed Git sources:
|
|
255
363
|
|
|
256
364
|
```jsonc
|
|
257
365
|
{
|
|
@@ -276,38 +384,25 @@ bun run catalog:refresh
|
|
|
276
384
|
bun run catalog:check
|
|
277
385
|
```
|
|
278
386
|
|
|
279
|
-
Adding a source does not require editing JSONC:
|
|
387
|
+
Adding a Git source does not require editing JSONC:
|
|
280
388
|
|
|
281
389
|
```bash
|
|
282
|
-
# Opens a skill picker in a terminal
|
|
283
390
|
dev-kit catalog add https://github.com/owner/repository
|
|
284
|
-
|
|
285
|
-
# Explicit and automation-friendly
|
|
286
391
|
dev-kit catalog add https://github.com/owner/repository \
|
|
287
392
|
--skill one --skill two
|
|
288
393
|
dev-kit catalog add https://github.com/owner/repository --all
|
|
289
394
|
```
|
|
290
395
|
|
|
291
|
-
GitHub tree URLs are accepted
|
|
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
|
|
396
|
+
GitHub tree URLs are accepted. `--all` expands to the skills found at that
|
|
294
397
|
exact snapshot; it never writes a wildcard that could silently approve a future
|
|
295
|
-
upstream addition.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
`skill-sources.lock.json`. It does not copy upstream skill trees into this
|
|
304
|
-
repository.
|
|
305
|
-
|
|
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.
|
|
398
|
+
upstream addition. Catalog refresh resolves refs to exact commits, validates
|
|
399
|
+
names and paths, rejects symlinks and collisions, extracts descriptions, and
|
|
400
|
+
updates `skill-sources.lock.json`.
|
|
401
|
+
|
|
402
|
+
When a project selects one of these Git-backed skills, Dev Kit fetches the
|
|
403
|
+
approved commit into the ignored `.dev-kit/cache` and installs it through the
|
|
404
|
+
same ownership-safe sync path. Only a reviewed catalog refresh changes the
|
|
405
|
+
approved Git content.
|
|
311
406
|
|
|
312
407
|
## Oxlint and Oxfmt configurations
|
|
313
408
|
|
package/dev-kit.example.jsonc
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danieljvdm/dev-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Declarative project development toolkit with portable agent skills.",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"skills/",
|
|
41
41
|
"skill-sources.jsonc",
|
|
42
42
|
"skill-sources.lock.json",
|
|
43
|
-
"src/"
|
|
43
|
+
"src/",
|
|
44
|
+
"templates/"
|
|
44
45
|
],
|
|
45
46
|
"scripts": {
|
|
46
47
|
"prepare": "./bin/dev-kit.mjs apply --locked",
|
|
@@ -10,21 +10,21 @@
|
|
|
10
10
|
"type": "string"
|
|
11
11
|
},
|
|
12
12
|
"include": {
|
|
13
|
-
"description": "
|
|
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": "
|
|
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,12 @@
|
|
|
34
34
|
"type": "object",
|
|
35
35
|
"additionalProperties": false,
|
|
36
36
|
"properties": {
|
|
37
|
+
"agentInstructions": {
|
|
38
|
+
"$ref": "#/$defs/agentInstructionsSetup"
|
|
39
|
+
},
|
|
40
|
+
"claudeInstructions": {
|
|
41
|
+
"$ref": "#/$defs/claudeInstructionsSetup"
|
|
42
|
+
},
|
|
37
43
|
"effectSource": {
|
|
38
44
|
"$ref": "#/$defs/effectSourceSetup"
|
|
39
45
|
},
|
|
@@ -58,6 +64,28 @@
|
|
|
58
64
|
},
|
|
59
65
|
"required": ["include"],
|
|
60
66
|
"$defs": {
|
|
67
|
+
"agentInstructionsSetup": {
|
|
68
|
+
"description": "Manage a project-root AGENTS.md wrapper with dev-kit guidance and conditional tool instructions.",
|
|
69
|
+
"type": "object",
|
|
70
|
+
"additionalProperties": false,
|
|
71
|
+
"properties": {
|
|
72
|
+
"enabled": {
|
|
73
|
+
"type": "boolean",
|
|
74
|
+
"default": false
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"claudeInstructionsSetup": {
|
|
79
|
+
"description": "Manage CLAUDE.md as a relative symlink to the project-root AGENTS.md file.",
|
|
80
|
+
"type": "object",
|
|
81
|
+
"additionalProperties": false,
|
|
82
|
+
"properties": {
|
|
83
|
+
"enabled": {
|
|
84
|
+
"type": "boolean",
|
|
85
|
+
"default": false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
61
89
|
"effectSourceSetup": {
|
|
62
90
|
"type": "object",
|
|
63
91
|
"additionalProperties": false,
|
package/skills/dev-kit/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: dev-kit
|
|
3
|
-
description: Dev-kit operations for projects that configure dev-kit.jsonc, sync portable skills, run plan/apply or
|
|
3
|
+
description: Dev-kit operations for projects that configure dev-kit.jsonc, sync portable skills, run plan/apply or automatic postinstalls, perform locked CI checks, maintain dev-kit.lock.json, resolve ownership conflicts, patch managed ignores, or enable Effect TypeScript-Go.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Dev Kit
|
|
@@ -51,8 +51,12 @@ stores explicit skill names and exact commit/content digests.
|
|
|
51
51
|
6. Resolve conflicts, then run `dev-kit apply`. Commit the manifest and
|
|
52
52
|
regenerated `dev-kit.lock.json`; keep `.dev-kit/` local. Finish when a second
|
|
53
53
|
plan reports only unchanged resources and setup tasks.
|
|
54
|
-
7. Use `dev-kit apply
|
|
55
|
-
|
|
54
|
+
7. Use `dev-kit apply` in the package lifecycle so intentional dependency
|
|
55
|
+
upgrades regenerate owned outputs and `dev-kit.lock.json`. For strict CI,
|
|
56
|
+
either disable lifecycle scripts before `dev-kit apply --locked`, or run the
|
|
57
|
+
normal lifecycle and require the tracked working tree to remain clean. Never
|
|
58
|
+
run an unlocked apply before locked verification. Finish when a clean install
|
|
59
|
+
converges from the committed manifest and lock.
|
|
56
60
|
|
|
57
61
|
## Manifest
|
|
58
62
|
|
|
@@ -67,6 +71,10 @@ skill as `dev-kit` when project agents should carry the toolkit procedure.
|
|
|
67
71
|
"$schema": "./node_modules/@danieljvdm/dev-kit/schema/dev-kit.schema.json",
|
|
68
72
|
"include": ["dev-kit", "effect"],
|
|
69
73
|
"exclude": [],
|
|
74
|
+
"setup": {
|
|
75
|
+
"agentInstructions": { "enabled": true },
|
|
76
|
+
"claudeInstructions": { "enabled": true }
|
|
77
|
+
},
|
|
70
78
|
"targets": {
|
|
71
79
|
"agents": { "enabled": true, "mode": "copy" },
|
|
72
80
|
"claude": { "enabled": true, "mode": "symlink" },
|
|
@@ -80,6 +88,15 @@ use symlinks for additional harness discovery paths. Keep every target path
|
|
|
80
88
|
project-relative and separate from the manifest, lock, state, and process-lock
|
|
81
89
|
paths.
|
|
82
90
|
|
|
91
|
+
Enable `setup.agentInstructions` to manage a project-root `AGENTS.md` wrapper
|
|
92
|
+
that points agents back to this skill. When `vite-plus` is a declared direct
|
|
93
|
+
dependency, dev-kit includes its installed agent instructions in the wrapper.
|
|
94
|
+
Enable `setup.claudeInstructions` when Claude Code should consume the same
|
|
95
|
+
project-root instructions; it manages `CLAUDE.md` as a relative symlink to the
|
|
96
|
+
wrapper or to an existing regular `AGENTS.md`. Preserve conflicting paths;
|
|
97
|
+
when disabled, dev-kit removes only unchanged outputs recorded in local
|
|
98
|
+
ownership state.
|
|
99
|
+
|
|
83
100
|
## Ownership and conflicts
|
|
84
101
|
|
|
85
102
|
Dev-kit adopts an existing destination only when its digest exactly matches a
|
|
@@ -109,11 +126,17 @@ For one lifecycle entry point, configure:
|
|
|
109
126
|
```jsonc
|
|
110
127
|
{
|
|
111
128
|
"scripts": {
|
|
112
|
-
"postinstall": "dev-kit apply
|
|
129
|
+
"postinstall": "dev-kit apply"
|
|
113
130
|
}
|
|
114
131
|
}
|
|
115
132
|
```
|
|
116
133
|
|
|
134
|
+
This intentionally refreshes the committed lock and owned outputs when the
|
|
135
|
+
package manager installs a new Dev Kit or selected package-skill version.
|
|
136
|
+
Review and commit those changes with the dependency update. Keep
|
|
137
|
+
`dev-kit apply --locked` as a verification command, not the normal local
|
|
138
|
+
lifecycle; in CI, run it only before any unlocked apply.
|
|
139
|
+
|
|
117
140
|
## Effect source checkout
|
|
118
141
|
|
|
119
142
|
Enable the source task when agents should have canonical source matching the
|
|
@@ -195,8 +218,10 @@ in the consuming project.
|
|
|
195
218
|
|
|
196
219
|
## Current boundary
|
|
197
220
|
|
|
198
|
-
Manage skill outputs, the `setup.
|
|
199
|
-
`setup.
|
|
221
|
+
Manage skill outputs, the `setup.agentInstructions` wrapper, the
|
|
222
|
+
`setup.claudeInstructions` link, the
|
|
223
|
+
`setup.effectSource` checkout, and the explicit `setup.effectTsgo` task. Edit
|
|
224
|
+
shared `package.json` and `tsconfig.json`
|
|
200
225
|
contributions deliberately. The Oxlint and Oxfmt configurations are composable
|
|
201
226
|
package exports, not manifest-managed outputs. Treat broader setup tasks as
|
|
202
227
|
future manifest capabilities until the installed CLI exposes them.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: effect-atom-data-fetching
|
|
3
|
+
description: Design, implement, debug, or review HTTP data fetching with Effect Atom in React applications. Use for AtomHttpApi.Service queries and mutations, parameterized Atom.family caches, RegistryProvider and runtime placement, TTL/SWR/polling behavior, reactivity-key invalidation, framework-appropriate SSR and focus handling, AsyncResult aggregation resets, and deterministic cache lifecycle tests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Effect Atom Data Fetching
|
|
7
|
+
|
|
8
|
+
Model server data as stable atoms owned outside React renders. Give the application one intentional registry boundary and one compatible runtime factory; then choose cache retention, freshness, polling, and invalidation independently.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Inspect the installed `effect` and `@effect/atom-react` versions and their source before copying signatures. These APIs live under `effect/unstable/reactivity` and may move.
|
|
13
|
+
2. Locate every `RegistryProvider`, runtime factory, query atom, mutation atom, and route-level `AsyncResult.all`. Draw the ownership boundary before changing behavior.
|
|
14
|
+
3. Keep one `RegistryProvider` for the intended client application lifetime. Define the shared runtime factory, API service, query families, and mutation atoms at module scope rather than in components.
|
|
15
|
+
4. Choose each lifecycle control for its actual job:
|
|
16
|
+
- idle `timeToLive`: retain an unused registry value before disposal;
|
|
17
|
+
- `Atom.swr({ staleTime })`: decide when mount/focus revalidation is needed;
|
|
18
|
+
- `Atom.withRefresh`: force periodic refresh while mounted.
|
|
19
|
+
5. Give queries stable identities and matching reactivity keys. Let successful mutations invalidate those keys rather than manually coordinating every consumer.
|
|
20
|
+
6. Keep browser-only signals behind an SSR-safe boundary. Decide explicitly whether initial data is client-only or hydrated from a request-scoped server registry.
|
|
21
|
+
7. Verify lifecycle behavior with fake time and request counters, not sleeps.
|
|
22
|
+
|
|
23
|
+
## Ownership rules
|
|
24
|
+
|
|
25
|
+
- Treat a query as shared read state: export one atom or `Atom.family` and let components subscribe.
|
|
26
|
+
- Treat an action as an event owned by the initiating UI or workflow: export the mutation atom, invoke it with `useAtomSet`, and observe its result only where useful.
|
|
27
|
+
- Never allocate a query atom in render. For parameterized queries, use a stable scalar or Effect `Hash`/`Equal` value as the family argument.
|
|
28
|
+
- Ensure every input to a route-level `AsyncResult.all` has stable atom identity and compatible retention. One newly allocated or immediately evicted input returns to `Initial` and makes the whole aggregate appear reset even when the other inputs remain cached.
|
|
29
|
+
- Do not describe manual refresh or polling as freshness caching. Refresh is forceful; `staleTime` only gates SWR's automatic mount/focus decisions.
|
|
30
|
+
|
|
31
|
+
## References
|
|
32
|
+
|
|
33
|
+
- Read `./references/cache-lifecycle.md` for registry scope, runtime memoization, families, TTL, SWR, polling, and the aggregation trap.
|
|
34
|
+
- Read `./references/http-and-invalidation.md` for `AtomHttpApi.Service`, query/mutation ownership, reactivity-key vocabulary, and invalidation.
|
|
35
|
+
- Read `./references/tanstack-start.md` only for its framework-specific provider placement, SSR isolation, hydration, and focus guidance.
|
|
36
|
+
- Read `./references/testing.md` when adding or diagnosing lifecycle tests.
|
|
37
|
+
|
|
38
|
+
## Completion check
|
|
39
|
+
|
|
40
|
+
Confirm one registry boundary, stable atom identity, deliberate TTL/staleness/polling values, matching query and mutation keys, SSR-safe browser access, and tests for every lifecycle behavior changed.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
interface:
|
|
2
|
+
display_name: "Effect Atom Data Fetching"
|
|
3
|
+
short_description: "Cache HTTP data safely with Effect Atom"
|
|
4
|
+
default_prompt: "Use $effect-atom-data-fetching to design or debug Effect Atom HTTP queries, caching, polling, invalidation, and SSR integration in a React app."
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Cache lifecycle
|
|
2
|
+
|
|
3
|
+
## Registry and runtime scope
|
|
4
|
+
|
|
5
|
+
`RegistryProvider` creates one `AtomRegistry` on its first render. Its options do not rebuild that registry later. Provider unmount schedules disposal after a short grace period, so a quick React remount can reuse the same registry; moving or keying the provider still changes the cache boundary.
|
|
6
|
+
|
|
7
|
+
Place one provider around the client application subtree that should share data. Nested or route-local providers create separate caches.
|
|
8
|
+
|
|
9
|
+
An atom runtime and a registry solve different problems:
|
|
10
|
+
|
|
11
|
+
- the registry stores atom nodes, values, subscriptions, idle timers, and finalizers;
|
|
12
|
+
- `Atom.context({ memoMap })` creates runtimes that share `Layer` construction through one `Layer.MemoMap`;
|
|
13
|
+
- the module-level `Atom.runtime` uses Effect's module-level default memo map.
|
|
14
|
+
|
|
15
|
+
Create one client runtime factory when several API/services must share layers:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Layer } from "effect";
|
|
19
|
+
import { Atom } from "effect/unstable/reactivity";
|
|
20
|
+
|
|
21
|
+
export const appAtomRuntime = Atom.context({
|
|
22
|
+
memoMap: Layer.makeMemoMapUnsafe(),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Pass `appAtomRuntime` to each `AtomHttpApi.Service`. Do not create a memo map per query or component. On an SSR server, do not put request-specific authentication or services into a process-global memo map; use a request-scoped atom environment or keep the atom data path client-only.
|
|
27
|
+
|
|
28
|
+
## Stable identity and families
|
|
29
|
+
|
|
30
|
+
Export fixed queries directly. Use `Atom.family` when a parameter selects the resource:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
export const projectAtom = Atom.family((projectId: string) =>
|
|
34
|
+
ApiClient.query("projects", "get", {
|
|
35
|
+
params: { projectId },
|
|
36
|
+
timeToLive: "5 minutes",
|
|
37
|
+
reactivityKeys: { projects: [projectId] },
|
|
38
|
+
}).pipe(Atom.swr({ staleTime: "30 seconds", revalidateOnMount: true })),
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The family must receive a stable key. Prefer a primitive ID. If the key is an object, give it deliberate Effect `Equal`/`Hash` semantics or reuse the same object; repeated object literals can produce distinct family entries.
|
|
43
|
+
|
|
44
|
+
## Three independent clocks
|
|
45
|
+
|
|
46
|
+
| Control | Clock starts | What happens | What it does not mean |
|
|
47
|
+
| ----------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
|
|
48
|
+
| Registry/default idle TTL or query `timeToLive` | When an atom becomes unused | The registry keeps the cached node until idle eviction | The value is fresh during that period |
|
|
49
|
+
| `Atom.swr({ staleTime })` | From the latest success timestamp | A stale value can revalidate automatically on mount or focus while the prior success remains visible | The node survives an unmount long enough to be reused |
|
|
50
|
+
| `Atom.withRefresh(interval)` | While the wrapper is mounted | A timer force-refreshes the source and is canceled on disposal | Fresh requests are skipped |
|
|
51
|
+
|
|
52
|
+
Set retention long enough for the navigation/remount reuse window. `staleTime` cannot rescue a source that idle eviction already removed. A common policy is a longer `timeToLive`, a shorter `staleTime`, and polling only on screens that truly need it.
|
|
53
|
+
|
|
54
|
+
Manual `registry.refresh`, `useAtomRefresh`, invalidation, and `Atom.withRefresh` are forceful. They do not consult SWR freshness. Polling stops when the polling wrapper's lifetime is disposed because its finalizer clears the timer; applying `keepAlive` to that wrapper intentionally keeps polling alive.
|
|
55
|
+
|
|
56
|
+
## The `AsyncResult.all` route reset
|
|
57
|
+
|
|
58
|
+
`AsyncResult.all` returns the first non-success input. Therefore a route aggregate is only as reusable as its least-stable input:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const routeDataAtom = Atom.make((get) =>
|
|
62
|
+
AsyncResult.all({
|
|
63
|
+
project: get(projectAtom("p-1")),
|
|
64
|
+
// Bad if created during render or rebuilt for every route visit:
|
|
65
|
+
preferences: get(makePreferencesAtom()),
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
If `preferences` is a fresh or evicted atom, it starts at `Initial`; the aggregate also looks initial even though `project` is cached. Fix the input's ownership and retention. Define a singleton/family atom outside render and give it a deliberate idle TTL. Memoizing only the `AsyncResult.all` call does not repair an unstable input atom.
|
|
71
|
+
|
|
72
|
+
`AsyncResult.all` also constructs a new success container. Keep aggregation inside a derived atom so the registry controls recomputation instead of rebuilding the container ad hoc in render.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# HTTP queries and invalidation
|
|
2
|
+
|
|
3
|
+
## Build one API service
|
|
4
|
+
|
|
5
|
+
`AtomHttpApi.Service` generates a typed client, a runtime, query atoms, and mutation functions. Pass the shared runtime factory so API services share the intended `Layer.MemoMap`:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { FetchHttpClient } from "effect/unstable/http";
|
|
9
|
+
import { AtomHttpApi } from "effect/unstable/reactivity";
|
|
10
|
+
import { appAtomRuntime } from "./atom-runtime";
|
|
11
|
+
import { Api } from "./api";
|
|
12
|
+
|
|
13
|
+
export const ApiClient = AtomHttpApi.Service()("ApiClient", {
|
|
14
|
+
api: Api,
|
|
15
|
+
httpClient: FetchHttpClient.layer,
|
|
16
|
+
baseUrl: "/api",
|
|
17
|
+
runtime: appAtomRuntime,
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Compile examples against the installed Effect version; the reactivity and HTTP APIs are unstable.
|
|
22
|
+
|
|
23
|
+
## Queries
|
|
24
|
+
|
|
25
|
+
`query(group, endpoint, request)` returns an `Atom<AsyncResult<...>>`. The service internally memoizes request keys with a family. A public `Atom.family` remains useful for expressing domain ownership with a simple, stable parameter and applying one cache policy.
|
|
26
|
+
|
|
27
|
+
Query options have separate roles:
|
|
28
|
+
|
|
29
|
+
- `timeToLive`: finite values apply idle TTL; infinity keeps the query alive;
|
|
30
|
+
- `reactivityKeys`: register the query for refresh after matching invalidation;
|
|
31
|
+
- `serializationKey`: make decoded-only results serializable for hydration; it is not the runtime cache key;
|
|
32
|
+
- `responseMode`: changes the response and error shape.
|
|
33
|
+
|
|
34
|
+
Never place secrets in `serializationKey`, URL state, hydration payloads, or client-visible layers.
|
|
35
|
+
|
|
36
|
+
## Mutations and action ownership
|
|
37
|
+
|
|
38
|
+
Create mutation atoms once, then invoke them from the component or workflow that owns the action:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
export const updateProject = ApiClient.mutation("projects", "update");
|
|
42
|
+
|
|
43
|
+
// In the initiating component:
|
|
44
|
+
const mutate = useAtomSet(updateProject, { mode: "promise" });
|
|
45
|
+
await mutate({
|
|
46
|
+
params: { projectId },
|
|
47
|
+
payload: patch,
|
|
48
|
+
reactivityKeys: { projects: [projectId] },
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
The mutation's `reactivityKeys` are invalidated only after the request succeeds. Failed mutations do not invalidate. Keep navigation, toasts, dialog closure, and optimistic UI at the action owner; keep shared server-state refresh in reactivity keys.
|
|
53
|
+
|
|
54
|
+
## Use one key vocabulary
|
|
55
|
+
|
|
56
|
+
Array keys represent independent keys. Record keys support hierarchical broad-plus-entity invalidation:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const listKeys = { projects: [] };
|
|
60
|
+
const detailKeys = { projects: [projectId] };
|
|
61
|
+
|
|
62
|
+
ApiClient.query("projects", "list", { reactivityKeys: listKeys });
|
|
63
|
+
ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
|
|
64
|
+
|
|
65
|
+
// This mutation invalidates the broad namespace and this entity key.
|
|
66
|
+
await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Record semantics register the property name and each `property:id` combination. Consequently, `{ projects: [projectId] }` is hierarchical: it invalidates both the broad `projects` namespace and the specific `projects:projectId` key. Every record-form project query also subscribed to that broad namespace can refresh. Use this when an entity write may affect lists or aggregates.
|
|
70
|
+
|
|
71
|
+
For exact entity-only invalidation, use namespaced primitive array keys consistently instead:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const projectKey = (id: string) => `project:${id}`;
|
|
75
|
+
|
|
76
|
+
const detailKeys = [projectKey(projectId)];
|
|
77
|
+
const collectionKeys = ["projects"];
|
|
78
|
+
|
|
79
|
+
ApiClient.query("projects", "get", { params: { projectId }, reactivityKeys: detailKeys });
|
|
80
|
+
await mutate({ params: { projectId }, payload: patch, reactivityKeys: detailKeys });
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Use `collectionKeys` for the collection query and for mutations that can change membership or ordering.
|
|
84
|
+
|
|
85
|
+
Standardize key constructors in one module when several endpoints share them; mismatched strings fail silently.
|
|
86
|
+
|
|
87
|
+
Choose invalidation breadth from the server write:
|
|
88
|
+
|
|
89
|
+
- invalidate an exact array-form entity key when only one detail changed;
|
|
90
|
+
- invalidate the collection key, or use hierarchical record keys, when list membership, ordering, totals, or filters can change;
|
|
91
|
+
- invalidate multiple record properties when a write affects related aggregates.
|
|
92
|
+
|
|
93
|
+
Do not both invalidate and manually refresh the same query unless two requests are intentional.
|