@danieljvdm/dev-kit 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -68
- package/dev-kit.example.jsonc +8 -3
- package/package.json +19 -15
- package/schema/dev-kit.schema.json +52 -0
- package/skill-sources.jsonc +8 -12
- package/skill-sources.lock.json +3 -9
- package/skills/dev-kit/SKILL.md +74 -24
- package/skills/effect-atom-data-fetching/SKILL.md +40 -0
- package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
- package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
- package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
- package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
- package/skills/effect-atom-data-fetching/references/testing.md +63 -0
- package/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 +72 -34
- 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 +12 -0
- package/src/manifest.ts +51 -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 +190 -75
- package/src/path-digest.ts +37 -10
- package/src/project-package.ts +59 -0
- package/src/project-process-lock.ts +19 -12
- package/src/project-state.ts +29 -2
- 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 +491 -121
- package/src/vendor.ts +112 -42
- package/src/vite-plus-hooks.ts +174 -0
- package/src/vite-plus-quality.ts +49 -0
- package/templates/AGENTS.md +9 -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,40 +9,50 @@ 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 {
|
|
22
|
+
digestFileContent,
|
|
24
23
|
digestSymlinkTarget,
|
|
25
24
|
digestText,
|
|
26
25
|
observePath,
|
|
27
26
|
type ObservedPath,
|
|
28
27
|
} from "./path-digest.ts";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
28
|
+
import { readDirectDependencyNames } from "./project-package.ts";
|
|
29
|
+
import { acquireProjectProcessLock, PROJECT_PROCESS_LOCK_PATH } from "./project-process-lock.ts";
|
|
31
30
|
import {
|
|
32
31
|
AppliedStateSchema,
|
|
33
32
|
DevKitLockSchema,
|
|
34
33
|
type AppliedState,
|
|
35
34
|
type DevKitLock,
|
|
36
|
-
type
|
|
35
|
+
type ManagedAgentInstructionsOutput,
|
|
36
|
+
type ManagedClaudeInstructionsOutput,
|
|
37
|
+
type ManagedGeneratedFileOutput,
|
|
37
38
|
type ManagedOutput,
|
|
38
39
|
type ManagedSkillOutput,
|
|
39
40
|
type OwnershipReceipt,
|
|
40
41
|
} from "./project-state.ts";
|
|
41
|
-
import {
|
|
42
|
-
acquireProjectProcessLock,
|
|
43
|
-
PROJECT_PROCESS_LOCK_PATH,
|
|
44
|
-
} from "./project-process-lock.ts";
|
|
45
|
-
import { observeSymbolicLink } from "./node-symbolic-link.ts";
|
|
42
|
+
import { parseSkillSelector } from "./skill-selector.ts";
|
|
46
43
|
import { DEV_KIT_VERSION } from "./tool-metadata.ts";
|
|
44
|
+
import {
|
|
45
|
+
applyVitePlusHooksPlan,
|
|
46
|
+
planVitePlusHooks,
|
|
47
|
+
type VitePlusHooksPlan,
|
|
48
|
+
} from "./vite-plus-hooks.ts";
|
|
49
|
+
import {
|
|
50
|
+
validateVitePlusQualitySupport,
|
|
51
|
+
VITE_PLUS_CONFIG_PATH,
|
|
52
|
+
VITE_PLUS_CONFIG_TEMPLATE,
|
|
53
|
+
VITE_PLUS_GITHUB_ACTIONS_PATH,
|
|
54
|
+
VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
|
|
55
|
+
} from "./vite-plus-quality.ts";
|
|
47
56
|
|
|
48
57
|
export type SyncOptions = {
|
|
49
58
|
readonly manifestPath?: string;
|
|
@@ -76,12 +85,27 @@ type DesiredSkillOutput =
|
|
|
76
85
|
readonly linkTarget: string;
|
|
77
86
|
});
|
|
78
87
|
|
|
79
|
-
type
|
|
88
|
+
type DesiredAgentInstructionsOutput = ManagedAgentInstructionsOutput & {
|
|
89
|
+
readonly content: string;
|
|
90
|
+
readonly destination: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
|
|
80
94
|
readonly destination: string;
|
|
81
95
|
readonly linkTarget: string;
|
|
82
96
|
};
|
|
83
97
|
|
|
84
|
-
type
|
|
98
|
+
type DesiredGeneratedFileOutput = ManagedGeneratedFileOutput & {
|
|
99
|
+
readonly adoptIfExact: true;
|
|
100
|
+
readonly content: string;
|
|
101
|
+
readonly destination: string;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
type DesiredOutput =
|
|
105
|
+
| DesiredSkillOutput
|
|
106
|
+
| DesiredAgentInstructionsOutput
|
|
107
|
+
| DesiredClaudeInstructionsOutput
|
|
108
|
+
| DesiredGeneratedFileOutput;
|
|
85
109
|
|
|
86
110
|
type SkillPlanAction =
|
|
87
111
|
| {
|
|
@@ -114,6 +138,7 @@ export type SkillPlan = {
|
|
|
114
138
|
readonly actions: ReadonlyArray<SkillPlanAction>;
|
|
115
139
|
readonly effectSource?: EffectSourcePlan;
|
|
116
140
|
readonly effectTsgo?: EffectTsgoPatchPlan;
|
|
141
|
+
readonly vitePlusHooks?: VitePlusHooksPlan;
|
|
117
142
|
readonly nextLock: DevKitLock;
|
|
118
143
|
readonly nextState: AppliedState;
|
|
119
144
|
readonly metadataChanged: boolean;
|
|
@@ -178,12 +203,12 @@ class LockedPlanMismatchError extends Schema.TaggedErrorClass<LockedPlanMismatch
|
|
|
178
203
|
{ message: Schema.String },
|
|
179
204
|
) {}
|
|
180
205
|
|
|
181
|
-
class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()(
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
) {
|
|
206
|
+
class PlanConflictError extends Schema.TaggedErrorClass<PlanConflictError>()("PlanConflictError", {
|
|
207
|
+
conflicts: Schema.Array(Schema.String),
|
|
208
|
+
}) {
|
|
185
209
|
override get message() {
|
|
186
210
|
const heading = `plan has ${this.conflicts.length} conflict${this.conflicts.length === 1 ? "" : "s"}`;
|
|
211
|
+
|
|
187
212
|
return `${heading}:\n${this.conflicts.map((conflict) => ` ${conflict}`).join("\n")}`;
|
|
188
213
|
}
|
|
189
214
|
}
|
|
@@ -196,14 +221,20 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
|
|
|
196
221
|
}
|
|
197
222
|
}
|
|
198
223
|
|
|
199
|
-
const SKILL_FAMILIES: SkillCatalog = {
|
|
224
|
+
const SKILL_FAMILIES: SkillCatalog = {
|
|
225
|
+
effect: ["effect-ts", "effect-atom-data-fetching"],
|
|
226
|
+
};
|
|
227
|
+
|
|
200
228
|
export const DEFAULT_MANIFEST = "dev-kit.jsonc";
|
|
201
229
|
const DEFAULT_LOCKFILE = "dev-kit.lock.json";
|
|
202
230
|
const DEFAULT_STATE = ".dev-kit/state.json";
|
|
231
|
+
const AGENT_INSTRUCTIONS_TEMPLATE = "templates/AGENTS.md";
|
|
232
|
+
const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
|
|
203
233
|
|
|
204
234
|
const resolvePackageRoot = Effect.fn("resolvePackageRoot")(function* () {
|
|
205
235
|
const path = yield* Path.Path;
|
|
206
236
|
const scriptPath = yield* path.fromFileUrl(new URL(import.meta.url));
|
|
237
|
+
|
|
207
238
|
return path.resolve(path.dirname(scriptPath), "..");
|
|
208
239
|
});
|
|
209
240
|
|
|
@@ -219,9 +250,11 @@ const runCommand = Effect.fn("runCommand")(function* (
|
|
|
219
250
|
child.exitCode,
|
|
220
251
|
]);
|
|
221
252
|
const trimmed = output.trim();
|
|
253
|
+
|
|
222
254
|
if (exitCode !== 0) {
|
|
223
255
|
return yield* new CommandError({ command: formatted, exitCode, output: trimmed });
|
|
224
256
|
}
|
|
257
|
+
|
|
225
258
|
return trimmed;
|
|
226
259
|
});
|
|
227
260
|
|
|
@@ -237,12 +270,14 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
|
|
|
237
270
|
const errors: Array<ParseError> = [];
|
|
238
271
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
239
272
|
const first = errors[0];
|
|
273
|
+
|
|
240
274
|
if (first !== undefined) {
|
|
241
275
|
return yield* new StructuredFileError({
|
|
242
276
|
path: filePath,
|
|
243
277
|
message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
|
|
244
278
|
});
|
|
245
279
|
}
|
|
280
|
+
|
|
246
281
|
return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
|
|
247
282
|
Effect.mapError((cause) => new StructuredFileError({ path: filePath, message: cause.message })),
|
|
248
283
|
);
|
|
@@ -250,10 +285,12 @@ const parseStructuredFile = Effect.fn("parseStructuredFile")(function* <A>(
|
|
|
250
285
|
|
|
251
286
|
const readManifest = Effect.fn("readManifest")(function* (manifestPath: string) {
|
|
252
287
|
const fs = yield* FileSystem.FileSystem;
|
|
288
|
+
|
|
253
289
|
if (!(yield* fs.exists(manifestPath))) {
|
|
254
290
|
return yield* new ManifestNotFoundError({ path: manifestPath });
|
|
255
291
|
}
|
|
256
292
|
const raw = yield* fs.readFileString(manifestPath);
|
|
293
|
+
|
|
257
294
|
return yield* parseStructuredFile(manifestPath, raw, DevKitManifestSchema);
|
|
258
295
|
});
|
|
259
296
|
|
|
@@ -262,9 +299,11 @@ const readOptionalStructuredFile = Effect.fn("readOptionalStructuredFile")(funct
|
|
|
262
299
|
schema: Schema.ConstraintDecoder<A>,
|
|
263
300
|
) {
|
|
264
301
|
const fs = yield* FileSystem.FileSystem;
|
|
302
|
+
|
|
265
303
|
if (!(yield* fs.exists(filePath))) {
|
|
266
304
|
return undefined;
|
|
267
305
|
}
|
|
306
|
+
|
|
268
307
|
return yield* parseStructuredFile(filePath, yield* fs.readFileString(filePath), schema);
|
|
269
308
|
});
|
|
270
309
|
|
|
@@ -276,6 +315,7 @@ const expandSelection = (
|
|
|
276
315
|
) => {
|
|
277
316
|
const known = [...new Set([...Object.keys(skillFamilies), ...availableSkills])].sort();
|
|
278
317
|
const selected = new Set<string>();
|
|
318
|
+
|
|
279
319
|
for (const name of include) {
|
|
280
320
|
if (skillFamilies[name]) {
|
|
281
321
|
for (const skill of skillFamilies[name]) selected.add(skill);
|
|
@@ -287,9 +327,11 @@ const expandSelection = (
|
|
|
287
327
|
}
|
|
288
328
|
for (const name of exclude) {
|
|
289
329
|
const family = skillFamilies[name];
|
|
330
|
+
|
|
290
331
|
if (family) for (const skill of family) selected.delete(skill);
|
|
291
332
|
else selected.delete(name);
|
|
292
333
|
}
|
|
334
|
+
|
|
293
335
|
return Effect.succeed([...selected].sort());
|
|
294
336
|
};
|
|
295
337
|
|
|
@@ -301,20 +343,35 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
|
|
|
301
343
|
candidate: string,
|
|
302
344
|
) {
|
|
303
345
|
const path = yield* Path.Path;
|
|
346
|
+
|
|
304
347
|
if (candidate.length === 0 || path.isAbsolute(candidate)) {
|
|
305
|
-
return yield* new UnsafeManagedPathError({
|
|
348
|
+
return yield* new UnsafeManagedPathError({
|
|
349
|
+
path: candidate,
|
|
350
|
+
reason: "must be a non-empty project-relative path",
|
|
351
|
+
});
|
|
306
352
|
}
|
|
307
353
|
const absolute = path.resolve(projectDir, candidate);
|
|
308
354
|
const relative = path.relative(projectDir, absolute);
|
|
309
|
-
|
|
310
|
-
|
|
355
|
+
|
|
356
|
+
if (
|
|
357
|
+
relative.length === 0 ||
|
|
358
|
+
relative === ".." ||
|
|
359
|
+
relative.startsWith(`..${path.sep}`) ||
|
|
360
|
+
path.isAbsolute(relative)
|
|
361
|
+
) {
|
|
362
|
+
return yield* new UnsafeManagedPathError({
|
|
363
|
+
path: candidate,
|
|
364
|
+
reason: "resolves outside the project",
|
|
365
|
+
});
|
|
311
366
|
}
|
|
312
367
|
|
|
313
368
|
const segments = relative.split(path.sep);
|
|
314
369
|
let ancestor = projectDir;
|
|
370
|
+
|
|
315
371
|
for (const segment of segments.slice(0, -1)) {
|
|
316
372
|
ancestor = path.join(ancestor, segment);
|
|
317
373
|
const target = yield* observeSymbolicLink(ancestor);
|
|
374
|
+
|
|
318
375
|
if (target.kind === "symlink") {
|
|
319
376
|
return yield* new UnsafeManagedPathError({
|
|
320
377
|
path: candidate,
|
|
@@ -322,6 +379,7 @@ const resolveManagedPath = Effect.fn("resolveManagedPath")(function* (
|
|
|
322
379
|
});
|
|
323
380
|
}
|
|
324
381
|
}
|
|
382
|
+
|
|
325
383
|
return { absolute, relative: portablePath(path, relative) } satisfies ManagedPath;
|
|
326
384
|
});
|
|
327
385
|
|
|
@@ -334,12 +392,14 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
|
|
|
334
392
|
outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
|
|
335
393
|
) {
|
|
336
394
|
const outputPaths = new Set<string>();
|
|
395
|
+
|
|
337
396
|
for (const output of outputs) {
|
|
338
397
|
outputPaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
|
|
339
398
|
}
|
|
340
399
|
|
|
341
400
|
for (let index = 0; index < reserved.length; index += 1) {
|
|
342
401
|
const current = reserved[index];
|
|
402
|
+
|
|
343
403
|
if (current === undefined) continue;
|
|
344
404
|
for (const other of reserved.slice(index + 1)) {
|
|
345
405
|
if (pathsOverlap(current.path, other.path)) {
|
|
@@ -378,12 +438,17 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
|
378
438
|
const ids = new Set<string>();
|
|
379
439
|
const paths = new Set<string>();
|
|
380
440
|
const sortedPaths: Array<string> = [];
|
|
441
|
+
|
|
381
442
|
for (const output of outputs) {
|
|
382
443
|
if (ids.has(output.resourceId)) {
|
|
383
|
-
return yield* new InvalidProjectStateError({
|
|
444
|
+
return yield* new InvalidProjectStateError({
|
|
445
|
+
message: `${label} contains duplicate resource id ${output.resourceId}`,
|
|
446
|
+
});
|
|
384
447
|
}
|
|
385
448
|
if (paths.has(output.path)) {
|
|
386
|
-
return yield* new InvalidProjectStateError({
|
|
449
|
+
return yield* new InvalidProjectStateError({
|
|
450
|
+
message: `${label} contains duplicate path ${output.path}`,
|
|
451
|
+
});
|
|
387
452
|
}
|
|
388
453
|
ids.add(output.resourceId);
|
|
389
454
|
paths.add(output.path);
|
|
@@ -393,9 +458,12 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
|
393
458
|
for (let index = 1; index < sortedPaths.length; index += 1) {
|
|
394
459
|
const previous = sortedPaths[index - 1];
|
|
395
460
|
const current = sortedPaths[index];
|
|
461
|
+
|
|
396
462
|
if (previous === undefined || current === undefined) continue;
|
|
397
463
|
if (current.startsWith(`${previous}/`)) {
|
|
398
|
-
return yield* new InvalidProjectStateError({
|
|
464
|
+
return yield* new InvalidProjectStateError({
|
|
465
|
+
message: `${label} contains overlapping paths ${previous} and ${current}`,
|
|
466
|
+
});
|
|
399
467
|
}
|
|
400
468
|
}
|
|
401
469
|
});
|
|
@@ -405,13 +473,16 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
|
|
|
405
473
|
outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
|
|
406
474
|
) {
|
|
407
475
|
const uniquePaths = new Set<string>();
|
|
476
|
+
|
|
408
477
|
for (const output of outputs) {
|
|
409
478
|
uniquePaths.add((yield* resolveManagedPath(projectDir, output.path)).relative);
|
|
410
479
|
}
|
|
411
480
|
const sortedPaths = [...uniquePaths].sort();
|
|
481
|
+
|
|
412
482
|
for (let index = 1; index < sortedPaths.length; index += 1) {
|
|
413
483
|
const previous = sortedPaths[index - 1];
|
|
414
484
|
const current = sortedPaths[index];
|
|
485
|
+
|
|
415
486
|
if (previous === undefined || current === undefined) continue;
|
|
416
487
|
if (current.startsWith(`${previous}/`)) {
|
|
417
488
|
return yield* new InvalidProjectStateError({
|
|
@@ -421,7 +492,75 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
|
|
|
421
492
|
}
|
|
422
493
|
});
|
|
423
494
|
|
|
495
|
+
const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
|
|
496
|
+
packageRoot: string,
|
|
497
|
+
projectDir: string,
|
|
498
|
+
sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
|
|
499
|
+
) {
|
|
500
|
+
const fs = yield* FileSystem.FileSystem;
|
|
501
|
+
const path = yield* Path.Path;
|
|
502
|
+
const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
|
|
503
|
+
|
|
504
|
+
if ((yield* observePath(templatePath)).kind !== "file") {
|
|
505
|
+
return yield* new InvalidProjectStateError({
|
|
506
|
+
message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const template = yield* fs.readFileString(templatePath);
|
|
510
|
+
|
|
511
|
+
if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
|
|
512
|
+
return yield* new InvalidProjectStateError({
|
|
513
|
+
message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const devKitSkill = sourceBySkill.get("dev-kit");
|
|
518
|
+
const devKitSkillPath =
|
|
519
|
+
devKitSkill === undefined
|
|
520
|
+
? "node_modules/@danieljvdm/dev-kit/skills/dev-kit/SKILL.md"
|
|
521
|
+
: portablePath(
|
|
522
|
+
path,
|
|
523
|
+
path.relative(
|
|
524
|
+
projectDir,
|
|
525
|
+
path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
|
|
526
|
+
),
|
|
527
|
+
);
|
|
528
|
+
const sections = [template.replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath).trimEnd()];
|
|
529
|
+
|
|
530
|
+
if ((yield* readDirectDependencyNames(projectDir)).includes("vite-plus")) {
|
|
531
|
+
const vitePlusTemplate = path.join(projectDir, "node_modules", "vite-plus", "AGENTS.md");
|
|
532
|
+
|
|
533
|
+
if ((yield* observePath(vitePlusTemplate)).kind !== "file") {
|
|
534
|
+
return yield* new InvalidProjectStateError({
|
|
535
|
+
message:
|
|
536
|
+
"Vite+ is a direct dependency but its agent instructions are not a regular file: node_modules/vite-plus/AGENTS.md",
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
sections.push((yield* fs.readFileString(vitePlusTemplate)).trim());
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return `${sections.join("\n\n")}\n`;
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const readGeneratedFileTemplate = Effect.fn("readGeneratedFileTemplate")(function* (
|
|
546
|
+
packageRoot: string,
|
|
547
|
+
sourcePath: string,
|
|
548
|
+
) {
|
|
549
|
+
const fs = yield* FileSystem.FileSystem;
|
|
550
|
+
const path = yield* Path.Path;
|
|
551
|
+
const templatePath = path.join(packageRoot, sourcePath);
|
|
552
|
+
|
|
553
|
+
if ((yield* observePath(templatePath)).kind !== "file") {
|
|
554
|
+
return yield* new InvalidProjectStateError({
|
|
555
|
+
message: `dev-kit generated file template is not a regular file: ${sourcePath}`,
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return yield* fs.readFileString(templatePath);
|
|
560
|
+
});
|
|
561
|
+
|
|
424
562
|
const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
563
|
+
packageRoot: string,
|
|
425
564
|
projectDir: string,
|
|
426
565
|
sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
|
|
427
566
|
skills: ReadonlyArray<CatalogSkill>,
|
|
@@ -430,16 +569,36 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
430
569
|
) {
|
|
431
570
|
const path = yield* Path.Path;
|
|
432
571
|
const outputs: Array<DesiredOutput> = [];
|
|
572
|
+
|
|
573
|
+
if (setup.agentInstructions.enabled) {
|
|
574
|
+
const managed = yield* resolveManagedPath(projectDir, "AGENTS.md");
|
|
575
|
+
const content = yield* renderAgentInstructions(packageRoot, projectDir, sourceBySkill);
|
|
576
|
+
|
|
577
|
+
outputs.push({
|
|
578
|
+
resourceId: "setup:agent-instructions",
|
|
579
|
+
path: managed.relative,
|
|
580
|
+
sourcePath: AGENT_INSTRUCTIONS_TEMPLATE,
|
|
581
|
+
mode: "copy",
|
|
582
|
+
kind: "file",
|
|
583
|
+
digest: yield* digestFileContent(content),
|
|
584
|
+
destination: managed.absolute,
|
|
585
|
+
content,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
433
588
|
if (setup.claudeInstructions.enabled) {
|
|
434
589
|
const source = yield* resolveManagedPath(projectDir, "AGENTS.md");
|
|
435
|
-
const sourceObservation =
|
|
436
|
-
|
|
590
|
+
const sourceObservation = setup.agentInstructions.enabled
|
|
591
|
+
? undefined
|
|
592
|
+
: yield* observePath(source.absolute);
|
|
593
|
+
|
|
594
|
+
if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
|
|
437
595
|
return yield* new InvalidProjectStateError({
|
|
438
596
|
message: "Claude instructions source is not a regular file: AGENTS.md",
|
|
439
597
|
});
|
|
440
598
|
}
|
|
441
599
|
const managed = yield* resolveManagedPath(projectDir, "CLAUDE.md");
|
|
442
600
|
const linkTarget = path.relative(path.dirname(managed.absolute), source.absolute);
|
|
601
|
+
|
|
443
602
|
outputs.push({
|
|
444
603
|
resourceId: "setup:claude-instructions",
|
|
445
604
|
path: managed.relative,
|
|
@@ -451,32 +610,71 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
451
610
|
linkTarget,
|
|
452
611
|
});
|
|
453
612
|
}
|
|
613
|
+
if (setup.vitePlus.quality.enabled) {
|
|
614
|
+
for (const generated of [
|
|
615
|
+
{
|
|
616
|
+
resourceId: "setup:vite-plus-config" as const,
|
|
617
|
+
path: VITE_PLUS_CONFIG_PATH,
|
|
618
|
+
sourcePath: VITE_PLUS_CONFIG_TEMPLATE,
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
resourceId: "setup:vite-plus-github-actions" as const,
|
|
622
|
+
path: VITE_PLUS_GITHUB_ACTIONS_PATH,
|
|
623
|
+
sourcePath: VITE_PLUS_GITHUB_ACTIONS_TEMPLATE,
|
|
624
|
+
},
|
|
625
|
+
]) {
|
|
626
|
+
const managed = yield* resolveManagedPath(projectDir, generated.path);
|
|
627
|
+
const content = yield* readGeneratedFileTemplate(packageRoot, generated.sourcePath);
|
|
628
|
+
|
|
629
|
+
outputs.push({
|
|
630
|
+
resourceId: generated.resourceId,
|
|
631
|
+
path: managed.relative,
|
|
632
|
+
sourcePath: generated.sourcePath,
|
|
633
|
+
mode: "copy",
|
|
634
|
+
kind: "file",
|
|
635
|
+
digest: yield* digestFileContent(content),
|
|
636
|
+
destination: managed.absolute,
|
|
637
|
+
content,
|
|
638
|
+
adoptIfExact: true,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
}
|
|
454
642
|
const agentsTarget = targets.agents;
|
|
455
|
-
const duplicateOutput = skills.find(
|
|
456
|
-
skills.findIndex((candidate) => candidate.name === skill.name) !== index
|
|
643
|
+
const duplicateOutput = skills.find(
|
|
644
|
+
(skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
|
|
457
645
|
);
|
|
646
|
+
|
|
458
647
|
if (duplicateOutput !== undefined) {
|
|
459
648
|
const selectors = skills
|
|
460
649
|
.filter((skill) => skill.name === duplicateOutput.name)
|
|
461
650
|
.map((skill) => skill.selector);
|
|
651
|
+
|
|
462
652
|
return yield* new InvalidProjectStateError({
|
|
463
653
|
message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
|
|
464
654
|
});
|
|
465
655
|
}
|
|
466
656
|
for (const skill of skills) {
|
|
467
657
|
const resolvedSource = sourceBySkill.get(skill.selector);
|
|
658
|
+
|
|
468
659
|
if (resolvedSource === undefined) {
|
|
469
|
-
return yield* new InvalidProjectStateError({
|
|
660
|
+
return yield* new InvalidProjectStateError({
|
|
661
|
+
message: `skill source is unavailable: ${skill.selector}`,
|
|
662
|
+
});
|
|
470
663
|
}
|
|
471
664
|
const source = resolvedSource.path;
|
|
472
665
|
const sourceObservation = yield* observePath(source);
|
|
666
|
+
|
|
473
667
|
if (sourceObservation.kind !== "directory") {
|
|
474
|
-
return yield* new InvalidProjectStateError({
|
|
668
|
+
return yield* new InvalidProjectStateError({
|
|
669
|
+
message: `skill source is not a directory: ${source}`,
|
|
670
|
+
});
|
|
475
671
|
}
|
|
476
672
|
for (const targetName of ["agents", "claude", "opencode"] as const) {
|
|
477
673
|
const target = targets[targetName];
|
|
674
|
+
|
|
478
675
|
if (!target.enabled) continue;
|
|
479
676
|
const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill.name));
|
|
677
|
+
|
|
480
678
|
if (target.mode === "copy") {
|
|
481
679
|
outputs.push({
|
|
482
680
|
resourceId: `skill:${skill.selector}@${targetName}`,
|
|
@@ -494,10 +692,12 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
494
692
|
}
|
|
495
693
|
const linkSource =
|
|
496
694
|
targetName === "agents" || !agentsTarget.enabled
|
|
497
|
-
? resolvedSource.linkPath ?? source
|
|
498
|
-
: (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name)))
|
|
695
|
+
? (resolvedSource.linkPath ?? source)
|
|
696
|
+
: (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name)))
|
|
697
|
+
.absolute;
|
|
499
698
|
const linkTarget = path.relative(path.dirname(managed.absolute), linkSource);
|
|
500
699
|
const linkDigest = yield* digestSymlinkTarget(linkTarget);
|
|
700
|
+
|
|
501
701
|
outputs.push({
|
|
502
702
|
resourceId: `skill:${skill.selector}@${targetName}`,
|
|
503
703
|
path: managed.relative,
|
|
@@ -514,6 +714,7 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
514
714
|
}
|
|
515
715
|
}
|
|
516
716
|
yield* validateInventory(projectDir, outputs, "desired outputs");
|
|
717
|
+
|
|
517
718
|
return outputs.sort((left, right) => left.path.localeCompare(right.path));
|
|
518
719
|
});
|
|
519
720
|
|
|
@@ -531,7 +732,9 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
531
732
|
if (currentState) yield* validateInventory(projectDir, currentState.outputs, "applied state");
|
|
532
733
|
yield* validateCrossInventoryPaths(projectDir, [...desired, ...(currentState?.outputs ?? [])]);
|
|
533
734
|
const lockById = new Map(currentLock?.outputs.map((output) => [output.resourceId, output]) ?? []);
|
|
534
|
-
const receiptsById = new Map(
|
|
735
|
+
const receiptsById = new Map(
|
|
736
|
+
currentState?.outputs.map((output) => [output.resourceId, output]) ?? [],
|
|
737
|
+
);
|
|
535
738
|
const desiredKeys = new Set(desired.map((output) => `${output.resourceId}\0${output.path}`));
|
|
536
739
|
const actions: Array<SkillPlanAction> = [];
|
|
537
740
|
|
|
@@ -545,10 +748,14 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
545
748
|
if (observed.kind === "missing") {
|
|
546
749
|
actions.push({ action: "create", desired: output, observed });
|
|
547
750
|
} else if (observed.kind === output.kind && observed.digest === output.digest) {
|
|
548
|
-
if (sameReceipt || adoptable) {
|
|
751
|
+
if (sameReceipt || adoptable || ("adoptIfExact" in output && output.adoptIfExact)) {
|
|
549
752
|
actions.push({ action: "unchanged", desired: output, observed, adopted: !sameReceipt });
|
|
550
753
|
} else {
|
|
551
|
-
actions.push({
|
|
754
|
+
actions.push({
|
|
755
|
+
action: "conflict",
|
|
756
|
+
path: output.path,
|
|
757
|
+
reason: "destination exists but is not owned",
|
|
758
|
+
});
|
|
552
759
|
}
|
|
553
760
|
} else if (
|
|
554
761
|
sameReceipt &&
|
|
@@ -560,7 +767,9 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
560
767
|
actions.push({
|
|
561
768
|
action: "conflict",
|
|
562
769
|
path: output.path,
|
|
563
|
-
reason: sameReceipt
|
|
770
|
+
reason: sameReceipt
|
|
771
|
+
? "owned destination was modified"
|
|
772
|
+
: "destination exists but is not owned",
|
|
564
773
|
});
|
|
565
774
|
}
|
|
566
775
|
}
|
|
@@ -569,11 +778,21 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
569
778
|
if (desiredKeys.has(`${receipt.resourceId}\0${receipt.path}`)) continue;
|
|
570
779
|
const managed = yield* resolveManagedPath(projectDir, receipt.path);
|
|
571
780
|
const observed = yield* observePath(managed.absolute);
|
|
781
|
+
|
|
572
782
|
if (observed.kind === "missing") continue;
|
|
573
783
|
if (observed.kind === receipt.kind && observed.digest === receipt.digest) {
|
|
574
|
-
actions.push({
|
|
784
|
+
actions.push({
|
|
785
|
+
action: "remove",
|
|
786
|
+
previous: receipt,
|
|
787
|
+
destination: managed.absolute,
|
|
788
|
+
observed,
|
|
789
|
+
});
|
|
575
790
|
} else {
|
|
576
|
-
actions.push({
|
|
791
|
+
actions.push({
|
|
792
|
+
action: "conflict",
|
|
793
|
+
path: receipt.path,
|
|
794
|
+
reason: "stale owned destination was modified",
|
|
795
|
+
});
|
|
577
796
|
}
|
|
578
797
|
}
|
|
579
798
|
|
|
@@ -588,11 +807,26 @@ const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
|
588
807
|
digest,
|
|
589
808
|
})),
|
|
590
809
|
};
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
actions: actions.sort((left, right) => {
|
|
813
|
+
const leftPath =
|
|
814
|
+
left.action === "remove"
|
|
815
|
+
? left.previous.path
|
|
816
|
+
: left.action === "conflict"
|
|
817
|
+
? left.path
|
|
818
|
+
: left.desired.path;
|
|
819
|
+
const rightPath =
|
|
820
|
+
right.action === "remove"
|
|
821
|
+
? right.previous.path
|
|
822
|
+
: right.action === "conflict"
|
|
823
|
+
? right.path
|
|
824
|
+
: right.desired.path;
|
|
825
|
+
|
|
826
|
+
return leftPath.localeCompare(rightPath);
|
|
827
|
+
}),
|
|
828
|
+
nextState,
|
|
829
|
+
};
|
|
596
830
|
});
|
|
597
831
|
|
|
598
832
|
const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
|
|
@@ -602,6 +836,7 @@ const lockedPlanMatches = (current: DevKitLock, next: DevKitLock): boolean =>
|
|
|
602
836
|
current.outputs.length === next.outputs.length &&
|
|
603
837
|
current.outputs.every((output, index) => {
|
|
604
838
|
const nextOutput = next.outputs[index];
|
|
839
|
+
|
|
605
840
|
return nextOutput !== undefined && outputIdentity(output) === outputIdentity(nextOutput);
|
|
606
841
|
});
|
|
607
842
|
|
|
@@ -617,12 +852,32 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
617
852
|
),
|
|
618
853
|
);
|
|
619
854
|
const projectDir = yield* fs.realPath(discoveredRoot);
|
|
620
|
-
const manifestManaged = yield* resolveManagedPath(
|
|
621
|
-
|
|
855
|
+
const manifestManaged = yield* resolveManagedPath(
|
|
856
|
+
projectDir,
|
|
857
|
+
options.manifestPath ?? DEFAULT_MANIFEST,
|
|
858
|
+
);
|
|
859
|
+
const lockManaged = yield* resolveManagedPath(
|
|
860
|
+
projectDir,
|
|
861
|
+
options.lockfilePath ?? DEFAULT_LOCKFILE,
|
|
862
|
+
);
|
|
622
863
|
const stateManaged = yield* resolveManagedPath(projectDir, options.statePath ?? DEFAULT_STATE);
|
|
623
864
|
const processLockManaged = yield* resolveManagedPath(projectDir, PROJECT_PROCESS_LOCK_PATH);
|
|
624
865
|
const packageRoot = yield* resolvePackageRoot();
|
|
625
866
|
const manifest = normalizeManifest(yield* readManifest(manifestManaged.absolute));
|
|
867
|
+
|
|
868
|
+
if (manifest.setup.vitePlus.quality.enabled) {
|
|
869
|
+
if (!manifest.setup.effectTsgo.enabled) {
|
|
870
|
+
return yield* new InvalidProjectStateError({
|
|
871
|
+
message:
|
|
872
|
+
"setup.vitePlus.quality requires setup.effectTsgo.enabled so vp run typecheck uses the Effect-patched compiler",
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
yield* validateVitePlusQualitySupport(
|
|
876
|
+
projectDir,
|
|
877
|
+
packageRoot,
|
|
878
|
+
manifest.setup.effectTsgo.typescriptPackage,
|
|
879
|
+
);
|
|
880
|
+
}
|
|
626
881
|
const effectSource = manifest.setup.effectSource.enabled
|
|
627
882
|
? yield* planEffectSource({
|
|
628
883
|
packageName: manifest.setup.effectSource.packageName,
|
|
@@ -638,19 +893,35 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
638
893
|
typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
|
|
639
894
|
})
|
|
640
895
|
: undefined;
|
|
896
|
+
const vitePlusHooks = manifest.setup.vitePlus.hooks.enabled
|
|
897
|
+
? yield* planVitePlusHooks(projectDir)
|
|
898
|
+
: undefined;
|
|
641
899
|
const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
|
|
642
900
|
const availableSkills = catalog.skills.map((skill) => skill.selector);
|
|
643
901
|
const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
|
|
902
|
+
|
|
644
903
|
for (const [family, familySkills] of Object.entries(skillFamilies)) {
|
|
645
904
|
if (availableSkills.includes(family)) {
|
|
646
|
-
return yield* new InvalidSkillCatalogError({
|
|
905
|
+
return yield* new InvalidSkillCatalogError({
|
|
906
|
+
family,
|
|
907
|
+
message: `family name conflicts with a skill name: ${family}`,
|
|
908
|
+
});
|
|
647
909
|
}
|
|
648
910
|
const missing = familySkills.filter((skill) => !availableSkills.includes(skill));
|
|
911
|
+
|
|
649
912
|
if (missing.length > 0) {
|
|
650
|
-
return yield* new InvalidSkillCatalogError({
|
|
913
|
+
return yield* new InvalidSkillCatalogError({
|
|
914
|
+
family,
|
|
915
|
+
message: `family references missing skills: ${missing.join(", ")}`,
|
|
916
|
+
});
|
|
651
917
|
}
|
|
652
918
|
}
|
|
653
|
-
const selectedSelectors = yield* expandSelection(
|
|
919
|
+
const selectedSelectors = yield* expandSelection(
|
|
920
|
+
manifest.include,
|
|
921
|
+
manifest.exclude,
|
|
922
|
+
availableSkills,
|
|
923
|
+
skillFamilies,
|
|
924
|
+
);
|
|
654
925
|
const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
|
|
655
926
|
const sourceBySkill = yield* withSpinner(
|
|
656
927
|
"Resolving selected skills",
|
|
@@ -663,8 +934,10 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
663
934
|
),
|
|
664
935
|
);
|
|
665
936
|
const selectedSkills: Array<CatalogSkill> = [];
|
|
937
|
+
|
|
666
938
|
for (const selector of selectedSelectors) {
|
|
667
939
|
const catalogSkill = catalogBySelector.get(selector);
|
|
940
|
+
|
|
668
941
|
if (catalogSkill === undefined) {
|
|
669
942
|
return yield* new InvalidProjectStateError({
|
|
670
943
|
message: `selected skill is unavailable: ${selector}`,
|
|
@@ -673,6 +946,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
673
946
|
selectedSkills.push(catalogSkill);
|
|
674
947
|
}
|
|
675
948
|
const desired = yield* buildDesiredOutputs(
|
|
949
|
+
packageRoot,
|
|
676
950
|
projectDir,
|
|
677
951
|
sourceBySkill,
|
|
678
952
|
selectedSkills,
|
|
@@ -705,27 +979,49 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
705
979
|
},
|
|
706
980
|
}),
|
|
707
981
|
},
|
|
708
|
-
outputs: desired.map((output): ManagedOutput =>
|
|
709
|
-
"skill" in output
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
982
|
+
outputs: desired.map((output): ManagedOutput => {
|
|
983
|
+
if ("skill" in output) {
|
|
984
|
+
return {
|
|
985
|
+
resourceId: output.resourceId,
|
|
986
|
+
path: output.path,
|
|
987
|
+
skill: output.skill,
|
|
988
|
+
target: output.target,
|
|
989
|
+
mode: output.mode,
|
|
990
|
+
kind: output.kind,
|
|
991
|
+
digest: output.digest,
|
|
992
|
+
...(output.catalog ? { catalog: output.catalog } : {}),
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
if (output.resourceId === "setup:agent-instructions") {
|
|
996
|
+
return {
|
|
997
|
+
resourceId: output.resourceId,
|
|
998
|
+
path: output.path,
|
|
999
|
+
sourcePath: output.sourcePath,
|
|
1000
|
+
mode: output.mode,
|
|
1001
|
+
kind: output.kind,
|
|
1002
|
+
digest: output.digest,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
if (output.resourceId === "setup:claude-instructions") {
|
|
1006
|
+
return {
|
|
1007
|
+
resourceId: output.resourceId,
|
|
1008
|
+
path: output.path,
|
|
1009
|
+
sourcePath: output.sourcePath,
|
|
1010
|
+
mode: output.mode,
|
|
1011
|
+
kind: output.kind,
|
|
1012
|
+
digest: output.digest,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return {
|
|
1017
|
+
resourceId: output.resourceId,
|
|
1018
|
+
path: output.path,
|
|
1019
|
+
sourcePath: output.sourcePath,
|
|
1020
|
+
mode: output.mode,
|
|
1021
|
+
kind: output.kind,
|
|
1022
|
+
digest: output.digest,
|
|
1023
|
+
};
|
|
1024
|
+
}),
|
|
729
1025
|
};
|
|
730
1026
|
const reservedPaths = [
|
|
731
1027
|
{ label: "manifest", path: manifestManaged.relative },
|
|
@@ -736,26 +1032,45 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
736
1032
|
? []
|
|
737
1033
|
: [{ label: "Effect source checkout", path: effectSource.path }]),
|
|
738
1034
|
];
|
|
1035
|
+
|
|
739
1036
|
yield* validateReservedPaths(projectDir, reservedPaths, desired);
|
|
740
1037
|
const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
|
|
741
1038
|
const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
1039
|
+
|
|
1040
|
+
if (
|
|
1041
|
+
manifest.setup.claudeInstructions.enabled &&
|
|
1042
|
+
!manifest.setup.agentInstructions.enabled &&
|
|
1043
|
+
currentState?.outputs.some((output) => output.resourceId === "setup:agent-instructions")
|
|
1044
|
+
) {
|
|
1045
|
+
return yield* new InvalidProjectStateError({
|
|
1046
|
+
message:
|
|
1047
|
+
"cannot disable agentInstructions while claudeInstructions still links to its AGENTS.md wrapper",
|
|
1048
|
+
});
|
|
1049
|
+
}
|
|
1050
|
+
yield* validateReservedPaths(projectDir, reservedPaths, [
|
|
1051
|
+
...(currentLock?.outputs ?? []),
|
|
1052
|
+
...(currentState?.outputs ?? []),
|
|
1053
|
+
]);
|
|
750
1054
|
if (options.locked) {
|
|
751
1055
|
if (!currentLock) {
|
|
752
|
-
return yield* new LockedPlanMismatchError({
|
|
1056
|
+
return yield* new LockedPlanMismatchError({
|
|
1057
|
+
message: "dev-kit.lock.json is required with --locked",
|
|
1058
|
+
});
|
|
753
1059
|
}
|
|
754
1060
|
if (!lockedPlanMatches(currentLock, nextLock)) {
|
|
755
|
-
return yield* new LockedPlanMismatchError({
|
|
1061
|
+
return yield* new LockedPlanMismatchError({
|
|
1062
|
+
message: "manifest or packaged skills differ from dev-kit.lock.json",
|
|
1063
|
+
});
|
|
756
1064
|
}
|
|
757
1065
|
}
|
|
758
|
-
const planned = yield* planDesiredOutputs(
|
|
1066
|
+
const planned = yield* planDesiredOutputs(
|
|
1067
|
+
projectDir,
|
|
1068
|
+
desired,
|
|
1069
|
+
currentLock,
|
|
1070
|
+
currentState,
|
|
1071
|
+
nextLock,
|
|
1072
|
+
);
|
|
1073
|
+
|
|
759
1074
|
return {
|
|
760
1075
|
projectDir,
|
|
761
1076
|
lockfilePath: lockManaged.absolute,
|
|
@@ -763,6 +1078,7 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
763
1078
|
actions: planned.actions,
|
|
764
1079
|
...(effectSource === undefined ? {} : { effectSource }),
|
|
765
1080
|
...(effectTsgo === undefined ? {} : { effectTsgo }),
|
|
1081
|
+
...(vitePlusHooks === undefined ? {} : { vitePlusHooks }),
|
|
766
1082
|
nextLock,
|
|
767
1083
|
nextState: planned.nextState,
|
|
768
1084
|
metadataChanged:
|
|
@@ -773,46 +1089,51 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
773
1089
|
|
|
774
1090
|
const formatAction = (action: SkillPlanAction): string => {
|
|
775
1091
|
if (action.action === "conflict") return `! ${action.path}: ${action.reason}`;
|
|
776
|
-
if (action.action === "remove")
|
|
1092
|
+
if (action.action === "remove")
|
|
1093
|
+
return `− ${action.previous.resourceId} → ${action.previous.path}`;
|
|
777
1094
|
const verb = action.desired.mode === "copy" ? "copy" : "link";
|
|
778
1095
|
const adoption = action.action === "unchanged" && action.adopted ? " (adopt)" : "";
|
|
779
1096
|
const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "=";
|
|
780
|
-
const source = "skill" in action.desired
|
|
781
|
-
|
|
782
|
-
: action.desired.sourcePath;
|
|
1097
|
+
const source = "skill" in action.desired ? action.desired.skill : action.desired.sourcePath;
|
|
1098
|
+
|
|
783
1099
|
return `${marker} ${verb} ${source} → ${action.desired.path}${adoption}`;
|
|
784
1100
|
};
|
|
785
1101
|
|
|
786
1102
|
const operationalChangeCount = (plan: SkillPlan): number =>
|
|
787
1103
|
plan.actions.filter((action) => action.action !== "unchanged").length +
|
|
788
1104
|
(plan.effectSource?.action === "sync" ? 1 : 0) +
|
|
789
|
-
(plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0)
|
|
1105
|
+
(plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched ? 1 : 0) +
|
|
1106
|
+
(plan.vitePlusHooks?.action === "configure" ? 1 : 0);
|
|
790
1107
|
|
|
791
1108
|
const plannedChangeCount = (plan: SkillPlan): number => {
|
|
792
1109
|
const operational = operationalChangeCount(plan);
|
|
1110
|
+
|
|
793
1111
|
return operational === 0 && plan.metadataChanged ? 1 : operational;
|
|
794
1112
|
};
|
|
795
1113
|
|
|
796
1114
|
export const printSkillPlan = Effect.fn("printSkillPlan")(function* (plan: SkillPlan) {
|
|
797
1115
|
const changes = plannedChangeCount(plan);
|
|
1116
|
+
|
|
798
1117
|
if (changes === 0) {
|
|
799
1118
|
yield* printStatus("success", "Already up to date");
|
|
1119
|
+
|
|
800
1120
|
return;
|
|
801
1121
|
}
|
|
802
1122
|
yield* printStatus("plan", `${changes} change${changes === 1 ? "" : "s"} planned`);
|
|
803
1123
|
for (const action of plan.actions) {
|
|
804
|
-
if (action.action !== "unchanged") yield* printDetail(formatAction(action));
|
|
1124
|
+
if (action.action !== "unchanged" || action.adopted) yield* printDetail(formatAction(action));
|
|
805
1125
|
}
|
|
806
1126
|
if (plan.effectSource?.action === "sync") {
|
|
807
|
-
yield* printDetail(
|
|
808
|
-
`+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`,
|
|
809
|
-
);
|
|
1127
|
+
yield* printDetail(`+ Effect source ${plan.effectSource.tag} → ${plan.effectSource.path}`);
|
|
810
1128
|
}
|
|
811
1129
|
if (plan.effectTsgo !== undefined && !plan.effectTsgo.alreadyPatched) {
|
|
812
1130
|
yield* printDetail(
|
|
813
1131
|
`+ TypeScript patch @effect/tsgo@${plan.effectTsgo.effectTsgoVersion} → ${plan.effectTsgo.typescriptPackage}@${plan.effectTsgo.typescriptVersion}`,
|
|
814
1132
|
);
|
|
815
1133
|
}
|
|
1134
|
+
if (plan.vitePlusHooks?.action === "configure") {
|
|
1135
|
+
yield* printDetail(`+ Vite+ hooks → ${plan.vitePlusHooks.hooksPath}`);
|
|
1136
|
+
}
|
|
816
1137
|
if (operationalChangeCount(plan) === 0 && plan.metadataChanged) {
|
|
817
1138
|
yield* printDetail("+ Dev kit metadata");
|
|
818
1139
|
}
|
|
@@ -822,22 +1143,24 @@ const observationsEqual = (left: ObservedPath, right: ObservedPath): boolean =>
|
|
|
822
1143
|
left.kind === right.kind &&
|
|
823
1144
|
(left.kind === "missing" || (right.kind !== "missing" && left.digest === right.digest));
|
|
824
1145
|
|
|
825
|
-
const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (
|
|
826
|
-
root: string,
|
|
827
|
-
) {
|
|
1146
|
+
const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (root: string) {
|
|
828
1147
|
const fs = yield* FileSystem.FileSystem;
|
|
829
1148
|
const path = yield* Path.Path;
|
|
830
1149
|
const pending = [root];
|
|
1150
|
+
|
|
831
1151
|
while (pending.length > 0) {
|
|
832
1152
|
const current = pending.pop();
|
|
1153
|
+
|
|
833
1154
|
if (current === undefined) continue;
|
|
834
1155
|
if ((yield* observeSymbolicLink(current)).kind === "symlink") return current;
|
|
835
1156
|
const info = yield* fs.stat(current);
|
|
1157
|
+
|
|
836
1158
|
if (info.type !== "Directory") continue;
|
|
837
1159
|
for (const entry of yield* fs.readDirectory(current)) {
|
|
838
1160
|
pending.push(path.join(current, entry));
|
|
839
1161
|
}
|
|
840
1162
|
}
|
|
1163
|
+
|
|
841
1164
|
return undefined;
|
|
842
1165
|
});
|
|
843
1166
|
|
|
@@ -845,18 +1168,26 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
|
|
|
845
1168
|
plan: SkillPlan,
|
|
846
1169
|
) {
|
|
847
1170
|
const verified = new Set<string>();
|
|
1171
|
+
|
|
848
1172
|
for (const action of plan.actions) {
|
|
849
1173
|
if (action.action === "remove" || action.action === "conflict") continue;
|
|
850
1174
|
if (!("skill" in action.desired)) continue;
|
|
851
1175
|
const catalog = action.desired.catalog;
|
|
1176
|
+
|
|
852
1177
|
if (catalog === undefined || !("package" in catalog)) continue;
|
|
853
1178
|
const selector = `${catalog.package}#${catalog.skill}`;
|
|
854
1179
|
const key = `${selector}\0${catalog.version}\0${catalog.digest}`;
|
|
1180
|
+
|
|
855
1181
|
if (verified.has(key)) continue;
|
|
856
1182
|
const resolved = yield* resolvePackageSkillSelector(plan.projectDir, selector);
|
|
857
1183
|
const observation = yield* observePath(resolved.path);
|
|
858
|
-
|
|
859
|
-
|
|
1184
|
+
|
|
1185
|
+
if (
|
|
1186
|
+
resolved.path !== action.desired.source ||
|
|
1187
|
+
resolved.version !== catalog.version ||
|
|
1188
|
+
observation.kind !== "directory" ||
|
|
1189
|
+
observation.digest !== catalog.digest
|
|
1190
|
+
) {
|
|
860
1191
|
return yield* new ApplyRaceError({ path: action.desired.source });
|
|
861
1192
|
}
|
|
862
1193
|
verified.add(key);
|
|
@@ -865,9 +1196,12 @@ const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(functio
|
|
|
865
1196
|
|
|
866
1197
|
const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function* (plan: SkillPlan) {
|
|
867
1198
|
const conflicts = plan.actions.filter((action) => action.action === "conflict");
|
|
1199
|
+
|
|
868
1200
|
if (conflicts.length > 0) {
|
|
869
1201
|
return yield* new PlanConflictError({
|
|
870
|
-
conflicts: conflicts.map((action) =>
|
|
1202
|
+
conflicts: conflicts.map((action) =>
|
|
1203
|
+
action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
|
|
1204
|
+
),
|
|
871
1205
|
});
|
|
872
1206
|
}
|
|
873
1207
|
|
|
@@ -877,10 +1211,15 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
877
1211
|
(action): action is Exclude<SkillPlanAction, { readonly action: "unchanged" | "conflict" }> =>
|
|
878
1212
|
action.action === "create" || action.action === "update" || action.action === "remove",
|
|
879
1213
|
);
|
|
1214
|
+
|
|
880
1215
|
for (const action of mutating) {
|
|
881
|
-
const destination =
|
|
1216
|
+
const destination =
|
|
1217
|
+
action.action === "remove" ? action.destination : action.desired.destination;
|
|
1218
|
+
|
|
882
1219
|
if (!observationsEqual(yield* observePath(destination), action.observed)) {
|
|
883
|
-
return yield* new ApplyRaceError({
|
|
1220
|
+
return yield* new ApplyRaceError({
|
|
1221
|
+
path: action.action === "remove" ? action.previous.path : action.desired.path,
|
|
1222
|
+
});
|
|
884
1223
|
}
|
|
885
1224
|
}
|
|
886
1225
|
|
|
@@ -888,29 +1227,42 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
888
1227
|
return;
|
|
889
1228
|
}
|
|
890
1229
|
|
|
891
|
-
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
1230
|
+
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
1231
|
+
directory: plan.projectDir,
|
|
1232
|
+
prefix: ".dev-kit-apply-",
|
|
1233
|
+
});
|
|
892
1234
|
const stageDir = path.join(tempDir, "stage");
|
|
893
1235
|
const backupDir = path.join(tempDir, "backup");
|
|
894
1236
|
const stagedByResource = new Map<string, string>();
|
|
895
1237
|
let stageIndex = 0;
|
|
1238
|
+
|
|
896
1239
|
for (const action of mutating) {
|
|
897
1240
|
if (action.action === "remove") continue;
|
|
898
1241
|
const staged = path.join(stageDir, String(stageIndex++));
|
|
1242
|
+
|
|
899
1243
|
yield* fs.makeDirectory(path.dirname(staged), { recursive: true });
|
|
900
1244
|
if (action.desired.mode === "copy") {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
1245
|
+
if (action.desired.kind === "file") {
|
|
1246
|
+
yield* fs.writeFileString(staged, action.desired.content, { mode: 0o644 });
|
|
1247
|
+
} else {
|
|
1248
|
+
yield* fs.copy(action.desired.source, staged, { overwrite: true });
|
|
1249
|
+
const symbolicLink = yield* findNestedSymbolicLink(staged);
|
|
1250
|
+
|
|
1251
|
+
if (symbolicLink !== undefined) {
|
|
1252
|
+
return yield* new InvalidProjectStateError({
|
|
1253
|
+
message: `staged skill contains a symlink: ${action.desired.path}`,
|
|
1254
|
+
});
|
|
1255
|
+
}
|
|
907
1256
|
}
|
|
908
1257
|
} else {
|
|
909
1258
|
yield* fs.symlink(action.desired.linkTarget, staged);
|
|
910
1259
|
}
|
|
911
1260
|
const observation = yield* observePath(staged);
|
|
1261
|
+
|
|
912
1262
|
if (observation.kind !== action.desired.kind || observation.digest !== action.desired.digest) {
|
|
913
|
-
return yield* new InvalidProjectStateError({
|
|
1263
|
+
return yield* new InvalidProjectStateError({
|
|
1264
|
+
message: `staged output digest mismatch for ${action.desired.path}`,
|
|
1265
|
+
});
|
|
914
1266
|
}
|
|
915
1267
|
stagedByResource.set(action.desired.resourceId, staged);
|
|
916
1268
|
}
|
|
@@ -919,6 +1271,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
919
1271
|
|
|
920
1272
|
const stagedLock = path.join(tempDir, "next-lock.json");
|
|
921
1273
|
const stagedState = path.join(tempDir, "next-state.json");
|
|
1274
|
+
|
|
922
1275
|
yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
|
|
923
1276
|
yield* fs.writeFileString(stagedState, canonicalState(plan.nextState));
|
|
924
1277
|
|
|
@@ -931,10 +1284,11 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
931
1284
|
};
|
|
932
1285
|
const replacements: Array<Replacement> = [];
|
|
933
1286
|
let replacementIndex = 0;
|
|
1287
|
+
|
|
934
1288
|
for (const action of mutating) {
|
|
935
|
-
const staged =
|
|
936
|
-
? undefined
|
|
937
|
-
|
|
1289
|
+
const staged =
|
|
1290
|
+
action.action === "remove" ? undefined : stagedByResource.get(action.desired.resourceId);
|
|
1291
|
+
|
|
938
1292
|
if (action.action !== "remove" && staged === undefined) {
|
|
939
1293
|
return yield* new InvalidProjectStateError({
|
|
940
1294
|
message: `missing staged output for ${action.desired.resourceId}`,
|
|
@@ -978,6 +1332,7 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
978
1332
|
const apply = Effect.gen(function* () {
|
|
979
1333
|
for (const replacement of replacements) {
|
|
980
1334
|
const observed = yield* observePath(replacement.destination);
|
|
1335
|
+
|
|
981
1336
|
if (
|
|
982
1337
|
replacement.expected !== undefined &&
|
|
983
1338
|
!observationsEqual(observed, replacement.expected)
|
|
@@ -997,25 +1352,33 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
997
1352
|
}
|
|
998
1353
|
});
|
|
999
1354
|
|
|
1000
|
-
yield* Effect.uninterruptible(
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
Effect.
|
|
1355
|
+
yield* Effect.uninterruptible(
|
|
1356
|
+
apply.pipe(
|
|
1357
|
+
Effect.catchCause((applyCause) =>
|
|
1358
|
+
rollback.pipe(
|
|
1359
|
+
Effect.catchCause((rollbackCause) =>
|
|
1360
|
+
Effect.failCause(Cause.combine(applyCause, rollbackCause)),
|
|
1361
|
+
),
|
|
1362
|
+
Effect.andThen(Effect.failCause(applyCause)),
|
|
1005
1363
|
),
|
|
1006
|
-
Effect.andThen(Effect.failCause(applyCause)),
|
|
1007
1364
|
),
|
|
1008
1365
|
),
|
|
1009
|
-
)
|
|
1366
|
+
);
|
|
1010
1367
|
});
|
|
1011
1368
|
|
|
1012
|
-
export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
|
|
1369
|
+
export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (
|
|
1370
|
+
options: SyncOptions,
|
|
1371
|
+
) {
|
|
1013
1372
|
const plan = yield* planProjectSkills(options);
|
|
1373
|
+
|
|
1014
1374
|
if (options.dryRun) yield* printSkillPlan(plan);
|
|
1015
1375
|
const conflicts = plan.actions.filter((action) => action.action === "conflict");
|
|
1376
|
+
|
|
1016
1377
|
if (conflicts.length > 0) {
|
|
1017
1378
|
return yield* new PlanConflictError({
|
|
1018
|
-
conflicts: conflicts.map((action) =>
|
|
1379
|
+
conflicts: conflicts.map((action) =>
|
|
1380
|
+
action.action === "conflict" ? `${action.path}: ${action.reason}` : "",
|
|
1381
|
+
),
|
|
1019
1382
|
});
|
|
1020
1383
|
}
|
|
1021
1384
|
if (options.dryRun) return;
|
|
@@ -1026,6 +1389,7 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
|
|
|
1026
1389
|
actions: plan.actions,
|
|
1027
1390
|
effectSource: plan.effectSource,
|
|
1028
1391
|
effectTsgo: plan.effectTsgo,
|
|
1392
|
+
vitePlusHooks: plan.vitePlusHooks,
|
|
1029
1393
|
nextLock: plan.nextLock,
|
|
1030
1394
|
nextState: plan.nextState,
|
|
1031
1395
|
});
|
|
@@ -1033,13 +1397,16 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
|
|
|
1033
1397
|
actions: replanned.actions,
|
|
1034
1398
|
effectSource: replanned.effectSource,
|
|
1035
1399
|
effectTsgo: replanned.effectTsgo,
|
|
1400
|
+
vitePlusHooks: replanned.vitePlusHooks,
|
|
1036
1401
|
nextLock: replanned.nextLock,
|
|
1037
1402
|
nextState: replanned.nextState,
|
|
1038
1403
|
});
|
|
1404
|
+
|
|
1039
1405
|
if (originalSignature !== nextSignature) {
|
|
1040
1406
|
return yield* new ApplyRaceError({ path: "project state" });
|
|
1041
1407
|
}
|
|
1042
1408
|
const changes = plannedChangeCount(replanned);
|
|
1409
|
+
|
|
1043
1410
|
yield* withSpinner(
|
|
1044
1411
|
"Applying dev kit",
|
|
1045
1412
|
Effect.gen(function* () {
|
|
@@ -1049,6 +1416,9 @@ export const runProjectSkillPlan = Effect.fn("runProjectSkillPlan")(function* (o
|
|
|
1049
1416
|
if (replanned.effectTsgo !== undefined) {
|
|
1050
1417
|
yield* applyEffectTsgoPatchPlan(replanned.effectTsgo);
|
|
1051
1418
|
}
|
|
1419
|
+
if (replanned.vitePlusHooks !== undefined) {
|
|
1420
|
+
yield* applyVitePlusHooksPlan(replanned.vitePlusHooks);
|
|
1421
|
+
}
|
|
1052
1422
|
yield* applyPlannedSkillChanges(replanned);
|
|
1053
1423
|
}),
|
|
1054
1424
|
);
|