@alexkroman1/aai-cli 1.9.2 → 1.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{_agent-cHGzbDVG.mjs → _agent-Ba5Ykp05.mjs} +22 -26
- package/dist/{_api-client-yJD7xGfd.mjs → _api-client-a6cPMebU.mjs} +2 -2
- package/dist/_bundler-CKz9Q15i.mjs +96 -0
- package/dist/{_config-zV70t71V.mjs → _config-BMjZt5hG.mjs} +58 -43
- package/dist/_dev-server-DZl-xNwH.mjs +411 -0
- package/dist/{_init-y7K-dzni.mjs → _init-BznoJC91.mjs} +57 -25
- package/dist/{_server-common-QpOTRZgi.mjs → _server-common-Bv-4Rskq.mjs} +2 -2
- package/dist/_ui-CKEIHAtB.mjs +111 -0
- package/dist/_utils-ECl2je7-.mjs +97 -0
- package/dist/_vite-env-DNb9R8xq.mjs +48 -0
- package/dist/cli.mjs +75 -66
- package/dist/{client-bundler-Bki_Rned.mjs → client-bundler-VdcgFcos.mjs} +22 -24
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-DE35s3d5.mjs → delete-xZ8PzM_4.mjs} +4 -7
- package/dist/{deploy-DOy6wOCd.mjs → deploy-Ds1spTPC.mjs} +23 -13
- package/dist/{dev-DGzDKzGZ.mjs → dev-CtE0d-Gs.mjs} +19 -12
- package/dist/{init-BDhs6-jB.mjs → init-CFwZf8q1.mjs} +22 -17
- package/dist/{secret-D-90QtDQ.mjs → secret-B4LRmUhF.mjs} +5 -7
- package/dist/{test-C6jwCnFu.mjs → test-DXPY9Bqq.mjs} +24 -11
- package/dist/worker-bundler.mjs +1 -1
- package/package.json +7 -11
- package/dist/_bundler-CpLVjRzc.mjs +0 -62
- package/dist/_dev-server-8pf2tqvC.mjs +0 -214
- package/dist/_output-fy2bXRNb.mjs +0 -63
- package/dist/_ui-YqQ6Fi8K.mjs +0 -34
- package/dist/_utils-BeU10C7O.mjs +0 -55
- package/dist/_vite-env-CmXG4ZAu.mjs +0 -31
- package/dist/types.mjs +0 -2
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as log } from "./_ui-CKEIHAtB.mjs";
|
|
3
|
+
import { r as errorMessage, t as errorCode } from "./_utils-ECl2je7-.mjs";
|
|
4
|
+
import { n as fallbackHtmlPlugin } from "./client-bundler-VdcgFcos.mjs";
|
|
5
|
+
import { buildWorker } from "./worker-bundler.mjs";
|
|
6
|
+
import { createWorkerEvaluator } from "./_bundler-CKz9Q15i.mjs";
|
|
7
|
+
import { n as ensureApiKey } from "./_config-BMjZt5hG.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
|
+
* Incremental worker builds for `aai dev`.
|
|
20
|
+
*
|
|
21
|
+
* The deploy path (`buildWorker` in `worker-bundler.ts`) is a from-scratch
|
|
22
|
+
* Vite/Rollup pass — right for a one-shot `aai deploy`, but 1–3 s per save
|
|
23
|
+
* when the dev watcher runs it on every change. This module keeps a
|
|
24
|
+
* long-lived esbuild `context()` whose `rebuild()` reuses the previous
|
|
25
|
+
* build's work, cutting rebuilds to tens of ms. Deploy keeps Vite untouched,
|
|
26
|
+
* so nothing that ships is produced by esbuild.
|
|
27
|
+
*
|
|
28
|
+
* Parity with `buildWorker` where it matters for dev:
|
|
29
|
+
*
|
|
30
|
+
* - single-file ESM output, unminified (dev builds never minify);
|
|
31
|
+
* - `node:` builtins external (esbuild's `platform: "node"`), everything
|
|
32
|
+
* else — zod, workspace deps, local imports — bundled in;
|
|
33
|
+
* - `.md` imports resolve to their raw text (esbuild's `text` loader is
|
|
34
|
+
* `rawMdPlugin`'s transform), and Vite-style `?raw` suffix imports are
|
|
35
|
+
* honored via `rawSuffixPlugin` below.
|
|
36
|
+
*
|
|
37
|
+
* Known dev/deploy differences, accepted: Rollup and esbuild can disagree on
|
|
38
|
+
* `exports`-condition ordering for exotic dual-format packages, and Vite's
|
|
39
|
+
* lib build applies `define`/`import.meta.env` replacements esbuild does not.
|
|
40
|
+
* When a rebuild fails for anything other than a compile error in the agent's
|
|
41
|
+
* code, the caller falls back to the cold Vite path (see `_dev-server.ts`),
|
|
42
|
+
* so an esbuild-specific resolution gap degrades to the old slow-but-correct
|
|
43
|
+
* behavior rather than a broken dev server.
|
|
44
|
+
*
|
|
45
|
+
* esbuild does not touch `process.env` (verified: no NODE_ENV write in its
|
|
46
|
+
* JS API), so the `withPreservedNodeEnv` wrapper Vite builds need is not
|
|
47
|
+
* required here.
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* Vite serves `import x from "./file?raw"` as the file's text. esbuild treats
|
|
51
|
+
* the suffix as part of the filename, so resolve it explicitly and load the
|
|
52
|
+
* real file with the `text` loader.
|
|
53
|
+
*/
|
|
54
|
+
const rawSuffixPlugin = {
|
|
55
|
+
name: "raw-suffix",
|
|
56
|
+
setup(build) {
|
|
57
|
+
build.onResolve({ filter: /\?raw$/ }, (args) => ({
|
|
58
|
+
path: path.resolve(args.resolveDir, args.path.slice(0, -4)),
|
|
59
|
+
namespace: "aai-raw"
|
|
60
|
+
}));
|
|
61
|
+
build.onLoad({
|
|
62
|
+
filter: /.*/,
|
|
63
|
+
namespace: "aai-raw"
|
|
64
|
+
}, async (args) => ({
|
|
65
|
+
contents: await fs.readFile(args.path, "utf8"),
|
|
66
|
+
loader: "text"
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* True for esbuild build failures — compile/resolve errors in the code being
|
|
72
|
+
* built (the esbuild analog of a Rollup diagnostic). Anything else coming out
|
|
73
|
+
* of `rebuild()` is an esbuild-infrastructure problem, which callers treat as
|
|
74
|
+
* "fall back to the cold Vite path" rather than "the user's code is broken".
|
|
75
|
+
*/
|
|
76
|
+
function isEsbuildBuildFailure(err) {
|
|
77
|
+
return err instanceof Error && "errors" in err && Array.isArray(err.errors);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Create an incremental dev builder for the agent at `cwd`.
|
|
81
|
+
*
|
|
82
|
+
* The context is created lazily on first `build()` and kept across calls —
|
|
83
|
+
* that reuse is the entire point. A rebuild that fails for a non-compile
|
|
84
|
+
* reason drops the context so the next call starts from a clean one.
|
|
85
|
+
*/
|
|
86
|
+
function createDevWorkerBuilder(cwd) {
|
|
87
|
+
let ctx;
|
|
88
|
+
async function ensureContext() {
|
|
89
|
+
if (ctx) return ctx;
|
|
90
|
+
const { context } = await import("esbuild");
|
|
91
|
+
ctx = await context({
|
|
92
|
+
entryPoints: [path.join(cwd, "agent.ts")],
|
|
93
|
+
absWorkingDir: cwd,
|
|
94
|
+
bundle: true,
|
|
95
|
+
format: "esm",
|
|
96
|
+
platform: "node",
|
|
97
|
+
target: "node20",
|
|
98
|
+
write: false,
|
|
99
|
+
outfile: "worker.js",
|
|
100
|
+
minify: false,
|
|
101
|
+
logLevel: "silent",
|
|
102
|
+
loader: { ".md": "text" },
|
|
103
|
+
plugins: [rawSuffixPlugin]
|
|
104
|
+
});
|
|
105
|
+
return ctx;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
async build() {
|
|
109
|
+
const active = await ensureContext();
|
|
110
|
+
let result;
|
|
111
|
+
try {
|
|
112
|
+
result = await active.rebuild();
|
|
113
|
+
} catch (err) {
|
|
114
|
+
if (!isEsbuildBuildFailure(err)) {
|
|
115
|
+
ctx = void 0;
|
|
116
|
+
await active.dispose().catch(() => void 0);
|
|
117
|
+
}
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
const file = result.outputFiles?.[0];
|
|
121
|
+
if (!file) throw new Error("esbuild produced no output for agent.ts");
|
|
122
|
+
return file.text;
|
|
123
|
+
},
|
|
124
|
+
async dispose() {
|
|
125
|
+
const active = ctx;
|
|
126
|
+
ctx = void 0;
|
|
127
|
+
await active?.dispose().catch(() => void 0);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region _dev-server.ts
|
|
133
|
+
/**
|
|
134
|
+
* Dev server for directory-based agents.
|
|
135
|
+
*
|
|
136
|
+
* Imports agent.ts directly for the full agent definition,
|
|
137
|
+
* builds a runtime, and starts an HTTP+WebSocket server. Watches for
|
|
138
|
+
* file changes and restarts automatically. Optionally runs Vite for
|
|
139
|
+
* client SPA HMR.
|
|
140
|
+
*/
|
|
141
|
+
async function resolveAgentEnv(root, agentDef) {
|
|
142
|
+
const env = await resolveServerEnv(root);
|
|
143
|
+
const required = requiredProviderEnvVars(agentDef);
|
|
144
|
+
if (required.includes("ASSEMBLYAI_API_KEY") && !env.ASSEMBLYAI_API_KEY) env.ASSEMBLYAI_API_KEY = await ensureApiKey();
|
|
145
|
+
const missing = required.filter((name) => !(env[name] || process.env[name]));
|
|
146
|
+
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.`);
|
|
147
|
+
return env;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The env handed to `createServer` for host-mode connections: provider
|
|
151
|
+
* credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
|
|
152
|
+
* (it is a control variable, not something an agent declares in `.env`).
|
|
153
|
+
*/
|
|
154
|
+
function hostModeEnv(providerEnv) {
|
|
155
|
+
const gate = process.env.AAI_ALLOW_HOST;
|
|
156
|
+
return gate === void 0 ? providerEnv : {
|
|
157
|
+
...providerEnv,
|
|
158
|
+
AAI_ALLOW_HOST: gate
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Explicit bind host for the dev server, or `undefined` to take the
|
|
163
|
+
* loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
|
|
164
|
+
* interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
|
|
165
|
+
* undo the loopback default this exists to guard.
|
|
166
|
+
*/
|
|
167
|
+
function devBindHost() {
|
|
168
|
+
const host = process.env.AAI_DEV_HOST?.trim();
|
|
169
|
+
return host ? host : void 0;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Load the agent definition by bundling agent.ts (and all its local imports)
|
|
173
|
+
* into a single ESM file, then importing that. A raw `import(agent.ts?t=...)`
|
|
174
|
+
* only cache-busts agent.ts itself — transitive imports (./tools.ts, etc.)
|
|
175
|
+
* stay in Node's ESM registry, so edits to them are ignored on reload.
|
|
176
|
+
* Bundling picks them up.
|
|
177
|
+
*
|
|
178
|
+
* The bundle comes from the incremental esbuild builder (`_dev-bundler.ts`)
|
|
179
|
+
* rather than the deploy path's cold Vite build — a save rebuilds in tens of
|
|
180
|
+
* ms instead of 1–3 s. Compile errors in the agent's code propagate (the
|
|
181
|
+
* restart loop reports them and keeps the old server); any other builder
|
|
182
|
+
* failure falls back to the cold Vite build so an esbuild-specific gap can't
|
|
183
|
+
* take the dev loop down. Evaluation goes through the memoizing evaluator so
|
|
184
|
+
* a no-op save doesn't leak another module into the ESM registry.
|
|
185
|
+
*/
|
|
186
|
+
async function loadAgentDefWith(cwd, builder, evaluate) {
|
|
187
|
+
let code;
|
|
188
|
+
try {
|
|
189
|
+
code = await builder.build();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
if (isEsbuildBuildFailure(err)) throw err;
|
|
192
|
+
code = await buildWorker(cwd);
|
|
193
|
+
}
|
|
194
|
+
return evaluate(code);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* True for paths that should never trigger a restart: anything inside
|
|
198
|
+
* `node_modules/` and any dot-entry (`.git/`, `.aai/`, `.DS_Store`, …).
|
|
199
|
+
* `.git/` especially matters — commits and status checks churn the index
|
|
200
|
+
* and would otherwise cause spurious full backend restarts.
|
|
201
|
+
*
|
|
202
|
+
* Exception: `.env` / `.env.*` files stay watched — env edits should
|
|
203
|
+
* restart the server with the new values.
|
|
204
|
+
*/
|
|
205
|
+
function isIgnoredPath(dir, filePath) {
|
|
206
|
+
const rel = path.relative(dir, filePath);
|
|
207
|
+
if (!rel || rel.startsWith("..")) return false;
|
|
208
|
+
return rel.split(path.sep).some((segment) => {
|
|
209
|
+
if (segment === "node_modules") return true;
|
|
210
|
+
if (segment === ".env" || segment.startsWith(".env.")) return false;
|
|
211
|
+
return segment.startsWith(".");
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Watch the agent directory for changes and call `onChange` when detected.
|
|
216
|
+
* Debounces to avoid rapid restarts. Uses chokidar for reliable recursive
|
|
217
|
+
* watching across platforms (raw `fs.watch` misses events on Linux).
|
|
218
|
+
*/
|
|
219
|
+
function watchDirectory(dir, onChange) {
|
|
220
|
+
const debouncedChange = pDebounce(() => {
|
|
221
|
+
log.info("File change detected, restarting...");
|
|
222
|
+
onChange();
|
|
223
|
+
}, 300);
|
|
224
|
+
const watcher = watch(dir, {
|
|
225
|
+
ignored: (filePath) => isIgnoredPath(dir, filePath),
|
|
226
|
+
ignoreInitial: true,
|
|
227
|
+
persistent: false
|
|
228
|
+
});
|
|
229
|
+
watcher.on("error", (err) => {
|
|
230
|
+
const hint = errorCode(err) === "ENOSPC" ? " The inotify watch limit was reached — raise the fs.inotify max_user_watches sysctl." : "";
|
|
231
|
+
log.error(`File watcher error: ${errorMessage(err)}.${hint} Auto-restart on file changes may have stopped; restart \`aai dev\` after fixing.`);
|
|
232
|
+
});
|
|
233
|
+
watcher.on("all", () => {
|
|
234
|
+
debouncedChange().catch((err) => {
|
|
235
|
+
log.error(`Watch handler failed: ${errorMessage(err)}`);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
return watcher;
|
|
239
|
+
}
|
|
240
|
+
/** Locate the pre-built default aai-ui client (served when no custom client.tsx). */
|
|
241
|
+
function resolveDefaultClientDir() {
|
|
242
|
+
const require = createRequire(import.meta.url);
|
|
243
|
+
let pkgPath;
|
|
244
|
+
try {
|
|
245
|
+
pkgPath = require.resolve("@alexkroman1/aai-ui/package.json");
|
|
246
|
+
} catch (err) {
|
|
247
|
+
throw new Error(`Could not locate the default client UI (${errorMessage(err)}) — is @alexkroman1/aai-ui installed? Try reinstalling dependencies (pnpm install).`, { cause: err });
|
|
248
|
+
}
|
|
249
|
+
return path.join(path.dirname(pkgPath), "dist", "default-client");
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Vite dev-server config for the client SPA. Extracted so the proxy wiring
|
|
253
|
+
* is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
|
|
254
|
+
* with a `client.tsx` serves a page whose WebSocket never connects.
|
|
255
|
+
*
|
|
256
|
+
* `strictPort` because the reported URL is `http://localhost:<port>` —
|
|
257
|
+
* without it, Vite silently binds port+N when the port is busy and the
|
|
258
|
+
* printed/JSON-returned URL points at whatever else was listening.
|
|
259
|
+
*/
|
|
260
|
+
function viteDevConfig(cwd, vitePort, backendPort) {
|
|
261
|
+
const target = `http://localhost:${backendPort}`;
|
|
262
|
+
return {
|
|
263
|
+
root: cwd,
|
|
264
|
+
plugins: [fallbackHtmlPlugin(cwd)],
|
|
265
|
+
server: {
|
|
266
|
+
port: vitePort,
|
|
267
|
+
strictPort: true,
|
|
268
|
+
proxy: {
|
|
269
|
+
"/health": target,
|
|
270
|
+
"/websocket": {
|
|
271
|
+
target,
|
|
272
|
+
ws: true
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Start the dev server for a directory-based agent.
|
|
280
|
+
*
|
|
281
|
+
* Returns a cleanup function to shut down the server and watchers.
|
|
282
|
+
*/
|
|
283
|
+
async function startDevServer(opts) {
|
|
284
|
+
const { cwd, port } = opts;
|
|
285
|
+
const { createRuntime, createServer } = await import("@alexkroman1/aai/runtime");
|
|
286
|
+
const hasClient = existsSync(path.join(cwd, "client.tsx"));
|
|
287
|
+
const backendPort = hasClient ? await getPort({ port: portNumbers(port + 1, port + 100) }) : port;
|
|
288
|
+
const vitePort = port;
|
|
289
|
+
const clientDirOpt = hasClient ? {} : { clientDir: resolveDefaultClientDir() };
|
|
290
|
+
const devBuilder = createDevWorkerBuilder(cwd);
|
|
291
|
+
const evaluateWorker = createWorkerEvaluator(cwd);
|
|
292
|
+
/** Full build sequence, shared by initial startup and every restart. */
|
|
293
|
+
async function buildServer() {
|
|
294
|
+
const agentDef = await loadAgentDefWith(cwd, devBuilder, evaluateWorker);
|
|
295
|
+
const env = await resolveAgentEnv(cwd, agentDef);
|
|
296
|
+
const providerEnv = withHostCredentialFallback(env);
|
|
297
|
+
const runtime = createRuntime({
|
|
298
|
+
agent: agentDef,
|
|
299
|
+
env,
|
|
300
|
+
providerEnv
|
|
301
|
+
});
|
|
302
|
+
return createServer({
|
|
303
|
+
runtime,
|
|
304
|
+
name: agentDef.name,
|
|
305
|
+
env: hostModeEnv(providerEnv),
|
|
306
|
+
hostBaseAgent: agentDef,
|
|
307
|
+
...clientDirOpt
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
let restarting = true;
|
|
311
|
+
let pendingRestart = false;
|
|
312
|
+
let closed = false;
|
|
313
|
+
let currentServer;
|
|
314
|
+
function kickRestart() {
|
|
315
|
+
if (restarting) {
|
|
316
|
+
pendingRestart = true;
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
restarting = true;
|
|
320
|
+
restart().catch((err) => {
|
|
321
|
+
log.error(`Restart failed: ${errorMessage(err)}`);
|
|
322
|
+
}).finally(() => {
|
|
323
|
+
restarting = false;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
const watcher = watchDirectory(cwd, kickRestart);
|
|
327
|
+
let viteServer;
|
|
328
|
+
try {
|
|
329
|
+
currentServer = await buildServer();
|
|
330
|
+
await currentServer.listen(backendPort, devBindHost());
|
|
331
|
+
if (hasClient) {
|
|
332
|
+
const { createServer: createViteServer } = await import("vite");
|
|
333
|
+
viteServer = await createViteServer(viteDevConfig(cwd, vitePort, backendPort));
|
|
334
|
+
await viteServer.listen();
|
|
335
|
+
viteServer.httpServer?.on("error", (err) => {
|
|
336
|
+
log.error(`Vite dev server error: ${errorMessage(err)}`);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
} catch (err) {
|
|
340
|
+
await watcher.close().catch(() => void 0);
|
|
341
|
+
await devBuilder.dispose().catch(() => void 0);
|
|
342
|
+
await viteServer?.close().catch(() => void 0);
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
restarting = false;
|
|
346
|
+
if (pendingRestart) kickRestart();
|
|
347
|
+
async function restart() {
|
|
348
|
+
do {
|
|
349
|
+
pendingRestart = false;
|
|
350
|
+
await restartOnce();
|
|
351
|
+
} while (pendingRestart && !closed);
|
|
352
|
+
}
|
|
353
|
+
async function restartOnce() {
|
|
354
|
+
let newServer;
|
|
355
|
+
try {
|
|
356
|
+
newServer = await buildServer();
|
|
357
|
+
} catch (err) {
|
|
358
|
+
log.error(`Restart failed: ${errorMessage(err)} (previous server still running)`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (closed) {
|
|
362
|
+
await newServer.close().catch(() => void 0);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
await currentServer.close();
|
|
367
|
+
} catch {}
|
|
368
|
+
try {
|
|
369
|
+
await listenWithRetry(newServer);
|
|
370
|
+
currentServer = newServer;
|
|
371
|
+
if (closed) {
|
|
372
|
+
await newServer.close().catch(() => void 0);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
log.success("Restarted");
|
|
376
|
+
} catch (err) {
|
|
377
|
+
log.error(`Restart failed: ${errorMessage(err)} — dev server is down; save a file to retry.`);
|
|
378
|
+
await newServer.close().catch(() => void 0);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Listen with a few short-backoff retries. During the close→listen swap the
|
|
383
|
+
* port is momentarily free, so another process can snatch it (or the OS can
|
|
384
|
+
* hold it in TIME_WAIT); one blind attempt would leave the dev server down
|
|
385
|
+
* until the next file change.
|
|
386
|
+
*/
|
|
387
|
+
async function listenWithRetry(server) {
|
|
388
|
+
const LISTEN_ATTEMPTS = 3;
|
|
389
|
+
const LISTEN_RETRY_DELAY_MS = 250;
|
|
390
|
+
for (let attempt = 1;; attempt++) try {
|
|
391
|
+
await server.listen(backendPort, devBindHost());
|
|
392
|
+
return;
|
|
393
|
+
} catch (err) {
|
|
394
|
+
if (attempt >= LISTEN_ATTEMPTS || closed) throw err;
|
|
395
|
+
await new Promise((resolve) => setTimeout(resolve, LISTEN_RETRY_DELAY_MS));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
let cleanupPromise;
|
|
399
|
+
return () => {
|
|
400
|
+
cleanupPromise ??= (async () => {
|
|
401
|
+
closed = true;
|
|
402
|
+
await watcher.close().catch(() => void 0);
|
|
403
|
+
await devBuilder.dispose().catch(() => void 0);
|
|
404
|
+
await viteServer?.close().catch(() => void 0);
|
|
405
|
+
await currentServer.close().catch(() => void 0);
|
|
406
|
+
})();
|
|
407
|
+
return cleanupPromise;
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
//#endregion
|
|
411
|
+
export { startDevServer };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as
|
|
3
|
-
import {
|
|
2
|
+
import { a as isEexist, l as writeJson, o as readJson, r as errorMessage } from "./_utils-ECl2je7-.mjs";
|
|
3
|
+
import { r as isDevMode, t as getMonorepoRoot } from "./_agent-Ba5Ykp05.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
|
|
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
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 {
|
|
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 =
|
|
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 };
|