@attest-ai/core 1.0.0-rc.2 → 1.0.0-rc.4

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/dist/esm/cli.js CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { spawnSync as spawnSync2 } from "child_process";
5
- import * as fs3 from "fs";
5
+ import * as fs4 from "fs";
6
6
  import * as os2 from "os";
7
- import { fileURLToPath } from "url";
7
+ import * as path4 from "path";
8
+ import { fileURLToPath as fileURLToPath2 } from "url";
8
9
 
9
10
  // src/version.ts
10
- var VERSION = "1.0.0-rc.2";
11
- var ENGINE_VERSION = "1.0.0-rc.2";
11
+ var VERSION = "1.0.0-rc.4";
12
+ var ENGINE_VERSION = "1.0.0-rc.4";
12
13
 
13
14
  // src/engine-manager.ts
14
15
  import { spawn, spawnSync } from "child_process";
@@ -76,7 +77,7 @@ function httpsGet(url, redirectCount = 0) {
76
77
  new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
77
78
  );
78
79
  }
79
- return new Promise((resolve, reject) => {
80
+ return new Promise((resolve2, reject) => {
80
81
  const client = url.startsWith("https:") ? https : http;
81
82
  client.get(url, (res) => {
82
83
  const status = res.statusCode ?? 0;
@@ -87,7 +88,7 @@ function httpsGet(url, redirectCount = 0) {
87
88
  return;
88
89
  }
89
90
  res.resume();
90
- resolve(httpsGet(location, redirectCount + 1));
91
+ resolve2(httpsGet(location, redirectCount + 1));
91
92
  return;
92
93
  }
93
94
  if (status < 200 || status >= 300) {
@@ -97,7 +98,7 @@ function httpsGet(url, redirectCount = 0) {
97
98
  }
98
99
  const chunks = [];
99
100
  res.on("data", (chunk) => chunks.push(chunk));
100
- res.on("end", () => resolve(Buffer.concat(chunks)));
101
+ res.on("end", () => resolve2(Buffer.concat(chunks)));
101
102
  res.on("error", reject);
102
103
  }).on("error", reject);
103
104
  });
@@ -228,8 +229,8 @@ Install options:
228
229
  }
229
230
  return [await downloadEngine(), "download"];
230
231
  }
231
- async function resolveEngine(explicitPath, options = {}) {
232
- const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
232
+ async function resolveEngine(explicitPath, options2 = {}) {
233
+ const [enginePath, source] = await locateEngine(explicitPath, options2.allowDownload ?? true);
233
234
  const found = engineVersion(enginePath);
234
235
  if (found !== ENGINE_VERSION) {
235
236
  throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
@@ -288,8 +289,8 @@ not_evaluated: warn
288
289
  `;
289
290
  var GITHUB_WORKFLOW_TEMPLATE = `# Written by \`attest init --ci github\`.
290
291
  # The test step records its outcome and continues so the report is always uploaded,
291
- # the policy evaluated and the pull request annotated; the last step fails the job
292
- # when the tests failed. A policy warning (exit 2) does not fail the job.
292
+ # the JUnit file written, the policy evaluated and the pull request summarised; the last
293
+ # step fails the job when the tests failed. A policy warning (exit 2) does not fail the job.
293
294
  name: attest
294
295
 
295
296
  on:
@@ -299,11 +300,13 @@ on:
299
300
 
300
301
  permissions:
301
302
  contents: read
302
- pull-requests: write
303
303
 
304
304
  jobs:
305
305
  attest:
306
306
  runs-on: ubuntu-latest
307
+ permissions:
308
+ contents: read
309
+ pull-requests: write
307
310
  steps:
308
311
  - uses: actions/checkout@v6
309
312
  with:
@@ -317,12 +320,17 @@ jobs:
317
320
  id: tests
318
321
  continue-on-error: true
319
322
  run: npx vitest run ${REPORT_ARGS}
323
+ - name: Write the JUnit report
324
+ if: always()
325
+ run: npx attest report junit --report attest-report.json --out attest-junit.xml
320
326
  - name: Upload the Attest report
321
327
  if: always()
322
328
  uses: actions/upload-artifact@v7
323
329
  with:
324
330
  name: attest-report
325
- path: attest-report.json
331
+ path: |
332
+ attest-report.json
333
+ attest-junit.xml
326
334
  if-no-files-found: error
327
335
  - name: Evaluate the merge policy
328
336
  if: always()
@@ -336,8 +344,9 @@ jobs:
336
344
  fi
337
345
  exit "$code"
338
346
  - name: Annotate the pull request
339
- # Fork pull requests get a read-only token that cannot post reviews; the last step
340
- # still fails the job when tests failed.
347
+ # Posts one summary comment (updated on every run) and annotates failures at their
348
+ # file and line. Fork pull requests get a read-only token that cannot comment; the
349
+ # last step still fails the job when tests failed.
341
350
  if: >-
342
351
  always() && github.event_name == 'pull_request' &&
343
352
  github.event.pull_request.head.repo.full_name == github.repository
@@ -350,49 +359,264 @@ jobs:
350
359
  echo "::error::vitest failed; see the Run tests step"
351
360
  exit 1
352
361
  `;
353
- function write(file, content) {
362
+ function write(targetDir, rel, content, result, quiet) {
363
+ const file = path2.join(targetDir, ...rel.split("/"));
354
364
  if (fs2.existsSync(file)) {
355
- process.stderr.write(`Skipped (exists): ${file}
365
+ result.skipped.push(rel);
366
+ if (!quiet) process.stderr.write(`Skipped (exists): ${file}
356
367
  `);
357
368
  return;
358
369
  }
359
370
  fs2.mkdirSync(path2.dirname(file), { recursive: true });
360
371
  fs2.writeFileSync(file, content);
361
- process.stdout.write(`Created: ${file}
372
+ result.created.push(rel);
373
+ if (!quiet) process.stdout.write(`Created: ${file}
362
374
  `);
363
375
  }
364
- function scaffoldProject(targetDir, options = {}) {
365
- write(path2.join(targetDir, "tests", "my-agent.test.ts"), SAMPLE_TEST_TEMPLATE);
366
- write(path2.join(targetDir, "attest.policy.yaml"), POLICY_TEMPLATE);
367
- if (options.ci === "github") {
368
- write(path2.join(targetDir, ".github", "workflows", "attest.yml"), GITHUB_WORKFLOW_TEMPLATE);
376
+ function scaffoldProject(targetDir, options2 = {}) {
377
+ const quiet = options2.quiet ?? false;
378
+ const result = { created: [], skipped: [] };
379
+ write(targetDir, "tests/my-agent.test.ts", SAMPLE_TEST_TEMPLATE, result, quiet);
380
+ write(targetDir, "attest.policy.yaml", POLICY_TEMPLATE, result, quiet);
381
+ if (options2.ci === "github") {
382
+ write(targetDir, ".github/workflows/attest.yml", GITHUB_WORKFLOW_TEMPLATE, result, quiet);
369
383
  }
370
- process.stdout.write(
371
- `
384
+ if (!quiet) {
385
+ process.stdout.write(
386
+ `
372
387
  Next: run \`npx vitest run\`. Then change the line marked ${DEMO_MARKER} in tests/my-agent.test.ts and run it again to see a failure.
373
388
  `
374
- );
389
+ );
390
+ }
391
+ return result;
375
392
  }
376
393
 
377
- // src/cli.ts
378
- var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
379
- var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml)\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
380
- var INIT_USAGE = "usage: attest init [--ci github] (scaffolds into the current directory)";
381
- function cmdInit(args) {
382
- if (args.some((a) => HELP_FLAGS.has(a))) {
383
- process.stdout.write(`${INIT_USAGE}
384
- `);
385
- return 0;
394
+ // src/capture.ts
395
+ import { spawn as spawn2 } from "child_process";
396
+ import * as fs3 from "fs";
397
+ import * as path3 from "path";
398
+ import { fileURLToPath } from "url";
399
+ var FIXTURE_VERSION = 1;
400
+ var DEFAULT_FIXTURE_DIR = path3.join("tests", "attest", "fixtures");
401
+ var CaptureError = class extends Error {
402
+ /** The fixture written, when the engine wrote one. */
403
+ fixture;
404
+ constructor(message, fixture = null) {
405
+ super(message);
406
+ this.name = "CaptureError";
407
+ this.fixture = fixture;
386
408
  }
387
- const joined = args.join(" ");
388
- if (args.length > 0 && joined !== "--ci github" && joined !== "--ci=github") {
389
- process.stderr.write(`${INIT_USAGE}
390
- `);
391
- return 2;
409
+ };
410
+ function toPath(p) {
411
+ return p instanceof URL ? fileURLToPath(p) : p;
412
+ }
413
+ function loadFixture(file) {
414
+ const p = toPath(file);
415
+ const doc = JSON.parse(fs3.readFileSync(p, "utf8"));
416
+ if (!isObject(doc) || doc["fixture_version"] !== FIXTURE_VERSION) {
417
+ throw new CaptureError(`${p}: not a fixture_version ${FIXTURE_VERSION} capture fixture`);
392
418
  }
393
- scaffoldProject(process.cwd(), args.length > 0 ? { ci: "github" } : {});
394
- return 0;
419
+ validate(p, doc);
420
+ return doc;
421
+ }
422
+ var NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
423
+ function isObject(v) {
424
+ return typeof v === "object" && v !== null && !Array.isArray(v);
425
+ }
426
+ function kindOf(v) {
427
+ if (Array.isArray(v)) return "array";
428
+ if (isObject(v)) return "object";
429
+ if (typeof v === "string" || typeof v === "boolean") return typeof v;
430
+ return "other";
431
+ }
432
+ function validate(p, doc) {
433
+ const need = (where, obj, fields) => {
434
+ if (!isObject(obj)) throw new CaptureError(`${p}: ${where} is not an object`);
435
+ for (const [key, kind] of Object.entries(fields)) {
436
+ if (kindOf(obj[key]) !== kind) throw new CaptureError(`${p}: ${where}.${key} is missing or not a ${kind}`);
437
+ }
438
+ return obj;
439
+ };
440
+ need("fixture", doc, { name: "string", source: "object", trace: "object", exchanges: "array", contracts: "array", verification: "object" });
441
+ if (!("world" in doc)) throw new CaptureError(`${p}: fixture.world is missing`);
442
+ if (!NAME_PATTERN.test(doc["name"])) {
443
+ throw new CaptureError(`${p}: fixture name ${JSON.stringify(doc["name"])} is not a valid fixture name`);
444
+ }
445
+ const v = need("fixture.verification", doc["verification"], {
446
+ verified: "boolean",
447
+ expect_fail: "boolean",
448
+ results: "array",
449
+ expected_failures: "array"
450
+ });
451
+ doc["contracts"].forEach(
452
+ (c, i) => need(`fixture.contracts[${i}]`, c, { id: "string", kind: "string", description: "string", type: "string", spec: "object" })
453
+ );
454
+ v["results"].forEach(
455
+ (r, i) => need(`fixture.verification.results[${i}]`, r, { contract: "string", status: "string", explanation: "string" })
456
+ );
457
+ }
458
+ function objectKey(k) {
459
+ if (k === "__proto__") return `[${JSON.stringify(k)}]`;
460
+ return /^[A-Za-z_$][\w$]*$/.test(k) ? k : JSON.stringify(k);
461
+ }
462
+ function comment(text) {
463
+ return text.replace(/[\p{Cc}\p{Zl}\p{Zp}]/gu, " ");
464
+ }
465
+ function ts(value) {
466
+ if (value === null || typeof value === "boolean" || typeof value === "number") {
467
+ return JSON.stringify(value);
468
+ }
469
+ if (typeof value === "string") return JSON.stringify(value);
470
+ if (Array.isArray(value)) return `[${value.map(ts).join(", ")}]`;
471
+ if (typeof value === "object") {
472
+ const entries = Object.entries(value).map(
473
+ ([k, v]) => `${objectKey(k)}: ${ts(v)}`
474
+ );
475
+ return entries.length === 0 ? "{}" : `{ ${entries.join(", ")} }`;
476
+ }
477
+ throw new Error(`value ${String(value)} has no literal`);
478
+ }
479
+ function str(spec, key) {
480
+ const v = spec[key];
481
+ if (typeof v !== "string") throw new Error(`missing ${key}`);
482
+ return v;
483
+ }
484
+ function options(entries) {
485
+ const set = entries.filter(([, v]) => v !== void 0 && v !== null && v !== false);
486
+ return set.length === 0 ? "" : `{ ${set.map(([k, v]) => `${k}: ${ts(v)}`).join(", ")} }`;
487
+ }
488
+ function call(method, args, opts) {
489
+ return `.${method}(${[...args, ...opts ? [opts] : []].join(", ")})`;
395
490
  }
491
+ function dslCall(contract) {
492
+ const s = contract.spec;
493
+ const soft = s["soft"] === true ? true : void 0;
494
+ try {
495
+ if (contract.type === "trace") {
496
+ const check = s["check"];
497
+ if (check === "trajectory" && s["mode"] !== "partial_order") {
498
+ const mode = s["mode"] === "strict" || s["mode"] === void 0 ? void 0 : s["mode"];
499
+ if (!Array.isArray(s["tools"])) throw new Error("missing tools");
500
+ return call("trajectory", [ts(s["tools"])], options([["mode", mode], ["soft", soft]]));
501
+ }
502
+ if (check === "tool_order") {
503
+ return call("toolCalledBefore", [ts(str(s, "before")), ts(str(s, "after"))], options([["soft", soft]]));
504
+ }
505
+ if (check === "tool_cardinality") {
506
+ return call(
507
+ "toolCardinality",
508
+ [ts(str(s, "tool"))],
509
+ options([
510
+ ["per", s["per"]],
511
+ ["atLeast", s["min"]],
512
+ ["atMost", s["max"]],
513
+ ["soft", soft]
514
+ ])
515
+ );
516
+ }
517
+ }
518
+ if (contract.type === "schema" && s["target"] === "output.structured") {
519
+ if (typeof s["schema"] !== "object" || s["schema"] === null) throw new Error("missing schema");
520
+ return call("outputMatchesSchema", [ts(s["schema"])], "");
521
+ }
522
+ if (contract.type === "constraint" && s["field"] === "metadata.cost_usd" && s["operator"] === "lte") {
523
+ if (typeof s["value"] !== "number") throw new Error("missing value");
524
+ return call("costUnder", [ts(s["value"])], options([["soft", soft]]));
525
+ }
526
+ } catch (err) {
527
+ throw new Error(`contract ${contract.id}: malformed spec (${err.message})`);
528
+ }
529
+ throw new Error(`contract ${contract.id}: no TypeScript DSL call for ${contract.type} spec`);
530
+ }
531
+ function testPathFor(fixturePath) {
532
+ const name = path3.basename(fixturePath).replace(/\.attest\.json$/, "");
533
+ return path3.join(path3.dirname(path3.dirname(fixturePath)), `${name}.test.ts`);
534
+ }
535
+ function renderVitest(fixture, fixturePath, testPath) {
536
+ const calls = [];
537
+ const skipped = [];
538
+ for (const c of fixture.contracts) {
539
+ try {
540
+ calls.push([c, dslCall(c)]);
541
+ } catch (err) {
542
+ skipped.push(err.message);
543
+ }
544
+ }
545
+ const rel = path3.relative(path3.dirname(testPath), fixturePath).split(path3.sep).join("/");
546
+ const status = new Map(fixture.verification.results.map((r) => [r.contract, r.status]));
547
+ const expectFail = fixture.verification.expect_fail;
548
+ const lines = [
549
+ `// Contracts drafted by \`attest capture\` from fixture ${comment(ts(fixture.name))}.`,
550
+ "//",
551
+ "// Review and edit before committing: they check the captured trace without running",
552
+ "// the agent. Re-capturing does not overwrite this file.",
553
+ `import { attestExpect, loadFixture, type ExpectChain } from "@attest-ai/core";`,
554
+ `import { ${expectFail ? "inspect" : "evaluate"} } from "@attest-ai/vitest";`,
555
+ `import { describe, ${expectFail ? "expect, " : ""}it } from "vitest";`,
556
+ "",
557
+ `const FIXTURE = loadFixture(new URL(${ts(rel.startsWith(".") ? rel : `./${rel}`)}, import.meta.url));`
558
+ ];
559
+ if (skipped.length > 0) {
560
+ lines.push("", "// Drafted contracts without a TypeScript DSL call (left out):");
561
+ for (const s of skipped) lines.push(`// - ${comment(s)}`);
562
+ }
563
+ lines.push("", "function contracts(): ExpectChain {", " return attestExpect(FIXTURE.trace)");
564
+ for (const [c, dsl] of calls) {
565
+ lines.push(` // ${comment(`${c.id}: ${c.description}`)}`, ` ${dsl}`);
566
+ }
567
+ lines[lines.length - 1] += ";";
568
+ lines.push("}");
569
+ if (expectFail) {
570
+ lines.push("", "// Status of each contract on the captured incident, in contracts() order.");
571
+ lines.push("const EXPECTED: Array<[string, string]> = [");
572
+ for (const [c] of calls) lines.push(` [${ts(c.id)}, ${ts(status.get(c.id) ?? "pass")}],`);
573
+ lines.push(
574
+ "];",
575
+ "",
576
+ `describe(${ts(fixture.name)}, () => {`,
577
+ ` it("reproduces the incident", async () => {`,
578
+ " const result = await inspect(contracts());",
579
+ "",
580
+ " expect(result.assertionResults.map((row) => row.status)).toEqual(EXPECTED.map(([, s]) => s));",
581
+ " });",
582
+ "});"
583
+ );
584
+ } else {
585
+ lines.push(
586
+ "",
587
+ `describe(${ts(fixture.name)}, () => {`,
588
+ ` it("keeps its drafted contracts", async () => {`,
589
+ " await evaluate(contracts());",
590
+ " });",
591
+ "});"
592
+ );
593
+ }
594
+ return `${lines.join("\n")}
595
+ `;
596
+ }
597
+ function emitVitest(fixturePath) {
598
+ const testPath = testPathFor(fixturePath);
599
+ const existing = fs3.lstatSync(testPath, { throwIfNoEntry: false });
600
+ if (existing?.isSymbolicLink()) {
601
+ throw new CaptureError(`${testPath} is a symbolic link; refusing to draft a test through it`);
602
+ }
603
+ if (existing !== void 0) return null;
604
+ const source = renderVitest(loadFixture(fixturePath), fixturePath, testPath);
605
+ fs3.mkdirSync(path3.dirname(testPath), { recursive: true });
606
+ try {
607
+ fs3.writeFileSync(testPath, source, { encoding: "utf8", flag: "wx" });
608
+ } catch (err) {
609
+ if (err.code === "EEXIST") {
610
+ throw new CaptureError(`${testPath} appeared while drafting; left unchanged`);
611
+ }
612
+ throw err;
613
+ }
614
+ return testPath;
615
+ }
616
+
617
+ // src/cli.ts
618
+ var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
619
+ var SDK_USAGE = "usage: attest <command> [args]\n\nSDK commands:\n init [--ci github] [--skills] [--mcp] [--json]\n scaffold a sample test and merge policy here\n (--ci github also writes .github/workflows/attest.yml;\n --skills installs the agent skills with attest-engine;\n --mcp writes the MCP client configs (.mcp.json, .cursor/mcp.json,\n .codex/config.toml) with attest-engine;\n --json prints {created, updated, skipped} paths as JSON)\n\n`attest capture` (engine) also drafts tests/attest/<name>.test.ts from a verified\nfixture, unless that file exists.\n\noptions:\n --version print the SDK version and the engine version it requires\n --help print this help\n";
396
620
  var HELP_PROBE_TIMEOUT_MS = 1e4;
397
621
  function errorMessage(err) {
398
622
  return err instanceof Error ? err.message : String(err);
@@ -450,28 +674,291 @@ function exitStatus(status, signal) {
450
674
  if (signal !== null) return 128 + (os2.constants.signals[signal] ?? 0);
451
675
  return 1;
452
676
  }
453
- async function forward(args) {
677
+ async function engineProviding(command) {
454
678
  let engine;
455
679
  try {
456
680
  engine = await resolveEngine();
457
681
  } catch (err) {
458
682
  process.stderr.write(`attest: ${errorMessage(err)}
459
683
  `);
460
- return 1;
684
+ return null;
461
685
  }
462
- const command = args[0] ?? "";
463
686
  const commands = engineCommands(engine);
464
687
  if (commands === null || command !== "help" && !commands.has(command)) {
465
688
  process.stderr.write(
466
689
  `attest: attest-engine at ${engine} (version ${ENGINE_VERSION}) predates \`attest ${command}\`: its help does not list that command. Set ATTEST_ENGINE_PATH to an engine built from this release (make engine), or install a release whose engine provides it.
467
690
  `
468
691
  );
692
+ return null;
693
+ }
694
+ return engine;
695
+ }
696
+ var INIT_HELP_ENTRY = {
697
+ name: "init",
698
+ summary: "scaffold a sample test and policy; --ci github, --skills, --mcp, --json",
699
+ schema: "cli/init.schema.json",
700
+ exit_codes: [
701
+ { code: 0, meaning: "success" },
702
+ {
703
+ code: 1,
704
+ meaning: "--skills or --mcp could not install: the engine is missing, too old or failed"
705
+ },
706
+ { code: 2, meaning: "usage error" }
707
+ ]
708
+ };
709
+ function helpJson(engine) {
710
+ const proc = spawnSync2(engine, ["help", "--json"], {
711
+ stdio: ["ignore", "pipe", "pipe"],
712
+ encoding: "utf-8"
713
+ });
714
+ if (proc.error) throw proc.error;
715
+ if (proc.status !== 0) {
716
+ process.stderr.write(proc.stderr);
717
+ return exitStatus(proc.status, proc.signal);
718
+ }
719
+ let doc;
720
+ try {
721
+ doc = JSON.parse(proc.stdout);
722
+ if (!Array.isArray(doc.commands)) throw new Error("no commands array");
723
+ } catch (err) {
724
+ process.stderr.write(`attest: \`${engine} help --json\` printed an unexpected document: ${errorMessage(err)}
725
+ `);
469
726
  return 1;
470
727
  }
728
+ doc.commands = [...doc.commands, INIT_HELP_ENTRY].sort(
729
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
730
+ );
731
+ process.stdout.write(`${JSON.stringify(doc, null, 2)}
732
+ `);
733
+ return 0;
734
+ }
735
+ async function forward(args) {
736
+ const command = args[0] ?? "";
737
+ const engine = await engineProviding(command);
738
+ if (engine === null) return 1;
739
+ if (args.length === 2 && command === "help" && args[1] === "--json") return helpJson(engine);
740
+ if (command === "capture" && captureWritesFixture(args.slice(1))) {
741
+ return captureAndEmit(engine, args.slice(1));
742
+ }
471
743
  const proc = spawnSync2(engine, args, { stdio: "inherit" });
472
744
  if (proc.error) throw proc.error;
473
745
  return exitStatus(proc.status, proc.signal);
474
746
  }
747
+ var CAPTURE_NO_TEST = /* @__PURE__ */ new Set(["check-redaction", "serve", "h", "help"]);
748
+ function flagName(arg) {
749
+ if (!arg.startsWith("-") || arg === "-" || arg === "--") return null;
750
+ return arg.replace(/^-+/, "").split("=", 1)[0] ?? null;
751
+ }
752
+ function captureWritesFixture(args) {
753
+ for (const a of args) {
754
+ if (a === "--") break;
755
+ const name = flagName(a);
756
+ if (name !== null && CAPTURE_NO_TEST.has(name)) return false;
757
+ }
758
+ return true;
759
+ }
760
+ function userResultFile(args) {
761
+ for (const a of args) {
762
+ if (a === "--") break;
763
+ if (flagName(a) === "result-file") return true;
764
+ }
765
+ return false;
766
+ }
767
+ function captureAndEmit(engine, args) {
768
+ if (userResultFile(args)) {
769
+ process.stderr.write(
770
+ "attest: capture --result-file is set by the attest CLI; run attest-engine capture directly to choose it\n"
771
+ );
772
+ return 2;
773
+ }
774
+ const tmp = fs4.mkdtempSync(path4.join(os2.tmpdir(), "attest-capture-"));
775
+ const resultFile = path4.join(tmp, "result.json");
776
+ let result;
777
+ let code;
778
+ try {
779
+ const proc = spawnSync2(engine, ["capture", "--result-file", resultFile, ...args], { stdio: "inherit" });
780
+ if (proc.error) throw proc.error;
781
+ code = exitStatus(proc.status, proc.signal);
782
+ if (!fs4.existsSync(resultFile)) return code;
783
+ result = JSON.parse(fs4.readFileSync(resultFile, "utf8"));
784
+ } finally {
785
+ fs4.rmSync(tmp, { recursive: true, force: true });
786
+ }
787
+ const fixture = result.fixture;
788
+ if (!result.verified) {
789
+ process.stderr.write(` test: none drafted for unverified ${fixture}
790
+ `);
791
+ return code;
792
+ }
793
+ const written = emitVitest(fixture);
794
+ process.stderr.write(
795
+ written === null ? ` test: ${testPathFor(fixture)} exists; left unchanged
796
+ ` : ` test: drafted ${written}
797
+ `
798
+ );
799
+ return code;
800
+ }
801
+ var INIT_USAGE = "usage: attest init [--ci github] [--skills] [--mcp] [--json] (scaffolds into the current directory)";
802
+ var INIT_KINDS = ["created", "updated", "skipped"];
803
+ var INIT_KINDS_WITH_CONFLICT = [...INIT_KINDS, "conflict"];
804
+ var SKILLS_KINDS = [
805
+ ["created", "created"],
806
+ ["updated", "updated"],
807
+ ["unchanged", "skipped"]
808
+ ];
809
+ var MCP_KINDS = [
810
+ ["created", "created"],
811
+ ["updated", "updated"],
812
+ ["unchanged", "skipped"],
813
+ ["conflict", "conflict"]
814
+ ];
815
+ function parseInitArgs(args) {
816
+ const parsed = { ci: false, skills: false, mcp: false, json: false };
817
+ for (let i = 0; i < args.length; i++) {
818
+ const arg = args[i];
819
+ if (arg === "--ci" && args[i + 1] === "github" && !parsed.ci) {
820
+ parsed.ci = true;
821
+ i++;
822
+ } else if (arg === "--ci=github" && !parsed.ci) {
823
+ parsed.ci = true;
824
+ } else if (arg === "--skills" && !parsed.skills) {
825
+ parsed.skills = true;
826
+ } else if (arg === "--mcp" && !parsed.mcp) {
827
+ parsed.mcp = true;
828
+ } else if (arg === "--json" && !parsed.json) {
829
+ parsed.json = true;
830
+ } else {
831
+ return null;
832
+ }
833
+ }
834
+ return parsed;
835
+ }
836
+ function emptyInitResult() {
837
+ return { created: [], updated: [], skipped: [], conflict: [] };
838
+ }
839
+ function runEngineInstall(engine, subcommand, kinds, what) {
840
+ const argv = [...subcommand, "--json"];
841
+ const shown = [engine, ...argv].join(" ");
842
+ const proc = spawnSync2(engine, argv, {
843
+ stdio: ["ignore", "pipe", "pipe"],
844
+ encoding: "utf-8"
845
+ });
846
+ if (proc.error) throw proc.error;
847
+ if (proc.status !== 0) {
848
+ const detail = proc.stderr.trim();
849
+ throw new Error(
850
+ `${what} failed (\`${shown}\` exited ${String(exitStatus(proc.status, proc.signal))})` + (detail === "" ? "" : `: ${detail}`)
851
+ );
852
+ }
853
+ process.stderr.write(proc.stderr);
854
+ let doc;
855
+ try {
856
+ doc = JSON.parse(proc.stdout);
857
+ } catch (err) {
858
+ throw new Error(`\`${shown}\` printed invalid JSON: ${errorMessage(err)}`);
859
+ }
860
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
861
+ throw new Error(`\`${shown}\` printed ${JSON.stringify(doc)}, not an object`);
862
+ }
863
+ const record = doc;
864
+ const result = emptyInitResult();
865
+ for (const [engineKind, initKind] of kinds) {
866
+ const paths = record[engineKind];
867
+ if (!Array.isArray(paths) || !paths.every((p) => typeof p === "string")) {
868
+ throw new Error(`\`${shown}\` printed no \`${engineKind}\` list of paths: ${proc.stdout}`);
869
+ }
870
+ result[initKind].push(...paths);
871
+ }
872
+ return result;
873
+ }
874
+ function installSkills(engine, root) {
875
+ return runEngineInstall(engine, ["skills", "install", "--dir", root], SKILLS_KINDS, "skills install");
876
+ }
877
+ function mcpLaunchCommand(root) {
878
+ return fs4.existsSync(path4.join(root, "pnpm-lock.yaml")) ? ["pnpm", "exec", "attest", "mcp"] : ["npx", "--no", "attest", "mcp"];
879
+ }
880
+ function installMcp(engine, root) {
881
+ return runEngineInstall(
882
+ engine,
883
+ ["mcp", "install", "--dir", root, "--command", JSON.stringify(mcpLaunchCommand(root))],
884
+ MCP_KINDS,
885
+ "mcp install"
886
+ );
887
+ }
888
+ function printSkills(root, skills) {
889
+ const abs = (rel) => path4.join(root, ...rel.split("/"));
890
+ for (const rel of skills.created) process.stdout.write(`Created: ${abs(rel)}
891
+ `);
892
+ for (const rel of skills.updated) process.stdout.write(`Updated: ${abs(rel)}
893
+ `);
894
+ for (const rel of skills.skipped) process.stderr.write(`Skipped (unchanged): ${abs(rel)}
895
+ `);
896
+ for (const rel of skills.conflict) {
897
+ process.stderr.write(
898
+ `Conflict (a different attest entry, left as it is; \`attest-engine mcp install --force\` replaces it): ${abs(rel)}
899
+ `
900
+ );
901
+ }
902
+ }
903
+ async function runInit(parsed, root) {
904
+ const result = emptyInitResult();
905
+ const installs = [
906
+ [parsed.skills, "skills", installSkills],
907
+ [parsed.mcp, "mcp", installMcp]
908
+ ];
909
+ for (const [wanted, command, install] of installs) {
910
+ if (!wanted) continue;
911
+ const engine = await engineProviding(command);
912
+ if (engine === null) return null;
913
+ let installed;
914
+ try {
915
+ installed = install(engine, root);
916
+ } catch (err) {
917
+ process.stderr.write(`attest: ${errorMessage(err)}
918
+ `);
919
+ return null;
920
+ }
921
+ if (!parsed.json) printSkills(root, installed);
922
+ for (const kind of INIT_KINDS_WITH_CONFLICT) result[kind].push(...installed[kind]);
923
+ }
924
+ const scaffolded = scaffoldProject(root, {
925
+ ...parsed.ci ? { ci: "github" } : {},
926
+ quiet: parsed.json
927
+ });
928
+ result.created.push(...scaffolded.created);
929
+ result.skipped.push(...scaffolded.skipped);
930
+ return result;
931
+ }
932
+ async function cmdInit(args) {
933
+ if (args.some((a) => HELP_FLAGS.has(a))) {
934
+ process.stdout.write(`${INIT_USAGE}
935
+ `);
936
+ return 0;
937
+ }
938
+ const parsed = parseInitArgs(args);
939
+ if (parsed === null) {
940
+ process.stderr.write(`${INIT_USAGE}
941
+ `);
942
+ return 2;
943
+ }
944
+ const root = process.cwd();
945
+ if (!parsed.json) return await runInit(parsed, root) === null ? 1 : 0;
946
+ const stdoutWrite = process.stdout.write;
947
+ process.stdout.write = process.stderr.write.bind(process.stderr);
948
+ let result;
949
+ try {
950
+ result = await runInit(parsed, root);
951
+ } finally {
952
+ process.stdout.write = stdoutWrite;
953
+ }
954
+ if (result === null) return 1;
955
+ const done = result;
956
+ const kinds = parsed.mcp ? INIT_KINDS_WITH_CONFLICT : INIT_KINDS;
957
+ const sorted = Object.fromEntries(kinds.map((k) => [k, [...done[k]].sort()]));
958
+ process.stdout.write(`${JSON.stringify(sorted)}
959
+ `);
960
+ return 0;
961
+ }
475
962
  async function main(argv = process.argv.slice(2)) {
476
963
  const [command] = argv;
477
964
  if (command === void 0 || HELP_FLAGS.has(command) || command === "help" && argv.length === 1) {
@@ -495,8 +982,8 @@ ${SDK_USAGE}`);
495
982
  }
496
983
  function isDirectExecution() {
497
984
  const script = process.argv[1];
498
- if (script === void 0 || !fs3.existsSync(script)) return false;
499
- return fs3.realpathSync(script) === fs3.realpathSync(fileURLToPath(import.meta.url));
985
+ if (script === void 0 || !fs4.existsSync(script)) return false;
986
+ return fs4.realpathSync(script) === fs4.realpathSync(fileURLToPath2(import.meta.url));
500
987
  }
501
988
  if (isDirectExecution()) {
502
989
  main().then(
@@ -509,8 +996,10 @@ if (isDirectExecution()) {
509
996
  );
510
997
  }
511
998
  export {
999
+ captureWritesFixture,
512
1000
  engineCommands,
513
1001
  exitStatus,
1002
+ helpJson,
514
1003
  main
515
1004
  };
516
1005
  //# sourceMappingURL=cli.js.map