@danieljvdm/dev-kit 0.17.0 → 1.0.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 (68) hide show
  1. package/README.md +148 -648
  2. package/package.json +19 -62
  3. package/skills/dev-kit/SKILL.md +42 -213
  4. package/skills/dev-kit/agents/openai.yaml +2 -2
  5. package/skills/dev-kit/references/cloudflare-worker-api.md +37 -0
  6. package/skills/dev-kit/references/default-typescript-repository.md +43 -0
  7. package/skills/dev-kit/references/legacy-eject.md +46 -0
  8. package/skills/dev-kit/references/repository-setup.md +53 -0
  9. package/skills/dev-kit/references/skills.md +35 -0
  10. package/src/bin/dev-kit.ts +153 -134
  11. package/src/catalog-manager.ts +23 -18
  12. package/src/catalog.ts +8 -5
  13. package/src/cli-ui.ts +2 -2
  14. package/src/effect-tsgo.ts +10 -6
  15. package/src/eject.ts +715 -0
  16. package/src/gitignore.ts +6 -3
  17. package/src/legacy-project.ts +67 -0
  18. package/src/oxfmt.ts +1 -4
  19. package/src/oxlint-plugin-anti-slop/LICENSE +21 -0
  20. package/src/oxlint-plugin-anti-slop/index.ts +43 -0
  21. package/src/oxlint-plugin-anti-slop/rules/no-chained-type-assertions.ts +79 -0
  22. package/src/oxlint-plugin-anti-slop/rules/no-conditional-empty-object-spread.ts +51 -0
  23. package/src/oxlint-plugin-anti-slop/rules/no-known-value-widening.ts +267 -0
  24. package/src/oxlint-plugin-anti-slop/rules/no-module-mocking.ts +105 -0
  25. package/src/oxlint-plugin-anti-slop/rules/no-object-parameters.ts +141 -0
  26. package/src/oxlint-plugin-anti-slop/rules/no-reflect-apply.ts +28 -0
  27. package/src/oxlint-plugin-anti-slop/rules/no-reflect-get.ts +28 -0
  28. package/src/oxlint-plugin-anti-slop/rules/no-runtime-typeof.ts +25 -0
  29. package/src/oxlint-plugin-anti-slop/rules/no-shape-in-symbol-names.ts +38 -0
  30. package/src/oxlint-plugin-anti-slop/rules/no-unknown-parameters.ts +86 -0
  31. package/src/oxlint-plugin-anti-slop/rules/no-unknown-returns.ts +125 -0
  32. package/src/oxlint-plugin-anti-slop/rules/no-unknown-type-aliases.ts +73 -0
  33. package/src/oxlint-plugin-anti-slop/rules/no-unsafe-dictionary-type.ts +139 -0
  34. package/src/oxlint-plugin-anti-slop/rules/no-widen-then-assert.ts +396 -0
  35. package/src/oxlint-plugin-anti-slop/rules/require-safety-comment-for-type-assertion.ts +61 -0
  36. package/src/oxlint-plugin-anti-slop/runtime.js +1209 -0
  37. package/src/oxlint-plugin-anti-slop/shared/dictionary-types.ts +555 -0
  38. package/src/oxlint-plugin-anti-slop/shared/reflect-method.ts +40 -0
  39. package/src/oxlint.ts +26 -14
  40. package/src/package-skill-source.ts +17 -25
  41. package/src/path-digest.ts +62 -1
  42. package/src/project-package.ts +14 -12
  43. package/src/project-skills.ts +722 -0
  44. package/src/tool-metadata.ts +0 -2
  45. package/src/vendor.ts +35 -21
  46. package/src/vite-plus.ts +1 -3
  47. package/dev-kit.example.jsonc +0 -22
  48. package/schema/dev-kit.schema.json +0 -218
  49. package/schema/skill-sources.schema.json +0 -83
  50. package/src/index.ts +0 -125
  51. package/src/manifest.ts +0 -216
  52. package/src/oxfmt.js +0 -23
  53. package/src/oxlint-plugin-effect.d.ts +0 -17
  54. package/src/oxlint-plugin-style.d.ts +0 -8
  55. package/src/oxlint.js +0 -94
  56. package/src/project-state.ts +0 -122
  57. package/src/scaffold.ts +0 -79
  58. package/src/skill-manager.ts +0 -515
  59. package/src/sync.ts +0 -1919
  60. package/src/tool-ignore-patterns.js +0 -9
  61. package/src/vite-plus-dependency.ts +0 -69
  62. package/src/vite-plus-hooks.ts +0 -175
  63. package/src/vite-plus-workflow.ts +0 -82
  64. package/src/vite-plus.js +0 -88
  65. package/src/worktrunk-config.ts +0 -88
  66. package/templates/AGENTS.md +0 -11
  67. package/templates/vite-plus/github-actions-check.yml +0 -51
  68. package/templates/worktrunk/wt.toml +0 -27
package/src/sync.ts DELETED
@@ -1,1919 +0,0 @@
1
- import { Cause, Effect, FileSystem, Path, Schema, SchemaGetter, Stream } from "effect";
2
- import { ChildProcess } from "effect/unstable/process";
3
- import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
4
-
5
- import {
6
- loadSkillCatalog,
7
- resolveSkillSources,
8
- type CatalogSkill,
9
- type ResolvedSkillSource,
10
- } from "./catalog.ts";
11
- import { printDetail, printStatus, withSpinner } from "./cli-ui.ts";
12
- import { applyEffectSourcePlan, planEffectSource, type EffectSourcePlan } from "./effect-source.ts";
13
- import {
14
- applyEffectTsgoPatchPlan,
15
- planEffectTsgoPatch,
16
- type EffectTsgoPatchPlan,
17
- } from "./effect-tsgo.ts";
18
- import { maybePruneGlobalCache } from "./global-cache.ts";
19
- import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
20
- import { observeSymbolicLink } from "./node-symbolic-link.ts";
21
- import { resolvePackageSkillSelector } from "./package-skill-source.ts";
22
- import {
23
- digestFileContent,
24
- digestSymlinkTarget,
25
- digestText,
26
- observePath,
27
- type ObservedPath,
28
- } from "./path-digest.ts";
29
- import {
30
- detectPackageManager,
31
- PACKAGE_MANAGER_COMMANDS,
32
- readDirectDependencyNames,
33
- readWorkspaceDependencyNames,
34
- readProjectPackage,
35
- type PackageManagerName,
36
- } from "./project-package.ts";
37
- import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-process-lock.ts";
38
- import {
39
- AppliedStateSchema,
40
- DevKitLockSchema,
41
- EffectSourceLockSchema,
42
- EffectTsgoLockSchema,
43
- ManagedOutputSchema,
44
- type AppliedState,
45
- type DevKitLock,
46
- type ManagedAgentInstructionsOutput,
47
- type ManagedClaudeInstructionsOutput,
48
- type ManagedOutput,
49
- type ManagedSkillOutput,
50
- type OwnershipReceipt,
51
- } from "./project-state.ts";
52
- import { applyScaffoldPlan, type ScaffoldPlan } from "./scaffold.ts";
53
- import { parseSkillSelector } from "./skill-selector.ts";
54
- import { DEV_KIT_VERSION } from "./tool-metadata.ts";
55
- import {
56
- applyVitePlusHooksPlan,
57
- planVitePlusHooks,
58
- type VitePlusHooksPlan,
59
- } from "./vite-plus-hooks.ts";
60
- import { planVitePlusWorkflow } from "./vite-plus-workflow.ts";
61
- import { planWorktrunkConfig } from "./worktrunk-config.ts";
62
-
63
- export type SyncOptions = {
64
- readonly manifestPath?: string;
65
- readonly projectDir?: string;
66
- readonly lockfilePath?: string;
67
- readonly statePath?: string;
68
- readonly dryRun?: boolean;
69
- readonly locked?: boolean;
70
- };
71
-
72
- type SkillCatalog = Readonly<Record<string, ReadonlyArray<string>>>;
73
-
74
- type ManagedPath = {
75
- readonly absolute: string;
76
- readonly relative: string;
77
- };
78
-
79
- type DesiredSkillOutput =
80
- | (Omit<ManagedSkillOutput, "mode" | "kind"> & {
81
- readonly mode: "copy";
82
- readonly kind: "directory";
83
- readonly source: string;
84
- readonly destination: string;
85
- })
86
- | (Omit<ManagedSkillOutput, "mode" | "kind"> & {
87
- readonly mode: "symlink";
88
- readonly kind: "symlink";
89
- readonly source: string;
90
- readonly destination: string;
91
- readonly linkTarget: string;
92
- });
93
-
94
- type DesiredAgentInstructionsOutput = ManagedAgentInstructionsOutput & {
95
- readonly content: string;
96
- readonly destination: string;
97
- };
98
-
99
- type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
100
- readonly destination: string;
101
- readonly linkTarget: string;
102
- };
103
-
104
- type DesiredOutput =
105
- | DesiredSkillOutput
106
- | DesiredAgentInstructionsOutput
107
- | DesiredClaudeInstructionsOutput;
108
-
109
- type SkillPlanAction =
110
- | {
111
- readonly action: "create" | "update";
112
- readonly desired: DesiredOutput;
113
- readonly observed: ObservedPath;
114
- readonly stagedContent?: string;
115
- }
116
- | {
117
- readonly action: "remove";
118
- readonly previous: OwnershipReceipt;
119
- readonly destination: string;
120
- readonly observed: ObservedPath;
121
- readonly stagedContent?: string;
122
- }
123
- | {
124
- readonly action: "unchanged";
125
- readonly desired: DesiredOutput;
126
- readonly observed: ObservedPath;
127
- readonly adopted: boolean;
128
- }
129
- | {
130
- readonly action: "conflict";
131
- readonly path: string;
132
- readonly reason: string;
133
- };
134
-
135
- export type SkillPlan = {
136
- readonly projectDir: string;
137
- readonly lockfilePath: string;
138
- readonly statePath: string;
139
- readonly actions: ReadonlyArray<SkillPlanAction>;
140
- readonly effectSource?: EffectSourcePlan;
141
- readonly effectTsgo?: EffectTsgoPatchPlan;
142
- readonly vitePlusHooks?: VitePlusHooksPlan;
143
- readonly vitePlusWorkflow?: ScaffoldPlan;
144
- readonly worktrunkConfig?: ScaffoldPlan;
145
- readonly nextLock: DevKitLock;
146
- readonly nextState: AppliedState;
147
- readonly metadataChanged: boolean;
148
- };
149
-
150
- class ManifestNotFoundError extends Schema.TaggedError<ManifestNotFoundError>()(
151
- "ManifestNotFoundError",
152
- { path: Schema.String },
153
- ) {
154
- override get message() {
155
- return `manifest not found: ${this.path}`;
156
- }
157
- }
158
-
159
- class StructuredFileError extends Schema.TaggedError<StructuredFileError>()("StructuredFileError", {
160
- path: Schema.String,
161
- message: Schema.String,
162
- }) {}
163
-
164
- class UnknownSkillOrFamilyError extends Schema.TaggedError<UnknownSkillOrFamilyError>()(
165
- "UnknownSkillOrFamilyError",
166
- { name: Schema.String, known: Schema.Array(Schema.String) },
167
- ) {
168
- override get message() {
169
- return `unknown skill or family "${this.name}". Known values: ${this.known.join(", ")}`;
170
- }
171
- }
172
-
173
- class InvalidSkillCatalogError extends Schema.TaggedError<InvalidSkillCatalogError>()(
174
- "InvalidSkillCatalogError",
175
- { family: Schema.String, message: Schema.String },
176
- ) {}
177
-
178
- class CommandError extends Schema.TaggedError<CommandError>()("CommandError", {
179
- command: Schema.String,
180
- exitCode: Schema.Int,
181
- output: Schema.String,
182
- }) {
183
- override get message() {
184
- return this.output.length > 0
185
- ? `${this.command} exited with code ${this.exitCode}: ${this.output}`
186
- : `${this.command} exited with code ${this.exitCode}`;
187
- }
188
- }
189
-
190
- class UnsafeManagedPathError extends Schema.TaggedError<UnsafeManagedPathError>()(
191
- "UnsafeManagedPathError",
192
- { path: Schema.String, reason: Schema.String },
193
- ) {
194
- override get message() {
195
- return `unsafe managed path "${this.path}": ${this.reason}`;
196
- }
197
- }
198
-
199
- class InvalidProjectStateError extends Schema.TaggedError<InvalidProjectStateError>()(
200
- "InvalidProjectStateError",
201
- { message: Schema.String },
202
- ) {}
203
-
204
- class LockedPlanMismatchError extends Schema.TaggedError<LockedPlanMismatchError>()(
205
- "LockedPlanMismatchError",
206
- { message: Schema.String },
207
- ) {}
208
-
209
- class PlanConflictError extends Schema.TaggedError<PlanConflictError>()("PlanConflictError", {
210
- conflicts: Schema.Array(Schema.String),
211
- }) {
212
- override get message() {
213
- const heading = `plan has ${this.conflicts.length} conflict${this.conflicts.length === 1 ? "" : "s"}`;
214
-
215
- return `${heading}:\n${this.conflicts.map((conflict) => ` ${conflict}`).join("\n")}`;
216
- }
217
- }
218
-
219
- class ApplyRaceError extends Schema.TaggedError<ApplyRaceError>()("ApplyRaceError", {
220
- path: Schema.String,
221
- }) {
222
- override get message() {
223
- return `managed path changed after planning: ${this.path}`;
224
- }
225
- }
226
-
227
- const fromJsonString = <S extends Schema.Constraint>(schema: S, space?: number) =>
228
- space === undefined
229
- ? Schema.fromJsonString(schema)
230
- : Schema.String.pipe(
231
- Schema.decodeTo(Schema.toCodecJson(schema), {
232
- decode: SchemaGetter.parseJson(),
233
- encode: SchemaGetter.stringifyJson({ space }),
234
- }),
235
- );
236
-
237
- const DevKitSetupSchema = Schema.Struct({
238
- effectSource: Schema.optional(EffectSourceLockSchema),
239
- effectTsgo: Schema.optional(EffectTsgoLockSchema),
240
- });
241
- const OutputOwnershipIdentitySchema = Schema.Union([
242
- Schema.Struct({
243
- resourceId: Schema.String,
244
- path: Schema.String,
245
- mode: Schema.Literals(["copy", "symlink"]),
246
- kind: Schema.Literals(["directory", "symlink"]),
247
- skill: Schema.String,
248
- target: Schema.Literals(["agents", "claude", "opencode"]),
249
- }),
250
- Schema.Struct({
251
- resourceId: Schema.String,
252
- path: Schema.String,
253
- mode: Schema.Literals(["copy", "symlink"]),
254
- kind: Schema.Literals(["file", "symlink"]),
255
- sourcePath: Schema.String,
256
- }),
257
- ]);
258
- const encodeAppliedStateJson = Schema.encodeSync(fromJsonString(AppliedStateSchema));
259
- const encodeDevKitLockJson = Schema.encodeSync(fromJsonString(DevKitLockSchema));
260
- const encodeDevKitLockPrettyJson = Schema.encodeSync(fromJsonString(DevKitLockSchema, 2));
261
- const encodeDevKitSetupJson = Schema.encodeSync(fromJsonString(DevKitSetupSchema));
262
- const encodeManifestJson = Schema.encodeSync(fromJsonString(DevKitManifestSchema));
263
- const encodeManagedOutputJson = Schema.encodeSync(fromJsonString(ManagedOutputSchema));
264
- const encodeOutputOwnershipIdentityJson = Schema.encodeSync(
265
- fromJsonString(OutputOwnershipIdentitySchema),
266
- );
267
- const encodePlanSnapshotJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
268
- const encodeAppliedStatePrettyJson = Schema.encodeSync(fromJsonString(AppliedStateSchema, 2));
269
-
270
- const SKILL_FAMILIES: SkillCatalog = {
271
- effect: [
272
- "effect-ts",
273
- "effect-architecture-audit",
274
- "build-effect-apis",
275
- "effect-atom-state",
276
- "build-effect-clis",
277
- ],
278
- };
279
-
280
- export const DEFAULT_MANIFEST = "dev-kit.jsonc";
281
- const DEFAULT_LOCKFILE = "dev-kit.lock.json";
282
- const DEFAULT_STATE = ".dev-kit/state.json";
283
- const AGENT_INSTRUCTIONS_TEMPLATE = "templates/AGENTS.md";
284
- const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
285
- const EFFECT_INSTRUCTIONS_PLACEHOLDER = "{{EFFECT_INSTRUCTIONS}}";
286
- const PROJECT_COMMAND_POLICY_PLACEHOLDER = "{{PROJECT_COMMAND_POLICY}}";
287
- const AGENT_INSTRUCTION_MARKERS = [
288
- { start: "<!-- DEV KIT START -->", end: "<!-- DEV KIT END -->" },
289
- // Legacy Dev Kit releases copied this upstream section into AGENTS.md. Keep
290
- // recognizing it so an owned section can be removed during migration.
291
- { start: "<!--VITE PLUS START-->", end: "<!--VITE PLUS END-->" },
292
- ] as const;
293
-
294
- type ManagedInstructionRange = {
295
- readonly start: number;
296
- readonly end: number;
297
- readonly content: string;
298
- };
299
-
300
- type ManagedInstructionInspection =
301
- | {
302
- readonly kind: "valid";
303
- readonly ranges: ReadonlyArray<ManagedInstructionRange>;
304
- readonly content?: string;
305
- }
306
- | { readonly kind: "invalid"; readonly reason: string };
307
-
308
- const findOccurrences = (content: string, marker: string): ReadonlyArray<number> => {
309
- const positions: Array<number> = [];
310
- let offset = 0;
311
-
312
- while (offset < content.length) {
313
- const position = content.indexOf(marker, offset);
314
-
315
- if (position === -1) break;
316
- positions.push(position);
317
- offset = position + marker.length;
318
- }
319
-
320
- return positions;
321
- };
322
-
323
- const inspectManagedInstructionSections = (content: string): ManagedInstructionInspection => {
324
- const ranges: Array<ManagedInstructionRange> = [];
325
-
326
- for (const markers of AGENT_INSTRUCTION_MARKERS) {
327
- const starts = findOccurrences(content, markers.start);
328
- const ends = findOccurrences(content, markers.end);
329
-
330
- if (starts.length === 0 && ends.length === 0) continue;
331
- if (
332
- starts.length !== 1 ||
333
- ends.length !== 1 ||
334
- starts[0] === undefined ||
335
- ends[0] === undefined
336
- ) {
337
- return {
338
- kind: "invalid",
339
- reason: `expected exactly one ${markers.start}/${markers.end} marker pair`,
340
- };
341
- }
342
- if (starts[0] >= ends[0]) {
343
- return { kind: "invalid", reason: `${markers.end} appears before ${markers.start}` };
344
- }
345
- const end = ends[0] + markers.end.length;
346
-
347
- ranges.push({ start: starts[0], end, content: content.slice(starts[0], end) });
348
- }
349
- ranges.sort((left, right) => left.start - right.start);
350
- for (let index = 1; index < ranges.length; index += 1) {
351
- const previous = ranges[index - 1];
352
- const current = ranges[index];
353
-
354
- if (previous !== undefined && current !== undefined && current.start < previous.end) {
355
- return { kind: "invalid", reason: "managed instruction marker pairs overlap" };
356
- }
357
- }
358
-
359
- return {
360
- kind: "valid",
361
- ranges,
362
- ...(ranges.length === 0
363
- ? {}
364
- : { content: `${ranges.map((range) => range.content.trim()).join("\n\n")}\n` }),
365
- };
366
- };
367
-
368
- const removeManagedInstructionSections = (
369
- content: string,
370
- ranges: ReadonlyArray<ManagedInstructionRange>,
371
- ): string => {
372
- const first = ranges[0];
373
- const last = ranges.at(-1);
374
- const hasOnlyManagedSeparators = ranges.every((range, index) => {
375
- const next = ranges[index + 1];
376
-
377
- return next === undefined || /^\s*$/.test(content.slice(range.end, next.start));
378
- });
379
-
380
- if (first?.start === 0 && last !== undefined && hasOnlyManagedSeparators) {
381
- let end = last.end;
382
-
383
- if (content.startsWith("\r\n", end)) end += 2;
384
- else if (content.startsWith("\n", end)) end += 1;
385
-
386
- return content.slice(end);
387
- }
388
- let result = content;
389
-
390
- for (const range of [...ranges].reverse()) {
391
- result = result.slice(0, range.start) + result.slice(range.end);
392
- }
393
-
394
- return result;
395
- };
396
-
397
- const prependManagedInstructionSections = (content: string, managed: string): string => {
398
- if (content.trim().length === 0) return managed;
399
-
400
- return `${managed}${content}`;
401
- };
402
-
403
- const reconcileManagedInstructionSections = (
404
- content: string,
405
- inspection: Extract<ManagedInstructionInspection, { readonly kind: "valid" }>,
406
- managed: string,
407
- ): string =>
408
- prependManagedInstructionSections(
409
- removeManagedInstructionSections(content, inspection.ranges),
410
- managed,
411
- );
412
-
413
- const renderVitePlusCommandPolicy = (
414
- scripts: Readonly<Record<string, string>>,
415
- managesQualityConfig: boolean,
416
- ): string => {
417
- const hasCheck = managesQualityConfig || scripts.check !== undefined;
418
- const hasTypecheck = managesQualityConfig || scripts.typecheck !== undefined;
419
-
420
- return [
421
- "## Project command policy",
422
- "",
423
- "Vite+ is the unified toolchain and command authority for this repository. It wraps Vite, Rolldown, Vitest, tsdown, Oxlint, Oxfmt, and Vite Task behind the `vp` CLI; Vite+ is distinct from Vite.",
424
- "",
425
- "Run `vp help` for available commands and `vp <command> --help` for command-specific options. Documentation is available locally in `node_modules/vite-plus/docs` and online at https://viteplus.dev/guide/.",
426
- "",
427
- "Use these repository commands:",
428
- "",
429
- "- Install dependencies: `vp install`.",
430
- ...(hasCheck ? ["- Full validation: `vp run check`."] : []),
431
- "- Static checks: `vp check`.",
432
- "- Format check: `vp fmt --check`; format fixes: `vp fmt`.",
433
- "- Lint only: `vp lint`; lint fixes: `vp lint --fix`.",
434
- "- Tests only: `vp test`.",
435
- ...(hasTypecheck ? ["- Typecheck only: `vp run typecheck`."] : []),
436
- "- Other repository tasks and package scripts: `vp run <task>`.",
437
- "- Toolchain or runtime troubleshooting: run `vp env doctor` and include its output when asking for help.",
438
- "",
439
- "Do not use `bun run`, `npm run`, `pnpm run`, or `yarn run` in this repository. Do not invoke underlying tools such as `tsc`, `vitest`, `oxlint`, or `oxfmt` directly; use the Vite+ entry points above.",
440
- ].join("\n");
441
- };
442
-
443
- const renderPackageScriptCommandPolicy = (
444
- manager: PackageManagerName | undefined,
445
- scripts: Readonly<Record<string, string>>,
446
- ): string => {
447
- const installer = manager === undefined ? undefined : PACKAGE_MANAGER_COMMANDS[manager];
448
- const entries = [
449
- ["check", "Full validation"],
450
- ["format:check", "Format check"],
451
- ["format", "Format"],
452
- ["lint", "Lint"],
453
- ["test", "Tests"],
454
- ["typecheck", "Typecheck"],
455
- ] as const;
456
- const commands = entries.flatMap(([script, label]) =>
457
- scripts[script] === undefined ? [] : [`- ${label}: \`bun run ${script}\`.`],
458
- );
459
- const knownScripts = new Set(entries.map(([script]) => script));
460
- const additionalCommands = Object.keys(scripts)
461
- .filter(
462
- (script) =>
463
- !knownScripts.has(script as (typeof entries)[number][0]) &&
464
- /^(?:check|validate|fmt|format|lint|test|type-?check)(?::|$)/.test(script),
465
- )
466
- .sort()
467
- .map((script) => `- Script \`${script}\`: \`bun run ${script}\`.`);
468
- const qualityCommands = [...commands, ...additionalCommands];
469
-
470
- return [
471
- "## Project command policy",
472
- "",
473
- "Bun is the package-script runner for this repository:",
474
- "",
475
- ...(installer === undefined
476
- ? []
477
- : [`- Install dependencies with ${installer.label}: \`${installer.install}\`.`]),
478
- ...qualityCommands,
479
- ...(qualityCommands.length === 0 ? ["- No root quality scripts are currently declared."] : []),
480
- "",
481
- "Run only declared scripts through `bun run <script>`. Do not use `npm run`, `pnpm run`, or `yarn run`, invent missing scripts, or invoke underlying tools such as `tsc`, `vitest`, `eslint`, or `prettier` directly. The Bun script-runner requirement does not choose the package manager used to install dependencies.",
482
- ].join("\n");
483
- };
484
-
485
- const resolvePackageRoot = Effect.fn("resolvePackageRoot")(function* () {
486
- const path = yield* Path.Path;
487
- const scriptPath = yield* path.fromFileUrl(new URL(import.meta.url));
488
-
489
- return path.resolve(path.dirname(scriptPath), "..");
490
- });
491
-
492
- const runCommand = Effect.fn("runCommand")(function* (
493
- cwd: string,
494
- command: string,
495
- args: ReadonlyArray<string>,
496
- ) {
497
- const formatted = [command, ...args].join(" ");
498
- const child = yield* ChildProcess.make(command, args, { cwd, stderr: "pipe", stdout: "pipe" });
499
- const [output, exitCode] = yield* Effect.all([
500
- Stream.mkString(Stream.decodeText(child.all)),
501
- child.exitCode,
502
- ]);
503
- const trimmed = output.trim();
504
-
505
- if (exitCode !== 0) {
506
- return yield* CommandError.make({ command: formatted, exitCode, output: trimmed });
507
- }
508
-
509
- return trimmed;
510
- });
511
-
512
- const resolveGitRoot = Effect.fn("resolveGitRoot")(function* (cwd: string) {
513
- return yield* runCommand(cwd, "git", ["rev-parse", "--show-toplevel"]);
514
- });
515
-
516
- const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
517
- filePath: string,
518
- raw: string,
519
- schema: Schema.ConstraintDecoder<A>,
520
- options: { readonly rejectExcessProperties?: boolean } = {},
521
- ) {
522
- const errors: Array<ParseError> = [];
523
- const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
524
- const first = errors[0];
525
-
526
- if (first !== undefined) {
527
- return yield* StructuredFileError.make({
528
- path: filePath,
529
- message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
530
- });
531
- }
532
-
533
- return yield* Schema.decodeUnknownEffect(
534
- schema,
535
- options.rejectExcessProperties ? { onExcessProperty: "error" } : undefined,
536
- )(parsed).pipe(
537
- Effect.mapError((cause) =>
538
- StructuredFileError.make({ path: filePath, message: cause.message }),
539
- ),
540
- );
541
- });
542
-
543
- const readManifest = Effect.fn("readManifest")(function* (manifestPath: string) {
544
- const fs = yield* FileSystem.FileSystem;
545
-
546
- if (!(yield* fs.exists(manifestPath))) {
547
- return yield* ManifestNotFoundError.make({ path: manifestPath });
548
- }
549
- const raw = yield* fs.readFileString(manifestPath);
550
-
551
- return yield* parseStructuredFile(manifestPath, raw, DevKitManifestSchema, {
552
- rejectExcessProperties: true,
553
- });
554
- });
555
-
556
- const readOptionalStructuredFile = Effect.fn("readOptionalStructuredFile")(function* <A>(
557
- filePath: string,
558
- schema: Schema.ConstraintDecoder<A>,
559
- ) {
560
- const fs = yield* FileSystem.FileSystem;
561
-
562
- if (!(yield* fs.exists(filePath))) {
563
- return undefined;
564
- }
565
-
566
- return yield* parseStructuredFile(filePath, yield* fs.readFileString(filePath), schema);
567
- });
568
-
569
- const expandSelection = (
570
- include: ReadonlyArray<string>,
571
- exclude: ReadonlyArray<string>,
572
- availableSkills: ReadonlyArray<string>,
573
- skillFamilies: SkillCatalog,
574
- ) => {
575
- const known = [...new Set([...Object.keys(skillFamilies), ...availableSkills])].sort();
576
- const selected = new Set<string>();
577
-
578
- for (const name of include) {
579
- if (skillFamilies[name]) {
580
- for (const skill of skillFamilies[name]) selected.add(skill);
581
- } else if (availableSkills.includes(name) || parseSkillSelector(name)?.type === "package") {
582
- selected.add(name);
583
- } else {
584
- return Effect.fail(UnknownSkillOrFamilyError.make({ name, known }));
585
- }
586
- }
587
- for (const name of exclude) {
588
- const family = skillFamilies[name];
589
-
590
- if (family) for (const skill of family) selected.delete(skill);
591
- else selected.delete(name);
592
- }
593
-
594
- return Effect.succeed([...selected].sort());
595
- };
596
-
597
- const portablePath = (path: Path.Path, value: string): string =>
598
- path.sep === "/" ? value : value.split(path.sep).join("/");
599
-
600
- const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
601
- projectDir: string,
602
- candidate: string,
603
- ) {
604
- const path = yield* Path.Path;
605
-
606
- if (candidate.length === 0 || path.isAbsolute(candidate)) {
607
- return yield* UnsafeManagedPathError.make({
608
- path: candidate,
609
- reason: "must be a non-empty project-relative path",
610
- });
611
- }
612
- const absolute = path.resolve(projectDir, candidate);
613
- const relative = path.relative(projectDir, absolute);
614
-
615
- if (
616
- relative.length === 0 ||
617
- relative === ".." ||
618
- relative.startsWith(`..${path.sep}`) ||
619
- path.isAbsolute(relative)
620
- ) {
621
- return yield* UnsafeManagedPathError.make({
622
- path: candidate,
623
- reason: "resolves outside the project",
624
- });
625
- }
626
-
627
- const segments = relative.split(path.sep);
628
- let ancestor = projectDir;
629
-
630
- for (const segment of segments.slice(0, -1)) {
631
- ancestor = path.join(ancestor, segment);
632
- const target = yield* observeSymbolicLink(ancestor);
633
-
634
- if (target.kind === "symlink") {
635
- return yield* UnsafeManagedPathError.make({
636
- path: candidate,
637
- reason: `ancestor is a symlink: ${portablePath(path, path.relative(projectDir, ancestor))}`,
638
- });
639
- }
640
- }
641
-
642
- return { absolute, relative: portablePath(path, relative) } satisfies ManagedPath;
643
- });
644
-
645
- const pathsOverlap = (left: string, right: string): boolean =>
646
- left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
647
-
648
- const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
649
- projectDir: string,
650
- reserved: ReadonlyArray<{ readonly label: string; readonly path: string }>,
651
- outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
652
- ) {
653
- const outputPaths = new Set<string>();
654
-
655
- for (const output of outputs) {
656
- outputPaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
657
- }
658
-
659
- for (let index = 0; index < reserved.length; index += 1) {
660
- const current = reserved[index];
661
-
662
- if (current === undefined) continue;
663
- for (const other of reserved.slice(index + 1)) {
664
- if (pathsOverlap(current.path, other.path)) {
665
- return yield* InvalidProjectStateError.make({
666
- message: `${current.label} path ${current.path} overlaps ${other.label} path ${other.path}`,
667
- });
668
- }
669
- }
670
- for (const outputPath of outputPaths) {
671
- if (pathsOverlap(current.path, outputPath)) {
672
- return yield* InvalidProjectStateError.make({
673
- message: `${current.label} path ${current.path} overlaps managed output ${outputPath}`,
674
- });
675
- }
676
- }
677
- }
678
- });
679
-
680
- const outputIdentity = (output: ManagedOutput) => encodeManagedOutputJson(output);
681
-
682
- const outputOwnershipIdentity = (output: ManagedOutput) =>
683
- encodeOutputOwnershipIdentityJson(
684
- "skill" in output
685
- ? {
686
- resourceId: output.resourceId,
687
- path: output.path,
688
- mode: output.mode,
689
- kind: output.kind,
690
- skill: output.skill,
691
- target: output.target,
692
- }
693
- : {
694
- resourceId: output.resourceId,
695
- path: output.path,
696
- mode: output.mode,
697
- kind: output.kind,
698
- sourcePath: output.sourcePath,
699
- },
700
- );
701
-
702
- const validateInventory = Effect.fn("validateManagedInventory")(function* (
703
- projectDir: string,
704
- outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
705
- label: string,
706
- ) {
707
- const ids = new Set<string>();
708
- const paths = new Set<string>();
709
- const sortedPaths: Array<string> = [];
710
-
711
- for (const output of outputs) {
712
- if (ids.has(output.resourceId)) {
713
- return yield* InvalidProjectStateError.make({
714
- message: `${label} contains duplicate resource id ${output.resourceId}`,
715
- });
716
- }
717
- if (paths.has(output.path)) {
718
- return yield* InvalidProjectStateError.make({
719
- message: `${label} contains duplicate path ${output.path}`,
720
- });
721
- }
722
- ids.add(output.resourceId);
723
- paths.add(output.path);
724
- sortedPaths.push((yield* resolveManagedPath(projectDir, output.path)).relative);
725
- }
726
- sortedPaths.sort();
727
- for (let index = 1; index < sortedPaths.length; index += 1) {
728
- const previous = sortedPaths[index - 1];
729
- const current = sortedPaths[index];
730
-
731
- if (previous === undefined || current === undefined) continue;
732
- if (current.startsWith(`${previous}/`)) {
733
- return yield* InvalidProjectStateError.make({
734
- message: `${label} contains overlapping paths ${previous} and ${current}`,
735
- });
736
- }
737
- }
738
- });
739
-
740
- const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(function* (
741
- projectDir: string,
742
- outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
743
- ) {
744
- const uniquePaths = new Set<string>();
745
-
746
- for (const output of outputs) {
747
- uniquePaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
748
- }
749
- const sortedPaths = [...uniquePaths].sort();
750
-
751
- for (let index = 1; index < sortedPaths.length; index += 1) {
752
- const previous = sortedPaths[index - 1];
753
- const current = sortedPaths[index];
754
-
755
- if (previous === undefined || current === undefined) continue;
756
- if (current.startsWith(`${previous}/`)) {
757
- return yield* InvalidProjectStateError.make({
758
- message: `desired and previously owned paths overlap: ${previous} and ${current}`,
759
- });
760
- }
761
- }
762
- });
763
-
764
- const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
765
- packageRoot: string,
766
- projectDir: string,
767
- sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
768
- usesRecommendedVitePlusTasks: boolean,
769
- targets: ReturnType<typeof normalizeManifest>["targets"],
770
- ) {
771
- const fs = yield* FileSystem.FileSystem;
772
- const path = yield* Path.Path;
773
- const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
774
-
775
- if ((yield* observePath(templatePath)).kind !== "file") {
776
- return yield* InvalidProjectStateError.make({
777
- message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
778
- });
779
- }
780
- const template = yield* fs.readFileString(templatePath);
781
-
782
- if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
783
- return yield* InvalidProjectStateError.make({
784
- message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
785
- });
786
- }
787
- if (!template.includes(EFFECT_INSTRUCTIONS_PLACEHOLDER)) {
788
- return yield* InvalidProjectStateError.make({
789
- message: `dev-kit agent instructions template is missing ${EFFECT_INSTRUCTIONS_PLACEHOLDER}`,
790
- });
791
- }
792
- if (!template.includes(PROJECT_COMMAND_POLICY_PLACEHOLDER)) {
793
- return yield* InvalidProjectStateError.make({
794
- message: `dev-kit agent instructions template is missing ${PROJECT_COMMAND_POLICY_PLACEHOLDER}`,
795
- });
796
- }
797
-
798
- const devKitSkill = sourceBySkill.get("dev-kit");
799
- const devKitTarget = (["agents", "claude", "opencode"] as const)
800
- .map((name) => targets[name])
801
- .find((target) => target.enabled);
802
- const devKitSkillPath =
803
- devKitSkill === undefined
804
- ? "node_modules/@danieljvdm/dev-kit/skills/dev-kit/SKILL.md"
805
- : devKitTarget !== undefined
806
- ? portablePath(path, path.join(devKitTarget.path, "dev-kit", "SKILL.md"))
807
- : portablePath(
808
- path,
809
- path.relative(
810
- projectDir,
811
- path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
812
- ),
813
- );
814
- const directDependencyNames = yield* readDirectDependencyNames(projectDir);
815
- const usesVitePlus = directDependencyNames.includes("vite-plus");
816
- const effectGuideInstructions =
817
- directDependencyNames.includes("effect") &&
818
- (yield* observePath(path.join(projectDir, "node_modules", "effect", "AGENTS.md"))).kind ===
819
- "file"
820
- ? `# Learning more about the Effect
821
-
822
- This repository uses the Effect Typescript library.
823
-
824
- Before writing any Effect code, first read \`node_modules/effect/AGENTS.md\`
825
- **completely**, and follow the links in the file when required.
826
-
827
- If you need to learn more about particular Effect apis and concepts that the
828
- guide doesn't cover, search through the source code in \`node_modules/effect/src\`.
829
-
830
- `
831
- : "";
832
- const atomBoundaryInstructions = (yield* readWorkspaceDependencyNames(projectDir)).includes(
833
- "@effect/atom-react",
834
- )
835
- ? `# Effect Atom client boundary
836
-
837
- This repository consumes APIs through Effect Atom clients (\`@effect/atom-react\`).
838
- Keep business logic in Effect: compose multi-step client workflows as atoms,
839
- declare cross-query invalidation as reactivity keys on mutations, and keep
840
- promise-mode dispatches at the React boundary logic-free — no \`.then\` chains
841
- in components or routes.
842
-
843
- `
844
- : "";
845
- const effectInstructions = `${effectGuideInstructions}${atomBoundaryInstructions}`;
846
- const projectPackage = yield* readProjectPackage(projectDir).pipe(
847
- Effect.catchTag("ProjectPackageError", (error) =>
848
- error.message.startsWith("package.json not found:") ? Effect.void : Effect.fail(error),
849
- ),
850
- );
851
- const manager = yield* detectPackageManager(projectDir, projectPackage?.packageManager);
852
- const commandPolicy = usesVitePlus
853
- ? renderVitePlusCommandPolicy(projectPackage?.scripts ?? {}, usesRecommendedVitePlusTasks)
854
- : renderPackageScriptCommandPolicy(manager, projectPackage?.scripts ?? {});
855
- const devKitInstructions = template
856
- .replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath)
857
- .replaceAll(EFFECT_INSTRUCTIONS_PLACEHOLDER, effectInstructions)
858
- .replaceAll(PROJECT_COMMAND_POLICY_PLACEHOLDER, commandPolicy)
859
- .trimEnd();
860
-
861
- return `${devKitInstructions}\n`;
862
- });
863
-
864
- const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
865
- packageRoot: string,
866
- projectDir: string,
867
- sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
868
- skills: ReadonlyArray<CatalogSkill>,
869
- setup: ReturnType<typeof normalizeManifest>["setup"],
870
- targets: ReturnType<typeof normalizeManifest>["targets"],
871
- ) {
872
- const path = yield* Path.Path;
873
- const outputs: Array<DesiredOutput> = [];
874
-
875
- if (setup.agentInstructions.enabled) {
876
- const managed = yield* resolveManagedPath(projectDir, "AGENTS.md");
877
- const content = yield* renderAgentInstructions(
878
- packageRoot,
879
- projectDir,
880
- sourceBySkill,
881
- setup.vitePlus.workflow.enabled,
882
- targets,
883
- );
884
-
885
- outputs.push({
886
- resourceId: "setup:agent-instructions",
887
- path: managed.relative,
888
- sourcePath: AGENT_INSTRUCTIONS_TEMPLATE,
889
- mode: "copy",
890
- kind: "file",
891
- digest: yield* digestFileContent(content),
892
- destination: managed.absolute,
893
- content,
894
- });
895
- }
896
- if (setup.claudeInstructions.enabled) {
897
- const source = yield* resolveManagedPath(projectDir, "AGENTS.md");
898
- const sourceObservation = setup.agentInstructions.enabled
899
- ? undefined
900
- : yield* observePath(source.absolute);
901
-
902
- if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
903
- return yield* InvalidProjectStateError.make({
904
- message: "Claude instructions source is not a regular file: AGENTS.md",
905
- });
906
- }
907
- const managed = yield* resolveManagedPath(projectDir, "CLAUDE.md");
908
- const linkTarget = path.relative(path.dirname(managed.absolute), source.absolute);
909
-
910
- outputs.push({
911
- resourceId: "setup:claude-instructions",
912
- path: managed.relative,
913
- sourcePath: source.relative,
914
- mode: "symlink",
915
- kind: "symlink",
916
- digest: yield* digestSymlinkTarget(linkTarget),
917
- destination: managed.absolute,
918
- linkTarget,
919
- });
920
- }
921
- const agentsTarget = targets.agents;
922
- const duplicateOutput = skills.find(
923
- (skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
924
- );
925
-
926
- if (duplicateOutput !== undefined) {
927
- const selectors = skills
928
- .filter((skill) => skill.name === duplicateOutput.name)
929
- .map((skill) => skill.selector);
930
-
931
- return yield* InvalidProjectStateError.make({
932
- message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
933
- });
934
- }
935
- for (const skill of skills) {
936
- const resolvedSource = sourceBySkill.get(skill.selector);
937
-
938
- if (resolvedSource === undefined) {
939
- return yield* InvalidProjectStateError.make({
940
- message: `skill source is unavailable: ${skill.selector}`,
941
- });
942
- }
943
- const source = resolvedSource.path;
944
- const sourceObservation = yield* observePath(source);
945
-
946
- if (sourceObservation.kind !== "directory") {
947
- return yield* InvalidProjectStateError.make({
948
- message: `skill source is not a directory: ${source}`,
949
- });
950
- }
951
- for (const targetName of ["agents", "claude", "opencode"] as const) {
952
- const target = targets[targetName];
953
-
954
- if (!target.enabled) continue;
955
- const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill.name));
956
-
957
- if (target.mode === "copy") {
958
- outputs.push({
959
- resourceId: `skill:${skill.selector}@${targetName}`,
960
- path: managed.relative,
961
- skill: skill.name,
962
- target: targetName,
963
- mode: "copy",
964
- kind: "directory",
965
- digest: sourceObservation.digest,
966
- ...(resolvedSource.catalog ? { catalog: resolvedSource.catalog } : {}),
967
- source,
968
- destination: managed.absolute,
969
- });
970
- continue;
971
- }
972
- const linkSource =
973
- targetName === "agents" || !agentsTarget.enabled
974
- ? (resolvedSource.linkPath ?? source)
975
- : (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name)))
976
- .absolute;
977
- const linkTarget = path.relative(path.dirname(managed.absolute), linkSource);
978
- const linkDigest = yield* digestSymlinkTarget(linkTarget);
979
-
980
- outputs.push({
981
- resourceId: `skill:${skill.selector}@${targetName}`,
982
- path: managed.relative,
983
- skill: skill.name,
984
- target: targetName,
985
- mode: "symlink",
986
- kind: "symlink",
987
- digest: linkDigest,
988
- ...(resolvedSource.catalog ? { catalog: resolvedSource.catalog } : {}),
989
- source,
990
- destination: managed.absolute,
991
- linkTarget,
992
- });
993
- }
994
- }
995
- yield* validateInventory(projectDir, outputs, "desired outputs");
996
-
997
- return outputs.sort((left, right) => left.path.localeCompare(right.path));
998
- });
999
-
1000
- const canonicalLock = (lock: DevKitLock): string => `${encodeDevKitLockPrettyJson(lock)}\n`;
1001
- const canonicalState = (state: AppliedState): string => `${encodeAppliedStatePrettyJson(state)}\n`;
1002
-
1003
- const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
1004
- projectDir: string,
1005
- desired: ReadonlyArray<DesiredOutput>,
1006
- currentLock: DevKitLock | undefined,
1007
- currentState: AppliedState | undefined,
1008
- nextLock: DevKitLock,
1009
- ) {
1010
- if (currentLock) yield* validateInventory(projectDir, currentLock.outputs, "dev-kit lock");
1011
- if (currentState) yield* validateInventory(projectDir, currentState.outputs, "applied state");
1012
- yield* validateCrossInventoryPaths(projectDir, [...desired, ...(currentState?.outputs ?? [])]);
1013
- const lockById = new Map(currentLock?.outputs.map((output) => [output.resourceId, output]) ?? []);
1014
- const receiptsById = new Map(
1015
- currentState?.outputs.map((output) => [output.resourceId, output]) ?? [],
1016
- );
1017
- const desiredKeys = new Set(desired.map((output) => `${output.resourceId}\0${output.path}`));
1018
- const actions: Array<SkillPlanAction> = [];
1019
-
1020
- for (const output of desired) {
1021
- const observed = yield* observePath(output.destination);
1022
- const receipt = receiptsById.get(output.resourceId);
1023
- const sameReceipt = receipt?.path === output.path ? receipt : undefined;
1024
- const locked = lockById.get(output.resourceId);
1025
- const matchingLockedOutput =
1026
- locked !== undefined &&
1027
- outputOwnershipIdentity(locked) === outputOwnershipIdentity(output) &&
1028
- observed.kind === locked.kind
1029
- ? locked
1030
- : undefined;
1031
- const lockedOwnsObserved =
1032
- matchingLockedOutput !== undefined &&
1033
- observed.kind !== "missing" &&
1034
- observed.digest === matchingLockedOutput.digest;
1035
-
1036
- if (output.resourceId === "setup:agent-instructions" && "content" in output) {
1037
- if (observed.kind === "missing") {
1038
- actions.push({
1039
- action: "create",
1040
- desired: output,
1041
- observed,
1042
- stagedContent: output.content,
1043
- });
1044
- continue;
1045
- }
1046
- if (observed.kind !== "file") {
1047
- actions.push({
1048
- action: "conflict",
1049
- path: output.path,
1050
- reason: "destination is not a regular file",
1051
- });
1052
- continue;
1053
- }
1054
- const existingContent = yield* FileSystem.FileSystem.pipe(
1055
- Effect.flatMap((fs) => fs.readFileString(output.destination)),
1056
- );
1057
- const inspection = inspectManagedInstructionSections(existingContent);
1058
-
1059
- if (inspection.kind === "invalid") {
1060
- actions.push({ action: "conflict", path: output.path, reason: inspection.reason });
1061
- continue;
1062
- }
1063
- const managedDigest =
1064
- inspection.content === undefined ? undefined : yield* digestFileContent(inspection.content);
1065
- const receiptOwnsManaged =
1066
- sameReceipt !== undefined &&
1067
- (managedDigest === sameReceipt.digest || observed.digest === sameReceipt.digest);
1068
- const lockOwnsManaged =
1069
- matchingLockedOutput !== undefined &&
1070
- (managedDigest === matchingLockedOutput.digest ||
1071
- observed.digest === matchingLockedOutput.digest);
1072
- const legacyOwnsWholeFile =
1073
- (sameReceipt !== undefined &&
1074
- observed.digest === sameReceipt.digest &&
1075
- managedDigest !== sameReceipt.digest) ||
1076
- (matchingLockedOutput !== undefined &&
1077
- lockedOwnsObserved &&
1078
- managedDigest !== matchingLockedOutput.digest);
1079
-
1080
- if (managedDigest === output.digest && !legacyOwnsWholeFile) {
1081
- if (sameReceipt !== undefined || lockOwnsManaged || lockedOwnsObserved) {
1082
- actions.push({
1083
- action: "unchanged",
1084
- desired: output,
1085
- observed,
1086
- adopted: sameReceipt === undefined,
1087
- });
1088
- } else {
1089
- actions.push({
1090
- action: "conflict",
1091
- path: output.path,
1092
- reason: "managed instruction sections exist but are not owned",
1093
- });
1094
- }
1095
- } else if (
1096
- managedDigest === undefined ||
1097
- receiptOwnsManaged ||
1098
- lockOwnsManaged ||
1099
- lockedOwnsObserved
1100
- ) {
1101
- actions.push({
1102
- action: "update",
1103
- desired: output,
1104
- observed,
1105
- stagedContent: legacyOwnsWholeFile
1106
- ? output.content
1107
- : reconcileManagedInstructionSections(existingContent, inspection, output.content),
1108
- });
1109
- } else {
1110
- actions.push({
1111
- action: "conflict",
1112
- path: output.path,
1113
- reason: "managed instruction sections exist but are not owned",
1114
- });
1115
- }
1116
- continue;
1117
- }
1118
-
1119
- if (observed.kind === "missing") {
1120
- actions.push({ action: "create", desired: output, observed });
1121
- } else if (observed.kind === output.kind && observed.digest === output.digest) {
1122
- if (sameReceipt || lockedOwnsObserved) {
1123
- actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
1124
- } else {
1125
- actions.push({
1126
- action: "conflict",
1127
- path: output.path,
1128
- reason: "destination exists but is not owned",
1129
- });
1130
- }
1131
- } else if (
1132
- (sameReceipt !== undefined &&
1133
- observed.kind === sameReceipt.kind &&
1134
- observed.digest === sameReceipt.digest) ||
1135
- lockedOwnsObserved
1136
- ) {
1137
- actions.push({ action: "update", desired: output, observed });
1138
- } else {
1139
- actions.push({
1140
- action: "conflict",
1141
- path: output.path,
1142
- reason: sameReceipt
1143
- ? "owned destination was modified"
1144
- : "destination exists but is not owned",
1145
- });
1146
- }
1147
- }
1148
-
1149
- for (const receipt of currentState?.outputs ?? []) {
1150
- if (desiredKeys.has(`${receipt.resourceId}\0${receipt.path}`)) continue;
1151
- const managed = yield* resolveManagedPath(projectDir, receipt.path);
1152
- const observed = yield* observePath(managed.absolute);
1153
-
1154
- if (observed.kind === "missing") continue;
1155
- if (receipt.resourceId === "setup:agent-instructions") {
1156
- if (observed.kind !== "file") {
1157
- actions.push({
1158
- action: "conflict",
1159
- path: receipt.path,
1160
- reason: "stale owned destination is not a regular file",
1161
- });
1162
- continue;
1163
- }
1164
- const existingContent = yield* FileSystem.FileSystem.pipe(
1165
- Effect.flatMap((fs) => fs.readFileString(managed.absolute)),
1166
- );
1167
- const inspection = inspectManagedInstructionSections(existingContent);
1168
-
1169
- if (inspection.kind === "invalid") {
1170
- actions.push({ action: "conflict", path: receipt.path, reason: inspection.reason });
1171
- continue;
1172
- }
1173
- if (inspection.content === undefined) continue;
1174
- const managedDigest = yield* digestFileContent(inspection.content);
1175
-
1176
- if (managedDigest === receipt.digest || observed.digest === receipt.digest) {
1177
- const remaining = removeManagedInstructionSections(existingContent, inspection.ranges);
1178
-
1179
- actions.push({
1180
- action: "remove",
1181
- previous: receipt,
1182
- destination: managed.absolute,
1183
- observed,
1184
- ...(remaining.trim().length === 0 ? {} : { stagedContent: remaining }),
1185
- });
1186
- } else {
1187
- actions.push({
1188
- action: "conflict",
1189
- path: receipt.path,
1190
- reason: "stale owned managed instruction sections were modified",
1191
- });
1192
- }
1193
- continue;
1194
- }
1195
- if (observed.kind === receipt.kind && observed.digest === receipt.digest) {
1196
- actions.push({
1197
- action: "remove",
1198
- previous: receipt,
1199
- destination: managed.absolute,
1200
- observed,
1201
- });
1202
- } else {
1203
- actions.push({
1204
- action: "conflict",
1205
- path: receipt.path,
1206
- reason: "stale owned destination was modified",
1207
- });
1208
- }
1209
- }
1210
-
1211
- const nextState: AppliedState = {
1212
- version: 1,
1213
- appliedLockDigest: yield* digestText(canonicalLock(nextLock)),
1214
- outputs: desired.map(({ resourceId, path, mode, kind, digest }) => ({
1215
- resourceId,
1216
- path,
1217
- mode,
1218
- kind,
1219
- digest,
1220
- })),
1221
- };
1222
-
1223
- return {
1224
- actions: actions.sort((left, right) => {
1225
- const leftPath =
1226
- left.action === "remove"
1227
- ? left.previous.path
1228
- : left.action === "conflict"
1229
- ? left.path
1230
- : left.desired.path;
1231
- const rightPath =
1232
- right.action === "remove"
1233
- ? right.previous.path
1234
- : right.action === "conflict"
1235
- ? right.path
1236
- : right.desired.path;
1237
-
1238
- return leftPath.localeCompare(rightPath);
1239
- }),
1240
- nextState,
1241
- };
1242
- });
1243
-
1244
- const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
1245
- current.toolVersion === next.toolVersion &&
1246
- current.manifestDigest === next.manifestDigest &&
1247
- encodeDevKitSetupJson(current.setup ?? {}) === encodeDevKitSetupJson(next.setup ?? {}) &&
1248
- current.outputs.length === next.outputs.length &&
1249
- current.outputs.every((output, index) => {
1250
- const nextOutput = next.outputs[index];
1251
-
1252
- return nextOutput !== undefined && outputIdentity(output) === outputIdentity(nextOutput);
1253
- });
1254
-
1255
- export const planProjectSkills = Effect.fn("planProjectSkills")(function* (options: SyncOptions) {
1256
- const path = yield* Path.Path;
1257
- const fs = yield* FileSystem.FileSystem;
1258
- const initialDir = path.resolve(options.projectDir ?? ".");
1259
- const discoveredRoot = yield* resolveGitRoot(initialDir).pipe(
1260
- Effect.catchTag("CommandError", (error) =>
1261
- error.output.includes("not a git repository")
1262
- ? Effect.succeed(initialDir)
1263
- : Effect.fail(error),
1264
- ),
1265
- );
1266
- const projectDir = yield* fs.realPath(discoveredRoot);
1267
- const manifestManaged = yield* resolveManagedPath(
1268
- projectDir,
1269
- options.manifestPath ?? DEFAULT_MANIFEST,
1270
- );
1271
- const lockManaged = yield* resolveManagedPath(
1272
- projectDir,
1273
- options.lockfilePath ?? DEFAULT_LOCKFILE,
1274
- );
1275
- const stateManaged = yield* resolveManagedPath(projectDir, options.statePath ?? DEFAULT_STATE);
1276
- const processLockManaged = yield* resolveManagedPath(projectDir, PROJECT_PROCESS_LOCK_PATH);
1277
- const packageRoot = yield* resolvePackageRoot();
1278
- const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
1279
-
1280
- const effectSource = manifest.setup.effectSource.enabled
1281
- ? yield* planEffectSource({
1282
- packageName: manifest.setup.effectSource.packageName,
1283
- path: manifest.setup.effectSource.path,
1284
- projectDir,
1285
- repository: manifest.setup.effectSource.repository,
1286
- })
1287
- : undefined;
1288
- const effectTsgo = manifest.setup.effectTsgo.enabled
1289
- ? yield* planEffectTsgoPatch({
1290
- force: manifest.setup.effectTsgo.force,
1291
- projectDir,
1292
- typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
1293
- })
1294
- : undefined;
1295
- const vitePlusHooks = manifest.setup.vitePlus.hooks.enabled
1296
- ? yield* planVitePlusHooks(projectDir)
1297
- : undefined;
1298
- const vitePlusWorkflow = manifest.setup.vitePlus.workflow.enabled
1299
- ? yield* planVitePlusWorkflow({
1300
- packageRoot,
1301
- projectDir,
1302
- effectTsgoEnabled: manifest.setup.effectTsgo.enabled,
1303
- typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
1304
- })
1305
- : undefined;
1306
- const worktrunkConfig = manifest.setup.worktrunk.config.enabled
1307
- ? yield* planWorktrunkConfig(packageRoot, projectDir)
1308
- : undefined;
1309
- const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
1310
- const availableSkills = catalog.skills.map((skill) => skill.selector);
1311
- const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
1312
-
1313
- for (const [family, familySkills] of Object.entries(skillFamilies)) {
1314
- if (availableSkills.includes(family)) {
1315
- return yield* InvalidSkillCatalogError.make({
1316
- family,
1317
- message: `family name conflicts with a skill name: ${family}`,
1318
- });
1319
- }
1320
- const missing = familySkills.filter((skill) => !availableSkills.includes(skill));
1321
-
1322
- if (missing.length > 0) {
1323
- return yield* InvalidSkillCatalogError.make({
1324
- family,
1325
- message: `family references missing skills: ${missing.join(", ")}`,
1326
- });
1327
- }
1328
- }
1329
- const selectedSelectors = yield* expandSelection(
1330
- manifest.include,
1331
- manifest.exclude,
1332
- availableSkills,
1333
- skillFamilies,
1334
- );
1335
- const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
1336
- const sourceBySkill = yield* withSpinner(
1337
- "Resolving selected skills",
1338
- resolveSkillSources(
1339
- packageRoot,
1340
- projectDir,
1341
- catalog,
1342
- selectedSelectors,
1343
- options.dryRun !== true,
1344
- ),
1345
- );
1346
- const selectedSkills: Array<CatalogSkill> = [];
1347
-
1348
- for (const selector of selectedSelectors) {
1349
- const catalogSkill = catalogBySelector.get(selector);
1350
-
1351
- if (catalogSkill === undefined) {
1352
- return yield* InvalidProjectStateError.make({
1353
- message: `selected skill is unavailable: ${selector}`,
1354
- });
1355
- }
1356
- selectedSkills.push(catalogSkill);
1357
- }
1358
- const desired = yield* buildDesiredOutputs(
1359
- packageRoot,
1360
- projectDir,
1361
- sourceBySkill,
1362
- selectedSkills,
1363
- manifest.setup,
1364
- manifest.targets,
1365
- );
1366
- const nextLock: DevKitLock = {
1367
- version: 1,
1368
- toolVersion: DEV_KIT_VERSION,
1369
- manifestDigest: yield* digestText(encodeManifestJson(manifest)),
1370
- setup: {
1371
- ...(effectSource === undefined
1372
- ? {}
1373
- : {
1374
- effectSource: {
1375
- packageName: effectSource.packageName,
1376
- packageVersion: effectSource.packageVersion,
1377
- path: effectSource.path,
1378
- repository: effectSource.repository,
1379
- tag: effectSource.tag,
1380
- },
1381
- }),
1382
- ...(effectTsgo === undefined
1383
- ? {}
1384
- : {
1385
- effectTsgo: {
1386
- effectTsgoVersion: effectTsgo.effectTsgoVersion,
1387
- typescriptPackage: effectTsgo.typescriptPackage,
1388
- typescriptVersion: effectTsgo.typescriptVersion,
1389
- },
1390
- }),
1391
- },
1392
- outputs: desired.map((output): ManagedOutput => {
1393
- if ("skill" in output) {
1394
- return {
1395
- resourceId: output.resourceId,
1396
- path: output.path,
1397
- skill: output.skill,
1398
- target: output.target,
1399
- mode: output.mode,
1400
- kind: output.kind,
1401
- digest: output.digest,
1402
- ...(output.catalog ? { catalog: output.catalog } : {}),
1403
- };
1404
- }
1405
- if (output.resourceId === "setup:agent-instructions") {
1406
- return {
1407
- resourceId: output.resourceId,
1408
- path: output.path,
1409
- sourcePath: output.sourcePath,
1410
- mode: output.mode,
1411
- kind: output.kind,
1412
- digest: output.digest,
1413
- };
1414
- }
1415
-
1416
- return {
1417
- resourceId: output.resourceId,
1418
- path: output.path,
1419
- sourcePath: output.sourcePath,
1420
- mode: output.mode,
1421
- kind: output.kind,
1422
- digest: output.digest,
1423
- };
1424
- }),
1425
- };
1426
- const reservedPaths = [
1427
- { label: "manifest", path: manifestManaged.relative },
1428
- { label: "lockfile", path: lockManaged.relative },
1429
- { label: "state", path: stateManaged.relative },
1430
- { label: "process lock", path: processLockManaged.relative },
1431
- ...(effectSource === undefined
1432
- ? []
1433
- : [{ label: "Effect source checkout", path: effectSource.path }]),
1434
- ];
1435
-
1436
- yield* validateReservedPaths(projectDir, reservedPaths, desired);
1437
- const rawLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
1438
- const rawState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
1439
- // Migration: dev-kit ≤0.14 owned the check workflow as a managed output. It
1440
- // is a scaffold now, so stale lock entries and receipts are dropped on read —
1441
- // releasing ownership to the repository instead of planning a removal.
1442
- const dropRetiredOutputs = <O extends { readonly resourceId: string }>(
1443
- outputs: ReadonlyArray<O>,
1444
- ) => outputs.filter((output) => output.resourceId !== "setup:vite-plus-github-actions");
1445
- const currentLock =
1446
- rawLock === undefined
1447
- ? undefined
1448
- : { ...rawLock, outputs: dropRetiredOutputs(rawLock.outputs) };
1449
- const currentState =
1450
- rawState === undefined
1451
- ? undefined
1452
- : { ...rawState, outputs: dropRetiredOutputs(rawState.outputs) };
1453
-
1454
- yield* validateReservedPaths(projectDir, reservedPaths, [
1455
- ...(currentLock?.outputs ?? []),
1456
- ...(currentState?.outputs ?? []),
1457
- ]);
1458
- if (options.locked) {
1459
- if (!currentLock) {
1460
- return yield* LockedPlanMismatchError.make({
1461
- message: "dev-kit.lock.json is required with --locked",
1462
- });
1463
- }
1464
- if (!lockedPlanMatches(currentLock, nextLock)) {
1465
- return yield* LockedPlanMismatchError.make({
1466
- message: "manifest or packaged skills differ from dev-kit.lock.json",
1467
- });
1468
- }
1469
- }
1470
- const planned = yield* planDesiredOutputs(
1471
- projectDir,
1472
- desired,
1473
- currentLock,
1474
- currentState,
1475
- nextLock,
1476
- );
1477
- const removesClaudeInstructionsSource = planned.actions.some(
1478
- (action) =>
1479
- action.action === "remove" &&
1480
- action.previous.resourceId === "setup:agent-instructions" &&
1481
- action.stagedContent === undefined,
1482
- );
1483
-
1484
- if (manifest.setup.claudeInstructions.enabled && removesClaudeInstructionsSource) {
1485
- return yield* InvalidProjectStateError.make({
1486
- message:
1487
- "cannot disable agentInstructions while claudeInstructions still links to an AGENTS.md that would be removed",
1488
- });
1489
- }
1490
-
1491
- return {
1492
- projectDir,
1493
- lockfilePath: lockManaged.absolute,
1494
- statePath: stateManaged.absolute,
1495
- actions: planned.actions,
1496
- ...(effectSource === undefined ? {} : { effectSource }),
1497
- ...(effectTsgo === undefined ? {} : { effectTsgo }),
1498
- ...(vitePlusHooks === undefined ? {} : { vitePlusHooks }),
1499
- ...(vitePlusWorkflow === undefined ? {} : { vitePlusWorkflow }),
1500
- ...(worktrunkConfig === undefined ? {} : { worktrunkConfig }),
1501
- nextLock,
1502
- nextState: planned.nextState,
1503
- metadataChanged:
1504
- currentLock === undefined ||
1505
- encodeDevKitLockJson(currentLock) !== encodeDevKitLockJson(nextLock) ||
1506
- currentState === undefined ||
1507
- encodeAppliedStateJson(currentState) !== encodeAppliedStateJson(planned.nextState),
1508
- } satisfies SkillPlan;
1509
- });
1510
-
1511
- const formatAction = (action: SkillPlanAction): string => {
1512
- if (action.action === "conflict") return `! ${action.path}: ${action.reason}`;
1513
- if (action.action === "remove")
1514
- return `− ${action.previous.resourceId} → ${action.previous.path}`;
1515
- const verb = action.desired.mode === "copy" ? "copy" : "link";
1516
- const adoption = action.action === "unchanged" && action.adopted ? " (adopt)" : "";
1517
- const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "=";
1518
- const source = "skill" in action.desired ? action.desired.skill : action.desired.sourcePath;
1519
-
1520
- return `${marker} ${verb} ${source} → ${action.desired.path}${adoption}`;
1521
- };
1522
-
1523
- const operationalChangeCount = (plan: SkillPlan): number =>
1524
- plan.actions.filter((action) => action.action !== "unchanged").length +
1525
- (plan.effectSource?.action === "sync" ? 1 : 0) +
1526
- (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0) +
1527
- (plan.vitePlusHooks?.action === "configure" ? 1 : 0) +
1528
- (plan.vitePlusWorkflow?.action === "scaffold" ? 1 : 0) +
1529
- (plan.worktrunkConfig?.action === "scaffold" ? 1 : 0);
1530
-
1531
- const plannedChangeCount = (plan: SkillPlan): number => {
1532
- const operational = operationalChangeCount(plan);
1533
-
1534
- return operational === 0 && plan.metadataChanged ? 1 : operational;
1535
- };
1536
-
1537
- export const printSkillPlan = Effect.fn("printSkillPlan")(function* (plan: SkillPlan) {
1538
- const changes = plannedChangeCount(plan);
1539
-
1540
- if (changes === 0) {
1541
- yield* printStatus("success", "Already up to date");
1542
-
1543
- return;
1544
- }
1545
- yield* printStatus("plan", `${changes} change${changes === 1 ? "" : "s"} planned`);
1546
- for (const action of plan.actions) {
1547
- if (action.action !== "unchanged" || action.adopted) yield* printDetail(formatAction(action));
1548
- }
1549
- if (plan.effectSource?.action === "sync") {
1550
- yield* printDetail(`+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`);
1551
- }
1552
- if (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched) {
1553
- yield* printDetail(
1554
- `+ TypeScript patch @effect/tsgo@${plan.effectTsgo.effectTsgoVersion} → ${plan.effectTsgo.typescriptPackage}@${plan.effectTsgo.typescriptVersion}`,
1555
- );
1556
- }
1557
- if (plan.vitePlusHooks?.action === "configure") {
1558
- yield* printDetail(`+ Vite+ hooks → ${plan.vitePlusHooks.hooksPath}`);
1559
- }
1560
- if (plan.vitePlusWorkflow?.action === "scaffold") {
1561
- yield* printDetail(`+ scaffold check workflow → ${plan.vitePlusWorkflow.path}`);
1562
- }
1563
- if (plan.worktrunkConfig?.action === "scaffold") {
1564
- yield* printDetail(`+ scaffold Worktrunk config → ${plan.worktrunkConfig.path}`);
1565
- }
1566
- if (operationalChangeCount(plan) === 0 && plan.metadataChanged) {
1567
- yield* printDetail("+ Dev kit metadata");
1568
- }
1569
- });
1570
-
1571
- const observationsEqual = (left: ObservedPath, right: ObservedPath): boolean =>
1572
- left.kind === right.kind &&
1573
- (left.kind === "missing" || (right.kind !== "missing" && left.digest === right.digest));
1574
-
1575
- const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (root: string) {
1576
- const fs = yield* FileSystem.FileSystem;
1577
- const path = yield* Path.Path;
1578
- const pending = [root];
1579
-
1580
- while (pending.length > 0) {
1581
- const current = pending.pop();
1582
-
1583
- if (current === undefined) continue;
1584
- if ((yield* observeSymbolicLink(current)).kind === "symlink") return current;
1585
- const info = yield* fs.stat(current);
1586
-
1587
- if (info.type !== "Directory") continue;
1588
- for (const entry of yield* fs.readDirectory(current)) {
1589
- pending.push(path.join(current, entry));
1590
- }
1591
- }
1592
-
1593
- return undefined;
1594
- });
1595
-
1596
- const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(function* (
1597
- plan: SkillPlan,
1598
- ) {
1599
- const verified = new Set<string>();
1600
-
1601
- for (const action of plan.actions) {
1602
- if (action.action === "remove" || action.action === "conflict") continue;
1603
- if (!("skill" in action.desired)) continue;
1604
- const catalog = action.desired.catalog;
1605
-
1606
- if (catalog === undefined || !("package" in catalog)) continue;
1607
- const selector = `${catalog.package}#${catalog.skill}`;
1608
- const key = `${selector}\0${catalog.version}\0${catalog.digest}`;
1609
-
1610
- if (verified.has(key)) continue;
1611
- const resolved = yield* resolvePackageSkillSelector(plan.projectDir, selector);
1612
- const observation = yield* observePath(resolved.path);
1613
-
1614
- if (
1615
- resolved.version !== catalog.version ||
1616
- observation.kind !== "directory" ||
1617
- observation.digest !== catalog.digest
1618
- ) {
1619
- return yield* ApplyRaceError.make({ path: action.desired.source });
1620
- }
1621
- verified.add(key);
1622
- }
1623
- });
1624
-
1625
- const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function* (plan: SkillPlan) {
1626
- const conflicts = plan.actions.filter((action) => action.action === "conflict");
1627
-
1628
- if (conflicts.length > 0) {
1629
- return yield* PlanConflictError.make({
1630
- conflicts: conflicts.map((action) =>
1631
- action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
1632
- ),
1633
- });
1634
- }
1635
-
1636
- const fs = yield* FileSystem.FileSystem;
1637
- const path = yield* Path.Path;
1638
- const mutating = plan.actions.filter(
1639
- (
1640
- action,
1641
- ): action is Extract<SkillPlanAction, { readonly action: "create" | "update" | "remove" }> =>
1642
- action.action === "create" || action.action === "update" || action.action === "remove",
1643
- );
1644
-
1645
- for (const action of mutating) {
1646
- const destination =
1647
- action.action === "remove" ? action.destination : action.desired.destination;
1648
-
1649
- if (!observationsEqual(yield* observePath(destination), action.observed)) {
1650
- return yield* ApplyRaceError.make({
1651
- path: action.action === "remove" ? action.previous.path : action.desired.path,
1652
- });
1653
- }
1654
- }
1655
-
1656
- if (mutating.length === 0 && !plan.metadataChanged) {
1657
- return;
1658
- }
1659
-
1660
- const tempDir = yield* fs.makeTempDirectoryScoped({
1661
- directory: plan.projectDir,
1662
- prefix: ".dev-kit-apply-",
1663
- });
1664
- const stageDir = path.join(tempDir, "stage");
1665
- const backupDir = path.join(tempDir, "backup");
1666
- const stagedByAction = new Map<(typeof mutating)[number], string>();
1667
- let stageIndex = 0;
1668
-
1669
- for (const action of mutating) {
1670
- if (action.action === "remove" && action.stagedContent === undefined) continue;
1671
- const staged = path.join(stageDir, String(stageIndex++));
1672
-
1673
- yield* fs.makeDirectory(path.dirname(staged), { recursive: true });
1674
- if (action.stagedContent !== undefined && action.observed.kind === "file") {
1675
- const destination =
1676
- action.action === "remove" ? action.destination : action.desired.destination;
1677
-
1678
- yield* fs.copy(destination, staged, { overwrite: true });
1679
- yield* fs.writeFileString(staged, action.stagedContent);
1680
- } else if (action.action === "remove") {
1681
- if (action.stagedContent === undefined) {
1682
- return yield* InvalidProjectStateError.make({
1683
- message: `missing staged content for ${action.previous.resourceId}`,
1684
- });
1685
- }
1686
- yield* fs.writeFileString(staged, action.stagedContent, { mode: 0o644 });
1687
- } else if (action.desired.mode === "copy") {
1688
- if (action.desired.kind === "file") {
1689
- yield* fs.writeFileString(staged, action.stagedContent ?? action.desired.content, {
1690
- mode: 0o644,
1691
- });
1692
- } else {
1693
- yield* fs.copy(action.desired.source, staged, { overwrite: true });
1694
- const symbolicLink = yield* findNestedSymbolicLink(staged);
1695
-
1696
- if (symbolicLink !== undefined) {
1697
- return yield* InvalidProjectStateError.make({
1698
- message: `staged skill contains a symlink: ${action.desired.path}`,
1699
- });
1700
- }
1701
- }
1702
- } else {
1703
- yield* fs.symlink(action.desired.linkTarget, staged);
1704
- }
1705
- if (action.action !== "remove") {
1706
- const observation = yield* observePath(staged);
1707
-
1708
- if (action.desired.resourceId === "setup:agent-instructions") {
1709
- const content = yield* fs.readFileString(staged);
1710
- const inspection = inspectManagedInstructionSections(content);
1711
- const digest =
1712
- inspection.kind === "valid" && inspection.content !== undefined
1713
- ? yield* digestFileContent(inspection.content)
1714
- : undefined;
1715
-
1716
- if (observation.kind !== "file" || digest !== action.desired.digest) {
1717
- return yield* InvalidProjectStateError.make({
1718
- message: `staged output digest mismatch for ${action.desired.path}`,
1719
- });
1720
- }
1721
- } else if (
1722
- observation.kind !== action.desired.kind ||
1723
- observation.digest !== action.desired.digest
1724
- ) {
1725
- return yield* InvalidProjectStateError.make({
1726
- message: `staged output digest mismatch for ${action.desired.path}`,
1727
- });
1728
- }
1729
- }
1730
- stagedByAction.set(action, staged);
1731
- }
1732
-
1733
- yield* verifyPackageSkillSources(plan);
1734
-
1735
- const stagedLock = path.join(tempDir, "next-lock.json");
1736
- const stagedState = path.join(tempDir, "next-state.json");
1737
-
1738
- yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
1739
- yield* fs.writeFileString(stagedState, canonicalState(plan.nextState));
1740
-
1741
- type Replacement = {
1742
- readonly destination: string;
1743
- readonly backup: string;
1744
- readonly expected?: ObservedPath;
1745
- readonly path: string;
1746
- readonly staged?: string;
1747
- };
1748
- const replacements: Array<Replacement> = [];
1749
- let replacementIndex = 0;
1750
-
1751
- for (const action of mutating) {
1752
- const staged = stagedByAction.get(action);
1753
-
1754
- if (
1755
- (action.action !== "remove" || action.stagedContent !== undefined) &&
1756
- staged === undefined
1757
- ) {
1758
- return yield* InvalidProjectStateError.make({
1759
- message: `missing staged output for ${
1760
- action.action === "remove" ? action.previous.resourceId : action.desired.resourceId
1761
- }`,
1762
- });
1763
- }
1764
- replacements.push({
1765
- destination: action.action === "remove" ? action.destination : action.desired.destination,
1766
- backup: path.join(backupDir, String(replacementIndex++)),
1767
- expected: action.observed,
1768
- path: action.action === "remove" ? action.previous.path : action.desired.path,
1769
- ...(staged === undefined ? {} : { staged }),
1770
- });
1771
- }
1772
- replacements.push(
1773
- {
1774
- destination: plan.lockfilePath,
1775
- backup: path.join(backupDir, "lock"),
1776
- path: plan.lockfilePath,
1777
- staged: stagedLock,
1778
- },
1779
- {
1780
- destination: plan.statePath,
1781
- backup: path.join(backupDir, "state"),
1782
- path: plan.statePath,
1783
- staged: stagedState,
1784
- },
1785
- );
1786
-
1787
- const installed: Array<string> = [];
1788
- const backedUp: Array<Replacement> = [];
1789
- const rollback = Effect.gen(function* () {
1790
- for (const destination of [...installed].reverse()) {
1791
- yield* fs.remove(destination, { recursive: true, force: true });
1792
- }
1793
- for (const replacement of [...backedUp].reverse()) {
1794
- yield* fs.makeDirectory(path.dirname(replacement.destination), { recursive: true });
1795
- yield* fs.rename(replacement.backup, replacement.destination);
1796
- }
1797
- });
1798
-
1799
- const apply = Effect.gen(function* () {
1800
- for (const replacement of replacements) {
1801
- const observed = yield* observePath(replacement.destination);
1802
-
1803
- if (
1804
- replacement.expected !== undefined &&
1805
- !observationsEqual(observed, replacement.expected)
1806
- ) {
1807
- return yield* ApplyRaceError.make({ path: replacement.path });
1808
- }
1809
- if (observed.kind !== "missing") {
1810
- yield* fs.makeDirectory(path.dirname(replacement.backup), { recursive: true });
1811
- yield* fs.rename(replacement.destination, replacement.backup);
1812
- backedUp.push(replacement);
1813
- }
1814
- if (replacement.staged) {
1815
- yield* fs.makeDirectory(path.dirname(replacement.destination), { recursive: true });
1816
- yield* fs.rename(replacement.staged, replacement.destination);
1817
- installed.push(replacement.destination);
1818
- }
1819
- }
1820
- });
1821
-
1822
- yield* Effect.uninterruptible(
1823
- apply.pipe(
1824
- Effect.catchCause((applyCause) =>
1825
- rollback.pipe(
1826
- Effect.catchCause((rollbackCause) =>
1827
- Effect.failCause(Cause.combine(applyCause, rollbackCause)),
1828
- ),
1829
- Effect.andThen(Effect.failCause(applyCause)),
1830
- ),
1831
- ),
1832
- ),
1833
- );
1834
- });
1835
-
1836
- export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
1837
- options: SyncOptions,
1838
- ) {
1839
- const plan = yield* planProjectSkills(options);
1840
-
1841
- if (options.dryRun) yield* printSkillPlan(plan);
1842
- const conflicts = plan.actions.filter((action) => action.action === "conflict");
1843
-
1844
- if (conflicts.length > 0) {
1845
- return yield* PlanConflictError.make({
1846
- conflicts: conflicts.map((action) =>
1847
- action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
1848
- ),
1849
- });
1850
- }
1851
- if (options.dryRun) return;
1852
-
1853
- yield* acquireProjectProcessLock(plan.projectDir);
1854
- const replanned = yield* planProjectSkills(options);
1855
- const originalSignature = encodePlanSnapshotJson({
1856
- actions: plan.actions,
1857
- effectSource: plan.effectSource,
1858
- effectTsgo: plan.effectTsgo,
1859
- vitePlusHooks: plan.vitePlusHooks,
1860
- vitePlusWorkflow: plan.vitePlusWorkflow,
1861
- worktrunkConfig: plan.worktrunkConfig,
1862
- nextLock: plan.nextLock,
1863
- nextState: plan.nextState,
1864
- });
1865
- const nextSignature = encodePlanSnapshotJson({
1866
- actions: replanned.actions,
1867
- effectSource: replanned.effectSource,
1868
- effectTsgo: replanned.effectTsgo,
1869
- vitePlusHooks: replanned.vitePlusHooks,
1870
- vitePlusWorkflow: replanned.vitePlusWorkflow,
1871
- worktrunkConfig: replanned.worktrunkConfig,
1872
- nextLock: replanned.nextLock,
1873
- nextState: replanned.nextState,
1874
- });
1875
-
1876
- if (originalSignature !== nextSignature) {
1877
- return yield* ApplyRaceError.make({ path: "project state" });
1878
- }
1879
- const changes = plannedChangeCount(replanned);
1880
-
1881
- yield* withSpinner(
1882
- "Applying dev kit",
1883
- Effect.gen(function* () {
1884
- if (replanned.effectSource !== undefined) {
1885
- yield* applyEffectSourcePlan(replanned.effectSource);
1886
- }
1887
- if (replanned.effectTsgo !== undefined) {
1888
- yield* applyEffectTsgoPatchPlan(replanned.effectTsgo);
1889
- }
1890
- if (replanned.vitePlusHooks !== undefined) {
1891
- yield* applyVitePlusHooksPlan(replanned.vitePlusHooks);
1892
- }
1893
- if (replanned.vitePlusWorkflow !== undefined) {
1894
- yield* applyScaffoldPlan(replanned.vitePlusWorkflow);
1895
- }
1896
- if (replanned.worktrunkConfig !== undefined) {
1897
- yield* applyScaffoldPlan(replanned.worktrunkConfig);
1898
- }
1899
- yield* applyPlannedSkillChanges(replanned);
1900
- }),
1901
- );
1902
- const fs = yield* FileSystem.FileSystem;
1903
- const path = yield* Path.Path;
1904
-
1905
- // Catalog checkouts moved to the machine-global cache; drop the regenerable
1906
- // project-local copies left behind by earlier dev-kit versions.
1907
- yield* fs
1908
- .remove(path.join(plan.projectDir, ".dev-kit", "cache", "catalog"), {
1909
- force: true,
1910
- recursive: true,
1911
- })
1912
- .pipe(Effect.ignore);
1913
- yield* maybePruneGlobalCache().pipe(Effect.ignore);
1914
- yield* printStatus(
1915
- "success",
1916
- changes === 0 && !replanned.metadataChanged ? "Dev kit up to date" : "Dev kit ready",
1917
- changes > 0 ? `${changes} change${changes === 1 ? "" : "s"}` : undefined,
1918
- );
1919
- });