@danieljvdm/dev-kit 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -68
- package/dev-kit.example.jsonc +8 -3
- package/package.json +19 -15
- package/schema/dev-kit.schema.json +52 -0
- package/skill-sources.jsonc +8 -12
- package/skill-sources.lock.json +3 -9
- package/skills/dev-kit/SKILL.md +74 -24
- package/skills/effect-atom-data-fetching/SKILL.md +40 -0
- package/skills/effect-atom-data-fetching/agents/openai.yaml +4 -0
- package/skills/effect-atom-data-fetching/references/cache-lifecycle.md +72 -0
- package/skills/effect-atom-data-fetching/references/http-and-invalidation.md +93 -0
- package/skills/effect-atom-data-fetching/references/tanstack-start.md +69 -0
- package/skills/effect-atom-data-fetching/references/testing.md +63 -0
- package/skills/effect-ts/agents/openai.yaml +0 -1
- package/skills/effect-ts/references/audit-services.md +11 -11
- package/skills/effect-ts/references/guide-effect.md +56 -69
- package/skills/effect-ts/references/guide-error-handling.md +64 -73
- package/skills/effect-ts/references/guide-layers.md +187 -215
- package/skills/effect-ts/references/guide-observability.md +91 -116
- package/skills/effect-ts/references/guide-retries.md +32 -44
- package/skills/effect-ts/references/guide-schedule.md +26 -40
- package/skills/effect-ts/references/guide-schema.md +50 -57
- package/skills/effect-ts/references/guide-sql.md +47 -50
- package/skills/effect-ts/references/guide-testing.md +96 -98
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
- package/skills/effect-ts/references/version-and-source.md +0 -1
- package/src/bin/dev-kit.ts +61 -28
- package/src/catalog-manager.ts +86 -34
- package/src/catalog.ts +72 -34
- package/src/cli-ui.ts +20 -16
- package/src/effect-source.ts +49 -19
- package/src/effect-tsgo.ts +66 -35
- package/src/gitignore.ts +19 -6
- package/src/index.ts +12 -0
- package/src/manifest.ts +51 -3
- package/src/node-symbolic-link.ts +3 -0
- package/src/oxlint-plugin-effect.js +3 -0
- package/src/oxlint-plugin-style.d.ts +8 -0
- package/src/oxlint-plugin-style.js +8 -0
- package/src/oxlint.js +14 -0
- package/src/oxlint.ts +14 -0
- package/src/package-skill-source.ts +190 -75
- package/src/path-digest.ts +37 -10
- package/src/project-package.ts +59 -0
- package/src/project-process-lock.ts +19 -12
- package/src/project-state.ts +29 -2
- package/src/skill-manager.ts +134 -55
- package/src/skill-selector.ts +8 -2
- package/src/source-manifest.ts +2 -6
- package/src/sync.ts +491 -121
- package/src/vendor.ts +112 -42
- package/src/vite-plus-hooks.ts +174 -0
- package/src/vite-plus-quality.ts +49 -0
- package/templates/AGENTS.md +9 -0
- package/templates/vite-plus/github-actions-check.yml +44 -0
- package/templates/vite-plus/vite.config.ts +22 -0
package/src/effect-source.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Config, Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
2
2
|
import { ChildProcess } from "effect/unstable/process";
|
|
3
3
|
|
|
4
|
-
import { acquireProjectProcessLock } from "./project-process-lock.ts";
|
|
5
4
|
import { printStatus, withSpinner } from "./cli-ui.ts";
|
|
6
5
|
import { observeSymbolicLink } from "./node-symbolic-link.ts";
|
|
6
|
+
import { acquireProjectProcessLock } from "./project-process-lock.ts";
|
|
7
7
|
import { isTypeScriptPackageName } from "./typescript-package-name.ts";
|
|
8
8
|
|
|
9
9
|
export const DEFAULT_EFFECT_REPOSITORY = "https://github.com/Effect-TS/effect.git";
|
|
@@ -55,9 +55,7 @@ class EffectSourceCommandError extends Schema.TaggedErrorClass<EffectSourceComma
|
|
|
55
55
|
|
|
56
56
|
const PackageVersionSchema = Schema.fromJsonString(
|
|
57
57
|
Schema.Struct({
|
|
58
|
-
version: Schema.String.check(
|
|
59
|
-
Schema.isPattern(/^[0-9A-Za-z][0-9A-Za-z.+-]*$/),
|
|
60
|
-
),
|
|
58
|
+
version: Schema.String.check(Schema.isPattern(/^[0-9A-Za-z][0-9A-Za-z.+-]*$/)),
|
|
61
59
|
}),
|
|
62
60
|
);
|
|
63
61
|
|
|
@@ -76,6 +74,7 @@ const runCommand = Effect.fn("runEffectSourceCommand")(function* (
|
|
|
76
74
|
child.exitCode,
|
|
77
75
|
]);
|
|
78
76
|
const trimmed = output.trim();
|
|
77
|
+
|
|
79
78
|
if (exitCode !== 0) {
|
|
80
79
|
return yield* new EffectSourceCommandError({
|
|
81
80
|
command: [command, ...args].join(" "),
|
|
@@ -83,11 +82,11 @@ const runCommand = Effect.fn("runEffectSourceCommand")(function* (
|
|
|
83
82
|
output: trimmed,
|
|
84
83
|
});
|
|
85
84
|
}
|
|
85
|
+
|
|
86
86
|
return trimmed;
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
const runGit = (cwd: string, args: ReadonlyArray<string>) =>
|
|
90
|
-
runCommand(cwd, "git", args);
|
|
89
|
+
const runGit = (cwd: string, args: ReadonlyArray<string>) => runCommand(cwd, "git", args);
|
|
91
90
|
|
|
92
91
|
const readPackageVersion = Effect.fn("readEffectSourcePackageVersion")(function* (
|
|
93
92
|
projectDir: string,
|
|
@@ -101,11 +100,14 @@ const readPackageVersion = Effect.fn("readEffectSourcePackageVersion")(function*
|
|
|
101
100
|
...packageName.split("/"),
|
|
102
101
|
"package.json",
|
|
103
102
|
);
|
|
104
|
-
const contents = yield* fs
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
const contents = yield* fs
|
|
104
|
+
.readFileString(manifestPath)
|
|
105
|
+
.pipe(
|
|
106
|
+
Effect.catchReason("PlatformError", "NotFound", () =>
|
|
107
|
+
Effect.fail(new EffectSourceDependencyError({ packageName })),
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
|
|
109
111
|
return yield* Schema.decodeUnknownEffect(PackageVersionSchema)(contents).pipe(
|
|
110
112
|
Effect.mapError(() => new EffectSourceDependencyError({ packageName })),
|
|
111
113
|
Effect.map((manifest) => manifest.version),
|
|
@@ -117,6 +119,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
117
119
|
candidate: string,
|
|
118
120
|
) {
|
|
119
121
|
const path = yield* Path.Path;
|
|
122
|
+
|
|
120
123
|
if (candidate.length === 0 || path.isAbsolute(candidate)) {
|
|
121
124
|
return yield* new EffectSourceCheckoutError({
|
|
122
125
|
message: `Effect source path must be a non-empty project-relative path: ${candidate}`,
|
|
@@ -124,6 +127,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
124
127
|
}
|
|
125
128
|
const checkoutDir = path.resolve(projectDir, candidate);
|
|
126
129
|
const relative = path.relative(projectDir, checkoutDir);
|
|
130
|
+
|
|
127
131
|
if (
|
|
128
132
|
relative.length === 0 ||
|
|
129
133
|
relative === ".." ||
|
|
@@ -135,6 +139,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
135
139
|
});
|
|
136
140
|
}
|
|
137
141
|
let ancestor = projectDir;
|
|
142
|
+
|
|
138
143
|
for (const segment of relative.split(path.sep).slice(0, -1)) {
|
|
139
144
|
ancestor = path.join(ancestor, segment);
|
|
140
145
|
if ((yield* observeSymbolicLink(ancestor)).kind === "symlink") {
|
|
@@ -143,6 +148,7 @@ const resolveCheckoutPath = Effect.fn("resolveEffectSourceCheckoutPath")(functio
|
|
|
143
148
|
});
|
|
144
149
|
}
|
|
145
150
|
}
|
|
151
|
+
|
|
146
152
|
return {
|
|
147
153
|
checkoutDir,
|
|
148
154
|
path: path.sep === "/" ? relative : relative.split(path.sep).join("/"),
|
|
@@ -155,6 +161,7 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
|
|
|
155
161
|
tag: string,
|
|
156
162
|
) {
|
|
157
163
|
const fs = yield* FileSystem.FileSystem;
|
|
164
|
+
|
|
158
165
|
if ((yield* observeSymbolicLink(checkoutDir)).kind === "symlink") {
|
|
159
166
|
return yield* new EffectSourceCheckoutError({
|
|
160
167
|
message: `Effect source destination is a symlink: ${checkoutDir}`,
|
|
@@ -163,19 +170,22 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
|
|
|
163
170
|
if (!(yield* fs.exists(checkoutDir))) return "sync" as const;
|
|
164
171
|
|
|
165
172
|
const actualRoot = yield* runGit(checkoutDir, ["rev-parse", "--show-toplevel"]).pipe(
|
|
166
|
-
Effect.mapError(
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
Effect.mapError(
|
|
174
|
+
() =>
|
|
175
|
+
new EffectSourceCheckoutError({
|
|
176
|
+
message: `Effect source destination exists but is not a Git checkout: ${checkoutDir}`,
|
|
177
|
+
}),
|
|
170
178
|
),
|
|
171
179
|
);
|
|
172
180
|
const expectedRoot = yield* fs.realPath(checkoutDir);
|
|
181
|
+
|
|
173
182
|
if ((yield* fs.realPath(actualRoot)) !== expectedRoot) {
|
|
174
183
|
return yield* new EffectSourceCheckoutError({
|
|
175
184
|
message: `Effect source destination is nested inside another Git checkout: ${checkoutDir}`,
|
|
176
185
|
});
|
|
177
186
|
}
|
|
178
187
|
const remote = yield* runGit(checkoutDir, ["remote", "get-url", "origin"]);
|
|
188
|
+
|
|
179
189
|
if (remote !== repository) {
|
|
180
190
|
return yield* new EffectSourceCheckoutError({
|
|
181
191
|
message: `Effect source origin is ${remote}; expected ${repository}`,
|
|
@@ -188,17 +198,21 @@ const inspectExistingCheckout = Effect.fn("inspectExistingEffectSource")(functio
|
|
|
188
198
|
"--verify",
|
|
189
199
|
`${tag}^{commit}`,
|
|
190
200
|
]).pipe(Effect.catchTag("EffectSourceCommandError", () => Effect.void));
|
|
201
|
+
|
|
191
202
|
if (target !== undefined) {
|
|
192
203
|
const current = yield* runGit(checkoutDir, ["rev-parse", "HEAD"]);
|
|
204
|
+
|
|
193
205
|
if (current === target) return "unchanged" as const;
|
|
194
206
|
}
|
|
195
207
|
|
|
196
208
|
const dirty = yield* runGit(checkoutDir, ["status", "--porcelain", "--untracked-files=all"]);
|
|
209
|
+
|
|
197
210
|
if (dirty.length > 0) {
|
|
198
211
|
return yield* new EffectSourceCheckoutError({
|
|
199
212
|
message: `Effect source checkout has local changes; refusing to switch ${checkoutDir} to ${tag}`,
|
|
200
213
|
});
|
|
201
214
|
}
|
|
215
|
+
|
|
202
216
|
return "sync" as const;
|
|
203
217
|
});
|
|
204
218
|
|
|
@@ -209,14 +223,18 @@ export const planEffectSource = Effect.fn("planEffectSource")(function* (
|
|
|
209
223
|
const path = yield* Path.Path;
|
|
210
224
|
const projectDir = yield* fs.realPath(path.resolve(options.projectDir ?? "."));
|
|
211
225
|
const packageName = options.packageName ?? "effect";
|
|
226
|
+
|
|
212
227
|
if (!isTypeScriptPackageName(packageName)) {
|
|
213
228
|
return yield* new EffectSourceCheckoutError({
|
|
214
229
|
message: `invalid Effect source package name: ${packageName}`,
|
|
215
230
|
});
|
|
216
231
|
}
|
|
217
232
|
const repository = options.repository ?? DEFAULT_EFFECT_REPOSITORY;
|
|
233
|
+
|
|
218
234
|
if (repository.length === 0) {
|
|
219
|
-
return yield* new EffectSourceCheckoutError({
|
|
235
|
+
return yield* new EffectSourceCheckoutError({
|
|
236
|
+
message: "Effect source repository cannot be empty",
|
|
237
|
+
});
|
|
220
238
|
}
|
|
221
239
|
const resolved = yield* resolveCheckoutPath(
|
|
222
240
|
projectDir,
|
|
@@ -225,9 +243,11 @@ export const planEffectSource = Effect.fn("planEffectSource")(function* (
|
|
|
225
243
|
const packageVersion = yield* readPackageVersion(projectDir, packageName);
|
|
226
244
|
const tag = `effect@${packageVersion}`;
|
|
227
245
|
const ci = yield* Config.string("CI").pipe(Config.withDefault(""));
|
|
228
|
-
const action =
|
|
229
|
-
|
|
230
|
-
|
|
246
|
+
const action =
|
|
247
|
+
ci === "true" || ci === "1"
|
|
248
|
+
? ("skipped" as const)
|
|
249
|
+
: yield* inspectExistingCheckout(resolved.checkoutDir, repository, tag);
|
|
250
|
+
|
|
231
251
|
return {
|
|
232
252
|
action,
|
|
233
253
|
checkoutDir: resolved.checkoutDir,
|
|
@@ -246,14 +266,17 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
|
|
|
246
266
|
if (plan.action !== "sync") return;
|
|
247
267
|
const fs = yield* FileSystem.FileSystem;
|
|
248
268
|
const path = yield* Path.Path;
|
|
269
|
+
|
|
249
270
|
if (!(yield* fs.exists(plan.checkoutDir))) {
|
|
250
271
|
const parent = path.dirname(plan.checkoutDir);
|
|
272
|
+
|
|
251
273
|
yield* fs.makeDirectory(parent, { recursive: true });
|
|
252
274
|
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
253
275
|
directory: parent,
|
|
254
276
|
prefix: ".dev-kit-effect-source-",
|
|
255
277
|
});
|
|
256
278
|
const staged = path.join(tempDir, "checkout");
|
|
279
|
+
|
|
257
280
|
yield* runGit(plan.projectDir, [
|
|
258
281
|
"clone",
|
|
259
282
|
"--depth",
|
|
@@ -271,6 +294,7 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
|
|
|
271
294
|
});
|
|
272
295
|
}
|
|
273
296
|
yield* fs.rename(staged, plan.checkoutDir);
|
|
297
|
+
|
|
274
298
|
return;
|
|
275
299
|
}
|
|
276
300
|
|
|
@@ -289,6 +313,7 @@ export const applyEffectSourcePlan = Effect.fn("applyEffectSourcePlan")(function
|
|
|
289
313
|
"--verify",
|
|
290
314
|
`${plan.tag}^{commit}`,
|
|
291
315
|
]);
|
|
316
|
+
|
|
292
317
|
yield* runGit(plan.checkoutDir, ["checkout", "--detach", target]);
|
|
293
318
|
});
|
|
294
319
|
|
|
@@ -297,8 +322,10 @@ export const syncEffectSource = Effect.fn("syncEffectSource")(function* (
|
|
|
297
322
|
) {
|
|
298
323
|
const plan = yield* planEffectSource(options);
|
|
299
324
|
const detail = `${plan.tag} → ${plan.path}`;
|
|
325
|
+
|
|
300
326
|
if (plan.action === "skipped") {
|
|
301
327
|
yield* printStatus("plan", "Effect source skipped", "CI");
|
|
328
|
+
|
|
302
329
|
return;
|
|
303
330
|
}
|
|
304
331
|
if (options.dryRun) {
|
|
@@ -307,14 +334,17 @@ export const syncEffectSource = Effect.fn("syncEffectSource")(function* (
|
|
|
307
334
|
plan.action === "sync" ? "Would sync Effect source" : "Effect source up to date",
|
|
308
335
|
detail,
|
|
309
336
|
);
|
|
337
|
+
|
|
310
338
|
return;
|
|
311
339
|
}
|
|
312
340
|
if (plan.action === "unchanged") {
|
|
313
341
|
yield* printStatus("success", "Effect source up to date", detail);
|
|
342
|
+
|
|
314
343
|
return;
|
|
315
344
|
}
|
|
316
345
|
yield* acquireProjectProcessLock(plan.projectDir);
|
|
317
346
|
const replanned = yield* planEffectSource(options);
|
|
347
|
+
|
|
318
348
|
if (JSON.stringify(plan) !== JSON.stringify(replanned)) {
|
|
319
349
|
return yield* new EffectSourceCheckoutError({
|
|
320
350
|
message: "Effect source checkout changed after planning; rerun the command",
|
package/src/effect-tsgo.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Crypto, Effect, Encoding, FileSystem, Path, Schema, Stream } from "effect";
|
|
2
2
|
import { ChildProcess } from "effect/unstable/process";
|
|
3
3
|
|
|
4
|
-
import { acquireProjectProcessLock } from "./project-process-lock.ts";
|
|
5
4
|
import { printStatus, withSpinner } from "./cli-ui.ts";
|
|
5
|
+
import { acquireProjectProcessLock } from "./project-process-lock.ts";
|
|
6
6
|
import { isTypeScriptPackageName } from "./typescript-package-name.ts";
|
|
7
7
|
|
|
8
8
|
export const EFFECT_TSGO_VERSION = "0.24.3";
|
|
@@ -65,9 +65,7 @@ export class InvalidEffectTsgoPackageNameError extends Schema.TaggedErrorClass<I
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const PackageVersionSchema = Schema.fromJsonString(
|
|
69
|
-
Schema.Struct({ version: Schema.String }),
|
|
70
|
-
);
|
|
68
|
+
const PackageVersionSchema = Schema.fromJsonString(Schema.Struct({ version: Schema.String }));
|
|
71
69
|
|
|
72
70
|
const packagePath = (path: Path.Path, projectDir: string, packageName: string): string =>
|
|
73
71
|
path.join(projectDir, "node_modules", ...packageName.split("/"), "package.json");
|
|
@@ -80,18 +78,17 @@ const readExactPackageVersion = Effect.fn("readExactEffectTsgoPackageVersion")(f
|
|
|
80
78
|
const fs = yield* FileSystem.FileSystem;
|
|
81
79
|
const path = yield* Path.Path;
|
|
82
80
|
const manifestPath = packagePath(path, projectDir, packageName);
|
|
83
|
-
const contents = yield* fs
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
"NotFound",
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
81
|
+
const contents = yield* fs
|
|
82
|
+
.readFileString(manifestPath)
|
|
83
|
+
.pipe(
|
|
84
|
+
Effect.catchReason("PlatformError", "NotFound", () =>
|
|
85
|
+
Effect.fail(new EffectTsgoDependencyError({ packageName, expectedVersion })),
|
|
86
|
+
),
|
|
87
|
+
);
|
|
90
88
|
const manifest = yield* Schema.decodeUnknownEffect(PackageVersionSchema)(contents).pipe(
|
|
91
|
-
Effect.mapError(() =>
|
|
92
|
-
new EffectTsgoDependencyError({ packageName, expectedVersion }),
|
|
93
|
-
),
|
|
89
|
+
Effect.mapError(() => new EffectTsgoDependencyError({ packageName, expectedVersion })),
|
|
94
90
|
);
|
|
91
|
+
|
|
95
92
|
if (manifest.version !== expectedVersion) {
|
|
96
93
|
return yield* new EffectTsgoDependencyError({
|
|
97
94
|
packageName,
|
|
@@ -99,6 +96,7 @@ const readExactPackageVersion = Effect.fn("readExactEffectTsgoPackageVersion")(f
|
|
|
99
96
|
actualVersion: manifest.version,
|
|
100
97
|
});
|
|
101
98
|
}
|
|
99
|
+
|
|
102
100
|
return manifest.version;
|
|
103
101
|
});
|
|
104
102
|
|
|
@@ -108,53 +106,80 @@ const resolveEffectTsgoExecutable = Effect.fn("resolveEffectTsgoExecutable")(fun
|
|
|
108
106
|
const fs = yield* FileSystem.FileSystem;
|
|
109
107
|
const path = yield* Path.Path;
|
|
110
108
|
const binDir = path.join(projectDir, "node_modules", ".bin");
|
|
111
|
-
const candidates =
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
const candidates =
|
|
110
|
+
path.sep === "\\"
|
|
111
|
+
? [path.join(binDir, "effect-tsgo.cmd"), path.join(binDir, "effect-tsgo")]
|
|
112
|
+
: [path.join(binDir, "effect-tsgo"), path.join(binDir, "effect-tsgo.cmd")];
|
|
113
|
+
|
|
114
114
|
for (const candidate of candidates) {
|
|
115
115
|
if (yield* fs.exists(candidate)) return candidate;
|
|
116
116
|
}
|
|
117
|
+
|
|
117
118
|
return yield* new EffectTsgoDependencyError({
|
|
118
119
|
packageName: "@effect/tsgo",
|
|
119
120
|
expectedVersion: EFFECT_TSGO_VERSION,
|
|
120
121
|
});
|
|
121
122
|
});
|
|
122
123
|
|
|
123
|
-
const digestFileContents = Effect.fn("digestEffectTsgoFileContents")(function* (
|
|
124
|
-
filePath: string,
|
|
125
|
-
) {
|
|
124
|
+
const digestFileContents = Effect.fn("digestEffectTsgoFileContents")(function* (filePath: string) {
|
|
126
125
|
const crypto = yield* Crypto.Crypto;
|
|
127
126
|
const fs = yield* FileSystem.FileSystem;
|
|
127
|
+
|
|
128
128
|
return Encoding.encodeHex(yield* crypto.digest("SHA-256", yield* fs.readFile(filePath)));
|
|
129
129
|
});
|
|
130
130
|
|
|
131
|
+
const findNodeModulesRoot = (path: Path.Path, packageJsonPath: string): string | undefined => {
|
|
132
|
+
let current = path.dirname(packageJsonPath);
|
|
133
|
+
|
|
134
|
+
while (true) {
|
|
135
|
+
if (path.basename(current) === "node_modules") return current;
|
|
136
|
+
const parent = path.dirname(current);
|
|
137
|
+
|
|
138
|
+
if (parent === current) return undefined;
|
|
139
|
+
current = parent;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
131
143
|
const isEffectTsgoPatched = Effect.fn("isEffectTsgoPatched")(function* (
|
|
132
144
|
projectDir: string,
|
|
145
|
+
typescriptPackage: string,
|
|
133
146
|
) {
|
|
134
147
|
const fs = yield* FileSystem.FileSystem;
|
|
135
148
|
const path = yield* Path.Path;
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
149
|
+
const typescriptPackageJson = yield* fs.realPath(
|
|
150
|
+
packagePath(path, projectDir, typescriptPackage),
|
|
151
|
+
);
|
|
152
|
+
const effectTsgoPackageJson = yield* fs.realPath(packagePath(path, projectDir, "@effect/tsgo"));
|
|
153
|
+
const typescriptNodeModules = findNodeModulesRoot(path, typescriptPackageJson);
|
|
154
|
+
const effectNodeModules = findNodeModulesRoot(path, effectTsgoPackageJson);
|
|
155
|
+
|
|
156
|
+
if (typescriptNodeModules === undefined || effectNodeModules === undefined) return false;
|
|
157
|
+
|
|
158
|
+
const typescriptScope = path.join(typescriptNodeModules, "@typescript");
|
|
159
|
+
const effectScope = path.join(effectNodeModules, "@effect");
|
|
160
|
+
|
|
161
|
+
if (!(yield* fs.exists(typescriptScope)) || !(yield* fs.exists(effectScope))) return false;
|
|
162
|
+
|
|
139
163
|
const executableName = path.sep === "\\" ? "tsc.exe" : "tsc";
|
|
140
|
-
const effectExecutableNames =
|
|
141
|
-
? ["tsc.exe", "tsc-next.exe"]
|
|
142
|
-
|
|
143
|
-
for (const entry of
|
|
164
|
+
const effectExecutableNames =
|
|
165
|
+
path.sep === "\\" ? ["tsc.exe", "tsc-next.exe"] : ["tsc", "tsc-next"];
|
|
166
|
+
|
|
167
|
+
for (const entry of yield* fs.readDirectory(typescriptScope)) {
|
|
144
168
|
if (!entry.startsWith("typescript-")) continue;
|
|
145
169
|
const platform = entry.slice("typescript-".length);
|
|
146
|
-
const installedPath = path.join(
|
|
170
|
+
const installedPath = path.join(typescriptScope, entry, "lib", executableName);
|
|
171
|
+
|
|
147
172
|
if (!(yield* fs.exists(installedPath))) continue;
|
|
148
173
|
const installedDigest = yield* digestFileContents(installedPath);
|
|
174
|
+
|
|
149
175
|
for (const effectExecutableName of effectExecutableNames) {
|
|
150
176
|
const effectBinaryPath = path.join(
|
|
151
|
-
|
|
152
|
-
"node_modules",
|
|
153
|
-
"@effect",
|
|
177
|
+
effectScope,
|
|
154
178
|
`tsgo-${platform}`,
|
|
155
179
|
"lib",
|
|
156
180
|
effectExecutableName,
|
|
157
181
|
);
|
|
182
|
+
|
|
158
183
|
if (
|
|
159
184
|
(yield* fs.exists(effectBinaryPath)) &&
|
|
160
185
|
installedDigest === (yield* digestFileContents(effectBinaryPath))
|
|
@@ -163,6 +188,7 @@ const isEffectTsgoPatched = Effect.fn("isEffectTsgoPatched")(function* (
|
|
|
163
188
|
}
|
|
164
189
|
}
|
|
165
190
|
}
|
|
191
|
+
|
|
166
192
|
return false;
|
|
167
193
|
});
|
|
168
194
|
|
|
@@ -173,6 +199,7 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
|
|
|
173
199
|
const path = yield* Path.Path;
|
|
174
200
|
const projectDir = yield* fs.realPath(path.resolve(options.projectDir ?? "."));
|
|
175
201
|
const typescriptPackage = options.typescriptPackage ?? "typescript";
|
|
202
|
+
|
|
176
203
|
if (!isTypeScriptPackageName(typescriptPackage)) {
|
|
177
204
|
return yield* new InvalidEffectTsgoPackageNameError({ packageName: typescriptPackage });
|
|
178
205
|
}
|
|
@@ -187,16 +214,15 @@ export const planEffectTsgoPatch = Effect.fn("planEffectTsgoPatch")(function* (
|
|
|
187
214
|
EFFECT_TSGO_TYPESCRIPT_VERSION,
|
|
188
215
|
);
|
|
189
216
|
const executable = yield* resolveEffectTsgoExecutable(projectDir);
|
|
217
|
+
|
|
190
218
|
return {
|
|
191
|
-
alreadyPatched: yield* isEffectTsgoPatched(projectDir),
|
|
219
|
+
alreadyPatched: yield* isEffectTsgoPatched(projectDir, typescriptPackage),
|
|
192
220
|
projectDir,
|
|
193
221
|
executable,
|
|
194
222
|
args: [
|
|
195
223
|
"patch",
|
|
196
224
|
...(options.force ? ["--force"] : []),
|
|
197
|
-
...(typescriptPackage === "typescript"
|
|
198
|
-
? []
|
|
199
|
-
: ["--typescript-package", typescriptPackage]),
|
|
225
|
+
...(typescriptPackage === "typescript" ? [] : ["--typescript-package", typescriptPackage]),
|
|
200
226
|
],
|
|
201
227
|
effectTsgoVersion,
|
|
202
228
|
typescriptPackage,
|
|
@@ -218,6 +244,7 @@ export const applyEffectTsgoPatchPlan = Effect.fn("applyEffectTsgoPatchPlan")(fu
|
|
|
218
244
|
child.exitCode,
|
|
219
245
|
]);
|
|
220
246
|
const trimmed = output.trim();
|
|
247
|
+
|
|
221
248
|
if (exitCode !== 0) {
|
|
222
249
|
return yield* new EffectTsgoPatchCommandError({
|
|
223
250
|
command: [plan.executable, ...plan.args].join(" "),
|
|
@@ -232,12 +259,14 @@ export const patchEffectTsgo = Effect.fn("patchEffectTsgo")(function* (
|
|
|
232
259
|
) {
|
|
233
260
|
const plan = yield* planEffectTsgoPatch(options);
|
|
234
261
|
const detail = `@effect/tsgo@${plan.effectTsgoVersion} → ${plan.typescriptPackage}@${plan.typescriptVersion}`;
|
|
262
|
+
|
|
235
263
|
if (options.dryRun) {
|
|
236
264
|
yield* printStatus(
|
|
237
265
|
plan.alreadyPatched ? "success" : "plan",
|
|
238
266
|
plan.alreadyPatched ? "TypeScript patch up to date" : "Would patch TypeScript",
|
|
239
267
|
detail,
|
|
240
268
|
);
|
|
269
|
+
|
|
241
270
|
return plan;
|
|
242
271
|
}
|
|
243
272
|
|
|
@@ -246,10 +275,12 @@ export const patchEffectTsgo = Effect.fn("patchEffectTsgo")(function* (
|
|
|
246
275
|
yield* acquireProjectProcessLock(plan.projectDir);
|
|
247
276
|
if (plan.alreadyPatched) {
|
|
248
277
|
yield* printStatus("success", "TypeScript patch up to date", detail);
|
|
278
|
+
|
|
249
279
|
return plan;
|
|
250
280
|
}
|
|
251
281
|
yield* withSpinner("Patching TypeScript", applyEffectTsgoPatchPlan(plan));
|
|
252
282
|
yield* printStatus("success", "TypeScript patched", detail);
|
|
283
|
+
|
|
253
284
|
return plan;
|
|
254
285
|
}),
|
|
255
286
|
);
|
package/src/gitignore.ts
CHANGED
|
@@ -61,17 +61,20 @@ export const patchGitignoreContents = (
|
|
|
61
61
|
): { readonly contents: string; readonly added: ReadonlyArray<string> } => {
|
|
62
62
|
const lines = current.split(/\r?\n/);
|
|
63
63
|
const added = DEV_KIT_GITIGNORE_ENTRIES.filter((entry) => !lines.includes(entry));
|
|
64
|
+
|
|
64
65
|
if (added.length === 0) return { contents: current, added };
|
|
65
66
|
|
|
66
67
|
const newline = current.includes("\r\n") ? "\r\n" : "\n";
|
|
67
|
-
const separator =
|
|
68
|
-
|
|
69
|
-
: current.endsWith(`${newline}${newline}`)
|
|
68
|
+
const separator =
|
|
69
|
+
current.length === 0
|
|
70
70
|
? ""
|
|
71
|
-
: current.endsWith(newline)
|
|
72
|
-
?
|
|
73
|
-
:
|
|
71
|
+
: current.endsWith(`${newline}${newline}`)
|
|
72
|
+
? ""
|
|
73
|
+
: current.endsWith(newline)
|
|
74
|
+
? newline
|
|
75
|
+
: `${newline}${newline}`;
|
|
74
76
|
const block = ["# dev-kit managed paths", ...added].join(newline);
|
|
77
|
+
|
|
75
78
|
return {
|
|
76
79
|
contents: `${current}${separator}${block}${newline}`,
|
|
77
80
|
added,
|
|
@@ -89,6 +92,7 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
|
|
|
89
92
|
const path = yield* Path.Path;
|
|
90
93
|
const gitignorePath = path.join(projectDir, ".gitignore");
|
|
91
94
|
const observation = yield* observeSymbolicLink(gitignorePath);
|
|
95
|
+
|
|
92
96
|
if (observation.kind === "symlink") {
|
|
93
97
|
return yield* new UnsafeGitignorePathError({
|
|
94
98
|
path: gitignorePath,
|
|
@@ -98,8 +102,10 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
|
|
|
98
102
|
|
|
99
103
|
let current = "";
|
|
100
104
|
let mode: number | undefined;
|
|
105
|
+
|
|
101
106
|
if (observation.kind !== "missing") {
|
|
102
107
|
const info = yield* fs.stat(gitignorePath);
|
|
108
|
+
|
|
103
109
|
if (info.type !== "File") {
|
|
104
110
|
return yield* new UnsafeGitignorePathError({
|
|
105
111
|
path: gitignorePath,
|
|
@@ -111,6 +117,7 @@ const planGitignorePatch = Effect.fn("planGitignorePatch")(function* (
|
|
|
111
117
|
}
|
|
112
118
|
|
|
113
119
|
const patch = patchGitignoreContents(current);
|
|
120
|
+
|
|
114
121
|
return {
|
|
115
122
|
path: gitignorePath,
|
|
116
123
|
changed: patch.added.length > 0,
|
|
@@ -135,6 +142,7 @@ const applyGitignorePatch = Effect.fn("applyGitignorePatch")(function* (
|
|
|
135
142
|
});
|
|
136
143
|
const staged = path.join(tempDir, "next.gitignore");
|
|
137
144
|
const backup = path.join(tempDir, "previous.gitignore");
|
|
145
|
+
|
|
138
146
|
yield* fs.writeFileString(staged, patch.contents, { mode: patch.mode ?? 0o666 });
|
|
139
147
|
|
|
140
148
|
const currentObservation = yield* observeSymbolicLink(patch.path);
|
|
@@ -142,6 +150,7 @@ const applyGitignorePatch = Effect.fn("applyGitignorePatch")(function* (
|
|
|
142
150
|
? currentObservation.kind !== "not-symlink" ||
|
|
143
151
|
(yield* fs.readFileString(patch.path)) !== patch.previousContents
|
|
144
152
|
: currentObservation.kind !== "missing";
|
|
153
|
+
|
|
145
154
|
if (changed) {
|
|
146
155
|
return yield* new GitignoreConflictError({ path: patch.path });
|
|
147
156
|
}
|
|
@@ -188,11 +197,13 @@ export const patchProjectGitignore = Effect.fn("patchProjectGitignore")(function
|
|
|
188
197
|
|
|
189
198
|
if (options.dryRun) {
|
|
190
199
|
const patch = yield* planGitignorePatch(projectDir);
|
|
200
|
+
|
|
191
201
|
yield* printStatus(
|
|
192
202
|
patch.changed ? "plan" : "success",
|
|
193
203
|
patch.changed ? "Would update .gitignore" : ".gitignore up to date",
|
|
194
204
|
patch.changed ? `add ${patch.added.join(", ")}` : undefined,
|
|
195
205
|
);
|
|
206
|
+
|
|
196
207
|
return publicPatch(patch);
|
|
197
208
|
}
|
|
198
209
|
|
|
@@ -200,12 +211,14 @@ export const patchProjectGitignore = Effect.fn("patchProjectGitignore")(function
|
|
|
200
211
|
Effect.gen(function* () {
|
|
201
212
|
yield* acquireProjectProcessLock(projectDir);
|
|
202
213
|
const patch = yield* planGitignorePatch(projectDir);
|
|
214
|
+
|
|
203
215
|
yield* applyGitignorePatch(projectDir, patch);
|
|
204
216
|
yield* printStatus(
|
|
205
217
|
"success",
|
|
206
218
|
patch.changed ? "Updated .gitignore" : ".gitignore up to date",
|
|
207
219
|
patch.changed ? `added ${patch.added.join(", ")}` : undefined,
|
|
208
220
|
);
|
|
221
|
+
|
|
209
222
|
return publicPatch(patch);
|
|
210
223
|
}),
|
|
211
224
|
);
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export {
|
|
2
|
+
type AgentInstructionsSetup,
|
|
3
|
+
AgentInstructionsSetupSchema,
|
|
2
4
|
type ClaudeInstructionsSetup,
|
|
3
5
|
ClaudeInstructionsSetupSchema,
|
|
4
6
|
type DevKitManifest,
|
|
@@ -9,6 +11,10 @@ export {
|
|
|
9
11
|
EffectTsgoSetupSchema,
|
|
10
12
|
type HarnessTarget,
|
|
11
13
|
TargetConfigSchema,
|
|
14
|
+
type VitePlusQualitySetup,
|
|
15
|
+
VitePlusQualitySetupSchema,
|
|
16
|
+
type VitePlusSetup,
|
|
17
|
+
VitePlusSetupSchema,
|
|
12
18
|
} from "./manifest.ts";
|
|
13
19
|
export {
|
|
14
20
|
applyEffectSourcePlan,
|
|
@@ -64,6 +70,9 @@ export {
|
|
|
64
70
|
DevKitLockSchema,
|
|
65
71
|
EffectSourceLockSchema,
|
|
66
72
|
EffectTsgoLockSchema,
|
|
73
|
+
ManagedAgentInstructionsOutputSchema,
|
|
74
|
+
ManagedClaudeInstructionsOutputSchema,
|
|
75
|
+
ManagedGeneratedFileOutputSchema,
|
|
67
76
|
ManagedInstructionOutputSchema,
|
|
68
77
|
ManagedOutputSchema,
|
|
69
78
|
ManagedSkillOutputSchema,
|
|
@@ -73,6 +82,9 @@ export {
|
|
|
73
82
|
type DevKitLock,
|
|
74
83
|
type EffectSourceLock,
|
|
75
84
|
type EffectTsgoLock,
|
|
85
|
+
type ManagedAgentInstructionsOutput,
|
|
86
|
+
type ManagedClaudeInstructionsOutput,
|
|
87
|
+
type ManagedGeneratedFileOutput,
|
|
76
88
|
type ManagedInstructionOutput,
|
|
77
89
|
type ManagedOutput,
|
|
78
90
|
type ManagedSkillOutput,
|