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