@alexkroman1/aai-cli 5.9.0 → 5.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin.mjs +32 -0
- package/dist/{_agent-BPEXSdWX.mjs → _agent-DS2PUJcl.mjs} +1 -1
- package/dist/{_bundler-Cjaxa2wi.mjs → _bundler-DC17suWN.mjs} +2 -2
- package/dist/{_config-BJm2o795.mjs → _config-DMyolIk9.mjs} +92 -10
- package/dist/_config.d.ts +12 -0
- package/dist/_dev-env.d.ts +38 -0
- package/dist/{_dev-server-Cnf4jNzE.mjs → _dev-server-B3ivqyEd.mjs} +72 -54
- package/dist/_dev-server.d.ts +2 -2
- package/dist/{_init-DVNeyGSl.mjs → _init-CYLvYU-B.mjs} +3 -3
- package/dist/{_server-common-CnaP_Urf.mjs → _server-common-DX8Bfrf5.mjs} +1 -1
- package/dist/{_slug-api-CQw1YGR5.mjs → _slug-api-fRNNR8tz.mjs} +1 -1
- package/dist/{_templates-DDx08LIG.mjs → _templates-BWJOiWOO.mjs} +2 -2
- package/dist/{_typecheck-gate-BT4iPz7A.mjs → _typecheck-gate-DB-PY0A3.mjs} +1 -1
- package/dist/{_ui-RZmPgrF6.mjs → _ui-DfwfDbT-.mjs} +26 -1
- package/dist/_ui.d.ts +19 -0
- package/dist/{_utils-Ch0J4s6a.mjs → _utils-8KKw-bzi.mjs} +9 -2
- package/dist/_utils.d.ts +9 -2
- package/dist/{build-WdGtFsSx.mjs → build-CACFbdQ4.mjs} +4 -4
- package/dist/cli.mjs +20 -20
- package/dist/{client-bundler-yiWoXrgb.mjs → client-bundler-DONm-khu.mjs} +1 -1
- package/dist/client-bundler.mjs +1 -1
- package/dist/{delete-Dr1Jn65f.mjs → delete-DEZ7u3u4.mjs} +5 -4
- package/dist/delete.d.ts +5 -0
- package/dist/{deploy-Bij1jES6.mjs → deploy-Ch0d_jje.mjs} +7 -7
- package/dist/{dev-HBCgChsA.mjs → dev-CdeRcYiQ.mjs} +6 -6
- package/dist/{init-BYGp3Usr.mjs → init-GQsNWkLJ.mjs} +7 -7
- package/dist/{login-CXazkkcR.mjs → login-BeFUiU6M.mjs} +6 -7
- package/dist/scaffold/CLAUDE.md +35 -12
- package/dist/scaffold/package.json +3 -3
- package/dist/{secret-vhGe-r2a.mjs → secret-4_dYyrpA.mjs} +2 -2
- package/dist/{storage-CMPsAmch.mjs → storage-BTfErOOW.mjs} +2 -2
- package/dist/{studio-B6zDNPPr.mjs → studio-LNvXtWak.mjs} +6 -6
- package/dist/templates/dispatch-center/agent.ts +4 -6
- package/dist/templates/retail/agent.ts +4 -6
- package/dist/templates/retail/registry.test.ts +5 -3
- package/dist/templates/retail/resolve.test.ts +6 -4
- package/dist/templates/retail/seed.test.ts +21 -13
- package/dist/templates/retail/shared.test.ts +9 -5
- package/dist/templates/solo-rpg/agent.test.ts +7 -5
- package/dist/templates/solo-rpg/shared.ts +2 -2
- package/dist/{test-CkXvfcpq.mjs → test-C-V98oC-.mjs} +3 -4
- package/dist/typecheck.mjs +65 -14
- package/package.json +5 -4
package/bin.mjs
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `aai` bin in BOTH layouts — the source checkout (`pnpm link --global`,
|
|
3
|
+
// where it loads `cli.ts` directly) and the published tarball (where only
|
|
4
|
+
// `dist/` ships, so it loads `dist/cli.mjs`). It used to be the dev bin only,
|
|
5
|
+
// with `publishConfig.bin` pointing straight at `dist/cli.mjs`.
|
|
6
|
+
//
|
|
7
|
+
// One bin for both is what makes the compile cache reachable. The cache only
|
|
8
|
+
// covers modules compiled AFTER the call, and every dependency of the CLI is
|
|
9
|
+
// external (`deps.neverBundle` in tsdown.config.ts), so `dist/cli.mjs` carries
|
|
10
|
+
// hoisted `import` statements for citty, execa and the rest — all evaluated
|
|
11
|
+
// before any statement in that file could run. A banner or a first-line call
|
|
12
|
+
// inside `cli.ts` would therefore cache nothing that costs anything. Loading
|
|
13
|
+
// the entry through a DYNAMIC import from a wrapper is the only ordering that
|
|
14
|
+
// puts the enable genuinely first.
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { enableCompileCache } from "node:module";
|
|
17
|
+
import { fileURLToPath } from "node:url";
|
|
18
|
+
|
|
19
|
+
// Caches V8 bytecode per (Node version, file content) under the user's cache
|
|
20
|
+
// dir, so `aai` pays parse+compile once per version rather than per
|
|
21
|
+
// invocation. Deliberately unguarded: this returns a `{ status }` result and
|
|
22
|
+
// does not throw, so an unwritable cache dir degrades to today's behaviour.
|
|
23
|
+
enableCompileCache();
|
|
24
|
+
|
|
25
|
+
// Source wins when present: a checkout that has also been built must keep
|
|
26
|
+
// running `cli.ts`, or `pnpm link --global` would silently serve a stale
|
|
27
|
+
// `dist/` instead of the working tree.
|
|
28
|
+
const source = new URL("./cli.ts", import.meta.url);
|
|
29
|
+
const entry = existsSync(fileURLToPath(source))
|
|
30
|
+
? source
|
|
31
|
+
: new URL("./dist/cli.mjs", import.meta.url);
|
|
32
|
+
await import(entry.href);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as serverOrigin, i as readProjectConfig, n as ensureApiKey, r as readGlobalConfig, t as approveServer } from "./_config-DMyolIk9.mjs";
|
|
3
3
|
import { existsSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { u as validateAgentExport } from "./_utils-
|
|
3
|
-
import { t as buildClient } from "./client-bundler-
|
|
2
|
+
import { u as validateAgentExport } from "./_utils-8KKw-bzi.mjs";
|
|
3
|
+
import { t as buildClient } from "./client-bundler-DONm-khu.mjs";
|
|
4
4
|
import { buildWorker } from "./worker-bundler.mjs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { t as CliError } from "./_output-CC300DzW.mjs";
|
|
3
|
-
import { a as errorMessage, c as readJson, d as writeJson } from "./_utils-
|
|
3
|
+
import { a as errorMessage, c as readJson, d as writeJson } from "./_utils-8KKw-bzi.mjs";
|
|
4
4
|
import { mkdtempSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
|
+
import fs from "node:fs/promises";
|
|
7
|
+
import { setTimeout } from "node:timers/promises";
|
|
6
8
|
import { tmpdir } from "node:os";
|
|
7
9
|
import envPaths from "env-paths";
|
|
8
10
|
import { z } from "zod";
|
|
@@ -107,6 +109,87 @@ function serverOrigin(url) {
|
|
|
107
109
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
|
|
108
110
|
return parsed.origin;
|
|
109
111
|
}
|
|
112
|
+
/** Bounded so a stuck lock degrades to the old racy write, never a hang. */
|
|
113
|
+
const CONFIG_LOCK_TIMEOUT_MS = 2e3;
|
|
114
|
+
const CONFIG_LOCK_RETRY_MS = 20;
|
|
115
|
+
/** Older than this and the holder is assumed dead (crashed mid-update). */
|
|
116
|
+
const CONFIG_LOCK_STALE_MS = 1e4;
|
|
117
|
+
/**
|
|
118
|
+
* Serialize a read-modify-write of the global config ACROSS PROCESSES.
|
|
119
|
+
*
|
|
120
|
+
* `writeJson` makes each individual write atomic, so no reader ever sees a
|
|
121
|
+
* torn file — but the read→modify→write SPAN is not atomic, and every writer
|
|
122
|
+
* here replaces the whole document. Two concurrent CLI invocations therefore
|
|
123
|
+
* lose each other's updates: measured on this repo, 8 parallel commands each
|
|
124
|
+
* approving a distinct origin recorded only 5 of them, and — the case that
|
|
125
|
+
* matters — a concurrent `approveServer` straddling the final write of
|
|
126
|
+
* `aai login` DISCARDS THE API KEY the login just reported saving, leaving
|
|
127
|
+
* the next command with `not_logged_in`. That window is wide open in practice:
|
|
128
|
+
* `aai login` polls for up to five minutes while the user approves in the
|
|
129
|
+
* browser, so any other command run in that time can be mid-update when the
|
|
130
|
+
* key lands.
|
|
131
|
+
*
|
|
132
|
+
* The lock is a `wx` (exclusive-create) lockfile — atomic on every platform
|
|
133
|
+
* we target, and the only primitive available across processes without a
|
|
134
|
+
* daemon. Three deliberate properties:
|
|
135
|
+
*
|
|
136
|
+
* - **Acquisition is bounded** (`CONFIG_LOCK_TIMEOUT_MS`). On timeout the
|
|
137
|
+
* update proceeds UNLOCKED rather than throwing: these are small
|
|
138
|
+
* convenience files, and failing `aai login` because a lockfile is stuck
|
|
139
|
+
* would be strictly worse than the lost update the lock exists to prevent.
|
|
140
|
+
* - **A stale lock is broken** (`CONFIG_LOCK_STALE_MS`). A process killed
|
|
141
|
+
* mid-update leaves the file behind; without this, one crash would make
|
|
142
|
+
* every later config write take the unlocked path forever.
|
|
143
|
+
* - **Never nest.** Re-entering from inside `fn` would self-deadlock until the
|
|
144
|
+
* timeout. `executeLogin` calls `approveServer` and the key update in
|
|
145
|
+
* sequence, not nested — keep it that way.
|
|
146
|
+
*/
|
|
147
|
+
async function withGlobalConfigLock(dir, fn) {
|
|
148
|
+
const lockPath = path.join(dir, "config.lock");
|
|
149
|
+
const deadline = Date.now() + CONFIG_LOCK_TIMEOUT_MS;
|
|
150
|
+
let held = false;
|
|
151
|
+
for (;;) try {
|
|
152
|
+
await fs.mkdir(dir, {
|
|
153
|
+
recursive: true,
|
|
154
|
+
mode: 448
|
|
155
|
+
});
|
|
156
|
+
await (await fs.open(lockPath, "wx", 384)).close();
|
|
157
|
+
held = true;
|
|
158
|
+
break;
|
|
159
|
+
} catch (err) {
|
|
160
|
+
if (err.code !== "EEXIST") break;
|
|
161
|
+
if (await fs.stat(lockPath).then((s) => Date.now() - s.mtimeMs).catch(() => 0) > CONFIG_LOCK_STALE_MS) {
|
|
162
|
+
await fs.rm(lockPath, { force: true }).catch(() => void 0);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (Date.now() >= deadline) break;
|
|
166
|
+
await setTimeout(CONFIG_LOCK_RETRY_MS);
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
return await fn();
|
|
170
|
+
} finally {
|
|
171
|
+
if (held) await fs.rm(lockPath, { force: true }).catch(() => void 0);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Apply `update` to the global config under the cross-process lock, re-reading
|
|
176
|
+
* inside it so the merge is against current contents rather than a snapshot
|
|
177
|
+
* taken before the lock was held. Every read-modify-write of the global config
|
|
178
|
+
* must go through this — a direct `readGlobalConfig`/`writeGlobalConfig` pair
|
|
179
|
+
* is the bug this exists to prevent.
|
|
180
|
+
*
|
|
181
|
+
* Returning the argument unchanged skips the write, so the common no-op case
|
|
182
|
+
* (an origin already approved — i.e. most `--server` invocations) costs a read
|
|
183
|
+
* rather than a rewrite plus the lock contention that comes with it.
|
|
184
|
+
*/
|
|
185
|
+
async function updateGlobalConfig(update, configDir) {
|
|
186
|
+
const dir = configDir ?? getConfigDir();
|
|
187
|
+
await withGlobalConfigLock(dir, async () => {
|
|
188
|
+
const current = await readGlobalConfig(dir);
|
|
189
|
+
const next = update(current);
|
|
190
|
+
if (next !== current) await writeGlobalConfig(dir, next);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
110
193
|
/**
|
|
111
194
|
* Record `url`'s origin as user-approved, so later commands in this project
|
|
112
195
|
* may send credentials there without re-passing `--server`.
|
|
@@ -114,14 +197,13 @@ function serverOrigin(url) {
|
|
|
114
197
|
async function approveServer(url, configDir) {
|
|
115
198
|
const origin = serverOrigin(url);
|
|
116
199
|
if (!origin) return;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
200
|
+
await updateGlobalConfig((config) => {
|
|
201
|
+
const approved = config.approvedServers ?? [];
|
|
202
|
+
return approved.includes(origin) ? config : {
|
|
203
|
+
...config,
|
|
204
|
+
approvedServers: [...approved, origin]
|
|
205
|
+
};
|
|
206
|
+
}, configDir);
|
|
125
207
|
}
|
|
126
208
|
async function readGlobalConfig(configDir) {
|
|
127
209
|
const dir = configDir ?? getConfigDir();
|
|
@@ -157,4 +239,4 @@ async function ensureApiKey(configDir) {
|
|
|
157
239
|
throw new CliError("not_logged_in", "You're not logged in.", "Run `aai login` to link your account. Non-interactive setups can point AAI_CONFIG_DIR at a config dir holding a logged-in key.");
|
|
158
240
|
}
|
|
159
241
|
//#endregion
|
|
160
|
-
export {
|
|
242
|
+
export { serverOrigin as a, writeProjectConfig as c, readProjectConfig as i, ensureApiKey as n, updateGlobalConfig as o, readGlobalConfig as r, updateProjectConfig as s, approveServer as t };
|
package/dist/_config.d.ts
CHANGED
|
@@ -54,6 +54,18 @@ export type GlobalConfig = {
|
|
|
54
54
|
* were a real origin.
|
|
55
55
|
*/
|
|
56
56
|
export declare function serverOrigin(url: string): string | null;
|
|
57
|
+
/**
|
|
58
|
+
* Apply `update` to the global config under the cross-process lock, re-reading
|
|
59
|
+
* inside it so the merge is against current contents rather than a snapshot
|
|
60
|
+
* taken before the lock was held. Every read-modify-write of the global config
|
|
61
|
+
* must go through this — a direct `readGlobalConfig`/`writeGlobalConfig` pair
|
|
62
|
+
* is the bug this exists to prevent.
|
|
63
|
+
*
|
|
64
|
+
* Returning the argument unchanged skips the write, so the common no-op case
|
|
65
|
+
* (an origin already approved — i.e. most `--server` invocations) costs a read
|
|
66
|
+
* rather than a rewrite plus the lock contention that comes with it.
|
|
67
|
+
*/
|
|
68
|
+
export declare function updateGlobalConfig(update: (current: GlobalConfig) => GlobalConfig, configDir?: string): Promise<void>;
|
|
57
69
|
/**
|
|
58
70
|
* Record `url`'s origin as user-approved, so later commands in this project
|
|
59
71
|
* may send credentials there without re-passing `--server`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The dev server's CONTROL variables — the three things read straight from the
|
|
3
|
+
* shell rather than from the agent's `.env`.
|
|
4
|
+
*
|
|
5
|
+
* That distinction is the reason these live together. `resolveServerEnv` builds
|
|
6
|
+
* `ctx.env` from `.env`-declared keys only, deliberately, so an agent cannot
|
|
7
|
+
* come to depend on a shell variable that will not exist after deploy. These
|
|
8
|
+
* three are not agent config at all — they configure the *dev server process*
|
|
9
|
+
* (who may connect, where it binds, whether it restarts), so they are read from
|
|
10
|
+
* the environment on purpose and must never leak into `ctx.env`.
|
|
11
|
+
*
|
|
12
|
+
* Split out of `_dev-server.ts` purely to keep that module under the
|
|
13
|
+
* file-length cap; it is the only consumer.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* The env handed to `createServer` for host-mode connections: provider
|
|
17
|
+
* credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
|
|
18
|
+
* (it is a control variable, not something an agent declares in `.env`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function hostModeEnv(providerEnv: Record<string, string>): Record<string, string>;
|
|
21
|
+
/**
|
|
22
|
+
* Explicit bind host for the dev server, or `undefined` to take the
|
|
23
|
+
* loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
|
|
24
|
+
* interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
|
|
25
|
+
* undo the loopback default this exists to guard.
|
|
26
|
+
*/
|
|
27
|
+
export declare function devBindHost(): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* File watching is OPT-IN — `AAI_DEV_WATCH=1` turns it on.
|
|
30
|
+
*
|
|
31
|
+
* A restart rebuilds the bundle and replaces the server, which drops nothing
|
|
32
|
+
* mid-request but does end in-flight voice sessions. That is the right default
|
|
33
|
+
* while editing an agent and the wrong one while a benchmark is driving the
|
|
34
|
+
* host for twenty minutes: a stray formatter save, a `.env` touch, or a git
|
|
35
|
+
* operation restarts the server underneath the run, and the harness reports it
|
|
36
|
+
* as a provider failure several records deep.
|
|
37
|
+
*/
|
|
38
|
+
export declare function devWatchEnabled(): boolean;
|
|
@@ -1,26 +1,77 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as log } from "./_ui-
|
|
3
|
-
import { a as errorMessage, r as errorCode } from "./_utils-
|
|
4
|
-
import { n as fallbackHtmlPlugin } from "./client-bundler-
|
|
2
|
+
import { n as log, r as notify } from "./_ui-DfwfDbT-.mjs";
|
|
3
|
+
import { a as errorMessage, r as errorCode } from "./_utils-8KKw-bzi.mjs";
|
|
4
|
+
import { n as fallbackHtmlPlugin } from "./client-bundler-DONm-khu.mjs";
|
|
5
5
|
import { buildWorker } from "./worker-bundler.mjs";
|
|
6
|
-
import { n as createWorkerEvaluator } from "./_bundler-
|
|
7
|
-
import { n as ensureApiKey } from "./_config-
|
|
8
|
-
import { t as resolveServerEnv } from "./_server-common-
|
|
9
|
-
import { createRequire } from "node:module";
|
|
6
|
+
import { n as createWorkerEvaluator } from "./_bundler-DC17suWN.mjs";
|
|
7
|
+
import { n as ensureApiKey } from "./_config-DMyolIk9.mjs";
|
|
8
|
+
import { t as resolveServerEnv } from "./_server-common-DX8Bfrf5.mjs";
|
|
10
9
|
import { existsSync } from "node:fs";
|
|
11
10
|
import path from "node:path";
|
|
12
11
|
import { setTimeout } from "node:timers/promises";
|
|
13
12
|
import { createRuntime, createServer, requiredProviderEnvVars, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
|
|
13
|
+
import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
|
|
14
14
|
import { watch } from "chokidar";
|
|
15
15
|
import getPort, { portNumbers } from "get-port";
|
|
16
16
|
import pDebounce from "p-debounce";
|
|
17
|
+
//#region _dev-env.ts
|
|
18
|
+
/**
|
|
19
|
+
* The dev server's CONTROL variables — the three things read straight from the
|
|
20
|
+
* shell rather than from the agent's `.env`.
|
|
21
|
+
*
|
|
22
|
+
* That distinction is the reason these live together. `resolveServerEnv` builds
|
|
23
|
+
* `ctx.env` from `.env`-declared keys only, deliberately, so an agent cannot
|
|
24
|
+
* come to depend on a shell variable that will not exist after deploy. These
|
|
25
|
+
* three are not agent config at all — they configure the *dev server process*
|
|
26
|
+
* (who may connect, where it binds, whether it restarts), so they are read from
|
|
27
|
+
* the environment on purpose and must never leak into `ctx.env`.
|
|
28
|
+
*
|
|
29
|
+
* Split out of `_dev-server.ts` purely to keep that module under the
|
|
30
|
+
* file-length cap; it is the only consumer.
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* The env handed to `createServer` for host-mode connections: provider
|
|
34
|
+
* credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
|
|
35
|
+
* (it is a control variable, not something an agent declares in `.env`).
|
|
36
|
+
*/
|
|
37
|
+
function hostModeEnv(providerEnv) {
|
|
38
|
+
const gate = process.env.AAI_ALLOW_HOST;
|
|
39
|
+
return gate === void 0 ? providerEnv : {
|
|
40
|
+
...providerEnv,
|
|
41
|
+
AAI_ALLOW_HOST: gate
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Explicit bind host for the dev server, or `undefined` to take the
|
|
46
|
+
* loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
|
|
47
|
+
* interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
|
|
48
|
+
* undo the loopback default this exists to guard.
|
|
49
|
+
*/
|
|
50
|
+
function devBindHost() {
|
|
51
|
+
const host = process.env.AAI_DEV_HOST?.trim();
|
|
52
|
+
return host ? host : void 0;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* File watching is OPT-IN — `AAI_DEV_WATCH=1` turns it on.
|
|
56
|
+
*
|
|
57
|
+
* A restart rebuilds the bundle and replaces the server, which drops nothing
|
|
58
|
+
* mid-request but does end in-flight voice sessions. That is the right default
|
|
59
|
+
* while editing an agent and the wrong one while a benchmark is driving the
|
|
60
|
+
* host for twenty minutes: a stray formatter save, a `.env` touch, or a git
|
|
61
|
+
* operation restarts the server underneath the run, and the harness reports it
|
|
62
|
+
* as a provider failure several records deep.
|
|
63
|
+
*/
|
|
64
|
+
function devWatchEnabled() {
|
|
65
|
+
return /^(1|true|yes|on)$/i.test(process.env.AAI_DEV_WATCH?.trim() ?? "");
|
|
66
|
+
}
|
|
67
|
+
//#endregion
|
|
17
68
|
//#region _dev-server.ts
|
|
18
69
|
/**
|
|
19
70
|
* Dev server for directory-based agents.
|
|
20
71
|
*
|
|
21
72
|
* Imports agent.ts directly for the full agent definition,
|
|
22
|
-
* builds a runtime, and starts an HTTP+WebSocket server.
|
|
23
|
-
*
|
|
73
|
+
* builds a runtime, and starts an HTTP+WebSocket server. File watching is
|
|
74
|
+
* opt-in via `AAI_DEV_WATCH=1` (see devWatchEnabled). Optionally runs Vite for
|
|
24
75
|
* client SPA HMR.
|
|
25
76
|
*/
|
|
26
77
|
/**
|
|
@@ -58,28 +109,6 @@ async function resolveAgentEnv(root, agentDef) {
|
|
|
58
109
|
return env;
|
|
59
110
|
}
|
|
60
111
|
/**
|
|
61
|
-
* The env handed to `createServer` for host-mode connections: provider
|
|
62
|
-
* credentials plus the `AAI_ALLOW_HOST` gate read straight from the shell
|
|
63
|
-
* (it is a control variable, not something an agent declares in `.env`).
|
|
64
|
-
*/
|
|
65
|
-
function hostModeEnv(providerEnv) {
|
|
66
|
-
const gate = process.env.AAI_ALLOW_HOST;
|
|
67
|
-
return gate === void 0 ? providerEnv : {
|
|
68
|
-
...providerEnv,
|
|
69
|
-
AAI_ALLOW_HOST: gate
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Explicit bind host for the dev server, or `undefined` to take the
|
|
74
|
-
* loopback default. An empty `AAI_DEV_HOST` means "unset", not "every
|
|
75
|
-
* interface" — Node treats `listen(port, "")` as 0.0.0.0, which would quietly
|
|
76
|
-
* undo the loopback default this exists to guard.
|
|
77
|
-
*/
|
|
78
|
-
function devBindHost() {
|
|
79
|
-
const host = process.env.AAI_DEV_HOST?.trim();
|
|
80
|
-
return host ? host : void 0;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
112
|
* Load the agent definition by bundling agent.ts (and all its local imports)
|
|
84
113
|
* into a single ESM file, then importing that. A raw `import(agent.ts?t=...)`
|
|
85
114
|
* only cache-busts agent.ts itself — transitive imports (./tools.ts, etc.)
|
|
@@ -120,7 +149,7 @@ function isIgnoredPath(dir, filePath) {
|
|
|
120
149
|
*/
|
|
121
150
|
function watchDirectory(dir, onChange) {
|
|
122
151
|
const debouncedChange = pDebounce(() => {
|
|
123
|
-
|
|
152
|
+
notify("info", "File change detected, restarting...");
|
|
124
153
|
onChange();
|
|
125
154
|
}, 300);
|
|
126
155
|
const watcher = watch(dir, {
|
|
@@ -130,26 +159,15 @@ function watchDirectory(dir, onChange) {
|
|
|
130
159
|
});
|
|
131
160
|
watcher.on("error", (err) => {
|
|
132
161
|
const hint = errorCode(err) === "ENOSPC" ? " The inotify watch limit was reached — raise the fs.inotify max_user_watches sysctl." : "";
|
|
133
|
-
|
|
162
|
+
notify("error", `File watcher error: ${errorMessage(err)}.${hint} Auto-restart on file changes may have stopped; restart \`aai dev\` after fixing.`);
|
|
134
163
|
});
|
|
135
164
|
watcher.on("all", () => {
|
|
136
165
|
debouncedChange().catch((err) => {
|
|
137
|
-
|
|
166
|
+
notify("error", `Watch handler failed: ${errorMessage(err)}`);
|
|
138
167
|
});
|
|
139
168
|
});
|
|
140
169
|
return watcher;
|
|
141
170
|
}
|
|
142
|
-
/** Locate the pre-built default aai-ui client (served when no custom client.tsx). */
|
|
143
|
-
function resolveDefaultClientDir() {
|
|
144
|
-
const require = createRequire(import.meta.url);
|
|
145
|
-
let pkgPath;
|
|
146
|
-
try {
|
|
147
|
-
pkgPath = require.resolve("@alexkroman1/aai-ui/package.json");
|
|
148
|
-
} catch (err) {
|
|
149
|
-
throw new Error(`Could not locate the default client UI (${errorMessage(err)}) — is @alexkroman1/aai-ui installed? Try reinstalling dependencies (pnpm install).`, { cause: err });
|
|
150
|
-
}
|
|
151
|
-
return path.join(path.dirname(pkgPath), "dist", "default-client");
|
|
152
|
-
}
|
|
153
171
|
/**
|
|
154
172
|
* Vite dev-server config for the client SPA. Extracted so the proxy wiring
|
|
155
173
|
* is unit-testable: `/websocket` MUST proxy with `ws: true` or `aai dev`
|
|
@@ -188,7 +206,7 @@ async function startDevServer(opts) {
|
|
|
188
206
|
const hasClient = existsSync(path.join(cwd, "client.tsx"));
|
|
189
207
|
const backendPort = hasClient ? await getPort({ port: portNumbers(port + 1, port + 100) }) : port;
|
|
190
208
|
const vitePort = port;
|
|
191
|
-
const clientDirOpt = hasClient ? {} : { clientDir:
|
|
209
|
+
const clientDirOpt = hasClient ? {} : { clientDir: defaultClientDir() };
|
|
192
210
|
const evaluateWorker = createWorkerEvaluator();
|
|
193
211
|
/** Full build sequence, shared by initial startup and every restart. */
|
|
194
212
|
async function buildServer() {
|
|
@@ -219,12 +237,12 @@ async function startDevServer(opts) {
|
|
|
219
237
|
}
|
|
220
238
|
restarting = true;
|
|
221
239
|
restart().catch((err) => {
|
|
222
|
-
|
|
240
|
+
notify("error", `Restart failed: ${errorMessage(err)}`);
|
|
223
241
|
}).finally(() => {
|
|
224
242
|
restarting = false;
|
|
225
243
|
});
|
|
226
244
|
}
|
|
227
|
-
const watcher = watchDirectory(cwd, kickRestart);
|
|
245
|
+
const watcher = devWatchEnabled() ? watchDirectory(cwd, kickRestart) : void 0;
|
|
228
246
|
let viteServer;
|
|
229
247
|
try {
|
|
230
248
|
currentServer = await buildServer();
|
|
@@ -234,11 +252,11 @@ async function startDevServer(opts) {
|
|
|
234
252
|
viteServer = await createViteServer(viteDevConfig(cwd, vitePort, backendPort));
|
|
235
253
|
await viteServer.listen();
|
|
236
254
|
viteServer.httpServer?.on("error", (err) => {
|
|
237
|
-
|
|
255
|
+
notify("error", `Vite dev server error: ${errorMessage(err)}`);
|
|
238
256
|
});
|
|
239
257
|
}
|
|
240
258
|
} catch (err) {
|
|
241
|
-
await watcher
|
|
259
|
+
await watcher?.close().catch(() => void 0);
|
|
242
260
|
await viteServer?.close().catch(() => void 0);
|
|
243
261
|
throw err;
|
|
244
262
|
}
|
|
@@ -255,7 +273,7 @@ async function startDevServer(opts) {
|
|
|
255
273
|
try {
|
|
256
274
|
newServer = await buildServer();
|
|
257
275
|
} catch (err) {
|
|
258
|
-
|
|
276
|
+
notify("error", `Restart failed: ${errorMessage(err)} (previous server still running)`);
|
|
259
277
|
return;
|
|
260
278
|
}
|
|
261
279
|
if (closed) {
|
|
@@ -272,9 +290,9 @@ async function startDevServer(opts) {
|
|
|
272
290
|
await newServer.close().catch(() => void 0);
|
|
273
291
|
return;
|
|
274
292
|
}
|
|
275
|
-
|
|
293
|
+
notify("success", "Restarted");
|
|
276
294
|
} catch (err) {
|
|
277
|
-
|
|
295
|
+
notify("error", `Restart failed: ${errorMessage(err)} — dev server is down; save a file to retry.`);
|
|
278
296
|
await newServer.close().catch(() => void 0);
|
|
279
297
|
}
|
|
280
298
|
}
|
|
@@ -299,7 +317,7 @@ async function startDevServer(opts) {
|
|
|
299
317
|
return () => {
|
|
300
318
|
cleanupPromise ??= (async () => {
|
|
301
319
|
closed = true;
|
|
302
|
-
await watcher
|
|
320
|
+
await watcher?.close().catch(() => void 0);
|
|
303
321
|
await viteServer?.close().catch(() => void 0);
|
|
304
322
|
await currentServer.close().catch(() => void 0);
|
|
305
323
|
})();
|
package/dist/_dev-server.d.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* Dev server for directory-based agents.
|
|
3
3
|
*
|
|
4
4
|
* Imports agent.ts directly for the full agent definition,
|
|
5
|
-
* builds a runtime, and starts an HTTP+WebSocket server.
|
|
6
|
-
*
|
|
5
|
+
* builds a runtime, and starts an HTTP+WebSocket server. File watching is
|
|
6
|
+
* opt-in via `AAI_DEV_WATCH=1` (see devWatchEnabled). Optionally runs Vite for
|
|
7
7
|
* client SPA HMR.
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentDef } from "@alexkroman1/aai";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { c as readJson, d as writeJson, s as isEexist } from "./_utils-
|
|
3
|
-
import { r as isDevMode, t as getMonorepoRoot } from "./_agent-
|
|
4
|
-
import { REPO_URL, downloadAndMergeTemplate } from "./_templates-
|
|
2
|
+
import { c as readJson, d as writeJson, s as isEexist } from "./_utils-8KKw-bzi.mjs";
|
|
3
|
+
import { r as isDevMode, t as getMonorepoRoot } from "./_agent-DS2PUJcl.mjs";
|
|
4
|
+
import { REPO_URL, downloadAndMergeTemplate } from "./_templates-BWJOiWOO.mjs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
//#region _init.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { a as errorMessage } from "./_utils-
|
|
3
|
-
import { t as getMonorepoRoot } from "./_agent-
|
|
2
|
+
import { a as errorMessage } from "./_utils-8KKw-bzi.mjs";
|
|
3
|
+
import { t as getMonorepoRoot } from "./_agent-DS2PUJcl.mjs";
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
@@ -13,6 +13,31 @@ function silenceOutput() {
|
|
|
13
13
|
silenced = true;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
+
* Emit a message that must survive JSON mode — human mode gets the normal
|
|
17
|
+
* clack styling, JSON mode gets a plain line on STDERR.
|
|
18
|
+
*
|
|
19
|
+
* `silenceOutput` no-ops every `log` method so that JSON mode's contract
|
|
20
|
+
* ("exactly one result line on stdout") holds. That is right for
|
|
21
|
+
* request/response commands, and wrong for LONG-RUNNING ones: `aai dev`
|
|
22
|
+
* writes its single JSON line at startup and then keeps running, so every
|
|
23
|
+
* later message — a failed rebuild, an unhandled rejection, "the dev server
|
|
24
|
+
* is down; save a file to retry" — was silenced for the rest of the process.
|
|
25
|
+
* And JSON mode is AUTO-DETECTED on a pipe, so that is the normal case:
|
|
26
|
+
* `aai dev > dev.log`, a process supervisor, or a container all hid every
|
|
27
|
+
* build failure, leaving the old agent served with nothing to say why edits
|
|
28
|
+
* had stopped taking effect.
|
|
29
|
+
*
|
|
30
|
+
* stderr keeps the stdout contract intact — a script still parses one JSON
|
|
31
|
+
* line — while a human tailing the log sees what happened.
|
|
32
|
+
*/
|
|
33
|
+
function notify(level, message) {
|
|
34
|
+
if (!silenced) {
|
|
35
|
+
log[level](message);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
process.stderr.write(`${message}\n`);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
16
41
|
* Unwrap a clack prompt result, exiting cleanly if the user cancelled.
|
|
17
42
|
* `message` lets the caller name what was cancelled (e.g. "Setup cancelled").
|
|
18
43
|
*/
|
|
@@ -40,4 +65,4 @@ function parsePort(raw) {
|
|
|
40
65
|
return port;
|
|
41
66
|
}
|
|
42
67
|
//#endregion
|
|
43
|
-
export {
|
|
68
|
+
export { silenceOutput as a, parsePort as i, log as n, unwrapCancel as o, notify as r, fmtUrl as t };
|
package/dist/_ui.d.ts
CHANGED
|
@@ -4,6 +4,25 @@ type Log = typeof p.log;
|
|
|
4
4
|
export declare const log: Log;
|
|
5
5
|
/** Replace all log methods with no-ops. Call once in JSON mode. */
|
|
6
6
|
export declare function silenceOutput(): void;
|
|
7
|
+
/**
|
|
8
|
+
* Emit a message that must survive JSON mode — human mode gets the normal
|
|
9
|
+
* clack styling, JSON mode gets a plain line on STDERR.
|
|
10
|
+
*
|
|
11
|
+
* `silenceOutput` no-ops every `log` method so that JSON mode's contract
|
|
12
|
+
* ("exactly one result line on stdout") holds. That is right for
|
|
13
|
+
* request/response commands, and wrong for LONG-RUNNING ones: `aai dev`
|
|
14
|
+
* writes its single JSON line at startup and then keeps running, so every
|
|
15
|
+
* later message — a failed rebuild, an unhandled rejection, "the dev server
|
|
16
|
+
* is down; save a file to retry" — was silenced for the rest of the process.
|
|
17
|
+
* And JSON mode is AUTO-DETECTED on a pipe, so that is the normal case:
|
|
18
|
+
* `aai dev > dev.log`, a process supervisor, or a container all hid every
|
|
19
|
+
* build failure, leaving the old agent served with nothing to say why edits
|
|
20
|
+
* had stopped taking effect.
|
|
21
|
+
*
|
|
22
|
+
* stderr keeps the stdout contract intact — a script still parses one JSON
|
|
23
|
+
* line — while a human tailing the log sees what happened.
|
|
24
|
+
*/
|
|
25
|
+
export declare function notify(level: "error" | "warn" | "info" | "success", message: string): void;
|
|
7
26
|
/**
|
|
8
27
|
* Unwrap a clack prompt result, exiting cleanly if the user cancelled.
|
|
9
28
|
* `message` lets the caller name what was cancelled (e.g. "Setup cancelled").
|
|
@@ -72,8 +72,15 @@ async function readJson(filePath) {
|
|
|
72
72
|
* torn config.json fails `JSON.parse`, reads back as `{}`, and the next
|
|
73
73
|
* read-modify-write silently wipes fields like `approvedServers`. Rename on
|
|
74
74
|
* the same filesystem is atomic, so readers only ever see a complete file.
|
|
75
|
-
*
|
|
76
|
-
*
|
|
75
|
+
*
|
|
76
|
+
* Atomic per WRITE is not atomic per read-modify-write: two concurrent CLI
|
|
77
|
+
* processes still lose each other's *updates* (last rename wins). That was
|
|
78
|
+
* long documented here as acceptable "for these small user-config files",
|
|
79
|
+
* and it is — for `.aai/project.json`, which is per-directory. It is NOT
|
|
80
|
+
* acceptable for the GLOBAL config, where it dropped the API key a
|
|
81
|
+
* successful `aai login` had just reported saving. Every global-config
|
|
82
|
+
* update therefore goes through `updateGlobalConfig` in `_config.ts`, which
|
|
83
|
+
* holds a cross-process lock around the read and the write.
|
|
77
84
|
*
|
|
78
85
|
* `mode` restricts the file's permissions (the rename carries the temp
|
|
79
86
|
* file's mode to the destination, so an existing world-readable file is
|
package/dist/_utils.d.ts
CHANGED
|
@@ -35,8 +35,15 @@ export declare function readJson(filePath: string): Promise<unknown>;
|
|
|
35
35
|
* torn config.json fails `JSON.parse`, reads back as `{}`, and the next
|
|
36
36
|
* read-modify-write silently wipes fields like `approvedServers`. Rename on
|
|
37
37
|
* the same filesystem is atomic, so readers only ever see a complete file.
|
|
38
|
-
*
|
|
39
|
-
*
|
|
38
|
+
*
|
|
39
|
+
* Atomic per WRITE is not atomic per read-modify-write: two concurrent CLI
|
|
40
|
+
* processes still lose each other's *updates* (last rename wins). That was
|
|
41
|
+
* long documented here as acceptable "for these small user-config files",
|
|
42
|
+
* and it is — for `.aai/project.json`, which is per-directory. It is NOT
|
|
43
|
+
* acceptable for the GLOBAL config, where it dropped the API key a
|
|
44
|
+
* successful `aai login` had just reported saving. Every global-config
|
|
45
|
+
* update therefore goes through `updateGlobalConfig` in `_config.ts`, which
|
|
46
|
+
* holds a cross-process lock around the read and the write.
|
|
40
47
|
*
|
|
41
48
|
* `mode` restricts the file's permissions (the rename carries the temp
|
|
42
49
|
* file's mode to the destination, so an existing world-readable file is
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, t as CliError } from "./_output-CC300DzW.mjs";
|
|
3
|
-
import { n as log } from "./_ui-
|
|
4
|
-
import { r as evalWorkerBundle, t as buildAgentBundle } from "./_bundler-
|
|
5
|
-
import { assertTypechecks } from "./_typecheck-gate-
|
|
6
|
-
import { classifyVitestError, runVitest } from "./test-
|
|
3
|
+
import { n as log } from "./_ui-DfwfDbT-.mjs";
|
|
4
|
+
import { r as evalWorkerBundle, t as buildAgentBundle } from "./_bundler-DC17suWN.mjs";
|
|
5
|
+
import { assertTypechecks } from "./_typecheck-gate-DB-PY0A3.mjs";
|
|
6
|
+
import { classifyVitestError, runVitest } from "./test-C-V98oC-.mjs";
|
|
7
7
|
//#region build.ts
|
|
8
8
|
/**
|
|
9
9
|
* `aai build` — bundle the agent without deploying, behind the same gates
|