@danieljvdm/dev-kit 0.6.0 → 0.7.1

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 (49) hide show
  1. package/README.md +123 -56
  2. package/dev-kit.example.jsonc +7 -3
  3. package/package.json +19 -16
  4. package/schema/dev-kit.schema.json +38 -0
  5. package/skill-sources.jsonc +8 -12
  6. package/skill-sources.lock.json +3 -9
  7. package/skills/dev-kit/SKILL.md +52 -17
  8. package/skills/effect-ts/agents/openai.yaml +0 -1
  9. package/skills/effect-ts/references/audit-services.md +11 -11
  10. package/skills/effect-ts/references/guide-effect.md +56 -69
  11. package/skills/effect-ts/references/guide-error-handling.md +64 -73
  12. package/skills/effect-ts/references/guide-layers.md +187 -215
  13. package/skills/effect-ts/references/guide-observability.md +91 -116
  14. package/skills/effect-ts/references/guide-retries.md +32 -44
  15. package/skills/effect-ts/references/guide-schedule.md +26 -40
  16. package/skills/effect-ts/references/guide-schema.md +50 -57
  17. package/skills/effect-ts/references/guide-sql.md +47 -50
  18. package/skills/effect-ts/references/guide-testing.md +96 -98
  19. package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +7 -7
  20. package/skills/effect-ts/references/version-and-source.md +0 -1
  21. package/src/bin/dev-kit.ts +61 -28
  22. package/src/catalog-manager.ts +86 -34
  23. package/src/catalog.ts +71 -33
  24. package/src/cli-ui.ts +20 -16
  25. package/src/effect-source.ts +49 -19
  26. package/src/effect-tsgo.ts +66 -35
  27. package/src/gitignore.ts +19 -6
  28. package/src/index.ts +6 -0
  29. package/src/manifest.ts +38 -3
  30. package/src/node-symbolic-link.ts +3 -0
  31. package/src/oxlint-plugin-effect.js +3 -0
  32. package/src/oxlint-plugin-style.d.ts +8 -0
  33. package/src/oxlint-plugin-style.js +8 -0
  34. package/src/oxlint.js +14 -0
  35. package/src/oxlint.ts +14 -0
  36. package/src/package-skill-source.ts +189 -52
  37. package/src/path-digest.ts +47 -13
  38. package/src/project-package.ts +44 -19
  39. package/src/project-process-lock.ts +19 -12
  40. package/src/project-state.ts +11 -0
  41. package/src/skill-manager.ts +134 -55
  42. package/src/skill-selector.ts +8 -2
  43. package/src/source-manifest.ts +2 -6
  44. package/src/sync.ts +417 -107
  45. package/src/vendor.ts +112 -42
  46. package/src/vite-plus-hooks.ts +174 -0
  47. package/src/vite-plus-quality.ts +49 -0
  48. package/templates/vite-plus/github-actions-check.yml +44 -0
  49. package/templates/vite-plus/vite.config.ts +22 -0
package/src/vendor.ts CHANGED
@@ -1,11 +1,10 @@
1
- import { parse as parseJsonc, printParseErrorCode, 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, printParseErrorCode, type ParseError } from "jsonc-parser";
4
4
 
5
5
  import { printStatus, withSpinner } from "./cli-ui.ts";
6
6
  import { observePath, type Digest } from "./path-digest.ts";
7
7
  import { acquireProjectProcessLock } from "./project-process-lock.ts";
8
-
9
8
  import {
10
9
  SkillSourcesLockSchema,
11
10
  SkillSourcesManifestSchema,
@@ -106,38 +105,57 @@ const inferSourceId = (repository: string): string => {
106
105
  const cleaned = repository.replace(/[\\/]+$/, "").replace(/\.git$/i, "");
107
106
  const segments = cleaned.split(/[\\/:]+/).filter(Boolean);
108
107
  const tail = segments.slice(-2).join("-").toLowerCase();
108
+
109
109
  return tail.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
110
110
  };
111
111
 
112
112
  const containsControlCharacter = (value: string): boolean => {
113
113
  for (const character of value) {
114
114
  const code = character.charCodeAt(0);
115
+
115
116
  if (code <= 0x1f || code === 0x7f) return true;
116
117
  }
118
+
117
119
  return false;
118
120
  };
119
121
 
120
122
  const normalizeRepositoryLocator = (
121
123
  repository: string,
122
- ): Effect.Effect<{
123
- readonly repository: string;
124
- readonly ref?: string;
125
- readonly skillsPath?: string;
126
- }, InvalidSourceError> => {
124
+ ): Effect.Effect<
125
+ {
126
+ readonly repository: string;
127
+ readonly ref?: string;
128
+ readonly skillsPath?: string;
129
+ },
130
+ InvalidSourceError
131
+ > => {
127
132
  if (containsControlCharacter(repository)) {
128
- return Effect.fail(new InvalidSourceError({ source: repository, reason: "repository contains control characters" }));
133
+ return Effect.fail(
134
+ new InvalidSourceError({
135
+ source: repository,
136
+ reason: "repository contains control characters",
137
+ }),
138
+ );
129
139
  }
130
140
  try {
131
141
  const url = new URL(repository);
142
+
132
143
  if ((url.protocol === "http:" || url.protocol === "https:") && (url.username || url.password)) {
133
- return Effect.fail(new InvalidSourceError({ source: repository, reason: "repository URLs must not contain credentials" }));
144
+ return Effect.fail(
145
+ new InvalidSourceError({
146
+ source: repository,
147
+ reason: "repository URLs must not contain credentials",
148
+ }),
149
+ );
134
150
  }
135
151
  if (url.hostname.toLowerCase() !== "github.com") return Effect.succeed({ repository });
136
152
  const segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
137
153
  const [owner, rawName] = segments;
154
+
138
155
  if (owner === undefined || rawName === undefined) return Effect.succeed({ repository });
139
156
  const name = rawName.replace(/\.git$/i, "");
140
157
  const normalized = `https://github.com/${owner}/${name}.git`;
158
+
141
159
  if (segments[2] === "tree" && segments[3]) {
142
160
  return Effect.succeed({
143
161
  repository: normalized,
@@ -145,6 +163,7 @@ const normalizeRepositoryLocator = (
145
163
  ...(segments.length > 4 ? { skillsPath: segments.slice(4).join("/") } : {}),
146
164
  });
147
165
  }
166
+
148
167
  return Effect.succeed({ repository: normalized });
149
168
  } catch {
150
169
  return Effect.succeed({ repository });
@@ -180,6 +199,7 @@ const readJsonc = Effect.fn("readVendorJsonc")(function* <A>(
180
199
  schema: Schema.ConstraintDecoder<A>,
181
200
  ) {
182
201
  const fs = yield* FileSystem.FileSystem;
202
+
183
203
  if (!(yield* fs.exists(filePath))) {
184
204
  return yield* new SourceManifestError({ path: filePath, message: "file not found" });
185
205
  }
@@ -189,6 +209,7 @@ const readJsonc = Effect.fn("readVendorJsonc")(function* <A>(
189
209
  const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
190
210
 
191
211
  const first = errors[0];
212
+
192
213
  if (first !== undefined) {
193
214
  return yield* new SourceManifestError({
194
215
  path: filePath,
@@ -197,9 +218,7 @@ const readJsonc = Effect.fn("readVendorJsonc")(function* <A>(
197
218
  }
198
219
 
199
220
  return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
200
- Effect.mapError(
201
- (cause) => new SourceManifestError({ path: filePath, message: cause.message }),
202
- ),
221
+ Effect.mapError((cause) => new SourceManifestError({ path: filePath, message: cause.message })),
203
222
  );
204
223
  });
205
224
 
@@ -213,6 +232,7 @@ const resolveInside = (
213
232
  ) => {
214
233
  const resolved = path.resolve(root, relativePath);
215
234
  const relative = path.relative(root, resolved);
235
+
216
236
  if (
217
237
  (!allowRoot && relative.length === 0) ||
218
238
  relative.startsWith("..") ||
@@ -225,6 +245,7 @@ const resolveInside = (
225
245
  }),
226
246
  );
227
247
  }
248
+
228
249
  return Effect.succeed(resolved);
229
250
  };
230
251
 
@@ -241,12 +262,14 @@ const ensureCanonicalPathInside = Effect.fn("ensureCanonicalSourcePathInside")(f
241
262
  fs.realPath(target),
242
263
  ]);
243
264
  const relative = path.relative(canonicalRoot, canonicalTarget);
265
+
244
266
  if (relative.startsWith("..") || path.isAbsolute(relative)) {
245
267
  return yield* new InvalidSourceError({
246
268
  source: sourceId,
247
269
  reason: `${field} resolves outside the source repository`,
248
270
  });
249
271
  }
272
+
250
273
  return canonicalTarget;
251
274
  });
252
275
 
@@ -262,6 +285,7 @@ const rejectGitSymlinks = Effect.fn("rejectGitSymlinks")(function* (
262
285
  relativePath,
263
286
  ]);
264
287
  const symlink = entries.split(/\r?\n/).find((line) => line.startsWith("120000 "));
288
+
265
289
  if (symlink) {
266
290
  return yield* new InvalidSourceError({
267
291
  source: sourceId,
@@ -286,10 +310,12 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
286
310
 
287
311
  const entries = yield* fs.readDirectory(skillsDir);
288
312
  const discovered: Array<string> = [];
313
+
289
314
  for (const entry of entries) {
290
315
  const skillDir = path.join(skillsDir, entry);
291
316
  const info = yield* fs.stat(skillDir);
292
317
  const skillDocumentPath = path.join(skillDir, "SKILL.md");
318
+
293
319
  if (info.type === "Directory" && (yield* fs.exists(skillDocumentPath))) {
294
320
  const skillDocument = yield* fs.readFileString(skillDocumentPath);
295
321
  const frontmatter = skillDocument.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
@@ -299,6 +325,7 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
299
325
  ?.slice("name:".length)
300
326
  .trim()
301
327
  .replace(/^(['"])(.*)\1$/, "$2");
328
+
302
329
  if (declaredName !== entry) {
303
330
  return yield* new InvalidSourceError({
304
331
  source: source.id,
@@ -311,6 +338,7 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
311
338
  discovered.sort();
312
339
 
313
340
  const includeAll = source.include.length === 1 && source.include[0] === "*";
341
+
314
342
  if (source.include.includes("*") && !includeAll) {
315
343
  return yield* new InvalidSourceError({
316
344
  source: source.id,
@@ -321,6 +349,7 @@ const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
321
349
  const selected = (includeAll ? discovered : [...source.include]).filter(
322
350
  (skill) => !(source.exclude ?? []).includes(skill),
323
351
  );
352
+
324
353
  if (selected.length === 0) {
325
354
  return yield* new InvalidSourceError({
326
355
  source: source.id,
@@ -375,7 +404,8 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
375
404
  });
376
405
  }
377
406
  if (
378
- useLock && validateLockConfig &&
407
+ useLock &&
408
+ validateLockConfig &&
379
409
  (!lockedSource ||
380
410
  lockedSource.repository !== source.repository ||
381
411
  lockedSource.ref !== source.ref ||
@@ -394,24 +424,19 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
394
424
  }
395
425
 
396
426
  const checkoutDir = path.join(tempDir, "checkouts", source.id);
427
+
397
428
  yield* fs.makeDirectory(checkoutDir, { recursive: true });
398
429
  yield* runCommand(checkoutDir, "git", ["init", "--quiet"]);
399
430
  yield* runCommand(checkoutDir, "git", ["remote", "add", "origin", source.repository]);
400
431
  const fetchRef = useLock ? lockedSource?.resolved : source.ref;
432
+
401
433
  if (fetchRef === undefined) {
402
434
  return yield* new InvalidSourceError({
403
435
  source: source.id,
404
436
  reason: "no matching lockfile entry; run catalog refresh without --locked first",
405
437
  });
406
438
  }
407
- yield* runCommand(checkoutDir, "git", [
408
- "fetch",
409
- "--quiet",
410
- "--depth",
411
- "1",
412
- "origin",
413
- fetchRef,
414
- ]);
439
+ yield* runCommand(checkoutDir, "git", ["fetch", "--quiet", "--depth", "1", "origin", fetchRef]);
415
440
  yield* runCommand(checkoutDir, "git", ["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
416
441
  const resolved = yield* runCommand(checkoutDir, "git", ["rev-parse", "HEAD"]);
417
442
 
@@ -423,6 +448,7 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
423
448
  "skillsPath",
424
449
  true,
425
450
  );
451
+
426
452
  yield* rejectGitSymlinks(checkoutDir, source.skillsPath, source.id);
427
453
  const skillsDir = yield* ensureCanonicalPathInside(
428
454
  checkoutDir,
@@ -432,6 +458,7 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
432
458
  );
433
459
  const skills = yield* discoverSkills(skillsDir, source);
434
460
  let licenseSource: string | undefined;
461
+
435
462
  if (source.licensePath) {
436
463
  licenseSource = yield* resolveInside(
437
464
  path,
@@ -454,6 +481,7 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
454
481
  "licensePath",
455
482
  );
456
483
  const licenseInfo = yield* fs.stat(licenseSource);
484
+
457
485
  if (licenseInfo.type !== "File") {
458
486
  return yield* new InvalidSourceError({
459
487
  source: source.id,
@@ -473,9 +501,11 @@ const prepareSource = Effect.fn("prepareSkillSource")(function* (
473
501
 
474
502
  const readCurrentLock = Effect.fn("readCurrentSkillSourcesLock")(function* (lockfilePath: string) {
475
503
  const fs = yield* FileSystem.FileSystem;
504
+
476
505
  if (!(yield* fs.exists(lockfilePath))) {
477
506
  return undefined;
478
507
  }
508
+
479
509
  return yield* readJsonc(lockfilePath, SkillSourcesLockSchema);
480
510
  });
481
511
 
@@ -487,6 +517,7 @@ const validateCurrentLock = Effect.fn("validateCurrentSkillSourcesLock")(functio
487
517
  }
488
518
  const sourceIds = new Set<string>();
489
519
  const skills = new Set<string>();
520
+
490
521
  for (const source of lock.sources) {
491
522
  if (!SOURCE_ID_PATTERN.test(source.id) || RESERVED_SOURCE_IDS.has(source.id)) {
492
523
  return yield* new InvalidSourceError({
@@ -530,11 +561,13 @@ const currentLocalSkills = Effect.fn("currentLocalSkills")(function* (
530
561
  currentLock: SkillSourcesLock | undefined,
531
562
  ) {
532
563
  const fs = yield* FileSystem.FileSystem;
564
+
533
565
  if (!(yield* fs.exists(skillsDir))) {
534
566
  return [];
535
567
  }
536
568
  const managed = new Set(currentLock?.sources.flatMap((source) => source.skills) ?? []);
537
569
  const entries = yield* fs.readDirectory(skillsDir);
570
+
538
571
  return entries.filter((entry) => !managed.has(entry));
539
572
  });
540
573
 
@@ -543,12 +576,14 @@ const validateOwnership = Effect.fn("validateSkillOwnership")(function* (
543
576
  localSkills: ReadonlyArray<string>,
544
577
  ) {
545
578
  const owners = new Map<string, Array<string>>();
579
+
546
580
  for (const skill of localSkills) {
547
581
  owners.set(skill, ["local"]);
548
582
  }
549
583
  for (const preparedSource of prepared) {
550
584
  for (const skill of preparedSource.skills) {
551
585
  const existing = owners.get(skill) ?? [];
586
+
552
587
  existing.push(preparedSource.source.id);
553
588
  owners.set(skill, existing);
554
589
  }
@@ -576,14 +611,12 @@ const validateOwnership = Effect.fn("validateSkillOwnership")(function* (
576
611
  }
577
612
  });
578
613
 
579
- const stripFrontmatterKeys = (
580
- skillDocument: string,
581
- keys: ReadonlyArray<string>,
582
- ): string => {
614
+ const stripFrontmatterKeys = (skillDocument: string, keys: ReadonlyArray<string>): string => {
583
615
  if (keys.length === 0) {
584
616
  return skillDocument;
585
617
  }
586
618
  const frontmatter = skillDocument.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
619
+
587
620
  if (!frontmatter) {
588
621
  return skillDocument;
589
622
  }
@@ -592,9 +625,11 @@ const stripFrontmatterKeys = (
592
625
  const keptLines: Array<string> = [];
593
626
  let skipping = false;
594
627
  const frontmatterBody = frontmatter[1];
628
+
595
629
  if (frontmatterBody === undefined) return skillDocument;
596
630
  for (const line of frontmatterBody.split(/\r?\n/)) {
597
631
  const key = line.match(/^([A-Za-z0-9_-]+):/)?.[1];
632
+
598
633
  if (key) {
599
634
  skipping = stripped.has(key);
600
635
  }
@@ -613,6 +648,7 @@ const stageSources = Effect.fn("stageSkillSources")(function* (
613
648
  const fs = yield* FileSystem.FileSystem;
614
649
  const path = yield* Path.Path;
615
650
  const stagedSkillsDir = path.join(tempDir, "staged", "skills");
651
+
616
652
  yield* fs.makeDirectory(stagedSkillsDir, { recursive: true });
617
653
 
618
654
  for (const preparedSource of prepared) {
@@ -620,12 +656,15 @@ const stageSources = Effect.fn("stageSkillSources")(function* (
620
656
  preparedSource.checkoutDir,
621
657
  preparedSource.source.skillsPath,
622
658
  );
659
+
623
660
  for (const skill of preparedSource.skills) {
624
661
  const stagedSkillDir = path.join(stagedSkillsDir, skill);
662
+
625
663
  yield* fs.copy(path.join(sourceSkillsDir, skill), stagedSkillDir, { overwrite: true });
626
664
  if (preparedSource.source.stripFrontmatter?.length) {
627
665
  const skillDocumentPath = path.join(stagedSkillDir, "SKILL.md");
628
666
  const skillDocument = yield* fs.readFileString(skillDocumentPath);
667
+
629
668
  yield* fs.writeFileString(
630
669
  skillDocumentPath,
631
670
  stripFrontmatterKeys(skillDocument, preparedSource.source.stripFrontmatter),
@@ -644,22 +683,30 @@ const buildLock = Effect.fn("buildSkillCatalogLock")(function* (
644
683
  const fs = yield* FileSystem.FileSystem;
645
684
  const path = yield* Path.Path;
646
685
  const sources: Array<LockedSkillSource> = [];
686
+
647
687
  for (const { resolved, skills, source } of prepared) {
648
688
  const descriptions: Record<string, string> = {};
649
689
  const digests: Record<string, Digest> = {};
690
+
650
691
  for (const skill of skills) {
651
692
  const stagedSkill = path.join(stagedSkillsDir, skill);
652
693
  const document = yield* fs.readFileString(path.join(stagedSkill, "SKILL.md"));
653
- descriptions[skill] = document
654
- .match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
655
- ?.split(/\r?\n/)
656
- .find((line) => line.startsWith("description:"))
657
- ?.slice("description:".length)
658
- .trim()
659
- .replace(/^(['"])(.*)\1$/, "$2") ?? "";
694
+
695
+ descriptions[skill] =
696
+ document
697
+ .match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
698
+ ?.split(/\r?\n/)
699
+ .find((line) => line.startsWith("description:"))
700
+ ?.slice("description:".length)
701
+ .trim()
702
+ .replace(/^(['"])(.*)\1$/, "$2") ?? "";
660
703
  const observation = yield* observePath(stagedSkill);
704
+
661
705
  if (observation.kind !== "directory") {
662
- return yield* new InvalidSourceError({ source: source.id, reason: `could not digest ${skill}` });
706
+ return yield* new InvalidSourceError({
707
+ source: source.id,
708
+ reason: `could not digest ${skill}`,
709
+ });
663
710
  }
664
711
  digests[skill] = observation.digest;
665
712
  }
@@ -678,6 +725,7 @@ const buildLock = Effect.fn("buildSkillCatalogLock")(function* (
678
725
  ...(source.stripFrontmatter ? { stripFrontmatter: source.stripFrontmatter } : {}),
679
726
  });
680
727
  }
728
+
681
729
  return { version: 1, sources } satisfies SkillSourcesLock;
682
730
  });
683
731
 
@@ -690,6 +738,7 @@ export const inspectCatalogRepository = Effect.fn("inspectCatalogRepository")(fu
690
738
  const locator = yield* normalizeRepositoryLocator(options.repository);
691
739
  const repository = locator.repository;
692
740
  const id = options.id ?? inferSourceId(repository);
741
+
693
742
  if (id.length === 0) {
694
743
  return yield* new InvalidSourceError({
695
744
  source: repository,
@@ -712,6 +761,7 @@ export const inspectCatalogRepository = Effect.fn("inspectCatalogRepository")(fu
712
761
  prepareSource(tempDir, source, undefined, false),
713
762
  );
714
763
  let ref = source.ref;
764
+
715
765
  if (ref === "HEAD") {
716
766
  const symbolicHead = yield* runCommand(prepared.checkoutDir, "git", [
717
767
  "ls-remote",
@@ -719,33 +769,41 @@ export const inspectCatalogRepository = Effect.fn("inspectCatalogRepository")(fu
719
769
  "origin",
720
770
  "HEAD",
721
771
  ]);
772
+
722
773
  ref = symbolicHead.match(/^ref:\s+refs\/heads\/([^\s]+)\s+HEAD$/m)?.[1] ?? ref;
723
774
  }
724
775
  const skills: Array<{ readonly name: string; readonly description: string }> = [];
776
+
725
777
  for (const name of prepared.skills) {
726
778
  const document = yield* fs.readFileString(
727
779
  path.join(prepared.checkoutDir, source.skillsPath, name, "SKILL.md"),
728
780
  );
729
- const description = document
730
- .match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
731
- ?.split(/\r?\n/)
732
- .find((line) => line.startsWith("description:"))
733
- ?.slice("description:".length)
734
- .trim()
735
- .replace(/^(['"])(.*)\1$/, "$2") ?? "";
781
+ const description =
782
+ document
783
+ .match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
784
+ ?.split(/\r?\n/)
785
+ .find((line) => line.startsWith("description:"))
786
+ ?.slice("description:".length)
787
+ .trim()
788
+ .replace(/^(['"])(.*)\1$/, "$2") ?? "";
789
+
736
790
  skills.push({ name, description });
737
791
  }
738
792
  let licensePath: string | undefined;
793
+
739
794
  for (const candidate of ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING"]) {
740
795
  const candidatePath = path.join(prepared.checkoutDir, candidate);
796
+
741
797
  if (yield* fs.exists(candidatePath)) {
742
798
  const info = yield* fs.stat(candidatePath);
799
+
743
800
  if (info.type === "File") {
744
801
  licensePath = candidate;
745
802
  break;
746
803
  }
747
804
  }
748
805
  }
806
+
749
807
  return {
750
808
  id,
751
809
  repository,
@@ -770,17 +828,20 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
770
828
  : Effect.fail(error),
771
829
  ),
772
830
  );
831
+
773
832
  yield* acquireProjectProcessLock(repoDir);
774
833
  const sourcesPath = path.resolve(repoDir, options.sourcesPath ?? DEFAULT_SOURCES_PATH);
775
834
  const lockfilePath = path.resolve(repoDir, options.lockfilePath ?? DEFAULT_LOCKFILE_PATH);
776
835
  const manifest = yield* readJsonc(sourcesPath, SkillSourcesManifestSchema);
777
836
  const currentLock = yield* readCurrentLock(lockfilePath);
837
+
778
838
  yield* validateCurrentLock(currentLock);
779
839
  const lockedById = new Map(
780
840
  currentLock?.sources.map((source) => [source.id, source] as const) ?? [],
781
841
  );
782
842
 
783
843
  const sourceIds = new Set<string>();
844
+
784
845
  for (const source of manifest.sources) {
785
846
  if (sourceIds.has(source.id)) {
786
847
  return yield* new InvalidSourceError({
@@ -800,6 +861,7 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
800
861
  const lockedIds = new Set(currentLock.sources.map((source) => source.id));
801
862
  const missingFromManifest = currentLock.sources.find((source) => !sourceIds.has(source.id));
802
863
  const missingFromLock = manifest.sources.find((source) => !lockedIds.has(source.id));
864
+
803
865
  if (missingFromManifest || missingFromLock) {
804
866
  return yield* new SourceManifestError({
805
867
  path: lockfilePath,
@@ -818,8 +880,11 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
818
880
  manifest.sources,
819
881
  (source) => {
820
882
  const pinned = options.pinSourceIds?.includes(source.id) ?? false;
821
- const useLock = (options.locked ?? false) || pinned ||
883
+ const useLock =
884
+ (options.locked ?? false) ||
885
+ pinned ||
822
886
  (options.updateSourceIds !== undefined && !options.updateSourceIds.includes(source.id));
887
+
823
888
  return prepareSource(
824
889
  tempDir,
825
890
  source,
@@ -832,6 +897,7 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
832
897
  ),
833
898
  );
834
899
  const localSkills = yield* currentLocalSkills(path.join(repoDir, "skills"), currentLock);
900
+
835
901
  yield* validateOwnership(prepared, localSkills);
836
902
  const staged = yield* stageSources(tempDir, prepared);
837
903
  const nextLock = yield* buildLock(prepared, staged.stagedSkillsDir);
@@ -843,19 +909,23 @@ export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
843
909
  if (JSON.stringify(currentLock) !== JSON.stringify(nextLock)) {
844
910
  return yield* new SourceManifestError({
845
911
  path: lockfilePath,
846
- message: "approved catalog metadata differs from the lock; run catalog refresh and review it",
912
+ message:
913
+ "approved catalog metadata differs from the lock; run catalog refresh and review it",
847
914
  });
848
915
  }
849
916
  yield* printStatus("success", "Catalog verified", summary);
917
+
850
918
  return;
851
919
  }
852
920
 
853
921
  if (options.dryRun) {
854
922
  yield* printStatus("plan", "Would refresh catalog", summary);
923
+
855
924
  return;
856
925
  }
857
926
 
858
927
  const nextLockPath = path.join(tempDir, "next-catalog-lock.json");
928
+
859
929
  yield* fs.writeFileString(nextLockPath, `${JSON.stringify(nextLock, null, 2)}\n`);
860
930
  yield* fs.rename(nextLockPath, lockfilePath);
861
931
  yield* printStatus("success", "Catalog refreshed", summary);