@microck/canonfig 2.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/agent/agent-resolution.errors.js +42 -0
  4. package/dist/agent/agent-resolution.layer.js +204 -0
  5. package/dist/agent/agent-resolution.service.js +2259 -0
  6. package/dist/agent/agent-resolution.types.js +1 -0
  7. package/dist/agent/controlled-executor.js +704 -0
  8. package/dist/agent/harness-adapters.js +85 -0
  9. package/dist/cli/cli.js +618 -0
  10. package/dist/cli/exit-codes.js +28 -0
  11. package/dist/cli/follower-commands.js +3 -0
  12. package/dist/cli/render.js +56 -0
  13. package/dist/cli/source-commands.js +5 -0
  14. package/dist/domain/brand.js +29 -0
  15. package/dist/domain/identity.js +31 -0
  16. package/dist/domain/npm-package-spec.js +186 -0
  17. package/dist/domain/profile.js +950 -0
  18. package/dist/domain/recipe-versions.js +297 -0
  19. package/dist/domain/resource.js +259 -0
  20. package/dist/domain/synchronization.js +346 -0
  21. package/dist/enrollment/enrollment.errors.js +43 -0
  22. package/dist/enrollment/enrollment.layer.js +724 -0
  23. package/dist/enrollment/enrollment.service.js +3 -0
  24. package/dist/enrollment/enrollment.types.js +59 -0
  25. package/dist/enrollment/follower-client.js +585 -0
  26. package/dist/enrollment/source-server.js +313 -0
  27. package/dist/machine/linux.layer.js +1183 -0
  28. package/dist/machine/machine-state.errors.js +52 -0
  29. package/dist/machine/machine-state.service.js +3 -0
  30. package/dist/machine/machine-state.types.js +1 -0
  31. package/dist/machine/macos.layer.js +470 -0
  32. package/dist/machine/windows.layer.js +879 -0
  33. package/dist/profile/discovery.js +740 -0
  34. package/dist/profile/profile-catalog.errors.js +50 -0
  35. package/dist/profile/profile-catalog.layer.js +20 -0
  36. package/dist/profile/profile-catalog.service.js +7 -0
  37. package/dist/profile/profile-codec.js +153 -0
  38. package/dist/profile/publication.js +298 -0
  39. package/dist/profile/tool-catalog.js +384 -0
  40. package/dist/runtime/doctor.js +306 -0
  41. package/dist/runtime/layers.js +706 -0
  42. package/dist/runtime/main.js +38 -0
  43. package/dist/schedule/linux-schedule.js +24 -0
  44. package/dist/schedule/macos-schedule.js +25 -0
  45. package/dist/schedule/schedule-manager.errors.js +17 -0
  46. package/dist/schedule/schedule-manager.layer.js +205 -0
  47. package/dist/schedule/schedule-manager.service.js +3 -0
  48. package/dist/schedule/schedule-manager.types.js +114 -0
  49. package/dist/schedule/windows-schedule.js +25 -0
  50. package/dist/state/state-repository.errors.js +55 -0
  51. package/dist/state/state-repository.layer.js +1507 -0
  52. package/dist/state/state-repository.service.js +3 -0
  53. package/dist/state/state-repository.types.js +1 -0
  54. package/dist/state/state-schema.js +298 -0
  55. package/dist/synchronization/config-codec.js +97 -0
  56. package/dist/synchronization/executor.js +700 -0
  57. package/dist/synchronization/follower-orchestration.js +939 -0
  58. package/dist/synchronization/follower-sync-config.js +81 -0
  59. package/dist/synchronization/npm-artifact.js +670 -0
  60. package/dist/synchronization/planner.js +378 -0
  61. package/dist/synchronization/recovery.js +397 -0
  62. package/dist/synchronization/resource-executors.js +1198 -0
  63. package/dist/synchronization/resource-plans.js +645 -0
  64. package/dist/synchronization/synchronization.errors.js +102 -0
  65. package/dist/synchronization/synchronization.layer.js +97 -0
  66. package/dist/synchronization/synchronization.service.js +11 -0
  67. package/dist/synchronization/synchronization.types.js +1 -0
  68. package/package.json +66 -0
@@ -0,0 +1,740 @@
1
+ import { constants } from "node:fs";
2
+ import { access, readFile, stat } from "node:fs/promises";
3
+ import { basename, delimiter, extname, join, resolve } from "node:path";
4
+ import { Effect, Schema } from "effect";
5
+ import { parse as parseToml } from "smol-toml";
6
+ import { BuildPolicy as BuildPolicySchema, RecipeIndexPolicy, } from "../domain/resource.js";
7
+ import { parseNpmPackageSpecification } from "../domain/npm-package-spec.js";
8
+ import { parseJsonc } from "./profile-codec.js";
9
+ import { DiscoveryFilesystemError, DiscoveryParseError, InvalidDiscoveryInputError, } from "./profile-catalog.errors.js";
10
+ import { buildToolCatalog, } from "./tool-catalog.js";
11
+ const compareText = (left, right) => left < right ? -1 : left > right ? 1 : 0;
12
+ const inferFileKind = (path) => {
13
+ const name = basename(path).toLowerCase();
14
+ if (name === "agents.md" || name === "claude.md")
15
+ return "agents";
16
+ if (name === "package.json" || name === "cargo.toml" || name === "pyproject.toml" || name === "brewfile" || name.includes("winget")) {
17
+ return "package-metadata";
18
+ }
19
+ if (name.includes("hook") || name === "settings.json")
20
+ return "hooks";
21
+ if (name.includes("mcp"))
22
+ return "mcp";
23
+ return "tool-config";
24
+ };
25
+ const readDiscoveryFile = (path) => Effect.tryPromise({
26
+ try: () => readFile(path, "utf8"),
27
+ catch: (cause) => new DiscoveryFilesystemError({
28
+ path,
29
+ operation: "read",
30
+ reason: String(cause),
31
+ }),
32
+ });
33
+ const executablePath = async (executable, pathValue) => {
34
+ if (executable.includes("/") || executable.includes("\\")) {
35
+ const absolute = resolve(executable);
36
+ try {
37
+ await access(absolute, constants.X_OK);
38
+ const details = await stat(absolute);
39
+ return details.isFile() ? absolute : undefined;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ for (const entry of pathValue.split(delimiter).filter((value) => value.length > 0)) {
46
+ const candidate = join(entry, executable);
47
+ try {
48
+ await access(candidate, constants.X_OK);
49
+ const details = await stat(candidate);
50
+ if (details.isFile())
51
+ return candidate;
52
+ }
53
+ catch {
54
+ // A missing or non-executable candidate means resolution continues.
55
+ }
56
+ }
57
+ return undefined;
58
+ };
59
+ const tokenize = (command) => {
60
+ const tokens = [];
61
+ let token = "";
62
+ let quote = null;
63
+ let escaped = false;
64
+ const push = () => {
65
+ if (token.length > 0)
66
+ tokens.push(token);
67
+ token = "";
68
+ };
69
+ for (const character of command.trim()) {
70
+ if (escaped) {
71
+ token += character;
72
+ escaped = false;
73
+ continue;
74
+ }
75
+ if (character === "\\" && quote !== "'") {
76
+ escaped = true;
77
+ continue;
78
+ }
79
+ if (quote !== null) {
80
+ if (character === quote)
81
+ quote = null;
82
+ else
83
+ token += character;
84
+ continue;
85
+ }
86
+ if (character === "'" || character === '"') {
87
+ quote = character;
88
+ continue;
89
+ }
90
+ if (/\s/u.test(character)) {
91
+ push();
92
+ continue;
93
+ }
94
+ token += character;
95
+ }
96
+ push();
97
+ return tokens;
98
+ };
99
+ const stripEnvironmentPrefix = (tokens) => {
100
+ let index = 0;
101
+ if (tokens[index] === "env")
102
+ index += 1;
103
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/u.test(tokens[index] ?? ""))
104
+ index += 1;
105
+ return tokens.slice(index);
106
+ };
107
+ const packageSpecification = (specification, separator) => {
108
+ if (separator === "==") {
109
+ const index = specification.lastIndexOf("==");
110
+ return index > 0
111
+ ? { name: specification.slice(0, index), version: specification.slice(index + 2) }
112
+ : { name: specification };
113
+ }
114
+ const index = specification.lastIndexOf("@");
115
+ if (index > 0)
116
+ return { name: specification.slice(0, index), version: specification.slice(index + 1) };
117
+ return { name: specification };
118
+ };
119
+ const isUnboundedPackageSpecification = (value) => value === "--"
120
+ || /^\s*-{1,2}\S*/u.test(value)
121
+ || /\s/u.test(value)
122
+ || parseNpmPackageSpecification(value).kind !== "registry";
123
+ const valueAfter = (tokens, options) => {
124
+ for (const option of options) {
125
+ const index = tokens.indexOf(option);
126
+ if (index >= 0)
127
+ return tokens[index + 1];
128
+ }
129
+ return undefined;
130
+ };
131
+ const positionalAfter = (tokens, commands) => {
132
+ const commandIndex = tokens.findIndex((token) => commands.includes(token));
133
+ if (commandIndex < 0)
134
+ return undefined;
135
+ return tokens.slice(commandIndex + 1).find((token) => !token.startsWith("-"));
136
+ };
137
+ const metadataFromInvocation = (input, sourcePath) => {
138
+ const tokens = stripEnvironmentPrefix(input);
139
+ const executable = tokens[0];
140
+ if (executable === "npm" || executable === "npx") {
141
+ if (tokens.includes("--"))
142
+ return undefined;
143
+ const specification = executable === "npx"
144
+ ? tokens.find((token, index) => index > 0 && !token.startsWith("-"))
145
+ : positionalAfter(tokens, ["install", "i", "add"]);
146
+ if (specification === undefined)
147
+ return undefined;
148
+ if (isUnboundedPackageSpecification(specification))
149
+ return undefined;
150
+ const parsed = packageSpecification(specification, "@");
151
+ return {
152
+ ecosystem: "npm",
153
+ ...parsed,
154
+ source: `${sourcePath}#npm`,
155
+ };
156
+ }
157
+ if (executable === "brew") {
158
+ const formula = positionalAfter(tokens, ["install"]);
159
+ if (formula === undefined)
160
+ return undefined;
161
+ const parsed = packageSpecification(formula, "@");
162
+ return {
163
+ ecosystem: "homebrew",
164
+ ...parsed,
165
+ source: `${sourcePath}#homebrew`,
166
+ };
167
+ }
168
+ if (executable === "winget") {
169
+ const id = valueAfter(tokens, ["--id"]) ?? positionalAfter(tokens, ["install"]);
170
+ if (id === undefined)
171
+ return undefined;
172
+ const version = valueAfter(tokens, ["--version", "-v"]);
173
+ return {
174
+ ecosystem: "winget",
175
+ name: id,
176
+ version,
177
+ source: `${sourcePath}#winget`,
178
+ };
179
+ }
180
+ if (executable === "uv" || executable === "uvx") {
181
+ if (tokens.includes("--"))
182
+ return undefined;
183
+ const specification = executable === "uvx"
184
+ ? tokens.find((token, index) => index > 0 && !token.startsWith("-"))
185
+ : positionalAfter(tokens, ["install"]);
186
+ if (specification === undefined)
187
+ return undefined;
188
+ if (isUnboundedPackageSpecification(specification))
189
+ return undefined;
190
+ const parsed = packageSpecification(specification, "==");
191
+ return {
192
+ ecosystem: "uv",
193
+ ...parsed,
194
+ source: `${sourcePath}#uv`,
195
+ };
196
+ }
197
+ if (executable === "cargo") {
198
+ const crate = positionalAfter(tokens, ["install"]);
199
+ if (crate === undefined)
200
+ return undefined;
201
+ const version = valueAfter(tokens, ["--version"]);
202
+ return {
203
+ ecosystem: "cargo",
204
+ name: crate,
205
+ version,
206
+ source: `${sourcePath}#cargo`,
207
+ };
208
+ }
209
+ return undefined;
210
+ };
211
+ const sourceKindFor = (fileKind, deterministic) => {
212
+ if (!deterministic)
213
+ return "prose";
214
+ switch (fileKind) {
215
+ case "agents":
216
+ return "agents";
217
+ case "hooks":
218
+ return "hook";
219
+ case "mcp":
220
+ return "mcp";
221
+ case "package-metadata":
222
+ return "package-metadata";
223
+ case "tool-config":
224
+ return "tool-config";
225
+ }
226
+ };
227
+ const invocationEvidence = async (context, invocation, location, deterministic, kindOverride) => {
228
+ const tokens = stripEnvironmentPrefix(invocation);
229
+ const executable = tokens[0];
230
+ if (executable === undefined || executable.length === 0)
231
+ return undefined;
232
+ const packageMetadata = deterministic
233
+ ? metadataFromInvocation(tokens, context.sourcePath)
234
+ : undefined;
235
+ const resolvedExecutable = deterministic
236
+ ? await executablePath(executable, context.pathValue)
237
+ : undefined;
238
+ return {
239
+ sourcePath: context.sourcePath,
240
+ location,
241
+ kind: kindOverride ?? sourceKindFor(context.fileKind, deterministic),
242
+ invocation: tokens,
243
+ resolvedExecutable,
244
+ package: packageMetadata,
245
+ confidence: deterministic
246
+ ? packageMetadata === undefined && resolvedExecutable === undefined ? "strong" : "deterministic"
247
+ : "review",
248
+ reviewStatus: deterministic ? "accepted" : "needs-review",
249
+ };
250
+ };
251
+ const scanMarkdown = async (context, text) => {
252
+ const evidence = [];
253
+ const skills = new Map();
254
+ let fenced = false;
255
+ let executableFence = false;
256
+ const lines = text.split(/\r?\n/u);
257
+ for (let index = 0; index < lines.length; index += 1) {
258
+ const line = lines[index] ?? "";
259
+ const fence = /^\s*```([A-Za-z0-9_-]*)/u.exec(line);
260
+ if (fence !== null) {
261
+ if (fenced) {
262
+ fenced = false;
263
+ executableFence = false;
264
+ }
265
+ else {
266
+ fenced = true;
267
+ executableFence = ["sh", "bash", "shell", "zsh", "fish", "powershell", "pwsh", "cmd"].includes((fence[1] ?? "").toLowerCase());
268
+ }
269
+ continue;
270
+ }
271
+ if (fenced && executableFence) {
272
+ const commands = line
273
+ .split(/\s*(?:&&|\|\||;|\|)\s*/u)
274
+ .map(tokenize)
275
+ .filter((tokens) => tokens.length > 0 && !tokens[0].startsWith("#"));
276
+ for (const invocation of commands) {
277
+ const record = await invocationEvidence(context, invocation, { kind: "line", line: index + 1 }, true);
278
+ if (record !== undefined)
279
+ evidence.push(record);
280
+ }
281
+ continue;
282
+ }
283
+ if (!fenced) {
284
+ for (const match of line.matchAll(/`([^`\r\n]+)`/gu)) {
285
+ const invocation = tokenize(match[1] ?? "");
286
+ const record = await invocationEvidence(context, invocation, { kind: "line", line: index + 1, column: (match.index ?? 0) + 1 }, false);
287
+ if (record !== undefined)
288
+ evidence.push(record);
289
+ }
290
+ for (const match of line.matchAll(/(?:^|[\s("'`])(?:\.\/)?skills\/([A-Za-z0-9._-]+)\/SKILL\.md/giu)) {
291
+ const id = (match[1] ?? "").toLowerCase();
292
+ if (id.length === 0)
293
+ continue;
294
+ const record = {
295
+ sourcePath: context.sourcePath,
296
+ location: { kind: "line", line: index + 1, column: (match.index ?? 0) + 1 },
297
+ kind: "prose",
298
+ invocation: [`skills/${id}/SKILL.md`],
299
+ confidence: "review",
300
+ reviewStatus: "needs-review",
301
+ };
302
+ skills.set(id, {
303
+ kind: "skill",
304
+ id,
305
+ sourcePath: context.sourcePath,
306
+ evidence: [record],
307
+ reviewStatus: "needs-review",
308
+ });
309
+ }
310
+ }
311
+ }
312
+ return { evidence, skills: [...skills.values()] };
313
+ };
314
+ const JsonObject = Schema.Record(Schema.String, Schema.MutableJson);
315
+ const jsonObject = (value) => Schema.is(JsonObject)(value) ? value : undefined;
316
+ const jsonString = (value) => Schema.is(Schema.String)(value) ? value : undefined;
317
+ const JsonStringArray = Schema.Array(Schema.String);
318
+ const JsonCommandArray = Schema.Array(JsonStringArray);
319
+ const lineForField = (text, field) => {
320
+ const quoted = `"${field.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`;
321
+ const index = text.indexOf(quoted);
322
+ if (index < 0)
323
+ return undefined;
324
+ return text.slice(0, index).split(/\r?\n/u).length;
325
+ };
326
+ const repositoryUrl = (value) => {
327
+ const direct = jsonString(value);
328
+ if (direct !== undefined)
329
+ return direct.replace(/^git\+/u, "");
330
+ const object = value === undefined ? undefined : jsonObject(value);
331
+ return jsonString(object?.url)?.replace(/^git\+/u, "");
332
+ };
333
+ const explicitMetadata = (value, sourcePath) => {
334
+ const object = jsonObject(value);
335
+ if (object === undefined)
336
+ return undefined;
337
+ const ecosystem = jsonString(object.ecosystem);
338
+ const name = jsonString(object.name);
339
+ const source = jsonString(object.source);
340
+ if (ecosystem === undefined
341
+ || !["npm", "homebrew", "winget", "uv", "cargo", "source"].includes(ecosystem)
342
+ || name === undefined) {
343
+ return undefined;
344
+ }
345
+ const buildCommands = Schema.is(JsonCommandArray)(object.buildCommands)
346
+ ? object.buildCommands
347
+ : undefined;
348
+ const buildPolicy = Schema.is(BuildPolicySchema)(object.buildPolicy)
349
+ ? object.buildPolicy
350
+ : undefined;
351
+ const indexPolicy = Schema.is(RecipeIndexPolicy)(object.indexPolicy)
352
+ ? object.indexPolicy
353
+ : undefined;
354
+ // SAFETY: ecosystem has been checked against every PackageEcosystem literal.
355
+ const checkedEcosystem = Schema.decodeUnknownSync(Schema.Literals(["npm", "homebrew", "winget", "uv", "cargo", "source"]))(ecosystem);
356
+ return {
357
+ ecosystem: checkedEcosystem,
358
+ name,
359
+ version: jsonString(object.version),
360
+ source: source ?? `${sourcePath}#canonfig.tools`,
361
+ integrity: jsonString(object.integrity),
362
+ indexPolicy,
363
+ upstream: jsonString(object.upstream),
364
+ buildCommands,
365
+ buildPolicy,
366
+ };
367
+ };
368
+ const scanPackageJson = (context, text, value) => {
369
+ const object = jsonObject(value);
370
+ if (object === undefined)
371
+ return [];
372
+ const records = [];
373
+ const name = jsonString(object.name);
374
+ const version = jsonString(object.version);
375
+ const bin = object.bin;
376
+ const upstream = repositoryUrl(object.repository) ?? (jsonString(object.homepage));
377
+ const binString = jsonString(bin);
378
+ const binEntries = binString !== undefined && name !== undefined
379
+ ? [[name, binString]]
380
+ : Object.entries(bin === undefined ? {} : jsonObject(bin) ?? {})
381
+ .filter((entry) => Schema.is(Schema.String)(entry[1]));
382
+ for (const [executable] of binEntries) {
383
+ const packageMetadata = {
384
+ ecosystem: "npm",
385
+ name: name ?? executable,
386
+ version,
387
+ source: context.sourcePath,
388
+ upstream,
389
+ };
390
+ records.push({
391
+ sourcePath: context.sourcePath,
392
+ location: {
393
+ kind: "field",
394
+ field: `bin.${executable}`,
395
+ line: lineForField(text, executable),
396
+ },
397
+ kind: "package-metadata",
398
+ invocation: [executable],
399
+ package: packageMetadata,
400
+ upstream,
401
+ confidence: version === undefined ? "strong" : "deterministic",
402
+ reviewStatus: "accepted",
403
+ });
404
+ }
405
+ const canonfig = object.canonfig === undefined ? undefined : jsonObject(object.canonfig);
406
+ const tools = canonfig?.tools;
407
+ if (Array.isArray(tools)) {
408
+ for (let index = 0; index < tools.length; index += 1) {
409
+ const metadata = explicitMetadata(tools[index], context.sourcePath);
410
+ const tool = jsonObject(tools[index]);
411
+ if (metadata === undefined || tool === undefined)
412
+ continue;
413
+ const executable = jsonString(tool.executable) ?? metadata.name;
414
+ records.push({
415
+ sourcePath: context.sourcePath,
416
+ location: {
417
+ kind: "field",
418
+ field: `canonfig.tools[${index}]`,
419
+ line: lineForField(text, "tools"),
420
+ },
421
+ kind: "package-metadata",
422
+ invocation: [executable],
423
+ package: metadata,
424
+ upstream: metadata.upstream,
425
+ confidence: metadata.version === undefined ? "strong" : "deterministic",
426
+ reviewStatus: "accepted",
427
+ });
428
+ }
429
+ }
430
+ return records;
431
+ };
432
+ const scanPackageLock = (context, text, value) => {
433
+ const object = jsonObject(value);
434
+ const packages = object?.packages === undefined ? undefined : jsonObject(object.packages);
435
+ if (packages === undefined)
436
+ return [];
437
+ const records = [];
438
+ for (const [packagePath, packageValue] of Object.entries(packages).sort(([left], [right]) => compareText(left, right))) {
439
+ const packageObject = jsonObject(packageValue);
440
+ if (packageObject === undefined)
441
+ continue;
442
+ const name = jsonString(packageObject.name)
443
+ ?? packagePath.slice(packagePath.lastIndexOf("node_modules/") + "node_modules/".length);
444
+ const version = jsonString(packageObject.version);
445
+ const bin = packageObject.bin;
446
+ const binString = jsonString(bin);
447
+ const binEntries = binString === undefined
448
+ ? Object.entries(bin === undefined ? {} : jsonObject(bin) ?? {})
449
+ .filter((entry) => Schema.is(Schema.String)(entry[1]))
450
+ : [[name, binString]];
451
+ for (const [executable] of binEntries) {
452
+ const metadata = {
453
+ ecosystem: "npm",
454
+ name,
455
+ version,
456
+ source: jsonString(packageObject.resolved) ?? `${context.sourcePath}#packages.${packagePath}`,
457
+ integrity: jsonString(packageObject.integrity),
458
+ };
459
+ records.push({
460
+ sourcePath: context.sourcePath,
461
+ location: {
462
+ kind: "field",
463
+ field: `packages.${packagePath}.bin.${executable}`,
464
+ line: lineForField(text, executable),
465
+ },
466
+ kind: "package-metadata",
467
+ invocation: [executable],
468
+ package: metadata,
469
+ confidence: version === undefined ? "strong" : "deterministic",
470
+ reviewStatus: "accepted",
471
+ });
472
+ }
473
+ }
474
+ return records;
475
+ };
476
+ const collectJsonCommands = (value, path, inheritedKind) => {
477
+ if (Array.isArray(value)) {
478
+ return value.flatMap((entry, index) => collectJsonCommands(entry, `${path}[${index}]`, inheritedKind));
479
+ }
480
+ const object = jsonObject(value);
481
+ if (object === undefined)
482
+ return [];
483
+ const lowered = path.toLowerCase();
484
+ const kind = lowered.includes("hook")
485
+ ? "hook"
486
+ : lowered.includes("mcp") || lowered.includes("server")
487
+ ? "mcp"
488
+ : inheritedKind;
489
+ const commandValue = object.command;
490
+ const args = Array.isArray(object.args)
491
+ ? object.args.filter((entry) => Schema.is(Schema.String)(entry))
492
+ : [];
493
+ const direct = [];
494
+ const commandString = jsonString(commandValue);
495
+ if (commandString !== undefined) {
496
+ direct.push({ command: [...tokenize(commandString), ...args], field: `${path}.command`, kind });
497
+ }
498
+ else if (Schema.is(JsonStringArray)(commandValue)) {
499
+ direct.push({ command: commandValue, field: `${path}.command`, kind });
500
+ }
501
+ const executable = jsonString(object.executable);
502
+ if (executable !== undefined && object.ecosystem === undefined) {
503
+ direct.push({
504
+ command: [executable, ...args],
505
+ field: `${path}.executable`,
506
+ kind: "executable-reference",
507
+ });
508
+ }
509
+ return [
510
+ ...direct,
511
+ ...Object.entries(object)
512
+ .filter(([key]) => key !== "command" && key !== "args" && key !== "executable")
513
+ .flatMap(([key, entry]) => collectJsonCommands(entry, path.length === 0 ? key : `${path}.${key}`, kind)),
514
+ ];
515
+ };
516
+ const scanJson = async (context, text) => {
517
+ let value;
518
+ try {
519
+ value = parseJsonc(text);
520
+ }
521
+ catch (cause) {
522
+ throw new DiscoveryParseError({
523
+ path: context.sourcePath,
524
+ format: "json",
525
+ reason: String(cause),
526
+ });
527
+ }
528
+ const file = basename(context.sourcePath).toLowerCase();
529
+ const packageRecords = file === "package.json"
530
+ ? scanPackageJson(context, text, value)
531
+ : file === "package-lock.json"
532
+ ? scanPackageLock(context, text, value)
533
+ : [];
534
+ const commandRecords = await Promise.all(collectJsonCommands(value, "", sourceKindFor(context.fileKind, true))
535
+ .map(async ({ command, field, kind }) => invocationEvidence(context, command, { kind: "field", field }, true, kind)));
536
+ return [...packageRecords, ...commandRecords.filter((record) => record !== undefined)];
537
+ };
538
+ const tomlString = (value) => jsonString(value);
539
+ const scanToml = async (context, text) => {
540
+ let parsed;
541
+ try {
542
+ parsed = Schema.decodeUnknownSync(Schema.MutableJson)(parseToml(text));
543
+ }
544
+ catch (cause) {
545
+ throw new DiscoveryParseError({
546
+ path: context.sourcePath,
547
+ format: "toml",
548
+ reason: String(cause),
549
+ });
550
+ }
551
+ const object = jsonObject(parsed);
552
+ if (object === undefined)
553
+ return [];
554
+ const records = [];
555
+ const file = basename(context.sourcePath).toLowerCase();
556
+ if (file === "cargo.toml") {
557
+ const packageObject = object.package === undefined ? undefined : jsonObject(object.package);
558
+ const name = tomlString(packageObject?.name);
559
+ if (name !== undefined) {
560
+ const upstream = tomlString(packageObject?.repository) ?? tomlString(packageObject?.homepage);
561
+ const version = tomlString(packageObject?.version);
562
+ const packageMetadata = {
563
+ ecosystem: "cargo",
564
+ name,
565
+ version,
566
+ source: context.sourcePath,
567
+ upstream,
568
+ };
569
+ records.push({
570
+ sourcePath: context.sourcePath,
571
+ location: { kind: "field", field: "package.name", line: lineForField(text, "name") },
572
+ kind: "package-metadata",
573
+ invocation: [name],
574
+ package: packageMetadata,
575
+ upstream,
576
+ confidence: version === undefined ? "strong" : "deterministic",
577
+ reviewStatus: "accepted",
578
+ });
579
+ }
580
+ }
581
+ if (file === "pyproject.toml") {
582
+ const project = object.project === undefined ? undefined : jsonObject(object.project);
583
+ const scripts = project?.scripts === undefined ? undefined : jsonObject(project.scripts);
584
+ const name = tomlString(project?.name);
585
+ for (const executable of Object.keys(scripts ?? {}).sort(compareText)) {
586
+ const urls = project?.urls === undefined ? undefined : jsonObject(project.urls);
587
+ const upstream = tomlString(urls?.Homepage) ?? tomlString(urls?.Repository);
588
+ const version = tomlString(project?.version);
589
+ const packageMetadata = {
590
+ ecosystem: "uv",
591
+ name: name ?? executable,
592
+ version,
593
+ source: context.sourcePath,
594
+ upstream,
595
+ };
596
+ records.push({
597
+ sourcePath: context.sourcePath,
598
+ location: { kind: "field", field: `project.scripts.${executable}`, line: lineForField(text, executable) },
599
+ kind: "package-metadata",
600
+ invocation: [executable],
601
+ package: packageMetadata,
602
+ upstream,
603
+ confidence: version === undefined ? "strong" : "deterministic",
604
+ reviewStatus: "accepted",
605
+ });
606
+ }
607
+ }
608
+ const commandRecords = await Promise.all(collectJsonCommands(parsed, "", sourceKindFor(context.fileKind, true))
609
+ .map(async ({ command, field, kind }) => invocationEvidence(context, command, { kind: "field", field }, true, kind)));
610
+ return [...records, ...commandRecords.filter((record) => record !== undefined)];
611
+ };
612
+ const scanLineMetadata = (context, text) => {
613
+ const records = [];
614
+ const file = basename(context.sourcePath).toLowerCase();
615
+ const lines = text.split(/\r?\n/u);
616
+ for (let index = 0; index < lines.length; index += 1) {
617
+ const line = lines[index] ?? "";
618
+ if (file === "brewfile") {
619
+ const match = /^\s*brew\s+["']([^"']+)["'](?:\s*,\s*version:\s*["']([^"']+)["'])?/u.exec(line);
620
+ if (match !== null) {
621
+ const packageMetadata = {
622
+ ecosystem: "homebrew",
623
+ name: match[1],
624
+ version: match[2],
625
+ source: context.sourcePath,
626
+ };
627
+ records.push({
628
+ sourcePath: context.sourcePath,
629
+ location: { kind: "line", line: index + 1 },
630
+ kind: "package-metadata",
631
+ invocation: [match[1]],
632
+ package: packageMetadata,
633
+ confidence: match[2] === undefined ? "strong" : "deterministic",
634
+ reviewStatus: "accepted",
635
+ });
636
+ }
637
+ }
638
+ const field = /^\s*(PackageIdentifier|PackageVersion|PackageUrl):\s*(.+?)\s*$/u.exec(line);
639
+ if (field !== null) {
640
+ // Winget fields are assembled after all lines have been read.
641
+ continue;
642
+ }
643
+ }
644
+ if (file.includes("winget") || extname(context.sourcePath).toLowerCase() === ".yaml" || extname(context.sourcePath).toLowerCase() === ".yml") {
645
+ const identifier = /^\s*PackageIdentifier:\s*(.+?)\s*$/mu.exec(text)?.[1];
646
+ const version = /^\s*PackageVersion:\s*(.+?)\s*$/mu.exec(text)?.[1];
647
+ const upstream = /^\s*PackageUrl:\s*(.+?)\s*$/mu.exec(text)?.[1];
648
+ if (identifier !== undefined) {
649
+ const packageMetadata = {
650
+ ecosystem: "winget",
651
+ name: identifier,
652
+ version,
653
+ source: context.sourcePath,
654
+ upstream,
655
+ };
656
+ records.push({
657
+ sourcePath: context.sourcePath,
658
+ location: {
659
+ kind: "field",
660
+ field: "PackageIdentifier",
661
+ line: text.slice(0, text.indexOf("PackageIdentifier")).split(/\r?\n/u).length,
662
+ },
663
+ kind: "package-metadata",
664
+ invocation: [identifier],
665
+ package: packageMetadata,
666
+ upstream,
667
+ confidence: version === undefined ? "strong" : "deterministic",
668
+ reviewStatus: "accepted",
669
+ });
670
+ }
671
+ }
672
+ return records;
673
+ };
674
+ const scanShell = async (context, text) => {
675
+ const records = [];
676
+ const lines = text.split(/\r?\n/u);
677
+ for (let index = 0; index < lines.length; index += 1) {
678
+ const line = lines[index]?.trim() ?? "";
679
+ if (line.length === 0 || line.startsWith("#"))
680
+ continue;
681
+ const invocations = line
682
+ .split(/\s*(?:&&|\|\||;|\|)\s*/u)
683
+ .map(tokenize)
684
+ .filter((tokens) => tokens.length > 0);
685
+ for (const invocation of invocations) {
686
+ const record = await invocationEvidence(context, invocation, { kind: "line", line: index + 1 }, true, context.fileKind === "hooks" ? "hook" : undefined);
687
+ if (record !== undefined)
688
+ records.push(record);
689
+ }
690
+ }
691
+ return records;
692
+ };
693
+ const scanOne = (file, pathValue) => Effect.gen(function* () {
694
+ const path = resolve(file.path);
695
+ const text = yield* readDiscoveryFile(path);
696
+ const fileKind = file.kind ?? inferFileKind(path);
697
+ const context = { sourcePath: path, fileKind, pathValue };
698
+ if (fileKind === "agents" || extname(path).toLowerCase() === ".md") {
699
+ const result = yield* Effect.promise(() => scanMarkdown(context, text));
700
+ return { path, ...result };
701
+ }
702
+ const extension = extname(path).toLowerCase();
703
+ if (extension === ".json" || extension === ".jsonc") {
704
+ const evidence = yield* Effect.tryPromise({
705
+ try: () => scanJson(context, text),
706
+ catch: (cause) => cause instanceof DiscoveryParseError
707
+ ? cause
708
+ : new DiscoveryParseError({ path, format: "json", reason: String(cause) }),
709
+ });
710
+ return { path, evidence, skills: [] };
711
+ }
712
+ if (extension === ".toml") {
713
+ const evidence = yield* Effect.tryPromise({
714
+ try: () => scanToml(context, text),
715
+ catch: (cause) => cause instanceof DiscoveryParseError
716
+ ? cause
717
+ : new DiscoveryParseError({ path, format: "toml", reason: String(cause) }),
718
+ });
719
+ return { path, evidence, skills: [] };
720
+ }
721
+ if ([".sh", ".bash", ".zsh", ".fish", ".ps1", ".cmd"].includes(extension)) {
722
+ const evidence = yield* Effect.promise(() => scanShell(context, text));
723
+ return { path, evidence, skills: [] };
724
+ }
725
+ return { path, evidence: scanLineMetadata(context, text), skills: [] };
726
+ });
727
+ export const scanDiscovery = (input) => {
728
+ if (input.files.length === 0) {
729
+ return Effect.fail(new InvalidDiscoveryInputError({ reason: "at least one discovery file is required" }));
730
+ }
731
+ const pathValue = input.path ?? process.env.PATH ?? "";
732
+ const files = [...input.files].sort((left, right) => compareText(resolve(left.path), resolve(right.path)));
733
+ return Effect.forEach(files, (file) => scanOne(file, pathValue), { concurrency: 4 }).pipe(Effect.map((scans) => {
734
+ const catalog = buildToolCatalog(scans.flatMap((scan) => scan.evidence), scans.flatMap((scan) => scan.skills), input.agentTaskBounds);
735
+ return {
736
+ ...catalog,
737
+ scannedPaths: scans.map((scan) => scan.path).sort(compareText),
738
+ };
739
+ }));
740
+ };