@danieljvdm/dev-kit 0.2.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 +290 -0
- package/bin/dev-kit.mjs +3 -0
- package/dev-kit.example.jsonc +13 -0
- package/package.json +69 -0
- package/schema/dev-kit.schema.json +128 -0
- package/schema/skill-sources.schema.json +83 -0
- package/skill-sources.jsonc +55 -0
- package/skill-sources.lock.json +136 -0
- package/skills/dev-kit/SKILL.md +145 -0
- package/skills/dev-kit/agents/openai.yaml +4 -0
- package/skills/effect-ts/SKILL.md +242 -0
- package/skills/effect-ts/UPSTREAM.md +28 -0
- package/skills/effect-ts/agents/openai.yaml +5 -0
- package/skills/effect-ts/references/audit-services.md +144 -0
- package/skills/effect-ts/references/features.md +525 -0
- package/skills/effect-ts/references/guide-cli.md +106 -0
- package/skills/effect-ts/references/guide-effect.md +453 -0
- package/skills/effect-ts/references/guide-error-handling.md +574 -0
- package/skills/effect-ts/references/guide-http-boundaries.md +55 -0
- package/skills/effect-ts/references/guide-layers.md +1017 -0
- package/skills/effect-ts/references/guide-observability.md +771 -0
- package/skills/effect-ts/references/guide-retries.md +446 -0
- package/skills/effect-ts/references/guide-schedule.md +357 -0
- package/skills/effect-ts/references/guide-schema.md +671 -0
- package/skills/effect-ts/references/guide-sql.md +539 -0
- package/skills/effect-ts/references/guide-testing.md +534 -0
- package/skills/effect-ts/references/guide-type-safety-and-boundaries.md +131 -0
- package/skills/effect-ts/references/version-and-source.md +87 -0
- package/src/bin/dev-kit.ts +372 -0
- package/src/catalog-manager.ts +345 -0
- package/src/catalog.ts +246 -0
- package/src/cli-ui.ts +110 -0
- package/src/effect-source.ts +325 -0
- package/src/effect-tsgo.ts +256 -0
- package/src/gitignore.ts +212 -0
- package/src/index.ts +98 -0
- package/src/manifest.ts +133 -0
- package/src/node-symbolic-link.ts +31 -0
- package/src/path-digest.ts +140 -0
- package/src/project-process-lock.ts +76 -0
- package/src/project-state.ts +67 -0
- package/src/skill-manager.ts +326 -0
- package/src/source-manifest.ts +51 -0
- package/src/sync.ts +900 -0
- package/src/tool-metadata.ts +3 -0
- package/src/typescript-package-name.ts +5 -0
- package/src/vendor.ts +848 -0
package/src/vendor.ts
ADDED
|
@@ -0,0 +1,848 @@
|
|
|
1
|
+
import { parse as parseJsonc, printParseErrorCode, type ParseError } from "jsonc-parser";
|
|
2
|
+
import { Effect, FileSystem, Path, Schema, Stream } from "effect";
|
|
3
|
+
import { ChildProcess } from "effect/unstable/process";
|
|
4
|
+
|
|
5
|
+
import { printStatus, withSpinner } from "./cli-ui.ts";
|
|
6
|
+
import { observePath, type Digest } from "./path-digest.ts";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
SkillSourcesLockSchema,
|
|
10
|
+
SkillSourcesManifestSchema,
|
|
11
|
+
type ExternalSkillSource,
|
|
12
|
+
type LockedSkillSource,
|
|
13
|
+
type SkillSourcesLock,
|
|
14
|
+
} from "./source-manifest.ts";
|
|
15
|
+
|
|
16
|
+
export type CatalogRefreshOptions = {
|
|
17
|
+
readonly repoDir?: string;
|
|
18
|
+
readonly sourcesPath?: string;
|
|
19
|
+
readonly lockfilePath?: string;
|
|
20
|
+
readonly dryRun?: boolean;
|
|
21
|
+
readonly locked?: boolean;
|
|
22
|
+
readonly updateSourceIds?: ReadonlyArray<string>;
|
|
23
|
+
readonly pinSourceIds?: ReadonlyArray<string>;
|
|
24
|
+
};
|
|
25
|
+
/** @deprecated Use CatalogRefreshOptions. */
|
|
26
|
+
export type VendorOptions = CatalogRefreshOptions;
|
|
27
|
+
|
|
28
|
+
export type CatalogInspection = {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly repository: string;
|
|
31
|
+
readonly ref: string;
|
|
32
|
+
readonly resolved: string;
|
|
33
|
+
readonly skillsPath: string;
|
|
34
|
+
readonly skills: ReadonlyArray<{ readonly name: string; readonly description: string }>;
|
|
35
|
+
readonly licensePath?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type CatalogInspectOptions = {
|
|
39
|
+
readonly repository: string;
|
|
40
|
+
readonly id?: string;
|
|
41
|
+
readonly ref?: string;
|
|
42
|
+
readonly skillsPath?: string;
|
|
43
|
+
readonly repoDir?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type PreparedSource = {
|
|
47
|
+
readonly source: ExternalSkillSource;
|
|
48
|
+
readonly resolved: string;
|
|
49
|
+
readonly skills: ReadonlyArray<string>;
|
|
50
|
+
readonly checkoutDir: string;
|
|
51
|
+
readonly licenseSource?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
class SourceManifestError extends Schema.TaggedErrorClass<SourceManifestError>()(
|
|
55
|
+
"SourceManifestError",
|
|
56
|
+
{
|
|
57
|
+
path: Schema.String,
|
|
58
|
+
message: Schema.String,
|
|
59
|
+
},
|
|
60
|
+
) {}
|
|
61
|
+
|
|
62
|
+
class InvalidSourceError extends Schema.TaggedErrorClass<InvalidSourceError>()(
|
|
63
|
+
"InvalidSourceError",
|
|
64
|
+
{
|
|
65
|
+
source: Schema.String,
|
|
66
|
+
reason: Schema.String,
|
|
67
|
+
},
|
|
68
|
+
) {
|
|
69
|
+
override get message(): string {
|
|
70
|
+
return `invalid source "${this.source}": ${this.reason}`;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class SkillCollisionError extends Schema.TaggedErrorClass<SkillCollisionError>()(
|
|
75
|
+
"SkillCollisionError",
|
|
76
|
+
{
|
|
77
|
+
skill: Schema.String,
|
|
78
|
+
owners: Schema.Array(Schema.String),
|
|
79
|
+
},
|
|
80
|
+
) {
|
|
81
|
+
override get message() {
|
|
82
|
+
return `skill "${this.skill}" is owned by more than one source: ${this.owners.join(", ")}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class CommandError extends Schema.TaggedErrorClass<CommandError>()("CommandError", {
|
|
87
|
+
command: Schema.String,
|
|
88
|
+
exitCode: Schema.Int,
|
|
89
|
+
output: Schema.String,
|
|
90
|
+
}) {
|
|
91
|
+
override get message() {
|
|
92
|
+
return this.output.length > 0
|
|
93
|
+
? `${this.command} exited with code ${this.exitCode}: ${this.output}`
|
|
94
|
+
: `${this.command} exited with code ${this.exitCode}`;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DEFAULT_SOURCES_PATH = "skill-sources.jsonc";
|
|
99
|
+
const DEFAULT_LOCKFILE_PATH = "skill-sources.lock.json";
|
|
100
|
+
const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
101
|
+
const SOURCE_ID_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
102
|
+
const RESERVED_SOURCE_IDS = new Set(["effect"]);
|
|
103
|
+
|
|
104
|
+
const inferSourceId = (repository: string): string => {
|
|
105
|
+
const cleaned = repository.replace(/[\\/]+$/, "").replace(/\.git$/i, "");
|
|
106
|
+
const segments = cleaned.split(/[\\/:]+/).filter(Boolean);
|
|
107
|
+
const tail = segments.slice(-2).join("-").toLowerCase();
|
|
108
|
+
return tail.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const normalizeRepositoryLocator = (
|
|
112
|
+
repository: string,
|
|
113
|
+
): Effect.Effect<{
|
|
114
|
+
readonly repository: string;
|
|
115
|
+
readonly ref?: string;
|
|
116
|
+
readonly skillsPath?: string;
|
|
117
|
+
}, InvalidSourceError> => {
|
|
118
|
+
if (/[\u0000-\u001f\u007f]/.test(repository)) {
|
|
119
|
+
return Effect.fail(new InvalidSourceError({ source: repository, reason: "repository contains control characters" }));
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const url = new URL(repository);
|
|
123
|
+
if ((url.protocol === "http:" || url.protocol === "https:") && (url.username || url.password)) {
|
|
124
|
+
return Effect.fail(new InvalidSourceError({ source: repository, reason: "repository URLs must not contain credentials" }));
|
|
125
|
+
}
|
|
126
|
+
if (url.hostname.toLowerCase() !== "github.com") return Effect.succeed({ repository });
|
|
127
|
+
const segments = url.pathname.split("/").filter(Boolean).map(decodeURIComponent);
|
|
128
|
+
if (segments.length < 2) return Effect.succeed({ repository });
|
|
129
|
+
const owner = segments[0]!;
|
|
130
|
+
const name = segments[1]!.replace(/\.git$/i, "");
|
|
131
|
+
const normalized = `https://github.com/${owner}/${name}.git`;
|
|
132
|
+
if (segments[2] === "tree" && segments[3]) {
|
|
133
|
+
return Effect.succeed({
|
|
134
|
+
repository: normalized,
|
|
135
|
+
ref: segments[3],
|
|
136
|
+
...(segments.length > 4 ? { skillsPath: segments.slice(4).join("/") } : {}),
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return Effect.succeed({ repository: normalized });
|
|
140
|
+
} catch {
|
|
141
|
+
return Effect.succeed({ repository });
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const runCommand = Effect.fn("runVendorCommand")(function* (
|
|
146
|
+
cwd: string,
|
|
147
|
+
command: string,
|
|
148
|
+
args: ReadonlyArray<string>,
|
|
149
|
+
) {
|
|
150
|
+
const formatted = [command, ...args].join(" ");
|
|
151
|
+
const child = yield* ChildProcess.make(command, args, { cwd, stderr: "pipe", stdout: "pipe" });
|
|
152
|
+
const [output, exitCode] = yield* Effect.all([
|
|
153
|
+
Stream.mkString(Stream.decodeText(child.all)),
|
|
154
|
+
child.exitCode,
|
|
155
|
+
]);
|
|
156
|
+
const trimmed = output.trim();
|
|
157
|
+
|
|
158
|
+
if (exitCode !== 0) {
|
|
159
|
+
return yield* new CommandError({ command: formatted, exitCode, output: trimmed });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return trimmed;
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const resolveGitRoot = Effect.fn("resolveVendorGitRoot")(function* (cwd: string) {
|
|
166
|
+
return yield* runCommand(cwd, "git", ["rev-parse", "--show-toplevel"]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
const readJsonc = Effect.fn("readVendorJsonc")(function* <A>(
|
|
170
|
+
filePath: string,
|
|
171
|
+
schema: Schema.ConstraintDecoder<A>,
|
|
172
|
+
) {
|
|
173
|
+
const fs = yield* FileSystem.FileSystem;
|
|
174
|
+
if (!(yield* fs.exists(filePath))) {
|
|
175
|
+
return yield* new SourceManifestError({ path: filePath, message: "file not found" });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const raw = yield* fs.readFileString(filePath);
|
|
179
|
+
const errors: Array<ParseError> = [];
|
|
180
|
+
const parsed = parseJsonc(raw, errors, { allowTrailingComma: true });
|
|
181
|
+
|
|
182
|
+
const first = errors[0];
|
|
183
|
+
if (first !== undefined) {
|
|
184
|
+
return yield* new SourceManifestError({
|
|
185
|
+
path: filePath,
|
|
186
|
+
message: `${printParseErrorCode(first.error)} at offset ${first.offset}`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return yield* Schema.decodeUnknownEffect(schema)(parsed).pipe(
|
|
191
|
+
Effect.mapError(
|
|
192
|
+
(cause) => new SourceManifestError({ path: filePath, message: cause.message }),
|
|
193
|
+
),
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const resolveInside = (
|
|
198
|
+
path: Path.Path,
|
|
199
|
+
root: string,
|
|
200
|
+
relativePath: string,
|
|
201
|
+
sourceId: string,
|
|
202
|
+
field: string,
|
|
203
|
+
allowRoot = false,
|
|
204
|
+
) => {
|
|
205
|
+
const resolved = path.resolve(root, relativePath);
|
|
206
|
+
const relative = path.relative(root, resolved);
|
|
207
|
+
if (
|
|
208
|
+
(!allowRoot && relative.length === 0) ||
|
|
209
|
+
relative.startsWith("..") ||
|
|
210
|
+
path.isAbsolute(relative)
|
|
211
|
+
) {
|
|
212
|
+
return Effect.fail(
|
|
213
|
+
new InvalidSourceError({
|
|
214
|
+
source: sourceId,
|
|
215
|
+
reason: `${field} must be a relative path inside the source repository`,
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return Effect.succeed(resolved);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const ensureCanonicalPathInside = Effect.fn("ensureCanonicalSourcePathInside")(function* (
|
|
223
|
+
root: string,
|
|
224
|
+
target: string,
|
|
225
|
+
sourceId: string,
|
|
226
|
+
field: string,
|
|
227
|
+
) {
|
|
228
|
+
const fs = yield* FileSystem.FileSystem;
|
|
229
|
+
const path = yield* Path.Path;
|
|
230
|
+
const [canonicalRoot, canonicalTarget] = yield* Effect.all([
|
|
231
|
+
fs.realPath(root),
|
|
232
|
+
fs.realPath(target),
|
|
233
|
+
]);
|
|
234
|
+
const relative = path.relative(canonicalRoot, canonicalTarget);
|
|
235
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
236
|
+
return yield* new InvalidSourceError({
|
|
237
|
+
source: sourceId,
|
|
238
|
+
reason: `${field} resolves outside the source repository`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return canonicalTarget;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const rejectGitSymlinks = Effect.fn("rejectGitSymlinks")(function* (
|
|
245
|
+
checkoutDir: string,
|
|
246
|
+
relativePath: string,
|
|
247
|
+
sourceId: string,
|
|
248
|
+
) {
|
|
249
|
+
const entries = yield* runCommand(checkoutDir, "git", [
|
|
250
|
+
"ls-files",
|
|
251
|
+
"--stage",
|
|
252
|
+
"--",
|
|
253
|
+
relativePath,
|
|
254
|
+
]);
|
|
255
|
+
const symlink = entries.split(/\r?\n/).find((line) => line.startsWith("120000 "));
|
|
256
|
+
if (symlink) {
|
|
257
|
+
return yield* new InvalidSourceError({
|
|
258
|
+
source: sourceId,
|
|
259
|
+
reason: `symlinks are not allowed in catalog paths: ${symlink.slice(symlink.indexOf("\t") + 1)}`,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const discoverSkills = Effect.fn("discoverVendoredSkills")(function* (
|
|
265
|
+
skillsDir: string,
|
|
266
|
+
source: ExternalSkillSource,
|
|
267
|
+
) {
|
|
268
|
+
const fs = yield* FileSystem.FileSystem;
|
|
269
|
+
const path = yield* Path.Path;
|
|
270
|
+
|
|
271
|
+
if (!(yield* fs.exists(skillsDir))) {
|
|
272
|
+
return yield* new InvalidSourceError({
|
|
273
|
+
source: source.id,
|
|
274
|
+
reason: `skillsPath does not exist: ${source.skillsPath}`,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const entries = yield* fs.readDirectory(skillsDir);
|
|
279
|
+
const discovered: Array<string> = [];
|
|
280
|
+
for (const entry of entries) {
|
|
281
|
+
const skillDir = path.join(skillsDir, entry);
|
|
282
|
+
const info = yield* fs.stat(skillDir);
|
|
283
|
+
const skillDocumentPath = path.join(skillDir, "SKILL.md");
|
|
284
|
+
if (info.type === "Directory" && (yield* fs.exists(skillDocumentPath))) {
|
|
285
|
+
const skillDocument = yield* fs.readFileString(skillDocumentPath);
|
|
286
|
+
const frontmatter = skillDocument.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
287
|
+
const declaredName = frontmatter?.[1]
|
|
288
|
+
?.split(/\r?\n/)
|
|
289
|
+
.find((line) => line.startsWith("name:"))
|
|
290
|
+
?.slice("name:".length)
|
|
291
|
+
.trim()
|
|
292
|
+
.replace(/^(['"])(.*)\1$/, "$2");
|
|
293
|
+
if (declaredName !== entry) {
|
|
294
|
+
return yield* new InvalidSourceError({
|
|
295
|
+
source: source.id,
|
|
296
|
+
reason: `${source.skillsPath}/${entry}/SKILL.md must declare name: ${entry}`,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
discovered.push(entry);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
discovered.sort();
|
|
303
|
+
|
|
304
|
+
const includeAll = source.include.length === 1 && source.include[0] === "*";
|
|
305
|
+
if (source.include.includes("*") && !includeAll) {
|
|
306
|
+
return yield* new InvalidSourceError({
|
|
307
|
+
source: source.id,
|
|
308
|
+
reason: 'include must contain either "*" or explicit skill names, not both',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const selected = (includeAll ? discovered : [...source.include]).filter(
|
|
313
|
+
(skill) => !(source.exclude ?? []).includes(skill),
|
|
314
|
+
);
|
|
315
|
+
if (selected.length === 0) {
|
|
316
|
+
return yield* new InvalidSourceError({
|
|
317
|
+
source: source.id,
|
|
318
|
+
reason: "include must select at least one skill",
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (const skill of selected) {
|
|
323
|
+
if (!SKILL_NAME_PATTERN.test(skill)) {
|
|
324
|
+
return yield* new InvalidSourceError({
|
|
325
|
+
source: source.id,
|
|
326
|
+
reason: `invalid skill name "${skill}"`,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
if (!discovered.includes(skill)) {
|
|
330
|
+
return yield* new InvalidSourceError({
|
|
331
|
+
source: source.id,
|
|
332
|
+
reason: `skill not found under ${source.skillsPath}: ${skill}`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return [...new Set(selected)].sort();
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const prepareSource = Effect.fn("prepareSkillSource")(function* (
|
|
341
|
+
tempDir: string,
|
|
342
|
+
source: ExternalSkillSource,
|
|
343
|
+
lockedSource: LockedSkillSource | undefined,
|
|
344
|
+
useLock: boolean,
|
|
345
|
+
validateLockConfig = useLock,
|
|
346
|
+
) {
|
|
347
|
+
const fs = yield* FileSystem.FileSystem;
|
|
348
|
+
const path = yield* Path.Path;
|
|
349
|
+
|
|
350
|
+
if (!SOURCE_ID_PATTERN.test(source.id)) {
|
|
351
|
+
return yield* new InvalidSourceError({
|
|
352
|
+
source: source.id,
|
|
353
|
+
reason: "id must use lowercase letters, numbers, and hyphens",
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
if (RESERVED_SOURCE_IDS.has(source.id)) {
|
|
357
|
+
return yield* new InvalidSourceError({
|
|
358
|
+
source: source.id,
|
|
359
|
+
reason: "id conflicts with a built-in skill family",
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
if (source.repository.length === 0 || source.ref.length === 0) {
|
|
363
|
+
return yield* new InvalidSourceError({
|
|
364
|
+
source: source.id,
|
|
365
|
+
reason: "repository and ref must not be empty",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (
|
|
369
|
+
useLock && validateLockConfig &&
|
|
370
|
+
(!lockedSource ||
|
|
371
|
+
lockedSource.repository !== source.repository ||
|
|
372
|
+
lockedSource.ref !== source.ref ||
|
|
373
|
+
lockedSource.skillsPath !== source.skillsPath ||
|
|
374
|
+
[...lockedSource.include].sort().join("\0") !== [...source.include].sort().join("\0") ||
|
|
375
|
+
[...(lockedSource.exclude ?? [])].sort().join("\0") !==
|
|
376
|
+
[...(source.exclude ?? [])].sort().join("\0") ||
|
|
377
|
+
lockedSource.licensePath !== source.licensePath ||
|
|
378
|
+
[...(lockedSource.stripFrontmatter ?? [])].sort().join("\0") !==
|
|
379
|
+
[...(source.stripFrontmatter ?? [])].sort().join("\0"))
|
|
380
|
+
) {
|
|
381
|
+
return yield* new InvalidSourceError({
|
|
382
|
+
source: source.id,
|
|
383
|
+
reason: "no matching lockfile entry; run catalog refresh without --locked first",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const checkoutDir = path.join(tempDir, "checkouts", source.id);
|
|
388
|
+
yield* fs.makeDirectory(checkoutDir, { recursive: true });
|
|
389
|
+
yield* runCommand(checkoutDir, "git", ["init", "--quiet"]);
|
|
390
|
+
yield* runCommand(checkoutDir, "git", ["remote", "add", "origin", source.repository]);
|
|
391
|
+
yield* runCommand(checkoutDir, "git", [
|
|
392
|
+
"fetch",
|
|
393
|
+
"--quiet",
|
|
394
|
+
"--depth",
|
|
395
|
+
"1",
|
|
396
|
+
"origin",
|
|
397
|
+
useLock ? lockedSource!.resolved : source.ref,
|
|
398
|
+
]);
|
|
399
|
+
yield* runCommand(checkoutDir, "git", ["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
|
|
400
|
+
const resolved = yield* runCommand(checkoutDir, "git", ["rev-parse", "HEAD"]);
|
|
401
|
+
|
|
402
|
+
const unresolvedSkillsDir = yield* resolveInside(
|
|
403
|
+
path,
|
|
404
|
+
checkoutDir,
|
|
405
|
+
source.skillsPath,
|
|
406
|
+
source.id,
|
|
407
|
+
"skillsPath",
|
|
408
|
+
true,
|
|
409
|
+
);
|
|
410
|
+
yield* rejectGitSymlinks(checkoutDir, source.skillsPath, source.id);
|
|
411
|
+
const skillsDir = yield* ensureCanonicalPathInside(
|
|
412
|
+
checkoutDir,
|
|
413
|
+
unresolvedSkillsDir,
|
|
414
|
+
source.id,
|
|
415
|
+
"skillsPath",
|
|
416
|
+
);
|
|
417
|
+
const skills = yield* discoverSkills(skillsDir, source);
|
|
418
|
+
let licenseSource: string | undefined;
|
|
419
|
+
if (source.licensePath) {
|
|
420
|
+
licenseSource = yield* resolveInside(
|
|
421
|
+
path,
|
|
422
|
+
checkoutDir,
|
|
423
|
+
source.licensePath,
|
|
424
|
+
source.id,
|
|
425
|
+
"licensePath",
|
|
426
|
+
);
|
|
427
|
+
yield* rejectGitSymlinks(checkoutDir, source.licensePath, source.id);
|
|
428
|
+
if (!(yield* fs.exists(licenseSource))) {
|
|
429
|
+
return yield* new InvalidSourceError({
|
|
430
|
+
source: source.id,
|
|
431
|
+
reason: `licensePath does not exist: ${source.licensePath}`,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
licenseSource = yield* ensureCanonicalPathInside(
|
|
435
|
+
checkoutDir,
|
|
436
|
+
licenseSource,
|
|
437
|
+
source.id,
|
|
438
|
+
"licensePath",
|
|
439
|
+
);
|
|
440
|
+
const licenseInfo = yield* fs.stat(licenseSource);
|
|
441
|
+
if (licenseInfo.type !== "File") {
|
|
442
|
+
return yield* new InvalidSourceError({
|
|
443
|
+
source: source.id,
|
|
444
|
+
reason: `licensePath must be a file: ${source.licensePath}`,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
checkoutDir,
|
|
451
|
+
resolved,
|
|
452
|
+
skills,
|
|
453
|
+
source,
|
|
454
|
+
...(licenseSource ? { licenseSource } : {}),
|
|
455
|
+
} satisfies PreparedSource;
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
const readCurrentLock = Effect.fn("readCurrentSkillSourcesLock")(function* (lockfilePath: string) {
|
|
459
|
+
const fs = yield* FileSystem.FileSystem;
|
|
460
|
+
if (!(yield* fs.exists(lockfilePath))) {
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
return yield* readJsonc(lockfilePath, SkillSourcesLockSchema);
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
const validateCurrentLock = Effect.fn("validateCurrentSkillSourcesLock")(function* (
|
|
467
|
+
lock: SkillSourcesLock | undefined,
|
|
468
|
+
) {
|
|
469
|
+
if (!lock) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
const sourceIds = new Set<string>();
|
|
473
|
+
const skills = new Set<string>();
|
|
474
|
+
for (const source of lock.sources) {
|
|
475
|
+
if (!SOURCE_ID_PATTERN.test(source.id) || RESERVED_SOURCE_IDS.has(source.id)) {
|
|
476
|
+
return yield* new InvalidSourceError({
|
|
477
|
+
source: source.id,
|
|
478
|
+
reason: "lockfile contains an invalid source id",
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
if (sourceIds.has(source.id)) {
|
|
482
|
+
return yield* new InvalidSourceError({
|
|
483
|
+
source: source.id,
|
|
484
|
+
reason: "lockfile source ids must be unique",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
sourceIds.add(source.id);
|
|
488
|
+
if (!/^[0-9a-f]{40,64}$/.test(source.resolved)) {
|
|
489
|
+
return yield* new InvalidSourceError({
|
|
490
|
+
source: source.id,
|
|
491
|
+
reason: "lockfile resolved commit must be a full hexadecimal object id",
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
for (const skill of source.skills) {
|
|
495
|
+
if (!SKILL_NAME_PATTERN.test(skill)) {
|
|
496
|
+
return yield* new InvalidSourceError({
|
|
497
|
+
source: source.id,
|
|
498
|
+
reason: `lockfile contains an invalid skill name: ${skill}`,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
if (skills.has(skill)) {
|
|
502
|
+
return yield* new SkillCollisionError({
|
|
503
|
+
skill,
|
|
504
|
+
owners: ["multiple lockfile sources"],
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
skills.add(skill);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const currentLocalSkills = Effect.fn("currentLocalSkills")(function* (
|
|
513
|
+
skillsDir: string,
|
|
514
|
+
currentLock: SkillSourcesLock | undefined,
|
|
515
|
+
) {
|
|
516
|
+
const fs = yield* FileSystem.FileSystem;
|
|
517
|
+
if (!(yield* fs.exists(skillsDir))) {
|
|
518
|
+
return [];
|
|
519
|
+
}
|
|
520
|
+
const managed = new Set(currentLock?.sources.flatMap((source) => source.skills) ?? []);
|
|
521
|
+
const entries = yield* fs.readDirectory(skillsDir);
|
|
522
|
+
return entries.filter((entry) => !managed.has(entry));
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
const validateOwnership = Effect.fn("validateSkillOwnership")(function* (
|
|
526
|
+
prepared: ReadonlyArray<PreparedSource>,
|
|
527
|
+
localSkills: ReadonlyArray<string>,
|
|
528
|
+
) {
|
|
529
|
+
const owners = new Map<string, Array<string>>();
|
|
530
|
+
for (const skill of localSkills) {
|
|
531
|
+
owners.set(skill, ["local"]);
|
|
532
|
+
}
|
|
533
|
+
for (const preparedSource of prepared) {
|
|
534
|
+
for (const skill of preparedSource.skills) {
|
|
535
|
+
const existing = owners.get(skill) ?? [];
|
|
536
|
+
existing.push(preparedSource.source.id);
|
|
537
|
+
owners.set(skill, existing);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
for (const [skill, skillOwners] of owners) {
|
|
541
|
+
if (skillOwners.length > 1) {
|
|
542
|
+
return yield* new SkillCollisionError({ skill, owners: skillOwners });
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const preparedSource of prepared) {
|
|
546
|
+
if (owners.has(preparedSource.source.id)) {
|
|
547
|
+
return yield* new InvalidSourceError({
|
|
548
|
+
source: preparedSource.source.id,
|
|
549
|
+
reason: "id conflicts with a skill name",
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
for (const reservedFamily of RESERVED_SOURCE_IDS) {
|
|
554
|
+
if (owners.has(reservedFamily)) {
|
|
555
|
+
return yield* new InvalidSourceError({
|
|
556
|
+
source: reservedFamily,
|
|
557
|
+
reason: "skill name conflicts with a built-in skill family",
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
const stripFrontmatterKeys = (
|
|
564
|
+
skillDocument: string,
|
|
565
|
+
keys: ReadonlyArray<string>,
|
|
566
|
+
): string => {
|
|
567
|
+
if (keys.length === 0) {
|
|
568
|
+
return skillDocument;
|
|
569
|
+
}
|
|
570
|
+
const frontmatter = skillDocument.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
571
|
+
if (!frontmatter) {
|
|
572
|
+
return skillDocument;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const stripped = new Set(keys);
|
|
576
|
+
const keptLines: Array<string> = [];
|
|
577
|
+
let skipping = false;
|
|
578
|
+
const frontmatterBody = frontmatter[1];
|
|
579
|
+
if (frontmatterBody === undefined) return skillDocument;
|
|
580
|
+
for (const line of frontmatterBody.split(/\r?\n/)) {
|
|
581
|
+
const key = line.match(/^([A-Za-z0-9_-]+):/)?.[1];
|
|
582
|
+
if (key) {
|
|
583
|
+
skipping = stripped.has(key);
|
|
584
|
+
}
|
|
585
|
+
if (!skipping) {
|
|
586
|
+
keptLines.push(line);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return `---\n${keptLines.join("\n")}\n---${frontmatter[2]}${skillDocument.slice(frontmatter[0].length)}`;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
const stageSources = Effect.fn("stageSkillSources")(function* (
|
|
594
|
+
tempDir: string,
|
|
595
|
+
prepared: ReadonlyArray<PreparedSource>,
|
|
596
|
+
) {
|
|
597
|
+
const fs = yield* FileSystem.FileSystem;
|
|
598
|
+
const path = yield* Path.Path;
|
|
599
|
+
const stagedSkillsDir = path.join(tempDir, "staged", "skills");
|
|
600
|
+
yield* fs.makeDirectory(stagedSkillsDir, { recursive: true });
|
|
601
|
+
|
|
602
|
+
for (const preparedSource of prepared) {
|
|
603
|
+
const sourceSkillsDir = path.resolve(
|
|
604
|
+
preparedSource.checkoutDir,
|
|
605
|
+
preparedSource.source.skillsPath,
|
|
606
|
+
);
|
|
607
|
+
for (const skill of preparedSource.skills) {
|
|
608
|
+
const stagedSkillDir = path.join(stagedSkillsDir, skill);
|
|
609
|
+
yield* fs.copy(path.join(sourceSkillsDir, skill), stagedSkillDir, { overwrite: true });
|
|
610
|
+
if (preparedSource.source.stripFrontmatter?.length) {
|
|
611
|
+
const skillDocumentPath = path.join(stagedSkillDir, "SKILL.md");
|
|
612
|
+
const skillDocument = yield* fs.readFileString(skillDocumentPath);
|
|
613
|
+
yield* fs.writeFileString(
|
|
614
|
+
skillDocumentPath,
|
|
615
|
+
stripFrontmatterKeys(skillDocument, preparedSource.source.stripFrontmatter),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
return { stagedSkillsDir };
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
const buildLock = Effect.fn("buildSkillCatalogLock")(function* (
|
|
625
|
+
prepared: ReadonlyArray<PreparedSource>,
|
|
626
|
+
stagedSkillsDir: string,
|
|
627
|
+
) {
|
|
628
|
+
const fs = yield* FileSystem.FileSystem;
|
|
629
|
+
const path = yield* Path.Path;
|
|
630
|
+
const sources: Array<LockedSkillSource> = [];
|
|
631
|
+
for (const { resolved, skills, source } of prepared) {
|
|
632
|
+
const descriptions: Record<string, string> = {};
|
|
633
|
+
const digests: Record<string, Digest> = {};
|
|
634
|
+
for (const skill of skills) {
|
|
635
|
+
const stagedSkill = path.join(stagedSkillsDir, skill);
|
|
636
|
+
const document = yield* fs.readFileString(path.join(stagedSkill, "SKILL.md"));
|
|
637
|
+
descriptions[skill] = document
|
|
638
|
+
.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
|
|
639
|
+
?.split(/\r?\n/)
|
|
640
|
+
.find((line) => line.startsWith("description:"))
|
|
641
|
+
?.slice("description:".length)
|
|
642
|
+
.trim()
|
|
643
|
+
.replace(/^(['"])(.*)\1$/, "$2") ?? "";
|
|
644
|
+
const observation = yield* observePath(stagedSkill);
|
|
645
|
+
if (observation.kind !== "directory") {
|
|
646
|
+
return yield* new InvalidSourceError({ source: source.id, reason: `could not digest ${skill}` });
|
|
647
|
+
}
|
|
648
|
+
digests[skill] = observation.digest;
|
|
649
|
+
}
|
|
650
|
+
sources.push({
|
|
651
|
+
id: source.id,
|
|
652
|
+
repository: source.repository,
|
|
653
|
+
ref: source.ref,
|
|
654
|
+
resolved,
|
|
655
|
+
skillsPath: source.skillsPath,
|
|
656
|
+
include: source.include,
|
|
657
|
+
...(source.exclude ? { exclude: source.exclude } : {}),
|
|
658
|
+
skills,
|
|
659
|
+
descriptions,
|
|
660
|
+
digests,
|
|
661
|
+
...(source.licensePath ? { licensePath: source.licensePath } : {}),
|
|
662
|
+
...(source.stripFrontmatter ? { stripFrontmatter: source.stripFrontmatter } : {}),
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
return { version: 1, sources } satisfies SkillSourcesLock;
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
export const inspectCatalogRepository = Effect.fn("inspectCatalogRepository")(function* (
|
|
669
|
+
options: CatalogInspectOptions,
|
|
670
|
+
) {
|
|
671
|
+
const fs = yield* FileSystem.FileSystem;
|
|
672
|
+
const path = yield* Path.Path;
|
|
673
|
+
const repoDir = path.resolve(options.repoDir ?? ".");
|
|
674
|
+
const locator = yield* normalizeRepositoryLocator(options.repository);
|
|
675
|
+
const repository = locator.repository;
|
|
676
|
+
const id = options.id ?? inferSourceId(repository);
|
|
677
|
+
if (id.length === 0) {
|
|
678
|
+
return yield* new InvalidSourceError({
|
|
679
|
+
source: repository,
|
|
680
|
+
reason: "could not infer a source id; pass --id",
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
const source: ExternalSkillSource = {
|
|
684
|
+
id,
|
|
685
|
+
repository,
|
|
686
|
+
ref: options.ref ?? locator.ref ?? "HEAD",
|
|
687
|
+
skillsPath: options.skillsPath ?? locator.skillsPath ?? "skills",
|
|
688
|
+
include: ["*"],
|
|
689
|
+
};
|
|
690
|
+
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
691
|
+
directory: repoDir,
|
|
692
|
+
prefix: ".dev-kit-inspect-",
|
|
693
|
+
});
|
|
694
|
+
const prepared = yield* withSpinner(
|
|
695
|
+
"Inspecting skill repository",
|
|
696
|
+
prepareSource(tempDir, source, undefined, false),
|
|
697
|
+
);
|
|
698
|
+
let ref = source.ref;
|
|
699
|
+
if (ref === "HEAD") {
|
|
700
|
+
const symbolicHead = yield* runCommand(prepared.checkoutDir, "git", [
|
|
701
|
+
"ls-remote",
|
|
702
|
+
"--symref",
|
|
703
|
+
"origin",
|
|
704
|
+
"HEAD",
|
|
705
|
+
]);
|
|
706
|
+
ref = symbolicHead.match(/^ref:\s+refs\/heads\/([^\s]+)\s+HEAD$/m)?.[1] ?? ref;
|
|
707
|
+
}
|
|
708
|
+
const skills: Array<{ readonly name: string; readonly description: string }> = [];
|
|
709
|
+
for (const name of prepared.skills) {
|
|
710
|
+
const document = yield* fs.readFileString(
|
|
711
|
+
path.join(prepared.checkoutDir, source.skillsPath, name, "SKILL.md"),
|
|
712
|
+
);
|
|
713
|
+
const description = document
|
|
714
|
+
.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1]
|
|
715
|
+
?.split(/\r?\n/)
|
|
716
|
+
.find((line) => line.startsWith("description:"))
|
|
717
|
+
?.slice("description:".length)
|
|
718
|
+
.trim()
|
|
719
|
+
.replace(/^(['"])(.*)\1$/, "$2") ?? "";
|
|
720
|
+
skills.push({ name, description });
|
|
721
|
+
}
|
|
722
|
+
let licensePath: string | undefined;
|
|
723
|
+
for (const candidate of ["LICENSE", "LICENSE.md", "LICENSE.txt", "COPYING"]) {
|
|
724
|
+
const candidatePath = path.join(prepared.checkoutDir, candidate);
|
|
725
|
+
if (yield* fs.exists(candidatePath)) {
|
|
726
|
+
const info = yield* fs.stat(candidatePath);
|
|
727
|
+
if (info.type === "File") {
|
|
728
|
+
licensePath = candidate;
|
|
729
|
+
break;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
id,
|
|
735
|
+
repository,
|
|
736
|
+
ref,
|
|
737
|
+
resolved: prepared.resolved,
|
|
738
|
+
skillsPath: source.skillsPath,
|
|
739
|
+
skills,
|
|
740
|
+
...(licensePath ? { licensePath } : {}),
|
|
741
|
+
} satisfies CatalogInspection;
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
export const refreshSkillCatalog = Effect.fn("refreshSkillCatalog")(function* (
|
|
745
|
+
options: CatalogRefreshOptions,
|
|
746
|
+
) {
|
|
747
|
+
const fs = yield* FileSystem.FileSystem;
|
|
748
|
+
const path = yield* Path.Path;
|
|
749
|
+
const initialDir = path.resolve(options.repoDir ?? ".");
|
|
750
|
+
const repoDir = yield* resolveGitRoot(initialDir).pipe(
|
|
751
|
+
Effect.catchTag("CommandError", (error) =>
|
|
752
|
+
error.output.includes("not a git repository")
|
|
753
|
+
? Effect.succeed(initialDir)
|
|
754
|
+
: Effect.fail(error),
|
|
755
|
+
),
|
|
756
|
+
);
|
|
757
|
+
const sourcesPath = path.resolve(repoDir, options.sourcesPath ?? DEFAULT_SOURCES_PATH);
|
|
758
|
+
const lockfilePath = path.resolve(repoDir, options.lockfilePath ?? DEFAULT_LOCKFILE_PATH);
|
|
759
|
+
const manifest = yield* readJsonc(sourcesPath, SkillSourcesManifestSchema);
|
|
760
|
+
const currentLock = yield* readCurrentLock(lockfilePath);
|
|
761
|
+
yield* validateCurrentLock(currentLock);
|
|
762
|
+
const lockedById = new Map(
|
|
763
|
+
currentLock?.sources.map((source) => [source.id, source] as const) ?? [],
|
|
764
|
+
);
|
|
765
|
+
|
|
766
|
+
const sourceIds = new Set<string>();
|
|
767
|
+
for (const source of manifest.sources) {
|
|
768
|
+
if (sourceIds.has(source.id)) {
|
|
769
|
+
return yield* new InvalidSourceError({
|
|
770
|
+
source: source.id,
|
|
771
|
+
reason: "source ids must be unique",
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
sourceIds.add(source.id);
|
|
775
|
+
}
|
|
776
|
+
if (options.locked) {
|
|
777
|
+
if (!currentLock) {
|
|
778
|
+
return yield* new SourceManifestError({
|
|
779
|
+
path: lockfilePath,
|
|
780
|
+
message: "lockfile is required with --locked",
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
const lockedIds = new Set(currentLock.sources.map((source) => source.id));
|
|
784
|
+
const missingFromManifest = currentLock.sources.find((source) => !sourceIds.has(source.id));
|
|
785
|
+
const missingFromLock = manifest.sources.find((source) => !lockedIds.has(source.id));
|
|
786
|
+
if (missingFromManifest || missingFromLock) {
|
|
787
|
+
return yield* new SourceManifestError({
|
|
788
|
+
path: lockfilePath,
|
|
789
|
+
message: "source ids differ from skill-sources.jsonc; run catalog refresh without --locked",
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
const tempDir = yield* fs.makeTempDirectoryScoped({
|
|
795
|
+
directory: repoDir,
|
|
796
|
+
prefix: ".dev-kit-vendor-",
|
|
797
|
+
});
|
|
798
|
+
const prepared = yield* withSpinner(
|
|
799
|
+
"Fetching skill sources",
|
|
800
|
+
Effect.forEach(
|
|
801
|
+
manifest.sources,
|
|
802
|
+
(source) => {
|
|
803
|
+
const pinned = options.pinSourceIds?.includes(source.id) ?? false;
|
|
804
|
+
const useLock = (options.locked ?? false) || pinned ||
|
|
805
|
+
(options.updateSourceIds !== undefined && !options.updateSourceIds.includes(source.id));
|
|
806
|
+
return prepareSource(
|
|
807
|
+
tempDir,
|
|
808
|
+
source,
|
|
809
|
+
lockedById.get(source.id),
|
|
810
|
+
useLock,
|
|
811
|
+
(options.locked ?? false) || !pinned,
|
|
812
|
+
);
|
|
813
|
+
},
|
|
814
|
+
{ concurrency: 4 },
|
|
815
|
+
),
|
|
816
|
+
);
|
|
817
|
+
const localSkills = yield* currentLocalSkills(path.join(repoDir, "skills"), currentLock);
|
|
818
|
+
yield* validateOwnership(prepared, localSkills);
|
|
819
|
+
const staged = yield* stageSources(tempDir, prepared);
|
|
820
|
+
const nextLock = yield* buildLock(prepared, staged.stagedSkillsDir);
|
|
821
|
+
|
|
822
|
+
const skillCount = new Set(nextLock.sources.flatMap((source) => source.skills)).size;
|
|
823
|
+
const summary = `${skillCount} skill${skillCount === 1 ? "" : "s"} from ${nextLock.sources.length} source${nextLock.sources.length === 1 ? "" : "s"}`;
|
|
824
|
+
|
|
825
|
+
if (options.locked) {
|
|
826
|
+
if (JSON.stringify(currentLock) !== JSON.stringify(nextLock)) {
|
|
827
|
+
return yield* new SourceManifestError({
|
|
828
|
+
path: lockfilePath,
|
|
829
|
+
message: "approved catalog metadata differs from the lock; run catalog refresh and review it",
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
yield* printStatus("success", "Catalog verified", summary);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (options.dryRun) {
|
|
837
|
+
yield* printStatus("plan", "Would refresh catalog", summary);
|
|
838
|
+
return;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const nextLockPath = path.join(tempDir, "next-catalog-lock.json");
|
|
842
|
+
yield* fs.writeFileString(nextLockPath, `${JSON.stringify(nextLock, null, 2)}\n`);
|
|
843
|
+
yield* fs.rename(nextLockPath, lockfilePath);
|
|
844
|
+
yield* printStatus("success", "Catalog refreshed", summary);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
/** @deprecated Use refreshSkillCatalog. */
|
|
848
|
+
export const vendorExternalSkills = refreshSkillCatalog;
|