@aexhq/cli 0.25.1 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/dist/index.d.ts +2 -7
- package/dist/index.js +233 -7
- package/package.json +26 -29
- package/LICENSE +0 -201
- package/dist/cli.d.ts +0 -1
- package/dist/cli.js +0 -51
- package/dist/cli.js.map +0 -1
- package/dist/cli.mjs +0 -4657
- package/dist/cli.mjs.sha256 +0 -1
- package/dist/host/cancel.d.ts +0 -3
- package/dist/host/cancel.js +0 -30
- package/dist/host/cancel.js.map +0 -1
- package/dist/host/common.d.ts +0 -135
- package/dist/host/common.js +0 -281
- package/dist/host/common.js.map +0 -1
- package/dist/host/delete-asset.d.ts +0 -3
- package/dist/host/delete-asset.js +0 -34
- package/dist/host/delete-asset.js.map +0 -1
- package/dist/host/delete.d.ts +0 -3
- package/dist/host/delete.js +0 -30
- package/dist/host/delete.js.map +0 -1
- package/dist/host/download.d.ts +0 -3
- package/dist/host/download.js +0 -80
- package/dist/host/download.js.map +0 -1
- package/dist/host/events.d.ts +0 -3
- package/dist/host/events.js +0 -97
- package/dist/host/events.js.map +0 -1
- package/dist/host/index.d.ts +0 -17
- package/dist/host/index.js +0 -18
- package/dist/host/index.js.map +0 -1
- package/dist/host/outputs.d.ts +0 -3
- package/dist/host/outputs.js +0 -36
- package/dist/host/outputs.js.map +0 -1
- package/dist/host/run-cmd.d.ts +0 -3
- package/dist/host/run-cmd.js +0 -549
- package/dist/host/run-cmd.js.map +0 -1
- package/dist/host/skills-cmd.d.ts +0 -3
- package/dist/host/skills-cmd.js +0 -304
- package/dist/host/skills-cmd.js.map +0 -1
- package/dist/host/status.d.ts +0 -3
- package/dist/host/status.js +0 -31
- package/dist/host/status.js.map +0 -1
- package/dist/host/wait.d.ts +0 -3
- package/dist/host/wait.js +0 -88
- package/dist/host/wait.js.map +0 -1
- package/dist/host/whoami.d.ts +0 -3
- package/dist/host/whoami.js +0 -33
- package/dist/host/whoami.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.d.ts +0 -53
- package/dist/internal.js +0 -10
- package/dist/internal.js.map +0 -1
- package/dist/outputs-sync.d.ts +0 -24
- package/dist/outputs-sync.js +0 -70
- package/dist/outputs-sync.js.map +0 -1
- package/dist/proxy.d.ts +0 -16
- package/dist/proxy.js +0 -316
- package/dist/proxy.js.map +0 -1
- package/dist/run.d.ts +0 -10
- package/dist/run.js +0 -166
- package/dist/run.js.map +0 -1
- package/dist/runtime.d.ts +0 -5
- package/dist/runtime.js +0 -4
- package/dist/runtime.js.map +0 -1
- package/scripts/finalize-bundle.mjs +0 -72
- package/src/cli.ts +0 -51
- package/src/host/cancel.ts +0 -39
- package/src/host/common.ts +0 -329
- package/src/host/delete-asset.ts +0 -43
- package/src/host/delete.ts +0 -39
- package/src/host/download.ts +0 -97
- package/src/host/events.ts +0 -113
- package/src/host/index.ts +0 -25
- package/src/host/outputs.ts +0 -45
- package/src/host/run-cmd.ts +0 -578
- package/src/host/skills-cmd.ts +0 -317
- package/src/host/status.ts +0 -40
- package/src/host/wait.ts +0 -110
- package/src/host/whoami.ts +0 -42
- package/src/index.ts +0 -7
- package/src/internal.ts +0 -63
- package/src/outputs-sync.ts +0 -76
- package/src/proxy.ts +0 -359
- package/src/run.ts +0 -187
- package/src/runtime.ts +0 -15
- package/test/cli.test.ts +0 -519
- package/test/host.test.ts +0 -1154
- package/test/no-env-vars.test.ts +0 -59
- package/test/outputs-sync.test.ts +0 -107
- package/tsconfig.build.json +0 -9
- package/tsconfig.json +0 -4
- package/vitest.config.ts +0 -14
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// Finalize the CLI bundle:
|
|
2
|
-
// 1. esbuild bundles dist/cli.js (the tsc-built entrypoint) and its
|
|
3
|
-
// workspace deps into a single ESM file.
|
|
4
|
-
// 2. Prepend the Node shebang.
|
|
5
|
-
// 3. Write to dist/cli.mjs so the `bin` field works on Unix and the
|
|
6
|
-
// ESM extension is unambiguous.
|
|
7
|
-
// 4. Compute a sha256 digest and write a sibling cli.mjs.sha256
|
|
8
|
-
// used by release/package integrity checks.
|
|
9
|
-
//
|
|
10
|
-
// The published SDK copies ONLY dist/cli.mjs as the user-facing `aex`
|
|
11
|
-
// bin. Managed run internals use the separate runtime bridge artifact, so
|
|
12
|
-
// this host-install bundle stays independent of platform injection.
|
|
13
|
-
import { build } from "esbuild";
|
|
14
|
-
import { readFile, writeFile, chmod } from "node:fs/promises";
|
|
15
|
-
import { createHash } from "node:crypto";
|
|
16
|
-
import { dirname, resolve } from "node:path";
|
|
17
|
-
import { fileURLToPath } from "node:url";
|
|
18
|
-
|
|
19
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
20
|
-
const pkgRoot = resolve(here, "..");
|
|
21
|
-
const distDir = resolve(pkgRoot, "dist");
|
|
22
|
-
const inputPath = resolve(distDir, "cli.js");
|
|
23
|
-
const outputPath = resolve(distDir, "cli.mjs");
|
|
24
|
-
const digestPath = resolve(distDir, "cli.mjs.sha256");
|
|
25
|
-
|
|
26
|
-
const result = await build({
|
|
27
|
-
entryPoints: [inputPath],
|
|
28
|
-
bundle: true,
|
|
29
|
-
platform: "node",
|
|
30
|
-
format: "esm",
|
|
31
|
-
target: "node20",
|
|
32
|
-
write: false,
|
|
33
|
-
// Mark node built-ins external; we only bundle our own + workspace deps.
|
|
34
|
-
external: [
|
|
35
|
-
"node:*",
|
|
36
|
-
"fs",
|
|
37
|
-
"fs/promises",
|
|
38
|
-
"path",
|
|
39
|
-
"url",
|
|
40
|
-
"crypto",
|
|
41
|
-
"buffer",
|
|
42
|
-
"stream",
|
|
43
|
-
"util"
|
|
44
|
-
],
|
|
45
|
-
// The bundle is loaded as ESM at runtime; suppress sourcemaps so the
|
|
46
|
-
// shipped artifact stays minimal and free of file-path leakage.
|
|
47
|
-
sourcemap: false,
|
|
48
|
-
legalComments: "none",
|
|
49
|
-
minify: false
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
if (result.outputFiles.length !== 1) {
|
|
53
|
-
throw new Error(`unexpected esbuild output count: ${result.outputFiles.length}`);
|
|
54
|
-
}
|
|
55
|
-
const bundled = result.outputFiles[0].text;
|
|
56
|
-
|
|
57
|
-
const shebang = "#!/usr/bin/env node\n";
|
|
58
|
-
const final = shebang + bundled;
|
|
59
|
-
await writeFile(outputPath, final, "utf8");
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
// mode 0755; harmless on Windows.
|
|
63
|
-
await chmod(outputPath, 0o755);
|
|
64
|
-
} catch {
|
|
65
|
-
/* ignore */
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const digest = createHash("sha256").update(final).digest("hex");
|
|
69
|
-
await writeFile(digestPath, `${digest} cli.mjs\n`, "utf8");
|
|
70
|
-
|
|
71
|
-
console.log(`built ${outputPath} (${final.length} bytes)`);
|
|
72
|
-
console.log(`sha256 ${digest}`);
|
package/src/cli.ts
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* aex CLI entrypoint. Wires the real IO surface and calls
|
|
3
|
-
* {@link runCli}. The shipped bundle has a `#!/usr/bin/env node` line
|
|
4
|
-
* prepended by `scripts/finalize-bundle.mjs`.
|
|
5
|
-
*
|
|
6
|
-
* NO `process.env.AEX_*` reads here — paths to the manifest and
|
|
7
|
-
* token file are fixed constants in `internal.ts`. The mechanical
|
|
8
|
-
* enforcement test in Phase 9 greps the built artifact to confirm.
|
|
9
|
-
*/
|
|
10
|
-
import { readFile, writeFile, readdir, stat } from "node:fs/promises";
|
|
11
|
-
import { resolve as resolvePath } from "node:path";
|
|
12
|
-
import { runCli } from "./run.js";
|
|
13
|
-
import type { CliIO, OutputsSyncFileEntry } from "./internal.js";
|
|
14
|
-
|
|
15
|
-
async function walkDirectory(root: string): Promise<readonly OutputsSyncFileEntry[] | null> {
|
|
16
|
-
try {
|
|
17
|
-
const rootStat = await stat(root);
|
|
18
|
-
if (!rootStat.isDirectory()) return null;
|
|
19
|
-
} catch {
|
|
20
|
-
return null;
|
|
21
|
-
}
|
|
22
|
-
const out: OutputsSyncFileEntry[] = [];
|
|
23
|
-
async function visit(dir: string): Promise<void> {
|
|
24
|
-
const entries = await readdir(dir, { withFileTypes: true });
|
|
25
|
-
for (const entry of entries) {
|
|
26
|
-
const full = resolvePath(dir, entry.name);
|
|
27
|
-
if (entry.isDirectory()) {
|
|
28
|
-
await visit(full);
|
|
29
|
-
} else if (entry.isFile()) {
|
|
30
|
-
const s = await stat(full);
|
|
31
|
-
out.push({ path: full, sizeBytes: s.size });
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
await visit(root);
|
|
36
|
-
return out;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const io: CliIO = {
|
|
40
|
-
readFile: (path) => readFile(path, "utf8"),
|
|
41
|
-
writeFile: (path, data) => writeFile(path, data),
|
|
42
|
-
fetchImpl: fetch,
|
|
43
|
-
stdout: (chunk) => process.stdout.write(chunk),
|
|
44
|
-
stderr: (chunk) => process.stderr.write(chunk),
|
|
45
|
-
exit: (code) => process.exit(code),
|
|
46
|
-
argv: process.argv,
|
|
47
|
-
cwd: () => process.cwd(),
|
|
48
|
-
walkDirectory
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
await runCli(io);
|
package/src/host/cancel.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex cancel <run-id>` — POST /api/runs/{id}/cancel.
|
|
3
|
-
*/
|
|
4
|
-
import { operations } from "@aexhq/contracts";
|
|
5
|
-
import type { CliIO } from "../internal.js";
|
|
6
|
-
import {
|
|
7
|
-
type CliExitCode,
|
|
8
|
-
SUCCESS,
|
|
9
|
-
USAGE_ERR,
|
|
10
|
-
emitJsonError,
|
|
11
|
-
makeHttpClient,
|
|
12
|
-
parseCommonHostFlags,
|
|
13
|
-
refuseInsideManagedRun
|
|
14
|
-
} from "./common.js";
|
|
15
|
-
|
|
16
|
-
export async function runCancelCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
17
|
-
if (await refuseInsideManagedRun(io, "cancel")) return USAGE_ERR;
|
|
18
|
-
|
|
19
|
-
const common = parseCommonHostFlags(argv);
|
|
20
|
-
if (!common.ok) {
|
|
21
|
-
io.stderr(`${common.reason}\n`);
|
|
22
|
-
return USAGE_ERR;
|
|
23
|
-
}
|
|
24
|
-
const positional = common.rest.filter((arg) => !arg.startsWith("--"));
|
|
25
|
-
if (positional.length !== 1) {
|
|
26
|
-
io.stderr("usage: aex cancel <run-id> [common flags]\n");
|
|
27
|
-
return USAGE_ERR;
|
|
28
|
-
}
|
|
29
|
-
const runId = positional[0]!;
|
|
30
|
-
|
|
31
|
-
const http = makeHttpClient(io, common.flags);
|
|
32
|
-
try {
|
|
33
|
-
await operations.cancelRun(http, runId);
|
|
34
|
-
io.stdout(JSON.stringify({ runId, status: "cancel_requested" }) + "\n");
|
|
35
|
-
return SUCCESS;
|
|
36
|
-
} catch (err) {
|
|
37
|
-
return emitJsonError(io, "cancel_failed", (err as Error).message ?? "cancel failed", { runId });
|
|
38
|
-
}
|
|
39
|
-
}
|
package/src/host/common.ts
DELETED
|
@@ -1,329 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared helpers for every host-side aex subcommand. Common flag
|
|
3
|
-
* parsing, HttpClient construction, manifest detection so we can refuse
|
|
4
|
-
* to run host commands inside a managed run container, and exit codes
|
|
5
|
-
* shared with the in-container proxy command.
|
|
6
|
-
*/
|
|
7
|
-
import { AEX_DEFAULT_BASE_URL, HttpClient, type FetchLike } from "@aexhq/contracts";
|
|
8
|
-
import { AEX_INDEX_PATH, type CliIO } from "../internal.js";
|
|
9
|
-
|
|
10
|
-
export interface CliExitCode {
|
|
11
|
-
readonly code: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const SUCCESS: CliExitCode = { code: 0 };
|
|
15
|
-
export const USAGE_ERR: CliExitCode = { code: 2 };
|
|
16
|
-
export const RUNTIME_ERR: CliExitCode = { code: 1 };
|
|
17
|
-
/**
|
|
18
|
-
* Distinct exit code for "the wait/follow deadline elapsed before the
|
|
19
|
-
* run reached a terminal status". Separated from RUNTIME_ERR (1) so a
|
|
20
|
-
* script can tell a timeout apart from a run that finished non-succeeded
|
|
21
|
-
* (which is also RUNTIME_ERR). Mirrors the SDK's `waitForRun` throwing a
|
|
22
|
-
* dedicated timeout error.
|
|
23
|
-
*/
|
|
24
|
-
export const TIMEOUT_ERR: CliExitCode = { code: 3 };
|
|
25
|
-
|
|
26
|
-
export interface CommonHostFlags {
|
|
27
|
-
readonly apiToken: string;
|
|
28
|
-
readonly aexUrl: string;
|
|
29
|
-
/** `--debug`: print a redacted per-request trace to stderr. Uploads nothing. */
|
|
30
|
-
readonly debug: boolean;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export type ParseCommonResult =
|
|
34
|
-
| { readonly ok: true; readonly flags: CommonHostFlags; readonly rest: readonly string[] }
|
|
35
|
-
| { readonly ok: false; readonly reason: string };
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Parse and remove the common flags every host-side subcommand needs,
|
|
39
|
-
* leaving the rest for the caller.
|
|
40
|
-
*
|
|
41
|
-
* The CLI is `flags_only` — no `AEX_*` env reads. `--api-token` is
|
|
42
|
-
* required; `--aex-url` defaults to `AEX_DEFAULT_BASE_URL`
|
|
43
|
-
* (`https://api.aex.dev`) so SaaS users never need to supply it.
|
|
44
|
-
*
|
|
45
|
-
* There is no `--workspace` flag: workspace identity is derived
|
|
46
|
-
* server-side from the API token.
|
|
47
|
-
*/
|
|
48
|
-
export function parseCommonHostFlags(argv: readonly string[]): ParseCommonResult {
|
|
49
|
-
let apiToken: string | null = null;
|
|
50
|
-
let aexUrl: string | null = null;
|
|
51
|
-
let debug = false;
|
|
52
|
-
const rest: string[] = [];
|
|
53
|
-
|
|
54
|
-
for (let i = 0; i < argv.length; i++) {
|
|
55
|
-
const arg = argv[i]!;
|
|
56
|
-
if (arg === "--debug") {
|
|
57
|
-
debug = true;
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
if (arg === "--api-token") {
|
|
61
|
-
const v = argv[++i];
|
|
62
|
-
if (v === undefined) return { ok: false, reason: "--api-token requires a value" };
|
|
63
|
-
apiToken = v;
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
if (arg === "--aex-url") {
|
|
67
|
-
const v = argv[++i];
|
|
68
|
-
if (v === undefined) return { ok: false, reason: "--aex-url requires a value" };
|
|
69
|
-
aexUrl = v;
|
|
70
|
-
continue;
|
|
71
|
-
}
|
|
72
|
-
if (arg === "--workspace" || arg === "--workspace-id") {
|
|
73
|
-
// Removed in favor of server-side derivation from the API token.
|
|
74
|
-
// Bail out early with an actionable message instead of letting
|
|
75
|
-
// the flag fall through and produce a confusing positional-arg
|
|
76
|
-
// count error from the calling subcommand.
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
reason: `unknown flag ${arg}: workspace is derived from --api-token on the server; drop this flag`
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
rest.push(arg);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (!apiToken) return { ok: false, reason: "--api-token is required" };
|
|
86
|
-
return {
|
|
87
|
-
ok: true,
|
|
88
|
-
flags: { apiToken, aexUrl: aexUrl ?? AEX_DEFAULT_BASE_URL, debug },
|
|
89
|
-
rest
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function makeHttpClient(io: CliIO, flags: CommonHostFlags): HttpClient {
|
|
94
|
-
return new HttpClient({
|
|
95
|
-
baseUrl: flags.aexUrl,
|
|
96
|
-
apiToken: flags.apiToken,
|
|
97
|
-
fetch: io.fetchImpl as FetchLike,
|
|
98
|
-
// `--debug`: route the transport's redacted per-request traces to stderr.
|
|
99
|
-
...(flags.debug ? { debug: (line: string) => io.stderr(`${line}\n`) } : {})
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Host subcommands refuse to run inside a managed run container. The
|
|
105
|
-
* heuristic: presence of the per-run manifest at AEX_INDEX_PATH
|
|
106
|
-
* (`/mnt/session/uploads/aex/index.json`) means we're inside a
|
|
107
|
-
* run and should expose `proxy`, not the platform-management verbs.
|
|
108
|
-
*
|
|
109
|
-
* Fails *closed* on read errors that are not "file not found": if the
|
|
110
|
-
* manifest exists but is unreadable for any other reason (permissions,
|
|
111
|
-
* IO error, etc.) we refuse to run the host verb rather than risk
|
|
112
|
-
* leaking workspace-management calls into a sandboxed container.
|
|
113
|
-
*/
|
|
114
|
-
export async function refuseInsideManagedRun(io: CliIO, verb: string): Promise<boolean> {
|
|
115
|
-
try {
|
|
116
|
-
await io.readFile(AEX_INDEX_PATH);
|
|
117
|
-
io.stderr(
|
|
118
|
-
`\`aex ${verb}\` is a host command and cannot run inside a managed run container.\n` +
|
|
119
|
-
"Use `aex proxy ...` to call your declared upstream endpoints from inside the run.\n"
|
|
120
|
-
);
|
|
121
|
-
return true;
|
|
122
|
-
} catch (err) {
|
|
123
|
-
const code = (err as NodeJS.ErrnoException | undefined)?.code;
|
|
124
|
-
if (code === "ENOENT" || code === "ENOTDIR") {
|
|
125
|
-
// Manifest definitively absent; we are on a host. Allow the verb.
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
io.stderr(
|
|
129
|
-
`\`aex ${verb}\` could not determine whether it is running inside a managed run ` +
|
|
130
|
-
`container (error reading ${AEX_INDEX_PATH}: ${(err as Error).message ?? "unknown"}). ` +
|
|
131
|
-
`Refusing to proceed.\n`
|
|
132
|
-
);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Emit a JSON error body and return RUNTIME_ERR. The shape mirrors the
|
|
139
|
-
* in-container proxy error envelope so a script-shaped consumer can
|
|
140
|
-
* parse both surfaces the same way.
|
|
141
|
-
*/
|
|
142
|
-
export function emitJsonError(io: CliIO, code: string, message: string, extra: Record<string, unknown> = {}): CliExitCode {
|
|
143
|
-
io.stderr(JSON.stringify({ error: code, message, ...extra }) + "\n");
|
|
144
|
-
return RUNTIME_ERR;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Repeatable `--var key=value` / `--mcp-secret name=value` /
|
|
149
|
-
* `--proxy-auth name=value`-style flag parser. Returns the values in
|
|
150
|
-
* insertion order (last-wins on duplicate keys).
|
|
151
|
-
*/
|
|
152
|
-
export function collectRepeatedKv(rest: readonly string[], flag: string): {
|
|
153
|
-
readonly entries: Record<string, string>;
|
|
154
|
-
readonly remaining: readonly string[];
|
|
155
|
-
readonly error: string | null;
|
|
156
|
-
} {
|
|
157
|
-
const entries: Record<string, string> = {};
|
|
158
|
-
const remaining: string[] = [];
|
|
159
|
-
for (let i = 0; i < rest.length; i++) {
|
|
160
|
-
const arg = rest[i]!;
|
|
161
|
-
if (arg === flag) {
|
|
162
|
-
const kv = rest[++i];
|
|
163
|
-
if (kv === undefined) {
|
|
164
|
-
return { entries, remaining, error: `${flag} requires a KEY=VALUE argument` };
|
|
165
|
-
}
|
|
166
|
-
const eq = kv.indexOf("=");
|
|
167
|
-
if (eq <= 0) {
|
|
168
|
-
return { entries, remaining, error: `${flag} must be in the form KEY=VALUE (got: ${kv})` };
|
|
169
|
-
}
|
|
170
|
-
entries[kv.slice(0, eq)] = kv.slice(eq + 1);
|
|
171
|
-
continue;
|
|
172
|
-
}
|
|
173
|
-
remaining.push(arg);
|
|
174
|
-
}
|
|
175
|
-
return { entries, remaining, error: null };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Repeatable `--flag <value>` collector. Each occurrence consumes one
|
|
180
|
-
* argv slot. Returns the collected values in insertion order alongside
|
|
181
|
-
* the remaining argv.
|
|
182
|
-
*/
|
|
183
|
-
export function collectRepeated(rest: readonly string[], flag: string): {
|
|
184
|
-
readonly values: readonly string[];
|
|
185
|
-
readonly remaining: readonly string[];
|
|
186
|
-
readonly error: string | null;
|
|
187
|
-
} {
|
|
188
|
-
const values: string[] = [];
|
|
189
|
-
const remaining: string[] = [];
|
|
190
|
-
for (let i = 0; i < rest.length; i++) {
|
|
191
|
-
const arg = rest[i]!;
|
|
192
|
-
if (arg === flag) {
|
|
193
|
-
const v = rest[++i];
|
|
194
|
-
if (v === undefined) {
|
|
195
|
-
return { values, remaining, error: `${flag} requires a value` };
|
|
196
|
-
}
|
|
197
|
-
values.push(v);
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
remaining.push(arg);
|
|
201
|
-
}
|
|
202
|
-
return { values, remaining, error: null };
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
* Repeatable `--flag KEY=VALUE` collector that PRESERVES every
|
|
207
|
-
* occurrence (no key-collision collapse). Use this when the same key
|
|
208
|
-
* may legitimately appear multiple times — for example,
|
|
209
|
-
* `--mcp-auth github=Authorization:Bearer t --mcp-auth github=X-Api-Key:k`
|
|
210
|
-
* needs to register both headers, not just the last one.
|
|
211
|
-
*
|
|
212
|
-
* Returns `[key, value]` pairs in argv order alongside the remaining
|
|
213
|
-
* argv with the consumed flags removed.
|
|
214
|
-
*/
|
|
215
|
-
export function collectRepeatedKvList(rest: readonly string[], flag: string): {
|
|
216
|
-
readonly entries: ReadonlyArray<readonly [string, string]>;
|
|
217
|
-
readonly remaining: readonly string[];
|
|
218
|
-
readonly error: string | null;
|
|
219
|
-
} {
|
|
220
|
-
const entries: Array<readonly [string, string]> = [];
|
|
221
|
-
const remaining: string[] = [];
|
|
222
|
-
for (let i = 0; i < rest.length; i++) {
|
|
223
|
-
const arg = rest[i]!;
|
|
224
|
-
if (arg === flag) {
|
|
225
|
-
const kv = rest[++i];
|
|
226
|
-
if (kv === undefined) {
|
|
227
|
-
return { entries, remaining, error: `${flag} requires a KEY=VALUE argument` };
|
|
228
|
-
}
|
|
229
|
-
const eq = kv.indexOf("=");
|
|
230
|
-
if (eq <= 0) {
|
|
231
|
-
return { entries, remaining, error: `${flag} must be in the form KEY=VALUE (got: ${kv})` };
|
|
232
|
-
}
|
|
233
|
-
entries.push([kv.slice(0, eq), kv.slice(eq + 1)] as const);
|
|
234
|
-
continue;
|
|
235
|
-
}
|
|
236
|
-
remaining.push(arg);
|
|
237
|
-
}
|
|
238
|
-
return { entries, remaining, error: null };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export function takeFlagValue(
|
|
242
|
-
rest: readonly string[],
|
|
243
|
-
flag: string
|
|
244
|
-
): { value: string | null; remaining: readonly string[]; error: string | null } {
|
|
245
|
-
let value: string | null = null;
|
|
246
|
-
const remaining: string[] = [];
|
|
247
|
-
for (let i = 0; i < rest.length; i++) {
|
|
248
|
-
const arg = rest[i]!;
|
|
249
|
-
if (arg === flag) {
|
|
250
|
-
const v = rest[++i];
|
|
251
|
-
if (v === undefined) {
|
|
252
|
-
return { value, remaining, error: `${flag} requires a value` };
|
|
253
|
-
}
|
|
254
|
-
value = v;
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
remaining.push(arg);
|
|
258
|
-
}
|
|
259
|
-
return { value, remaining, error: null };
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Parse a human-friendly duration into milliseconds. Accepts a bare
|
|
264
|
-
* integer (interpreted as milliseconds) or `<number><unit>` where unit
|
|
265
|
-
* is one of `ms`, `s`, `m`, `h` — e.g. `500ms`, `30s`, `8m`, `1h`.
|
|
266
|
-
*
|
|
267
|
-
* Returns the millisecond count, or an `error` string describing the
|
|
268
|
-
* malformed input (the value is never silently coerced to 0). Rejects
|
|
269
|
-
* negatives and non-finite values so a `--timeout` can never disable the
|
|
270
|
-
* deadline by accident.
|
|
271
|
-
*/
|
|
272
|
-
export function parseDuration(input: string): { readonly ms: number | null; readonly error: string | null } {
|
|
273
|
-
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)?$/.exec(input.trim());
|
|
274
|
-
if (!match) {
|
|
275
|
-
return { ms: null, error: `invalid duration "${input}" (expected e.g. 500ms, 30s, 8m, 1h, or a bare ms integer)` };
|
|
276
|
-
}
|
|
277
|
-
const value = Number(match[1]);
|
|
278
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
279
|
-
return { ms: null, error: `invalid duration "${input}" (must be a non-negative number)` };
|
|
280
|
-
}
|
|
281
|
-
const unit = match[2] ?? "ms";
|
|
282
|
-
const factor = unit === "h" ? 3_600_000 : unit === "m" ? 60_000 : unit === "s" ? 1_000 : 1;
|
|
283
|
-
return { ms: Math.round(value * factor), error: null };
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export function takeBooleanFlag(rest: readonly string[], flag: string): {
|
|
287
|
-
readonly present: boolean;
|
|
288
|
-
readonly remaining: readonly string[];
|
|
289
|
-
} {
|
|
290
|
-
const remaining: string[] = [];
|
|
291
|
-
let present = false;
|
|
292
|
-
for (const arg of rest) {
|
|
293
|
-
if (arg === flag) {
|
|
294
|
-
present = true;
|
|
295
|
-
continue;
|
|
296
|
-
}
|
|
297
|
-
remaining.push(arg);
|
|
298
|
-
}
|
|
299
|
-
return { present, remaining };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* Take an option flag in either `--flag value` or `--flag=value` form.
|
|
304
|
-
* Returns the trailing value (or undefined when the flag is absent)
|
|
305
|
-
* plus the remaining args. Unlike `takeFlagValue`, this is permissive
|
|
306
|
-
* about the `=` form which CLI users frequently expect.
|
|
307
|
-
*/
|
|
308
|
-
export function takeOptionFlag(
|
|
309
|
-
rest: readonly string[],
|
|
310
|
-
flag: string
|
|
311
|
-
): { readonly value: string | undefined; readonly remaining: readonly string[] } {
|
|
312
|
-
const remaining: string[] = [];
|
|
313
|
-
let value: string | undefined;
|
|
314
|
-
const prefix = `${flag}=`;
|
|
315
|
-
for (let i = 0; i < rest.length; i++) {
|
|
316
|
-
const arg = rest[i]!;
|
|
317
|
-
if (arg === flag) {
|
|
318
|
-
const next = rest[++i];
|
|
319
|
-
if (next !== undefined) value = next;
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (arg.startsWith(prefix)) {
|
|
323
|
-
value = arg.slice(prefix.length);
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
remaining.push(arg);
|
|
327
|
-
}
|
|
328
|
-
return { value, remaining };
|
|
329
|
-
}
|
package/src/host/delete-asset.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex delete-asset <assetId|hash>` — DELETE /assets/{assetId}.
|
|
3
|
-
*
|
|
4
|
-
* Removes a workspace asset blob from the shared content-addressed store.
|
|
5
|
-
* Runs that already snapshotted the asset into their own prefix are
|
|
6
|
-
* unaffected. Accepts `sha256:<hex>` or a bare 64-hex digest.
|
|
7
|
-
*/
|
|
8
|
-
import { operations } from "@aexhq/contracts";
|
|
9
|
-
import type { CliIO } from "../internal.js";
|
|
10
|
-
import {
|
|
11
|
-
type CliExitCode,
|
|
12
|
-
SUCCESS,
|
|
13
|
-
USAGE_ERR,
|
|
14
|
-
emitJsonError,
|
|
15
|
-
makeHttpClient,
|
|
16
|
-
parseCommonHostFlags,
|
|
17
|
-
refuseInsideManagedRun
|
|
18
|
-
} from "./common.js";
|
|
19
|
-
|
|
20
|
-
export async function runDeleteAssetCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
21
|
-
if (await refuseInsideManagedRun(io, "delete-asset")) return USAGE_ERR;
|
|
22
|
-
|
|
23
|
-
const common = parseCommonHostFlags(argv);
|
|
24
|
-
if (!common.ok) {
|
|
25
|
-
io.stderr(`${common.reason}\n`);
|
|
26
|
-
return USAGE_ERR;
|
|
27
|
-
}
|
|
28
|
-
const positional = common.rest.filter((arg) => !arg.startsWith("--"));
|
|
29
|
-
if (positional.length !== 1) {
|
|
30
|
-
io.stderr("usage: aex delete-asset <hash> [common flags]\n");
|
|
31
|
-
return USAGE_ERR;
|
|
32
|
-
}
|
|
33
|
-
const hash = positional[0]!;
|
|
34
|
-
|
|
35
|
-
const http = makeHttpClient(io, common.flags);
|
|
36
|
-
try {
|
|
37
|
-
await operations.deleteWorkspaceAsset(http, hash);
|
|
38
|
-
io.stdout(JSON.stringify({ hash, deleted: true }) + "\n");
|
|
39
|
-
return SUCCESS;
|
|
40
|
-
} catch (err) {
|
|
41
|
-
return emitJsonError(io, "delete_asset_failed", (err as Error).message ?? "delete-asset failed", { hash });
|
|
42
|
-
}
|
|
43
|
-
}
|
package/src/host/delete.ts
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex delete <run-id>` — DELETE /api/runs/{id}.
|
|
3
|
-
*/
|
|
4
|
-
import { operations } from "@aexhq/contracts";
|
|
5
|
-
import type { CliIO } from "../internal.js";
|
|
6
|
-
import {
|
|
7
|
-
type CliExitCode,
|
|
8
|
-
SUCCESS,
|
|
9
|
-
USAGE_ERR,
|
|
10
|
-
emitJsonError,
|
|
11
|
-
makeHttpClient,
|
|
12
|
-
parseCommonHostFlags,
|
|
13
|
-
refuseInsideManagedRun
|
|
14
|
-
} from "./common.js";
|
|
15
|
-
|
|
16
|
-
export async function runDeleteCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
17
|
-
if (await refuseInsideManagedRun(io, "delete")) return USAGE_ERR;
|
|
18
|
-
|
|
19
|
-
const common = parseCommonHostFlags(argv);
|
|
20
|
-
if (!common.ok) {
|
|
21
|
-
io.stderr(`${common.reason}\n`);
|
|
22
|
-
return USAGE_ERR;
|
|
23
|
-
}
|
|
24
|
-
const positional = common.rest.filter((arg) => !arg.startsWith("--"));
|
|
25
|
-
if (positional.length !== 1) {
|
|
26
|
-
io.stderr("usage: aex delete <run-id> [common flags]\n");
|
|
27
|
-
return USAGE_ERR;
|
|
28
|
-
}
|
|
29
|
-
const runId = positional[0]!;
|
|
30
|
-
|
|
31
|
-
const http = makeHttpClient(io, common.flags);
|
|
32
|
-
try {
|
|
33
|
-
await operations.deleteRun(http, runId);
|
|
34
|
-
io.stdout(JSON.stringify({ runId, deleted: true }) + "\n");
|
|
35
|
-
return SUCCESS;
|
|
36
|
-
} catch (err) {
|
|
37
|
-
return emitJsonError(io, "delete_failed", (err as Error).message ?? "delete failed", { runId });
|
|
38
|
-
}
|
|
39
|
-
}
|
package/src/host/download.ts
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex download <run-id> [--only namespace] [--out path]` — download
|
|
3
|
-
* a run's content as a zip, assembled client-side from the public read
|
|
4
|
-
* endpoints (no per-output id required).
|
|
5
|
-
*
|
|
6
|
-
* Without `--only`, downloads everything public — organised into
|
|
7
|
-
* `metadata/run.json`, typed `events/events.jsonl`, `outputs/<rel>`
|
|
8
|
-
* (deliverables), plus `manifest.json`.
|
|
9
|
-
*
|
|
10
|
-
* `--only outputs|events|metadata` downloads just that one
|
|
11
|
-
* namespace (files at the zip root).
|
|
12
|
-
*
|
|
13
|
-
* `--out` resolves relative to the host CWD; if omitted the file is
|
|
14
|
-
* written to `aex-run-<run-id>[-<namespace>].zip` in the current
|
|
15
|
-
* directory.
|
|
16
|
-
*/
|
|
17
|
-
import { resolve as resolvePath } from "node:path";
|
|
18
|
-
import { operations } from "@aexhq/contracts";
|
|
19
|
-
import type { CliIO } from "../internal.js";
|
|
20
|
-
import {
|
|
21
|
-
type CliExitCode,
|
|
22
|
-
SUCCESS,
|
|
23
|
-
USAGE_ERR,
|
|
24
|
-
emitJsonError,
|
|
25
|
-
makeHttpClient,
|
|
26
|
-
parseCommonHostFlags,
|
|
27
|
-
refuseInsideManagedRun,
|
|
28
|
-
takeFlagValue
|
|
29
|
-
} from "./common.js";
|
|
30
|
-
|
|
31
|
-
type Namespace = "outputs" | "events" | "metadata";
|
|
32
|
-
|
|
33
|
-
const NAMESPACE_DOWNLOADERS = {
|
|
34
|
-
outputs: operations.downloadOutputs,
|
|
35
|
-
events: operations.downloadEvents,
|
|
36
|
-
metadata: operations.downloadMetadata
|
|
37
|
-
} satisfies Record<Namespace, typeof operations.download>;
|
|
38
|
-
|
|
39
|
-
export async function runDownloadCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
40
|
-
if (await refuseInsideManagedRun(io, "download")) return USAGE_ERR;
|
|
41
|
-
|
|
42
|
-
const common = parseCommonHostFlags(argv);
|
|
43
|
-
if (!common.ok) {
|
|
44
|
-
io.stderr(`${common.reason}\n`);
|
|
45
|
-
return USAGE_ERR;
|
|
46
|
-
}
|
|
47
|
-
const outFlag = takeFlagValue(common.rest, "--out");
|
|
48
|
-
if (outFlag.error) {
|
|
49
|
-
io.stderr(`${outFlag.error}\n`);
|
|
50
|
-
return USAGE_ERR;
|
|
51
|
-
}
|
|
52
|
-
const onlyFlag = takeFlagValue(outFlag.remaining, "--only");
|
|
53
|
-
if (onlyFlag.error) {
|
|
54
|
-
io.stderr(`${onlyFlag.error}\n`);
|
|
55
|
-
return USAGE_ERR;
|
|
56
|
-
}
|
|
57
|
-
if (onlyFlag.value !== null && !(onlyFlag.value in NAMESPACE_DOWNLOADERS)) {
|
|
58
|
-
io.stderr(`--only must be one of: ${Object.keys(NAMESPACE_DOWNLOADERS).join(", ")}\n`);
|
|
59
|
-
return USAGE_ERR;
|
|
60
|
-
}
|
|
61
|
-
const namespace = onlyFlag.value as Namespace | null;
|
|
62
|
-
|
|
63
|
-
const positional = onlyFlag.remaining.filter((arg) => !arg.startsWith("--"));
|
|
64
|
-
if (positional.length !== 1) {
|
|
65
|
-
io.stderr("usage: aex download <run-id> [--only outputs|events|metadata] [--out path] [common flags]\n");
|
|
66
|
-
return USAGE_ERR;
|
|
67
|
-
}
|
|
68
|
-
const runId = positional[0]!;
|
|
69
|
-
|
|
70
|
-
const http = makeHttpClient(io, common.flags);
|
|
71
|
-
const downloader = namespace ? NAMESPACE_DOWNLOADERS[namespace] : operations.download;
|
|
72
|
-
|
|
73
|
-
let bytes: Uint8Array;
|
|
74
|
-
try {
|
|
75
|
-
bytes = await downloader(http, runId);
|
|
76
|
-
} catch (err) {
|
|
77
|
-
return emitJsonError(io, "download_failed", (err as Error).message ?? "download failed", { runId });
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const destination = resolveDestination(io, outFlag.value, runId, namespace);
|
|
81
|
-
try {
|
|
82
|
-
await io.writeFile(destination, bytes);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
return emitJsonError(io, "write_failed", `failed to write archive: ${(err as Error).message}`, { destination });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
io.stdout(JSON.stringify({ runId, namespace: namespace ?? "all", path: destination, bytes: bytes.byteLength }) + "\n");
|
|
88
|
-
return SUCCESS;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function resolveDestination(io: CliIO, out: string | null, runId: string, namespace: Namespace | null): string {
|
|
92
|
-
if (out) {
|
|
93
|
-
return resolvePath(io.cwd(), out);
|
|
94
|
-
}
|
|
95
|
-
const suffix = namespace ? `-${namespace}` : "";
|
|
96
|
-
return resolvePath(io.cwd(), `aex-run-${runId}${suffix}.zip`);
|
|
97
|
-
}
|