@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/cjs/index.cjs +890 -127
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +158 -16
- package/dist/cjs/proto/index.cjs +117 -0
- package/dist/cjs/proto/index.cjs.map +1 -1
- package/dist/cjs/proto/index.d.cts +135 -1
- package/dist/esm/cli.d.ts +19 -4
- package/dist/esm/cli.js +478 -116
- package/dist/esm/cli.js.map +1 -1
- package/dist/esm/index.d.ts +158 -16
- package/dist/esm/index.js +870 -128
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/proto/index.d.ts +135 -1
- package/dist/esm/proto/index.js +103 -0
- package/dist/esm/proto/index.js.map +1 -1
- package/package.json +4 -3
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
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
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.
|
|
10
|
+
var VERSION = "1.0.0-rc.1";
|
|
11
|
+
var ENGINE_VERSION = "1.0.0-rc.1";
|
|
10
12
|
|
|
11
|
-
// src/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
function
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
process.stdout.write(`Found ${testFiles.length} test file(s) in ${testsDir}
|
|
83
|
-
`);
|
|
429
|
+
return 0;
|
|
84
430
|
}
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
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
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
131
|
-
process.exit(1);
|
|
132
|
-
}
|
|
483
|
+
return 0;
|
|
133
484
|
}
|
|
134
|
-
if (
|
|
135
|
-
cmdInit(
|
|
136
|
-
process.exit(0);
|
|
485
|
+
if (command === "init") {
|
|
486
|
+
return cmdInit(argv.slice(1));
|
|
137
487
|
}
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
488
|
+
if (command.startsWith("-")) {
|
|
489
|
+
process.stderr.write(`attest: unknown option ${command}
|
|
490
|
+
|
|
491
|
+
${SDK_USAGE}`);
|
|
492
|
+
return 2;
|
|
141
493
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|