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