@danieljvdm/dev-kit 0.14.0 → 0.16.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 +155 -99
- package/dev-kit.example.jsonc +4 -3
- package/package.json +1 -1
- package/schema/dev-kit.schema.json +19 -42
- package/skills/build-effect-apis/SKILL.md +13 -31
- package/skills/build-effect-apis/references/verification.md +3 -3
- package/skills/dev-kit/SKILL.md +115 -222
- package/skills/effect-atom-state/SKILL.md +97 -0
- package/skills/effect-atom-state/agents/openai.yaml +4 -0
- package/skills/effect-atom-state/references/effect-atom-workflows.md +180 -0
- package/skills/open-pull-request/SKILL.md +62 -23
- package/src/bin/dev-kit.ts +21 -0
- package/src/catalog.ts +39 -15
- package/src/effect-source.ts +70 -4
- package/src/global-cache.ts +304 -0
- package/src/index.ts +6 -6
- package/src/manifest.ts +28 -29
- package/src/oxlint.js +23 -0
- package/src/oxlint.ts +37 -1
- package/src/path-digest.ts +0 -13
- package/src/project-package.ts +127 -12
- package/src/project-state.ts +3 -0
- package/src/scaffold.ts +79 -0
- package/src/sync.ts +100 -173
- package/src/vite-plus-workflow.ts +82 -0
- package/src/vite-plus.js +8 -1
- package/src/vite-plus.ts +15 -1
- package/src/worktrunk-config.ts +88 -0
- package/templates/vite-plus/github-actions-check.yml +0 -2
- package/templates/worktrunk/wt.toml +27 -0
- package/src/vite-plus-quality.ts +0 -148
- /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-client.md +0 -0
- /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-lifecycle.md +0 -0
- /package/skills/{build-effect-apis → effect-atom-state}/references/effect-atom-testing.md +0 -0
- /package/skills/{build-effect-apis → effect-atom-state}/references/tanstack-start.md +0 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Config,
|
|
3
|
+
DateTime,
|
|
4
|
+
Effect,
|
|
5
|
+
FileSystem,
|
|
6
|
+
Option,
|
|
7
|
+
Path,
|
|
8
|
+
Schema,
|
|
9
|
+
Stream,
|
|
10
|
+
type PlatformError,
|
|
11
|
+
} from "effect";
|
|
12
|
+
import { ChildProcess } from "effect/unstable/process";
|
|
13
|
+
|
|
14
|
+
import { printStatus } from "./cli-ui.ts";
|
|
15
|
+
|
|
16
|
+
export class GlobalCacheError extends Schema.TaggedError<GlobalCacheError>()("GlobalCacheError", {
|
|
17
|
+
message: Schema.String,
|
|
18
|
+
}) {}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Resolves the machine-global dev-kit cache directory. Entries stored here are
|
|
22
|
+
* keyed by immutable identifiers (resolved commit SHAs, repository URLs), so
|
|
23
|
+
* the cache is shared safely across projects and git worktrees. Populating it
|
|
24
|
+
* is not project state: locked verification and dry-run planning may write
|
|
25
|
+
* here without violating their read-only project semantics.
|
|
26
|
+
*/
|
|
27
|
+
export const resolveGlobalCacheDirectory = Effect.fn("resolveGlobalCacheDirectory")(function* () {
|
|
28
|
+
const fs = yield* FileSystem.FileSystem;
|
|
29
|
+
const path = yield* Path.Path;
|
|
30
|
+
const override = yield* Config.string("DEV_KIT_CACHE_DIR").pipe(Config.withDefault(""));
|
|
31
|
+
|
|
32
|
+
if (override.length > 0) return path.resolve(override);
|
|
33
|
+
const xdgCacheHome = yield* Config.string("XDG_CACHE_HOME").pipe(Config.withDefault(""));
|
|
34
|
+
|
|
35
|
+
if (xdgCacheHome.length > 0 && path.isAbsolute(xdgCacheHome)) {
|
|
36
|
+
return path.join(xdgCacheHome, "dev-kit");
|
|
37
|
+
}
|
|
38
|
+
const home = yield* Config.string("HOME").pipe(
|
|
39
|
+
Config.orElse(() => Config.string("USERPROFILE")),
|
|
40
|
+
Config.withDefault(""),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (home.length === 0 || !path.isAbsolute(home)) {
|
|
44
|
+
return yield* GlobalCacheError.make({
|
|
45
|
+
message: "cannot locate the dev-kit cache; set DEV_KIT_CACHE_DIR, XDG_CACHE_HOME, or HOME",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
// The user cache convention differs per platform; an existing Library/Caches
|
|
49
|
+
// identifies macOS without reaching for Node platform APIs.
|
|
50
|
+
const macCaches = path.join(home, "Library", "Caches");
|
|
51
|
+
|
|
52
|
+
return (yield* fs.exists(macCaches))
|
|
53
|
+
? path.join(macCaches, "dev-kit")
|
|
54
|
+
: path.join(home, ".cache", "dev-kit");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Publishes a fully staged cache entry at its immutable destination. The
|
|
59
|
+
* staged directory must live on the same filesystem so the rename is atomic;
|
|
60
|
+
* when a concurrent writer publishes the destination first, its entry wins and
|
|
61
|
+
* the staged copy is discarded.
|
|
62
|
+
*/
|
|
63
|
+
export const commitCacheDirectory = Effect.fn("commitCacheDirectory")(function* (
|
|
64
|
+
staged: string,
|
|
65
|
+
destination: string,
|
|
66
|
+
isPopulated: Effect.Effect<boolean, PlatformError.PlatformError>,
|
|
67
|
+
) {
|
|
68
|
+
const fs = yield* FileSystem.FileSystem;
|
|
69
|
+
|
|
70
|
+
if ((yield* fs.exists(destination)) && !(yield* isPopulated)) {
|
|
71
|
+
yield* fs.remove(destination, { force: true, recursive: true });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
yield* fs.rename(staged, destination).pipe(
|
|
75
|
+
Effect.catch((error) =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
if (!(yield* isPopulated)) return yield* error;
|
|
78
|
+
|
|
79
|
+
return yield* fs.remove(staged, { force: true, recursive: true });
|
|
80
|
+
}),
|
|
81
|
+
),
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const CACHE_PRUNE_AGE_DAYS = 30;
|
|
86
|
+
|
|
87
|
+
const DAY_MILLIS = 24 * 60 * 60 * 1000;
|
|
88
|
+
const LAST_USED_STAMP = ".last-used";
|
|
89
|
+
const TAG_USAGE_DIRECTORY = "dev-kit-tag-usage";
|
|
90
|
+
|
|
91
|
+
const runGit = Effect.fn("runGlobalCacheGit")(function* (cwd: string, args: ReadonlyArray<string>) {
|
|
92
|
+
const child = yield* ChildProcess.make("git", args, {
|
|
93
|
+
cwd,
|
|
94
|
+
stderr: "pipe",
|
|
95
|
+
stdout: "pipe",
|
|
96
|
+
});
|
|
97
|
+
const [output, exitCode] = yield* Effect.all([
|
|
98
|
+
Stream.mkString(Stream.decodeText(child.all)),
|
|
99
|
+
child.exitCode,
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
if (exitCode !== 0) {
|
|
103
|
+
return yield* GlobalCacheError.make({
|
|
104
|
+
message: `git ${args.join(" ")} failed: ${output.trim()}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return output.trim();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const readMtimeMillis = Effect.fn("readCacheMtimeMillis")(function* (target: string) {
|
|
112
|
+
const fs = yield* FileSystem.FileSystem;
|
|
113
|
+
const info = yield* fs.stat(target).pipe(Effect.catch(() => Effect.void));
|
|
114
|
+
|
|
115
|
+
if (info === undefined) return undefined;
|
|
116
|
+
|
|
117
|
+
return Option.match(info.mtime, {
|
|
118
|
+
onNone: () => undefined,
|
|
119
|
+
onSome: (mtime) => mtime.getTime(),
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const listDirectory = Effect.fn("listCacheDirectory")(function* (directory: string) {
|
|
124
|
+
const fs = yield* FileSystem.FileSystem;
|
|
125
|
+
|
|
126
|
+
return (yield* fs.exists(directory)) ? yield* fs.readDirectory(directory) : [];
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/** Best-effort recency stamp for a catalog cache entry; read by pruning. */
|
|
130
|
+
export const stampCacheEntryUsage = Effect.fn("stampCacheEntryUsage")(function* (entryDir: string) {
|
|
131
|
+
const fs = yield* FileSystem.FileSystem;
|
|
132
|
+
const path = yield* Path.Path;
|
|
133
|
+
|
|
134
|
+
yield* fs.writeFileString(path.join(entryDir, LAST_USED_STAMP), "").pipe(Effect.ignore);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Best-effort recency stamp for one tag in a shared repository. Written before
|
|
139
|
+
* the tag is fetched so pruning never mistakes an in-flight fetch for an
|
|
140
|
+
* unused tag.
|
|
141
|
+
*/
|
|
142
|
+
export const stampTagUsage = Effect.fn("stampTagUsage")(function* (
|
|
143
|
+
repositoryDir: string,
|
|
144
|
+
tag: string,
|
|
145
|
+
) {
|
|
146
|
+
const fs = yield* FileSystem.FileSystem;
|
|
147
|
+
const path = yield* Path.Path;
|
|
148
|
+
const usageDir = path.join(repositoryDir, TAG_USAGE_DIRECTORY);
|
|
149
|
+
|
|
150
|
+
yield* fs
|
|
151
|
+
.makeDirectory(usageDir, { recursive: true })
|
|
152
|
+
.pipe(
|
|
153
|
+
Effect.andThen(fs.writeFileString(path.join(usageDir, encodeURIComponent(tag)), "")),
|
|
154
|
+
Effect.ignore,
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export type GlobalCachePruneOptions = {
|
|
159
|
+
readonly all?: boolean;
|
|
160
|
+
readonly maxAgeDays?: number;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type GlobalCachePruneSummary = {
|
|
164
|
+
readonly removedEntries: number;
|
|
165
|
+
readonly removedRepositories: number;
|
|
166
|
+
readonly removedTags: number;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Evicts cache content that no project on the machine has used recently.
|
|
171
|
+
* Everything here is regenerable, so eviction is always safe: a wrongly
|
|
172
|
+
* removed entry is simply fetched again on the next apply.
|
|
173
|
+
*/
|
|
174
|
+
export const pruneGlobalCache = Effect.fn("pruneGlobalCache")(function* (
|
|
175
|
+
options: GlobalCachePruneOptions = {},
|
|
176
|
+
) {
|
|
177
|
+
const fs = yield* FileSystem.FileSystem;
|
|
178
|
+
const path = yield* Path.Path;
|
|
179
|
+
const cacheDir = yield* resolveGlobalCacheDirectory();
|
|
180
|
+
const catalogDir = path.join(cacheDir, "catalog");
|
|
181
|
+
const repositoriesDir = path.join(cacheDir, "effect-source");
|
|
182
|
+
let removedEntries = 0;
|
|
183
|
+
let removedRepositories = 0;
|
|
184
|
+
let removedTags = 0;
|
|
185
|
+
|
|
186
|
+
if (options.all === true) {
|
|
187
|
+
for (const id of yield* listDirectory(catalogDir)) {
|
|
188
|
+
removedEntries += (yield* listDirectory(path.join(catalogDir, id))).length;
|
|
189
|
+
}
|
|
190
|
+
removedRepositories = (yield* listDirectory(repositoriesDir)).length;
|
|
191
|
+
yield* fs.remove(catalogDir, { force: true, recursive: true });
|
|
192
|
+
yield* fs.remove(repositoriesDir, { force: true, recursive: true });
|
|
193
|
+
|
|
194
|
+
return { removedEntries, removedRepositories, removedTags } satisfies GlobalCachePruneSummary;
|
|
195
|
+
}
|
|
196
|
+
const now = DateTime.toEpochMillis(yield* DateTime.now);
|
|
197
|
+
const cutoff = now - (options.maxAgeDays ?? CACHE_PRUNE_AGE_DAYS) * DAY_MILLIS;
|
|
198
|
+
|
|
199
|
+
// Catalog entries and orphaned staging directories age out individually.
|
|
200
|
+
for (const id of yield* listDirectory(catalogDir)) {
|
|
201
|
+
const idDir = path.join(catalogDir, id);
|
|
202
|
+
|
|
203
|
+
for (const entry of yield* listDirectory(idDir)) {
|
|
204
|
+
const entryDir = path.join(idDir, entry);
|
|
205
|
+
const lastUsed =
|
|
206
|
+
(yield* readMtimeMillis(path.join(entryDir, LAST_USED_STAMP))) ??
|
|
207
|
+
(yield* readMtimeMillis(path.join(entryDir, ".ready"))) ??
|
|
208
|
+
(yield* readMtimeMillis(entryDir));
|
|
209
|
+
|
|
210
|
+
if (lastUsed !== undefined && lastUsed < cutoff) {
|
|
211
|
+
yield* fs.remove(entryDir, { force: true, recursive: true }).pipe(Effect.ignore);
|
|
212
|
+
removedEntries += 1;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
// Shared repositories drop tags individually; a repository that no longer
|
|
217
|
+
// holds any tag is removed whole.
|
|
218
|
+
for (const name of yield* listDirectory(repositoriesDir)) {
|
|
219
|
+
const repositoryDir = path.join(repositoriesDir, name);
|
|
220
|
+
|
|
221
|
+
if (!(yield* fs.exists(path.join(repositoryDir, "HEAD")))) {
|
|
222
|
+
const mtime = yield* readMtimeMillis(repositoryDir);
|
|
223
|
+
|
|
224
|
+
if (mtime !== undefined && mtime < cutoff) {
|
|
225
|
+
yield* fs.remove(repositoryDir, { force: true, recursive: true }).pipe(Effect.ignore);
|
|
226
|
+
removedRepositories += 1;
|
|
227
|
+
}
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const listed = yield* runGit(repositoryDir, ["tag", "--list"]).pipe(
|
|
231
|
+
Effect.catchTag("GlobalCacheError", () => Effect.void),
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
if (listed === undefined) {
|
|
235
|
+
// Unreadable repository: age it out by initialization time.
|
|
236
|
+
if (((yield* readMtimeMillis(path.join(repositoryDir, "HEAD"))) ?? 0) < cutoff) {
|
|
237
|
+
yield* fs.remove(repositoryDir, { force: true, recursive: true }).pipe(Effect.ignore);
|
|
238
|
+
removedRepositories += 1;
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const tags = listed.split("\n").filter((tag) => tag.length > 0);
|
|
243
|
+
let kept = 0;
|
|
244
|
+
let deleted = 0;
|
|
245
|
+
|
|
246
|
+
for (const tag of tags) {
|
|
247
|
+
const stamp = path.join(repositoryDir, TAG_USAGE_DIRECTORY, encodeURIComponent(tag));
|
|
248
|
+
|
|
249
|
+
if (((yield* readMtimeMillis(stamp)) ?? 0) < cutoff) {
|
|
250
|
+
yield* runGit(repositoryDir, ["update-ref", "-d", `refs/tags/${tag}`]).pipe(Effect.ignore);
|
|
251
|
+
yield* fs.remove(stamp, { force: true }).pipe(Effect.ignore);
|
|
252
|
+
removedTags += 1;
|
|
253
|
+
deleted += 1;
|
|
254
|
+
} else {
|
|
255
|
+
kept += 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (kept === 0) {
|
|
259
|
+
if (
|
|
260
|
+
deleted > 0 ||
|
|
261
|
+
((yield* readMtimeMillis(path.join(repositoryDir, "HEAD"))) ?? 0) < cutoff
|
|
262
|
+
) {
|
|
263
|
+
yield* fs.remove(repositoryDir, { force: true, recursive: true }).pipe(Effect.ignore);
|
|
264
|
+
removedRepositories += 1;
|
|
265
|
+
}
|
|
266
|
+
} else if (deleted > 0) {
|
|
267
|
+
yield* runGit(repositoryDir, ["gc", "--prune=now", "--quiet"]).pipe(Effect.ignore);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return { removedEntries, removedRepositories, removedTags } satisfies GlobalCachePruneSummary;
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Opportunistic prune for the apply lifecycle: runs the age-based sweep at
|
|
276
|
+
* most once a day so routine applies stay fast.
|
|
277
|
+
*/
|
|
278
|
+
export const maybePruneGlobalCache = Effect.fn("maybePruneGlobalCache")(function* () {
|
|
279
|
+
const fs = yield* FileSystem.FileSystem;
|
|
280
|
+
const path = yield* Path.Path;
|
|
281
|
+
const cacheDir = yield* resolveGlobalCacheDirectory();
|
|
282
|
+
const marker = path.join(cacheDir, ".last-pruned");
|
|
283
|
+
const lastPruned = yield* readMtimeMillis(marker);
|
|
284
|
+
const now = DateTime.toEpochMillis(yield* DateTime.now);
|
|
285
|
+
|
|
286
|
+
if (lastPruned !== undefined && now - lastPruned < DAY_MILLIS) return;
|
|
287
|
+
yield* fs.makeDirectory(cacheDir, { recursive: true });
|
|
288
|
+
yield* fs.writeFileString(marker, "");
|
|
289
|
+
yield* pruneGlobalCache();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
export const runCachePrune = Effect.fn("runCachePrune")(function* (
|
|
293
|
+
options: GlobalCachePruneOptions = {},
|
|
294
|
+
) {
|
|
295
|
+
const cacheDir = yield* resolveGlobalCacheDirectory();
|
|
296
|
+
const summary = yield* pruneGlobalCache(options);
|
|
297
|
+
|
|
298
|
+
yield* printStatus(
|
|
299
|
+
"success",
|
|
300
|
+
options.all === true ? "Cache cleared" : "Cache pruned",
|
|
301
|
+
`removed ${summary.removedEntries} catalog entries, ${summary.removedTags} tags, ` +
|
|
302
|
+
`${summary.removedRepositories} repositories from ${cacheDir}`,
|
|
303
|
+
);
|
|
304
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -11,14 +11,14 @@ export {
|
|
|
11
11
|
EffectTsgoSetupSchema,
|
|
12
12
|
type HarnessTarget,
|
|
13
13
|
TargetConfigSchema,
|
|
14
|
-
type VitePlusQualitySetup,
|
|
15
|
-
VitePlusQualitySetupSchema,
|
|
16
|
-
type VitePlusQualityWorkflowSetup,
|
|
17
|
-
VitePlusQualityWorkflowSetupSchema,
|
|
18
|
-
type VitePlusQualityWorkflowStep,
|
|
19
|
-
VitePlusQualityWorkflowStepSchema,
|
|
20
14
|
type VitePlusSetup,
|
|
21
15
|
VitePlusSetupSchema,
|
|
16
|
+
type VitePlusWorkflowSetup,
|
|
17
|
+
VitePlusWorkflowSetupSchema,
|
|
18
|
+
type WorktrunkConfigSetup,
|
|
19
|
+
WorktrunkConfigSetupSchema,
|
|
20
|
+
type WorktrunkSetup,
|
|
21
|
+
WorktrunkSetupSchema,
|
|
22
22
|
} from "./manifest.ts";
|
|
23
23
|
export {
|
|
24
24
|
applyEffectSourcePlan,
|
package/src/manifest.ts
CHANGED
|
@@ -53,31 +53,28 @@ export const VitePlusHooksSetupSchema = Schema.Struct({
|
|
|
53
53
|
enabled: Schema.optional(Schema.Boolean),
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
-
export const
|
|
57
|
-
name: Schema.String,
|
|
58
|
-
run: Schema.Array(Schema.String),
|
|
59
|
-
});
|
|
60
|
-
export type VitePlusQualityWorkflowStep = typeof VitePlusQualityWorkflowStepSchema.Type;
|
|
61
|
-
|
|
62
|
-
export const VitePlusQualityWorkflowSetupSchema = Schema.Struct({
|
|
56
|
+
export const VitePlusWorkflowSetupSchema = Schema.Struct({
|
|
63
57
|
enabled: Schema.optional(Schema.Boolean),
|
|
64
|
-
beforeChecks: Schema.optional(Schema.Array(VitePlusQualityWorkflowStepSchema)),
|
|
65
|
-
typecheck: Schema.optional(Schema.Array(Schema.String)),
|
|
66
|
-
});
|
|
67
|
-
export type VitePlusQualityWorkflowSetup = typeof VitePlusQualityWorkflowSetupSchema.Type;
|
|
68
|
-
|
|
69
|
-
export const VitePlusQualitySetupSchema = Schema.Struct({
|
|
70
|
-
workflow: Schema.optional(VitePlusQualityWorkflowSetupSchema),
|
|
71
58
|
});
|
|
72
|
-
export type
|
|
59
|
+
export type VitePlusWorkflowSetup = typeof VitePlusWorkflowSetupSchema.Type;
|
|
73
60
|
|
|
74
61
|
export const VitePlusSetupSchema = Schema.Struct({
|
|
75
62
|
hooks: Schema.optional(VitePlusHooksSetupSchema),
|
|
76
|
-
|
|
63
|
+
workflow: Schema.optional(VitePlusWorkflowSetupSchema),
|
|
77
64
|
});
|
|
78
65
|
|
|
79
66
|
export type VitePlusSetup = typeof VitePlusSetupSchema.Type;
|
|
80
67
|
|
|
68
|
+
export const WorktrunkConfigSetupSchema = Schema.Struct({
|
|
69
|
+
enabled: Schema.optional(Schema.Boolean),
|
|
70
|
+
});
|
|
71
|
+
export type WorktrunkConfigSetup = typeof WorktrunkConfigSetupSchema.Type;
|
|
72
|
+
|
|
73
|
+
export const WorktrunkSetupSchema = Schema.Struct({
|
|
74
|
+
config: Schema.optional(WorktrunkConfigSetupSchema),
|
|
75
|
+
});
|
|
76
|
+
export type WorktrunkSetup = typeof WorktrunkSetupSchema.Type;
|
|
77
|
+
|
|
81
78
|
export const DevKitManifestSchema = Schema.Struct({
|
|
82
79
|
$schema: Schema.optional(Schema.String),
|
|
83
80
|
include: Schema.Array(Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN))),
|
|
@@ -91,6 +88,7 @@ export const DevKitManifestSchema = Schema.Struct({
|
|
|
91
88
|
effectSource: Schema.optional(EffectSourceSetupSchema),
|
|
92
89
|
effectTsgo: Schema.optional(EffectTsgoSetupSchema),
|
|
93
90
|
vitePlus: Schema.optional(VitePlusSetupSchema),
|
|
91
|
+
worktrunk: Schema.optional(WorktrunkSetupSchema),
|
|
94
92
|
}),
|
|
95
93
|
),
|
|
96
94
|
targets: Schema.optional(
|
|
@@ -135,12 +133,13 @@ export type NormalizedManifest = {
|
|
|
135
133
|
readonly hooks: {
|
|
136
134
|
readonly enabled: boolean;
|
|
137
135
|
};
|
|
138
|
-
readonly
|
|
139
|
-
readonly
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
136
|
+
readonly workflow: {
|
|
137
|
+
readonly enabled: boolean;
|
|
138
|
+
};
|
|
139
|
+
};
|
|
140
|
+
readonly worktrunk: {
|
|
141
|
+
readonly config: {
|
|
142
|
+
readonly enabled: boolean;
|
|
144
143
|
};
|
|
145
144
|
};
|
|
146
145
|
};
|
|
@@ -175,7 +174,6 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
|
|
|
175
174
|
};
|
|
176
175
|
}
|
|
177
176
|
}
|
|
178
|
-
const quality = manifest.setup?.vitePlus?.quality;
|
|
179
177
|
|
|
180
178
|
return {
|
|
181
179
|
exclude: manifest.exclude ?? [],
|
|
@@ -203,12 +201,13 @@ export const normalizeManifest = (manifest: DevKitManifest): NormalizedManifest
|
|
|
203
201
|
hooks: {
|
|
204
202
|
enabled: manifest.setup?.vitePlus?.hooks?.enabled ?? false,
|
|
205
203
|
},
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
204
|
+
workflow: {
|
|
205
|
+
enabled: manifest.setup?.vitePlus?.workflow?.enabled ?? false,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
worktrunk: {
|
|
209
|
+
config: {
|
|
210
|
+
enabled: manifest.setup?.worktrunk?.config?.enabled ?? false,
|
|
212
211
|
},
|
|
213
212
|
},
|
|
214
213
|
},
|
package/src/oxlint.js
CHANGED
|
@@ -8,6 +8,29 @@ import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
|
|
|
8
8
|
|
|
9
9
|
export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.js";
|
|
10
10
|
|
|
11
|
+
export const createAbsoluteImportsOxlintOverride = (options) => {
|
|
12
|
+
if (options.files.length === 0) {
|
|
13
|
+
throw new Error("absolute imports enforcement requires at least one file glob");
|
|
14
|
+
}
|
|
15
|
+
const files = [...new Set(options.files)];
|
|
16
|
+
|
|
17
|
+
if (files.length !== options.files.length) {
|
|
18
|
+
throw new Error("absolute imports file globs must be unique");
|
|
19
|
+
}
|
|
20
|
+
for (const glob of files) {
|
|
21
|
+
if (glob.trim().length === 0) {
|
|
22
|
+
throw new Error("absolute imports file globs must not be blank");
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
files,
|
|
28
|
+
rules: {
|
|
29
|
+
"import/no-relative-parent-imports": "error",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
11
34
|
export const recommendedOxlintConfig = {
|
|
12
35
|
ignorePatterns: [...devKitToolIgnorePatterns],
|
|
13
36
|
options: {
|
package/src/oxlint.ts
CHANGED
|
@@ -1,9 +1,45 @@
|
|
|
1
|
-
import type { OxlintConfig } from "oxlint";
|
|
1
|
+
import type { OxlintConfig, OxlintOverride } from "oxlint";
|
|
2
2
|
|
|
3
3
|
import { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
|
|
4
4
|
|
|
5
5
|
export { devKitToolIgnorePatterns } from "./tool-ignore-patterns.ts";
|
|
6
6
|
|
|
7
|
+
export type AbsoluteImportsOptions = {
|
|
8
|
+
/** Globs that must use path-alias imports, e.g. `"apps/app/src/**"`. */
|
|
9
|
+
readonly files: ReadonlyArray<string>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build an Oxlint override that forbids `../` imports inside the given globs,
|
|
14
|
+
* so those files import through tsconfig path aliases such as `@/*`. Append it
|
|
15
|
+
* to a standalone Oxlint config's `overrides`, or opt in through
|
|
16
|
+
* `createRecommendedVitePlusConfig({ absoluteImports })`.
|
|
17
|
+
*/
|
|
18
|
+
export const createAbsoluteImportsOxlintOverride = (
|
|
19
|
+
options: AbsoluteImportsOptions,
|
|
20
|
+
): OxlintOverride => {
|
|
21
|
+
if (options.files.length === 0) {
|
|
22
|
+
throw new Error("absolute imports enforcement requires at least one file glob");
|
|
23
|
+
}
|
|
24
|
+
const files = [...new Set(options.files)];
|
|
25
|
+
|
|
26
|
+
if (files.length !== options.files.length) {
|
|
27
|
+
throw new Error("absolute imports file globs must be unique");
|
|
28
|
+
}
|
|
29
|
+
for (const glob of files) {
|
|
30
|
+
if (glob.trim().length === 0) {
|
|
31
|
+
throw new Error("absolute imports file globs must not be blank");
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
files,
|
|
37
|
+
rules: {
|
|
38
|
+
"import/no-relative-parent-imports": "error",
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
7
43
|
/**
|
|
8
44
|
* High-signal Oxlint defaults for TypeScript projects.
|
|
9
45
|
*
|
package/src/path-digest.ts
CHANGED
|
@@ -27,7 +27,6 @@ const textEncoder = new TextEncoder();
|
|
|
27
27
|
// Git preserves only the executable distinction for regular files. Canonicalizing
|
|
28
28
|
// the remaining bits keeps digests stable across checkout and copy umasks.
|
|
29
29
|
const canonicalFileMode = (mode: number): number => ((mode & 0o111) === 0 ? 0o644 : 0o755);
|
|
30
|
-
const rawFileMode = (mode: number): number => mode & 0o777;
|
|
31
30
|
|
|
32
31
|
const frame = (value: string | Uint8Array): Uint8Array => {
|
|
33
32
|
const bytes = typeof value === "string" ? textEncoder.encode(value) : value;
|
|
@@ -153,18 +152,6 @@ export const observePath = Effect.fn("observeManagedPath")(function* (absolutePa
|
|
|
153
152
|
);
|
|
154
153
|
});
|
|
155
154
|
|
|
156
|
-
export const observePathWithRawModes = Effect.fn("observeManagedPathWithRawModes")(function* (
|
|
157
|
-
absolutePath: string,
|
|
158
|
-
) {
|
|
159
|
-
return yield* digestFileSystemPath(absolutePath, rawFileMode).pipe(
|
|
160
|
-
Effect.mapError((cause) =>
|
|
161
|
-
Schema.is(PathInspectionError)(cause)
|
|
162
|
-
? cause
|
|
163
|
-
: PathInspectionError.make({ path: absolutePath, operation: "inspect", cause }),
|
|
164
|
-
),
|
|
165
|
-
);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
155
|
export const digestText = Effect.fn("digestText")(function* (value: string) {
|
|
169
156
|
return yield* digestFrames(["text-v1", value]);
|
|
170
157
|
});
|
package/src/project-package.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
1
|
+
import { Effect, FileSystem, Option, Path, Schema } from "effect";
|
|
2
2
|
|
|
3
3
|
export class ProjectPackageError extends Schema.TaggedError<ProjectPackageError>()(
|
|
4
4
|
"ProjectPackageError",
|
|
@@ -40,23 +40,138 @@ export const readProjectPackage = Effect.fn("readProjectPackage")(function* (pro
|
|
|
40
40
|
return manifest;
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
export const
|
|
43
|
+
export const PACKAGE_MANAGER_COMMANDS = {
|
|
44
|
+
bun: { install: "bun install", label: "Bun" },
|
|
45
|
+
npm: { install: "npm install", label: "npm" },
|
|
46
|
+
pnpm: { install: "pnpm install", label: "pnpm" },
|
|
47
|
+
yarn: { install: "yarn install", label: "Yarn" },
|
|
48
|
+
} as const;
|
|
49
|
+
|
|
50
|
+
export type PackageManagerName = keyof typeof PACKAGE_MANAGER_COMMANDS;
|
|
51
|
+
|
|
52
|
+
const packageManagerName = (declaration: string | undefined): PackageManagerName | undefined => {
|
|
53
|
+
const name = declaration?.split("@", 1)[0];
|
|
54
|
+
|
|
55
|
+
return name !== undefined && name in PACKAGE_MANAGER_COMMANDS
|
|
56
|
+
? (name as PackageManagerName)
|
|
57
|
+
: undefined;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const detectPackageManager = Effect.fn("detectPackageManager")(function* (
|
|
44
61
|
projectDir: string,
|
|
62
|
+
declaration: string | undefined,
|
|
63
|
+
) {
|
|
64
|
+
const declared = packageManagerName(declaration);
|
|
65
|
+
|
|
66
|
+
if (declared !== undefined || declaration !== undefined) return declared;
|
|
67
|
+
const fs = yield* FileSystem.FileSystem;
|
|
68
|
+
const path = yield* Path.Path;
|
|
69
|
+
const lockfiles: ReadonlyArray<readonly [PackageManagerName, ReadonlyArray<string>]> = [
|
|
70
|
+
["bun", ["bun.lock", "bun.lockb"]],
|
|
71
|
+
["npm", ["package-lock.json", "npm-shrinkwrap.json"]],
|
|
72
|
+
["pnpm", ["pnpm-lock.yaml"]],
|
|
73
|
+
["yarn", ["yarn.lock"]],
|
|
74
|
+
];
|
|
75
|
+
const detected: Array<PackageManagerName> = [];
|
|
76
|
+
|
|
77
|
+
for (const [manager, files] of lockfiles) {
|
|
78
|
+
let found = false;
|
|
79
|
+
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
if (yield* fs.exists(path.join(projectDir, file))) {
|
|
82
|
+
found = true;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (found) {
|
|
87
|
+
detected.push(manager);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return detected.length === 1 ? detected[0] : undefined;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const readOptionalProjectPackage = Effect.fn("readOptionalProjectPackage")(function* (
|
|
95
|
+
packageDir: string,
|
|
45
96
|
) {
|
|
46
|
-
|
|
97
|
+
return yield* readProjectPackage(packageDir).pipe(
|
|
47
98
|
Effect.catchTag("ProjectPackageError", (error) =>
|
|
48
99
|
error.message.startsWith("package.json not found:") ? Effect.void : Effect.fail(error),
|
|
49
100
|
),
|
|
50
101
|
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const manifestDependencyNames = (
|
|
105
|
+
manifest: (typeof ProjectPackageSchema)["Type"] | undefined | void,
|
|
106
|
+
): ReadonlyArray<string> =>
|
|
107
|
+
manifest === undefined
|
|
108
|
+
? []
|
|
109
|
+
: [
|
|
110
|
+
...Object.keys(manifest.dependencies ?? {}),
|
|
111
|
+
...Object.keys(manifest.devDependencies ?? {}),
|
|
112
|
+
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
113
|
+
...Object.keys(manifest.peerDependencies ?? {}),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
export const readDirectDependencyNames = Effect.fn("readDirectDependencyNames")(function* (
|
|
117
|
+
projectDir: string,
|
|
118
|
+
) {
|
|
119
|
+
const manifest = yield* readOptionalProjectPackage(projectDir);
|
|
120
|
+
|
|
121
|
+
return [...new Set(manifestDependencyNames(manifest))].sort();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const WorkspacePatternsSchema = Schema.Union([
|
|
125
|
+
Schema.Array(Schema.String),
|
|
126
|
+
Schema.Struct({ packages: Schema.Array(Schema.String) }),
|
|
127
|
+
]);
|
|
128
|
+
// `workspaces` is declared `Schema.Unknown` in the project manifest schema, so
|
|
129
|
+
// this is a genuinely untyped boundary.
|
|
130
|
+
const decodeWorkspacePatterns = Schema.decodeUnknownOption(WorkspacePatternsSchema);
|
|
131
|
+
|
|
132
|
+
const workspacePatterns = (workspaces: unknown): ReadonlyArray<string> => {
|
|
133
|
+
const decoded = decodeWorkspacePatterns(workspaces);
|
|
134
|
+
|
|
135
|
+
if (Option.isNone(decoded)) return [];
|
|
136
|
+
|
|
137
|
+
return "packages" in decoded.value ? decoded.value.packages : decoded.value;
|
|
138
|
+
};
|
|
51
139
|
|
|
52
|
-
|
|
140
|
+
/**
|
|
141
|
+
* Direct dependency names of the project package plus every workspace member
|
|
142
|
+
* package. Only literal workspace paths and single trailing-star globs
|
|
143
|
+
* (`apps/*`) are expanded; other patterns are skipped.
|
|
144
|
+
*/
|
|
145
|
+
export const readWorkspaceDependencyNames = Effect.fn("readWorkspaceDependencyNames")(function* (
|
|
146
|
+
projectDir: string,
|
|
147
|
+
) {
|
|
148
|
+
const fs = yield* FileSystem.FileSystem;
|
|
149
|
+
const path = yield* Path.Path;
|
|
150
|
+
const manifest = yield* readOptionalProjectPackage(projectDir);
|
|
151
|
+
const names = new Set(manifestDependencyNames(manifest));
|
|
152
|
+
|
|
153
|
+
for (const pattern of workspacePatterns(manifest?.workspaces)) {
|
|
154
|
+
if (pattern.startsWith("!")) continue;
|
|
155
|
+
const star = pattern.indexOf("*");
|
|
156
|
+
let memberDirs: ReadonlyArray<string> = [];
|
|
157
|
+
|
|
158
|
+
if (star === -1) {
|
|
159
|
+
memberDirs = [pattern];
|
|
160
|
+
} else if (pattern.endsWith("/*") && star === pattern.length - 1) {
|
|
161
|
+
const parent = path.join(projectDir, pattern.slice(0, -2));
|
|
162
|
+
|
|
163
|
+
if (yield* fs.exists(parent)) {
|
|
164
|
+
memberDirs = (yield* fs.readDirectory(parent)).map((name) =>
|
|
165
|
+
path.join(pattern.slice(0, -2), name),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
for (const memberDir of memberDirs) {
|
|
170
|
+
const member = yield* readOptionalProjectPackage(path.join(projectDir, memberDir));
|
|
171
|
+
|
|
172
|
+
for (const name of manifestDependencyNames(member)) names.add(name);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
53
175
|
|
|
54
|
-
return [
|
|
55
|
-
...new Set([
|
|
56
|
-
...Object.keys(manifest.dependencies ?? {}),
|
|
57
|
-
...Object.keys(manifest.devDependencies ?? {}),
|
|
58
|
-
...Object.keys(manifest.optionalDependencies ?? {}),
|
|
59
|
-
...Object.keys(manifest.peerDependencies ?? {}),
|
|
60
|
-
]),
|
|
61
|
-
].sort();
|
|
176
|
+
return [...names].sort();
|
|
62
177
|
});
|
package/src/project-state.ts
CHANGED
|
@@ -49,6 +49,9 @@ export const ManagedClaudeInstructionsOutputSchema = Schema.Struct({
|
|
|
49
49
|
});
|
|
50
50
|
export type ManagedClaudeInstructionsOutput = typeof ManagedClaudeInstructionsOutputSchema.Type;
|
|
51
51
|
|
|
52
|
+
// Legacy (dev-kit ≤0.14) lock entry for the previously managed check workflow.
|
|
53
|
+
// Kept only so old locks and receipts still decode; planning discards these
|
|
54
|
+
// entries, releasing the file to the repository. Never produced anymore.
|
|
52
55
|
export const ManagedGeneratedFileOutputSchema = Schema.Struct({
|
|
53
56
|
resourceId: Schema.Literal("setup:vite-plus-github-actions"),
|
|
54
57
|
path: Schema.String,
|