@attest-ai/core 0.6.0 → 1.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/cli.js CHANGED
@@ -1,154 +1,516 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import * as fs from "fs";
5
- import * as path from "path";
6
- import * as os from "os";
4
+ import { spawnSync as spawnSync2 } from "child_process";
5
+ import * as fs3 from "fs";
6
+ import * as os2 from "os";
7
+ import { fileURLToPath } from "url";
7
8
 
8
9
  // src/version.ts
9
- var VERSION = "0.6.0";
10
+ var VERSION = "1.0.0-rc.1";
11
+ var ENGINE_VERSION = "1.0.0-rc.1";
10
12
 
11
- // src/cli.ts
12
- function cacheDir() {
13
- const envOverride = process.env["ATTEST_CACHE_DIR"];
14
- if (envOverride) return envOverride;
15
- return path.join(os.homedir(), ".attest", "cache");
16
- }
17
- function cacheDbPath() {
18
- return path.join(cacheDir(), "attest.db");
19
- }
20
- function cmdCacheStats() {
21
- const dbPath = cacheDbPath();
22
- const exists = fs.existsSync(dbPath);
23
- let fileSize = 0;
24
- if (exists) {
25
- fileSize = fs.statSync(dbPath).size;
26
- }
27
- const stats = { exists, file_size: fileSize, path: dbPath };
28
- process.stdout.write(JSON.stringify(stats) + "\n");
29
- }
30
- function cmdCacheClear() {
31
- const dbPath = cacheDbPath();
32
- if (fs.existsSync(dbPath)) {
33
- fs.unlinkSync(dbPath);
34
- process.stdout.write(`Cleared cache: ${dbPath}
13
+ // src/engine-manager.ts
14
+ import { spawn, spawnSync } from "child_process";
15
+ import { createInterface } from "readline";
16
+ import { existsSync, accessSync, statSync as statSync2, constants as fsConstants } from "fs";
17
+ import { delimiter, join as join2 } from "path";
18
+
19
+ // src/engine-downloader.ts
20
+ import * as https from "https";
21
+ import * as http from "http";
22
+ import * as fs from "fs";
23
+ import * as path from "path";
24
+ import * as os from "os";
25
+ import * as crypto from "crypto";
26
+ var RELEASE_BASE = `https://github.com/attest-framework/attest/releases/download/v${ENGINE_VERSION}`;
27
+ var CHECKSUMS_URL = `${RELEASE_BASE}/checksums-sha256.txt`;
28
+ var MAX_REDIRECTS = 5;
29
+ function versionedBinDir() {
30
+ return path.join(os.homedir(), ".attest", "bin", ENGINE_VERSION);
31
+ }
32
+ function platformKey() {
33
+ const platform2 = os.platform();
34
+ const arch2 = os.arch();
35
+ const mapping = {
36
+ darwin: { arm64: "darwin-arm64", x64: "darwin-amd64" },
37
+ linux: { arm64: "linux-arm64", x64: "linux-amd64" },
38
+ win32: { x64: "windows-amd64", arm64: "windows-arm64" }
39
+ };
40
+ const osMap = mapping[platform2];
41
+ if (!osMap) {
42
+ throw new Error(
43
+ `Unsupported platform: ${platform2}. Supported: darwin, linux, win32.`
44
+ );
45
+ }
46
+ const key = osMap[arch2];
47
+ if (!key) {
48
+ throw new Error(
49
+ `Unsupported architecture '${arch2}' on platform '${platform2}'. Supported architectures: ${Object.keys(osMap).join(", ")}.`
50
+ );
51
+ }
52
+ return key;
53
+ }
54
+ function binaryFilename() {
55
+ return os.platform() === "win32" ? "attest-engine.exe" : "attest-engine";
56
+ }
57
+ function cachedEnginePath() {
58
+ const binPath = path.join(versionedBinDir(), binaryFilename());
59
+ return fs.statSync(binPath, { throwIfNoEntry: false })?.isFile() ? binPath : null;
60
+ }
61
+ function parseChecksums(text) {
62
+ const checksums = /* @__PURE__ */ new Map();
63
+ for (const line of text.split("\n")) {
64
+ const trimmed = line.trim();
65
+ if (!trimmed) continue;
66
+ const match = trimmed.match(/^([a-f0-9]{64})\s+(.+)$/);
67
+ if (match) {
68
+ checksums.set(match[2], match[1]);
69
+ }
70
+ }
71
+ return checksums;
72
+ }
73
+ function httpsGet(url, redirectCount = 0) {
74
+ if (redirectCount > MAX_REDIRECTS) {
75
+ return Promise.reject(
76
+ new Error(`Too many redirects (>${MAX_REDIRECTS}) following ${url}`)
77
+ );
78
+ }
79
+ return new Promise((resolve, reject) => {
80
+ const client = url.startsWith("https:") ? https : http;
81
+ client.get(url, (res) => {
82
+ const status = res.statusCode ?? 0;
83
+ if (status === 301 || status === 302) {
84
+ const location = res.headers.location;
85
+ if (!location) {
86
+ reject(new Error(`Redirect ${status} without Location header from ${url}`));
87
+ return;
88
+ }
89
+ res.resume();
90
+ resolve(httpsGet(location, redirectCount + 1));
91
+ return;
92
+ }
93
+ if (status < 200 || status >= 300) {
94
+ res.resume();
95
+ reject(new Error(`HTTP ${status} fetching ${url}`));
96
+ return;
97
+ }
98
+ const chunks = [];
99
+ res.on("data", (chunk) => chunks.push(chunk));
100
+ res.on("end", () => resolve(Buffer.concat(chunks)));
101
+ res.on("error", reject);
102
+ }).on("error", reject);
103
+ });
104
+ }
105
+ async function downloadEngine() {
106
+ const key = platformKey();
107
+ const filename = `attest-engine-${key}${os.platform() === "win32" ? ".exe" : ""}`;
108
+ const binaryUrl = `${RELEASE_BASE}/${filename}`;
109
+ process.stderr.write(`[attest] Downloading engine v${ENGINE_VERSION} for ${key}...
35
110
  `);
36
- } else {
37
- process.stdout.write(`No cache to clear: ${dbPath}
111
+ process.stderr.write(`[attest] Fetching checksums...
38
112
  `);
113
+ const checksumsText = (await httpsGet(CHECKSUMS_URL)).toString("utf-8");
114
+ const checksums = parseChecksums(checksumsText);
115
+ const expectedHash = checksums.get(filename);
116
+ if (!expectedHash) {
117
+ throw new Error(
118
+ `No checksum found for '${filename}' in checksums-sha256.txt. Available files: ${[...checksums.keys()].join(", ")}`
119
+ );
39
120
  }
121
+ process.stderr.write(`[attest] Downloading ${binaryUrl}...
122
+ `);
123
+ const data = await httpsGet(binaryUrl);
124
+ const actualHash = crypto.createHash("sha256").update(data).digest("hex");
125
+ if (actualHash !== expectedHash) {
126
+ throw new Error(
127
+ `SHA256 mismatch for ${filename}. Expected: ${expectedHash}, got: ${actualHash}. The download may be corrupted. Retry or download manually.`
128
+ );
129
+ }
130
+ process.stderr.write(`[attest] Checksum verified.
131
+ `);
132
+ const dir = versionedBinDir();
133
+ fs.mkdirSync(dir, { recursive: true });
134
+ const destPath = path.join(dir, binaryFilename());
135
+ const suffix = `${String(process.pid)}.${crypto.randomBytes(8).toString("hex")}`;
136
+ const tmpPath = path.join(dir, `.${binaryFilename()}.tmp.${suffix}`);
137
+ try {
138
+ fs.writeFileSync(tmpPath, data, { flag: "wx", mode: 493 });
139
+ fs.chmodSync(tmpPath, 493);
140
+ fs.renameSync(tmpPath, destPath);
141
+ } catch (err) {
142
+ if (err.code !== "EEXIST") fs.rmSync(tmpPath, { force: true });
143
+ throw err;
144
+ }
145
+ process.stderr.write(`[attest] Engine installed at ${destPath}
146
+ `);
147
+ return destPath;
40
148
  }
41
- var CONFTEST_TEMPLATE = `import { describe, it, expect } from "vitest";
42
- import { attestExpect, TraceBuilder } from "@attest-ai/core";
43
-
44
- describe("my agent", () => {
45
- it("returns expected output", async () => {
46
- const trace = new TraceBuilder("my-agent")
47
- .setInput({ user_message: "hello" })
48
- .setOutput({ message: "world" })
49
- .build();
50
149
 
51
- attestExpect(trace).outputContains("world");
150
+ // src/engine-manager.ts
151
+ var ENGINE_BINARY_NAME = "attest-engine";
152
+ var VERSION_PROBE_TIMEOUT_MS = 1e4;
153
+ var EngineVersionMismatchError = class extends Error {
154
+ enginePath;
155
+ source;
156
+ found;
157
+ required;
158
+ constructor(enginePath, source, found, required) {
159
+ const reported = found === null ? "reports no version" : `reports version ${found}`;
160
+ super(
161
+ `attest-engine at ${enginePath} (from ${source}) ${reported}; this SDK requires attest-engine ${required}. Point ATTEST_ENGINE_PATH at a matching engine, or remove the mismatched one from PATH so the SDK can download ${required} into ~/.attest/bin/${required}/.`
162
+ );
163
+ this.name = "EngineVersionMismatchError";
164
+ this.enginePath = enginePath;
165
+ this.source = source;
166
+ this.found = found;
167
+ this.required = required;
168
+ }
169
+ };
170
+ function engineVersion(enginePath) {
171
+ const proc = spawnSync(enginePath, ["version"], {
172
+ stdio: ["ignore", "pipe", "pipe"],
173
+ encoding: "utf-8",
174
+ timeout: VERSION_PROBE_TIMEOUT_MS
52
175
  });
176
+ if (proc.error) throw proc.error;
177
+ const out = proc.stdout.trim();
178
+ const prefix = `${ENGINE_BINARY_NAME} `;
179
+ if (proc.status !== 0 || !out.startsWith(prefix)) return null;
180
+ return out.slice(prefix.length).trim();
181
+ }
182
+ function onPath() {
183
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
184
+ if (!dir) continue;
185
+ const candidate = join2(dir, binaryFilename());
186
+ try {
187
+ accessSync(candidate, fsConstants.X_OK);
188
+ } catch {
189
+ continue;
190
+ }
191
+ if (statSync2(candidate).isFile()) return candidate;
192
+ }
193
+ return null;
194
+ }
195
+ function requireFile(enginePath, label) {
196
+ if (!existsSync(enginePath) || !statSync2(enginePath).isFile()) {
197
+ throw new Error(`${label}=${enginePath} does not exist or is not a file.`);
198
+ }
199
+ }
200
+ async function locateEngine(explicitPath, allowDownload) {
201
+ if (explicitPath) {
202
+ requireFile(explicitPath, "Engine path");
203
+ return [explicitPath, "explicit engine path"];
204
+ }
205
+ const envPath = process.env["ATTEST_ENGINE_PATH"];
206
+ if (envPath) {
207
+ requireFile(envPath, "ATTEST_ENGINE_PATH");
208
+ return [envPath, "ATTEST_ENGINE_PATH"];
209
+ }
210
+ const found = onPath();
211
+ if (found) return [found, "PATH"];
212
+ const cached = cachedEnginePath();
213
+ if (cached) return [cached, "engine cache"];
214
+ if (!allowDownload) {
215
+ throw new Error(
216
+ `No '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} in ATTEST_ENGINE_PATH, PATH or ~/.attest/bin/${ENGINE_VERSION}/.`
217
+ );
218
+ }
219
+ const noDownload = process.env["ATTEST_ENGINE_NO_DOWNLOAD"]?.toLowerCase();
220
+ if (noDownload === "1" || noDownload === "true" || noDownload === "yes") {
221
+ throw new Error(
222
+ `Cannot find '${ENGINE_BINARY_NAME}' ${ENGINE_VERSION} and ATTEST_ENGINE_NO_DOWNLOAD is set.
223
+ Install options:
224
+ 1. Build from source: make engine, then set ATTEST_ENGINE_PATH
225
+ 2. Download release v${ENGINE_VERSION}: https://github.com/attest-framework/attest/releases
226
+ 3. Allow auto-download by unsetting ATTEST_ENGINE_NO_DOWNLOAD`
227
+ );
228
+ }
229
+ return [await downloadEngine(), "download"];
230
+ }
231
+ async function resolveEngine(explicitPath, options = {}) {
232
+ const [enginePath, source] = await locateEngine(explicitPath, options.allowDownload ?? true);
233
+ const found = engineVersion(enginePath);
234
+ if (found !== ENGINE_VERSION) {
235
+ throw new EngineVersionMismatchError(enginePath, source, found, ENGINE_VERSION);
236
+ }
237
+ return enginePath;
238
+ }
239
+
240
+ // src/scaffold.ts
241
+ import * as fs2 from "fs";
242
+ import * as path2 from "path";
243
+ var DEMO_MARKER = "ATTEST-DEMO";
244
+ var SAMPLE_TEST_TEMPLATE = `// Sample Attest test written by \`attest init\`. Run it with \`npx vitest run\`.
245
+ import { it } from "vitest";
246
+ import { attestExpect, TraceBuilder, type Trace } from "@attest-ai/core";
247
+ import { evaluate } from "@attest-ai/vitest";
248
+
249
+ // The trace of one agent run, built by hand so the sample needs no model, API key
250
+ // or network. Replace it with a trace from your agent: wrap the agent with
251
+ // \`new Agent(...)\` or record it with an adapter such as OpenAIAdapter.
252
+ function myAgentResult(): Trace {
253
+ return new TraceBuilder("refund-agent")
254
+ .setInput({ user_message: "Please refund order ORD-1001." })
255
+ .addToolCall("lookup_order", {
256
+ args: { order_id: "ORD-1001" },
257
+ result: { status: "delivered", amount: 42.0 },
258
+ })
259
+ .addToolCall("issue_refund", {
260
+ args: { order_id: "ORD-1001", amount: 42.0 },
261
+ result: { refund_id: "RF-1001" },
262
+ })
263
+ .setOutput({
264
+ message: "Your refund of $42.00 has been issued.",
265
+ structured: { refund_id: "RF-1001", amount: 42.0 },
266
+ })
267
+ .setMetadata({ total_tokens: 180, cost_usd: 0.002, latency_ms: 900 })
268
+ .build();
269
+ }
270
+
271
+ it("my agent issues the refund", async (context) => {
272
+ const chain = attestExpect(myAgentResult())
273
+ .outputMatchesSchema({ type: "object", required: ["refund_id"] }) // layer 1: schema
274
+ .costUnder(0.01) // layer 2: constraint
275
+ .toolsCalledInOrder(["lookup_order", "issue_refund"]) // layer 3: trace
276
+ .outputContains("refund"); // ${DEMO_MARKER}: change "refund" to "store credit"
277
+ await evaluate(chain, { context });
53
278
  });
54
279
  `;
55
- function cmdInit(targetDir) {
56
- const testsDir = path.join(targetDir, "tests");
57
- fs.mkdirSync(testsDir, { recursive: true });
58
- process.stdout.write(`Directory: ${testsDir}
280
+ var REPORT_ARGS = "--reporter=default --reporter=@attest-ai/vitest/reporter --outputFile.attest=attest-report.json";
281
+ var POLICY_TEMPLATE = `# Merge policy for \`attest policy evaluate --report <report> --policy attest.policy.yaml\`.
282
+ # Exit 0: pass. Exit 1: a blocking rule failed. Exit 2: warnings only.
283
+ layers:
284
+ 0: # layer 0 caps every layer together
285
+ max_hard_fails: 0
286
+ # Assertions that could not run (no judge configured, simulation) warn instead of block.
287
+ not_evaluated: warn
288
+ `;
289
+ var GITHUB_WORKFLOW_TEMPLATE = `# Written by \`attest init --ci github\`.
290
+ # 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.
293
+ name: attest
294
+
295
+ on:
296
+ pull_request:
297
+ push:
298
+ branches: [main]
299
+
300
+ permissions:
301
+ contents: read
302
+ pull-requests: write
303
+
304
+ jobs:
305
+ attest:
306
+ runs-on: ubuntu-latest
307
+ steps:
308
+ - uses: actions/checkout@v6
309
+ with:
310
+ persist-credentials: false
311
+ - uses: actions/setup-node@v6
312
+ with:
313
+ node-version: 22
314
+ - name: Install dependencies
315
+ run: npm ci
316
+ - name: Run tests
317
+ id: tests
318
+ continue-on-error: true
319
+ run: npx vitest run ${REPORT_ARGS}
320
+ - name: Upload the Attest report
321
+ if: always()
322
+ uses: actions/upload-artifact@v7
323
+ with:
324
+ name: attest-report
325
+ path: attest-report.json
326
+ if-no-files-found: error
327
+ - name: Evaluate the merge policy
328
+ if: always()
329
+ run: |
330
+ set +e
331
+ npx attest policy evaluate --report attest-report.json --policy attest.policy.yaml
332
+ code=$?
333
+ if [ "$code" -eq 2 ]; then
334
+ echo "::warning::attest policy: warnings only (exit 2)"
335
+ exit 0
336
+ fi
337
+ exit "$code"
338
+ - 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.
341
+ if: >-
342
+ always() && github.event_name == 'pull_request' &&
343
+ github.event.pull_request.head.repo.full_name == github.repository
344
+ uses: attest-framework/attest/.github/actions/pr-annotate@v${VERSION}
345
+ with:
346
+ report: attest-report.json
347
+ - name: Fail the job when tests failed
348
+ if: always() && steps.tests.outcome == 'failure'
349
+ run: |
350
+ echo "::error::vitest failed; see the Run tests step"
351
+ exit 1
352
+ `;
353
+ function write(file, content) {
354
+ if (fs2.existsSync(file)) {
355
+ process.stderr.write(`Skipped (exists): ${file}
59
356
  `);
60
- const sampleTestPath = path.join(testsDir, "test_my_agent.test.ts");
61
- if (fs.existsSync(sampleTestPath)) {
62
- process.stderr.write(`Skipped (exists): ${sampleTestPath}
357
+ return;
358
+ }
359
+ fs2.mkdirSync(path2.dirname(file), { recursive: true });
360
+ fs2.writeFileSync(file, content);
361
+ process.stdout.write(`Created: ${file}
362
+ `);
363
+ }
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);
369
+ }
370
+ process.stdout.write(
371
+ `
372
+ 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
+ `
374
+ );
375
+ }
376
+
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}
63
384
  `);
64
- } else {
65
- fs.writeFileSync(sampleTestPath, CONFTEST_TEMPLATE);
66
- process.stdout.write(`Created: ${sampleTestPath}
385
+ return 0;
386
+ }
387
+ const joined = args.join(" ");
388
+ if (args.length > 0 && joined !== "--ci github" && joined !== "--ci=github") {
389
+ process.stderr.write(`${INIT_USAGE}
67
390
  `);
391
+ return 2;
68
392
  }
393
+ scaffoldProject(process.cwd(), args.length > 0 ? { ci: "github" } : {});
394
+ return 0;
395
+ }
396
+ var HELP_PROBE_TIMEOUT_MS = 1e4;
397
+ function errorMessage(err) {
398
+ return err instanceof Error ? err.message : String(err);
69
399
  }
70
- function cmdValidate(targetDir) {
71
- const testsDir = path.join(targetDir, "tests");
72
- let testFiles = [];
73
- if (fs.existsSync(testsDir)) {
74
- testFiles = fs.readdirSync(testsDir).filter((f) => f.endsWith(".test.ts") || f.endsWith(".test.js"));
400
+ function engineCommands(engine, timeoutMs = HELP_PROBE_TIMEOUT_MS) {
401
+ const proc = spawnSync2(engine, ["help"], {
402
+ stdio: ["ignore", "pipe", "pipe"],
403
+ encoding: "utf-8",
404
+ timeout: timeoutMs
405
+ });
406
+ if (proc.error || proc.status !== 0) return null;
407
+ const lines = proc.stdout.split("\n");
408
+ const start = lines.indexOf("commands:");
409
+ if (start === -1) return null;
410
+ const commands = /* @__PURE__ */ new Map();
411
+ for (const line of lines.slice(start + 1)) {
412
+ const match = /^\s*(\S+)\s*(.*)$/.exec(line);
413
+ if (match?.[1]) commands.set(match[1], (match[2] ?? "").trim());
75
414
  }
76
- if (testFiles.length === 0) {
77
- process.stderr.write(
78
- `Warning: no test files (*.test.ts / *.test.js) found in ${testsDir}.
415
+ return commands;
416
+ }
417
+ async function printHelp() {
418
+ process.stdout.write(`${SDK_USAGE}
419
+ `);
420
+ let engine;
421
+ try {
422
+ engine = await resolveEngine(void 0, { allowDownload: false });
423
+ } catch (err) {
424
+ const firstLine = (errorMessage(err).split("\n")[0] ?? "").replace(/\.$/, "");
425
+ process.stdout.write(
426
+ `engine commands: not available (${firstLine}). Run \`attest doctor\` to download attest-engine ${ENGINE_VERSION}, or set ATTEST_ENGINE_PATH.
79
427
  `
80
428
  );
81
- } else {
82
- process.stdout.write(`Found ${testFiles.length} test file(s) in ${testsDir}
83
- `);
429
+ return 0;
84
430
  }
85
- const STALE_DAYS = 30;
86
- const now = Date.now();
87
- const goldenFiles = findGoldenFiles(targetDir);
88
- for (const golden of goldenFiles) {
89
- const mtime = fs.statSync(golden).mtimeMs;
90
- const ageDays = Math.floor((now - mtime) / (1e3 * 60 * 60 * 24));
91
- if (ageDays > STALE_DAYS) {
92
- process.stderr.write(
93
- `Warning: golden trace ${golden} is ${ageDays} days old (>${STALE_DAYS} days). Consider regenerating.
431
+ const commands = engineCommands(engine);
432
+ if (commands === null) {
433
+ process.stdout.write(
434
+ `engine commands: not available (attest-engine at ${engine} lists no commands; it predates the command registry).
94
435
  `
95
- );
96
- }
436
+ );
437
+ return 0;
97
438
  }
98
- }
99
- function findGoldenFiles(dir) {
100
- const results = [];
101
- if (!fs.existsSync(dir)) return results;
102
- const entries = fs.readdirSync(dir, { withFileTypes: true });
103
- for (const entry of entries) {
104
- const fullPath = path.join(dir, entry.name);
105
- if (entry.isDirectory() && entry.name !== "node_modules") {
106
- results.push(...findGoldenFiles(fullPath));
107
- } else if (entry.isFile() && entry.name.endsWith(".golden")) {
108
- results.push(fullPath);
109
- }
439
+ const width = Math.max(0, ...[...commands.keys()].map((name) => name.length));
440
+ process.stdout.write(`engine commands (attest-engine ${ENGINE_VERSION}, ${engine}):
441
+ `);
442
+ for (const [name, summary] of commands) {
443
+ process.stdout.write(` ${name.padEnd(width)} ${summary}
444
+ `);
110
445
  }
111
- return results;
446
+ return 0;
447
+ }
448
+ function exitStatus(status, signal) {
449
+ if (status !== null) return status;
450
+ if (signal !== null) return 128 + (os2.constants.signals[signal] ?? 0);
451
+ return 1;
112
452
  }
113
- function main(argv = process.argv.slice(2)) {
114
- const args = argv;
115
- if (args[0] === "--version") {
116
- process.stdout.write(`attest ${VERSION}
453
+ async function forward(args) {
454
+ let engine;
455
+ try {
456
+ engine = await resolveEngine();
457
+ } catch (err) {
458
+ process.stderr.write(`attest: ${errorMessage(err)}
117
459
  `);
118
- process.exit(0);
119
- }
120
- if (args[0] === "cache" && args.length >= 2) {
121
- if (args[1] === "stats") {
122
- cmdCacheStats();
123
- process.exit(0);
124
- } else if (args[1] === "clear") {
125
- cmdCacheClear();
126
- process.exit(0);
127
- } else {
128
- process.stderr.write(`Unknown cache subcommand: ${args[1]}
460
+ return 1;
461
+ }
462
+ const command = args[0] ?? "";
463
+ const commands = engineCommands(engine);
464
+ if (commands === null || command !== "help" && !commands.has(command)) {
465
+ process.stderr.write(
466
+ `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
+ `
468
+ );
469
+ return 1;
470
+ }
471
+ const proc = spawnSync2(engine, args, { stdio: "inherit" });
472
+ if (proc.error) throw proc.error;
473
+ return exitStatus(proc.status, proc.signal);
474
+ }
475
+ async function main(argv = process.argv.slice(2)) {
476
+ const [command] = argv;
477
+ if (command === void 0 || HELP_FLAGS.has(command) || command === "help" && argv.length === 1) {
478
+ return printHelp();
479
+ }
480
+ if (command === "--version") {
481
+ process.stdout.write(`attest ${VERSION} (requires attest-engine ${ENGINE_VERSION})
129
482
  `);
130
- process.stderr.write("Available: cache stats, cache clear\n");
131
- process.exit(1);
132
- }
483
+ return 0;
133
484
  }
134
- if (args[0] === "init") {
135
- cmdInit(process.cwd());
136
- process.exit(0);
485
+ if (command === "init") {
486
+ return cmdInit(argv.slice(1));
137
487
  }
138
- if (args[0] === "validate") {
139
- cmdValidate(process.cwd());
140
- process.exit(0);
488
+ if (command.startsWith("-")) {
489
+ process.stderr.write(`attest: unknown option ${command}
490
+
491
+ ${SDK_USAGE}`);
492
+ return 2;
141
493
  }
142
- process.stderr.write(
143
- "Usage: attest <command>\n\nCommands:\n --version Print version\n init Scaffold a test project\n validate Validate test suite\n cache stats Show cache statistics\n cache clear Clear cache database\n"
144
- );
145
- process.exit(args.length === 0 ? 0 : 1);
494
+ return forward(argv);
495
+ }
496
+ function isDirectExecution() {
497
+ 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));
146
500
  }
147
- var isDirectExecution = typeof import.meta.url === "string" && import.meta.url.startsWith("file:") && process.argv[1] !== void 0 && import.meta.url.endsWith(path.basename(process.argv[1]));
148
- if (isDirectExecution) {
149
- main();
501
+ if (isDirectExecution()) {
502
+ main().then(
503
+ (code) => process.exit(code),
504
+ (err) => {
505
+ process.stderr.write(`attest: ${errorMessage(err)}
506
+ `);
507
+ process.exit(1);
508
+ }
509
+ );
150
510
  }
151
511
  export {
512
+ engineCommands,
513
+ exitStatus,
152
514
  main
153
515
  };
154
516
  //# sourceMappingURL=cli.js.map