@alexkroman1/aai-cli 3.2.0 → 5.0.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-BWYgLjiI.mjs";
2
+ import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-Czjhvaax.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env node
2
+ import { n as log, u as ok } from "./_ui-DXZ9prrM.mjs";
3
+ import { c as validateAgentExport } from "./_utils-C502jKo8.mjs";
4
+ import { t as buildClient } from "./client-bundler-DO3GgO6p.mjs";
5
+ import { buildWorker } from "./worker-bundler.mjs";
6
+ import path from "node:path";
7
+ import { pathToFileURL } from "node:url";
8
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
9
+ import { hash } from "node:crypto";
10
+ import { tmpdir } from "node:os";
11
+ //#region _bundler.ts
12
+ /**
13
+ * Bundle an agent directory: build agent.ts into worker ESM + client files.
14
+ *
15
+ * agent.ts is the single entry point: `export default agent({...})`. The
16
+ * worker self-describes (it exports `__aaiConfig` — see `worker-bundler.ts`),
17
+ * so nothing here evaluates the bundle: the server extracts the config inside
18
+ * a guest sandbox at deploy time.
19
+ */
20
+ async function buildAgentBundle(cwd, opts = {}) {
21
+ const [worker, clientFiles] = await Promise.all([buildWorker(cwd, opts), buildClient(cwd)]);
22
+ return {
23
+ worker,
24
+ clientFiles
25
+ };
26
+ }
27
+ /**
28
+ * Import the worker ESM from a uniquely named temp file and return the
29
+ * AgentDef default export. A real `file:` URL, not a `data:` URL: deploy
30
+ * bundles ship the SDK runtime, whose CJS interop calls
31
+ * `createRequire(import.meta.url)` — which rejects anything that isn't a
32
+ * file URL or absolute path. (The guest harness imports bundles the same
33
+ * way, for the same reason.) The file is removed after import; the module
34
+ * lives on in memory.
35
+ *
36
+ * Each call imports a unique URL, and Node's ESM registry never evicts — so
37
+ * every call retains one bundle for the process lifetime. That is fine for
38
+ * one-shot commands (`aai build`); long-lived callers must go through
39
+ * `createWorkerEvaluator` to at least dedupe identical builds. Evaluating in
40
+ * a discardable context is not an option: tool `execute` functions from the
41
+ * returned AgentDef are called in-process by the dev runtime, which rules
42
+ * out worker threads, and `node:vm` ESM evaluation is still flagged
43
+ * experimental.
44
+ */
45
+ async function evalWorkerBundle(code) {
46
+ const dir = await mkdtemp(path.join(tmpdir(), "aai-worker-"));
47
+ const file = path.join(dir, "worker.mjs");
48
+ let mod;
49
+ try {
50
+ await writeFile(file, code, "utf-8");
51
+ mod = await import(pathToFileURL(file).href);
52
+ } finally {
53
+ await rm(dir, {
54
+ recursive: true,
55
+ force: true
56
+ }).catch(() => void 0);
57
+ }
58
+ const agentDef = mod.default ?? mod;
59
+ validateAgentExport(agentDef);
60
+ return agentDef;
61
+ }
62
+ /**
63
+ * Memoizing wrapper around `evalWorkerBundle` for long-lived callers (the
64
+ * dev server): byte-identical worker code returns the previously evaluated
65
+ * AgentDef without touching the ESM registry. No-op saves and formatter
66
+ * churn are the common watcher events, so this caps the registry leak (see
67
+ * `evalWorkerBundle`) to genuinely-new bundles — the residual one-module-per-
68
+ * distinct-build leak is accepted for the reasons documented there.
69
+ */
70
+ function createWorkerEvaluator() {
71
+ let lastHash;
72
+ let lastAgentDef;
73
+ return async (code) => {
74
+ const codeHash = hash("sha256", code);
75
+ if (lastAgentDef && codeHash === lastHash) return lastAgentDef;
76
+ const agentDef = await evalWorkerBundle(code);
77
+ lastHash = codeHash;
78
+ lastAgentDef = agentDef;
79
+ return agentDef;
80
+ };
81
+ }
82
+ async function executeBuild(cwd) {
83
+ const bundle = await buildAgentBundle(cwd, { minify: true });
84
+ const agentDef = await evalWorkerBundle(bundle.worker);
85
+ log.success("Build complete");
86
+ return ok({
87
+ name: agentDef.name,
88
+ workerBytes: bundle.worker.length
89
+ });
90
+ }
91
+ //#endregion
92
+ export { buildAgentBundle, createWorkerEvaluator, executeBuild };
@@ -1,10 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { a as unwrapCancel, n as log, o as CliError } from "./_ui-CKEIHAtB.mjs";
2
+ import { a as unwrapCancel, n as log, o as CliError } from "./_ui-DXZ9prrM.mjs";
3
3
  import { l as writeJson, o as readJson, r as errorMessage } from "./_utils-C502jKo8.mjs";
4
- import { existsSync } from "node:fs";
5
4
  import path from "node:path";
6
5
  import * as p from "@clack/prompts";
7
- import os from "node:os";
8
6
  import envPaths from "env-paths";
9
7
  import { z } from "zod";
10
8
  //#region _config.ts
@@ -24,32 +22,18 @@ const ProjectConfigSchema = z.object({
24
22
  serverUrl: z.string()
25
23
  });
26
24
  /**
27
- * Config dir the CLI used before switching to env-paths. On Linux it matches
28
- * env-paths exactly; on macOS (XDG-style vs ~/Library/Preferences/aai) and
29
- * Windows (no trailing `Config` segment) it does not.
30
- */
31
- function legacyConfigDir() {
32
- if (process.platform === "win32") return path.join(process.env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming"), "aai");
33
- return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "aai");
34
- }
35
- /**
36
- * Resolve the global config directory, preferring the platform-conventional
37
- * env-paths location while staying backward compatible: an existing config at
38
- * the legacy path keeps winning so already-authenticated users are not
39
- * silently logged out. Injectable for tests.
25
+ * Resolve the global config directory (the platform-conventional env-paths
26
+ * location).
40
27
  *
41
28
  * `AAI_CONFIG_DIR` overrides everything — it exists so tests (and unusual
42
29
  * setups) can redirect ALL global-config reads and writes away from the
43
30
  * user's real config. The test suite's `approveServer` calls used to
44
31
  * permanently pollute `~/.config/aai/config.json` with approved origins.
45
32
  */
46
- function getConfigDir(dirs = {
47
- legacy: legacyConfigDir(),
48
- modern: envPaths("aai", { suffix: "" }).config
49
- }, exists = existsSync) {
33
+ function getConfigDir() {
50
34
  const override = process.env.AAI_CONFIG_DIR?.trim();
51
35
  if (override) return override;
52
- return exists(path.join(dirs.legacy, "config.json")) ? dirs.legacy : dirs.modern;
36
+ return envPaths("aai", { suffix: "" }).config;
53
37
  }
54
38
  async function readProjectConfig(agentDir) {
55
39
  const file = path.join(agentDir, ".aai", "project.json");
@@ -1,129 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { n as log } from "./_ui-CKEIHAtB.mjs";
2
+ import { n as log } from "./_ui-DXZ9prrM.mjs";
3
3
  import { r as errorMessage, t as errorCode } from "./_utils-C502jKo8.mjs";
4
4
  import { n as fallbackHtmlPlugin } from "./client-bundler-DO3GgO6p.mjs";
5
5
  import { buildWorker } from "./worker-bundler.mjs";
6
- import { createWorkerEvaluator } from "./_bundler-DnZnKsIU.mjs";
7
- import { n as ensureApiKey } from "./_config-BWYgLjiI.mjs";
8
- import { t as resolveServerEnv } from "./_server-common-Bv-4Rskq.mjs";
6
+ import { createWorkerEvaluator } from "./_bundler-DdmuZzeR.mjs";
7
+ import { n as ensureApiKey } from "./_config-Czjhvaax.mjs";
8
+ import { t as resolveServerEnv } from "./_server-common-B75oco06.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { existsSync } from "node:fs";
11
11
  import path from "node:path";
12
- import fs from "node:fs/promises";
12
+ import { setTimeout } from "node:timers/promises";
13
13
  import { requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
14
14
  import { watch } from "chokidar";
15
15
  import getPort, { portNumbers } from "get-port";
16
16
  import pDebounce from "p-debounce";
17
- //#region _dev-bundler.ts
18
- /**
19
- * Fast worker builds for `aai dev`.
20
- *
21
- * The deploy path (`buildWorker` in `worker-bundler.ts`) is a full Vite
22
- * pipeline pass — right for a one-shot `aai deploy`, but 1–3 s per save when
23
- * the dev watcher runs it on every change. This module builds with Rolldown
24
- * directly (the native bundler Vite 8 itself runs on, so it adds no install
25
- * weight), skipping Vite's config/plugin/CSS machinery: a from-scratch build
26
- * of a typical agent lands in tens of ms. Deploy keeps Vite untouched, so
27
- * nothing that ships is produced by this path.
28
- *
29
- * Parity with `buildWorker` where it matters for dev:
30
- *
31
- * - single-file ESM output, unminified (dev builds never minify);
32
- * - `node:` builtins external (Rolldown's `platform: "node"`), everything
33
- * else — zod, workspace deps, local imports — bundled in;
34
- * - `.md` imports resolve to their raw text (`mdPlugin` below is
35
- * `rawMdPlugin`'s transform), and Vite-style `?raw` suffix imports are
36
- * honored via `rawSuffixPlugin`.
37
- *
38
- * Known dev/deploy differences, accepted: Vite's lib build applies
39
- * `define`/`import.meta.env` replacements this path does not. When a build
40
- * fails for anything other than a compile error in the agent's code, the
41
- * caller falls back to the cold Vite path (see `_dev-server.ts`), so a
42
- * resolution gap here degrades to the old slow-but-correct behavior rather
43
- * than a broken dev server.
44
- *
45
- * Rolldown does not touch `process.env` the way Vite's `build()` does, so
46
- * the `withPreservedNodeEnv` wrapper Vite builds need is not required here.
47
- */
48
- const RAW_NAMESPACE = "\0aai-raw:";
49
- /**
50
- * Vite serves `import x from "./file?raw"` as the file's text. Rolldown
51
- * treats the suffix as part of the filename, so resolve it explicitly and
52
- * load the real file as a string export.
53
- */
54
- const rawSuffixPlugin = {
55
- name: "aai-raw-suffix",
56
- resolveId: {
57
- filter: { id: /\?raw$/ },
58
- handler(id, importer) {
59
- const file = id.slice(0, -4);
60
- const base = importer ? path.dirname(importer) : process.cwd();
61
- return RAW_NAMESPACE + path.resolve(base, file);
62
- }
63
- },
64
- load: {
65
- filter: { id: new RegExp(`^${RAW_NAMESPACE}`) },
66
- async handler(id) {
67
- const text = await fs.readFile(id.slice(9), "utf8");
68
- return `export default ${JSON.stringify(text)};`;
69
- }
70
- }
71
- };
72
- /** `.md` imports resolve to their raw text (rawMdPlugin parity). */
73
- const mdPlugin = {
74
- name: "aai-raw-md",
75
- load: {
76
- filter: { id: /\.md$/ },
77
- async handler(id) {
78
- const text = await fs.readFile(id, "utf8");
79
- return `export default ${JSON.stringify(text)};`;
80
- }
81
- }
82
- };
83
- /**
84
- * True for bundler build failures — compile/resolve errors in the code being
85
- * built (Rolldown aggregates its diagnostics onto an `errors` array, the same
86
- * shape esbuild used). Anything else coming out of `build()` is a
87
- * bundler-infrastructure problem, which callers treat as "fall back to the
88
- * cold Vite path" rather than "the user's code is broken".
89
- */
90
- function isBundlerBuildFailure(err) {
91
- return err instanceof Error && "errors" in err && Array.isArray(err.errors);
92
- }
93
- /**
94
- * Create a dev builder for the agent at `cwd`.
95
- *
96
- * Each `build()` is a from-scratch Rolldown pass — native-code bundling is
97
- * fast enough (tens of ms for a typical agent) that no incremental context
98
- * is worth holding between saves. `dispose()` exists for interface parity
99
- * with resource-holding builders and is a no-op.
100
- */
101
- function createDevWorkerBuilder(cwd) {
102
- return {
103
- async build() {
104
- const { rolldown } = await import("rolldown");
105
- const bundle = await rolldown({
106
- input: path.join(cwd, "agent.ts"),
107
- cwd,
108
- platform: "node",
109
- logLevel: "silent",
110
- plugins: [rawSuffixPlugin, mdPlugin]
111
- });
112
- try {
113
- const file = (await bundle.generate({
114
- format: "esm",
115
- minify: false
116
- })).output[0];
117
- if (!file) throw new Error("Rolldown produced no output for agent.ts");
118
- return file.code;
119
- } finally {
120
- await bundle.close().catch(() => void 0);
121
- }
122
- },
123
- async dispose() {}
124
- };
125
- }
126
- //#endregion
127
17
  //#region _dev-server.ts
128
18
  /**
129
19
  * Dev server for directory-based agents.
@@ -133,12 +23,36 @@ function createDevWorkerBuilder(cwd) {
133
23
  * file changes and restarts automatically. Optionally runs Vite for
134
24
  * client SPA HMR.
135
25
  */
26
+ /**
27
+ * Warnings about the agent's credentials, computed against the `.env`-derived
28
+ * env and the shell. Pure so it is directly testable; `resolveAgentEnv` logs
29
+ * each entry. Three cases, in increasing subtlety:
30
+ *
31
+ * - a provider key found nowhere → the first session will fail auth;
32
+ * - a provider key found only in the shell → works here (the
33
+ * `withHostCredentialFallback` ergonomic) but is invisible to `aai deploy`,
34
+ * which uploads `.env` — the classic "works locally, dead on deploy";
35
+ * - a declared `requiredEnv` key absent from `.env` → `ctx.env` won't contain
36
+ * it at all: custom keys never fall back to the shell, so a shell export
37
+ * can't mask one that would be missing both here and after deploy.
38
+ */
39
+ function agentEnvWarnings(agentDef, env, shellEnv = process.env) {
40
+ const s = (names) => names.length > 1 ? "s" : "";
41
+ const them = (names) => names.length > 1 ? "them" : "it";
42
+ const required = requiredProviderEnvVars(agentDef);
43
+ const warnings = [];
44
+ const missing = required.filter((name) => !(env[name] || shellEnv[name]));
45
+ if (missing.length > 0) warnings.push(`Missing provider credential${s(missing)}: ${missing.join(", ")}. Set ${them(missing)} in .env or the environment.`);
46
+ const shellOnly = required.filter((name) => !env[name] && shellEnv[name]);
47
+ if (shellOnly.length > 0) warnings.push(`${shellOnly.join(", ")} resolved from your shell, not .env — deployed agents won't have ${them(shellOnly)}. Declare ${them(shellOnly)} in .env before \`aai deploy\`.`);
48
+ const declared = (agentDef.requiredEnv ?? []).filter((name) => !env[name]);
49
+ if (declared.length > 0) warnings.push(`Missing requiredEnv key${s(declared)} declared by the agent: ${declared.join(", ")}. Set ${them(declared)} in .env — ctx.env will not contain ${them(declared)} otherwise.`);
50
+ return warnings;
51
+ }
136
52
  async function resolveAgentEnv(root, agentDef) {
137
53
  const env = await resolveServerEnv(root);
138
- const required = requiredProviderEnvVars(agentDef);
139
- if (required.includes("ASSEMBLYAI_API_KEY") && !env.ASSEMBLYAI_API_KEY) env.ASSEMBLYAI_API_KEY = await ensureApiKey();
140
- const missing = required.filter((name) => !(env[name] || process.env[name]));
141
- if (missing.length > 0) log.warn(`Missing provider credential${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}. Set ${missing.length > 1 ? "them" : "it"} in .env or the environment.`);
54
+ if (requiredProviderEnvVars(agentDef).includes("ASSEMBLYAI_API_KEY") && !env.ASSEMBLYAI_API_KEY) env.ASSEMBLYAI_API_KEY = await ensureApiKey();
55
+ for (const warning of agentEnvWarnings(agentDef, env)) log.warn(warning);
142
56
  return env;
143
57
  }
144
58
  /**
@@ -170,23 +84,14 @@ function devBindHost() {
170
84
  * stay in Node's ESM registry, so edits to them are ignored on reload.
171
85
  * Bundling picks them up.
172
86
  *
173
- * The bundle comes from the fast Rolldown builder (`_dev-bundler.ts`)
174
- * rather than the deploy path's cold Vite build a save rebuilds in tens of
175
- * ms instead of 1–3 s. Compile errors in the agent's code propagate (the
176
- * restart loop reports them and keeps the old server); any other builder
177
- * failure falls back to the cold Vite build so a Rolldown-specific gap can't
178
- * take the dev loop down. Evaluation goes through the memoizing evaluator so
87
+ * The bundle comes from the same Vite pass deploy runs (`buildWorker`), so
88
+ * dev and deploy can't drift; a warm rebuild is well under 100ms. Compile
89
+ * errors in the agent's code propagate the restart loop reports them and
90
+ * keeps the old server. Evaluation goes through the memoizing evaluator so
179
91
  * a no-op save doesn't leak another module into the ESM registry.
180
92
  */
181
- async function loadAgentDefWith(cwd, builder, evaluate) {
182
- let code;
183
- try {
184
- code = await builder.build();
185
- } catch (err) {
186
- if (isBundlerBuildFailure(err)) throw err;
187
- code = await buildWorker(cwd);
188
- }
189
- return evaluate(code);
93
+ async function loadAgentDef(cwd, evaluate) {
94
+ return evaluate(await buildWorker(cwd, { runtime: false }));
190
95
  }
191
96
  /**
192
97
  * True for paths that should never trigger a restart: anything inside
@@ -283,11 +188,10 @@ async function startDevServer(opts) {
283
188
  const backendPort = hasClient ? await getPort({ port: portNumbers(port + 1, port + 100) }) : port;
284
189
  const vitePort = port;
285
190
  const clientDirOpt = hasClient ? {} : { clientDir: resolveDefaultClientDir() };
286
- const devBuilder = createDevWorkerBuilder(cwd);
287
- const evaluateWorker = createWorkerEvaluator(cwd);
191
+ const evaluateWorker = createWorkerEvaluator();
288
192
  /** Full build sequence, shared by initial startup and every restart. */
289
193
  async function buildServer() {
290
- const agentDef = await loadAgentDefWith(cwd, devBuilder, evaluateWorker);
194
+ const agentDef = await loadAgentDef(cwd, evaluateWorker);
291
195
  const env = await resolveAgentEnv(cwd, agentDef);
292
196
  const providerEnv = withHostCredentialFallback(env);
293
197
  const runtime = createRuntime({
@@ -335,7 +239,6 @@ async function startDevServer(opts) {
335
239
  }
336
240
  } catch (err) {
337
241
  await watcher.close().catch(() => void 0);
338
- await devBuilder.dispose().catch(() => void 0);
339
242
  await viteServer?.close().catch(() => void 0);
340
243
  throw err;
341
244
  }
@@ -389,7 +292,7 @@ async function startDevServer(opts) {
389
292
  return;
390
293
  } catch (err) {
391
294
  if (attempt >= LISTEN_ATTEMPTS || closed) throw err;
392
- await new Promise((resolve) => setTimeout(resolve, LISTEN_RETRY_DELAY_MS));
295
+ await setTimeout(LISTEN_RETRY_DELAY_MS);
393
296
  }
394
297
  }
395
298
  let cleanupPromise;
@@ -397,7 +300,6 @@ async function startDevServer(opts) {
397
300
  cleanupPromise ??= (async () => {
398
301
  closed = true;
399
302
  await watcher.close().catch(() => void 0);
400
- await devBuilder.dispose().catch(() => void 0);
401
303
  await viteServer?.close().catch(() => void 0);
402
304
  await currentServer.close().catch(() => void 0);
403
305
  })();
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as isEexist, l as writeJson, o as readJson, r as errorMessage } from "./_utils-C502jKo8.mjs";
3
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-4rsUGPhx.mjs";
3
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-D39qatZJ.mjs";
4
4
  import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
@@ -17,7 +17,7 @@ async function resolveTemplatesDir() {
17
17
  root: process.env.AAI_TEMPLATES_DIR,
18
18
  cleanup: noCleanup
19
19
  };
20
- const monorepoRoot = isDevMode() ? getMonorepoRoot() : null;
20
+ const monorepoRoot = getMonorepoRoot();
21
21
  if (monorepoRoot) return {
22
22
  root: path.join(monorepoRoot, "packages", "aai-templates"),
23
23
  cleanup: noCleanup
@@ -117,9 +117,7 @@ aai secret delete MY_KEY # Remove a secret
117
117
  const WORKSPACE_PKG_DIRS = {
118
118
  "@alexkroman1/aai": "aai",
119
119
  "@alexkroman1/aai-cli": "aai-cli",
120
- "@alexkroman1/aai-ui": "aai-ui",
121
- "aai-server": "aai-server",
122
- "aai-templates": "aai-templates"
120
+ "@alexkroman1/aai-ui": "aai-ui"
123
121
  };
124
122
  /** Rewrite workspace deps to link: paths so pnpm links to local source. */
125
123
  async function patchPackageJsonForWorkspace(targetDir) {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
- import fs from "node:fs/promises";
4
3
  import { parseEnv } from "node:util";
4
+ import fs from "node:fs/promises";
5
5
  //#region _server-common.ts
6
6
  /**
7
7
  * Build the `ctx.env` record that agent tools will see at runtime.
@@ -21,9 +21,15 @@ import { parseEnv } from "node:util";
21
21
  */
22
22
  async function resolveServerEnv(cwd, baseEnv) {
23
23
  let fileEntries = {};
24
- if (cwd) try {
25
- fileEntries = parseEnv(await fs.readFile(path.join(cwd, ".env"), "utf-8"));
26
- } catch {}
24
+ if (cwd) {
25
+ let content = null;
26
+ try {
27
+ content = await fs.readFile(path.join(cwd, ".env"), "utf-8");
28
+ } catch (err) {
29
+ if (err.code !== "ENOENT") throw err;
30
+ }
31
+ if (content !== null) fileEntries = parseEnv(content);
32
+ }
27
33
  const source = baseEnv ?? process.env;
28
34
  const env = {};
29
35
  for (const [key, fileVal] of Object.entries(fileEntries)) {
@@ -0,0 +1,17 @@
1
+ #!/usr/bin/env node
2
+ import { n as log, o as CliError } from "./_ui-DXZ9prrM.mjs";
3
+ import { typecheckProject } from "./typecheck.mjs";
4
+ //#region _typecheck-gate.ts
5
+ /**
6
+ * The build/deploy typecheck gate: run the project's own `tsc --noEmit`
7
+ * (see `typecheck.ts`) and turn a failure into a structured CliError. The
8
+ * bundlers strip types unchecked, so without this a type-broken agent
9
+ * ships and misbehaves at runtime instead of failing here.
10
+ */
11
+ async function assertTypechecks(cwd) {
12
+ log.step("Type checking…");
13
+ const result = await typecheckProject(cwd);
14
+ if (!result.ok) throw new CliError("typecheck_failed", result.output, "Fix the type errors, or pass --skipTypecheck to build anyway");
15
+ }
16
+ //#endregion
17
+ export { assertTypechecks };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
+ import { styleText } from "node:util";
2
3
  import * as p from "@clack/prompts";
3
- import pc from "picocolors";
4
4
  //#region _output.ts
5
5
  /**
6
6
  * Determine output mode from CLI flags and TTY state.
@@ -93,7 +93,7 @@ function unwrapCancel(result) {
93
93
  }
94
94
  /** Format a URL for display. */
95
95
  function fmtUrl(url) {
96
- return pc.cyanBright(url);
96
+ return styleText("cyanBright", url);
97
97
  }
98
98
  /**
99
99
  * Parse and validate a port string. Returns the numeric port or throws.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { c as getOutputMode, d as writeLine, i as silenceOutput, l as installStdoutGuard, n as log, o as CliError, s as fail } from "./_ui-CKEIHAtB.mjs";
2
+ import { c as getOutputMode, d as writeLine, i as silenceOutput, l as installStdoutGuard, n as log, o as CliError, s as fail } from "./_ui-DXZ9prrM.mjs";
3
3
  import { i as fileExists, r as errorMessage, s as resolveCwd } from "./_utils-C502jKo8.mjs";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import path from "node:path";
@@ -108,10 +108,6 @@ const init = defineCommand({
108
108
  server: sharedArgs.server,
109
109
  yes: sharedArgs.yes,
110
110
  json: sharedArgs.json,
111
- skipApi: {
112
- type: "boolean",
113
- description: "Deprecated no-op: the API key is now only requested when deploying"
114
- },
115
111
  skipDeploy: {
116
112
  type: "boolean",
117
113
  description: "Skip deploy after scaffolding"
@@ -119,7 +115,7 @@ const init = defineCommand({
119
115
  },
120
116
  async run({ args }) {
121
117
  await runCommand(args, async (mode) => {
122
- const { executeInit } = await import("./init-mdG0rdRZ.mjs");
118
+ const { executeInit } = await import("./init-B53rR3yD.mjs");
123
119
  return executeInit({
124
120
  dir: args.dir,
125
121
  force: args.force,
@@ -143,7 +139,7 @@ const dev = defineCommand({
143
139
  async run({ args }) {
144
140
  await runCommand(args, async () => {
145
141
  const cwd = await setup({ agent: true });
146
- const { executeDev } = await import("./dev-DlseKVVF.mjs");
142
+ const { executeDev } = await import("./dev-C8o-p13B.mjs");
147
143
  return executeDev({
148
144
  cwd,
149
145
  port: args.port
@@ -160,7 +156,7 @@ const test = defineCommand({
160
156
  async run({ args }) {
161
157
  await runCommand(args, async () => {
162
158
  const cwd = await setup();
163
- const { executeTest } = await import("./test-B3BkLNn_.mjs");
159
+ const { executeTest } = await import("./test-CgWhus_Z.mjs");
164
160
  return executeTest(cwd);
165
161
  });
166
162
  }
@@ -175,13 +171,17 @@ const build = defineCommand({
175
171
  skipTests: {
176
172
  type: "boolean",
177
173
  description: "Skip running tests before build"
174
+ },
175
+ skipTypecheck: {
176
+ type: "boolean",
177
+ description: "Skip type checking before build"
178
178
  }
179
179
  },
180
180
  async run({ args }) {
181
181
  await runCommand(args, async () => {
182
182
  const cwd = await setup({ agent: true });
183
183
  if (!args.skipTests) {
184
- const { classifyVitestError, runVitest } = await import("./test-B3BkLNn_.mjs");
184
+ const { classifyVitestError, runVitest } = await import("./test-CgWhus_Z.mjs");
185
185
  const toCliError = (err) => {
186
186
  const { code, message } = classifyVitestError(err);
187
187
  return new CliError(code, message, "Re-run with --skipTests to build without tests", { cause: err });
@@ -192,7 +192,11 @@ const build = defineCommand({
192
192
  throw toCliError(err);
193
193
  }
194
194
  }
195
- const { executeBuild } = await import("./_bundler-DnZnKsIU.mjs");
195
+ if (!args.skipTypecheck) {
196
+ const { assertTypechecks } = await import("./_typecheck-gate-B1GcKZX-.mjs");
197
+ await assertTypechecks(cwd);
198
+ }
199
+ const { executeBuild } = await import("./_bundler-DdmuZzeR.mjs");
196
200
  return executeBuild(cwd);
197
201
  });
198
202
  }
@@ -204,15 +208,25 @@ const deploy = defineCommand({
204
208
  },
205
209
  args: {
206
210
  server: sharedArgs.server,
207
- json: sharedArgs.json
211
+ json: sharedArgs.json,
212
+ allowMissingSecrets: {
213
+ type: "boolean",
214
+ description: "Deploy even when the agent's providers are missing credentials (the server warns instead of rejecting; set them afterwards with `aai secret put`)"
215
+ },
216
+ skipTypecheck: {
217
+ type: "boolean",
218
+ description: "Skip type checking before deploy"
219
+ }
208
220
  },
209
221
  async run({ args }) {
210
222
  await runCommand(args, async () => {
211
223
  const cwd = await setup({ agent: true });
212
- const { executeDeploy } = await import("./deploy-CQ69yWAK.mjs");
224
+ const { executeDeploy } = await import("./deploy-D-dnt9L8.mjs");
213
225
  return executeDeploy({
214
226
  cwd,
215
- ...args.server ? { server: args.server } : {}
227
+ ...args.server ? { server: args.server } : {},
228
+ ...args.allowMissingSecrets ? { allowMissingSecrets: true } : {},
229
+ ...args.skipTypecheck ? { skipTypecheck: true } : {}
216
230
  });
217
231
  });
218
232
  }
@@ -229,7 +243,7 @@ const del = defineCommand({
229
243
  async run({ args }) {
230
244
  await runCommand(args, async () => {
231
245
  const cwd = await setup();
232
- const { executeDelete } = await import("./delete-BHSeDVBT.mjs");
246
+ const { executeDelete } = await import("./delete-K24hW1ab.mjs");
233
247
  return executeDelete({
234
248
  cwd,
235
249
  ...args.server ? { server: args.server } : {}
@@ -260,7 +274,7 @@ const secret = defineCommand({
260
274
  async run({ args }) {
261
275
  await runCommand(args, async (mode) => {
262
276
  const cwd = await setup();
263
- const { executeSecretPut, readStdin } = await import("./secret-CJcR121q.mjs");
277
+ const { executeSecretPut, readStdin } = await import("./secret-C4unwCEw.mjs");
264
278
  const value = mode === "json" ? await readStdin() : void 0;
265
279
  if (mode === "json" && !value) throw new CliError("no_input", "No value provided", "Pipe secret value to stdin");
266
280
  return executeSecretPut(cwd, args.name, value, args.server);
@@ -284,7 +298,7 @@ const secret = defineCommand({
284
298
  async run({ args }) {
285
299
  await runCommand(args, async () => {
286
300
  const cwd = await setup();
287
- const { executeSecretDelete } = await import("./secret-CJcR121q.mjs");
301
+ const { executeSecretDelete } = await import("./secret-C4unwCEw.mjs");
288
302
  return executeSecretDelete(cwd, args.name, args.server);
289
303
  });
290
304
  }
@@ -301,7 +315,7 @@ const secret = defineCommand({
301
315
  async run({ args }) {
302
316
  await runCommand(args, async () => {
303
317
  const cwd = await setup();
304
- const { executeSecretList } = await import("./secret-CJcR121q.mjs");
318
+ const { executeSecretList } = await import("./secret-C4unwCEw.mjs");
305
319
  return executeSecretList(cwd, args.server);
306
320
  });
307
321
  }
@@ -336,7 +350,7 @@ const storage = defineCommand({
336
350
  },
337
351
  async run({ args }) {
338
352
  await runCommand(args, async () => {
339
- const { executeStorageStatus } = await import("./storage-D0odnyBO.mjs");
353
+ const { executeStorageStatus } = await import("./storage-tZM1J8qj.mjs");
340
354
  return executeStorageStatus(resolveStorageCwd(args.dir), args.server);
341
355
  });
342
356
  }
@@ -353,7 +367,7 @@ const storage = defineCommand({
353
367
  },
354
368
  async run({ args }) {
355
369
  await runCommand(args, async () => {
356
- const { executeStorageEnable } = await import("./storage-D0odnyBO.mjs");
370
+ const { executeStorageEnable } = await import("./storage-tZM1J8qj.mjs");
357
371
  return executeStorageEnable(resolveStorageCwd(args.dir), args.server);
358
372
  });
359
373
  }
@@ -375,7 +389,7 @@ const storage = defineCommand({
375
389
  },
376
390
  async run({ args }) {
377
391
  await runCommand(args, async () => {
378
- const { executeStorageDisable } = await import("./storage-D0odnyBO.mjs");
392
+ const { executeStorageDisable } = await import("./storage-tZM1J8qj.mjs");
379
393
  return executeStorageDisable(resolveStorageCwd(args.dir), {
380
394
  server: args.server,
381
395
  force: args.force
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, u as ok } from "./_ui-CKEIHAtB.mjs";
3
- import { n as getServerInfo } from "./_agent-4rsUGPhx.mjs";
2
+ import { n as log, u as ok } from "./_ui-DXZ9prrM.mjs";
3
+ import { n as getServerInfo } from "./_agent-D39qatZJ.mjs";
4
4
  import { t as apiRequest } from "./_api-client-a6cPMebU.mjs";
5
5
  //#region delete.ts
6
6
  async function runDelete(opts) {
@@ -1,22 +1,23 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, t as fmtUrl, u as ok } from "./_ui-CKEIHAtB.mjs";
2
+ import { n as log, t as fmtUrl, u as ok } from "./_ui-DXZ9prrM.mjs";
3
3
  import { r as errorMessage } from "./_utils-C502jKo8.mjs";
4
- import { buildAgentBundle } from "./_bundler-DnZnKsIU.mjs";
5
- import { o as writeProjectConfig } from "./_config-BWYgLjiI.mjs";
6
- import { t as resolveServerEnv } from "./_server-common-Bv-4Rskq.mjs";
7
- import { i as resolveDeployTarget } from "./_agent-4rsUGPhx.mjs";
4
+ import { buildAgentBundle } from "./_bundler-DdmuZzeR.mjs";
5
+ import { o as writeProjectConfig } from "./_config-Czjhvaax.mjs";
6
+ import { t as resolveServerEnv } from "./_server-common-B75oco06.mjs";
7
+ import { i as resolveDeployTarget } from "./_agent-D39qatZJ.mjs";
8
+ import { assertTypechecks } from "./_typecheck-gate-B1GcKZX-.mjs";
8
9
  import { t as apiRequest } from "./_api-client-a6cPMebU.mjs";
9
10
  import { gzipSync } from "node:zlib";
10
11
  //#region _deploy.ts
11
12
  async function runDeploy(opts) {
12
13
  const body = gzipSync(JSON.stringify({
13
14
  ...opts.slug ? { slug: opts.slug } : {},
15
+ ...opts.allowMissingSecrets ? { credentialPolicy: "warn" } : {},
14
16
  env: opts.env,
15
17
  worker: opts.bundle.worker,
16
- clientFiles: opts.bundle.clientFiles,
17
- agentConfig: opts.bundle.agentConfig
18
+ clientFiles: opts.bundle.clientFiles
18
19
  }));
19
- return { slug: (await apiRequest(`${opts.url}/deploy`, {
20
+ const data = await apiRequest(`${opts.url}/deploy`, {
20
21
  method: "POST",
21
22
  body,
22
23
  headers: {
@@ -29,13 +30,18 @@ async function runDeploy(opts) {
29
30
  ...opts.slug ? {} : { retry: 0 },
30
31
  ...opts.retryDelay !== void 0 ? { retryDelay: opts.retryDelay } : {},
31
32
  ...opts.fetch ? { fetch: opts.fetch } : {}
32
- })).slug };
33
+ });
34
+ return {
35
+ slug: data.slug,
36
+ ...data.warnings ? { warnings: data.warnings } : {}
37
+ };
33
38
  }
34
39
  //#endregion
35
40
  //#region deploy.ts
36
41
  async function executeDeploy(opts) {
37
42
  const { cwd } = opts;
38
43
  const { config: projectConfig, serverUrl, apiKey } = await resolveDeployTarget(cwd, opts.server);
44
+ if (!opts.skipTypecheck) await assertTypechecks(cwd);
39
45
  const bundle = await buildAgentBundle(cwd, { minify: true });
40
46
  const slug = projectConfig?.slug;
41
47
  const env = await resolveServerEnv(cwd);
@@ -48,6 +54,7 @@ async function executeDeploy(opts) {
48
54
  ...env
49
55
  },
50
56
  ...slug ? { slug } : {},
57
+ ...opts.allowMissingSecrets ? { allowMissingSecrets: true } : {},
51
58
  apiKey
52
59
  });
53
60
  const agentUrl = `${serverUrl}/${deployed.slug}`;
@@ -63,10 +70,12 @@ async function executeDeploy(opts) {
63
70
  serverUrl
64
71
  })}`);
65
72
  }
73
+ for (const warning of deployed.warnings ?? []) log.warn(warning);
66
74
  log.success(`Deployed ${fmtUrl(agentUrl)}`);
67
75
  return ok({
68
76
  slug: deployed.slug,
69
- url: agentUrl
77
+ url: agentUrl,
78
+ ...deployed.warnings ? { warnings: deployed.warnings } : {}
70
79
  });
71
80
  }
72
81
  //#endregion
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, r as parsePort, t as fmtUrl, u as ok } from "./_ui-CKEIHAtB.mjs";
2
+ import { n as log, r as parsePort, t as fmtUrl, u as ok } from "./_ui-DXZ9prrM.mjs";
3
3
  import { n as errorDetail } from "./_utils-C502jKo8.mjs";
4
4
  import path from "node:path";
5
- import pc from "picocolors";
5
+ import { styleText } from "node:util";
6
6
  //#region dev.ts
7
7
  /**
8
8
  * Start the dev server and return the result.
@@ -11,7 +11,7 @@ import pc from "picocolors";
11
11
  async function executeDev(opts) {
12
12
  const port = parsePort(opts.port);
13
13
  const agentName = path.basename(path.resolve(opts.cwd));
14
- const { startDevServer } = await import("./_dev-server-C8lcQOXt.mjs");
14
+ const { startDevServer } = await import("./_dev-server-BCbEUhmv.mjs");
15
15
  let cleanup;
16
16
  let shuttingDown = false;
17
17
  const onSignal = () => {
@@ -30,7 +30,7 @@ async function executeDev(opts) {
30
30
  port
31
31
  });
32
32
  const url = `http://localhost:${port}`;
33
- log.success(`${pc.bold(agentName)} running at ${fmtUrl(url)}`);
33
+ log.success(`${styleText("bold", agentName)} running at ${fmtUrl(url)}`);
34
34
  log.info("Press Ctrl-C to stop");
35
35
  process.on("unhandledRejection", (err) => {
36
36
  log.error(`Unhandled rejection: ${errorDetail(err)}`);
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { a as unwrapCancel, n as log, u as ok } from "./_ui-CKEIHAtB.mjs";
2
+ import { a as unwrapCancel, n as log, u as ok } from "./_ui-DXZ9prrM.mjs";
3
3
  import { i as fileExists, o as readJson, r as errorMessage, s as resolveCwd } from "./_utils-C502jKo8.mjs";
4
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-4rsUGPhx.mjs";
4
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-D39qatZJ.mjs";
5
5
  import path from "node:path";
6
+ import { styleText } from "node:util";
6
7
  import * as p from "@clack/prompts";
7
- import pc from "picocolors";
8
8
  import { execa } from "execa";
9
9
  //#region init.ts
10
10
  const DEFAULT_PROJECT_NAME = "my-voice-agent";
@@ -74,7 +74,7 @@ function resolveTargetDir(dir) {
74
74
  }
75
75
  /** Run deploy after init and return deploy metadata if successful. */
76
76
  async function tryDeploy(cwd, server) {
77
- const { executeDeploy } = await import("./deploy-CQ69yWAK.mjs");
77
+ const { executeDeploy } = await import("./deploy-D-dnt9L8.mjs");
78
78
  try {
79
79
  const result = await executeDeploy({
80
80
  cwd,
@@ -92,7 +92,7 @@ async function tryDeploy(cwd, server) {
92
92
  }
93
93
  /** Scaffold the project, optionally showing a spinner. */
94
94
  async function scaffoldProject(dir, cwd, template, silent) {
95
- const { runInit } = await import("./_init-C3xJKK3L.mjs");
95
+ const { runInit } = await import("./_init-H-lw7czL.mjs");
96
96
  if (silent) {
97
97
  await runInit({
98
98
  targetDir: cwd,
@@ -116,11 +116,11 @@ function printPostInitInfo(cwd, monorepoRoot) {
116
116
  }
117
117
  async function executeInit(opts, extra) {
118
118
  const suppressUi = extra?.silent;
119
- if (!suppressUi) p.intro(pc.cyanBright("Create a new voice agent"));
119
+ if (!suppressUi) p.intro(styleText("cyanBright", "Create a new voice agent"));
120
120
  const dir = opts.dir ?? await promptProjectName(opts.yes);
121
121
  const monorepoRoot = getMonorepoRoot();
122
122
  const cwd = resolveTargetDir(dir);
123
- if (!opts.force && await fileExists(path.join(cwd, "agent.ts"))) throw new Error(`agent.ts already exists in this directory. Use ${pc.cyanBright("--force")} to overwrite.`);
123
+ if (!opts.force && await fileExists(path.join(cwd, "agent.ts"))) throw new Error(`agent.ts already exists in this directory. Use ${styleText("cyanBright", "--force")} to overwrite.`);
124
124
  const template = opts.template ?? "simple";
125
125
  await scaffoldProject(dir, cwd, template, suppressUi);
126
126
  const installed = await installDeps(cwd, suppressUi);
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as unwrapCancel, n as log, s as fail, u as ok } from "./_ui-CKEIHAtB.mjs";
3
- import { n as getServerInfo } from "./_agent-4rsUGPhx.mjs";
2
+ import { a as unwrapCancel, n as log, s as fail, u as ok } from "./_ui-DXZ9prrM.mjs";
3
+ import { n as getServerInfo } from "./_agent-D39qatZJ.mjs";
4
4
  import { t as apiRequest } from "./_api-client-a6cPMebU.mjs";
5
5
  import * as p from "@clack/prompts";
6
6
  import { text } from "node:stream/consumers";
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, s as fail, u as ok } from "./_ui-CKEIHAtB.mjs";
3
- import { n as getServerInfo } from "./_agent-4rsUGPhx.mjs";
2
+ import { n as log, s as fail, u as ok } from "./_ui-DXZ9prrM.mjs";
3
+ import { n as getServerInfo } from "./_agent-D39qatZJ.mjs";
4
4
  import { t as apiRequest } from "./_api-client-a6cPMebU.mjs";
5
5
  import * as p from "@clack/prompts";
6
6
  //#region storage.ts
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as log, s as fail, u as ok } from "./_ui-CKEIHAtB.mjs";
2
+ import { n as log, s as fail, u as ok } from "./_ui-DXZ9prrM.mjs";
3
3
  import { r as errorMessage, t as errorCode } from "./_utils-C502jKo8.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, readFileSync } from "node:fs";
@@ -0,0 +1,129 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { readFile } from "node:fs/promises";
5
+ import { spawn } from "node:child_process";
6
+ //#region typecheck.ts
7
+ /**
8
+ * Project typechecking — `tsc --noEmit` over the project's own tsconfig.
9
+ *
10
+ * Public (no `_` prefix) for the same reason as the bundlers: the guest
11
+ * sandbox runs the same check before `test_agent` builds, so the studio's
12
+ * coding agent sees type errors as build feedback instead of shipping
13
+ * runtime-working-but-wrong code (the bundlers strip types unchecked —
14
+ * excess-property bugs like `send`/`state` shipped exactly that way).
15
+ *
16
+ * Gated on `tsconfig.json`: a project that declares its type discipline is
17
+ * checked with it; one that doesn't isn't (scaffolded projects and studio
18
+ * workspaces always have one). TypeScript itself resolves from the
19
+ * PROJECT'S node_modules — the user's pinned compiler, not ours.
20
+ */
21
+ /** Bound on one typecheck run — a hung compiler must not wedge a deploy. */
22
+ const TYPECHECK_TIMEOUT_MS = 12e4;
23
+ /** Diagnostics tail kept for the failure message. */
24
+ const OUTPUT_CAP = 16e3;
25
+ /**
26
+ * The project's own TypeScript package root — `node_modules/typescript`,
27
+ * walking up from `cwd` exactly as a bare import would.
28
+ *
29
+ * Deliberately NOT `require.resolve("typescript")`. Node appends
30
+ * `Module.globalPaths` — `NODE_PATH`, `~/.node_modules`, the install prefix —
31
+ * to EVERY lookup, and the `paths` option does not suppress them, so an
32
+ * ambient TypeScript anywhere on the host silently satisfies a project that
33
+ * never declared one. That breaks the promise in this module's header twice
34
+ * over: the gate would check a user's build with a compiler their project
35
+ * doesn't pin, and the "TypeScript is not installed" branch below would be
36
+ * unreachable on any host that sets NODE_PATH — which is how vitest runs its
37
+ * workers (it points NODE_PATH at pnpm's hidden store), so the test for that
38
+ * branch could not fail honestly either.
39
+ *
40
+ * The walk-up follows symlinks, which is what pnpm's `node_modules/typescript
41
+ * -> .pnpm/typescript@x/node_modules/typescript` link needs, and what lets a
42
+ * guest sandbox workspace reach the toolchain baked next to the harness.
43
+ */
44
+ function findTypescriptPackage(cwd) {
45
+ let dir = path.resolve(cwd);
46
+ for (;;) {
47
+ const candidate = path.join(dir, "node_modules", "typescript");
48
+ if (existsSync(path.join(candidate, "package.json"))) return candidate;
49
+ const parent = path.dirname(dir);
50
+ if (parent === dir) return;
51
+ dir = parent;
52
+ }
53
+ }
54
+ /** Resolve the project's TypeScript compiler entry (its own `tsc` bin). */
55
+ function resolveTscEntry(cwd) {
56
+ const dir = findTypescriptPackage(cwd);
57
+ if (dir === void 0) throw new Error("no typescript package in the project's node_modules");
58
+ const pkg = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf-8"));
59
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.tsc;
60
+ if (!bin) throw new Error("installed typescript package declares no tsc bin");
61
+ return path.join(dir, bin);
62
+ }
63
+ /**
64
+ * Typecheck the project at `cwd` with its own tsconfig + compiler.
65
+ * Skips (ok, skipped: true) when the project has no tsconfig.json.
66
+ */
67
+ async function typecheckProject(cwd) {
68
+ if (!await readFile(path.join(cwd, "tsconfig.json"), "utf-8").then(() => true, () => false)) return {
69
+ ok: true,
70
+ skipped: true
71
+ };
72
+ let tscEntry;
73
+ try {
74
+ tscEntry = resolveTscEntry(cwd);
75
+ } catch (err) {
76
+ return {
77
+ ok: false,
78
+ output: `tsconfig.json is present but TypeScript is not installed — add it (npm install -D typescript) or remove tsconfig.json: ${errMessage(err)}`
79
+ };
80
+ }
81
+ const result = await new Promise((resolve, reject) => {
82
+ const child = spawn(process.execPath, [
83
+ tscEntry,
84
+ "--noEmit",
85
+ "--pretty",
86
+ "false"
87
+ ], {
88
+ cwd,
89
+ timeout: TYPECHECK_TIMEOUT_MS,
90
+ stdio: [
91
+ "ignore",
92
+ "pipe",
93
+ "pipe"
94
+ ]
95
+ });
96
+ let output = "";
97
+ const keep = (s) => s.length > OUTPUT_CAP ? `…${s.slice(-16e3)}` : s;
98
+ child.stdout.on("data", (chunk) => {
99
+ output = keep(output + chunk.toString());
100
+ });
101
+ child.stderr.on("data", (chunk) => {
102
+ output = keep(output + chunk.toString());
103
+ });
104
+ child.on("error", reject);
105
+ child.on("close", (code, signal) => {
106
+ if (signal) {
107
+ reject(/* @__PURE__ */ new Error(`tsc killed by ${signal} after ${TYPECHECK_TIMEOUT_MS}ms`));
108
+ return;
109
+ }
110
+ resolve({
111
+ code,
112
+ output
113
+ });
114
+ });
115
+ });
116
+ if (result.code === 0) return {
117
+ ok: true,
118
+ skipped: false
119
+ };
120
+ return {
121
+ ok: false,
122
+ output: `Type check failed:\n${result.output.trim()}`
123
+ };
124
+ }
125
+ function errMessage(err) {
126
+ return err instanceof Error ? err.message : String(err);
127
+ }
128
+ //#endregion
129
+ export { typecheckProject };
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as withPreservedNodeEnv } from "./_vite-env-Dg_QlVv0.mjs";
3
3
  import path from "node:path";
4
+ import fs from "node:fs/promises";
4
5
  import { build } from "vite";
5
6
  //#region worker-bundler.ts
6
7
  /**
@@ -12,26 +13,48 @@ import { build } from "vite";
12
13
  * worker published from the browser comes out of the same Vite/Rollup pass as
13
14
  * one from `aai deploy`.
14
15
  *
16
+ * Every worker is built through a generated wrapper entry that re-exports the
17
+ * agent *and* its extracted config as `__aaiConfig` (via the dependency-free
18
+ * `@alexkroman1/aai/manifest` helpers, bundled in). The guest harness returns
19
+ * that export from `bundle/load`, which is how the platform obtains an
20
+ * agent's config without ever evaluating tenant code on the host — the
21
+ * `POST /deploy` route and the studio's sandbox inspection both rely on it.
22
+ *
23
+ * **The worker ships its own runtime.** Deploy builds also export
24
+ * `__aaiCreateRuntime` — a factory over the *user's installed* SDK's
25
+ * `createRuntime`, bundled in alongside the provider SDKs. The guest harness
26
+ * builds the session runtime through it, so a deployed agent runs exactly the
27
+ * runtime version it was built and tested against (identical to `aai dev`),
28
+ * instead of whatever SDK the platform's harness image was baked with. The
29
+ * harness↔bundle contract is deliberately tiny: the factory takes
30
+ * `{ env, db?, runCode? }` and returns `{ startSession, shutdown }`.
31
+ *
15
32
  * What the studio supplies via options, because a workspace is not a project:
16
33
  *
17
- * - **`root` + `entry`.** The studio builds a generated entry that re-exports
18
- * the agent *and* its extracted config, so the guest can report the config
19
- * back without the host ever evaluating agent code.
20
34
  * - **`configFile: false`.** Workspace files are untrusted and a Vite config
21
35
  * is executable host code.
22
36
  * - **`plugins`.** The studio adds its import allowlist; without it, Vite
23
37
  * would happily resolve any package in the server's `node_modules`.
24
38
  */
25
39
  /**
26
- * Transform `.md` imports into raw string exports so templates that do
27
- * `import systemPrompt from "./system-prompt.md"` bundle correctly.
40
+ * Generated wrapper entry, written under `.aai/` for the duration of the
41
+ * build (the CLI's own scratch dir — dot-paths are ignored by the dev
42
+ * watcher, and the studio's workspace materialization never writes there).
28
43
  */
29
- const rawMdPlugin = {
30
- name: "raw-md",
31
- transform(code, id) {
32
- if (id.endsWith(".md")) return `export default ${JSON.stringify(code)}`;
33
- }
44
+ const WRAPPER_ENTRY_REL = path.join(".aai", "worker-entry.ts");
45
+ function wrapperEntrySource(runtime) {
46
+ return `import def from "../agent.ts";
47
+ import { agentToolsToSchemas, toAgentConfig } from "@alexkroman1/aai/manifest";
48
+ ${runtime ? `import { createRuntime } from "@alexkroman1/aai/runtime";` : ""}
49
+ export default def;
50
+ export const __aaiConfig = {
51
+ ...toAgentConfig(def),
52
+ toolSchemas: agentToolsToSchemas(def.tools ?? {}),
34
53
  };
54
+ ${runtime ? `export const __aaiCreateRuntime = (opts: Record<string, unknown>) =>
55
+ createRuntime({ ...opts, agent: def });
56
+ ` : ""}`;
57
+ }
35
58
  /**
36
59
  * Bundle agent.ts into a single ESM string for the sandbox worker.
37
60
  *
@@ -39,25 +62,36 @@ const rawMdPlugin = {
39
62
  * and gracefully degrades in restricted environments like Deno.
40
63
  */
41
64
  async function buildWorker(cwd, opts = {}) {
42
- const entry = opts.entry ?? "agent.ts";
43
- const agentEntry = path.isAbsolute(entry) ? entry : path.join(cwd, entry);
44
- const result = await withPreservedNodeEnv(() => build({
45
- root: cwd,
46
- logLevel: "silent",
47
- ...opts.configFile === false && { configFile: false },
48
- plugins: [rawMdPlugin, ...opts.plugins ?? []],
49
- build: {
50
- lib: {
51
- entry: agentEntry,
52
- formats: ["es"],
53
- fileName: "worker"
54
- },
55
- target: "node20",
56
- minify: opts.minify ? "oxc" : false,
57
- write: false,
58
- rollupOptions: { output: { entryFileNames: "[name].js" } }
59
- }
60
- }));
65
+ const wrapperPath = path.join(cwd, WRAPPER_ENTRY_REL);
66
+ await fs.mkdir(path.dirname(wrapperPath), { recursive: true });
67
+ await fs.writeFile(wrapperPath, wrapperEntrySource(opts.runtime !== false), "utf-8");
68
+ let result;
69
+ try {
70
+ result = await withPreservedNodeEnv(() => build({
71
+ root: cwd,
72
+ logLevel: "silent",
73
+ ...opts.configFile === false && { configFile: false },
74
+ ...opts.plugins && { plugins: opts.plugins },
75
+ ssr: { noExternal: true },
76
+ build: {
77
+ ssr: true,
78
+ lib: {
79
+ entry: wrapperPath,
80
+ formats: ["es"],
81
+ fileName: "worker"
82
+ },
83
+ target: "node20",
84
+ minify: opts.minify ? "oxc" : false,
85
+ write: false,
86
+ rollupOptions: { output: {
87
+ entryFileNames: "[name].js",
88
+ codeSplitting: false
89
+ } }
90
+ }
91
+ }));
92
+ } finally {
93
+ await fs.rm(wrapperPath, { force: true }).catch(() => void 0);
94
+ }
61
95
  const output = Array.isArray(result) ? result[0] : result;
62
96
  if (!output) throw new Error("Vite produced no output for agent.ts");
63
97
  const chunk = output.output.find((o) => o.type === "chunk" && o.isEntry);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "3.2.0",
3
+ "version": "5.0.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "aai": "dist/cli.mjs"
@@ -13,6 +13,10 @@
13
13
  "./worker-bundler": {
14
14
  "@dev/source": "./worker-bundler.ts",
15
15
  "import": "./dist/worker-bundler.mjs"
16
+ },
17
+ "./typecheck": {
18
+ "@dev/source": "./typecheck.ts",
19
+ "import": "./dist/typecheck.mjs"
16
20
  }
17
21
  },
18
22
  "files": [
@@ -28,12 +32,11 @@
28
32
  "giget": "^3.3.0",
29
33
  "ofetch": "^1.5.1",
30
34
  "p-debounce": "^5.1.0",
31
- "picocolors": "^1.1.1",
32
- "rolldown": "~1.1.4",
35
+ "p-timeout": "^7.0.1",
33
36
  "vite": "^8.1.5",
34
37
  "zod": "^4.4.3",
35
- "@alexkroman1/aai": "3.2.0",
36
- "@alexkroman1/aai-ui": "3.2.0"
38
+ "@alexkroman1/aai-ui": "5.0.0",
39
+ "@alexkroman1/aai": "5.0.0"
37
40
  },
38
41
  "devDependencies": {
39
42
  "playwright": "^1.61.1",
@@ -63,6 +66,8 @@
63
66
  "build": "tsdown",
64
67
  "typecheck": "tsc --noEmit",
65
68
  "lint": "biome check .",
69
+ "check:publint": "publint",
70
+ "check:attw": "attw --pack --profile esm-only",
66
71
  "test:e2e": "VITEST_PROFILE=e2e VITEST_INCLUDE=e2e*.test.ts vitest run -c ../../vitest.slow.config.ts",
67
72
  "check:e2e": "pnpm run test:e2e"
68
73
  }
@@ -1,96 +0,0 @@
1
- #!/usr/bin/env node
2
- import { n as log, u as ok } from "./_ui-CKEIHAtB.mjs";
3
- import { c as validateAgentExport, r as errorMessage } from "./_utils-C502jKo8.mjs";
4
- import { t as buildClient } from "./client-bundler-DO3GgO6p.mjs";
5
- import { buildWorker } from "./worker-bundler.mjs";
6
- import path from "node:path";
7
- import { pathToFileURL } from "node:url";
8
- import fs from "node:fs/promises";
9
- import { createHash } from "node:crypto";
10
- import { agentToolsToSchemas, toAgentConfig } from "@alexkroman1/aai/manifest";
11
- //#region _bundler.ts
12
- /**
13
- * Bundle an agent directory: build agent.ts into worker ESM + extract config.
14
- *
15
- * - agent.ts is the single entry point: `export default agent({...})`
16
- * - A single Vite build produces the worker ESM (all deps bundled in).
17
- * The AgentDef is extracted from that bundle via dynamic import, avoiding a
18
- * second build pass.
19
- */
20
- async function buildAgentBundle(cwd, opts = {}) {
21
- const [[worker, agentDef], clientFiles] = await Promise.all([buildWorker(cwd, opts).then(async (code) => [code, await evalWorkerBundle(code, cwd)]), buildClient(cwd)]);
22
- log.step(`Bundling ${agentDef.name}`);
23
- const config = toAgentConfig(agentDef);
24
- const toolSchemas = agentToolsToSchemas(agentDef.tools ?? {});
25
- return {
26
- worker,
27
- clientFiles,
28
- agentConfig: {
29
- ...config,
30
- toolSchemas
31
- }
32
- };
33
- }
34
- /**
35
- * Write the worker ESM to a temp file and dynamic-import it, returning
36
- * the AgentDef default export. All dependencies are bundled in, so the
37
- * file can be evaluated from any directory.
38
- *
39
- * Each call imports a uniquely-named file, and Node's ESM registry never
40
- * evicts — so every call retains one bundle for the process lifetime. That is
41
- * fine for one-shot commands (`aai build`/`aai deploy`); long-lived callers
42
- * must go through `createWorkerEvaluator` to at least dedupe identical
43
- * builds. Evaluating in a discardable context is not an option: tool
44
- * `execute` functions from the returned AgentDef are called in-process by the
45
- * dev runtime, which rules out worker threads, and `node:vm` ESM evaluation
46
- * is still flagged experimental.
47
- */
48
- async function evalWorkerBundle(code, cwd) {
49
- const evalDir = path.join(cwd, ".aai", "eval");
50
- const tmpPath = path.join(evalDir, `agent-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`);
51
- try {
52
- await fs.mkdir(evalDir, { recursive: true });
53
- await fs.writeFile(tmpPath, code);
54
- } catch (err) {
55
- await fs.rm(tmpPath, { force: true }).catch(() => void 0);
56
- throw new Error(`Failed to write the eval bundle under ${evalDir} — is the project directory writable? (${errorMessage(err)})`, { cause: err });
57
- }
58
- try {
59
- const mod = await import(pathToFileURL(tmpPath).href);
60
- const agentDef = mod.default ?? mod;
61
- validateAgentExport(agentDef);
62
- return agentDef;
63
- } finally {
64
- await fs.rm(tmpPath).catch(() => {});
65
- }
66
- }
67
- /**
68
- * Memoizing wrapper around `evalWorkerBundle` for long-lived callers (the
69
- * dev server): byte-identical worker code returns the previously evaluated
70
- * AgentDef without touching the ESM registry. No-op saves and formatter
71
- * churn are the common watcher events, so this caps the registry leak (see
72
- * `evalWorkerBundle`) to genuinely-new bundles — the residual one-module-per-
73
- * distinct-build leak is accepted for the reasons documented there.
74
- */
75
- function createWorkerEvaluator(cwd) {
76
- let lastHash;
77
- let lastAgentDef;
78
- return async (code) => {
79
- const hash = createHash("sha256").update(code).digest("hex");
80
- if (lastAgentDef && hash === lastHash) return lastAgentDef;
81
- const agentDef = await evalWorkerBundle(code, cwd);
82
- lastHash = hash;
83
- lastAgentDef = agentDef;
84
- return agentDef;
85
- };
86
- }
87
- async function executeBuild(cwd) {
88
- const bundle = await buildAgentBundle(cwd, { minify: true });
89
- log.success("Build complete");
90
- return ok({
91
- name: bundle.agentConfig.name,
92
- workerBytes: bundle.worker.length
93
- });
94
- }
95
- //#endregion
96
- export { buildAgentBundle, createWorkerEvaluator, executeBuild };