@aexhq/cli 0.25.2 → 0.26.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/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 -4685
- 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 -565
- 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 -167
- 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 -594
- 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 -188
- 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
package/src/host/skills-cmd.ts
DELETED
|
@@ -1,317 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex skills <verb>` — workspace skill bundle management. Mirrors
|
|
3
|
-
* the SDK's `client.skills` namespace 1:1 so SDK and CLI never drift.
|
|
4
|
-
*
|
|
5
|
-
* Verbs:
|
|
6
|
-
* aex skills upload --from-path <dir> --name <n>
|
|
7
|
-
* aex skills upload --file <path> [--file <path> ...] --name <n>
|
|
8
|
-
* aex skills list
|
|
9
|
-
* aex skills get <skill-id>
|
|
10
|
-
* aex skills delete <skill-id>
|
|
11
|
-
*
|
|
12
|
-
* `--file` stores each path verbatim (relative to cwd) under its
|
|
13
|
-
* basename inside the bundle. For directory uploads, use `--from-path`.
|
|
14
|
-
*/
|
|
15
|
-
import { readFile, readdir, stat } from "node:fs/promises";
|
|
16
|
-
import { basename, join, posix, relative, resolve as resolvePath, sep } from "node:path";
|
|
17
|
-
import { zipSync, type Zippable } from "fflate";
|
|
18
|
-
import {
|
|
19
|
-
SKILL_BUNDLE_LIMITS,
|
|
20
|
-
operations,
|
|
21
|
-
validateSkillBundleEntry
|
|
22
|
-
} from "@aexhq/contracts";
|
|
23
|
-
import type { CliIO } from "../internal.js";
|
|
24
|
-
import {
|
|
25
|
-
type CliExitCode,
|
|
26
|
-
RUNTIME_ERR,
|
|
27
|
-
SUCCESS,
|
|
28
|
-
USAGE_ERR,
|
|
29
|
-
collectRepeated,
|
|
30
|
-
emitJsonError,
|
|
31
|
-
makeHttpClient,
|
|
32
|
-
parseCommonHostFlags,
|
|
33
|
-
refuseInsideManagedRun,
|
|
34
|
-
takeFlagValue
|
|
35
|
-
} from "./common.js";
|
|
36
|
-
|
|
37
|
-
export async function runSkillsCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
38
|
-
if (await refuseInsideManagedRun(io, "skills")) return USAGE_ERR;
|
|
39
|
-
const [verb, ...rest] = argv;
|
|
40
|
-
if (!verb) {
|
|
41
|
-
io.stderr("usage: aex skills <upload|list|get|delete> [flags]\n");
|
|
42
|
-
return USAGE_ERR;
|
|
43
|
-
}
|
|
44
|
-
switch (verb) {
|
|
45
|
-
case "upload": return runSkillsUpload(io, rest);
|
|
46
|
-
case "list": return runSkillsList(io, rest);
|
|
47
|
-
case "get": return runSkillsGet(io, rest);
|
|
48
|
-
case "delete": return runSkillsDelete(io, rest);
|
|
49
|
-
default:
|
|
50
|
-
io.stderr(`unknown skills verb: ${verb}\n`);
|
|
51
|
-
return USAGE_ERR;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function runSkillsUpload(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
56
|
-
const common = parseCommonHostFlags(argv);
|
|
57
|
-
if (!common.ok) { io.stderr(`${common.reason}\n`); return USAGE_ERR; }
|
|
58
|
-
let rest = common.rest;
|
|
59
|
-
|
|
60
|
-
const nameFlag = takeFlagValue(rest, "--name");
|
|
61
|
-
if (nameFlag.error) { io.stderr(`${nameFlag.error}\n`); return USAGE_ERR; }
|
|
62
|
-
rest = nameFlag.remaining;
|
|
63
|
-
if (!nameFlag.value) {
|
|
64
|
-
io.stderr("--name is required\n");
|
|
65
|
-
return USAGE_ERR;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const fromPath = takeFlagValue(rest, "--from-path");
|
|
69
|
-
if (fromPath.error) { io.stderr(`${fromPath.error}\n`); return USAGE_ERR; }
|
|
70
|
-
rest = fromPath.remaining;
|
|
71
|
-
|
|
72
|
-
const fileFlags = collectRepeated(rest, "--file");
|
|
73
|
-
if (fileFlags.error) { io.stderr(`${fileFlags.error}\n`); return USAGE_ERR; }
|
|
74
|
-
rest = fileFlags.remaining;
|
|
75
|
-
|
|
76
|
-
const unknown = rest.filter((a) => a.startsWith("--"));
|
|
77
|
-
if (unknown.length > 0) {
|
|
78
|
-
io.stderr(`unknown flag: ${unknown[0]}\n`);
|
|
79
|
-
return USAGE_ERR;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if ((fromPath.value && fileFlags.values.length > 0) || (!fromPath.value && fileFlags.values.length === 0)) {
|
|
83
|
-
io.stderr("aex skills upload requires exactly one of: --from-path <dir> | --file <path> [--file ...]\n");
|
|
84
|
-
return USAGE_ERR;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
let built: SkillBundleBuild;
|
|
88
|
-
try {
|
|
89
|
-
if (fromPath.value) {
|
|
90
|
-
built = await zipDirectory(resolvePath(io.cwd(), fromPath.value));
|
|
91
|
-
} else {
|
|
92
|
-
built = await zipFiles(io.cwd(), fileFlags.values);
|
|
93
|
-
}
|
|
94
|
-
} catch (err) {
|
|
95
|
-
io.stderr(`failed to build skill bundle: ${(err as Error).message}\n`);
|
|
96
|
-
return USAGE_ERR;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const contentHash = `sha256:${await sha256Hex(built.zip)}`;
|
|
100
|
-
const http = makeHttpClient(io, common.flags);
|
|
101
|
-
try {
|
|
102
|
-
// Direct-to-storage upload (bytes bypass the hosted API); falls back to the
|
|
103
|
-
// buffered multipart path when object-store presign creds are unconfigured.
|
|
104
|
-
const skill = await operations.createSkillBundleDirect(http, io.fetchImpl as DirectPutFetch, {
|
|
105
|
-
name: nameFlag.value,
|
|
106
|
-
body: built.zip,
|
|
107
|
-
contentHash,
|
|
108
|
-
manifest: built.manifest,
|
|
109
|
-
contentType: "application/zip"
|
|
110
|
-
});
|
|
111
|
-
io.stdout(JSON.stringify(skill) + "\n");
|
|
112
|
-
return SUCCESS;
|
|
113
|
-
} catch (err) {
|
|
114
|
-
return emitJsonError(io, "skill_upload_failed", (err as Error).message ?? "upload failed");
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Raw fetch shape the direct-to-storage PUT needs (a structural subset of fetch). */
|
|
119
|
-
type DirectPutFetch = (input: string, init?: RequestInit) => Promise<{ ok: boolean; status: number; text(): Promise<string> }>;
|
|
120
|
-
|
|
121
|
-
/** A built skill bundle: the zip bytes plus the client-computed manifest. */
|
|
122
|
-
interface SkillBundleBuild {
|
|
123
|
-
readonly zip: Uint8Array;
|
|
124
|
-
readonly manifest: ReadonlyArray<{ readonly path: string; readonly size: number }>;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function sha256Hex(bytes: Uint8Array): Promise<string> {
|
|
128
|
-
const subtle = (globalThis as { crypto?: { subtle?: SubtleCrypto } }).crypto?.subtle;
|
|
129
|
-
if (!subtle) throw new Error("aex skills upload: globalThis.crypto.subtle is required (Node 18+)");
|
|
130
|
-
const copy = new Uint8Array(bytes.byteLength);
|
|
131
|
-
copy.set(bytes);
|
|
132
|
-
const digest = await subtle.digest("SHA-256", copy.buffer);
|
|
133
|
-
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function runSkillsList(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
137
|
-
const common = parseCommonHostFlags(argv);
|
|
138
|
-
if (!common.ok) { io.stderr(`${common.reason}\n`); return USAGE_ERR; }
|
|
139
|
-
const positional = common.rest.filter((a) => !a.startsWith("--"));
|
|
140
|
-
const unknown = common.rest.filter((a) => a.startsWith("--"));
|
|
141
|
-
if (unknown.length > 0) {
|
|
142
|
-
io.stderr(`unknown flag: ${unknown[0]}\n`);
|
|
143
|
-
return USAGE_ERR;
|
|
144
|
-
}
|
|
145
|
-
if (positional.length > 0) {
|
|
146
|
-
io.stderr(`aex skills list takes no positional arguments\n`);
|
|
147
|
-
return USAGE_ERR;
|
|
148
|
-
}
|
|
149
|
-
const http = makeHttpClient(io, common.flags);
|
|
150
|
-
try {
|
|
151
|
-
const skills = await operations.listSkills(http);
|
|
152
|
-
for (const s of skills) io.stdout(JSON.stringify(s) + "\n");
|
|
153
|
-
return SUCCESS;
|
|
154
|
-
} catch (err) {
|
|
155
|
-
return emitJsonError(io, "skills_list_failed", (err as Error).message ?? "list failed");
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function runSkillsGet(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
160
|
-
const common = parseCommonHostFlags(argv);
|
|
161
|
-
if (!common.ok) { io.stderr(`${common.reason}\n`); return USAGE_ERR; }
|
|
162
|
-
const positional = common.rest.filter((a) => !a.startsWith("--"));
|
|
163
|
-
const unknown = common.rest.filter((a) => a.startsWith("--"));
|
|
164
|
-
if (unknown.length > 0) {
|
|
165
|
-
io.stderr(`unknown flag: ${unknown[0]}\n`);
|
|
166
|
-
return USAGE_ERR;
|
|
167
|
-
}
|
|
168
|
-
if (positional.length !== 1) {
|
|
169
|
-
io.stderr("usage: aex skills get <skill-id>\n");
|
|
170
|
-
return USAGE_ERR;
|
|
171
|
-
}
|
|
172
|
-
const skillId = positional[0]!;
|
|
173
|
-
const http = makeHttpClient(io, common.flags);
|
|
174
|
-
try {
|
|
175
|
-
const skill = await operations.getSkill(http, skillId);
|
|
176
|
-
io.stdout(JSON.stringify(skill) + "\n");
|
|
177
|
-
return SUCCESS;
|
|
178
|
-
} catch (err) {
|
|
179
|
-
return emitJsonError(io, "skill_get_failed", (err as Error).message ?? "get failed");
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
async function runSkillsDelete(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
184
|
-
const common = parseCommonHostFlags(argv);
|
|
185
|
-
if (!common.ok) { io.stderr(`${common.reason}\n`); return USAGE_ERR; }
|
|
186
|
-
const positional = common.rest.filter((a) => !a.startsWith("--"));
|
|
187
|
-
const unknown = common.rest.filter((a) => a.startsWith("--"));
|
|
188
|
-
if (unknown.length > 0) {
|
|
189
|
-
io.stderr(`unknown flag: ${unknown[0]}\n`);
|
|
190
|
-
return USAGE_ERR;
|
|
191
|
-
}
|
|
192
|
-
if (positional.length !== 1) {
|
|
193
|
-
io.stderr("usage: aex skills delete <skill-id>\n");
|
|
194
|
-
return USAGE_ERR;
|
|
195
|
-
}
|
|
196
|
-
const skillId = positional[0]!;
|
|
197
|
-
const http = makeHttpClient(io, common.flags);
|
|
198
|
-
try {
|
|
199
|
-
await operations.deleteSkill(http, skillId);
|
|
200
|
-
io.stdout(JSON.stringify({ skillId, deleted: true }) + "\n");
|
|
201
|
-
return SUCCESS;
|
|
202
|
-
} catch (err) {
|
|
203
|
-
return emitJsonError(io, "skill_delete_failed", (err as Error).message ?? "delete failed");
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/* ---------- bundle building ---------- */
|
|
208
|
-
|
|
209
|
-
/** Stable mtime for every zip entry — keeps the bundle byte-deterministic
|
|
210
|
-
* across machines and re-runs so SDK/CLI hashes match the BFF's
|
|
211
|
-
* canonical hash. ZIP's legacy DOS date encoding requires year >= 1980,
|
|
212
|
-
* so we anchor to 1980-01-01 UTC rather than the unix epoch. */
|
|
213
|
-
const ZIP_EPOCH = new Date(Date.UTC(1980, 0, 1));
|
|
214
|
-
|
|
215
|
-
function zipEntryFor(bytes: Uint8Array): [Uint8Array, { mtime: Date }] {
|
|
216
|
-
return [bytes, { mtime: ZIP_EPOCH }];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function zipDirectory(rootDir: string): Promise<SkillBundleBuild> {
|
|
220
|
-
const rootStat = await stat(rootDir);
|
|
221
|
-
if (!rootStat.isDirectory()) {
|
|
222
|
-
throw new Error(`${rootDir} is not a directory`);
|
|
223
|
-
}
|
|
224
|
-
const collected = new Map<string, Uint8Array>();
|
|
225
|
-
let totalDecompressed = 0;
|
|
226
|
-
let hasSkillMd = false;
|
|
227
|
-
await walk(rootDir, rootDir, async (relPathPosix, bytes) => {
|
|
228
|
-
const entry = validateSkillBundleEntry({ path: relPathPosix, size: bytes.byteLength });
|
|
229
|
-
if (entry.path === "SKILL.md") hasSkillMd = true;
|
|
230
|
-
totalDecompressed += bytes.byteLength;
|
|
231
|
-
if (totalDecompressed > SKILL_BUNDLE_LIMITS.maxDecompressedBytes) {
|
|
232
|
-
throw new Error(`skill bundle exceeds decompressed cap of ${SKILL_BUNDLE_LIMITS.maxDecompressedBytes} bytes`);
|
|
233
|
-
}
|
|
234
|
-
if (collected.has(entry.path)) {
|
|
235
|
-
throw new Error(`skill bundle contains duplicate path: ${entry.path}`);
|
|
236
|
-
}
|
|
237
|
-
if (collected.size >= SKILL_BUNDLE_LIMITS.maxFiles) {
|
|
238
|
-
throw new Error(`skill bundle exceeds ${SKILL_BUNDLE_LIMITS.maxFiles} file limit`);
|
|
239
|
-
}
|
|
240
|
-
collected.set(entry.path, bytes);
|
|
241
|
-
});
|
|
242
|
-
if (collected.size === 0) {
|
|
243
|
-
throw new Error(`${rootDir} contains no regular files`);
|
|
244
|
-
}
|
|
245
|
-
if (!hasSkillMd) {
|
|
246
|
-
throw new Error(
|
|
247
|
-
'skill bundle must contain a "SKILL.md" file at the root. ' +
|
|
248
|
-
"For AGENTS.md / generic files use the corresponding `aex agentsmd` / " +
|
|
249
|
-
"`aex files` commands instead."
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
// Sort entries for deterministic ordering (zipSync preserves insertion order).
|
|
253
|
-
const sorted = [...collected.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
254
|
-
const zippable: Zippable = {};
|
|
255
|
-
for (const [path, bytes] of sorted) zippable[path] = zipEntryFor(bytes);
|
|
256
|
-
const out = zipSync(zippable, { level: 6 });
|
|
257
|
-
if (out.byteLength > SKILL_BUNDLE_LIMITS.maxCompressedBytes) {
|
|
258
|
-
throw new Error(`skill bundle exceeds compressed cap of ${SKILL_BUNDLE_LIMITS.maxCompressedBytes} bytes (got ${out.byteLength})`);
|
|
259
|
-
}
|
|
260
|
-
return { zip: out, manifest: sorted.map(([path, bytes]) => ({ path, size: bytes.byteLength })) };
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
async function zipFiles(cwd: string, paths: readonly string[]): Promise<SkillBundleBuild> {
|
|
264
|
-
if (paths.length > SKILL_BUNDLE_LIMITS.maxFiles) {
|
|
265
|
-
throw new Error(`skill bundle exceeds ${SKILL_BUNDLE_LIMITS.maxFiles} file limit`);
|
|
266
|
-
}
|
|
267
|
-
const collected = new Map<string, Uint8Array>();
|
|
268
|
-
let totalDecompressed = 0;
|
|
269
|
-
let hasSkillMd = false;
|
|
270
|
-
for (const p of paths) {
|
|
271
|
-
const abs = resolvePath(cwd, p);
|
|
272
|
-
const bytes = await readFile(abs);
|
|
273
|
-
const name = basename(p);
|
|
274
|
-
const entry = validateSkillBundleEntry({ path: name, size: bytes.byteLength });
|
|
275
|
-
if (entry.path === "SKILL.md") hasSkillMd = true;
|
|
276
|
-
totalDecompressed += bytes.byteLength;
|
|
277
|
-
if (totalDecompressed > SKILL_BUNDLE_LIMITS.maxDecompressedBytes) {
|
|
278
|
-
throw new Error(`skill bundle exceeds decompressed cap of ${SKILL_BUNDLE_LIMITS.maxDecompressedBytes} bytes`);
|
|
279
|
-
}
|
|
280
|
-
if (collected.has(entry.path)) {
|
|
281
|
-
throw new Error(`skill bundle contains duplicate path (basenames must be unique): ${entry.path}`);
|
|
282
|
-
}
|
|
283
|
-
collected.set(entry.path, bytes);
|
|
284
|
-
}
|
|
285
|
-
if (!hasSkillMd) {
|
|
286
|
-
throw new Error('skill bundle must contain a "SKILL.md" file at the root');
|
|
287
|
-
}
|
|
288
|
-
const sorted = [...collected.entries()].sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
289
|
-
const zippable: Zippable = {};
|
|
290
|
-
for (const [path, bytes] of sorted) zippable[path] = zipEntryFor(bytes);
|
|
291
|
-
const out = zipSync(zippable, { level: 6 });
|
|
292
|
-
if (out.byteLength > SKILL_BUNDLE_LIMITS.maxCompressedBytes) {
|
|
293
|
-
throw new Error(`skill bundle exceeds compressed cap of ${SKILL_BUNDLE_LIMITS.maxCompressedBytes} bytes`);
|
|
294
|
-
}
|
|
295
|
-
return { zip: out, manifest: sorted.map(([path, bytes]) => ({ path, size: bytes.byteLength })) };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
async function walk(
|
|
299
|
-
rootDir: string,
|
|
300
|
-
currentDir: string,
|
|
301
|
-
visit: (relPathPosix: string, bytes: Uint8Array) => Promise<void>
|
|
302
|
-
): Promise<void> {
|
|
303
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
304
|
-
for (const dirent of entries) {
|
|
305
|
-
const full = join(currentDir, dirent.name);
|
|
306
|
-
if (dirent.isSymbolicLink()) continue;
|
|
307
|
-
if (dirent.isDirectory()) {
|
|
308
|
-
await walk(rootDir, full, visit);
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
if (!dirent.isFile()) continue;
|
|
312
|
-
const rel = relative(rootDir, full);
|
|
313
|
-
const posixPath = sep === "/" ? rel : rel.split(sep).join(posix.sep);
|
|
314
|
-
const bytes = await readFile(full);
|
|
315
|
-
await visit(posixPath, bytes);
|
|
316
|
-
}
|
|
317
|
-
}
|
package/src/host/status.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex status <run-id>` — fetch a single run record via GET
|
|
3
|
-
* /api/runs/{id} and print it as JSON.
|
|
4
|
-
*/
|
|
5
|
-
import { operations } from "@aexhq/contracts";
|
|
6
|
-
import type { CliIO } from "../internal.js";
|
|
7
|
-
import {
|
|
8
|
-
type CliExitCode,
|
|
9
|
-
SUCCESS,
|
|
10
|
-
USAGE_ERR,
|
|
11
|
-
emitJsonError,
|
|
12
|
-
makeHttpClient,
|
|
13
|
-
parseCommonHostFlags,
|
|
14
|
-
refuseInsideManagedRun
|
|
15
|
-
} from "./common.js";
|
|
16
|
-
|
|
17
|
-
export async function runStatusCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
18
|
-
if (await refuseInsideManagedRun(io, "status")) return USAGE_ERR;
|
|
19
|
-
|
|
20
|
-
const common = parseCommonHostFlags(argv);
|
|
21
|
-
if (!common.ok) {
|
|
22
|
-
io.stderr(`${common.reason}\n`);
|
|
23
|
-
return USAGE_ERR;
|
|
24
|
-
}
|
|
25
|
-
const positional = common.rest.filter((arg) => !arg.startsWith("--"));
|
|
26
|
-
if (positional.length !== 1) {
|
|
27
|
-
io.stderr("usage: aex status <run-id> [common flags]\n");
|
|
28
|
-
return USAGE_ERR;
|
|
29
|
-
}
|
|
30
|
-
const runId = positional[0]!;
|
|
31
|
-
|
|
32
|
-
const http = makeHttpClient(io, common.flags);
|
|
33
|
-
try {
|
|
34
|
-
const run = await operations.getRun(http, runId);
|
|
35
|
-
io.stdout(JSON.stringify(run) + "\n");
|
|
36
|
-
return SUCCESS;
|
|
37
|
-
} catch (err) {
|
|
38
|
-
return emitJsonError(io, "status_failed", (err as Error).message ?? "status fetch failed", { runId });
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/host/wait.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex wait <run-id> [--timeout <dur>] [--interval <dur>]`
|
|
3
|
-
*
|
|
4
|
-
* Block until the run reaches a terminal status (the host-side mirror of
|
|
5
|
-
* the SDK's `client.waitForRun` / `client.wait`), then print the final
|
|
6
|
-
* `Run` record as JSON. Exits 0 when the run `succeeded`, RUNTIME_ERR
|
|
7
|
-
* when it reached a non-succeeded terminal status, and TIMEOUT_ERR when
|
|
8
|
-
* the `--timeout` deadline elapsed first.
|
|
9
|
-
*
|
|
10
|
-
* Where `events --follow` streams the event log, `wait` is the quiet
|
|
11
|
-
* "tell me when it's done and what the outcome was" verb — one final
|
|
12
|
-
* line of JSON, script-friendly exit code.
|
|
13
|
-
*/
|
|
14
|
-
import { operations, TERMINAL_RUN_STATUSES } from "@aexhq/contracts";
|
|
15
|
-
import type { CliIO } from "../internal.js";
|
|
16
|
-
import {
|
|
17
|
-
type CliExitCode,
|
|
18
|
-
SUCCESS,
|
|
19
|
-
RUNTIME_ERR,
|
|
20
|
-
TIMEOUT_ERR,
|
|
21
|
-
USAGE_ERR,
|
|
22
|
-
emitJsonError,
|
|
23
|
-
makeHttpClient,
|
|
24
|
-
parseCommonHostFlags,
|
|
25
|
-
parseDuration,
|
|
26
|
-
refuseInsideManagedRun,
|
|
27
|
-
takeOptionFlag
|
|
28
|
-
} from "./common.js";
|
|
29
|
-
|
|
30
|
-
// Membership-tested against the loose `string` run status from the BFF, so we
|
|
31
|
-
// back it with the canonical terminal set rather than a drift-prone local list.
|
|
32
|
-
const TERMINAL_STATUSES = new Set<string>(TERMINAL_RUN_STATUSES);
|
|
33
|
-
|
|
34
|
-
const DEFAULT_INTERVAL_MS = 2_000;
|
|
35
|
-
|
|
36
|
-
export async function runWaitCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
37
|
-
if (await refuseInsideManagedRun(io, "wait")) return USAGE_ERR;
|
|
38
|
-
|
|
39
|
-
const common = parseCommonHostFlags(argv);
|
|
40
|
-
if (!common.ok) {
|
|
41
|
-
io.stderr(`${common.reason}\n`);
|
|
42
|
-
return USAGE_ERR;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const timeoutFlag = takeOptionFlag(common.rest, "--timeout");
|
|
46
|
-
const intervalFlag = takeOptionFlag(timeoutFlag.remaining, "--interval");
|
|
47
|
-
|
|
48
|
-
let timeoutMs: number | null = null;
|
|
49
|
-
if (timeoutFlag.value !== undefined) {
|
|
50
|
-
const parsed = parseDuration(timeoutFlag.value);
|
|
51
|
-
if (parsed.error) {
|
|
52
|
-
io.stderr(`--timeout: ${parsed.error}\n`);
|
|
53
|
-
return USAGE_ERR;
|
|
54
|
-
}
|
|
55
|
-
timeoutMs = parsed.ms;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
let intervalMs = DEFAULT_INTERVAL_MS;
|
|
59
|
-
if (intervalFlag.value !== undefined) {
|
|
60
|
-
const parsed = parseDuration(intervalFlag.value);
|
|
61
|
-
if (parsed.error) {
|
|
62
|
-
io.stderr(`--interval: ${parsed.error}\n`);
|
|
63
|
-
return USAGE_ERR;
|
|
64
|
-
}
|
|
65
|
-
intervalMs = parsed.ms!;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const positional = intervalFlag.remaining.filter((arg) => !arg.startsWith("--"));
|
|
69
|
-
if (positional.length !== 1) {
|
|
70
|
-
io.stderr("usage: aex wait <run-id> [--timeout <dur>] [--interval <dur>] [common flags]\n");
|
|
71
|
-
return USAGE_ERR;
|
|
72
|
-
}
|
|
73
|
-
const runId = positional[0]!;
|
|
74
|
-
|
|
75
|
-
const http = makeHttpClient(io, common.flags);
|
|
76
|
-
const deadline = timeoutMs === null ? Number.POSITIVE_INFINITY : Date.now() + timeoutMs;
|
|
77
|
-
|
|
78
|
-
// Emit the timeout JSON error body (via emitJsonError's side effect)
|
|
79
|
-
// but return the dedicated TIMEOUT_ERR code rather than its RUNTIME_ERR.
|
|
80
|
-
const timeout = (extra: Record<string, unknown>): CliExitCode => {
|
|
81
|
-
emitJsonError(io, "wait_timeout", `timed out after ${timeoutMs}ms waiting for run to finish`, { runId, ...extra });
|
|
82
|
-
return TIMEOUT_ERR;
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
while (true) {
|
|
86
|
-
let run;
|
|
87
|
-
try {
|
|
88
|
-
run = await operations.getRun(http, runId);
|
|
89
|
-
} catch (err) {
|
|
90
|
-
// Transient read failures are non-fatal until the deadline — a slow
|
|
91
|
-
// BFF or a brief network blip shouldn't abort a multi-minute wait.
|
|
92
|
-
io.stderr(`(transient) status poll failed: ${(err as Error).message}\n`);
|
|
93
|
-
if (Date.now() >= deadline) return timeout({});
|
|
94
|
-
await sleep(intervalMs);
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (TERMINAL_STATUSES.has(run.status)) {
|
|
99
|
-
io.stdout(JSON.stringify(run) + "\n");
|
|
100
|
-
return run.status === "succeeded" ? SUCCESS : RUNTIME_ERR;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (Date.now() >= deadline) return timeout({ lastStatus: run.status });
|
|
104
|
-
await sleep(intervalMs);
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function sleep(ms: number): Promise<void> {
|
|
109
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
110
|
-
}
|
package/src/host/whoami.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `aex whoami` — GET /api/whoami. Lets agents confirm the token
|
|
3
|
-
* resolves to a workspace + scopes before submitting a real run.
|
|
4
|
-
*
|
|
5
|
-
* `--workspace` is NOT required: the whoami endpoint resolves the
|
|
6
|
-
* principal by the bearer alone and tells the caller which workspace
|
|
7
|
-
* the token belongs to.
|
|
8
|
-
*/
|
|
9
|
-
import { operations } from "@aexhq/contracts";
|
|
10
|
-
import type { CliIO } from "../internal.js";
|
|
11
|
-
import {
|
|
12
|
-
type CliExitCode,
|
|
13
|
-
SUCCESS,
|
|
14
|
-
USAGE_ERR,
|
|
15
|
-
emitJsonError,
|
|
16
|
-
makeHttpClient,
|
|
17
|
-
parseCommonHostFlags,
|
|
18
|
-
refuseInsideManagedRun
|
|
19
|
-
} from "./common.js";
|
|
20
|
-
|
|
21
|
-
export async function runWhoamiCmd(io: CliIO, argv: readonly string[]): Promise<CliExitCode> {
|
|
22
|
-
if (await refuseInsideManagedRun(io, "whoami")) return USAGE_ERR;
|
|
23
|
-
|
|
24
|
-
const common = parseCommonHostFlags(argv);
|
|
25
|
-
if (!common.ok) {
|
|
26
|
-
io.stderr(`${common.reason}\n`);
|
|
27
|
-
return USAGE_ERR;
|
|
28
|
-
}
|
|
29
|
-
if (common.rest.length > 0) {
|
|
30
|
-
io.stderr(`unexpected arguments: ${common.rest.join(" ")}\n`);
|
|
31
|
-
return USAGE_ERR;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const http = makeHttpClient(io, common.flags);
|
|
35
|
-
try {
|
|
36
|
-
const me = await operations.whoami(http);
|
|
37
|
-
io.stdout(JSON.stringify(me) + "\n");
|
|
38
|
-
return SUCCESS;
|
|
39
|
-
} catch (err) {
|
|
40
|
-
return emitJsonError(io, "whoami_failed", (err as Error).message ?? "whoami failed");
|
|
41
|
-
}
|
|
42
|
-
}
|
package/src/index.ts
DELETED
package/src/internal.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* aex CLI — internal IO surface.
|
|
3
|
-
*
|
|
4
|
-
* This module exports the dependency-injection types and a thin
|
|
5
|
-
* `createCli()` factory that the entrypoint (`cli.ts`) calls with real
|
|
6
|
-
* IO. Tests import this module directly and inject fakes — there are
|
|
7
|
-
* NO `process.env.AEX_*` reads in the production code paths
|
|
8
|
-
* (mechanical test in Phase 9 enforces this against the built bundle).
|
|
9
|
-
*/
|
|
10
|
-
import type {
|
|
11
|
-
ProxyIndexFile,
|
|
12
|
-
ProxyIndexEntry,
|
|
13
|
-
ProxyMethod,
|
|
14
|
-
ProxyResponseMode
|
|
15
|
-
} from "@aexhq/contracts";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Manifest file path inside the run container (always present).
|
|
19
|
-
*
|
|
20
|
-
* The runner writes the manifest to this fixed path before invoking
|
|
21
|
-
* in-container `aex` commands.
|
|
22
|
-
*/
|
|
23
|
-
export const AEX_INDEX_PATH = "/mnt/session/uploads/aex/index.json";
|
|
24
|
-
|
|
25
|
-
/** Per-run bearer file path (only present when proxyEndpoints declared). */
|
|
26
|
-
export const AEX_RUN_TOKEN_PATH = "/mnt/session/uploads/aex/run-token";
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* IO surface the CLI depends on. The production entrypoint passes real
|
|
30
|
-
* implementations; tests pass fakes. New IO methods must be additive
|
|
31
|
-
* and orthogonal: a fake that lacks a method should still satisfy the
|
|
32
|
-
* subset of subcommands that don't touch that method.
|
|
33
|
-
*/
|
|
34
|
-
export interface CliIO {
|
|
35
|
-
readonly readFile: (path: string) => Promise<string>;
|
|
36
|
-
readonly writeFile: (path: string, data: Uint8Array) => Promise<void>;
|
|
37
|
-
readonly fetchImpl: typeof fetch;
|
|
38
|
-
readonly stdout: (chunk: string) => void;
|
|
39
|
-
readonly stderr: (chunk: string) => void;
|
|
40
|
-
readonly exit: (code: number) => void;
|
|
41
|
-
readonly argv: readonly string[];
|
|
42
|
-
/** Current working directory; used to resolve relative `--out` paths. */
|
|
43
|
-
readonly cwd: () => string;
|
|
44
|
-
/**
|
|
45
|
-
* Walk a directory and return every regular-file entry beneath it
|
|
46
|
-
* (recursive). Used only by the internal `outputs sync` subcommand
|
|
47
|
-
* that the worker invokes from inside a managed run container at
|
|
48
|
-
* session terminal. Returns `null` (not throws) when the directory
|
|
49
|
-
* does not exist or is unreadable — `outputs sync` records the
|
|
50
|
-
* miss in its structured output and continues to the next dir.
|
|
51
|
-
*/
|
|
52
|
-
readonly walkDirectory?: (root: string) => Promise<readonly OutputsSyncFileEntry[] | null>;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface OutputsSyncFileEntry {
|
|
56
|
-
/** Absolute path inside the container. */
|
|
57
|
-
readonly path: string;
|
|
58
|
-
/** File size in bytes. */
|
|
59
|
-
readonly sizeBytes: number;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** Re-exported helpers used by CLI consumers. */
|
|
63
|
-
export type { ProxyIndexFile, ProxyIndexEntry, ProxyMethod, ProxyResponseMode };
|
package/src/outputs-sync.ts
DELETED
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* aex outputs sync — IN-CONTAINER ONLY internal subcommand.
|
|
3
|
-
*
|
|
4
|
-
* This is NOT a user-facing verb. It is a legacy/internal directory walker:
|
|
5
|
-
* callers pass explicit absolute directories, and the command emits a
|
|
6
|
-
* structured JSON line per file to stdout. Managed runs now capture output by
|
|
7
|
-
* filesystem baseline/delta; there is no default output directory and no
|
|
8
|
-
* synthetic terminal agent turn.
|
|
9
|
-
*
|
|
10
|
-
* The subcommand:
|
|
11
|
-
* - REFUSES to run outside a managed run (no AEX_INDEX_PATH file).
|
|
12
|
-
* - Walks each provided directory recursively.
|
|
13
|
-
* - Emits one JSON line per file to stdout:
|
|
14
|
-
* {"dir":"/workspace/reports","path":"/workspace/reports/x.txt","sizeBytes":42}
|
|
15
|
-
* - Reports missing dirs via stderr (non-fatal). Exits 0 if at least
|
|
16
|
-
* one dir contained at least one file; 0 also when ALL dirs were
|
|
17
|
-
* empty (best-effort semantics — the worker still records the
|
|
18
|
-
* capture attempt either way).
|
|
19
|
-
*
|
|
20
|
-
* No flags. Each positional argument is one directory to walk.
|
|
21
|
-
*/
|
|
22
|
-
import { AEX_INDEX_PATH, type CliIO } from "./internal.js";
|
|
23
|
-
import { RUNTIME_ERR, SUCCESS, USAGE_ERR, type CliExitCode } from "./host/common.js";
|
|
24
|
-
|
|
25
|
-
export async function runOutputsSyncCmd(io: CliIO, dirs: readonly string[]): Promise<CliExitCode> {
|
|
26
|
-
if (dirs.length === 0) {
|
|
27
|
-
io.stderr("usage: aex outputs sync <dir> [<dir> ...]\n");
|
|
28
|
-
return USAGE_ERR;
|
|
29
|
-
}
|
|
30
|
-
try {
|
|
31
|
-
await io.readFile(AEX_INDEX_PATH);
|
|
32
|
-
} catch {
|
|
33
|
-
io.stderr(
|
|
34
|
-
"`aex outputs sync` is an in-container internal command and cannot run on the host.\n"
|
|
35
|
-
);
|
|
36
|
-
return USAGE_ERR;
|
|
37
|
-
}
|
|
38
|
-
if (!io.walkDirectory) {
|
|
39
|
-
io.stderr("aex outputs sync: walkDirectory IO is not available\n");
|
|
40
|
-
return RUNTIME_ERR;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
let scanned = 0;
|
|
44
|
-
let missing = 0;
|
|
45
|
-
for (const dir of dirs) {
|
|
46
|
-
if (!dir.startsWith("/")) {
|
|
47
|
-
io.stderr(
|
|
48
|
-
JSON.stringify({ dir, error: "non_absolute_path", message: "skipping non-absolute output dir" }) + "\n"
|
|
49
|
-
);
|
|
50
|
-
missing++;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
let entries;
|
|
54
|
-
try {
|
|
55
|
-
entries = await io.walkDirectory(dir);
|
|
56
|
-
} catch (err) {
|
|
57
|
-
io.stderr(
|
|
58
|
-
JSON.stringify({ dir, error: "walk_failed", message: (err as Error).message ?? "walk failed" }) + "\n"
|
|
59
|
-
);
|
|
60
|
-
missing++;
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
if (entries === null) {
|
|
64
|
-
io.stderr(JSON.stringify({ dir, error: "missing_or_unreadable" }) + "\n");
|
|
65
|
-
missing++;
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
io.stdout(JSON.stringify({ dir, path: entry.path, sizeBytes: entry.sizeBytes }) + "\n");
|
|
70
|
-
scanned++;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
io.stdout(JSON.stringify({ summary: { dirs: dirs.length, files: scanned, missing } }) + "\n");
|
|
75
|
-
return SUCCESS;
|
|
76
|
-
}
|