@attest-ai/core 1.0.0-rc.3 → 1.0.0-rc.5

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
@@ -8,8 +8,8 @@ import * as path4 from "path";
8
8
  import { fileURLToPath as fileURLToPath2 } from "url";
9
9
 
10
10
  // src/version.ts
11
- var VERSION = "1.0.0-rc.3";
12
- var ENGINE_VERSION = "1.0.0-rc.3";
11
+ var VERSION = "1.0.0-rc.5";
12
+ var ENGINE_VERSION = "1.0.0-rc.5";
13
13
 
14
14
  // src/engine-manager.ts
15
15
  import { spawn, spawnSync } from "child_process";
@@ -319,10 +319,17 @@ jobs:
319
319
  - name: Run tests
320
320
  id: tests
321
321
  continue-on-error: true
322
+ # Traces (prompts, outputs, tool args; not redacted) travel in the artifact; on a public
323
+ # repository any signed-in GitHub user can download it. Enable on private repositories only:
324
+ # env:
325
+ # ATTEST_REPORT_TRACES: "1"
322
326
  run: npx vitest run ${REPORT_ARGS}
323
327
  - name: Write the JUnit report
324
328
  if: always()
325
329
  run: npx attest report junit --report attest-report.json --out attest-junit.xml
330
+ - name: Write the failure viewer
331
+ if: always()
332
+ run: npx attest view attest-report.json --no-open --out attest-report.html
326
333
  - name: Upload the Attest report
327
334
  if: always()
328
335
  uses: actions/upload-artifact@v7
@@ -331,6 +338,7 @@ jobs:
331
338
  path: |
332
339
  attest-report.json
333
340
  attest-junit.xml
341
+ attest-report.html
334
342
  if-no-files-found: error
335
343
  - name: Evaluate the merge policy
336
344
  if: always()
@@ -359,28 +367,36 @@ jobs:
359
367
  echo "::error::vitest failed; see the Run tests step"
360
368
  exit 1
361
369
  `;
362
- function write(file, content) {
370
+ function write(targetDir, rel, content, result, quiet) {
371
+ const file = path2.join(targetDir, ...rel.split("/"));
363
372
  if (fs2.existsSync(file)) {
364
- process.stderr.write(`Skipped (exists): ${file}
373
+ result.skipped.push(rel);
374
+ if (!quiet) process.stderr.write(`Skipped (exists): ${file}
365
375
  `);
366
376
  return;
367
377
  }
368
378
  fs2.mkdirSync(path2.dirname(file), { recursive: true });
369
379
  fs2.writeFileSync(file, content);
370
- process.stdout.write(`Created: ${file}
380
+ result.created.push(rel);
381
+ if (!quiet) process.stdout.write(`Created: ${file}
371
382
  `);
372
383
  }
373
384
  function scaffoldProject(targetDir, options2 = {}) {
374
- write(path2.join(targetDir, "tests", "my-agent.test.ts"), SAMPLE_TEST_TEMPLATE);
375
- write(path2.join(targetDir, "attest.policy.yaml"), POLICY_TEMPLATE);
385
+ const quiet = options2.quiet ?? false;
386
+ const result = { created: [], skipped: [] };
387
+ write(targetDir, "tests/my-agent.test.ts", SAMPLE_TEST_TEMPLATE, result, quiet);
388
+ write(targetDir, "attest.policy.yaml", POLICY_TEMPLATE, result, quiet);
376
389
  if (options2.ci === "github") {
377
- write(path2.join(targetDir, ".github", "workflows", "attest.yml"), GITHUB_WORKFLOW_TEMPLATE);
390
+ write(targetDir, ".github/workflows/attest.yml", GITHUB_WORKFLOW_TEMPLATE, result, quiet);
378
391
  }
379
- process.stdout.write(
380
- `
392
+ if (!quiet) {
393
+ process.stdout.write(
394
+ `
381
395
  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.
382
396
  `
383
- );
397
+ );
398
+ }
399
+ return result;
384
400
  }
385
401
 
386
402
  // src/capture.ts
@@ -608,23 +624,7 @@ function emitVitest(fixturePath) {
608
624
 
609
625
  // src/cli.ts
610
626
  var HELP_FLAGS = /* @__PURE__ */ new Set(["-h", "--help"]);
611
- 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\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";
612
- var INIT_USAGE = "usage: attest init [--ci github] (scaffolds into the current directory)";
613
- function cmdInit(args) {
614
- if (args.some((a) => HELP_FLAGS.has(a))) {
615
- process.stdout.write(`${INIT_USAGE}
616
- `);
617
- return 0;
618
- }
619
- const joined = args.join(" ");
620
- if (args.length > 0 && joined !== "--ci github" && joined !== "--ci=github") {
621
- process.stderr.write(`${INIT_USAGE}
622
- `);
623
- return 2;
624
- }
625
- scaffoldProject(process.cwd(), args.length > 0 ? { ci: "github" } : {});
626
- return 0;
627
- }
627
+ 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";
628
628
  var HELP_PROBE_TIMEOUT_MS = 1e4;
629
629
  function errorMessage(err) {
630
630
  return err instanceof Error ? err.message : String(err);
@@ -682,24 +682,69 @@ function exitStatus(status, signal) {
682
682
  if (signal !== null) return 128 + (os2.constants.signals[signal] ?? 0);
683
683
  return 1;
684
684
  }
685
- async function forward(args) {
685
+ async function engineProviding(command) {
686
686
  let engine;
687
687
  try {
688
688
  engine = await resolveEngine();
689
689
  } catch (err) {
690
690
  process.stderr.write(`attest: ${errorMessage(err)}
691
691
  `);
692
- return 1;
692
+ return null;
693
693
  }
694
- const command = args[0] ?? "";
695
694
  const commands = engineCommands(engine);
696
695
  if (commands === null || command !== "help" && !commands.has(command)) {
697
696
  process.stderr.write(
698
697
  `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.
699
698
  `
700
699
  );
700
+ return null;
701
+ }
702
+ return engine;
703
+ }
704
+ var INIT_HELP_ENTRY = {
705
+ name: "init",
706
+ summary: "scaffold a sample test and policy; --ci github, --skills, --mcp, --json",
707
+ schema: "cli/init.schema.json",
708
+ exit_codes: [
709
+ { code: 0, meaning: "success" },
710
+ {
711
+ code: 1,
712
+ meaning: "--skills or --mcp could not install: the engine is missing, too old or failed"
713
+ },
714
+ { code: 2, meaning: "usage error" }
715
+ ]
716
+ };
717
+ function helpJson(engine) {
718
+ const proc = spawnSync2(engine, ["help", "--json"], {
719
+ stdio: ["ignore", "pipe", "pipe"],
720
+ encoding: "utf-8"
721
+ });
722
+ if (proc.error) throw proc.error;
723
+ if (proc.status !== 0) {
724
+ process.stderr.write(proc.stderr);
725
+ return exitStatus(proc.status, proc.signal);
726
+ }
727
+ let doc;
728
+ try {
729
+ doc = JSON.parse(proc.stdout);
730
+ if (!Array.isArray(doc.commands)) throw new Error("no commands array");
731
+ } catch (err) {
732
+ process.stderr.write(`attest: \`${engine} help --json\` printed an unexpected document: ${errorMessage(err)}
733
+ `);
701
734
  return 1;
702
735
  }
736
+ doc.commands = [...doc.commands, INIT_HELP_ENTRY].sort(
737
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
738
+ );
739
+ process.stdout.write(`${JSON.stringify(doc, null, 2)}
740
+ `);
741
+ return 0;
742
+ }
743
+ async function forward(args) {
744
+ const command = args[0] ?? "";
745
+ const engine = await engineProviding(command);
746
+ if (engine === null) return 1;
747
+ if (args.length === 2 && command === "help" && args[1] === "--json") return helpJson(engine);
703
748
  if (command === "capture" && captureWritesFixture(args.slice(1))) {
704
749
  return captureAndEmit(engine, args.slice(1));
705
750
  }
@@ -761,6 +806,167 @@ function captureAndEmit(engine, args) {
761
806
  );
762
807
  return code;
763
808
  }
809
+ var INIT_USAGE = "usage: attest init [--ci github] [--skills] [--mcp] [--json] (scaffolds into the current directory)";
810
+ var INIT_KINDS = ["created", "updated", "skipped"];
811
+ var INIT_KINDS_WITH_CONFLICT = [...INIT_KINDS, "conflict"];
812
+ var SKILLS_KINDS = [
813
+ ["created", "created"],
814
+ ["updated", "updated"],
815
+ ["unchanged", "skipped"]
816
+ ];
817
+ var MCP_KINDS = [
818
+ ["created", "created"],
819
+ ["updated", "updated"],
820
+ ["unchanged", "skipped"],
821
+ ["conflict", "conflict"]
822
+ ];
823
+ function parseInitArgs(args) {
824
+ const parsed = { ci: false, skills: false, mcp: false, json: false };
825
+ for (let i = 0; i < args.length; i++) {
826
+ const arg = args[i];
827
+ if (arg === "--ci" && args[i + 1] === "github" && !parsed.ci) {
828
+ parsed.ci = true;
829
+ i++;
830
+ } else if (arg === "--ci=github" && !parsed.ci) {
831
+ parsed.ci = true;
832
+ } else if (arg === "--skills" && !parsed.skills) {
833
+ parsed.skills = true;
834
+ } else if (arg === "--mcp" && !parsed.mcp) {
835
+ parsed.mcp = true;
836
+ } else if (arg === "--json" && !parsed.json) {
837
+ parsed.json = true;
838
+ } else {
839
+ return null;
840
+ }
841
+ }
842
+ return parsed;
843
+ }
844
+ function emptyInitResult() {
845
+ return { created: [], updated: [], skipped: [], conflict: [] };
846
+ }
847
+ function runEngineInstall(engine, subcommand, kinds, what) {
848
+ const argv = [...subcommand, "--json"];
849
+ const shown = [engine, ...argv].join(" ");
850
+ const proc = spawnSync2(engine, argv, {
851
+ stdio: ["ignore", "pipe", "pipe"],
852
+ encoding: "utf-8"
853
+ });
854
+ if (proc.error) throw proc.error;
855
+ if (proc.status !== 0) {
856
+ const detail = proc.stderr.trim();
857
+ throw new Error(
858
+ `${what} failed (\`${shown}\` exited ${String(exitStatus(proc.status, proc.signal))})` + (detail === "" ? "" : `: ${detail}`)
859
+ );
860
+ }
861
+ process.stderr.write(proc.stderr);
862
+ let doc;
863
+ try {
864
+ doc = JSON.parse(proc.stdout);
865
+ } catch (err) {
866
+ throw new Error(`\`${shown}\` printed invalid JSON: ${errorMessage(err)}`);
867
+ }
868
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
869
+ throw new Error(`\`${shown}\` printed ${JSON.stringify(doc)}, not an object`);
870
+ }
871
+ const record = doc;
872
+ const result = emptyInitResult();
873
+ for (const [engineKind, initKind] of kinds) {
874
+ const paths = record[engineKind];
875
+ if (!Array.isArray(paths) || !paths.every((p) => typeof p === "string")) {
876
+ throw new Error(`\`${shown}\` printed no \`${engineKind}\` list of paths: ${proc.stdout}`);
877
+ }
878
+ result[initKind].push(...paths);
879
+ }
880
+ return result;
881
+ }
882
+ function installSkills(engine, root) {
883
+ return runEngineInstall(engine, ["skills", "install", "--dir", root], SKILLS_KINDS, "skills install");
884
+ }
885
+ function mcpLaunchCommand(root) {
886
+ return fs4.existsSync(path4.join(root, "pnpm-lock.yaml")) ? ["pnpm", "exec", "attest", "mcp"] : ["npx", "--no", "attest", "mcp"];
887
+ }
888
+ function installMcp(engine, root) {
889
+ return runEngineInstall(
890
+ engine,
891
+ ["mcp", "install", "--dir", root, "--command", JSON.stringify(mcpLaunchCommand(root))],
892
+ MCP_KINDS,
893
+ "mcp install"
894
+ );
895
+ }
896
+ function printSkills(root, skills) {
897
+ const abs = (rel) => path4.join(root, ...rel.split("/"));
898
+ for (const rel of skills.created) process.stdout.write(`Created: ${abs(rel)}
899
+ `);
900
+ for (const rel of skills.updated) process.stdout.write(`Updated: ${abs(rel)}
901
+ `);
902
+ for (const rel of skills.skipped) process.stderr.write(`Skipped (unchanged): ${abs(rel)}
903
+ `);
904
+ for (const rel of skills.conflict) {
905
+ process.stderr.write(
906
+ `Conflict (a different attest entry, left as it is; \`attest-engine mcp install --force\` replaces it): ${abs(rel)}
907
+ `
908
+ );
909
+ }
910
+ }
911
+ async function runInit(parsed, root) {
912
+ const result = emptyInitResult();
913
+ const installs = [
914
+ [parsed.skills, "skills", installSkills],
915
+ [parsed.mcp, "mcp", installMcp]
916
+ ];
917
+ for (const [wanted, command, install] of installs) {
918
+ if (!wanted) continue;
919
+ const engine = await engineProviding(command);
920
+ if (engine === null) return null;
921
+ let installed;
922
+ try {
923
+ installed = install(engine, root);
924
+ } catch (err) {
925
+ process.stderr.write(`attest: ${errorMessage(err)}
926
+ `);
927
+ return null;
928
+ }
929
+ if (!parsed.json) printSkills(root, installed);
930
+ for (const kind of INIT_KINDS_WITH_CONFLICT) result[kind].push(...installed[kind]);
931
+ }
932
+ const scaffolded = scaffoldProject(root, {
933
+ ...parsed.ci ? { ci: "github" } : {},
934
+ quiet: parsed.json
935
+ });
936
+ result.created.push(...scaffolded.created);
937
+ result.skipped.push(...scaffolded.skipped);
938
+ return result;
939
+ }
940
+ async function cmdInit(args) {
941
+ if (args.some((a) => HELP_FLAGS.has(a))) {
942
+ process.stdout.write(`${INIT_USAGE}
943
+ `);
944
+ return 0;
945
+ }
946
+ const parsed = parseInitArgs(args);
947
+ if (parsed === null) {
948
+ process.stderr.write(`${INIT_USAGE}
949
+ `);
950
+ return 2;
951
+ }
952
+ const root = process.cwd();
953
+ if (!parsed.json) return await runInit(parsed, root) === null ? 1 : 0;
954
+ const stdoutWrite = process.stdout.write;
955
+ process.stdout.write = process.stderr.write.bind(process.stderr);
956
+ let result;
957
+ try {
958
+ result = await runInit(parsed, root);
959
+ } finally {
960
+ process.stdout.write = stdoutWrite;
961
+ }
962
+ if (result === null) return 1;
963
+ const done = result;
964
+ const kinds = parsed.mcp ? INIT_KINDS_WITH_CONFLICT : INIT_KINDS;
965
+ const sorted = Object.fromEntries(kinds.map((k) => [k, [...done[k]].sort()]));
966
+ process.stdout.write(`${JSON.stringify(sorted)}
967
+ `);
968
+ return 0;
969
+ }
764
970
  async function main(argv = process.argv.slice(2)) {
765
971
  const [command] = argv;
766
972
  if (command === void 0 || HELP_FLAGS.has(command) || command === "help" && argv.length === 1) {
@@ -801,6 +1007,7 @@ export {
801
1007
  captureWritesFixture,
802
1008
  engineCommands,
803
1009
  exitStatus,
1010
+ helpJson,
804
1011
  main
805
1012
  };
806
1013
  //# sourceMappingURL=cli.js.map