@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.
- 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 +47 -13
- 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 +417 -107
- 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/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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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(
|
|
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(
|
|
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 &&
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
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({
|
|
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 =
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
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 =
|
|
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:
|
|
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);
|