@danieljvdm/dev-kit 0.18.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +148 -659
  2. package/package.json +8 -62
  3. package/skills/dev-kit/SKILL.md +42 -212
  4. package/skills/dev-kit/agents/openai.yaml +2 -2
  5. package/skills/dev-kit/references/cloudflare-worker-api.md +37 -0
  6. package/skills/dev-kit/references/default-typescript-repository.md +43 -0
  7. package/skills/dev-kit/references/legacy-eject.md +46 -0
  8. package/skills/dev-kit/references/repository-setup.md +53 -0
  9. package/skills/dev-kit/references/skills.md +35 -0
  10. package/src/bin/dev-kit.ts +142 -127
  11. package/src/eject.ts +715 -0
  12. package/src/legacy-project.ts +67 -0
  13. package/src/oxfmt.ts +1 -4
  14. package/src/oxlint.ts +5 -10
  15. package/src/path-digest.ts +60 -0
  16. package/src/project-skills.ts +722 -0
  17. package/src/tool-metadata.ts +0 -2
  18. package/src/vendor.ts +0 -5
  19. package/src/vite-plus.ts +1 -3
  20. package/dev-kit.example.jsonc +0 -22
  21. package/schema/dev-kit.schema.json +0 -218
  22. package/schema/skill-sources.schema.json +0 -83
  23. package/scripts/sync-anti-slop-runtime.mjs +0 -19
  24. package/src/index.ts +0 -125
  25. package/src/manifest.ts +0 -224
  26. package/src/oxfmt.js +0 -23
  27. package/src/oxlint-plugin-anti-slop/runtime.d.ts +0 -22
  28. package/src/oxlint-plugin-effect.d.ts +0 -19
  29. package/src/oxlint-plugin-style.d.ts +0 -8
  30. package/src/oxlint.js +0 -113
  31. package/src/project-state.ts +0 -122
  32. package/src/scaffold.ts +0 -79
  33. package/src/skill-manager.ts +0 -527
  34. package/src/sync.ts +0 -1935
  35. package/src/tool-ignore-patterns.js +0 -9
  36. package/src/vite-plus-dependency.ts +0 -69
  37. package/src/vite-plus-hooks.ts +0 -175
  38. package/src/vite-plus-workflow.ts +0 -82
  39. package/src/vite-plus.js +0 -88
  40. package/src/worktrunk-config.ts +0 -88
  41. package/templates/AGENTS.md +0 -11
  42. package/templates/vite-plus/github-actions-check.yml +0 -51
  43. package/templates/worktrunk/wt.toml +0 -27
@@ -0,0 +1,722 @@
1
+ import { Effect, FileSystem, Path, Schema, SchemaGetter, Stream } from "effect";
2
+ import { ChildProcess } from "effect/unstable/process";
3
+
4
+ import {
5
+ loadSkillCatalog,
6
+ resolveSkillSources,
7
+ type CatalogSkill,
8
+ type ResolvedSkillSource,
9
+ type SkillCatalog,
10
+ } from "./catalog.ts";
11
+ import { printDetail, printLine, printStatus, withSpinner } from "./cli-ui.ts";
12
+ import { observeSymbolicLink } from "./node-symbolic-link.ts";
13
+ import {
14
+ DigestSchema,
15
+ observeDirectoryWithoutEntry,
16
+ observePath,
17
+ type Digest,
18
+ } from "./path-digest.ts";
19
+ import { isSkillName, SKILL_SELECTOR_PATTERN } from "./skill-selector.ts";
20
+ import { DEV_KIT_VERSION } from "./tool-metadata.ts";
21
+
22
+ export const SKILL_ORIGIN_FILE = ".dev-kit-origin.json";
23
+ export const DEFAULT_SKILLS_TARGET = ".agents/skills";
24
+
25
+ const SkillOriginSourceSchema = Schema.Union([
26
+ Schema.Struct({
27
+ type: Schema.Literal("bundled"),
28
+ version: Schema.String,
29
+ }),
30
+ Schema.Struct({
31
+ type: Schema.Literal("git"),
32
+ source: Schema.String,
33
+ repository: Schema.String,
34
+ resolved: Schema.String,
35
+ }),
36
+ Schema.Struct({
37
+ type: Schema.Literal("package"),
38
+ package: Schema.String,
39
+ version: Schema.String,
40
+ skill: Schema.String,
41
+ digest: DigestSchema,
42
+ }),
43
+ ]);
44
+
45
+ export const SkillOriginSchema = Schema.Struct({
46
+ version: Schema.Literal(1),
47
+ selector: Schema.String.check(Schema.isPattern(SKILL_SELECTOR_PATTERN)),
48
+ name: Schema.String.check(Schema.isPattern(/^[a-z0-9]+(?:-[a-z0-9]+)*$/)),
49
+ baseDigest: DigestSchema,
50
+ source: SkillOriginSourceSchema,
51
+ });
52
+ export type SkillOrigin = typeof SkillOriginSchema.Type;
53
+
54
+ const decodeSkillOrigin = Schema.decodeUnknownEffect(Schema.fromJsonString(SkillOriginSchema));
55
+ const encodeSkillOrigin = Schema.encodeSync(
56
+ Schema.String.pipe(
57
+ Schema.decodeTo(Schema.toCodecJson(SkillOriginSchema), {
58
+ decode: SchemaGetter.parseJson(),
59
+ encode: SchemaGetter.stringifyJson({ space: 2 }),
60
+ }),
61
+ ),
62
+ );
63
+
64
+ export class ProjectSkillError extends Schema.TaggedError<ProjectSkillError>()(
65
+ "ProjectSkillError",
66
+ {
67
+ message: Schema.String,
68
+ },
69
+ ) {}
70
+
71
+ export type ProjectSkillOptions = {
72
+ readonly projectDir?: string;
73
+ readonly target?: string;
74
+ readonly dryRun?: boolean;
75
+ };
76
+
77
+ export type UpdateProjectSkillOptions = ProjectSkillOptions & {
78
+ readonly acceptLocal?: boolean;
79
+ };
80
+
81
+ type InstalledSkill = {
82
+ readonly name: string;
83
+ readonly path: string;
84
+ readonly origin?: SkillOrigin;
85
+ readonly originError?: string;
86
+ };
87
+
88
+ const packageRoot = Effect.fn("projectSkillsPackageRoot")(function* () {
89
+ const path = yield* Path.Path;
90
+
91
+ return path.resolve(path.dirname(yield* path.fromFileUrl(new URL(import.meta.url))), "..");
92
+ });
93
+
94
+ const resolveProjectPaths = Effect.fn("resolveProjectSkillPaths")(function* (
95
+ options: ProjectSkillOptions,
96
+ ) {
97
+ const fs = yield* FileSystem.FileSystem;
98
+ const path = yield* Path.Path;
99
+ const projectDir = yield* fs.realPath(path.resolve(options.projectDir ?? ".")).pipe(
100
+ Effect.mapError(() =>
101
+ ProjectSkillError.make({
102
+ message: `project directory not found: ${options.projectDir ?? "."}`,
103
+ }),
104
+ ),
105
+ );
106
+ const requestedTarget = options.target ?? DEFAULT_SKILLS_TARGET;
107
+
108
+ if (requestedTarget.length === 0 || path.isAbsolute(requestedTarget)) {
109
+ return yield* ProjectSkillError.make({
110
+ message: "--target must be a non-empty project-relative directory",
111
+ });
112
+ }
113
+ const target = path.resolve(projectDir, requestedTarget);
114
+ const relative = path.relative(projectDir, target);
115
+
116
+ if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
117
+ return yield* ProjectSkillError.make({ message: "--target must resolve inside the project" });
118
+ }
119
+ let ancestor = projectDir;
120
+
121
+ for (const segment of relative.split(path.sep)) {
122
+ ancestor = path.join(ancestor, segment);
123
+ if ((yield* observeSymbolicLink(ancestor)).kind === "symlink") {
124
+ return yield* ProjectSkillError.make({
125
+ message: `skills target passes through a symlink: ${path.relative(projectDir, ancestor)}`,
126
+ });
127
+ }
128
+ }
129
+
130
+ return { projectDir, target, targetRelative: relative };
131
+ });
132
+
133
+ const readOrigin = Effect.fn("readProjectSkillOrigin")(function* (skillPath: string) {
134
+ const fs = yield* FileSystem.FileSystem;
135
+ const path = yield* Path.Path;
136
+ const originPath = path.join(skillPath, SKILL_ORIGIN_FILE);
137
+
138
+ if (!(yield* fs.exists(originPath))) return undefined;
139
+
140
+ return yield* fs.readFileString(originPath).pipe(
141
+ Effect.flatMap(decodeSkillOrigin),
142
+ Effect.mapError((error) =>
143
+ ProjectSkillError.make({ message: `invalid ${originPath}: ${error.message}` }),
144
+ ),
145
+ );
146
+ });
147
+
148
+ const inspectInstalledSkills = Effect.fn("inspectInstalledProjectSkills")(function* (
149
+ target: string,
150
+ ) {
151
+ const fs = yield* FileSystem.FileSystem;
152
+ const path = yield* Path.Path;
153
+ const installed: Array<InstalledSkill> = [];
154
+
155
+ if (!(yield* fs.exists(target))) return installed;
156
+ const targetInfo = yield* fs.stat(target);
157
+
158
+ if (targetInfo.type !== "Directory") {
159
+ return yield* ProjectSkillError.make({
160
+ message: `skills target is not a directory: ${target}`,
161
+ });
162
+ }
163
+ for (const name of (yield* fs.readDirectory(target)).filter(isSkillName).sort()) {
164
+ const skillPath = path.join(target, name);
165
+ const observation = yield* observePath(skillPath);
166
+
167
+ if (observation.kind !== "directory" || !(yield* fs.exists(path.join(skillPath, "SKILL.md")))) {
168
+ continue;
169
+ }
170
+ const originResult = yield* Effect.result(readOrigin(skillPath));
171
+
172
+ if (originResult._tag === "Success") {
173
+ const skill: InstalledSkill = { name, path: skillPath };
174
+
175
+ if (originResult.success !== undefined)
176
+ Object.assign(skill, { origin: originResult.success });
177
+ installed.push(skill);
178
+ } else {
179
+ installed.push({ name, path: skillPath, originError: originResult.failure.message });
180
+ }
181
+ }
182
+
183
+ return installed;
184
+ });
185
+
186
+ const displayValue = (value: string): string =>
187
+ [...value]
188
+ .map((character) => {
189
+ const code = character.charCodeAt(0);
190
+
191
+ return code <= 31 || (code >= 127 && code <= 159) ? " " : character;
192
+ })
193
+ .join("")
194
+ .replace(/\s+/g, " ")
195
+ .trim();
196
+
197
+ const summary = (description: string, fallback: string): string => {
198
+ const text = displayValue(description || fallback);
199
+ const firstSentence = text.match(/^.*?[.!?](?:\s|$)/)?.[0]?.trim() ?? text;
200
+
201
+ return firstSentence.length > 96 ? `${firstSentence.slice(0, 93).trimEnd()}…` : firstSentence;
202
+ };
203
+
204
+ const selectSkills = (
205
+ catalog: SkillCatalog,
206
+ names: ReadonlyArray<string>,
207
+ ): ReadonlyArray<CatalogSkill> => {
208
+ const selectors = new Set<string>();
209
+
210
+ for (const name of names) {
211
+ const family = catalog.families[name];
212
+
213
+ if (family !== undefined) {
214
+ for (const selector of family) selectors.add(selector);
215
+ } else {
216
+ selectors.add(name);
217
+ }
218
+ }
219
+ const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
220
+ const unknown = [...selectors].filter((selector) => !catalogBySelector.has(selector));
221
+
222
+ if (unknown.length > 0) {
223
+ throw new Error(
224
+ `unknown skill${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}. Try \`dev-kit skills search ${unknown[0]}\`.`,
225
+ );
226
+ }
227
+
228
+ return [...selectors].map((selector) => catalogBySelector.get(selector)!);
229
+ };
230
+
231
+ const sourceOrigin = (
232
+ skill: CatalogSkill,
233
+ source: ResolvedSkillSource,
234
+ baseDigest: Digest,
235
+ ): SkillOrigin => {
236
+ const common = {
237
+ version: 1 as const,
238
+ selector: skill.selector,
239
+ name: skill.name,
240
+ baseDigest,
241
+ };
242
+
243
+ if (source.catalog === undefined) {
244
+ return { ...common, source: { type: "bundled", version: DEV_KIT_VERSION } };
245
+ }
246
+ if ("source" in source.catalog) {
247
+ return {
248
+ ...common,
249
+ source: {
250
+ type: "git",
251
+ source: source.catalog.source,
252
+ repository: source.catalog.repository,
253
+ resolved: source.catalog.resolved,
254
+ },
255
+ };
256
+ }
257
+
258
+ return { ...common, source: { type: "package", ...source.catalog } };
259
+ };
260
+
261
+ export const renderSkillOrigin = (origin: SkillOrigin): string => `${encodeSkillOrigin(origin)}\n`;
262
+
263
+ const resolveSources = Effect.fn("resolveProjectSkillSources")(function* (
264
+ projectDir: string,
265
+ skills: ReadonlyArray<CatalogSkill>,
266
+ catalog: SkillCatalog,
267
+ ) {
268
+ const selectors = skills.map((skill) => skill.selector);
269
+
270
+ return yield* withSpinner(
271
+ "Resolving skills",
272
+ resolveSkillSources(yield* packageRoot(), projectDir, catalog, selectors, false),
273
+ );
274
+ });
275
+
276
+ export const addProjectSkills = Effect.fn("addProjectSkills")(function* (
277
+ names: ReadonlyArray<string>,
278
+ options: ProjectSkillOptions = {},
279
+ ) {
280
+ if (names.length === 0) {
281
+ return yield* ProjectSkillError.make({ message: "choose at least one skill to add" });
282
+ }
283
+ const fs = yield* FileSystem.FileSystem;
284
+ const path = yield* Path.Path;
285
+ const paths = yield* resolveProjectPaths(options);
286
+ const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
287
+ const skills = yield* Effect.try({
288
+ try: () => selectSkills(catalog, names),
289
+ catch: (error) =>
290
+ ProjectSkillError.make({ message: error instanceof Error ? error.message : String(error) }),
291
+ });
292
+ const duplicateName = skills.find(
293
+ (skill, index) => skills.findIndex((candidate) => candidate.name === skill.name) !== index,
294
+ );
295
+
296
+ if (duplicateName !== undefined) {
297
+ return yield* ProjectSkillError.make({
298
+ message: `selected skills collide at ${duplicateName.name}`,
299
+ });
300
+ }
301
+ for (const skill of skills) {
302
+ const destination = path.join(paths.target, skill.name);
303
+
304
+ if ((yield* observePath(destination)).kind !== "missing") {
305
+ return yield* ProjectSkillError.make({
306
+ message: `skill destination already exists: ${path.relative(paths.projectDir, destination)}`,
307
+ });
308
+ }
309
+ }
310
+ const sources = yield* resolveSources(paths.projectDir, skills, catalog);
311
+
312
+ for (const skill of skills) {
313
+ const source = sources.get(skill.selector);
314
+
315
+ if (source === undefined) {
316
+ return yield* ProjectSkillError.make({
317
+ message: `skill source unavailable: ${skill.selector}`,
318
+ });
319
+ }
320
+ const observation = yield* observePath(source.path);
321
+
322
+ if (observation.kind !== "directory") {
323
+ return yield* ProjectSkillError.make({
324
+ message: `skill source is not a directory: ${skill.selector}`,
325
+ });
326
+ }
327
+ const relativeDestination = path.join(paths.targetRelative, skill.name);
328
+
329
+ if (options.dryRun) {
330
+ yield* printStatus("plan", `Add ${skill.selector}`, relativeDestination);
331
+ continue;
332
+ }
333
+ const temp = yield* fs.makeTempDirectoryScoped({
334
+ directory: paths.projectDir,
335
+ prefix: ".dev-kit-skill-add-",
336
+ });
337
+ const staged = path.join(temp, skill.name);
338
+
339
+ yield* fs.copy(source.path, staged, { overwrite: true });
340
+ yield* fs.writeFileString(
341
+ path.join(staged, SKILL_ORIGIN_FILE),
342
+ renderSkillOrigin(sourceOrigin(skill, source, observation.digest)),
343
+ );
344
+ yield* fs.makeDirectory(paths.target, { recursive: true });
345
+ yield* fs.rename(staged, path.join(paths.target, skill.name));
346
+ yield* printStatus("success", `Added ${skill.selector}`, relativeDestination);
347
+ }
348
+ });
349
+
350
+ export const setupProject = Effect.fn("setupProject")(function* (
351
+ options: ProjectSkillOptions = {},
352
+ ) {
353
+ const paths = yield* resolveProjectPaths(options);
354
+ const installed = yield* inspectInstalledSkills(paths.target);
355
+ const existing = installed.find((skill) => skill.name === "dev-kit");
356
+
357
+ if (existing?.origin?.selector === "dev-kit") {
358
+ yield* printStatus("success", "Dev Kit setup skill already present", paths.targetRelative);
359
+
360
+ return;
361
+ }
362
+ yield* addProjectSkills(["dev-kit"], options);
363
+
364
+ if (!options.dryRun) {
365
+ yield* printDetail("Ask your agent: Use $dev-kit to set up this repository.");
366
+ }
367
+ });
368
+
369
+ export const listProjectSkills = Effect.fn("listProjectSkills")(function* (
370
+ options: ProjectSkillOptions & { readonly all?: boolean; readonly query?: string } = {},
371
+ ) {
372
+ const paths = yield* resolveProjectPaths(options);
373
+ const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
374
+ const installed = yield* inspectInstalledSkills(paths.target);
375
+ const installedBySelector = new Map(
376
+ installed.flatMap((skill) => (skill.origin ? [[skill.origin.selector, skill] as const] : [])),
377
+ );
378
+ const query = options.query?.toLowerCase();
379
+ const visible = catalog.skills.filter(
380
+ (skill) =>
381
+ (options.all || installedBySelector.has(skill.selector)) &&
382
+ (!query ||
383
+ `${skill.selector} ${skill.description} ${skill.source}`.toLowerCase().includes(query)),
384
+ );
385
+
386
+ for (const skill of visible) {
387
+ const marker = installedBySelector.has(skill.selector) ? "✓" : " ";
388
+ const provenance = skill.package
389
+ ? ` [installed ${displayValue(skill.package.version)}]`
390
+ : skill.bundled
391
+ ? ""
392
+ : ` [${skill.source}]`;
393
+
394
+ yield* printLine(
395
+ `${marker} ${skill.selector}${provenance} ${summary(skill.description, skill.source)}`,
396
+ );
397
+ }
398
+ const local = installed.filter(
399
+ (skill) => skill.origin === undefined && (!query || skill.name.toLowerCase().includes(query)),
400
+ );
401
+
402
+ for (const skill of local) {
403
+ yield* printLine(`• ${skill.name} [local]`);
404
+ if (skill.originError) yield* printDetail(skill.originError);
405
+ }
406
+ if (visible.length === 0 && local.length === 0) {
407
+ yield* printStatus("info", query ? "No matching skills" : "No tracked skills");
408
+ if (!query && !options.all) yield* printDetail("Browse with: dev-kit skills list --all");
409
+
410
+ return;
411
+ }
412
+ yield* printLine();
413
+ yield* printLine(`${installed.length} installed · ${catalog.skills.length} available`);
414
+ });
415
+
416
+ export const showProjectSkill = Effect.fn("showProjectSkill")(function* (
417
+ selector: string,
418
+ options: ProjectSkillOptions = {},
419
+ ) {
420
+ const paths = yield* resolveProjectPaths(options);
421
+ const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
422
+ const skill = catalog.skills.find((candidate) => candidate.selector === selector);
423
+
424
+ if (skill === undefined) {
425
+ return yield* ProjectSkillError.make({ message: `unknown skill: ${selector}` });
426
+ }
427
+ yield* printLine(skill.selector);
428
+ if (skill.description) yield* printLine(displayValue(skill.description));
429
+ yield* printLine(`Source: ${skill.bundled ? "Dev Kit" : skill.source}`);
430
+ if (skill.package) yield* printLine(`Package: ${skill.package.name}@${skill.package.version}`);
431
+ });
432
+
433
+ const resolveTrackedSkills = Effect.fn("resolveTrackedProjectSkills")(function* (
434
+ names: ReadonlyArray<string>,
435
+ options: ProjectSkillOptions,
436
+ ) {
437
+ const paths = yield* resolveProjectPaths(options);
438
+ const installed = yield* inspectInstalledSkills(paths.target);
439
+ const tracked = installed.filter(
440
+ (skill): skill is InstalledSkill & { readonly origin: SkillOrigin } =>
441
+ skill.origin !== undefined &&
442
+ (names.length === 0 || names.includes(skill.name) || names.includes(skill.origin.selector)),
443
+ );
444
+ const unknown = names.filter(
445
+ (name) => !tracked.some((skill) => skill.name === name || skill.origin.selector === name),
446
+ );
447
+
448
+ if (unknown.length > 0) {
449
+ return yield* ProjectSkillError.make({
450
+ message: `tracked skill not found: ${unknown.join(", ")}`,
451
+ });
452
+ }
453
+
454
+ return { paths, tracked };
455
+ });
456
+
457
+ const inspectTrackedSkill = Effect.fn("inspectTrackedProjectSkill")(function* (
458
+ skill: InstalledSkill & { readonly origin: SkillOrigin },
459
+ latest: ResolvedSkillSource,
460
+ ) {
461
+ const current = yield* observeDirectoryWithoutEntry(skill.path, SKILL_ORIGIN_FILE);
462
+ const upstream = yield* observePath(latest.path);
463
+
464
+ if (current.kind !== "directory" || upstream.kind !== "directory") {
465
+ return yield* ProjectSkillError.make({ message: `could not inspect skill: ${skill.name}` });
466
+ }
467
+
468
+ return {
469
+ currentDigest: current.digest,
470
+ upstreamDigest: upstream.digest,
471
+ locallyModified: current.digest !== skill.origin.baseDigest,
472
+ upstreamChanged: upstream.digest !== skill.origin.baseDigest,
473
+ };
474
+ });
475
+
476
+ export const statusProjectSkills = Effect.fn("statusProjectSkills")(function* (
477
+ options: ProjectSkillOptions = {},
478
+ ) {
479
+ const { paths, tracked } = yield* resolveTrackedSkills([], options);
480
+
481
+ if (tracked.length === 0) {
482
+ yield* printStatus("info", "No tracked skills", paths.targetRelative);
483
+
484
+ return;
485
+ }
486
+ const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
487
+ const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
488
+ const available = tracked.flatMap((installed) => {
489
+ const skill = catalogBySelector.get(installed.origin.selector);
490
+
491
+ return skill === undefined ? [] : [{ installed, skill }];
492
+ });
493
+ const sources = yield* resolveSources(
494
+ paths.projectDir,
495
+ available.map(({ skill }) => skill),
496
+ catalog,
497
+ );
498
+
499
+ for (const trackedSkill of tracked) {
500
+ const catalogSkill = catalogBySelector.get(trackedSkill.origin.selector);
501
+
502
+ if (catalogSkill === undefined) {
503
+ yield* printStatus("error", trackedSkill.name, "upstream unavailable");
504
+ continue;
505
+ }
506
+ const source = sources.get(catalogSkill.selector);
507
+
508
+ if (source === undefined) {
509
+ yield* printStatus("error", trackedSkill.name, "source unavailable");
510
+ continue;
511
+ }
512
+ const status = yield* inspectTrackedSkill(trackedSkill, source);
513
+ const detail = status.locallyModified
514
+ ? status.upstreamChanged
515
+ ? "local and upstream changes"
516
+ : "locally modified"
517
+ : status.upstreamChanged
518
+ ? "update available"
519
+ : "current";
520
+
521
+ yield* printStatus(detail === "current" ? "success" : "info", trackedSkill.name, detail);
522
+ }
523
+ });
524
+
525
+ export const updateProjectSkills = Effect.fn("updateProjectSkills")(function* (
526
+ names: ReadonlyArray<string>,
527
+ options: UpdateProjectSkillOptions = {},
528
+ ) {
529
+ if (options.acceptLocal && names.length === 0) {
530
+ return yield* ProjectSkillError.make({
531
+ message: "--accept-local requires explicit skill names",
532
+ });
533
+ }
534
+ const fs = yield* FileSystem.FileSystem;
535
+ const path = yield* Path.Path;
536
+ const { paths, tracked } = yield* resolveTrackedSkills(names, options);
537
+
538
+ if (tracked.length === 0) {
539
+ yield* printStatus("info", "No tracked skills", paths.targetRelative);
540
+
541
+ return;
542
+ }
543
+ const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
544
+ const catalogBySelector = new Map(catalog.skills.map((skill) => [skill.selector, skill]));
545
+ const available: Array<{
546
+ readonly installed: (typeof tracked)[number];
547
+ readonly skill: CatalogSkill;
548
+ }> = [];
549
+
550
+ for (const installed of tracked) {
551
+ const skill = catalogBySelector.get(installed.origin.selector);
552
+
553
+ if (skill === undefined) {
554
+ return yield* ProjectSkillError.make({
555
+ message: `upstream unavailable: ${installed.origin.selector}`,
556
+ });
557
+ }
558
+ available.push({ installed, skill });
559
+ }
560
+ const sources = yield* resolveSources(
561
+ paths.projectDir,
562
+ available.map(({ skill }) => skill),
563
+ catalog,
564
+ );
565
+ let conflicts = 0;
566
+
567
+ for (const { installed, skill } of available) {
568
+ const source = sources.get(skill.selector);
569
+
570
+ if (source === undefined) {
571
+ return yield* ProjectSkillError.make({ message: `source unavailable: ${skill.selector}` });
572
+ }
573
+ const status = yield* inspectTrackedSkill(installed, source);
574
+
575
+ if (!status.upstreamChanged) {
576
+ yield* printStatus(
577
+ "success",
578
+ installed.name,
579
+ status.locallyModified ? "locally modified" : "current",
580
+ );
581
+ continue;
582
+ }
583
+ if (status.locallyModified) {
584
+ if (options.acceptLocal) {
585
+ if (options.dryRun) {
586
+ yield* printStatus("plan", `Keep local ${installed.name}`, "accept latest upstream base");
587
+ } else {
588
+ yield* fs.writeFileString(
589
+ path.join(installed.path, SKILL_ORIGIN_FILE),
590
+ renderSkillOrigin(sourceOrigin(skill, source, status.upstreamDigest)),
591
+ );
592
+ yield* printStatus(
593
+ "success",
594
+ `Kept local ${installed.name}`,
595
+ "accepted latest upstream base",
596
+ );
597
+ }
598
+ continue;
599
+ }
600
+ conflicts += 1;
601
+ yield* printStatus("error", installed.name, "local and upstream changes");
602
+ yield* printDetail(`Inspect with: dev-kit skills diff ${installed.name}`);
603
+ continue;
604
+ }
605
+ if (options.dryRun) {
606
+ yield* printStatus("plan", `Update ${installed.name}`);
607
+ continue;
608
+ }
609
+ const temp = yield* fs.makeTempDirectoryScoped({
610
+ directory: paths.projectDir,
611
+ prefix: ".dev-kit-skill-update-",
612
+ });
613
+ const staged = path.join(temp, installed.name);
614
+ const backup = path.join(temp, `${installed.name}.previous`);
615
+
616
+ yield* fs.copy(source.path, staged, { overwrite: true });
617
+ yield* fs.writeFileString(
618
+ path.join(staged, SKILL_ORIGIN_FILE),
619
+ renderSkillOrigin(sourceOrigin(skill, source, status.upstreamDigest)),
620
+ );
621
+ yield* fs.rename(installed.path, backup);
622
+ yield* fs
623
+ .rename(staged, installed.path)
624
+ .pipe(
625
+ Effect.catch((error) =>
626
+ fs.rename(backup, installed.path).pipe(Effect.andThen(Effect.fail(error))),
627
+ ),
628
+ );
629
+ yield* printStatus("success", `Updated ${installed.name}`);
630
+ }
631
+ if (conflicts > 0) {
632
+ return yield* ProjectSkillError.make({
633
+ message: `${conflicts} modified skill${conflicts === 1 ? " requires" : "s require"} an agent-guided merge`,
634
+ });
635
+ }
636
+ });
637
+
638
+ export const diffProjectSkill = Effect.fn("diffProjectSkill")(function* (
639
+ name: string,
640
+ options: ProjectSkillOptions = {},
641
+ ) {
642
+ const fs = yield* FileSystem.FileSystem;
643
+ const path = yield* Path.Path;
644
+ const { paths, tracked } = yield* resolveTrackedSkills([name], options);
645
+ const installed = tracked[0];
646
+
647
+ if (installed === undefined) {
648
+ return yield* ProjectSkillError.make({ message: `tracked skill not found: ${name}` });
649
+ }
650
+ const catalog = yield* loadSkillCatalog(yield* packageRoot(), paths.projectDir);
651
+ const skill = catalog.skills.find(
652
+ (candidate) => candidate.selector === installed.origin.selector,
653
+ );
654
+
655
+ if (skill === undefined) {
656
+ return yield* ProjectSkillError.make({
657
+ message: `upstream unavailable: ${installed.origin.selector}`,
658
+ });
659
+ }
660
+ const sources = yield* resolveSources(paths.projectDir, [skill], catalog);
661
+ const source = sources.get(skill.selector);
662
+
663
+ if (source === undefined) {
664
+ return yield* ProjectSkillError.make({ message: `source unavailable: ${skill.selector}` });
665
+ }
666
+ const temp = yield* fs.makeTempDirectoryScoped({ prefix: "dev-kit-skill-diff-" });
667
+ const current = path.join(temp, installed.name);
668
+
669
+ yield* fs.copy(installed.path, current, { overwrite: true });
670
+ yield* fs.remove(path.join(current, SKILL_ORIGIN_FILE), { force: true });
671
+ const child = yield* ChildProcess.make(
672
+ "git",
673
+ ["diff", "--no-index", "--", current, source.path],
674
+ {
675
+ cwd: paths.projectDir,
676
+ stderr: "pipe",
677
+ stdout: "pipe",
678
+ },
679
+ );
680
+ const [output, exitCode] = yield* Effect.all([
681
+ Stream.mkString(Stream.decodeText(child.all)),
682
+ child.exitCode,
683
+ ]);
684
+
685
+ if (exitCode > 1) {
686
+ return yield* ProjectSkillError.make({ message: `git diff failed: ${output.trim()}` });
687
+ }
688
+ yield* printLine(output.trimEnd());
689
+ });
690
+
691
+ export const detachProjectSkills = Effect.fn("detachProjectSkills")(function* (
692
+ names: ReadonlyArray<string>,
693
+ options: ProjectSkillOptions = {},
694
+ ) {
695
+ if (names.length === 0) {
696
+ return yield* ProjectSkillError.make({ message: "choose at least one skill to detach" });
697
+ }
698
+ const fs = yield* FileSystem.FileSystem;
699
+ const path = yield* Path.Path;
700
+ const { tracked } = yield* resolveTrackedSkills(names, options);
701
+
702
+ for (const skill of tracked) {
703
+ const originPath = path.join(skill.path, SKILL_ORIGIN_FILE);
704
+
705
+ if (options.dryRun) yield* printStatus("plan", `Detach ${skill.name}`);
706
+ else {
707
+ yield* fs.remove(originPath);
708
+ yield* printStatus("success", `Detached ${skill.name}`);
709
+ }
710
+ }
711
+ yield* printDetail("Detached skills remain in the repository as ordinary local skills.");
712
+ });
713
+
714
+ export const showProjectSkillsDashboard = Effect.fn("showProjectSkillsDashboard")(function* () {
715
+ yield* printLine("Dev Kit copies agent guidance into a repository, then gets out of the way.");
716
+ yield* printLine();
717
+ yield* printLine("Start dev-kit setup");
718
+ yield* printLine("Browse dev-kit skills list --all");
719
+ yield* printLine("Add dev-kit skills add <skill>");
720
+ yield* printLine("Update dev-kit skills update");
721
+ yield* printLine("Migrate dev-kit eject --dry-run");
722
+ });