@alexkroman1/aai-cli 1.9.2 → 1.11.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.
@@ -0,0 +1,406 @@
1
+ #!/usr/bin/env node
2
+ import { n as log } from "./_ui-CKEIHAtB.mjs";
3
+ import { r as errorMessage, t as errorCode } from "./_utils-C502jKo8.mjs";
4
+ import { n as fallbackHtmlPlugin } from "./client-bundler-BO1Wm1wT.mjs";
5
+ import { buildWorker } from "./worker-bundler.mjs";
6
+ import { createWorkerEvaluator } from "./_bundler-z6wTRMX8.mjs";
7
+ import { n as ensureApiKey } from "./_config-BWYgLjiI.mjs";
8
+ import { t as resolveServerEnv } from "./_server-common-Bv-4Rskq.mjs";
9
+ import { createRequire } from "node:module";
10
+ import { existsSync } from "node:fs";
11
+ import path from "node:path";
12
+ import fs from "node:fs/promises";
13
+ import { requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
14
+ import { watch } from "chokidar";
15
+ import getPort, { portNumbers } from "get-port";
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
+ //#region _dev-server.ts
128
+ /**
129
+ * Dev server for directory-based agents.
130
+ *
131
+ * Imports agent.ts directly for the full agent definition,
132
+ * builds a runtime, and starts an HTTP+WebSocket server. Watches for
133
+ * file changes and restarts automatically. Optionally runs Vite for
134
+ * client SPA HMR.
135
+ */
136
+ async function resolveAgentEnv(root, agentDef) {
137
+ 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.`);
142
+ return env;
143
+ }
144
+ /**
145
+ * The env handed to `createServer` for host-mode connections: provider
146
+ * credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
147
+ * (it is a control variable, not something an agent declares in `.env`).
148
+ */
149
+ function hostModeEnv(providerEnv) {
150
+ const gate = process.env.AAI_ALLOW_HOST;
151
+ return gate === void 0 ? providerEnv : {
152
+ ...providerEnv,
153
+ AAI_ALLOW_HOST: gate
154
+ };
155
+ }
156
+ /**
157
+ * Explicit bind host for the dev server, or `undefined` to take the
158
+ * loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
159
+ * interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
160
+ * undo the loopback default this exists to guard.
161
+ */
162
+ function devBindHost() {
163
+ const host = process.env.AAI_DEV_HOST?.trim();
164
+ return host ? host : void 0;
165
+ }
166
+ /**
167
+ * Load the agent definition by bundling agent.ts (and all its local imports)
168
+ * into a single ESM file, then importing that. A raw `import(agent.ts?t=...)`
169
+ * only cache-busts agent.ts itself — transitive imports (./tools.ts, etc.)
170
+ * stay in Node's ESM registry, so edits to them are ignored on reload.
171
+ * Bundling picks them up.
172
+ *
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
179
+ * a no-op save doesn't leak another module into the ESM registry.
180
+ */
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);
190
+ }
191
+ /**
192
+ * True for paths that should never trigger a restart: anything inside
193
+ * `node_modules/` and any dot-entry (`.git/`, `.aai/`, `.DS_Store`, …).
194
+ * `.git/` especially matters — commits and status checks churn the index
195
+ * and would otherwise cause spurious full backend restarts.
196
+ *
197
+ * Exception: `.env` / `.env.*` files stay watched — env edits should
198
+ * restart the server with the new values.
199
+ */
200
+ function isIgnoredPath(dir, filePath) {
201
+ const rel = path.relative(dir, filePath);
202
+ if (!rel || rel.startsWith("..")) return false;
203
+ return rel.split(path.sep).some((segment) => {
204
+ if (segment === "node_modules") return true;
205
+ if (segment === ".env" || segment.startsWith(".env.")) return false;
206
+ return segment.startsWith(".");
207
+ });
208
+ }
209
+ /**
210
+ * Watch the agent directory for changes and call `onChange` when detected.
211
+ * Debounces to avoid rapid restarts. Uses chokidar for reliable recursive
212
+ * watching across platforms (raw `fs.watch` misses events on Linux).
213
+ */
214
+ function watchDirectory(dir, onChange) {
215
+ const debouncedChange = pDebounce(() => {
216
+ log.info("File change detected, restarting...");
217
+ onChange();
218
+ }, 300);
219
+ const watcher = watch(dir, {
220
+ ignored: (filePath) => isIgnoredPath(dir, filePath),
221
+ ignoreInitial: true,
222
+ persistent: false
223
+ });
224
+ watcher.on("error", (err) => {
225
+ const hint = errorCode(err) === "ENOSPC" ? " The inotify watch limit was reached — raise the fs.inotify max_user_watches sysctl." : "";
226
+ log.error(`File watcher error: ${errorMessage(err)}.${hint} Auto-restart on file changes may have stopped; restart \`aai dev\` after fixing.`);
227
+ });
228
+ watcher.on("all", () => {
229
+ debouncedChange().catch((err) => {
230
+ log.error(`Watch handler failed: ${errorMessage(err)}`);
231
+ });
232
+ });
233
+ return watcher;
234
+ }
235
+ /** Locate the pre-built default aai-ui client (served when no custom client.tsx). */
236
+ function resolveDefaultClientDir() {
237
+ const require = createRequire(import.meta.url);
238
+ let pkgPath;
239
+ try {
240
+ pkgPath = require.resolve("@alexkroman1/aai-ui/package.json");
241
+ } catch (err) {
242
+ throw new Error(`Could not locate the default client UI (${errorMessage(err)}) — is @alexkroman1/aai-ui installed? Try reinstalling dependencies (pnpm install).`, { cause: err });
243
+ }
244
+ return path.join(path.dirname(pkgPath), "dist", "default-client");
245
+ }
246
+ /**
247
+ * Vite dev-server config for the client SPA. Extracted so the proxy wiring
248
+ * is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
249
+ * with a `client.tsx` serves a page whose WebSocket never connects.
250
+ *
251
+ * `strictPort` because the reported URL is `http://localhost:<port>` —
252
+ * without it, Vite silently binds port+N when the port is busy and the
253
+ * printed/JSON-returned URL points at whatever else was listening.
254
+ */
255
+ function viteDevConfig(cwd, vitePort, backendPort) {
256
+ const target = `http://localhost:${backendPort}`;
257
+ return {
258
+ root: cwd,
259
+ plugins: [fallbackHtmlPlugin(cwd)],
260
+ server: {
261
+ port: vitePort,
262
+ strictPort: true,
263
+ proxy: {
264
+ "/health": target,
265
+ "/websocket": {
266
+ target,
267
+ ws: true
268
+ }
269
+ }
270
+ }
271
+ };
272
+ }
273
+ /**
274
+ * Start the dev server for a directory-based agent.
275
+ *
276
+ * Returns a cleanup function to shut down the server and watchers.
277
+ */
278
+ async function startDevServer(opts) {
279
+ const { cwd, port } = opts;
280
+ const { createRuntime, createServer } = await import("@alexkroman1/aai/runtime");
281
+ const hasClient = existsSync(path.join(cwd, "client.tsx"));
282
+ const backendPort = hasClient ? await getPort({ port: portNumbers(port + 1, port + 100) }) : port;
283
+ const vitePort = port;
284
+ const clientDirOpt = hasClient ? {} : { clientDir: resolveDefaultClientDir() };
285
+ const devBuilder = createDevWorkerBuilder(cwd);
286
+ const evaluateWorker = createWorkerEvaluator(cwd);
287
+ /** Full build sequence, shared by initial startup and every restart. */
288
+ async function buildServer() {
289
+ const agentDef = await loadAgentDefWith(cwd, devBuilder, evaluateWorker);
290
+ const env = await resolveAgentEnv(cwd, agentDef);
291
+ const providerEnv = withHostCredentialFallback(env);
292
+ const runtime = createRuntime({
293
+ agent: agentDef,
294
+ env,
295
+ providerEnv
296
+ });
297
+ return createServer({
298
+ runtime,
299
+ name: agentDef.name,
300
+ env: hostModeEnv(providerEnv),
301
+ hostBaseAgent: agentDef,
302
+ ...clientDirOpt
303
+ });
304
+ }
305
+ let restarting = true;
306
+ let pendingRestart = false;
307
+ let closed = false;
308
+ let currentServer;
309
+ function kickRestart() {
310
+ if (restarting) {
311
+ pendingRestart = true;
312
+ return;
313
+ }
314
+ restarting = true;
315
+ restart().catch((err) => {
316
+ log.error(`Restart failed: ${errorMessage(err)}`);
317
+ }).finally(() => {
318
+ restarting = false;
319
+ });
320
+ }
321
+ const watcher = watchDirectory(cwd, kickRestart);
322
+ let viteServer;
323
+ try {
324
+ currentServer = await buildServer();
325
+ await currentServer.listen(backendPort, devBindHost());
326
+ if (hasClient) {
327
+ const { createServer: createViteServer } = await import("vite");
328
+ viteServer = await createViteServer(viteDevConfig(cwd, vitePort, backendPort));
329
+ await viteServer.listen();
330
+ viteServer.httpServer?.on("error", (err) => {
331
+ log.error(`Vite dev server error: ${errorMessage(err)}`);
332
+ });
333
+ }
334
+ } catch (err) {
335
+ await watcher.close().catch(() => void 0);
336
+ await devBuilder.dispose().catch(() => void 0);
337
+ await viteServer?.close().catch(() => void 0);
338
+ throw err;
339
+ }
340
+ restarting = false;
341
+ if (pendingRestart) kickRestart();
342
+ async function restart() {
343
+ do {
344
+ pendingRestart = false;
345
+ await restartOnce();
346
+ } while (pendingRestart && !closed);
347
+ }
348
+ async function restartOnce() {
349
+ let newServer;
350
+ try {
351
+ newServer = await buildServer();
352
+ } catch (err) {
353
+ log.error(`Restart failed: ${errorMessage(err)} (previous server still running)`);
354
+ return;
355
+ }
356
+ if (closed) {
357
+ await newServer.close().catch(() => void 0);
358
+ return;
359
+ }
360
+ try {
361
+ await currentServer.close();
362
+ } catch {}
363
+ try {
364
+ await listenWithRetry(newServer);
365
+ currentServer = newServer;
366
+ if (closed) {
367
+ await newServer.close().catch(() => void 0);
368
+ return;
369
+ }
370
+ log.success("Restarted");
371
+ } catch (err) {
372
+ log.error(`Restart failed: ${errorMessage(err)} — dev server is down; save a file to retry.`);
373
+ await newServer.close().catch(() => void 0);
374
+ }
375
+ }
376
+ /**
377
+ * Listen with a few short-backoff retries. During the close→listen swap the
378
+ * port is momentarily free, so another process can snatch it (or the OS can
379
+ * hold it in TIME_WAIT); one blind attempt would leave the dev server down
380
+ * until the next file change.
381
+ */
382
+ async function listenWithRetry(server) {
383
+ const LISTEN_ATTEMPTS = 3;
384
+ const LISTEN_RETRY_DELAY_MS = 250;
385
+ for (let attempt = 1;; attempt++) try {
386
+ await server.listen(backendPort, devBindHost());
387
+ return;
388
+ } catch (err) {
389
+ if (attempt >= LISTEN_ATTEMPTS || closed) throw err;
390
+ await new Promise((resolve) => setTimeout(resolve, LISTEN_RETRY_DELAY_MS));
391
+ }
392
+ }
393
+ let cleanupPromise;
394
+ return () => {
395
+ cleanupPromise ??= (async () => {
396
+ closed = true;
397
+ await watcher.close().catch(() => void 0);
398
+ await devBuilder.dispose().catch(() => void 0);
399
+ await viteServer?.close().catch(() => void 0);
400
+ await currentServer.close().catch(() => void 0);
401
+ })();
402
+ return cleanupPromise;
403
+ };
404
+ }
405
+ //#endregion
406
+ export { startDevServer };
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { a as readJson, c as writeJson, i as isEexist } from "./_utils-BeU10C7O.mjs";
3
- import { i as isDevMode, n as getMonorepoRoot } from "./_agent-cHGzbDVG.mjs";
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-Ds5LyAq5.mjs";
4
4
  import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
@@ -9,37 +9,70 @@ import { downloadTemplate } from "giget";
9
9
  //#region _templates.ts
10
10
  const GIGET_SOURCE = "github:alexkroman/agent/packages/aai-templates";
11
11
  const GIGET_REF = process.env.AAI_TEMPLATES_REF ?? "main";
12
+ const VALID_REF_RE = /^[\w./-]+$/;
13
+ const noCleanup = async () => void 0;
12
14
  /** Resolve the templates directory — local in dev, giget download in prod. */
13
15
  async function resolveTemplatesDir() {
14
- if (process.env.AAI_TEMPLATES_DIR) return process.env.AAI_TEMPLATES_DIR;
16
+ if (process.env.AAI_TEMPLATES_DIR) return {
17
+ root: process.env.AAI_TEMPLATES_DIR,
18
+ cleanup: noCleanup
19
+ };
15
20
  const monorepoRoot = isDevMode() ? getMonorepoRoot() : null;
16
- if (monorepoRoot) return path.join(monorepoRoot, "packages", "aai-templates");
21
+ if (monorepoRoot) return {
22
+ root: path.join(monorepoRoot, "packages", "aai-templates"),
23
+ cleanup: noCleanup
24
+ };
25
+ if (!VALID_REF_RE.test(GIGET_REF)) throw new Error(`Invalid AAI_TEMPLATES_REF: ${JSON.stringify(GIGET_REF)} is not a git ref.`);
17
26
  const extractDir = await fs.mkdtemp(path.join(os.tmpdir(), "aai-templates-"));
18
- const { dir } = await downloadTemplate(`${GIGET_SOURCE}#${GIGET_REF}`, {
19
- dir: extractDir,
20
- force: true,
21
- forceClean: true
22
- });
23
- return dir;
27
+ const cleanup = async () => {
28
+ await fs.rm(extractDir, {
29
+ recursive: true,
30
+ force: true
31
+ }).catch(() => void 0);
32
+ };
33
+ try {
34
+ const { dir } = await downloadTemplate(`${GIGET_SOURCE}#${GIGET_REF}`, {
35
+ dir: extractDir,
36
+ force: true,
37
+ forceClean: true
38
+ });
39
+ return {
40
+ root: dir,
41
+ cleanup
42
+ };
43
+ } catch (err) {
44
+ await cleanup();
45
+ throw new Error(`Failed to download templates from ${GIGET_SOURCE}#${GIGET_REF}: ${errorMessage(err)}`, { cause: err });
46
+ }
24
47
  }
25
48
  /**
26
49
  * Download a template into targetDir, merging scaffold files underneath.
27
50
  */
28
51
  async function downloadAndMergeTemplate(template, targetDir) {
29
- const root = await resolveTemplatesDir();
30
- const templatesDir = path.join(root, "templates");
31
- const names = (await fs.readdir(templatesDir, { withFileTypes: true })).filter((e) => e.isDirectory()).map((e) => e.name);
32
- if (!names.includes(template)) throw new Error(`Unknown template "${template}". Available templates: ${names.join(", ")}`);
33
- await fs.cp(path.join(templatesDir, template), targetDir, {
34
- recursive: true,
35
- force: true
36
- });
37
- const scaffoldDir = path.join(root, "scaffold");
38
- if (existsSync(scaffoldDir)) await fs.cp(scaffoldDir, targetDir, {
39
- recursive: true,
40
- force: false,
41
- errorOnExist: false
42
- });
52
+ const { root, cleanup } = await resolveTemplatesDir();
53
+ try {
54
+ const templatesDir = path.join(root, "templates");
55
+ let available;
56
+ try {
57
+ available = await fs.readdir(templatesDir, { withFileTypes: true });
58
+ } catch (err) {
59
+ throw new Error(`Templates directory is missing or unreadable at ${templatesDir} (corrupt or incomplete template download?): ${errorMessage(err)}`, { cause: err });
60
+ }
61
+ const names = available.filter((e) => e.isDirectory()).map((e) => e.name);
62
+ if (!names.includes(template)) throw new Error(`Unknown template "${template}". Available templates: ${names.join(", ")}`);
63
+ await fs.cp(path.join(templatesDir, template), targetDir, {
64
+ recursive: true,
65
+ force: true
66
+ });
67
+ const scaffoldDir = path.join(root, "scaffold");
68
+ if (existsSync(scaffoldDir)) await fs.cp(scaffoldDir, targetDir, {
69
+ recursive: true,
70
+ force: false,
71
+ errorOnExist: false
72
+ });
73
+ } finally {
74
+ await cleanup();
75
+ }
43
76
  }
44
77
  //#endregion
45
78
  //#region _init.ts
@@ -95,7 +128,6 @@ async function patchPackageJsonForWorkspace(targetDir) {
95
128
  if (!pkgJson) return;
96
129
  pkgJson.name = path.basename(targetDir);
97
130
  delete pkgJson.packageManager;
98
- const { getMonorepoRoot } = await import("./_agent-cHGzbDVG.mjs").then((n) => n.t);
99
131
  const root = getMonorepoRoot();
100
132
  if (!root) return;
101
133
  const packagesDir = path.join(root, "packages");
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import path from "node:path";
3
3
  import fs from "node:fs/promises";
4
- import { parse } from "dotenv";
4
+ import { parseEnv } from "node:util";
5
5
  //#region _server-common.ts
6
6
  /**
7
7
  * Build the `ctx.env` record that agent tools will see at runtime.
@@ -22,7 +22,7 @@ import { parse } from "dotenv";
22
22
  async function resolveServerEnv(cwd, baseEnv) {
23
23
  let fileEntries = {};
24
24
  if (cwd) try {
25
- fileEntries = parse(await fs.readFile(path.join(cwd, ".env"), "utf-8"));
25
+ fileEntries = parseEnv(await fs.readFile(path.join(cwd, ".env"), "utf-8"));
26
26
  } catch {}
27
27
  const source = baseEnv ?? process.env;
28
28
  const env = {};
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+ import * as p from "@clack/prompts";
3
+ import pc from "picocolors";
4
+ //#region _output.ts
5
+ /**
6
+ * Determine output mode from CLI flags and TTY state.
7
+ *
8
+ * Priority: --json flag > --no-json flag > TTY auto-detection.
9
+ */
10
+ function getOutputMode(args, isTTY = Boolean(process.stdout.isTTY)) {
11
+ if (args.json === true) return "json";
12
+ if (args.json === false) return "human";
13
+ return isTTY ? "human" : "json";
14
+ }
15
+ /**
16
+ * Write a line to stdout, resolving only once it has been flushed.
17
+ *
18
+ * Resolves (never rejects) even when the write fails: the common failure is
19
+ * EPIPE — the consumer closed the pipe (`aai … --json | head -1`) — and there
20
+ * is nothing useful to do about a broken stdout except carry on and exit.
21
+ * Stream-level `'error'` events are handled by {@link installStdoutGuard}.
22
+ */
23
+ function writeLine(line) {
24
+ return new Promise((resolve) => {
25
+ process.stdout.write(line, () => resolve());
26
+ });
27
+ }
28
+ /**
29
+ * Install an `'error'` listener on stdout so a broken pipe doesn't crash the
30
+ * CLI with an unhandled `'error'` event. EPIPE (consumer went away, e.g.
31
+ * `aai … --json | head -1`) exits quietly; anything else is reported on
32
+ * stderr and exits non-zero.
33
+ */
34
+ function installStdoutGuard(stream = process.stdout) {
35
+ stream.on("error", (err) => {
36
+ if (err.code === "EPIPE") process.exit(0);
37
+ else {
38
+ process.stderr.write(`stdout error: ${err.message}\n`);
39
+ process.exit(1);
40
+ }
41
+ });
42
+ }
43
+ /** Create an ok result. */
44
+ function ok(data) {
45
+ return {
46
+ ok: true,
47
+ data
48
+ };
49
+ }
50
+ /** Create an error result. */
51
+ function fail(code, error, hint) {
52
+ return hint ? {
53
+ ok: false,
54
+ error,
55
+ code,
56
+ hint
57
+ } : {
58
+ ok: false,
59
+ error,
60
+ code
61
+ };
62
+ }
63
+ /** Typed CLI error that carries a structured error code and optional hint. */
64
+ var CliError = class extends Error {
65
+ code;
66
+ hint;
67
+ constructor(code, message, hint, options) {
68
+ super(message, options);
69
+ this.name = "CliError";
70
+ this.code = code;
71
+ if (hint !== void 0) this.hint = hint;
72
+ }
73
+ };
74
+ //#endregion
75
+ //#region _ui.ts
76
+ const noop = () => {};
77
+ let silenced = false;
78
+ /** Log instance that delegates to clack (human mode) or no-ops (JSON mode). */
79
+ const log = new Proxy(p.log, { get(target, prop, receiver) {
80
+ return silenced ? noop : Reflect.get(target, prop, receiver);
81
+ } });
82
+ /** Replace all log methods with no-ops. Call once in JSON mode. */
83
+ function silenceOutput() {
84
+ silenced = true;
85
+ }
86
+ /** Unwrap a clack prompt result, exiting cleanly if the user cancelled. */
87
+ function unwrapCancel(result) {
88
+ if (p.isCancel(result)) {
89
+ p.cancel("Setup cancelled");
90
+ process.exit(0);
91
+ }
92
+ return result;
93
+ }
94
+ /** Format a URL for display. */
95
+ function fmtUrl(url) {
96
+ return pc.cyanBright(url);
97
+ }
98
+ /**
99
+ * Parse and validate a port string. Returns the numeric port or throws.
100
+ *
101
+ * Deliberately zod-free: this module loads on every CLI invocation
102
+ * (including `aai --help`), and keeping zod off that path is the same
103
+ * startup-cost invariant `_utils.ts` documents for its error helpers.
104
+ */
105
+ function parsePort(raw) {
106
+ const port = raw.trim() === "" ? NaN : Number(raw);
107
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error(`Invalid port: ${raw}. Must be a number between 0 and 65535.`);
108
+ return port;
109
+ }
110
+ //#endregion
111
+ export { unwrapCancel as a, getOutputMode as c, writeLine as d, silenceOutput as i, installStdoutGuard as l, log as n, CliError as o, parsePort as r, fail as s, fmtUrl as t, ok as u };