@danieljvdm/dev-kit 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -35
- package/dev-kit.example.jsonc +2 -0
- package/package.json +3 -2
- package/schema/dev-kit.schema.json +32 -4
- package/skills/dev-kit/SKILL.md +31 -6
- package/skills/effect-atom-data-fetching/SKILL.md +40 -0
- package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
- package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
- package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
- package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
- package/skills/effect-atom-data-fetching/references/testing.md +63 -0
- package/src/bin/dev-kit.ts +5 -5
- package/src/catalog.ts +73 -16
- package/src/index.ts +14 -0
- package/src/manifest.ts +29 -2
- package/src/package-skill-source.ts +250 -0
- package/src/path-digest.ts +7 -0
- package/src/project-package.ts +34 -0
- package/src/project-state.ts +50 -9
- package/src/skill-manager.ts +66 -30
- package/src/skill-selector.ts +43 -0
- package/src/sync.ts +279 -40
- package/templates/AGENTS.md +9 -0
package/src/sync.ts
CHANGED
|
@@ -3,7 +3,12 @@ import { Cause, Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
|
3
3
|
import { ChildProcess } from "effect/unstable/process";
|
|
4
4
|
|
|
5
5
|
import { DevKitManifestSchema, normalizeManifest } from "./manifest.ts";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
loadSkillCatalog,
|
|
8
|
+
resolveSkillSources,
|
|
9
|
+
type CatalogSkill,
|
|
10
|
+
type ResolvedSkillSource,
|
|
11
|
+
} from "./catalog.ts";
|
|
7
12
|
import { printDetail, printStatus, withSpinner } from "./cli-ui.ts";
|
|
8
13
|
import {
|
|
9
14
|
applyEffectSourcePlan,
|
|
@@ -16,16 +21,23 @@ import {
|
|
|
16
21
|
type EffectTsgoPatchPlan,
|
|
17
22
|
} from "./effect-tsgo.ts";
|
|
18
23
|
import {
|
|
24
|
+
digestFileContent,
|
|
19
25
|
digestSymlinkTarget,
|
|
20
26
|
digestText,
|
|
21
27
|
observePath,
|
|
22
28
|
type ObservedPath,
|
|
23
29
|
} from "./path-digest.ts";
|
|
30
|
+
import { resolvePackageSkillSelector } from "./package-skill-source.ts";
|
|
31
|
+
import { readDirectDependencyNames } from "./project-package.ts";
|
|
32
|
+
import { parseSkillSelector } from "./skill-selector.ts";
|
|
24
33
|
import {
|
|
25
34
|
AppliedStateSchema,
|
|
26
35
|
DevKitLockSchema,
|
|
27
36
|
type AppliedState,
|
|
28
37
|
type DevKitLock,
|
|
38
|
+
type ManagedAgentInstructionsOutput,
|
|
39
|
+
type ManagedClaudeInstructionsOutput,
|
|
40
|
+
type ManagedOutput,
|
|
29
41
|
type ManagedSkillOutput,
|
|
30
42
|
type OwnershipReceipt,
|
|
31
43
|
} from "./project-state.ts";
|
|
@@ -67,10 +79,25 @@ type DesiredSkillOutput =
|
|
|
67
79
|
readonly linkTarget: string;
|
|
68
80
|
});
|
|
69
81
|
|
|
82
|
+
type DesiredAgentInstructionsOutput = ManagedAgentInstructionsOutput & {
|
|
83
|
+
readonly content: string;
|
|
84
|
+
readonly destination: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
type DesiredClaudeInstructionsOutput = ManagedClaudeInstructionsOutput & {
|
|
88
|
+
readonly destination: string;
|
|
89
|
+
readonly linkTarget: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
type DesiredOutput =
|
|
93
|
+
| DesiredSkillOutput
|
|
94
|
+
| DesiredAgentInstructionsOutput
|
|
95
|
+
| DesiredClaudeInstructionsOutput;
|
|
96
|
+
|
|
70
97
|
type SkillPlanAction =
|
|
71
98
|
| {
|
|
72
99
|
readonly action: "create" | "update";
|
|
73
|
-
readonly desired:
|
|
100
|
+
readonly desired: DesiredOutput;
|
|
74
101
|
readonly observed: ObservedPath;
|
|
75
102
|
}
|
|
76
103
|
| {
|
|
@@ -81,7 +108,7 @@ type SkillPlanAction =
|
|
|
81
108
|
}
|
|
82
109
|
| {
|
|
83
110
|
readonly action: "unchanged";
|
|
84
|
-
readonly desired:
|
|
111
|
+
readonly desired: DesiredOutput;
|
|
85
112
|
readonly observed: ObservedPath;
|
|
86
113
|
readonly adopted: boolean;
|
|
87
114
|
}
|
|
@@ -180,10 +207,14 @@ class ApplyRaceError extends Schema.TaggedErrorClass<ApplyRaceError>()("ApplyRac
|
|
|
180
207
|
}
|
|
181
208
|
}
|
|
182
209
|
|
|
183
|
-
const SKILL_FAMILIES: SkillCatalog = {
|
|
210
|
+
const SKILL_FAMILIES: SkillCatalog = {
|
|
211
|
+
effect: ["effect-ts", "effect-atom-data-fetching"],
|
|
212
|
+
};
|
|
184
213
|
export const DEFAULT_MANIFEST = "dev-kit.jsonc";
|
|
185
214
|
const DEFAULT_LOCKFILE = "dev-kit.lock.json";
|
|
186
215
|
const DEFAULT_STATE = ".dev-kit/state.json";
|
|
216
|
+
const AGENT_INSTRUCTIONS_TEMPLATE = "templates/AGENTS.md";
|
|
217
|
+
const DEV_KIT_SKILL_PATH_PLACEHOLDER = "{{DEV_KIT_SKILL_PATH}}";
|
|
187
218
|
|
|
188
219
|
const resolvePackageRoot = Effect.fn("resolvePackageRoot")(function* () {
|
|
189
220
|
const path = yield* Path.Path;
|
|
@@ -263,7 +294,7 @@ const expandSelection = (
|
|
|
263
294
|
for (const name of include) {
|
|
264
295
|
if (skillFamilies[name]) {
|
|
265
296
|
for (const skill of skillFamilies[name]) selected.add(skill);
|
|
266
|
-
} else if (availableSkills.includes(name)) {
|
|
297
|
+
} else if (availableSkills.includes(name) || parseSkillSelector(name)?.type === "package") {
|
|
267
298
|
selected.add(name);
|
|
268
299
|
} else {
|
|
269
300
|
return Effect.fail(new UnknownSkillOrFamilyError({ name, known }));
|
|
@@ -315,7 +346,7 @@ const pathsOverlap = (left: string, right: string): boolean =>
|
|
|
315
346
|
const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
|
|
316
347
|
projectDir: string,
|
|
317
348
|
reserved: ReadonlyArray<{ readonly label: string; readonly path: string }>,
|
|
318
|
-
outputs: ReadonlyArray<Pick<
|
|
349
|
+
outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
|
|
319
350
|
) {
|
|
320
351
|
const outputPaths = new Set<string>();
|
|
321
352
|
for (const output of outputs) {
|
|
@@ -342,19 +373,21 @@ const validateReservedPaths = Effect.fn("validateReservedPaths")(function* (
|
|
|
342
373
|
}
|
|
343
374
|
});
|
|
344
375
|
|
|
345
|
-
const outputIdentity = (output:
|
|
376
|
+
const outputIdentity = (output: ManagedOutput) =>
|
|
346
377
|
JSON.stringify({
|
|
347
378
|
resourceId: output.resourceId,
|
|
348
379
|
path: output.path,
|
|
349
380
|
mode: output.mode,
|
|
350
381
|
kind: output.kind,
|
|
351
382
|
digest: output.digest,
|
|
352
|
-
|
|
383
|
+
...("skill" in output
|
|
384
|
+
? { skill: output.skill, target: output.target, catalog: output.catalog }
|
|
385
|
+
: { sourcePath: output.sourcePath }),
|
|
353
386
|
});
|
|
354
387
|
|
|
355
388
|
const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
356
389
|
projectDir: string,
|
|
357
|
-
outputs: ReadonlyArray<
|
|
390
|
+
outputs: ReadonlyArray<ManagedOutput | OwnershipReceipt>,
|
|
358
391
|
label: string,
|
|
359
392
|
) {
|
|
360
393
|
const ids = new Set<string>();
|
|
@@ -384,7 +417,7 @@ const validateInventory = Effect.fn("validateManagedInventory")(function* (
|
|
|
384
417
|
|
|
385
418
|
const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(function* (
|
|
386
419
|
projectDir: string,
|
|
387
|
-
outputs: ReadonlyArray<Pick<
|
|
420
|
+
outputs: ReadonlyArray<Pick<ManagedOutput | OwnershipReceipt, "path">>,
|
|
388
421
|
) {
|
|
389
422
|
const uniquePaths = new Set<string>();
|
|
390
423
|
for (const output of outputs) {
|
|
@@ -403,19 +436,112 @@ const validateCrossInventoryPaths = Effect.fn("validateCrossInventoryPaths")(fun
|
|
|
403
436
|
}
|
|
404
437
|
});
|
|
405
438
|
|
|
439
|
+
const renderAgentInstructions = Effect.fn("renderAgentInstructions")(function* (
|
|
440
|
+
packageRoot: string,
|
|
441
|
+
projectDir: string,
|
|
442
|
+
sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
|
|
443
|
+
) {
|
|
444
|
+
const fs = yield* FileSystem.FileSystem;
|
|
445
|
+
const path = yield* Path.Path;
|
|
446
|
+
const templatePath = path.join(packageRoot, AGENT_INSTRUCTIONS_TEMPLATE);
|
|
447
|
+
if ((yield* observePath(templatePath)).kind !== "file") {
|
|
448
|
+
return yield* new InvalidProjectStateError({
|
|
449
|
+
message: `dev-kit agent instructions template is not a regular file: ${AGENT_INSTRUCTIONS_TEMPLATE}`,
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
const template = yield* fs.readFileString(templatePath);
|
|
453
|
+
if (!template.includes(DEV_KIT_SKILL_PATH_PLACEHOLDER)) {
|
|
454
|
+
return yield* new InvalidProjectStateError({
|
|
455
|
+
message: `dev-kit agent instructions template is missing ${DEV_KIT_SKILL_PATH_PLACEHOLDER}`,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const devKitSkill = sourceBySkill.get("dev-kit");
|
|
460
|
+
const devKitSkillPath = devKitSkill === undefined
|
|
461
|
+
? "node_modules/@danieljvdm/dev-kit/skills/dev-kit/SKILL.md"
|
|
462
|
+
: portablePath(
|
|
463
|
+
path,
|
|
464
|
+
path.relative(
|
|
465
|
+
projectDir,
|
|
466
|
+
path.join(devKitSkill.linkPath ?? devKitSkill.path, "SKILL.md"),
|
|
467
|
+
),
|
|
468
|
+
);
|
|
469
|
+
const sections = [template.replaceAll(DEV_KIT_SKILL_PATH_PLACEHOLDER, devKitSkillPath).trimEnd()];
|
|
470
|
+
if ((yield* readDirectDependencyNames(projectDir)).includes("vite-plus")) {
|
|
471
|
+
const vitePlusTemplate = path.join(projectDir, "node_modules", "vite-plus", "AGENTS.md");
|
|
472
|
+
if ((yield* observePath(vitePlusTemplate)).kind !== "file") {
|
|
473
|
+
return yield* new InvalidProjectStateError({
|
|
474
|
+
message: "Vite+ is a direct dependency but its agent instructions are not a regular file: node_modules/vite-plus/AGENTS.md",
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
sections.push((yield* fs.readFileString(vitePlusTemplate)).trim());
|
|
478
|
+
}
|
|
479
|
+
return `${sections.join("\n\n")}\n`;
|
|
480
|
+
});
|
|
481
|
+
|
|
406
482
|
const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
483
|
+
packageRoot: string,
|
|
407
484
|
projectDir: string,
|
|
408
485
|
sourceBySkill: ReadonlyMap<string, ResolvedSkillSource>,
|
|
409
|
-
skills: ReadonlyArray<
|
|
486
|
+
skills: ReadonlyArray<CatalogSkill>,
|
|
487
|
+
setup: ReturnType<typeof normalizeManifest>["setup"],
|
|
410
488
|
targets: ReturnType<typeof normalizeManifest>["targets"],
|
|
411
489
|
) {
|
|
412
490
|
const path = yield* Path.Path;
|
|
413
|
-
const outputs: Array<
|
|
491
|
+
const outputs: Array<DesiredOutput> = [];
|
|
492
|
+
if (setup.agentInstructions.enabled) {
|
|
493
|
+
const managed = yield* resolveManagedPath(projectDir, "AGENTS.md");
|
|
494
|
+
const content = yield* renderAgentInstructions(packageRoot, projectDir, sourceBySkill);
|
|
495
|
+
outputs.push({
|
|
496
|
+
resourceId: "setup:agent-instructions",
|
|
497
|
+
path: managed.relative,
|
|
498
|
+
sourcePath: AGENT_INSTRUCTIONS_TEMPLATE,
|
|
499
|
+
mode: "copy",
|
|
500
|
+
kind: "file",
|
|
501
|
+
digest: yield* digestFileContent(content),
|
|
502
|
+
destination: managed.absolute,
|
|
503
|
+
content,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (setup.claudeInstructions.enabled) {
|
|
507
|
+
const source = yield* resolveManagedPath(projectDir, "AGENTS.md");
|
|
508
|
+
const sourceObservation = setup.agentInstructions.enabled
|
|
509
|
+
? undefined
|
|
510
|
+
: yield* observePath(source.absolute);
|
|
511
|
+
if (!setup.agentInstructions.enabled && sourceObservation?.kind !== "file") {
|
|
512
|
+
return yield* new InvalidProjectStateError({
|
|
513
|
+
message: "Claude instructions source is not a regular file: AGENTS.md",
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
const managed = yield* resolveManagedPath(projectDir, "CLAUDE.md");
|
|
517
|
+
const linkTarget = path.relative(path.dirname(managed.absolute), source.absolute);
|
|
518
|
+
outputs.push({
|
|
519
|
+
resourceId: "setup:claude-instructions",
|
|
520
|
+
path: managed.relative,
|
|
521
|
+
sourcePath: source.relative,
|
|
522
|
+
mode: "symlink",
|
|
523
|
+
kind: "symlink",
|
|
524
|
+
digest: yield* digestSymlinkTarget(linkTarget),
|
|
525
|
+
destination: managed.absolute,
|
|
526
|
+
linkTarget,
|
|
527
|
+
});
|
|
528
|
+
}
|
|
414
529
|
const agentsTarget = targets.agents;
|
|
530
|
+
const duplicateOutput = skills.find((skill, index) =>
|
|
531
|
+
skills.findIndex((candidate) => candidate.name === skill.name) !== index
|
|
532
|
+
);
|
|
533
|
+
if (duplicateOutput !== undefined) {
|
|
534
|
+
const selectors = skills
|
|
535
|
+
.filter((skill) => skill.name === duplicateOutput.name)
|
|
536
|
+
.map((skill) => skill.selector);
|
|
537
|
+
return yield* new InvalidProjectStateError({
|
|
538
|
+
message: `selected skills would both install as ${duplicateOutput.name}: ${selectors.join(", ")}`,
|
|
539
|
+
});
|
|
540
|
+
}
|
|
415
541
|
for (const skill of skills) {
|
|
416
|
-
const resolvedSource = sourceBySkill.get(skill);
|
|
542
|
+
const resolvedSource = sourceBySkill.get(skill.selector);
|
|
417
543
|
if (resolvedSource === undefined) {
|
|
418
|
-
return yield* new InvalidProjectStateError({ message: `skill source is unavailable: ${skill}` });
|
|
544
|
+
return yield* new InvalidProjectStateError({ message: `skill source is unavailable: ${skill.selector}` });
|
|
419
545
|
}
|
|
420
546
|
const source = resolvedSource.path;
|
|
421
547
|
const sourceObservation = yield* observePath(source);
|
|
@@ -425,12 +551,12 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
425
551
|
for (const targetName of ["agents", "claude", "opencode"] as const) {
|
|
426
552
|
const target = targets[targetName];
|
|
427
553
|
if (!target.enabled) continue;
|
|
428
|
-
const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill));
|
|
554
|
+
const managed = yield* resolveManagedPath(projectDir, path.join(target.path, skill.name));
|
|
429
555
|
if (target.mode === "copy") {
|
|
430
556
|
outputs.push({
|
|
431
|
-
resourceId: `skill:${skill}@${targetName}`,
|
|
557
|
+
resourceId: `skill:${skill.selector}@${targetName}`,
|
|
432
558
|
path: managed.relative,
|
|
433
|
-
skill,
|
|
559
|
+
skill: skill.name,
|
|
434
560
|
target: targetName,
|
|
435
561
|
mode: "copy",
|
|
436
562
|
kind: "directory",
|
|
@@ -443,14 +569,14 @@ const buildDesiredOutputs = Effect.fn("buildDesiredSkillOutputs")(function* (
|
|
|
443
569
|
}
|
|
444
570
|
const linkSource =
|
|
445
571
|
targetName === "agents" || !agentsTarget.enabled
|
|
446
|
-
? source
|
|
447
|
-
: (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill))).absolute;
|
|
572
|
+
? resolvedSource.linkPath ?? source
|
|
573
|
+
: (yield* resolveManagedPath(projectDir, path.join(agentsTarget.path, skill.name))).absolute;
|
|
448
574
|
const linkTarget = path.relative(path.dirname(managed.absolute), linkSource);
|
|
449
575
|
const linkDigest = yield* digestSymlinkTarget(linkTarget);
|
|
450
576
|
outputs.push({
|
|
451
|
-
resourceId: `skill:${skill}@${targetName}`,
|
|
577
|
+
resourceId: `skill:${skill.selector}@${targetName}`,
|
|
452
578
|
path: managed.relative,
|
|
453
|
-
skill,
|
|
579
|
+
skill: skill.name,
|
|
454
580
|
target: targetName,
|
|
455
581
|
mode: "symlink",
|
|
456
582
|
kind: "symlink",
|
|
@@ -471,7 +597,7 @@ const canonicalState = (state: AppliedState): string => `${JSON.stringify(state,
|
|
|
471
597
|
|
|
472
598
|
const planDesiredOutputs = Effect.fn("planDesiredSkillOutputs")(function* (
|
|
473
599
|
projectDir: string,
|
|
474
|
-
desired: ReadonlyArray<
|
|
600
|
+
desired: ReadonlyArray<DesiredOutput>,
|
|
475
601
|
currentLock: DevKitLock | undefined,
|
|
476
602
|
currentState: AppliedState | undefined,
|
|
477
603
|
nextLock: DevKitLock,
|
|
@@ -587,8 +713,8 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
587
713
|
typescriptPackage: manifest.setup.effectTsgo.typescriptPackage,
|
|
588
714
|
})
|
|
589
715
|
: undefined;
|
|
590
|
-
const catalog = yield* loadSkillCatalog(packageRoot);
|
|
591
|
-
const availableSkills = catalog.skills.map((skill) => skill.
|
|
716
|
+
const catalog = yield* loadSkillCatalog(packageRoot, projectDir);
|
|
717
|
+
const availableSkills = catalog.skills.map((skill) => skill.selector);
|
|
592
718
|
const skillFamilies = { ...SKILL_FAMILIES, ...catalog.families };
|
|
593
719
|
for (const [family, familySkills] of Object.entries(skillFamilies)) {
|
|
594
720
|
if (availableSkills.includes(family)) {
|
|
@@ -599,12 +725,36 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
599
725
|
return yield* new InvalidSkillCatalogError({ family, message: `family references missing skills: ${missing.join(", ")}` });
|
|
600
726
|
}
|
|
601
727
|
}
|
|
602
|
-
const
|
|
728
|
+
const selectedSelectors = yield* expandSelection(manifest.include, manifest.exclude, availableSkills, skillFamilies);
|
|
729
|
+
const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
|
|
603
730
|
const sourceBySkill = yield* withSpinner(
|
|
604
|
-
"
|
|
605
|
-
resolveSkillSources(
|
|
731
|
+
"Resolving selected skills",
|
|
732
|
+
resolveSkillSources(
|
|
733
|
+
packageRoot,
|
|
734
|
+
projectDir,
|
|
735
|
+
catalog,
|
|
736
|
+
selectedSelectors,
|
|
737
|
+
options.dryRun !== true,
|
|
738
|
+
),
|
|
739
|
+
);
|
|
740
|
+
const selectedSkills: Array<CatalogSkill> = [];
|
|
741
|
+
for (const selector of selectedSelectors) {
|
|
742
|
+
const catalogSkill = catalogBySelector.get(selector);
|
|
743
|
+
if (catalogSkill === undefined) {
|
|
744
|
+
return yield* new InvalidProjectStateError({
|
|
745
|
+
message: `selected skill is unavailable: ${selector}`,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
selectedSkills.push(catalogSkill);
|
|
749
|
+
}
|
|
750
|
+
const desired = yield* buildDesiredOutputs(
|
|
751
|
+
packageRoot,
|
|
752
|
+
projectDir,
|
|
753
|
+
sourceBySkill,
|
|
754
|
+
selectedSkills,
|
|
755
|
+
manifest.setup,
|
|
756
|
+
manifest.targets,
|
|
606
757
|
);
|
|
607
|
-
const desired = yield* buildDesiredOutputs(projectDir, sourceBySkill, selectedSkills, manifest.targets);
|
|
608
758
|
const nextLock: DevKitLock = {
|
|
609
759
|
version: 1,
|
|
610
760
|
toolVersion: DEV_KIT_VERSION,
|
|
@@ -631,16 +781,38 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
631
781
|
},
|
|
632
782
|
}),
|
|
633
783
|
},
|
|
634
|
-
outputs: desired.map((
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
784
|
+
outputs: desired.map((output): ManagedOutput => {
|
|
785
|
+
if ("skill" in output) {
|
|
786
|
+
return {
|
|
787
|
+
resourceId: output.resourceId,
|
|
788
|
+
path: output.path,
|
|
789
|
+
skill: output.skill,
|
|
790
|
+
target: output.target,
|
|
791
|
+
mode: output.mode,
|
|
792
|
+
kind: output.kind,
|
|
793
|
+
digest: output.digest,
|
|
794
|
+
...(output.catalog ? { catalog: output.catalog } : {}),
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
if (output.resourceId === "setup:agent-instructions") {
|
|
798
|
+
return {
|
|
799
|
+
resourceId: output.resourceId,
|
|
800
|
+
path: output.path,
|
|
801
|
+
sourcePath: output.sourcePath,
|
|
802
|
+
mode: output.mode,
|
|
803
|
+
kind: output.kind,
|
|
804
|
+
digest: output.digest,
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
resourceId: output.resourceId,
|
|
809
|
+
path: output.path,
|
|
810
|
+
sourcePath: output.sourcePath,
|
|
811
|
+
mode: output.mode,
|
|
812
|
+
kind: output.kind,
|
|
813
|
+
digest: output.digest,
|
|
814
|
+
};
|
|
815
|
+
}),
|
|
644
816
|
};
|
|
645
817
|
const reservedPaths = [
|
|
646
818
|
{ label: "manifest", path: manifestManaged.relative },
|
|
@@ -654,6 +826,17 @@ export const planProjectSkills = Effect.fn("planProjectSkills")(function* (optio
|
|
|
654
826
|
yield* validateReservedPaths(projectDir, reservedPaths, desired);
|
|
655
827
|
const currentLock = yield* readOptionalStructuredFile(lockManaged.absolute, DevKitLockSchema);
|
|
656
828
|
const currentState = yield* readOptionalStructuredFile(stateManaged.absolute, AppliedStateSchema);
|
|
829
|
+
if (
|
|
830
|
+
manifest.setup.claudeInstructions.enabled &&
|
|
831
|
+
!manifest.setup.agentInstructions.enabled &&
|
|
832
|
+
currentState?.outputs.some(
|
|
833
|
+
(output) => output.resourceId === "setup:agent-instructions",
|
|
834
|
+
)
|
|
835
|
+
) {
|
|
836
|
+
return yield* new InvalidProjectStateError({
|
|
837
|
+
message: "cannot disable agentInstructions while claudeInstructions still links to its AGENTS.md wrapper",
|
|
838
|
+
});
|
|
839
|
+
}
|
|
657
840
|
yield* validateReservedPaths(
|
|
658
841
|
projectDir,
|
|
659
842
|
reservedPaths,
|
|
@@ -692,7 +875,10 @@ const formatAction = (action: SkillPlanAction): string => {
|
|
|
692
875
|
const verb = action.desired.mode === "copy" ? "copy" : "link";
|
|
693
876
|
const adoption = action.action === "unchanged" && action.adopted ? " (adopt)" : "";
|
|
694
877
|
const marker = action.action === "create" ? "+" : action.action === "update" ? "~" : "=";
|
|
695
|
-
|
|
878
|
+
const source = "skill" in action.desired
|
|
879
|
+
? action.desired.skill
|
|
880
|
+
: action.desired.sourcePath;
|
|
881
|
+
return `${marker} ${verb} ${source} → ${action.desired.path}${adoption}`;
|
|
696
882
|
};
|
|
697
883
|
|
|
698
884
|
const operationalChangeCount = (plan: SkillPlan): number =>
|
|
@@ -734,6 +920,47 @@ const observationsEqual = (left: ObservedPath, right: ObservedPath): boolean =>
|
|
|
734
920
|
left.kind === right.kind &&
|
|
735
921
|
(left.kind === "missing" || (right.kind !== "missing" && left.digest === right.digest));
|
|
736
922
|
|
|
923
|
+
const findNestedSymbolicLink = Effect.fn("findNestedSkillSymbolicLink")(function* (
|
|
924
|
+
root: string,
|
|
925
|
+
) {
|
|
926
|
+
const fs = yield* FileSystem.FileSystem;
|
|
927
|
+
const path = yield* Path.Path;
|
|
928
|
+
const pending = [root];
|
|
929
|
+
while (pending.length > 0) {
|
|
930
|
+
const current = pending.pop();
|
|
931
|
+
if (current === undefined) continue;
|
|
932
|
+
if ((yield* observeSymbolicLink(current)).kind === "symlink") return current;
|
|
933
|
+
const info = yield* fs.stat(current);
|
|
934
|
+
if (info.type !== "Directory") continue;
|
|
935
|
+
for (const entry of yield* fs.readDirectory(current)) {
|
|
936
|
+
pending.push(path.join(current, entry));
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
return undefined;
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
const verifyPackageSkillSources = Effect.fn("verifyPackageSkillSources")(function* (
|
|
943
|
+
plan: SkillPlan,
|
|
944
|
+
) {
|
|
945
|
+
const verified = new Set<string>();
|
|
946
|
+
for (const action of plan.actions) {
|
|
947
|
+
if (action.action === "remove" || action.action === "conflict") continue;
|
|
948
|
+
if (!("skill" in action.desired)) continue;
|
|
949
|
+
const catalog = action.desired.catalog;
|
|
950
|
+
if (catalog === undefined || !("package" in catalog)) continue;
|
|
951
|
+
const selector = `${catalog.package}#${catalog.skill}`;
|
|
952
|
+
const key = `${selector}\0${catalog.version}\0${catalog.digest}`;
|
|
953
|
+
if (verified.has(key)) continue;
|
|
954
|
+
const resolved = yield* resolvePackageSkillSelector(plan.projectDir, selector);
|
|
955
|
+
const observation = yield* observePath(resolved.path);
|
|
956
|
+
if (resolved.path !== action.desired.source || resolved.version !== catalog.version ||
|
|
957
|
+
observation.kind !== "directory" || observation.digest !== catalog.digest) {
|
|
958
|
+
return yield* new ApplyRaceError({ path: action.desired.source });
|
|
959
|
+
}
|
|
960
|
+
verified.add(key);
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
737
964
|
const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function* (plan: SkillPlan) {
|
|
738
965
|
const conflicts = plan.actions.filter((action) => action.action === "conflict");
|
|
739
966
|
if (conflicts.length > 0) {
|
|
@@ -769,7 +996,17 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
769
996
|
const staged = path.join(stageDir, String(stageIndex++));
|
|
770
997
|
yield* fs.makeDirectory(path.dirname(staged), { recursive: true });
|
|
771
998
|
if (action.desired.mode === "copy") {
|
|
772
|
-
|
|
999
|
+
if (action.desired.kind === "file") {
|
|
1000
|
+
yield* fs.writeFileString(staged, action.desired.content, { mode: 0o644 });
|
|
1001
|
+
} else {
|
|
1002
|
+
yield* fs.copy(action.desired.source, staged, { overwrite: true });
|
|
1003
|
+
const symbolicLink = yield* findNestedSymbolicLink(staged);
|
|
1004
|
+
if (symbolicLink !== undefined) {
|
|
1005
|
+
return yield* new InvalidProjectStateError({
|
|
1006
|
+
message: `staged skill contains a symlink: ${action.desired.path}`,
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
773
1010
|
} else {
|
|
774
1011
|
yield* fs.symlink(action.desired.linkTarget, staged);
|
|
775
1012
|
}
|
|
@@ -780,6 +1017,8 @@ const applyPlannedSkillChanges = Effect.fn("applyPlannedSkillChanges")(function*
|
|
|
780
1017
|
stagedByResource.set(action.desired.resourceId, staged);
|
|
781
1018
|
}
|
|
782
1019
|
|
|
1020
|
+
yield* verifyPackageSkillSources(plan);
|
|
1021
|
+
|
|
783
1022
|
const stagedLock = path.join(tempDir, "next-lock.json");
|
|
784
1023
|
const stagedState = path.join(tempDir, "next-state.json");
|
|
785
1024
|
yield* fs.writeFileString(stagedLock, canonicalLock(plan.nextLock));
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- DEV KIT START -->
|
|
2
|
+
|
|
3
|
+
# Dev Kit
|
|
4
|
+
|
|
5
|
+
This project uses `@danieljvdm/dev-kit` to manage portable agent skills and reproducible setup from `dev-kit.jsonc` and `dev-kit.lock.json`.
|
|
6
|
+
|
|
7
|
+
For dev-kit operations, use the `dev-kit` skill and read `{{DEV_KIT_SKILL_PATH}}` before changing managed outputs.
|
|
8
|
+
|
|
9
|
+
<!-- DEV KIT END -->
|