@danieljvdm/dev-kit 0.6.0 → 0.7.1
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 +123 -56
- package/dev-kit.example.jsonc +7 -3
- package/package.json +19 -16
- package/schema/dev-kit.schema.json +38 -0
- package/skill-sources.jsonc +8 -12
- package/skill-sources.lock.json +3 -9
- package/skills/dev-kit/SKILL.md +52 -17
- package/skills/effect-ts/agents/openai.yaml +0 -1
- package/skills/effect-ts/references/audit-services.md +11 -11
- package/skills/effect-ts/references/guide-effect.md +56 -69
- package/skills/effect-ts/references/guide-error-handling.md +64 -73
- package/skills/effect-ts/references/guide-layers.md +187 -215
- package/skills/effect-ts/references/guide-observability.md +91 -116
- package/skills/effect-ts/references/guide-retries.md +32 -44
- package/skills/effect-ts/references/guide-schedule.md +26 -40
- package/skills/effect-ts/references/guide-schema.md +50 -57
- package/skills/effect-ts/references/guide-sql.md +47 -50
- package/skills/effect-ts/references/guide-testing.md +96 -98
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
- package/skills/effect-ts/references/version-and-source.md +0 -1
- package/src/bin/dev-kit.ts +61 -28
- package/src/catalog-manager.ts +86 -34
- package/src/catalog.ts +71 -33
- package/src/cli-ui.ts +20 -16
- package/src/effect-source.ts +49 -19
- package/src/effect-tsgo.ts +66 -35
- package/src/gitignore.ts +19 -6
- package/src/index.ts +6 -0
- package/src/manifest.ts +38 -3
- package/src/node-symbolic-link.ts +3 -0
- package/src/oxlint-plugin-effect.js +3 -0
- package/src/oxlint-plugin-style.d.ts +8 -0
- package/src/oxlint-plugin-style.js +8 -0
- package/src/oxlint.js +14 -0
- package/src/oxlint.ts +14 -0
- package/src/package-skill-source.ts +189 -52
- package/src/path-digest.ts +47 -13
- package/src/project-package.ts +44 -19
- package/src/project-process-lock.ts +19 -12
- package/src/project-state.ts +11 -0
- package/src/skill-manager.ts +134 -55
- package/src/skill-selector.ts +8 -2
- package/src/source-manifest.ts +2 -6
- package/src/sync.ts +417 -107
- package/src/vendor.ts +112 -42
- package/src/vite-plus-hooks.ts +174 -0
- package/src/vite-plus-quality.ts +49 -0
- package/templates/vite-plus/github-actions-check.yml +44 -0
- package/templates/vite-plus/vite.config.ts +22 -0
package/src/sync.ts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
2
1
|
import { Cause, Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
3
2
|
import { ChildProcess } from "effect/unstable/process";
|
|
3
|
+
import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
4
4
|
|
|
5
|
-
import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
|
|
6
5
|
import {
|
|
7
6
|
loadSkillCatalog,
|
|
8
7
|
resolveSkillSources,
|
|
@@ -10,26 +9,25 @@ import {
|
|
|
10
9
|
type ResolvedSkillSource,
|
|
11
10
|
} from "./catalog.ts";
|
|
12
11
|
import { printDetail, printStatus, withSpinner } from "./cli-ui.ts";
|
|
13
|
-
import {
|
|
14
|
-
applyEffectSourcePlan,
|
|
15
|
-
planEffectSource,
|
|
16
|
-
type EffectSourcePlan,
|
|
17
|
-
} from "./effect-source.ts";
|
|
12
|
+
import { applyEffectSourcePlan, planEffectSource, type EffectSourcePlan } from "./effect-source.ts";
|
|
18
13
|
import {
|
|
19
14
|
applyEffectTsgoPatchPlan,
|
|
20
15
|
planEffectTsgoPatch,
|
|
21
16
|
type EffectTsgoPatchPlan,
|
|
22
17
|
} from "./effect-tsgo.ts";
|
|
18
|
+
import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
|
|
19
|
+
import { observeSymbolicLink } from "./node-symbolic-link.ts";
|
|
20
|
+
import { resolvePackageSkillSelector } from "./package-skill-source.ts";
|
|
23
21
|
import {
|
|
24
22
|
digestFileContent,
|
|
25
23
|
digestSymlinkTarget,
|
|
26
24
|
digestText,
|
|
27
25
|
observePath,
|
|
26
|
+
observePathWithRawModes,
|
|
28
27
|
type ObservedPath,
|
|
29
28
|
} from "./path-digest.ts";
|
|
30
|
-
import { resolvePackageSkillSelector } from "./package-skill-source.ts";
|
|
31
29
|
import { readDirectDependencyNames } from "./project-package.ts";
|
|
32
|
-
import {
|
|
30
|
+
import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-process-lock.ts";
|
|
33
31
|
import {
|
|
34
32
|
AppliedStateSchema,
|
|
35
33
|
DevKitLockSchema,
|
|
@@ -37,16 +35,25 @@ import {
|
|
|
37
35
|
type DevKitLock,
|
|
38
36
|
type ManagedAgentInstructionsOutput,
|
|
39
37
|
type ManagedClaudeInstructionsOutput,
|
|
38
|
+
type ManagedGeneratedFileOutput,
|
|
40
39
|
type ManagedOutput,
|
|
41
40
|
type ManagedSkillOutput,
|
|
42
41
|
type OwnershipReceipt,
|
|
43
42
|
} from "./project-state.ts";
|
|
44
|
-
import {
|
|
45
|
-
acquireProjectProcessLock,
|
|
46
|
-
PROJECT_PROCESS_LOCK_PATH,
|
|
47
|
-
} from "./project-process-lock.ts";
|
|
48
|
-
import { observeSymbolicLink } from "./node-symbolic-link.ts";
|
|
43
|
+
import { parseSkillSelector } from "./skill-selector.ts";
|
|
49
44
|
import { DEV_KIT_VERSION } from "./tool-metadata.ts";
|
|
45
|
+
import {
|
|
46
|
+
applyVitePlusHooksPlan,
|
|
47
|
+
planVitePlusHooks,
|
|
48
|
+
type VitePlusHooksPlan,
|
|
49
|
+
} from "./vite-plus-hooks.ts";
|
|
50
|
+
import {
|
|
51
|
+
validateVitePlusQualitySupport,
|
|
52
|
+
VITE_PLUS_CONFIG_PATH,
|
|
53
|
+
VITE_PLUS_CONFIG_TEMPLATE,
|
|
54
|
+
VITE_PLUS_GITHUB_ACTIONS_PATH,
|
|
55
|
+
VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
|
|
56
|
+
} from "./vite-plus-quality.ts";
|
|
50
57
|
|
|
51
58
|
export type SyncOptions = {
|
|
52
59
|
readonly manifestPath?: string;
|
|
@@ -89,10 +96,17 @@ type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
|
|
|
89
96
|
readonly linkTarget: string;
|
|
90
97
|
};
|
|
91
98
|
|
|
99
|
+
type DesiredGeneratedFileOutput = ManagedGeneratedFileOutput & {
|
|
100
|
+
readonly adoptIfExact: true;
|
|
101
|
+
readonly content: string;
|
|
102
|
+
readonly destination: string;
|
|
103
|
+
};
|
|
104
|
+
|
|
92
105
|
type DesiredOutput =
|
|
93
106
|
| DesiredSkillOutput
|
|
94
107
|
| DesiredAgentInstructionsOutput
|
|
95
|
-
| DesiredClaudeInstructionsOutput
|
|
108
|
+
| DesiredClaudeInstructionsOutput
|
|
109
|
+
| DesiredGeneratedFileOutput;
|
|
96
110
|
|
|
97
111
|
type SkillPlanAction =
|
|
98
112
|
| {
|
|
@@ -125,6 +139,7 @@ export type SkillPlan = {
|
|
|
125
139
|
readonly actions: ReadonlyArray<SkillPlanAction>;
|
|
126
140
|
readonly effectSource?: EffectSourcePlan;
|
|
127
141
|
readonly effectTsgo?: EffectTsgoPatchPlan;
|
|
142
|
+
readonly vitePlusHooks?: VitePlusHooksPlan;
|
|
128
143
|
readonly nextLock: DevKitLock;
|
|
129
144
|
readonly nextState: AppliedState;
|
|
130
145
|
readonly metadataChanged: boolean;
|
|
@@ -189,12 +204,12 @@ class LockedPlanMismatchError extends Schema.TaggedErrorClass<LockedPlanMismatch
|
|
|
189
204
|
{ message: Schema.String },
|
|
190
205
|
) {}
|
|
191
206
|
|
|
192
|
-
class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
) {
|
|
207
|
+
class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()("PlanConflictError", {
|
|
208
|
+
conflicts: Schema.Array(Schema.String),
|
|
209
|
+
}) {
|
|
196
210
|
override get message() {
|
|
197
211
|
const heading = `plan has ${this.conflicts.length} conflict${this.conflicts.length === 1 ? "" : "s"}`;
|
|
212
|
+
|
|
198
213
|
return `${heading}:\n${this.conflicts.map((conflict) => ` ${conflict}`).join("\n")}`;
|
|
199
214
|
}
|
|
200
215
|
}
|
|
@@ -210,6 +225,7 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
|
|
|
210
225
|
const SKILL_FAMILIES: SkillCatalog = {
|
|
211
226
|
effect: ["effect-ts", "effect-atom-data-fetching"],
|
|
212
227
|
};
|
|
228
|
+
|
|
213
229
|
export const DEFAULT_MANIFEST = "dev-kit.jsonc";
|
|
214
230
|
const DEFAULT_LOCKFILE = "dev-kit.lock.json";
|
|
215
231
|
const DEFAULT_STATE = ".dev-kit/state.json";
|
|
@@ -219,6 +235,7 @@ const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
|
|
|
219
235
|
const resolvePackageRoot = Effect.fn("resolvePackageRoot")(function* () {
|
|
220
236
|
const path = yield* Path.Path;
|
|
221
237
|
const scriptPath = yield* path.fromFileUrl(new URL(import.meta.url));
|
|
238
|
+
|
|
222
239
|
return path.resolve(path.dirname(scriptPath), "..");
|
|
223
240
|
});
|
|
224
241
|
|
|
@@ -234,9 +251,11 @@ const runCommand = Effect.fn("runCommand")(function* (
|
|
|
234
251
|
child.exitCode,
|
|
235
252
|
]);
|
|
236
253
|
const trimmed = output.trim();
|
|
254
|
+
|
|
237
255
|
if (exitCode !== 0) {
|
|
238
256
|
return yield* new CommandError({ command: formatted, exitCode, output: trimmed });
|
|
239
257
|
}
|
|
258
|
+
|
|
240
259
|
return trimmed;
|
|
241
260
|
});
|
|
242
261
|
|
|
@@ -252,12 +271,14 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
|
|
|
252
271
|
const errors: Array<ParseError> = [];
|
|
253
272
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
254
273
|
const first = errors[0];
|
|
274
|
+
|
|
255
275
|
if (first !== undefined) {
|
|
256
276
|
return yield* new StructuredFileError({
|
|
257
277
|
path: filePath,
|
|
258
278
|
message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
|
|
259
279
|
});
|
|
260
280
|
}
|
|
281
|
+
|
|
261
282
|
return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
|
|
262
283
|
Effect.mapError((cause) => new StructuredFileError({ path: filePath, message: cause.message })),
|
|
263
284
|
);
|
|
@@ -265,10 +286,12 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
|
|
|
265
286
|
|
|
266
287
|
const readManifest = Effect.fn("readManifest")(function* (manifestPath: string) {
|
|
267
288
|
const fs = yield* FileSystem.FileSystem;
|
|
289
|
+
|
|
268
290
|
if (!(yield* fs.exists(manifestPath))) {
|
|
269
291
|
return yield* new ManifestNotFoundError({ path: manifestPath });
|
|
270
292
|
}
|
|
271
293
|
const raw = yield* fs.readFileString(manifestPath);
|
|
294
|
+
|
|
272
295
|
return yield* parseStructuredFile(manifestPath, raw, DevKitManifestSchema);
|
|
273
296
|
});
|
|
274
297
|
|
|
@@ -277,9 +300,11 @@ const readOptionalStructuredFile = Effect.fn("readOptionalStructuredFile")(funct
|
|
|
277
300
|
schema: Schema.ConstraintDecoder<A>,
|
|
278
301
|
) {
|
|
279
302
|
const fs = yield* FileSystem.FileSystem;
|
|
303
|
+
|
|
280
304
|
if (!(yield* fs.exists(filePath))) {
|
|
281
305
|
return undefined;
|
|
282
306
|
}
|
|
307
|
+
|
|
283
308
|
return yield* parseStructuredFile(filePath, yield* fs.readFileString(filePath), schema);
|
|
284
309
|
});
|
|
285
310
|
|
|
@@ -291,6 +316,7 @@ const expandSelection = (
|
|
|
291
316
|
) => {
|
|
292
317
|
const known = [...new Set([...Object.keys(skillFamilies), ...availableSkills])].sort();
|
|
293
318
|
const selected = new Set<string>();
|
|
319
|
+
|
|
294
320
|
for (const name of include) {
|
|
295
321
|
if (skillFamilies[name]) {
|
|
296
322
|
for (const skill of skillFamilies[name]) selected.add(skill);
|
|
@@ -302,9 +328,11 @@ const expandSelection = (
|
|
|
302
328
|
}
|
|
303
329
|
for (const name of exclude) {
|
|
304
330
|
const family = skillFamilies[name];
|
|
331
|
+
|
|
305
332
|
if (family) for (const skill of family) selected.delete(skill);
|
|
306
333
|
else selected.delete(name);
|
|
307
334
|
}
|
|
335
|
+
|
|
308
336
|
return Effect.succeed([...selected].sort());
|
|
309
337
|
};
|
|
310
338
|
|
|
@@ -316,20 +344,35 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
|
|
|
316
344
|
candidate: string,
|
|
317
345
|
) {
|
|
318
346
|
const path = yield* Path.Path;
|
|
347
|
+
|
|
319
348
|
if (candidate.length === 0 || path.isAbsolute(candidate)) {
|
|
320
|
-
return yield* new UnsafeManagedPathError({
|
|
349
|
+
return yield* new UnsafeManagedPathError({
|
|
350
|
+
path: candidate,
|
|
351
|
+
reason: "must be a non-empty project-relative path",
|
|
352
|
+
});
|
|
321
353
|
}
|
|
322
354
|
const absolute = path.resolve(projectDir, candidate);
|
|
323
355
|
const relative = path.relative(projectDir, absolute);
|
|
324
|
-
|
|
325
|
-
|
|
356
|
+
|
|
357
|
+
if (
|
|
358
|
+
relative.length === 0 ||
|
|
359
|
+
relative === ".." ||
|
|
360
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
361
|
+
path.isAbsolute(relative)
|
|
362
|
+
) {
|
|
363
|
+
return yield* new UnsafeManagedPathError({
|
|
364
|
+
path: candidate,
|
|
365
|
+
reason: "resolves outside the project",
|
|
366
|
+
});
|
|
326
367
|
}
|
|
327
368
|
|
|
328
369
|
const segments = relative.split(path.sep);
|
|
329
370
|
let ancestor = projectDir;
|
|
371
|
+
|
|
330
372
|
for (const segment of segments.slice(0, -1)) {
|
|
331
373
|
ancestor = path.join(ancestor, segment);
|
|
332
374
|
const target = yield* observeSymbolicLink(ancestor);
|
|
375
|
+
|
|
333
376
|
if (target.kind === "symlink") {
|
|
334
377
|
return yield* new UnsafeManagedPathError({
|
|
335
378
|
path: candidate,
|
|
@@ -337,6 +380,7 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
|
|
|
337
380
|
});
|
|
338
381
|
}
|
|
339
382
|
}
|
|
383
|
+
|
|
340
384
|
return { absolute, relative: portablePath(path, relative) } satisfies ManagedPath;
|
|
341
385
|
});
|
|
342
386
|
|
|
@@ -349,12 +393,14 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
|
|
|
349
393
|
outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
|
|
350
394
|
) {
|
|
351
395
|
const outputPaths = new Set<string>();
|
|
396
|
+
|
|
352
397
|
for (const output of outputs) {
|
|
353
398
|
outputPaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
|
|
354
399
|
}
|
|
355
400
|
|
|
356
401
|
for (let index = 0; index < reserved.length; index += 1) {
|
|
357
402
|
const current = reserved[index];
|
|
403
|
+
|
|
358
404
|
if (current === undefined) continue;
|
|
359
405
|
for (const other of reserved.slice(index + 1)) {
|
|
360
406
|
if (pathsOverlap(current.path, other.path)) {
|
|
@@ -385,6 +431,27 @@ const outputIdentity = (output: ManagedOutput) =>
|
|
|
385
431
|
: { sourcePath: output.sourcePath }),
|
|
386
432
|
});
|
|
387
433
|
|
|
434
|
+
const outputOwnershipIdentity = (output: ManagedOutput) =>
|
|
435
|
+
JSON.stringify({
|
|
436
|
+
resourceId: output.resourceId,
|
|
437
|
+
path: output.path,
|
|
438
|
+
mode: output.mode,
|
|
439
|
+
kind: output.kind,
|
|
440
|
+
...("skill" in output
|
|
441
|
+
? { skill: output.skill, target: output.target }
|
|
442
|
+
: { sourcePath: output.sourcePath }),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const usesRawFileModeDigests = (toolVersion: string): boolean => {
|
|
446
|
+
const match = /^(\d+)\.(\d+)\./.exec(toolVersion);
|
|
447
|
+
|
|
448
|
+
if (match === null) return false;
|
|
449
|
+
const major = Number(match[1]);
|
|
450
|
+
const minor = Number(match[2]);
|
|
451
|
+
|
|
452
|
+
return major === 0 && minor <= 6;
|
|
453
|
+
};
|
|
454
|
+
|
|
388
455
|
const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
389
456
|
projectDir: string,
|
|
390
457
|
outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
|
|
@@ -393,12 +460,17 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
|
393
460
|
const ids = new Set<string>();
|
|
394
461
|
const paths = new Set<string>();
|
|
395
462
|
const sortedPaths: Array<string> = [];
|
|
463
|
+
|
|
396
464
|
for (const output of outputs) {
|
|
397
465
|
if (ids.has(output.resourceId)) {
|
|
398
|
-
return yield* new InvalidProjectStateError({
|
|
466
|
+
return yield* new InvalidProjectStateError({
|
|
467
|
+
message: `${label} contains duplicate resource id ${output.resourceId}`,
|
|
468
|
+
});
|
|
399
469
|
}
|
|
400
470
|
if (paths.has(output.path)) {
|
|
401
|
-
return yield* new InvalidProjectStateError({
|
|
471
|
+
return yield* new InvalidProjectStateError({
|
|
472
|
+
message: `${label} contains duplicate path ${output.path}`,
|
|
473
|
+
});
|
|
402
474
|
}
|
|
403
475
|
ids.add(output.resourceId);
|
|
404
476
|
paths.add(output.path);
|
|
@@ -408,9 +480,12 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
|
408
480
|
for (let index = 1; index < sortedPaths.length; index += 1) {
|
|
409
481
|
const previous = sortedPaths[index - 1];
|
|
410
482
|
const current = sortedPaths[index];
|
|
483
|
+
|
|
411
484
|
if (previous === undefined || current === undefined) continue;
|
|
412
485
|
if (current.startsWith(`${previous}/`)) {
|
|
413
|
-
return yield* new InvalidProjectStateError({
|
|
486
|
+
return yield* new InvalidProjectStateError({
|
|
487
|
+
message: `${label} contains overlapping paths ${previous} and ${current}`,
|
|
488
|
+
});
|
|
414
489
|
}
|
|
415
490
|
}
|
|
416
491
|
});
|
|
@@ -420,13 +495,16 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
|
|
|
420
495
|
outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
|
|
421
496
|
) {
|
|
422
497
|
const uniquePaths = new Set<string>();
|
|
498
|
+
|
|
423
499
|
for (const output of outputs) {
|
|
424
500
|
uniquePaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
|
|
425
501
|
}
|
|
426
502
|
const sortedPaths = [...uniquePaths].sort();
|
|
503
|
+
|
|
427
504
|
for (let index = 1; index < sortedPaths.length; index += 1) {
|
|
428
505
|
const previous = sortedPaths[index - 1];
|
|
429
506
|
const current = sortedPaths[index];
|
|
507
|
+
|
|
430
508
|
if (previous === undefined || current === undefined) continue;
|
|
431
509
|
if (current.startsWith(`${previous}/`)) {
|
|
432
510
|
return yield* new InvalidProjectStateError({
|
|
@@ -444,12 +522,14 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
|
|
|
444
522
|
const fs = yield* FileSystem.FileSystem;
|
|
445
523
|
const path = yield* Path.Path;
|
|
446
524
|
const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
|
|
525
|
+
|
|
447
526
|
if ((yield* observePath(templatePath)).kind !== "file") {
|
|
448
527
|
return yield* new InvalidProjectStateError({
|
|
449
528
|
message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
|
|
450
529
|
});
|
|
451
530
|
}
|
|
452
531
|
const template = yield* fs.readFileString(templatePath);
|
|
532
|
+
|
|
453
533
|
if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
|
|
454
534
|
return yield* new InvalidProjectStateError({
|
|
455
535
|
message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
|
|
@@ -457,28 +537,50 @@ const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
|
|
|
457
537
|
}
|
|
458
538
|
|
|
459
539
|
const devKitSkill = sourceBySkill.get("dev-kit");
|
|
460
|
-
const devKitSkillPath =
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
540
|
+
const devKitSkillPath =
|
|
541
|
+
devKitSkill === undefined
|
|
542
|
+
? "node_modules/@danieljvdm/dev-kit/skills/dev-kit/SKILL.md"
|
|
543
|
+
: portablePath(
|
|
544
|
+
path,
|
|
545
|
+
path.relative(
|
|
546
|
+
projectDir,
|
|
547
|
+
path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
|
|
548
|
+
),
|
|
549
|
+
);
|
|
469
550
|
const sections = [template.replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath).trimEnd()];
|
|
551
|
+
|
|
470
552
|
if ((yield* readDirectDependencyNames(projectDir)).includes("vite-plus")) {
|
|
471
553
|
const vitePlusTemplate = path.join(projectDir, "node_modules", "vite-plus", "AGENTS.md");
|
|
554
|
+
|
|
472
555
|
if ((yield* observePath(vitePlusTemplate)).kind !== "file") {
|
|
473
556
|
return yield* new InvalidProjectStateError({
|
|
474
|
-
message:
|
|
557
|
+
message:
|
|
558
|
+
"Vite+ is a direct dependency but its agent instructions are not a regular file: node_modules/vite-plus/AGENTS.md",
|
|
475
559
|
});
|
|
476
560
|
}
|
|
477
561
|
sections.push((yield* fs.readFileString(vitePlusTemplate)).trim());
|
|
478
562
|
}
|
|
563
|
+
|
|
479
564
|
return `${sections.join("\n\n")}\n`;
|
|
480
565
|
});
|
|
481
566
|
|
|
567
|
+
const readGeneratedFileTemplate = Effect.fn("readGeneratedFileTemplate")(function* (
|
|
568
|
+
packageRoot: string,
|
|
569
|
+
sourcePath: string,
|
|
570
|
+
) {
|
|
571
|
+
const fs = yield* FileSystem.FileSystem;
|
|
572
|
+
const path = yield* Path.Path;
|
|
573
|
+
const templatePath = path.join(packageRoot, sourcePath);
|
|
574
|
+
|
|
575
|
+
if ((yield* observePath(templatePath)).kind !== "file") {
|
|
576
|
+
return yield* new InvalidProjectStateError({
|
|
577
|
+
message: `dev-kit generated file template is not a regular file: ${sourcePath}`,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return yield* fs.readFileString(templatePath);
|
|
582
|
+
});
|
|
583
|
+
|
|
482
584
|
const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
483
585
|
packageRoot: string,
|
|
484
586
|
projectDir: string,
|
|
@@ -489,9 +591,11 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
489
591
|
) {
|
|
490
592
|
const path = yield* Path.Path;
|
|
491
593
|
const outputs: Array<DesiredOutput> = [];
|
|
594
|
+
|
|
492
595
|
if (setup.agentInstructions.enabled) {
|
|
493
596
|
const managed = yield* resolveManagedPath(projectDir, "AGENTS.md");
|
|
494
597
|
const content = yield* renderAgentInstructions(packageRoot, projectDir, sourceBySkill);
|
|
598
|
+
|
|
495
599
|
outputs.push({
|
|
496
600
|
resourceId: "setup:agent-instructions",
|
|
497
601
|
path: managed.relative,
|
|
@@ -508,6 +612,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
508
612
|
const sourceObservation = setup.agentInstructions.enabled
|
|
509
613
|
? undefined
|
|
510
614
|
: yield* observePath(source.absolute);
|
|
615
|
+
|
|
511
616
|
if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
|
|
512
617
|
return yield* new InvalidProjectStateError({
|
|
513
618
|
message: "Claude instructions source is not a regular file: AGENTS.md",
|
|
@@ -515,6 +620,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
515
620
|
}
|
|
516
621
|
const managed = yield* resolveManagedPath(projectDir, "CLAUDE.md");
|
|
517
622
|
const linkTarget = path.relative(path.dirname(managed.absolute), source.absolute);
|
|
623
|
+
|
|
518
624
|
outputs.push({
|
|
519
625
|
resourceId: "setup:claude-instructions",
|
|
520
626
|
path: managed.relative,
|
|
@@ -526,32 +632,71 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
526
632
|
linkTarget,
|
|
527
633
|
});
|
|
528
634
|
}
|
|
635
|
+
if (setup.vitePlus.quality.enabled) {
|
|
636
|
+
for (const generated of [
|
|
637
|
+
{
|
|
638
|
+
resourceId: "setup:vite-plus-config" as const,
|
|
639
|
+
path: VITE_PLUS_CONFIG_PATH,
|
|
640
|
+
sourcePath: VITE_PLUS_CONFIG_TEMPLATE,
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
resourceId: "setup:vite-plus-github-actions" as const,
|
|
644
|
+
path: VITE_PLUS_GITHUB_ACTIONS_PATH,
|
|
645
|
+
sourcePath: VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
|
|
646
|
+
},
|
|
647
|
+
]) {
|
|
648
|
+
const managed = yield* resolveManagedPath(projectDir, generated.path);
|
|
649
|
+
const content = yield* readGeneratedFileTemplate(packageRoot, generated.sourcePath);
|
|
650
|
+
|
|
651
|
+
outputs.push({
|
|
652
|
+
resourceId: generated.resourceId,
|
|
653
|
+
path: managed.relative,
|
|
654
|
+
sourcePath: generated.sourcePath,
|
|
655
|
+
mode: "copy",
|
|
656
|
+
kind: "file",
|
|
657
|
+
digest: yield* digestFileContent(content),
|
|
658
|
+
destination: managed.absolute,
|
|
659
|
+
content,
|
|
660
|
+
adoptIfExact: true,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
}
|
|
529
664
|
const agentsTarget = targets.agents;
|
|
530
|
-
const duplicateOutput = skills.find(
|
|
531
|
-
skills.findIndex((candidate) => candidate.name === skill.name) !== index
|
|
665
|
+
const duplicateOutput = skills.find(
|
|
666
|
+
(skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
|
|
532
667
|
);
|
|
668
|
+
|
|
533
669
|
if (duplicateOutput !== undefined) {
|
|
534
670
|
const selectors = skills
|
|
535
671
|
.filter((skill) => skill.name === duplicateOutput.name)
|
|
536
672
|
.map((skill) => skill.selector);
|
|
673
|
+
|
|
537
674
|
return yield* new InvalidProjectStateError({
|
|
538
675
|
message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
|
|
539
676
|
});
|
|
540
677
|
}
|
|
541
678
|
for (const skill of skills) {
|
|
542
679
|
const resolvedSource = sourceBySkill.get(skill.selector);
|
|
680
|
+
|
|
543
681
|
if (resolvedSource === undefined) {
|
|
544
|
-
return yield* new InvalidProjectStateError({
|
|
682
|
+
return yield* new InvalidProjectStateError({
|
|
683
|
+
message: `skill source is unavailable: ${skill.selector}`,
|
|
684
|
+
});
|
|
545
685
|
}
|
|
546
686
|
const source = resolvedSource.path;
|
|
547
687
|
const sourceObservation = yield* observePath(source);
|
|
688
|
+
|
|
548
689
|
if (sourceObservation.kind !== "directory") {
|
|
549
|
-
return yield* new InvalidProjectStateError({
|
|
690
|
+
return yield* new InvalidProjectStateError({
|
|
691
|
+
message: `skill source is not a directory: ${source}`,
|
|
692
|
+
});
|
|
550
693
|
}
|
|
551
694
|
for (const targetName of ["agents", "claude", "opencode"] as const) {
|
|
552
695
|
const target = targets[targetName];
|
|
696
|
+
|
|
553
697
|
if (!target.enabled) continue;
|
|
554
698
|
const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill.name));
|
|
699
|
+
|
|
555
700
|
if (target.mode === "copy") {
|
|
556
701
|
outputs.push({
|
|
557
702
|
resourceId: `skill:${skill.selector}@${targetName}`,
|
|
@@ -569,10 +714,12 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
569
714
|
}
|
|
570
715
|
const linkSource =
|
|
571
716
|
targetName === "agents" || !agentsTarget.enabled
|
|
572
|
-
? resolvedSource.linkPath ?? source
|
|
573
|
-
: (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name)))
|
|
717
|
+
? (resolvedSource.linkPath ?? source)
|
|
718
|
+
: (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name)))
|
|
719
|
+
.absolute;
|
|
574
720
|
const linkTarget = path.relative(path.dirname(managed.absolute), linkSource);
|
|
575
721
|
const linkDigest = yield* digestSymlinkTarget(linkTarget);
|
|
722
|
+
|
|
576
723
|
outputs.push({
|
|
577
724
|
resourceId: `skill:${skill.selector}@${targetName}`,
|
|
578
725
|
path: managed.relative,
|
|
@@ -589,6 +736,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
589
736
|
}
|
|
590
737
|
}
|
|
591
738
|
yield* validateInventory(projectDir, outputs, "desired outputs");
|
|
739
|
+
|
|
592
740
|
return outputs.sort((left, right) => left.path.localeCompare(right.path));
|
|
593
741
|
});
|
|
594
742
|
|
|
@@ -606,7 +754,9 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
606
754
|
if (currentState) yield* validateInventory(projectDir, currentState.outputs, "applied state");
|
|
607
755
|
yield* validateCrossInventoryPaths(projectDir, [...desired, ...(currentState?.outputs ?? [])]);
|
|
608
756
|
const lockById = new Map(currentLock?.outputs.map((output) => [output.resourceId, output]) ?? []);
|
|
609
|
-
const receiptsById = new Map(
|
|
757
|
+
const receiptsById = new Map(
|
|
758
|
+
currentState?.outputs.map((output) => [output.resourceId, output]) ?? [],
|
|
759
|
+
);
|
|
610
760
|
const desiredKeys = new Set(desired.map((output) => `${output.resourceId}\0${output.path}`));
|
|
611
761
|
const actions: Array<SkillPlanAction> = [];
|
|
612
762
|
|
|
@@ -615,27 +765,53 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
615
765
|
const receipt = receiptsById.get(output.resourceId);
|
|
616
766
|
const sameReceipt = receipt?.path === output.path ? receipt : undefined;
|
|
617
767
|
const locked = lockById.get(output.resourceId);
|
|
618
|
-
const
|
|
768
|
+
const matchingLockedOutput =
|
|
769
|
+
locked !== undefined &&
|
|
770
|
+
outputOwnershipIdentity(locked) === outputOwnershipIdentity(output) &&
|
|
771
|
+
observed.kind === locked.kind
|
|
772
|
+
? locked
|
|
773
|
+
: undefined;
|
|
774
|
+
const rawModeObservation =
|
|
775
|
+
matchingLockedOutput !== undefined &&
|
|
776
|
+
observed.kind !== "missing" &&
|
|
777
|
+
observed.digest !== matchingLockedOutput.digest &&
|
|
778
|
+
currentLock !== undefined &&
|
|
779
|
+
usesRawFileModeDigests(currentLock.toolVersion)
|
|
780
|
+
? yield* observePathWithRawModes(output.destination)
|
|
781
|
+
: undefined;
|
|
782
|
+
const lockedOwnsObserved =
|
|
783
|
+
matchingLockedOutput !== undefined &&
|
|
784
|
+
observed.kind !== "missing" &&
|
|
785
|
+
(observed.digest === matchingLockedOutput.digest ||
|
|
786
|
+
(rawModeObservation?.kind === matchingLockedOutput.kind &&
|
|
787
|
+
rawModeObservation.digest === matchingLockedOutput.digest));
|
|
619
788
|
|
|
620
789
|
if (observed.kind === "missing") {
|
|
621
790
|
actions.push({ action: "create", desired: output, observed });
|
|
622
791
|
} else if (observed.kind === output.kind && observed.digest === output.digest) {
|
|
623
|
-
if (sameReceipt ||
|
|
792
|
+
if (sameReceipt || lockedOwnsObserved || ("adoptIfExact" in output && output.adoptIfExact)) {
|
|
624
793
|
actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
|
|
625
794
|
} else {
|
|
626
|
-
actions.push({
|
|
795
|
+
actions.push({
|
|
796
|
+
action: "conflict",
|
|
797
|
+
path: output.path,
|
|
798
|
+
reason: "destination exists but is not owned",
|
|
799
|
+
});
|
|
627
800
|
}
|
|
628
801
|
} else if (
|
|
629
|
-
sameReceipt &&
|
|
630
|
-
|
|
631
|
-
|
|
802
|
+
(sameReceipt !== undefined &&
|
|
803
|
+
observed.kind === sameReceipt.kind &&
|
|
804
|
+
observed.digest === sameReceipt.digest) ||
|
|
805
|
+
lockedOwnsObserved
|
|
632
806
|
) {
|
|
633
807
|
actions.push({ action: "update", desired: output, observed });
|
|
634
808
|
} else {
|
|
635
809
|
actions.push({
|
|
636
810
|
action: "conflict",
|
|
637
811
|
path: output.path,
|
|
638
|
-
reason: sameReceipt
|
|
812
|
+
reason: sameReceipt
|
|
813
|
+
? "owned destination was modified"
|
|
814
|
+
: "destination exists but is not owned",
|
|
639
815
|
});
|
|
640
816
|
}
|
|
641
817
|
}
|
|
@@ -644,11 +820,21 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
644
820
|
if (desiredKeys.has(`${receipt.resourceId}\0${receipt.path}`)) continue;
|
|
645
821
|
const managed = yield* resolveManagedPath(projectDir, receipt.path);
|
|
646
822
|
const observed = yield* observePath(managed.absolute);
|
|
823
|
+
|
|
647
824
|
if (observed.kind === "missing") continue;
|
|
648
825
|
if (observed.kind === receipt.kind && observed.digest === receipt.digest) {
|
|
649
|
-
actions.push({
|
|
826
|
+
actions.push({
|
|
827
|
+
action: "remove",
|
|
828
|
+
previous: receipt,
|
|
829
|
+
destination: managed.absolute,
|
|
830
|
+
observed,
|
|
831
|
+
});
|
|
650
832
|
} else {
|
|
651
|
-
actions.push({
|
|
833
|
+
actions.push({
|
|
834
|
+
action: "conflict",
|
|
835
|
+
path: receipt.path,
|
|
836
|
+
reason: "stale owned destination was modified",
|
|
837
|
+
});
|
|
652
838
|
}
|
|
653
839
|
}
|
|
654
840
|
|
|
@@ -663,11 +849,26 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
663
849
|
digest,
|
|
664
850
|
})),
|
|
665
851
|
};
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
actions: actions.sort((left, right) => {
|
|
855
|
+
const leftPath =
|
|
856
|
+
left.action === "remove"
|
|
857
|
+
? left.previous.path
|
|
858
|
+
: left.action === "conflict"
|
|
859
|
+
? left.path
|
|
860
|
+
: left.desired.path;
|
|
861
|
+
const rightPath =
|
|
862
|
+
right.action === "remove"
|
|
863
|
+
? right.previous.path
|
|
864
|
+
: right.action === "conflict"
|
|
865
|
+
? right.path
|
|
866
|
+
: right.desired.path;
|
|
867
|
+
|
|
868
|
+
return leftPath.localeCompare(rightPath);
|
|
869
|
+
}),
|
|
870
|
+
nextState,
|
|
871
|
+
};
|
|
671
872
|
});
|
|
672
873
|
|
|
673
874
|
const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
|
|
@@ -677,6 +878,7 @@ const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
|
|
|
677
878
|
current.outputs.length === next.outputs.length &&
|
|
678
879
|
current.outputs.every((output, index) => {
|
|
679
880
|
const nextOutput = next.outputs[index];
|
|
881
|
+
|
|
680
882
|
return nextOutput !== undefined && outputIdentity(output) === outputIdentity(nextOutput);
|
|
681
883
|
});
|
|
682
884
|
|
|
@@ -692,12 +894,32 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
692
894
|
),
|
|
693
895
|
);
|
|
694
896
|
const projectDir = yield* fs.realPath(discoveredRoot);
|
|
695
|
-
const manifestManaged = yield* resolveManagedPath(
|
|
696
|
-
|
|
897
|
+
const manifestManaged = yield* resolveManagedPath(
|
|
898
|
+
projectDir,
|
|
899
|
+
options.manifestPath ?? DEFAULT_MANIFEST,
|
|
900
|
+
);
|
|
901
|
+
const lockManaged = yield* resolveManagedPath(
|
|
902
|
+
projectDir,
|
|
903
|
+
options.lockfilePath ?? DEFAULT_LOCKFILE,
|
|
904
|
+
);
|
|
697
905
|
const stateManaged = yield* resolveManagedPath(projectDir, options.statePath ?? DEFAULT_STATE);
|
|
698
906
|
const processLockManaged = yield* resolveManagedPath(projectDir, PROJECT_PROCESS_LOCK_PATH);
|
|
699
907
|
const packageRoot = yield* resolvePackageRoot();
|
|
700
908
|
const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
|
|
909
|
+
|
|
910
|
+
if (manifest.setup.vitePlus.quality.enabled) {
|
|
911
|
+
if (!manifest.setup.effectTsgo.enabled) {
|
|
912
|
+
return yield* new InvalidProjectStateError({
|
|
913
|
+
message:
|
|
914
|
+
"setup.vitePlus.quality requires setup.effectTsgo.enabled so vp run typecheck uses the Effect-patched compiler",
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
yield* validateVitePlusQualitySupport(
|
|
918
|
+
projectDir,
|
|
919
|
+
packageRoot,
|
|
920
|
+
manifest.setup.effectTsgo.typescriptPackage,
|
|
921
|
+
);
|
|
922
|
+
}
|
|
701
923
|
const effectSource = manifest.setup.effectSource.enabled
|
|
702
924
|
? yield* planEffectSource({
|
|
703
925
|
packageName: manifest.setup.effectSource.packageName,
|
|
@@ -713,19 +935,35 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
713
935
|
typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
|
|
714
936
|
})
|
|
715
937
|
: undefined;
|
|
938
|
+
const vitePlusHooks = manifest.setup.vitePlus.hooks.enabled
|
|
939
|
+
? yield* planVitePlusHooks(projectDir)
|
|
940
|
+
: undefined;
|
|
716
941
|
const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
|
|
717
942
|
const availableSkills = catalog.skills.map((skill) => skill.selector);
|
|
718
943
|
const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
|
|
944
|
+
|
|
719
945
|
for (const [family, familySkills] of Object.entries(skillFamilies)) {
|
|
720
946
|
if (availableSkills.includes(family)) {
|
|
721
|
-
return yield* new InvalidSkillCatalogError({
|
|
947
|
+
return yield* new InvalidSkillCatalogError({
|
|
948
|
+
family,
|
|
949
|
+
message: `family name conflicts with a skill name: ${family}`,
|
|
950
|
+
});
|
|
722
951
|
}
|
|
723
952
|
const missing = familySkills.filter((skill) => !availableSkills.includes(skill));
|
|
953
|
+
|
|
724
954
|
if (missing.length > 0) {
|
|
725
|
-
return yield* new InvalidSkillCatalogError({
|
|
955
|
+
return yield* new InvalidSkillCatalogError({
|
|
956
|
+
family,
|
|
957
|
+
message: `family references missing skills: ${missing.join(", ")}`,
|
|
958
|
+
});
|
|
726
959
|
}
|
|
727
960
|
}
|
|
728
|
-
const selectedSelectors = yield* expandSelection(
|
|
961
|
+
const selectedSelectors = yield* expandSelection(
|
|
962
|
+
manifest.include,
|
|
963
|
+
manifest.exclude,
|
|
964
|
+
availableSkills,
|
|
965
|
+
skillFamilies,
|
|
966
|
+
);
|
|
729
967
|
const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
|
|
730
968
|
const sourceBySkill = yield* withSpinner(
|
|
731
969
|
"Resolving selected skills",
|
|
@@ -738,8 +976,10 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
738
976
|
),
|
|
739
977
|
);
|
|
740
978
|
const selectedSkills: Array<CatalogSkill> = [];
|
|
979
|
+
|
|
741
980
|
for (const selector of selectedSelectors) {
|
|
742
981
|
const catalogSkill = catalogBySelector.get(selector);
|
|
982
|
+
|
|
743
983
|
if (catalogSkill === undefined) {
|
|
744
984
|
return yield* new InvalidProjectStateError({
|
|
745
985
|
message: `selected skill is unavailable: ${selector}`,
|
|
@@ -804,6 +1044,17 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
804
1044
|
digest: output.digest,
|
|
805
1045
|
};
|
|
806
1046
|
}
|
|
1047
|
+
if (output.resourceId === "setup:claude-instructions") {
|
|
1048
|
+
return {
|
|
1049
|
+
resourceId: output.resourceId,
|
|
1050
|
+
path: output.path,
|
|
1051
|
+
sourcePath: output.sourcePath,
|
|
1052
|
+
mode: output.mode,
|
|
1053
|
+
kind: output.kind,
|
|
1054
|
+
digest: output.digest,
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
|
|
807
1058
|
return {
|
|
808
1059
|
resourceId: output.resourceId,
|
|
809
1060
|
path: output.path,
|
|
@@ -823,37 +1074,45 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
823
1074
|
? []
|
|
824
1075
|
: [{ label: "Effect source checkout", path: effectSource.path }]),
|
|
825
1076
|
];
|
|
1077
|
+
|
|
826
1078
|
yield* validateReservedPaths(projectDir, reservedPaths, desired);
|
|
827
1079
|
const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
|
|
828
1080
|
const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
|
|
1081
|
+
|
|
829
1082
|
if (
|
|
830
1083
|
manifest.setup.claudeInstructions.enabled &&
|
|
831
1084
|
!manifest.setup.agentInstructions.enabled &&
|
|
832
|
-
currentState?.outputs.some(
|
|
833
|
-
(output) => output.resourceId === "setup:agent-instructions",
|
|
834
|
-
)
|
|
1085
|
+
currentState?.outputs.some((output) => output.resourceId === "setup:agent-instructions")
|
|
835
1086
|
) {
|
|
836
1087
|
return yield* new InvalidProjectStateError({
|
|
837
|
-
message:
|
|
1088
|
+
message:
|
|
1089
|
+
"cannot disable agentInstructions while claudeInstructions still links to its AGENTS.md wrapper",
|
|
838
1090
|
});
|
|
839
1091
|
}
|
|
840
|
-
yield* validateReservedPaths(
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
...(currentLock?.outputs ?? []),
|
|
845
|
-
...(currentState?.outputs ?? []),
|
|
846
|
-
],
|
|
847
|
-
);
|
|
1092
|
+
yield* validateReservedPaths(projectDir, reservedPaths, [
|
|
1093
|
+
...(currentLock?.outputs ?? []),
|
|
1094
|
+
...(currentState?.outputs ?? []),
|
|
1095
|
+
]);
|
|
848
1096
|
if (options.locked) {
|
|
849
1097
|
if (!currentLock) {
|
|
850
|
-
return yield* new LockedPlanMismatchError({
|
|
1098
|
+
return yield* new LockedPlanMismatchError({
|
|
1099
|
+
message: "dev-kit.lock.json is required with --locked",
|
|
1100
|
+
});
|
|
851
1101
|
}
|
|
852
1102
|
if (!lockedPlanMatches(currentLock, nextLock)) {
|
|
853
|
-
return yield* new LockedPlanMismatchError({
|
|
1103
|
+
return yield* new LockedPlanMismatchError({
|
|
1104
|
+
message: "manifest or packaged skills differ from dev-kit.lock.json",
|
|
1105
|
+
});
|
|
854
1106
|
}
|
|
855
1107
|
}
|
|
856
|
-
const planned = yield* planDesiredOutputs(
|
|
1108
|
+
const planned = yield* planDesiredOutputs(
|
|
1109
|
+
projectDir,
|
|
1110
|
+
desired,
|
|
1111
|
+
currentLock,
|
|
1112
|
+
currentState,
|
|
1113
|
+
nextLock,
|
|
1114
|
+
);
|
|
1115
|
+
|
|
857
1116
|
return {
|
|
858
1117
|
projectDir,
|
|
859
1118
|
lockfilePath: lockManaged.absolute,
|
|
@@ -861,6 +1120,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
861
1120
|
actions: planned.actions,
|
|
862
1121
|
...(effectSource === undefined ? {} : { effectSource }),
|
|
863
1122
|
...(effectTsgo === undefined ? {} : { effectTsgo }),
|
|
1123
|
+
...(vitePlusHooks === undefined ? {} : { vitePlusHooks }),
|
|
864
1124
|
nextLock,
|
|
865
1125
|
nextState: planned.nextState,
|
|
866
1126
|
metadataChanged:
|
|
@@ -871,46 +1131,51 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
871
1131
|
|
|
872
1132
|
const formatAction = (action: SkillPlanAction): string => {
|
|
873
1133
|
if (action.action === "conflict") return `! ${action.path}: ${action.reason}`;
|
|
874
|
-
if (action.action === "remove")
|
|
1134
|
+
if (action.action === "remove")
|
|
1135
|
+
return `− ${action.previous.resourceId} → ${action.previous.path}`;
|
|
875
1136
|
const verb = action.desired.mode === "copy" ? "copy" : "link";
|
|
876
1137
|
const adoption = action.action === "unchanged" && action.adopted ? " (adopt)" : "";
|
|
877
1138
|
const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "=";
|
|
878
|
-
const source = "skill" in action.desired
|
|
879
|
-
|
|
880
|
-
: action.desired.sourcePath;
|
|
1139
|
+
const source = "skill" in action.desired ? action.desired.skill : action.desired.sourcePath;
|
|
1140
|
+
|
|
881
1141
|
return `${marker} ${verb} ${source} → ${action.desired.path}${adoption}`;
|
|
882
1142
|
};
|
|
883
1143
|
|
|
884
1144
|
const operationalChangeCount = (plan: SkillPlan): number =>
|
|
885
1145
|
plan.actions.filter((action) => action.action !== "unchanged").length +
|
|
886
1146
|
(plan.effectSource?.action === "sync" ? 1 : 0) +
|
|
887
|
-
(plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0)
|
|
1147
|
+
(plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0) +
|
|
1148
|
+
(plan.vitePlusHooks?.action === "configure" ? 1 : 0);
|
|
888
1149
|
|
|
889
1150
|
const plannedChangeCount = (plan: SkillPlan): number => {
|
|
890
1151
|
const operational = operationalChangeCount(plan);
|
|
1152
|
+
|
|
891
1153
|
return operational === 0 && plan.metadataChanged ? 1 : operational;
|
|
892
1154
|
};
|
|
893
1155
|
|
|
894
1156
|
export const printSkillPlan = Effect.fn("printSkillPlan")(function* (plan: SkillPlan) {
|
|
895
1157
|
const changes = plannedChangeCount(plan);
|
|
1158
|
+
|
|
896
1159
|
if (changes === 0) {
|
|
897
1160
|
yield* printStatus("success", "Already up to date");
|
|
1161
|
+
|
|
898
1162
|
return;
|
|
899
1163
|
}
|
|
900
1164
|
yield* printStatus("plan", `${changes} change${changes === 1 ? "" : "s"} planned`);
|
|
901
1165
|
for (const action of plan.actions) {
|
|
902
|
-
if (action.action !== "unchanged") yield* printDetail(formatAction(action));
|
|
1166
|
+
if (action.action !== "unchanged" || action.adopted) yield* printDetail(formatAction(action));
|
|
903
1167
|
}
|
|
904
1168
|
if (plan.effectSource?.action === "sync") {
|
|
905
|
-
yield* printDetail(
|
|
906
|
-
`+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`,
|
|
907
|
-
);
|
|
1169
|
+
yield* printDetail(`+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`);
|
|
908
1170
|
}
|
|
909
1171
|
if (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched) {
|
|
910
1172
|
yield* printDetail(
|
|
911
1173
|
`+ TypeScript patch @effect/tsgo@${plan.effectTsgo.effectTsgoVersion} → ${plan.effectTsgo.typescriptPackage}@${plan.effectTsgo.typescriptVersion}`,
|
|
912
1174
|
);
|
|
913
1175
|
}
|
|
1176
|
+
if (plan.vitePlusHooks?.action === "configure") {
|
|
1177
|
+
yield* printDetail(`+ Vite+ hooks → ${plan.vitePlusHooks.hooksPath}`);
|
|
1178
|
+
}
|
|
914
1179
|
if (operationalChangeCount(plan) === 0 && plan.metadataChanged) {
|
|
915
1180
|
yield* printDetail("+ Dev kit metadata");
|
|
916
1181
|
}
|
|
@@ -920,22 +1185,24 @@ const observationsEqual = (left: ObservedPath, right: ObservedPath): boolean =>
|
|
|
920
1185
|
left.kind === right.kind &&
|
|
921
1186
|
(left.kind === "missing" || (right.kind !== "missing" && left.digest === right.digest));
|
|
922
1187
|
|
|
923
|
-
const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (
|
|
924
|
-
root: string,
|
|
925
|
-
) {
|
|
1188
|
+
const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (root: string) {
|
|
926
1189
|
const fs = yield* FileSystem.FileSystem;
|
|
927
1190
|
const path = yield* Path.Path;
|
|
928
1191
|
const pending = [root];
|
|
1192
|
+
|
|
929
1193
|
while (pending.length > 0) {
|
|
930
1194
|
const current = pending.pop();
|
|
1195
|
+
|
|
931
1196
|
if (current === undefined) continue;
|
|
932
1197
|
if ((yield* observeSymbolicLink(current)).kind === "symlink") return current;
|
|
933
1198
|
const info = yield* fs.stat(current);
|
|
1199
|
+
|
|
934
1200
|
if (info.type !== "Directory") continue;
|
|
935
1201
|
for (const entry of yield* fs.readDirectory(current)) {
|
|
936
1202
|
pending.push(path.join(current, entry));
|
|
937
1203
|
}
|
|
938
1204
|
}
|
|
1205
|
+
|
|
939
1206
|
return undefined;
|
|
940
1207
|
});
|
|
941
1208
|
|
|
@@ -943,18 +1210,26 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
|
|
|
943
1210
|
plan: SkillPlan,
|
|
944
1211
|
) {
|
|
945
1212
|
const verified = new Set<string>();
|
|
1213
|
+
|
|
946
1214
|
for (const action of plan.actions) {
|
|
947
1215
|
if (action.action === "remove" || action.action === "conflict") continue;
|
|
948
1216
|
if (!("skill" in action.desired)) continue;
|
|
949
1217
|
const catalog = action.desired.catalog;
|
|
1218
|
+
|
|
950
1219
|
if (catalog === undefined || !("package" in catalog)) continue;
|
|
951
1220
|
const selector = `${catalog.package}#${catalog.skill}`;
|
|
952
1221
|
const key = `${selector}\0${catalog.version}\0${catalog.digest}`;
|
|
1222
|
+
|
|
953
1223
|
if (verified.has(key)) continue;
|
|
954
1224
|
const resolved = yield* resolvePackageSkillSelector(plan.projectDir, selector);
|
|
955
1225
|
const observation = yield* observePath(resolved.path);
|
|
956
|
-
|
|
957
|
-
|
|
1226
|
+
|
|
1227
|
+
if (
|
|
1228
|
+
resolved.path !== action.desired.source ||
|
|
1229
|
+
resolved.version !== catalog.version ||
|
|
1230
|
+
observation.kind !== "directory" ||
|
|
1231
|
+
observation.digest !== catalog.digest
|
|
1232
|
+
) {
|
|
958
1233
|
return yield* new ApplyRaceError({ path: action.desired.source });
|
|
959
1234
|
}
|
|
960
1235
|
verified.add(key);
|
|
@@ -963,9 +1238,12 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
|
|
|
963
1238
|
|
|
964
1239
|
const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function* (plan: SkillPlan) {
|
|
965
1240
|
const conflicts = plan.actions.filter((action) => action.action === "conflict");
|
|
1241
|
+
|
|
966
1242
|
if (conflicts.length > 0) {
|
|
967
1243
|
return yield* new PlanConflictError({
|
|
968
|
-
conflicts: conflicts.map((action) =>
|
|
1244
|
+
conflicts: conflicts.map((action) =>
|
|
1245
|
+
action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
|
|
1246
|
+
),
|
|
969
1247
|
});
|
|
970
1248
|
}
|
|
971
1249
|
|
|
@@ -975,10 +1253,15 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
975
1253
|
(action): action is Exclude<SkillPlanAction, { readonly action: "unchanged" | "conflict" }> =>
|
|
976
1254
|
action.action === "create" || action.action === "update" || action.action === "remove",
|
|
977
1255
|
);
|
|
1256
|
+
|
|
978
1257
|
for (const action of mutating) {
|
|
979
|
-
const destination =
|
|
1258
|
+
const destination =
|
|
1259
|
+
action.action === "remove" ? action.destination : action.desired.destination;
|
|
1260
|
+
|
|
980
1261
|
if (!observationsEqual(yield* observePath(destination), action.observed)) {
|
|
981
|
-
return yield* new ApplyRaceError({
|
|
1262
|
+
return yield* new ApplyRaceError({
|
|
1263
|
+
path: action.action === "remove" ? action.previous.path : action.desired.path,
|
|
1264
|
+
});
|
|
982
1265
|
}
|
|
983
1266
|
}
|
|
984
1267
|
|
|
@@ -986,14 +1269,19 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
986
1269
|
return;
|
|
987
1270
|
}
|
|
988
1271
|
|
|
989
|
-
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
1272
|
+
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
1273
|
+
directory: plan.projectDir,
|
|
1274
|
+
prefix: ".dev-kit-apply-",
|
|
1275
|
+
});
|
|
990
1276
|
const stageDir = path.join(tempDir, "stage");
|
|
991
1277
|
const backupDir = path.join(tempDir, "backup");
|
|
992
1278
|
const stagedByResource = new Map<string, string>();
|
|
993
1279
|
let stageIndex = 0;
|
|
1280
|
+
|
|
994
1281
|
for (const action of mutating) {
|
|
995
1282
|
if (action.action === "remove") continue;
|
|
996
1283
|
const staged = path.join(stageDir, String(stageIndex++));
|
|
1284
|
+
|
|
997
1285
|
yield* fs.makeDirectory(path.dirname(staged), { recursive: true });
|
|
998
1286
|
if (action.desired.mode === "copy") {
|
|
999
1287
|
if (action.desired.kind === "file") {
|
|
@@ -1001,6 +1289,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
1001
1289
|
} else {
|
|
1002
1290
|
yield* fs.copy(action.desired.source, staged, { overwrite: true });
|
|
1003
1291
|
const symbolicLink = yield* findNestedSymbolicLink(staged);
|
|
1292
|
+
|
|
1004
1293
|
if (symbolicLink !== undefined) {
|
|
1005
1294
|
return yield* new InvalidProjectStateError({
|
|
1006
1295
|
message: `staged skill contains a symlink: ${action.desired.path}`,
|
|
@@ -1011,8 +1300,11 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
1011
1300
|
yield* fs.symlink(action.desired.linkTarget, staged);
|
|
1012
1301
|
}
|
|
1013
1302
|
const observation = yield* observePath(staged);
|
|
1303
|
+
|
|
1014
1304
|
if (observation.kind !== action.desired.kind || observation.digest !== action.desired.digest) {
|
|
1015
|
-
return yield* new InvalidProjectStateError({
|
|
1305
|
+
return yield* new InvalidProjectStateError({
|
|
1306
|
+
message: `staged output digest mismatch for ${action.desired.path}`,
|
|
1307
|
+
});
|
|
1016
1308
|
}
|
|
1017
1309
|
stagedByResource.set(action.desired.resourceId, staged);
|
|
1018
1310
|
}
|
|
@@ -1021,6 +1313,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
1021
1313
|
|
|
1022
1314
|
const stagedLock = path.join(tempDir, "next-lock.json");
|
|
1023
1315
|
const stagedState = path.join(tempDir, "next-state.json");
|
|
1316
|
+
|
|
1024
1317
|
yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
|
|
1025
1318
|
yield* fs.writeFileString(stagedState, canonicalState(plan.nextState));
|
|
1026
1319
|
|
|
@@ -1033,10 +1326,11 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
1033
1326
|
};
|
|
1034
1327
|
const replacements: Array<Replacement> = [];
|
|
1035
1328
|
let replacementIndex = 0;
|
|
1329
|
+
|
|
1036
1330
|
for (const action of mutating) {
|
|
1037
|
-
const staged =
|
|
1038
|
-
? undefined
|
|
1039
|
-
|
|
1331
|
+
const staged =
|
|
1332
|
+
action.action === "remove" ? undefined : stagedByResource.get(action.desired.resourceId);
|
|
1333
|
+
|
|
1040
1334
|
if (action.action !== "remove" && staged === undefined) {
|
|
1041
1335
|
return yield* new InvalidProjectStateError({
|
|
1042
1336
|
message: `missing staged output for ${action.desired.resourceId}`,
|
|
@@ -1080,6 +1374,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
1080
1374
|
const apply = Effect.gen(function* () {
|
|
1081
1375
|
for (const replacement of replacements) {
|
|
1082
1376
|
const observed = yield* observePath(replacement.destination);
|
|
1377
|
+
|
|
1083
1378
|
if (
|
|
1084
1379
|
replacement.expected !== undefined &&
|
|
1085
1380
|
!observationsEqual(observed, replacement.expected)
|
|
@@ -1099,25 +1394,33 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
1099
1394
|
}
|
|
1100
1395
|
});
|
|
1101
1396
|
|
|
1102
|
-
yield* Effect.uninterruptible(
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
Effect.
|
|
1397
|
+
yield* Effect.uninterruptible(
|
|
1398
|
+
apply.pipe(
|
|
1399
|
+
Effect.catchCause((applyCause) =>
|
|
1400
|
+
rollback.pipe(
|
|
1401
|
+
Effect.catchCause((rollbackCause) =>
|
|
1402
|
+
Effect.failCause(Cause.combine(applyCause, rollbackCause)),
|
|
1403
|
+
),
|
|
1404
|
+
Effect.andThen(Effect.failCause(applyCause)),
|
|
1107
1405
|
),
|
|
1108
|
-
Effect.andThen(Effect.failCause(applyCause)),
|
|
1109
1406
|
),
|
|
1110
1407
|
),
|
|
1111
|
-
)
|
|
1408
|
+
);
|
|
1112
1409
|
});
|
|
1113
1410
|
|
|
1114
|
-
export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
|
|
1411
|
+
export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
|
|
1412
|
+
options: SyncOptions,
|
|
1413
|
+
) {
|
|
1115
1414
|
const plan = yield* planProjectSkills(options);
|
|
1415
|
+
|
|
1116
1416
|
if (options.dryRun) yield* printSkillPlan(plan);
|
|
1117
1417
|
const conflicts = plan.actions.filter((action) => action.action === "conflict");
|
|
1418
|
+
|
|
1118
1419
|
if (conflicts.length > 0) {
|
|
1119
1420
|
return yield* new PlanConflictError({
|
|
1120
|
-
conflicts: conflicts.map((action) =>
|
|
1421
|
+
conflicts: conflicts.map((action) =>
|
|
1422
|
+
action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
|
|
1423
|
+
),
|
|
1121
1424
|
});
|
|
1122
1425
|
}
|
|
1123
1426
|
if (options.dryRun) return;
|
|
@@ -1128,6 +1431,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
|
|
|
1128
1431
|
actions: plan.actions,
|
|
1129
1432
|
effectSource: plan.effectSource,
|
|
1130
1433
|
effectTsgo: plan.effectTsgo,
|
|
1434
|
+
vitePlusHooks: plan.vitePlusHooks,
|
|
1131
1435
|
nextLock: plan.nextLock,
|
|
1132
1436
|
nextState: plan.nextState,
|
|
1133
1437
|
});
|
|
@@ -1135,13 +1439,16 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
|
|
|
1135
1439
|
actions: replanned.actions,
|
|
1136
1440
|
effectSource: replanned.effectSource,
|
|
1137
1441
|
effectTsgo: replanned.effectTsgo,
|
|
1442
|
+
vitePlusHooks: replanned.vitePlusHooks,
|
|
1138
1443
|
nextLock: replanned.nextLock,
|
|
1139
1444
|
nextState: replanned.nextState,
|
|
1140
1445
|
});
|
|
1446
|
+
|
|
1141
1447
|
if (originalSignature !== nextSignature) {
|
|
1142
1448
|
return yield* new ApplyRaceError({ path: "project state" });
|
|
1143
1449
|
}
|
|
1144
1450
|
const changes = plannedChangeCount(replanned);
|
|
1451
|
+
|
|
1145
1452
|
yield* withSpinner(
|
|
1146
1453
|
"Applying dev kit",
|
|
1147
1454
|
Effect.gen(function* () {
|
|
@@ -1151,6 +1458,9 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
|
|
|
1151
1458
|
if (replanned.effectTsgo !== undefined) {
|
|
1152
1459
|
yield* applyEffectTsgoPatchPlan(replanned.effectTsgo);
|
|
1153
1460
|
}
|
|
1461
|
+
if (replanned.vitePlusHooks !== undefined) {
|
|
1462
|
+
yield* applyVitePlusHooksPlan(replanned.vitePlusHooks);
|
|
1463
|
+
}
|
|
1154
1464
|
yield* applyPlannedSkillChanges(replanned);
|
|
1155
1465
|
}),
|
|
1156
1466
|
);
|