@danieljvdm/dev-kit 0.11.3 → 0.12.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 +70 -76
- package/dev-kit.example.jsonc +0 -4
- package/package.json +10 -6
- package/schema/dev-kit.schema.json +1 -46
- package/skills/build-effect-apis/SKILL.md +77 -0
- package/skills/build-effect-apis/agents/openai.yaml +4 -0
- package/skills/build-effect-apis/references/cloudflare-workers.md +71 -0
- package/skills/build-effect-apis/references/effect-atom-client.md +161 -0
- package/skills/build-effect-apis/references/effect-atom-lifecycle.md +78 -0
- package/skills/build-effect-apis/references/effect-atom-testing.md +74 -0
- package/skills/build-effect-apis/references/runtime-assembly.md +56 -0
- package/skills/build-effect-apis/references/server-and-middleware.md +174 -0
- package/skills/build-effect-apis/references/shared-contracts.md +108 -0
- package/skills/build-effect-apis/references/tanstack-start.md +86 -0
- package/skills/build-effect-apis/references/verification.md +50 -0
- package/skills/dev-kit/SKILL.md +58 -46
- package/skills/effect-architecture-audit/SKILL.md +26 -0
- package/skills/effect-architecture-audit/agents/openai.yaml +4 -0
- package/skills/effect-architecture-audit/references/service-and-boundary-audit.md +150 -0
- package/skills/effect-ts/SKILL.md +21 -256
- package/skills/effect-ts/agents/openai.yaml +3 -3
- package/skills/testing/SKILL.md +5 -0
- package/src/catalog-manager.ts +16 -17
- package/src/catalog.ts +71 -16
- package/src/effect-source.ts +46 -24
- package/src/effect-tsgo.ts +49 -24
- package/src/gitignore.ts +5 -5
- package/src/index.ts +0 -6
- package/src/manifest.ts +0 -34
- package/src/node-symbolic-link.ts +2 -2
- package/src/oxfmt.js +5 -0
- package/src/oxfmt.ts +5 -0
- package/src/oxlint.js +5 -0
- package/src/oxlint.ts +5 -0
- package/src/package-skill-source.ts +51 -59
- package/src/path-digest.ts +7 -7
- package/src/project-package.ts +8 -7
- package/src/project-process-lock.ts +17 -12
- package/src/project-state.ts +1 -1
- package/src/skill-manager.ts +16 -14
- package/src/skill-selector.ts +12 -0
- package/src/sync.ts +170 -120
- package/src/tool-ignore-patterns.js +9 -0
- package/src/tool-ignore-patterns.ts +15 -0
- package/src/vendor.ts +67 -61
- package/src/vite-plus-dependency.ts +10 -11
- package/src/vite-plus-hooks.ts +24 -14
- package/src/vite-plus-quality.ts +21 -172
- package/src/vite-plus.js +81 -0
- package/src/vite-plus.ts +102 -0
- package/templates/AGENTS.md +1 -1
- package/skills/effect-ts/UPSTREAM.md +0 -28
- package/skills/effect-ts/references/atom-cache-lifecycle.md +0 -78
- package/skills/effect-ts/references/atom-http-and-invalidation.md +0 -97
- package/skills/effect-ts/references/atom-tanstack-start.md +0 -69
- package/skills/effect-ts/references/atom-testing.md +0 -67
- package/skills/effect-ts/references/audit-services.md +0 -144
- package/skills/effect-ts/references/features.md +0 -525
- package/skills/effect-ts/references/guide-atom-data-fetching.md +0 -44
- package/skills/effect-ts/references/guide-cli.md +0 -107
- package/skills/effect-ts/references/guide-datetime.md +0 -72
- package/skills/effect-ts/references/guide-effect.md +0 -440
- package/skills/effect-ts/references/guide-error-handling.md +0 -565
- package/skills/effect-ts/references/guide-http-boundaries.md +0 -55
- package/skills/effect-ts/references/guide-layers.md +0 -989
- package/skills/effect-ts/references/guide-observability.md +0 -746
- package/skills/effect-ts/references/guide-retries.md +0 -434
- package/skills/effect-ts/references/guide-schedule.md +0 -343
- package/skills/effect-ts/references/guide-schema.md +0 -664
- package/skills/effect-ts/references/guide-sql.md +0 -536
- package/skills/effect-ts/references/guide-testing.md +0 -532
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +0 -131
- package/skills/effect-ts/references/version-and-source.md +0 -86
- package/templates/vite-plus/vite.config.ts +0 -22
package/src/catalog-manager.ts
CHANGED
|
@@ -36,10 +36,9 @@ export type CatalogAddOptions = CatalogCommandOptions & {
|
|
|
36
36
|
readonly dryRun?: boolean;
|
|
37
37
|
};
|
|
38
38
|
|
|
39
|
-
class CatalogManagerError extends Schema.
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
) {}
|
|
39
|
+
class CatalogManagerError extends Schema.TaggedError<CatalogManagerError>()("CatalogManagerError", {
|
|
40
|
+
message: Schema.String,
|
|
41
|
+
}) {}
|
|
43
42
|
|
|
44
43
|
const formattingOptions = { insertSpaces: true, tabSize: 2 } as const;
|
|
45
44
|
|
|
@@ -63,17 +62,17 @@ const readJsonc = Effect.fn("readCatalogManagerJsonc")(function* <A>(
|
|
|
63
62
|
const fs = yield* FileSystem.FileSystem;
|
|
64
63
|
|
|
65
64
|
if (!(yield* fs.exists(filePath))) {
|
|
66
|
-
return yield*
|
|
65
|
+
return yield* CatalogManagerError.make({ message: `file not found: ${filePath}` });
|
|
67
66
|
}
|
|
68
67
|
const raw = yield* fs.readFileString(filePath);
|
|
69
68
|
const errors: Array<ParseError> = [];
|
|
70
69
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
71
70
|
|
|
72
71
|
if (errors.length > 0) {
|
|
73
|
-
return yield*
|
|
72
|
+
return yield* CatalogManagerError.make({ message: `could not parse ${filePath}` });
|
|
74
73
|
}
|
|
75
74
|
const value = yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
|
|
76
|
-
Effect.mapError((error) =>
|
|
75
|
+
Effect.mapError((error) => CatalogManagerError.make({ message: error.message })),
|
|
77
76
|
);
|
|
78
77
|
|
|
79
78
|
return { raw, value };
|
|
@@ -105,7 +104,7 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
|
|
|
105
104
|
const unknown = requested.filter((skill) => !available.has(skill));
|
|
106
105
|
|
|
107
106
|
if (unknown.length > 0) {
|
|
108
|
-
return yield*
|
|
107
|
+
return yield* CatalogManagerError.make({
|
|
109
108
|
message: `repository does not contain: ${unknown.join(", ")}`,
|
|
110
109
|
});
|
|
111
110
|
}
|
|
@@ -117,7 +116,7 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
|
|
|
117
116
|
if (requested.length > 0)
|
|
118
117
|
return { include: [...new Set(requested)], selected: [...new Set(requested)] };
|
|
119
118
|
if (!(yield* isInteractiveTerminal)) {
|
|
120
|
-
return yield*
|
|
119
|
+
return yield* CatalogManagerError.make({
|
|
121
120
|
message: "choose skills with --skill <name>, or pass --all",
|
|
122
121
|
});
|
|
123
122
|
}
|
|
@@ -194,7 +193,7 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
|
|
|
194
193
|
options: CatalogAddOptions,
|
|
195
194
|
) {
|
|
196
195
|
if (options.all && (options.skills?.length ?? 0) > 0) {
|
|
197
|
-
return yield*
|
|
196
|
+
return yield* CatalogManagerError.make({ message: "use either --all or --skill, not both" });
|
|
198
197
|
}
|
|
199
198
|
const state = yield* readState(options);
|
|
200
199
|
const inspection = yield* inspectCatalogRepository({
|
|
@@ -210,12 +209,12 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
|
|
|
210
209
|
const byRepository = sources.findIndex((source) => source.repository === inspection.repository);
|
|
211
210
|
|
|
212
211
|
if (byId >= 0 && sources[byId]?.repository !== inspection.repository) {
|
|
213
|
-
return yield*
|
|
212
|
+
return yield* CatalogManagerError.make({
|
|
214
213
|
message: `source id ${inspection.id} is already used by ${sources[byId]?.repository}`,
|
|
215
214
|
});
|
|
216
215
|
}
|
|
217
216
|
if (byRepository >= 0 && sources[byRepository]?.id !== inspection.id) {
|
|
218
|
-
return yield*
|
|
217
|
+
return yield* CatalogManagerError.make({
|
|
219
218
|
message: `repository is already cataloged as ${sources[byRepository]?.id}`,
|
|
220
219
|
});
|
|
221
220
|
}
|
|
@@ -226,7 +225,7 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
|
|
|
226
225
|
const existing = sources[existingIndex];
|
|
227
226
|
|
|
228
227
|
if (existing === undefined) {
|
|
229
|
-
return yield*
|
|
228
|
+
return yield* CatalogManagerError.make({ message: "catalog source index is out of bounds" });
|
|
230
229
|
}
|
|
231
230
|
const approved =
|
|
232
231
|
state.lock?.value.sources.find((source) => source.id === existing.id)?.skills ?? [];
|
|
@@ -308,12 +307,12 @@ export const removeCatalogEntry = Effect.fn("removeCatalogEntry")(function* (
|
|
|
308
307
|
const owner = state.lock?.value.sources.find((source) => source.skills.includes(name));
|
|
309
308
|
|
|
310
309
|
if (!owner)
|
|
311
|
-
return yield*
|
|
310
|
+
return yield* CatalogManagerError.make({ message: `catalog entry not found: ${name}` });
|
|
312
311
|
const index = sources.findIndex((source) => source.id === owner.id);
|
|
313
312
|
const source = sources[index];
|
|
314
313
|
|
|
315
314
|
if (!source)
|
|
316
|
-
return yield*
|
|
315
|
+
return yield* CatalogManagerError.make({ message: `source not found: ${owner.id}` });
|
|
317
316
|
if (source.include.includes("*")) {
|
|
318
317
|
const exclude = [...new Set([...(source.exclude ?? []), name])];
|
|
319
318
|
|
|
@@ -342,7 +341,7 @@ export const removeCatalogEntry = Effect.fn("removeCatalogEntry")(function* (
|
|
|
342
341
|
}
|
|
343
342
|
if (!options.yes) {
|
|
344
343
|
if (!(yield* isInteractiveTerminal)) {
|
|
345
|
-
return yield*
|
|
344
|
+
return yield* CatalogManagerError.make({
|
|
346
345
|
message: "catalog removal requires --yes outside a terminal",
|
|
347
346
|
});
|
|
348
347
|
}
|
|
@@ -388,7 +387,7 @@ export const showCatalogSource = Effect.fn("showCatalogSource")(function* (
|
|
|
388
387
|
const source = state.sources.value.sources.find((candidate) => candidate.id === id);
|
|
389
388
|
|
|
390
389
|
if (!source)
|
|
391
|
-
return yield*
|
|
390
|
+
return yield* CatalogManagerError.make({ message: `catalog source not found: ${id}` });
|
|
392
391
|
const locked = state.lock?.value.sources.find((candidate) => candidate.id === id);
|
|
393
392
|
|
|
394
393
|
yield* printLine(source.id);
|
package/src/catalog.ts
CHANGED
|
@@ -2,8 +2,13 @@ import { Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
|
2
2
|
import { ChildProcess } from "effect/unstable/process";
|
|
3
3
|
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
discoverPackageSkills,
|
|
7
|
+
resolvePackageSkillSelector,
|
|
8
|
+
type DiscoveredPackageSkill,
|
|
9
|
+
} from "./package-skill-source.ts";
|
|
6
10
|
import { observePath, type Digest } from "./path-digest.ts";
|
|
11
|
+
import { packageSkillInstallName } from "./skill-selector.ts";
|
|
7
12
|
import {
|
|
8
13
|
SkillSourcesLockSchema,
|
|
9
14
|
type LockedSkillSource,
|
|
@@ -45,7 +50,7 @@ export type ResolvedSkillSource = {
|
|
|
45
50
|
};
|
|
46
51
|
};
|
|
47
52
|
|
|
48
|
-
class CatalogError extends Schema.
|
|
53
|
+
class CatalogError extends Schema.TaggedError<CatalogError>()("CatalogError", {
|
|
49
54
|
message: Schema.String,
|
|
50
55
|
}) {}
|
|
51
56
|
|
|
@@ -61,7 +66,7 @@ const runGit = Effect.fn("runCatalogGit")(function* (cwd: string, args: Readonly
|
|
|
61
66
|
]);
|
|
62
67
|
|
|
63
68
|
if (exitCode !== 0) {
|
|
64
|
-
return yield*
|
|
69
|
+
return yield* CatalogError.make({
|
|
65
70
|
message: `git ${args.join(" ")} failed: ${output.trim()}`,
|
|
66
71
|
});
|
|
67
72
|
}
|
|
@@ -80,11 +85,11 @@ const readCatalogLock = Effect.fn("readCatalogLock")(function* (packageRoot: str
|
|
|
80
85
|
const value = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
81
86
|
|
|
82
87
|
if (errors.length > 0) {
|
|
83
|
-
return yield*
|
|
88
|
+
return yield* CatalogError.make({ message: `invalid skill catalog lock: ${lockPath}` });
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
return yield* Schema.decodeUnknownEffect(SkillSourcesLockSchema)(value).pipe(
|
|
87
|
-
Effect.mapError((error) =>
|
|
92
|
+
Effect.mapError((error) => CatalogError.make({ message: error.message })),
|
|
88
93
|
);
|
|
89
94
|
});
|
|
90
95
|
|
|
@@ -145,7 +150,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
145
150
|
|
|
146
151
|
for (const candidate of discovery.candidates) {
|
|
147
152
|
skills.push({
|
|
148
|
-
name: candidate.name,
|
|
153
|
+
name: packageSkillInstallName(candidate.package, candidate.name),
|
|
149
154
|
selector: candidate.selector,
|
|
150
155
|
description: candidate.description,
|
|
151
156
|
source: candidate.package,
|
|
@@ -159,7 +164,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
159
164
|
);
|
|
160
165
|
|
|
161
166
|
if (duplicates.length > 0) {
|
|
162
|
-
return yield*
|
|
167
|
+
return yield* CatalogError.make({
|
|
163
168
|
message: `duplicate catalog skill selector: ${duplicates[0]?.selector ?? "unknown"}`,
|
|
164
169
|
});
|
|
165
170
|
}
|
|
@@ -171,13 +176,12 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
171
176
|
);
|
|
172
177
|
|
|
173
178
|
if (duplicateFamily !== undefined) {
|
|
174
|
-
return yield*
|
|
179
|
+
return yield* CatalogError.make({
|
|
175
180
|
message: `duplicate catalog family: ${duplicateFamily[0]}`,
|
|
176
181
|
});
|
|
177
182
|
}
|
|
178
183
|
const families: Readonly<Record<string, ReadonlyArray<string>>> = {
|
|
179
|
-
effect: ["effect-ts"],
|
|
180
|
-
"effect-atom-data-fetching": ["effect-ts"],
|
|
184
|
+
effect: ["effect-ts", "effect-architecture-audit", "build-effect-apis"],
|
|
181
185
|
...Object.fromEntries(externalFamilies),
|
|
182
186
|
};
|
|
183
187
|
|
|
@@ -206,6 +210,57 @@ const stripFrontmatterKeys = (text: string, keys: ReadonlyArray<string>): string
|
|
|
206
210
|
return `---\n${lines.join("\n")}\n---${frontmatter[2]}${text.slice(frontmatter[0].length)}`;
|
|
207
211
|
};
|
|
208
212
|
|
|
213
|
+
const rewriteFrontmatterName = (text: string, name: string): string => {
|
|
214
|
+
const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
215
|
+
|
|
216
|
+
if (!frontmatter?.[1]) return text;
|
|
217
|
+
let replacing = false;
|
|
218
|
+
const lines = frontmatter[1].split(/\r?\n/).flatMap((line) => {
|
|
219
|
+
const key = line.match(/^([A-Za-z0-9_-]+):/)?.[1];
|
|
220
|
+
|
|
221
|
+
if (key !== undefined) replacing = key === "name";
|
|
222
|
+
|
|
223
|
+
return replacing ? (key === undefined ? [] : [`name: ${name}`]) : [line];
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return `---\n${lines.join("\n")}\n---${frontmatter[2]}${text.slice(frontmatter[0].length)}`;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const materializePackageSkill = Effect.fn("materializePackageSkill")(function* (
|
|
230
|
+
projectDir: string,
|
|
231
|
+
skill: DiscoveredPackageSkill,
|
|
232
|
+
digest: Digest,
|
|
233
|
+
cache: boolean,
|
|
234
|
+
) {
|
|
235
|
+
const fs = yield* FileSystem.FileSystem;
|
|
236
|
+
const path = yield* Path.Path;
|
|
237
|
+
const installName = packageSkillInstallName(skill.package, skill.name);
|
|
238
|
+
const root = cache
|
|
239
|
+
? path.join(projectDir, ".dev-kit", "cache", "package-skills", installName)
|
|
240
|
+
: path.join(
|
|
241
|
+
yield* fs.makeTempDirectoryScoped({ prefix: "dev-kit-package-plan-" }),
|
|
242
|
+
installName,
|
|
243
|
+
);
|
|
244
|
+
const staged = path.join(root, "skill");
|
|
245
|
+
const ready = path.join(root, ".ready");
|
|
246
|
+
const stamp = `${skill.version}\n${digest}\n`;
|
|
247
|
+
|
|
248
|
+
if (!(yield* fs.exists(ready)) || (yield* fs.readFileString(ready)) !== stamp) {
|
|
249
|
+
yield* fs.remove(root, { force: true, recursive: true });
|
|
250
|
+
yield* fs.makeDirectory(root, { recursive: true });
|
|
251
|
+
yield* fs.copy(skill.path, staged, { overwrite: true });
|
|
252
|
+
const document = path.join(staged, "SKILL.md");
|
|
253
|
+
|
|
254
|
+
yield* fs.writeFileString(
|
|
255
|
+
document,
|
|
256
|
+
rewriteFrontmatterName(yield* fs.readFileString(document), installName),
|
|
257
|
+
);
|
|
258
|
+
yield* fs.writeFileString(ready, stamp);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return staged;
|
|
262
|
+
});
|
|
263
|
+
|
|
209
264
|
const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
210
265
|
projectDir: string,
|
|
211
266
|
source: LockedSkillSource,
|
|
@@ -234,14 +289,14 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
234
289
|
const actual = yield* runGit(checkout, ["rev-parse", "HEAD"]);
|
|
235
290
|
|
|
236
291
|
if (actual !== source.resolved) {
|
|
237
|
-
return yield*
|
|
292
|
+
return yield* CatalogError.make({
|
|
238
293
|
message: `source ${source.id} resolved to ${actual}, expected ${source.resolved}`,
|
|
239
294
|
});
|
|
240
295
|
}
|
|
241
296
|
const symlinks = yield* runGit(checkout, ["ls-files", "--stage", "--", source.skillsPath]);
|
|
242
297
|
|
|
243
298
|
if (symlinks.split(/\r?\n/).some((line) => line.startsWith("120000 "))) {
|
|
244
|
-
return yield*
|
|
299
|
+
return yield* CatalogError.make({
|
|
245
300
|
message: `source ${source.id} contains symlinks; refusing to install it`,
|
|
246
301
|
});
|
|
247
302
|
}
|
|
@@ -251,7 +306,7 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
251
306
|
const observation = yield* observePath(from);
|
|
252
307
|
|
|
253
308
|
if (observation.kind !== "directory") {
|
|
254
|
-
return yield*
|
|
309
|
+
return yield* CatalogError.make({
|
|
255
310
|
message: `source ${source.id} is missing skill ${skill}`,
|
|
256
311
|
});
|
|
257
312
|
}
|
|
@@ -275,7 +330,7 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
275
330
|
approvedDigest !== undefined &&
|
|
276
331
|
(observation.kind !== "directory" || observation.digest !== approvedDigest)
|
|
277
332
|
) {
|
|
278
|
-
return yield*
|
|
333
|
+
return yield* CatalogError.make({
|
|
279
334
|
message: `cached skill ${skill} does not match the approved catalog; remove ${root} and retry`,
|
|
280
335
|
});
|
|
281
336
|
}
|
|
@@ -324,10 +379,10 @@ export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
|
|
|
324
379
|
const observation = yield* observePath(resolved.path);
|
|
325
380
|
|
|
326
381
|
if (observation.kind !== "directory") {
|
|
327
|
-
return yield*
|
|
382
|
+
return yield* CatalogError.make({ message: `package skill is missing: ${selector}` });
|
|
328
383
|
}
|
|
329
384
|
sources.set(selector, {
|
|
330
|
-
path: resolved.
|
|
385
|
+
path: yield* materializePackageSkill(projectDir, resolved, observation.digest, cache),
|
|
331
386
|
linkPath: resolved.linkPath,
|
|
332
387
|
catalog: {
|
|
333
388
|
package: resolved.package,
|
package/src/effect-source.ts
CHANGED
|
@@ -28,7 +28,20 @@ export type EffectSourcePlan = {
|
|
|
28
28
|
readonly tag: string;
|
|
29
29
|
};
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
const EffectSourcePlanJsonSchema = Schema.fromJsonString(
|
|
32
|
+
Schema.Struct({
|
|
33
|
+
action: Schema.Literals(["sync", "unchanged", "skipped"]),
|
|
34
|
+
checkoutDir: Schema.String,
|
|
35
|
+
packageName: Schema.String,
|
|
36
|
+
packageVersion: Schema.String,
|
|
37
|
+
path: Schema.String,
|
|
38
|
+
projectDir: Schema.String,
|
|
39
|
+
repository: Schema.String,
|
|
40
|
+
tag: Schema.String,
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
export class EffectSourceDependencyError extends Schema.TaggedError<EffectSourceDependencyError>()(
|
|
32
45
|
"EffectSourceDependencyError",
|
|
33
46
|
{ packageName: Schema.String },
|
|
34
47
|
) {
|
|
@@ -37,12 +50,12 @@ export class EffectSourceDependencyError extends Schema.TaggedErrorClass<EffectS
|
|
|
37
50
|
}
|
|
38
51
|
}
|
|
39
52
|
|
|
40
|
-
export class EffectSourceCheckoutError extends Schema.
|
|
53
|
+
export class EffectSourceCheckoutError extends Schema.TaggedError<EffectSourceCheckoutError>()(
|
|
41
54
|
"EffectSourceCheckoutError",
|
|
42
55
|
{ message: Schema.String },
|
|
43
56
|
) {}
|
|
44
57
|
|
|
45
|
-
class EffectSourceCommandError extends Schema.
|
|
58
|
+
class EffectSourceCommandError extends Schema.TaggedError<EffectSourceCommandError>()(
|
|
46
59
|
"EffectSourceCommandError",
|
|
47
60
|
{ command: Schema.String, exitCode: Schema.Int, output: Schema.String },
|
|
48
61
|
) {
|
|
@@ -76,7 +89,7 @@ const runCommand = Effect.fn("runEffectSourceCommand")(function* (
|
|
|
76
89
|
const trimmed = output.trim();
|
|
77
90
|
|
|
78
91
|
if (exitCode !== 0) {
|
|
79
|
-
return yield*
|
|
92
|
+
return yield* EffectSourceCommandError.make({
|
|
80
93
|
command: [command, ...args].join(" "),
|
|
81
94
|
exitCode,
|
|
82
95
|
output: trimmed,
|
|
@@ -104,12 +117,12 @@ const readPackageVersion = Effect.fn("readEffectSourcePackageVersion")(function*
|
|
|
104
117
|
.readFileString(manifestPath)
|
|
105
118
|
.pipe(
|
|
106
119
|
Effect.catchReason("PlatformError", "NotFound", () =>
|
|
107
|
-
Effect.fail(
|
|
120
|
+
Effect.fail(EffectSourceDependencyError.make({ packageName })),
|
|
108
121
|
),
|
|
109
122
|
);
|
|
110
123
|
|
|
111
|
-
return yield* Schema.
|
|
112
|
-
Effect.mapError(() =>
|
|
124
|
+
return yield* Schema.decodeEffect(PackageVersionSchema)(contents).pipe(
|
|
125
|
+
Effect.mapError(() => EffectSourceDependencyError.make({ packageName })),
|
|
113
126
|
Effect.map((manifest) => manifest.version),
|
|
114
127
|
);
|
|
115
128
|
});
|
|
@@ -121,7 +134,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
121
134
|
const path = yield* Path.Path;
|
|
122
135
|
|
|
123
136
|
if (candidate.length === 0 || path.isAbsolute(candidate)) {
|
|
124
|
-
return yield*
|
|
137
|
+
return yield* EffectSourceCheckoutError.make({
|
|
125
138
|
message: `Effect source path must be a non-empty project-relative path: ${candidate}`,
|
|
126
139
|
});
|
|
127
140
|
}
|
|
@@ -134,7 +147,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
134
147
|
relative.startsWith(`..${path.sep}`) ||
|
|
135
148
|
path.isAbsolute(relative)
|
|
136
149
|
) {
|
|
137
|
-
return yield*
|
|
150
|
+
return yield* EffectSourceCheckoutError.make({
|
|
138
151
|
message: `Effect source path resolves outside the project: ${candidate}`,
|
|
139
152
|
});
|
|
140
153
|
}
|
|
@@ -143,7 +156,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
143
156
|
for (const segment of relative.split(path.sep).slice(0, -1)) {
|
|
144
157
|
ancestor = path.join(ancestor, segment);
|
|
145
158
|
if ((yield* observeSymbolicLink(ancestor)).kind === "symlink") {
|
|
146
|
-
return yield*
|
|
159
|
+
return yield* EffectSourceCheckoutError.make({
|
|
147
160
|
message: `Effect source path has a symlink ancestor: ${ancestor}`,
|
|
148
161
|
});
|
|
149
162
|
}
|
|
@@ -163,31 +176,30 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
|
|
|
163
176
|
const fs = yield* FileSystem.FileSystem;
|
|
164
177
|
|
|
165
178
|
if ((yield* observeSymbolicLink(checkoutDir)).kind === "symlink") {
|
|
166
|
-
return yield*
|
|
179
|
+
return yield* EffectSourceCheckoutError.make({
|
|
167
180
|
message: `Effect source destination is a symlink: ${checkoutDir}`,
|
|
168
181
|
});
|
|
169
182
|
}
|
|
170
183
|
if (!(yield* fs.exists(checkoutDir))) return "sync" as const;
|
|
171
184
|
|
|
172
185
|
const actualRoot = yield* runGit(checkoutDir, ["rev-parse", "--show-toplevel"]).pipe(
|
|
173
|
-
Effect.mapError(
|
|
174
|
-
(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
}),
|
|
186
|
+
Effect.mapError(() =>
|
|
187
|
+
EffectSourceCheckoutError.make({
|
|
188
|
+
message: `Effect source destination exists but is not a Git checkout: ${checkoutDir}`,
|
|
189
|
+
}),
|
|
178
190
|
),
|
|
179
191
|
);
|
|
180
192
|
const expectedRoot = yield* fs.realPath(checkoutDir);
|
|
181
193
|
|
|
182
194
|
if ((yield* fs.realPath(actualRoot)) !== expectedRoot) {
|
|
183
|
-
return yield*
|
|
195
|
+
return yield* EffectSourceCheckoutError.make({
|
|
184
196
|
message: `Effect source destination is nested inside another Git checkout: ${checkoutDir}`,
|
|
185
197
|
});
|
|
186
198
|
}
|
|
187
199
|
const remote = yield* runGit(checkoutDir, ["remote", "get-url", "origin"]);
|
|
188
200
|
|
|
189
201
|
if (remote !== repository) {
|
|
190
|
-
return yield*
|
|
202
|
+
return yield* EffectSourceCheckoutError.make({
|
|
191
203
|
message: `Effect source origin is ${remote}; expected ${repository}`,
|
|
192
204
|
});
|
|
193
205
|
}
|
|
@@ -208,7 +220,7 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
|
|
|
208
220
|
const dirty = yield* runGit(checkoutDir, ["status", "--porcelain", "--untracked-files=all"]);
|
|
209
221
|
|
|
210
222
|
if (dirty.length > 0) {
|
|
211
|
-
return yield*
|
|
223
|
+
return yield* EffectSourceCheckoutError.make({
|
|
212
224
|
message: `Effect source checkout has local changes; refusing to switch ${checkoutDir} to ${tag}`,
|
|
213
225
|
});
|
|
214
226
|
}
|
|
@@ -225,14 +237,14 @@ export const planEffectSource = Effect.fn("planEffectSource")(function* (
|
|
|
225
237
|
const packageName = options.packageName ?? "effect";
|
|
226
238
|
|
|
227
239
|
if (!isTypeScriptPackageName(packageName)) {
|
|
228
|
-
return yield*
|
|
240
|
+
return yield* EffectSourceCheckoutError.make({
|
|
229
241
|
message: `invalid Effect source package name: ${packageName}`,
|
|
230
242
|
});
|
|
231
243
|
}
|
|
232
244
|
const repository = options.repository ?? DEFAULT_EFFECT_REPOSITORY;
|
|
233
245
|
|
|
234
246
|
if (repository.length === 0) {
|
|
235
|
-
return yield*
|
|
247
|
+
return yield* EffectSourceCheckoutError.make({
|
|
236
248
|
message: "Effect source repository cannot be empty",
|
|
237
249
|
});
|
|
238
250
|
}
|
|
@@ -289,7 +301,7 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
|
|
|
289
301
|
staged,
|
|
290
302
|
]);
|
|
291
303
|
if (yield* fs.exists(plan.checkoutDir)) {
|
|
292
|
-
return yield*
|
|
304
|
+
return yield* EffectSourceCheckoutError.make({
|
|
293
305
|
message: `Effect source destination appeared while cloning: ${plan.checkoutDir}`,
|
|
294
306
|
});
|
|
295
307
|
}
|
|
@@ -344,9 +356,19 @@ export const syncEffectSource = Effect.fn("syncEffectSource")(function* (
|
|
|
344
356
|
}
|
|
345
357
|
yield* acquireProjectProcessLock(plan.projectDir);
|
|
346
358
|
const replanned = yield* planEffectSource(options);
|
|
359
|
+
const [plannedSignature, replannedSignature] = yield* Effect.all([
|
|
360
|
+
Schema.encodeEffect(EffectSourcePlanJsonSchema)(plan),
|
|
361
|
+
Schema.encodeEffect(EffectSourcePlanJsonSchema)(replanned),
|
|
362
|
+
]).pipe(
|
|
363
|
+
Effect.mapError((error) =>
|
|
364
|
+
EffectSourceCheckoutError.make({
|
|
365
|
+
message: `could not encode Effect source plan: ${error.message}`,
|
|
366
|
+
}),
|
|
367
|
+
),
|
|
368
|
+
);
|
|
347
369
|
|
|
348
|
-
if (
|
|
349
|
-
return yield*
|
|
370
|
+
if (plannedSignature !== replannedSignature) {
|
|
371
|
+
return yield* EffectSourceCheckoutError.make({
|
|
350
372
|
message: "Effect source checkout changed after planning; rerun the command",
|
|
351
373
|
});
|
|
352
374
|
}
|