@danieljvdm/dev-kit 0.6.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 +123 -56
- package/dev-kit.example.jsonc +7 -3
- package/package.json +19 -16
- package/schema/dev-kit.schema.json +38 -0
- package/skill-sources.jsonc +8 -12
- package/skill-sources.lock.json +3 -9
- package/skills/dev-kit/SKILL.md +52 -17
- 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 +71 -33
- 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 +6 -0
- package/src/manifest.ts +38 -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 +189 -52
- package/src/path-digest.ts +31 -11
- package/src/project-package.ts +44 -19
- package/src/project-process-lock.ts +19 -12
- package/src/project-state.ts +11 -0
- 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 +371 -103
- package/src/vendor.ts +112 -42
- package/src/vite-plus-hooks.ts +174 -0
- package/src/vite-plus-quality.ts +49 -0
- package/templates/vite-plus/github-actions-check.yml +44 -0
- package/templates/vite-plus/vite.config.ts +22 -0
package/src/catalog-manager.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { applyEdits, modify, parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
2
1
|
import { Effect, FileSystem, Path, Schema } from "effect";
|
|
3
2
|
import { Prompt } from "effect/unstable/cli";
|
|
3
|
+
import { applyEdits, modify, parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
4
4
|
|
|
5
5
|
import { isInteractiveTerminal, printLine, printStatus } from "./cli-ui.ts";
|
|
6
6
|
import {
|
|
@@ -10,11 +10,7 @@ import {
|
|
|
10
10
|
type SkillSourcesLock,
|
|
11
11
|
type SkillSourcesManifest,
|
|
12
12
|
} from "./source-manifest.ts";
|
|
13
|
-
import {
|
|
14
|
-
inspectCatalogRepository,
|
|
15
|
-
refreshSkillCatalog,
|
|
16
|
-
type CatalogInspection,
|
|
17
|
-
} from "./vendor.ts";
|
|
13
|
+
import { inspectCatalogRepository, refreshSkillCatalog, type CatalogInspection } from "./vendor.ts";
|
|
18
14
|
|
|
19
15
|
type CatalogPaths = {
|
|
20
16
|
readonly repoDir: string;
|
|
@@ -52,6 +48,7 @@ const resolvePaths = Effect.fn("resolveCatalogManagerPaths")(function* (
|
|
|
52
48
|
) {
|
|
53
49
|
const path = yield* Path.Path;
|
|
54
50
|
const repoDir = path.resolve(options.repoDir ?? ".");
|
|
51
|
+
|
|
55
52
|
return {
|
|
56
53
|
repoDir,
|
|
57
54
|
sourcesPath: path.resolve(repoDir, options.sourcesPath ?? "skill-sources.jsonc"),
|
|
@@ -64,35 +61,38 @@ const readJsonc = Effect.fn("readCatalogManagerJsonc")(function* <A>(
|
|
|
64
61
|
schema: Schema.ConstraintDecoder<A>,
|
|
65
62
|
) {
|
|
66
63
|
const fs = yield* FileSystem.FileSystem;
|
|
64
|
+
|
|
67
65
|
if (!(yield* fs.exists(filePath))) {
|
|
68
66
|
return yield* new CatalogManagerError({ message: `file not found: ${filePath}` });
|
|
69
67
|
}
|
|
70
68
|
const raw = yield* fs.readFileString(filePath);
|
|
71
69
|
const errors: Array<ParseError> = [];
|
|
72
70
|
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
71
|
+
|
|
73
72
|
if (errors.length > 0) {
|
|
74
73
|
return yield* new CatalogManagerError({ message: `could not parse ${filePath}` });
|
|
75
74
|
}
|
|
76
75
|
const value = yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
|
|
77
76
|
Effect.mapError((error) => new CatalogManagerError({ message: error.message })),
|
|
78
77
|
);
|
|
78
|
+
|
|
79
79
|
return { raw, value };
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
-
const readState = Effect.fn("readCatalogManagerState")(function* (
|
|
83
|
-
options: CatalogCommandOptions,
|
|
84
|
-
) {
|
|
82
|
+
const readState = Effect.fn("readCatalogManagerState")(function* (options: CatalogCommandOptions) {
|
|
85
83
|
const fs = yield* FileSystem.FileSystem;
|
|
86
84
|
const paths = yield* resolvePaths(options);
|
|
87
85
|
const sources = yield* readJsonc(paths.sourcesPath, SkillSourcesManifestSchema);
|
|
88
86
|
const lock = (yield* fs.exists(paths.lockfilePath))
|
|
89
87
|
? yield* readJsonc(paths.lockfilePath, SkillSourcesLockSchema)
|
|
90
88
|
: undefined;
|
|
89
|
+
|
|
91
90
|
return { ...paths, sources, lock };
|
|
92
91
|
});
|
|
93
92
|
|
|
94
93
|
const compactDescription = (description: string): string => {
|
|
95
94
|
const first = description.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim() ?? description;
|
|
95
|
+
|
|
96
96
|
return first.length > 90 ? `${first.slice(0, 87).trimEnd()}…` : first;
|
|
97
97
|
};
|
|
98
98
|
|
|
@@ -103,6 +103,7 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
|
|
|
103
103
|
) {
|
|
104
104
|
const available = new Set(inspection.skills.map((skill) => skill.name));
|
|
105
105
|
const unknown = requested.filter((skill) => !available.has(skill));
|
|
106
|
+
|
|
106
107
|
if (unknown.length > 0) {
|
|
107
108
|
return yield* new CatalogManagerError({
|
|
108
109
|
message: `repository does not contain: ${unknown.join(", ")}`,
|
|
@@ -110,9 +111,11 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
|
|
|
110
111
|
}
|
|
111
112
|
if (all) {
|
|
112
113
|
const selected = inspection.skills.map((skill) => skill.name);
|
|
114
|
+
|
|
113
115
|
return { include: selected, selected };
|
|
114
116
|
}
|
|
115
|
-
if (requested.length > 0)
|
|
117
|
+
if (requested.length > 0)
|
|
118
|
+
return { include: [...new Set(requested)], selected: [...new Set(requested)] };
|
|
116
119
|
if (!(yield* isInteractiveTerminal)) {
|
|
117
120
|
return yield* new CatalogManagerError({
|
|
118
121
|
message: "choose skills with --skill <name>, or pass --all",
|
|
@@ -127,6 +130,7 @@ const selectSkills = Effect.fn("selectCatalogSkills")(function* (
|
|
|
127
130
|
})),
|
|
128
131
|
min: 1,
|
|
129
132
|
});
|
|
133
|
+
|
|
130
134
|
return { include: selected, selected };
|
|
131
135
|
});
|
|
132
136
|
|
|
@@ -138,6 +142,7 @@ const refreshWithRollback = Effect.fn("refreshCatalogWithRollback")(function* (
|
|
|
138
142
|
pinSourceIds: ReadonlyArray<string>,
|
|
139
143
|
) {
|
|
140
144
|
const fs = yield* FileSystem.FileSystem;
|
|
145
|
+
|
|
141
146
|
yield* refreshSkillCatalog({
|
|
142
147
|
repoDir: paths.repoDir,
|
|
143
148
|
sourcesPath: paths.sourcesPath,
|
|
@@ -153,6 +158,7 @@ const refreshWithRollback = Effect.fn("refreshCatalogWithRollback")(function* (
|
|
|
153
158
|
} else {
|
|
154
159
|
yield* fs.writeFileString(paths.lockfilePath, previousLock);
|
|
155
160
|
}
|
|
161
|
+
|
|
156
162
|
return yield* Effect.failCause(cause);
|
|
157
163
|
}),
|
|
158
164
|
),
|
|
@@ -172,11 +178,14 @@ const writeAndRefresh = Effect.fn("writeAndRefreshCatalog")(function* (
|
|
|
172
178
|
pinSourceIds: ReadonlyArray<string> = [],
|
|
173
179
|
) {
|
|
174
180
|
const fs = yield* FileSystem.FileSystem;
|
|
181
|
+
|
|
175
182
|
if (nextSources === state.sources.raw) {
|
|
176
183
|
yield* printStatus("info", "Catalog already contains that selection");
|
|
184
|
+
|
|
177
185
|
return;
|
|
178
186
|
}
|
|
179
187
|
const previousLock = state.lock?.raw;
|
|
188
|
+
|
|
180
189
|
yield* fs.writeFileString(state.sourcesPath, nextSources);
|
|
181
190
|
yield* refreshWithRollback(state, state.sources.raw, previousLock, updateSourceIds, pinSourceIds);
|
|
182
191
|
});
|
|
@@ -195,14 +204,11 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
|
|
|
195
204
|
...(options.ref ? { ref: options.ref } : {}),
|
|
196
205
|
...(options.skillsPath ? { skillsPath: options.skillsPath } : {}),
|
|
197
206
|
});
|
|
198
|
-
const selection = yield* selectSkills(
|
|
199
|
-
inspection,
|
|
200
|
-
options.skills ?? [],
|
|
201
|
-
options.all ?? false,
|
|
202
|
-
);
|
|
207
|
+
const selection = yield* selectSkills(inspection, options.skills ?? [], options.all ?? false);
|
|
203
208
|
const sources = state.sources.value.sources;
|
|
204
209
|
const byId = sources.findIndex((source) => source.id === inspection.id);
|
|
205
210
|
const byRepository = sources.findIndex((source) => source.repository === inspection.repository);
|
|
211
|
+
|
|
206
212
|
if (byId >= 0 && sources[byId]?.repository !== inspection.repository) {
|
|
207
213
|
return yield* new CatalogManagerError({
|
|
208
214
|
message: `source id ${inspection.id} is already used by ${sources[byId]?.repository}`,
|
|
@@ -215,21 +221,34 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
|
|
|
215
221
|
}
|
|
216
222
|
const existingIndex = byId >= 0 ? byId : byRepository;
|
|
217
223
|
let next = state.sources.raw;
|
|
224
|
+
|
|
218
225
|
if (existingIndex >= 0) {
|
|
219
226
|
const existing = sources[existingIndex];
|
|
227
|
+
|
|
220
228
|
if (existing === undefined) {
|
|
221
229
|
return yield* new CatalogManagerError({ message: "catalog source index is out of bounds" });
|
|
222
230
|
}
|
|
223
|
-
const approved =
|
|
231
|
+
const approved =
|
|
232
|
+
state.lock?.value.sources.find((source) => source.id === existing.id)?.skills ?? [];
|
|
224
233
|
const currentInclude = existing.include.includes("*") ? approved : existing.include;
|
|
225
234
|
const include = [...new Set([...currentInclude, ...selection.include])].sort();
|
|
226
235
|
const exclude = (existing.exclude ?? []).filter((skill) => !selection.selected.includes(skill));
|
|
227
|
-
|
|
236
|
+
|
|
237
|
+
next = applyEdits(
|
|
238
|
+
next,
|
|
239
|
+
modify(next, ["sources", existingIndex, "include"], include, { formattingOptions }),
|
|
240
|
+
);
|
|
228
241
|
if (exclude.length > 0 || existing.exclude !== undefined) {
|
|
229
|
-
next = applyEdits(
|
|
242
|
+
next = applyEdits(
|
|
243
|
+
next,
|
|
244
|
+
modify(next, ["sources", existingIndex, "exclude"], exclude, { formattingOptions }),
|
|
245
|
+
);
|
|
230
246
|
}
|
|
231
247
|
if (existing.ref === "HEAD" && inspection.ref !== "HEAD") {
|
|
232
|
-
next = applyEdits(
|
|
248
|
+
next = applyEdits(
|
|
249
|
+
next,
|
|
250
|
+
modify(next, ["sources", existingIndex, "ref"], inspection.ref, { formattingOptions }),
|
|
251
|
+
);
|
|
233
252
|
}
|
|
234
253
|
} else {
|
|
235
254
|
const source: ExternalSkillSource = {
|
|
@@ -241,16 +260,20 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
|
|
|
241
260
|
...(options.licensePath
|
|
242
261
|
? { licensePath: options.licensePath }
|
|
243
262
|
: inspection.licensePath
|
|
244
|
-
|
|
245
|
-
|
|
263
|
+
? { licensePath: inspection.licensePath }
|
|
264
|
+
: {}),
|
|
246
265
|
...(options.stripFrontmatter?.length
|
|
247
266
|
? { stripFrontmatter: [...new Set(options.stripFrontmatter)] }
|
|
248
267
|
: {}),
|
|
249
268
|
};
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
269
|
+
|
|
270
|
+
next = applyEdits(
|
|
271
|
+
next,
|
|
272
|
+
modify(next, ["sources", sources.length], source, {
|
|
273
|
+
formattingOptions,
|
|
274
|
+
isArrayInsertion: true,
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
254
277
|
}
|
|
255
278
|
if (options.dryRun) {
|
|
256
279
|
yield* printStatus(
|
|
@@ -258,6 +281,7 @@ export const addCatalogSource = Effect.fn("addCatalogSource")(function* (
|
|
|
258
281
|
existingIndex >= 0 ? "Would update catalog source" : "Would add catalog source",
|
|
259
282
|
`${inspection.id} · ${selection.selected.length} skill${selection.selected.length === 1 ? "" : "s"}`,
|
|
260
283
|
);
|
|
284
|
+
|
|
261
285
|
return;
|
|
262
286
|
}
|
|
263
287
|
yield* writeAndRefresh(state, next, [inspection.id]);
|
|
@@ -273,29 +297,47 @@ export const removeCatalogEntry = Effect.fn("removeCatalogEntry")(function* (
|
|
|
273
297
|
let next = state.sources.raw;
|
|
274
298
|
let label: string;
|
|
275
299
|
let pinSourceIds: ReadonlyArray<string> = [];
|
|
300
|
+
|
|
276
301
|
if (sourceIndex >= 0) {
|
|
277
|
-
next = applyEdits(
|
|
302
|
+
next = applyEdits(
|
|
303
|
+
next,
|
|
304
|
+
modify(next, ["sources", sourceIndex], undefined, { formattingOptions }),
|
|
305
|
+
);
|
|
278
306
|
label = `source ${name}`;
|
|
279
307
|
} else {
|
|
280
308
|
const owner = state.lock?.value.sources.find((source) => source.skills.includes(name));
|
|
281
|
-
|
|
309
|
+
|
|
310
|
+
if (!owner)
|
|
311
|
+
return yield* new CatalogManagerError({ message: `catalog entry not found: ${name}` });
|
|
282
312
|
const index = sources.findIndex((source) => source.id === owner.id);
|
|
283
313
|
const source = sources[index];
|
|
284
|
-
|
|
314
|
+
|
|
315
|
+
if (!source)
|
|
316
|
+
return yield* new CatalogManagerError({ message: `source not found: ${owner.id}` });
|
|
285
317
|
if (source.include.includes("*")) {
|
|
286
318
|
const exclude = [...new Set([...(source.exclude ?? []), name])];
|
|
287
|
-
|
|
319
|
+
|
|
320
|
+
next = applyEdits(
|
|
321
|
+
next,
|
|
322
|
+
modify(next, ["sources", index, "exclude"], exclude, { formattingOptions }),
|
|
323
|
+
);
|
|
288
324
|
} else {
|
|
289
325
|
const include = source.include.filter((skill) => skill !== name);
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
326
|
+
|
|
327
|
+
next =
|
|
328
|
+
include.length === 0
|
|
329
|
+
? applyEdits(next, modify(next, ["sources", index], undefined, { formattingOptions }))
|
|
330
|
+
: applyEdits(
|
|
331
|
+
next,
|
|
332
|
+
modify(next, ["sources", index, "include"], include, { formattingOptions }),
|
|
333
|
+
);
|
|
293
334
|
}
|
|
294
335
|
label = `skill ${name}`;
|
|
295
336
|
pinSourceIds = [owner.id];
|
|
296
337
|
}
|
|
297
338
|
if (options.dryRun) {
|
|
298
339
|
yield* printStatus("plan", "Would remove catalog entry", label);
|
|
340
|
+
|
|
299
341
|
return;
|
|
300
342
|
}
|
|
301
343
|
if (!options.yes) {
|
|
@@ -308,8 +350,10 @@ export const removeCatalogEntry = Effect.fn("removeCatalogEntry")(function* (
|
|
|
308
350
|
message: `Remove ${label} from the approved catalog?`,
|
|
309
351
|
initial: false,
|
|
310
352
|
});
|
|
353
|
+
|
|
311
354
|
if (!confirmed) {
|
|
312
355
|
yield* printStatus("info", "Cancelled");
|
|
356
|
+
|
|
313
357
|
return;
|
|
314
358
|
}
|
|
315
359
|
}
|
|
@@ -320,14 +364,19 @@ export const listCatalogSources = Effect.fn("listCatalogSources")(function* (
|
|
|
320
364
|
options: CatalogCommandOptions,
|
|
321
365
|
) {
|
|
322
366
|
const state = yield* readState(options);
|
|
367
|
+
|
|
323
368
|
if (state.sources.value.sources.length === 0) {
|
|
324
369
|
yield* printStatus("info", "Catalog has no external sources");
|
|
370
|
+
|
|
325
371
|
return;
|
|
326
372
|
}
|
|
327
373
|
for (const source of state.sources.value.sources) {
|
|
328
374
|
const locked = state.lock?.value.sources.find((candidate) => candidate.id === source.id);
|
|
375
|
+
|
|
329
376
|
yield* printLine(`${source.id} ${source.repository}`);
|
|
330
|
-
yield* printLine(
|
|
377
|
+
yield* printLine(
|
|
378
|
+
` ${locked?.skills.length ?? 0} skills · ${locked?.resolved.slice(0, 12) ?? "not refreshed"}`,
|
|
379
|
+
);
|
|
331
380
|
}
|
|
332
381
|
});
|
|
333
382
|
|
|
@@ -337,8 +386,11 @@ export const showCatalogSource = Effect.fn("showCatalogSource")(function* (
|
|
|
337
386
|
) {
|
|
338
387
|
const state = yield* readState(options);
|
|
339
388
|
const source = state.sources.value.sources.find((candidate) => candidate.id === id);
|
|
340
|
-
|
|
389
|
+
|
|
390
|
+
if (!source)
|
|
391
|
+
return yield* new CatalogManagerError({ message: `catalog source not found: ${id}` });
|
|
341
392
|
const locked = state.lock?.value.sources.find((candidate) => candidate.id === id);
|
|
393
|
+
|
|
342
394
|
yield* printLine(source.id);
|
|
343
395
|
yield* printLine(`Repository: ${source.repository}`);
|
|
344
396
|
yield* printLine(`Tracking: ${source.ref}`);
|
package/src/catalog.ts
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
2
1
|
import { Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
3
2
|
import { ChildProcess } from "effect/unstable/process";
|
|
3
|
+
import { parse as parseJsonc, type ParseError } from "jsonc-parser";
|
|
4
4
|
|
|
5
|
+
import { discoverPackageSkills, resolvePackageSkillSelector } from "./package-skill-source.ts";
|
|
5
6
|
import { observePath, type Digest } from "./path-digest.ts";
|
|
6
|
-
import {
|
|
7
|
-
discoverPackageSkills,
|
|
8
|
-
resolvePackageSkillSelector,
|
|
9
|
-
} from "./package-skill-source.ts";
|
|
10
7
|
import {
|
|
11
8
|
SkillSourcesLockSchema,
|
|
12
9
|
type LockedSkillSource,
|
|
@@ -52,10 +49,7 @@ class CatalogError extends Schema.TaggedErrorClass<CatalogError>()("CatalogError
|
|
|
52
49
|
message: Schema.String,
|
|
53
50
|
}) {}
|
|
54
51
|
|
|
55
|
-
const runGit = Effect.fn("runCatalogGit")(function* (
|
|
56
|
-
cwd: string,
|
|
57
|
-
args: ReadonlyArray<string>,
|
|
58
|
-
) {
|
|
52
|
+
const runGit = Effect.fn("runCatalogGit")(function* (cwd: string, args: ReadonlyArray<string>) {
|
|
59
53
|
const child = yield* ChildProcess.make("git", args, {
|
|
60
54
|
cwd,
|
|
61
55
|
stderr: "pipe",
|
|
@@ -65,11 +59,13 @@ const runGit = Effect.fn("runCatalogGit")(function* (
|
|
|
65
59
|
Stream.mkString(Stream.decodeText(child.all)),
|
|
66
60
|
child.exitCode,
|
|
67
61
|
]);
|
|
62
|
+
|
|
68
63
|
if (exitCode !== 0) {
|
|
69
64
|
return yield* new CatalogError({
|
|
70
65
|
message: `git ${args.join(" ")} failed: ${output.trim()}`,
|
|
71
66
|
});
|
|
72
67
|
}
|
|
68
|
+
|
|
73
69
|
return output.trim();
|
|
74
70
|
});
|
|
75
71
|
|
|
@@ -77,13 +73,16 @@ const readCatalogLock = Effect.fn("readCatalogLock")(function* (packageRoot: str
|
|
|
77
73
|
const fs = yield* FileSystem.FileSystem;
|
|
78
74
|
const path = yield* Path.Path;
|
|
79
75
|
const lockPath = path.join(packageRoot, "skill-sources.lock.json");
|
|
76
|
+
|
|
80
77
|
if (!(yield* fs.exists(lockPath))) return undefined;
|
|
81
78
|
const raw = yield* fs.readFileString(lockPath);
|
|
82
79
|
const errors: Array<ParseError> = [];
|
|
83
80
|
const value = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
81
|
+
|
|
84
82
|
if (errors.length > 0) {
|
|
85
83
|
return yield* new CatalogError({ message: `invalid skill catalog lock: ${lockPath}` });
|
|
86
84
|
}
|
|
85
|
+
|
|
87
86
|
return yield* Schema.decodeUnknownEffect(SkillSourcesLockSchema)(value).pipe(
|
|
88
87
|
Effect.mapError((error) => new CatalogError({ message: error.message })),
|
|
89
88
|
);
|
|
@@ -93,13 +92,16 @@ const readDescription = Effect.fn("readSkillDescription")(function* (skillPath:
|
|
|
93
92
|
const fs = yield* FileSystem.FileSystem;
|
|
94
93
|
const path = yield* Path.Path;
|
|
95
94
|
const text = yield* fs.readFileString(path.join(skillPath, "SKILL.md"));
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
text
|
|
98
|
+
.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
|
|
99
|
+
?.split(/\r?\n/)
|
|
100
|
+
.find((line) => line.startsWith("description:"))
|
|
101
|
+
?.slice("description:".length)
|
|
102
|
+
.trim()
|
|
103
|
+
.replace(/^(['"])(.*)\1$/, "$2") ?? ""
|
|
104
|
+
);
|
|
103
105
|
});
|
|
104
106
|
|
|
105
107
|
export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
@@ -110,10 +112,12 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
110
112
|
const path = yield* Path.Path;
|
|
111
113
|
const skillsDir = path.join(packageRoot, "skills");
|
|
112
114
|
const skills: Array<CatalogSkill> = [];
|
|
115
|
+
|
|
113
116
|
if (yield* fs.exists(skillsDir)) {
|
|
114
117
|
for (const name of (yield* fs.readDirectory(skillsDir)).sort()) {
|
|
115
118
|
const skillPath = path.join(skillsDir, name);
|
|
116
|
-
|
|
119
|
+
|
|
120
|
+
if (yield* fs.exists(path.join(skillPath, "SKILL.md"))) {
|
|
117
121
|
skills.push({
|
|
118
122
|
name,
|
|
119
123
|
selector: name,
|
|
@@ -125,6 +129,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
125
129
|
}
|
|
126
130
|
}
|
|
127
131
|
const lock = yield* readCatalogLock(packageRoot);
|
|
132
|
+
|
|
128
133
|
for (const source of lock?.sources ?? []) {
|
|
129
134
|
for (const name of source.skills) {
|
|
130
135
|
skills.push({
|
|
@@ -137,6 +142,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
137
142
|
}
|
|
138
143
|
}
|
|
139
144
|
const discovery = yield* discoverPackageSkills(projectDir);
|
|
145
|
+
|
|
140
146
|
for (const candidate of discovery.candidates) {
|
|
141
147
|
skills.push({
|
|
142
148
|
name: candidate.name,
|
|
@@ -148,19 +154,22 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
148
154
|
});
|
|
149
155
|
}
|
|
150
156
|
const duplicates = skills.filter(
|
|
151
|
-
(skill, index) =>
|
|
157
|
+
(skill, index) =>
|
|
158
|
+
skills.findIndex((candidate) => candidate.selector === skill.selector) !== index,
|
|
152
159
|
);
|
|
160
|
+
|
|
153
161
|
if (duplicates.length > 0) {
|
|
154
162
|
return yield* new CatalogError({
|
|
155
163
|
message: `duplicate catalog skill selector: ${duplicates[0]?.selector ?? "unknown"}`,
|
|
156
164
|
});
|
|
157
165
|
}
|
|
158
|
-
const externalFamilies = (lock?.sources ?? []).map(
|
|
159
|
-
[source.id, source.skills] as const
|
|
166
|
+
const externalFamilies = (lock?.sources ?? []).map(
|
|
167
|
+
(source) => [source.id, source.skills] as const,
|
|
160
168
|
);
|
|
161
169
|
const duplicateFamily = externalFamilies.find(
|
|
162
170
|
([id], index) => externalFamilies.findIndex(([candidate]) => candidate === id) !== index,
|
|
163
171
|
);
|
|
172
|
+
|
|
164
173
|
if (duplicateFamily !== undefined) {
|
|
165
174
|
return yield* new CatalogError({
|
|
166
175
|
message: `duplicate catalog family: ${duplicateFamily[0]}`,
|
|
@@ -170,6 +179,7 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
170
179
|
effect: ["effect-ts", "effect-atom-data-fetching"],
|
|
171
180
|
...Object.fromEntries(externalFamilies),
|
|
172
181
|
};
|
|
182
|
+
|
|
173
183
|
return {
|
|
174
184
|
skills: skills.sort((left, right) => left.selector.localeCompare(right.selector)),
|
|
175
185
|
families,
|
|
@@ -180,14 +190,18 @@ export const loadSkillCatalog = Effect.fn("loadSkillCatalog")(function* (
|
|
|
180
190
|
const stripFrontmatterKeys = (text: string, keys: ReadonlyArray<string>): string => {
|
|
181
191
|
if (keys.length === 0) return text;
|
|
182
192
|
const frontmatter = text.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
193
|
+
|
|
183
194
|
if (!frontmatter?.[1]) return text;
|
|
184
195
|
const stripped = new Set(keys);
|
|
185
196
|
let skipping = false;
|
|
186
197
|
const lines = frontmatter[1].split(/\r?\n/).filter((line) => {
|
|
187
198
|
const key = line.match(/^([A-Za-z0-9_-]+):/)?.[1];
|
|
199
|
+
|
|
188
200
|
if (key) skipping = stripped.has(key);
|
|
201
|
+
|
|
189
202
|
return !skipping;
|
|
190
203
|
});
|
|
204
|
+
|
|
191
205
|
return `---\n${lines.join("\n")}\n---${frontmatter[2]}${text.slice(frontmatter[0].length)}`;
|
|
192
206
|
};
|
|
193
207
|
|
|
@@ -208,6 +222,7 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
208
222
|
);
|
|
209
223
|
const checkout = path.join(root, "checkout");
|
|
210
224
|
const ready = path.join(root, ".ready");
|
|
225
|
+
|
|
211
226
|
if (!(yield* fs.exists(ready))) {
|
|
212
227
|
yield* fs.remove(root, { force: true, recursive: true });
|
|
213
228
|
yield* fs.makeDirectory(checkout, { recursive: true });
|
|
@@ -216,23 +231,33 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
216
231
|
yield* runGit(checkout, ["fetch", "--quiet", "--depth", "1", "origin", source.resolved]);
|
|
217
232
|
yield* runGit(checkout, ["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
|
|
218
233
|
const actual = yield* runGit(checkout, ["rev-parse", "HEAD"]);
|
|
234
|
+
|
|
219
235
|
if (actual !== source.resolved) {
|
|
220
|
-
return yield* new CatalogError({
|
|
236
|
+
return yield* new CatalogError({
|
|
237
|
+
message: `source ${source.id} resolved to ${actual}, expected ${source.resolved}`,
|
|
238
|
+
});
|
|
221
239
|
}
|
|
222
240
|
const symlinks = yield* runGit(checkout, ["ls-files", "--stage", "--", source.skillsPath]);
|
|
241
|
+
|
|
223
242
|
if (symlinks.split(/\r?\n/).some((line) => line.startsWith("120000 "))) {
|
|
224
|
-
return yield* new CatalogError({
|
|
243
|
+
return yield* new CatalogError({
|
|
244
|
+
message: `source ${source.id} contains symlinks; refusing to install it`,
|
|
245
|
+
});
|
|
225
246
|
}
|
|
226
247
|
for (const skill of source.skills) {
|
|
227
248
|
const from = path.join(checkout, source.skillsPath, skill);
|
|
228
249
|
const to = path.join(root, "skills", skill);
|
|
229
250
|
const observation = yield* observePath(from);
|
|
251
|
+
|
|
230
252
|
if (observation.kind !== "directory") {
|
|
231
|
-
return yield* new CatalogError({
|
|
253
|
+
return yield* new CatalogError({
|
|
254
|
+
message: `source ${source.id} is missing skill ${skill}`,
|
|
255
|
+
});
|
|
232
256
|
}
|
|
233
257
|
yield* fs.copy(from, to, { overwrite: true });
|
|
234
258
|
if (source.stripFrontmatter?.length) {
|
|
235
259
|
const document = path.join(to, "SKILL.md");
|
|
260
|
+
|
|
236
261
|
yield* fs.writeFileString(
|
|
237
262
|
document,
|
|
238
263
|
stripFrontmatterKeys(yield* fs.readFileString(document), source.stripFrontmatter),
|
|
@@ -244,21 +269,30 @@ const materializeSource = Effect.fn("materializeCatalogSource")(function* (
|
|
|
244
269
|
for (const skill of selected) {
|
|
245
270
|
const observation = yield* observePath(path.join(root, "skills", skill));
|
|
246
271
|
const approvedDigest = source.digests?.[skill];
|
|
247
|
-
|
|
248
|
-
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
approvedDigest !== undefined &&
|
|
275
|
+
(observation.kind !== "directory" || observation.digest !== approvedDigest)
|
|
276
|
+
) {
|
|
249
277
|
return yield* new CatalogError({
|
|
250
278
|
message: `cached skill ${skill} does not match the approved catalog; remove ${root} and retry`,
|
|
251
279
|
});
|
|
252
280
|
}
|
|
253
281
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
282
|
+
|
|
283
|
+
return new Map(
|
|
284
|
+
selected.map((skill) => [
|
|
285
|
+
skill,
|
|
286
|
+
{
|
|
287
|
+
path: path.join(root, "skills", skill),
|
|
288
|
+
catalog: {
|
|
289
|
+
source: source.id,
|
|
290
|
+
repository: source.repository,
|
|
291
|
+
resolved: source.resolved,
|
|
292
|
+
},
|
|
293
|
+
} satisfies ResolvedSkillSource,
|
|
294
|
+
]),
|
|
295
|
+
);
|
|
262
296
|
});
|
|
263
297
|
|
|
264
298
|
export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
|
|
@@ -270,6 +304,7 @@ export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
|
|
|
270
304
|
) {
|
|
271
305
|
const path = yield* Path.Path;
|
|
272
306
|
const sources = new Map<string, ResolvedSkillSource>();
|
|
307
|
+
|
|
273
308
|
for (const skill of catalog.skills.filter((skill) => skill.bundled)) {
|
|
274
309
|
if (selected.includes(skill.selector)) {
|
|
275
310
|
sources.set(skill.selector, { path: path.join(packageRoot, "skills", skill.name) });
|
|
@@ -277,6 +312,7 @@ export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
|
|
|
277
312
|
}
|
|
278
313
|
for (const source of catalog.lock?.sources ?? []) {
|
|
279
314
|
const wanted = source.skills.filter((skill) => selected.includes(skill));
|
|
315
|
+
|
|
280
316
|
if (wanted.length === 0) continue;
|
|
281
317
|
for (const [name, sourcePath] of yield* materializeSource(projectDir, source, wanted, cache)) {
|
|
282
318
|
sources.set(name, sourcePath);
|
|
@@ -285,6 +321,7 @@ export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
|
|
|
285
321
|
for (const selector of selected.filter((value) => value.includes("#"))) {
|
|
286
322
|
const resolved = yield* resolvePackageSkillSelector(projectDir, selector);
|
|
287
323
|
const observation = yield* observePath(resolved.path);
|
|
324
|
+
|
|
288
325
|
if (observation.kind !== "directory") {
|
|
289
326
|
return yield* new CatalogError({ message: `package skill is missing: ${selector}` });
|
|
290
327
|
}
|
|
@@ -299,5 +336,6 @@ export const resolveSkillSources = Effect.fn("resolveSkillSources")(function* (
|
|
|
299
336
|
},
|
|
300
337
|
});
|
|
301
338
|
}
|
|
339
|
+
|
|
302
340
|
return sources;
|
|
303
341
|
});
|
package/src/cli-ui.ts
CHANGED
|
@@ -26,6 +26,7 @@ const terminalCapabilities = Effect.fn("terminalCapabilities")(function* () {
|
|
|
26
26
|
const term = yield* Config.string("TERM").pipe(Config.withDefault(""), Effect.orDie);
|
|
27
27
|
const ci = yield* Config.string("CI").pipe(Config.withDefault(""), Effect.orDie);
|
|
28
28
|
const interactive = columns > 0 && term !== "dumb" && ci !== "true" && ci !== "1";
|
|
29
|
+
|
|
29
30
|
return {
|
|
30
31
|
color: interactive && Option.isNone(noColor),
|
|
31
32
|
interactive,
|
|
@@ -41,8 +42,8 @@ const formatDetail = (detail: string | undefined, color: boolean): string =>
|
|
|
41
42
|
detail === undefined || detail.length === 0
|
|
42
43
|
? ""
|
|
43
44
|
: color
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
? ` ${ANSI.dim}${detail}${ANSI.reset}`
|
|
46
|
+
: ` ${detail}`;
|
|
46
47
|
|
|
47
48
|
export const printStatus = Effect.fn("printCliStatus")(function* (
|
|
48
49
|
kind: StatusKind,
|
|
@@ -52,22 +53,23 @@ export const printStatus = Effect.fn("printCliStatus")(function* (
|
|
|
52
53
|
const capabilities = yield* terminalCapabilities();
|
|
53
54
|
const [symbol, color] = statusAppearance[kind];
|
|
54
55
|
const prefix = capabilities.color ? `${color}${symbol}${ANSI.reset}` : symbol;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
|
|
57
|
+
yield* capabilities.terminal
|
|
58
|
+
.display(`${prefix} ${label}${formatDetail(detail, capabilities.color)}\n`)
|
|
59
|
+
.pipe(Effect.orDie);
|
|
58
60
|
});
|
|
59
61
|
|
|
60
62
|
export const printDetail = Effect.fn("printCliDetail")(function* (text: string) {
|
|
61
63
|
const capabilities = yield* terminalCapabilities();
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
).pipe(Effect.orDie);
|
|
64
|
+
|
|
65
|
+
yield* capabilities.terminal
|
|
66
|
+
.display(capabilities.color ? ` ${ANSI.dim}${text}${ANSI.reset}\n` : ` ${text}\n`)
|
|
67
|
+
.pipe(Effect.orDie);
|
|
67
68
|
});
|
|
68
69
|
|
|
69
70
|
export const printLine = Effect.fn("printCliLine")(function* (text = "") {
|
|
70
71
|
const capabilities = yield* terminalCapabilities();
|
|
72
|
+
|
|
71
73
|
yield* capabilities.terminal.display(`${text}\n`).pipe(Effect.orDie);
|
|
72
74
|
});
|
|
73
75
|
|
|
@@ -77,6 +79,7 @@ export const withSpinner = <A, E, R>(
|
|
|
77
79
|
): Effect.Effect<A, E, R | Terminal.Terminal> =>
|
|
78
80
|
Effect.gen(function* () {
|
|
79
81
|
const capabilities = yield* terminalCapabilities();
|
|
82
|
+
|
|
80
83
|
if (!capabilities.interactive) return yield* effect;
|
|
81
84
|
|
|
82
85
|
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -84,15 +87,15 @@ export const withSpinner = <A, E, R>(
|
|
|
84
87
|
const animate = Effect.forever(
|
|
85
88
|
Effect.suspend(() => {
|
|
86
89
|
const frame = frames[index++ % frames.length];
|
|
87
|
-
const symbol = capabilities.color
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
).pipe(Effect.orDie, Effect.andThen(Effect.sleep("80 millis")));
|
|
90
|
+
const symbol = capabilities.color ? `${ANSI.cyan}${frame}${ANSI.reset}` : frame;
|
|
91
|
+
|
|
92
|
+
return capabilities.terminal
|
|
93
|
+
.display(`${ANSI.clearLine}${symbol} ${label}`)
|
|
94
|
+
.pipe(Effect.orDie, Effect.andThen(Effect.sleep("80 millis")));
|
|
93
95
|
}),
|
|
94
96
|
);
|
|
95
97
|
const fiber = yield* Effect.forkChild(animate);
|
|
98
|
+
|
|
96
99
|
return yield* effect.pipe(
|
|
97
100
|
Effect.ensuring(
|
|
98
101
|
Effect.sync(() => fiber.interruptUnsafe()).pipe(
|
|
@@ -106,5 +109,6 @@ export const printError = Effect.fn("printCliError")(function* (message: string)
|
|
|
106
109
|
const capabilities = yield* terminalCapabilities();
|
|
107
110
|
const [symbol, color] = statusAppearance.error;
|
|
108
111
|
const prefix = capabilities.color ? `${color}${symbol}${ANSI.reset}` : symbol;
|
|
112
|
+
|
|
109
113
|
yield* Console.error(`${prefix} ${message}`);
|
|
110
114
|
});
|