@alexkroman1/aai-cli 5.10.1 → 5.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.
- package/dist/{_init-CYLvYU-B.mjs → _init-5hHkHe-V.mjs} +1 -1
- package/dist/{_templates-BWJOiWOO.mjs → _templates-DmdG-voA.mjs} +17 -5
- package/dist/_templates.d.ts +10 -0
- package/dist/cli.mjs +31 -6
- package/dist/eject-DSrAp8PL.mjs +90 -0
- package/dist/eject.d.ts +33 -0
- package/dist/{init-GQsNWkLJ.mjs → init-BBAoRI_f.mjs} +2 -2
- package/dist/scaffold/CLAUDE.md +23 -0
- package/dist/scaffold/package.json +4 -3
- package/dist/scaffold/server.mjs +156 -0
- package/dist/{studio-LNvXtWak.mjs → studio-DPDFKZ9i.mjs} +46 -12
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { c as readJson, d as writeJson, s as isEexist } from "./_utils-8KKw-bzi.mjs";
|
|
3
3
|
import { r as isDevMode, t as getMonorepoRoot } from "./_agent-DS2PUJcl.mjs";
|
|
4
|
-
import { REPO_URL, downloadAndMergeTemplate } from "./_templates-
|
|
4
|
+
import { REPO_URL, downloadAndMergeTemplate } from "./_templates-DmdG-voA.mjs";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
7
|
//#region _init.ts
|
|
@@ -119,6 +119,18 @@ async function readJsonFile(file) {
|
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
/**
|
|
122
|
+
* Directory holding the base scaffold — the files every project gets
|
|
123
|
+
* underneath its template (package.json, tsconfig, `server.mjs`, …).
|
|
124
|
+
*
|
|
125
|
+
* Exported so `aai eject` can copy one file out of it rather than carrying a
|
|
126
|
+
* second copy of that file's contents: the scaffold is the single definition
|
|
127
|
+
* of the self-hosted entrypoint, and a project retrofitted with `eject` gets
|
|
128
|
+
* byte-identical output to one `aai init` scaffolded.
|
|
129
|
+
*/
|
|
130
|
+
function scaffoldDir() {
|
|
131
|
+
return path.join(resolveTemplatesDir(), "scaffold");
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
122
134
|
* Layer the base scaffold (package.json, tsconfig, …) into targetDir
|
|
123
135
|
* WITHOUT overwriting anything already there. Shared by `aai init`
|
|
124
136
|
* (underneath a template) and `aai pull` (underneath the studio workspace
|
|
@@ -130,14 +142,14 @@ async function readJsonFile(file) {
|
|
|
130
142
|
* {@link layerScaffoldManifest}.
|
|
131
143
|
*/
|
|
132
144
|
async function layerScaffold(targetDir) {
|
|
133
|
-
const
|
|
134
|
-
if (!existsSync(
|
|
135
|
-
await fs.cp(
|
|
145
|
+
const dir = scaffoldDir();
|
|
146
|
+
if (!existsSync(dir)) return;
|
|
147
|
+
await fs.cp(dir, targetDir, {
|
|
136
148
|
recursive: true,
|
|
137
149
|
force: false,
|
|
138
150
|
errorOnExist: false
|
|
139
151
|
});
|
|
140
|
-
await layerScaffoldManifest(
|
|
152
|
+
await layerScaffoldManifest(dir, targetDir);
|
|
141
153
|
}
|
|
142
154
|
/**
|
|
143
155
|
* Copy a template into targetDir, merging scaffold files underneath.
|
|
@@ -154,4 +166,4 @@ async function downloadAndMergeTemplate(template, targetDir) {
|
|
|
154
166
|
await layerScaffold(targetDir);
|
|
155
167
|
}
|
|
156
168
|
//#endregion
|
|
157
|
-
export { REPO_URL, downloadAndMergeTemplate, layerScaffold, listTemplates };
|
|
169
|
+
export { REPO_URL, downloadAndMergeTemplate, layerScaffold, listTemplates, scaffoldDir };
|
package/dist/_templates.d.ts
CHANGED
|
@@ -35,6 +35,16 @@ type Manifest = Record<string, unknown>;
|
|
|
35
35
|
* Returns null when nothing was missing, so the common case writes no file.
|
|
36
36
|
*/
|
|
37
37
|
export declare function mergeScaffoldManifest(existing: Manifest, scaffold: Manifest): Manifest | null;
|
|
38
|
+
/**
|
|
39
|
+
* Directory holding the base scaffold — the files every project gets
|
|
40
|
+
* underneath its template (package.json, tsconfig, `server.mjs`, …).
|
|
41
|
+
*
|
|
42
|
+
* Exported so `aai eject` can copy one file out of it rather than carrying a
|
|
43
|
+
* second copy of that file's contents: the scaffold is the single definition
|
|
44
|
+
* of the self-hosted entrypoint, and a project retrofitted with `eject` gets
|
|
45
|
+
* byte-identical output to one `aai init` scaffolded.
|
|
46
|
+
*/
|
|
47
|
+
export declare function scaffoldDir(): string;
|
|
38
48
|
/**
|
|
39
49
|
* Layer the base scaffold (package.json, tsconfig, …) into targetDir
|
|
40
50
|
* WITHOUT overwriting anything already there. Shared by `aai init`
|
package/dist/cli.mjs
CHANGED
|
@@ -166,7 +166,7 @@ const list = defineCommand({
|
|
|
166
166
|
async run({ args }) {
|
|
167
167
|
await runCommand(args, async () => {
|
|
168
168
|
const cwd = resolveCwd();
|
|
169
|
-
const { executeList } = await import("./studio-
|
|
169
|
+
const { executeList } = await import("./studio-DPDFKZ9i.mjs");
|
|
170
170
|
return executeList({
|
|
171
171
|
cwd,
|
|
172
172
|
server: args.server
|
|
@@ -201,7 +201,7 @@ const pull = defineCommand({
|
|
|
201
201
|
async run({ args }) {
|
|
202
202
|
await runCommand(args, async () => {
|
|
203
203
|
const cwd = resolveCwd();
|
|
204
|
-
const { executePull } = await import("./studio-
|
|
204
|
+
const { executePull } = await import("./studio-DPDFKZ9i.mjs");
|
|
205
205
|
return executePull({
|
|
206
206
|
cwd,
|
|
207
207
|
project: args.project,
|
|
@@ -229,7 +229,7 @@ const push = defineCommand({
|
|
|
229
229
|
async run({ args }) {
|
|
230
230
|
await runCommand(args, async () => {
|
|
231
231
|
const cwd = await setup({ agent: true });
|
|
232
|
-
const { executePush } = await import("./studio-
|
|
232
|
+
const { executePush } = await import("./studio-DPDFKZ9i.mjs");
|
|
233
233
|
return executePush({
|
|
234
234
|
cwd,
|
|
235
235
|
server: args.server,
|
|
@@ -259,7 +259,7 @@ const publish = defineCommand({
|
|
|
259
259
|
async run({ args }) {
|
|
260
260
|
await runCommand(args, async () => {
|
|
261
261
|
const cwd = await setup({ agent: true });
|
|
262
|
-
const { executePublish } = await import("./studio-
|
|
262
|
+
const { executePublish } = await import("./studio-DPDFKZ9i.mjs");
|
|
263
263
|
return executePublish({
|
|
264
264
|
cwd,
|
|
265
265
|
server: args.server,
|
|
@@ -317,7 +317,7 @@ const init = defineCommand({
|
|
|
317
317
|
},
|
|
318
318
|
async run({ args }) {
|
|
319
319
|
await runCommand(args, async (mode) => {
|
|
320
|
-
const { executeInit } = await import("./init-
|
|
320
|
+
const { executeInit } = await import("./init-BBAoRI_f.mjs");
|
|
321
321
|
return executeInit({
|
|
322
322
|
dir: args.dir,
|
|
323
323
|
force: args.force,
|
|
@@ -396,6 +396,30 @@ const build = defineCommand({
|
|
|
396
396
|
});
|
|
397
397
|
}
|
|
398
398
|
});
|
|
399
|
+
const eject = defineCommand({
|
|
400
|
+
meta: {
|
|
401
|
+
name: "eject",
|
|
402
|
+
description: "Add the self-hosted server.mjs entrypoint to an older project"
|
|
403
|
+
},
|
|
404
|
+
args: {
|
|
405
|
+
force: {
|
|
406
|
+
type: "boolean",
|
|
407
|
+
alias: "f",
|
|
408
|
+
description: "Replace an existing server.mjs"
|
|
409
|
+
},
|
|
410
|
+
json: sharedArgs.json
|
|
411
|
+
},
|
|
412
|
+
async run({ args }) {
|
|
413
|
+
await runCommand(args, async () => {
|
|
414
|
+
const cwd = await setup({ agent: true });
|
|
415
|
+
const { executeEject } = await import("./eject-DSrAp8PL.mjs");
|
|
416
|
+
return executeEject({
|
|
417
|
+
cwd,
|
|
418
|
+
force: args.force
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
});
|
|
399
423
|
const deploy = defineCommand({
|
|
400
424
|
meta: {
|
|
401
425
|
name: "deploy",
|
|
@@ -624,7 +648,7 @@ const templates = defineCommand({
|
|
|
624
648
|
args: { json: sharedArgs.json },
|
|
625
649
|
async run({ args }) {
|
|
626
650
|
await runCommand(args, async (mode) => {
|
|
627
|
-
const { listTemplates } = await import("./_templates-
|
|
651
|
+
const { listTemplates } = await import("./_templates-DmdG-voA.mjs");
|
|
628
652
|
const names = await listTemplates();
|
|
629
653
|
if (mode === "human") {
|
|
630
654
|
for (const name of names) log.message(name);
|
|
@@ -648,6 +672,7 @@ const mainCommand = defineCommand({
|
|
|
648
672
|
dev,
|
|
649
673
|
test,
|
|
650
674
|
build,
|
|
675
|
+
eject,
|
|
651
676
|
list,
|
|
652
677
|
pull,
|
|
653
678
|
push,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as ok, t as CliError } from "./_output-CC300DzW.mjs";
|
|
3
|
+
import { n as log } from "./_ui-DfwfDbT-.mjs";
|
|
4
|
+
import { a as errorMessage, c as readJson, d as writeJson, o as fileExists } from "./_utils-8KKw-bzi.mjs";
|
|
5
|
+
import { scaffoldDir } from "./_templates-DmdG-voA.mjs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
8
|
+
//#region eject.ts
|
|
9
|
+
/**
|
|
10
|
+
* `aai eject` — retrofit the self-hosted entrypoint into an existing project.
|
|
11
|
+
*
|
|
12
|
+
* Every project scaffolded from this CLI version onwards already has
|
|
13
|
+
* `server.mjs` and an `npm start` script: the scaffold ships them, so
|
|
14
|
+
* self-hosting is the default rather than something to opt into. This command
|
|
15
|
+
* exists for the projects that predate that — `aai init` before it, or a
|
|
16
|
+
* workspace pulled from a studio that was created earlier — where the files
|
|
17
|
+
* are simply missing.
|
|
18
|
+
*
|
|
19
|
+
* It COPIES from the scaffold rather than writing its own contents. Two
|
|
20
|
+
* definitions of "the self-hosted entrypoint" would drift, and the one nobody
|
|
21
|
+
* runs locally is the one that would rot; this way an ejected project is
|
|
22
|
+
* byte-identical to a freshly scaffolded one.
|
|
23
|
+
*/
|
|
24
|
+
/** The file `npm start` runs. Named once — the scaffold ships it under this name. */
|
|
25
|
+
const SERVER_ENTRY = "server.mjs";
|
|
26
|
+
/** The `scripts.start` value that runs {@link SERVER_ENTRY}. */
|
|
27
|
+
const START_SCRIPT = `node ${SERVER_ENTRY}`;
|
|
28
|
+
/**
|
|
29
|
+
* Add `scripts.start` unless the project already declares one.
|
|
30
|
+
*
|
|
31
|
+
* An existing `start` is left alone even under `--force`: `--force` is about
|
|
32
|
+
* replacing the entrypoint file, and silently rewriting the command a project
|
|
33
|
+
* boots with is a different, larger act. The mismatch is reported instead, so
|
|
34
|
+
* the choice stays the author's.
|
|
35
|
+
*/
|
|
36
|
+
async function ensureStartScript(cwd) {
|
|
37
|
+
const manifestPath = path.join(cwd, "package.json");
|
|
38
|
+
const manifest = await readJson(manifestPath);
|
|
39
|
+
if (!manifest) {
|
|
40
|
+
log.warn(`No package.json here — add "start": "${START_SCRIPT}" yourself to get \`npm start\`.`);
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const existing = manifest.scripts?.start;
|
|
44
|
+
if (existing === START_SCRIPT) return false;
|
|
45
|
+
if (existing !== void 0) {
|
|
46
|
+
log.warn(`Kept your existing "start" script (${existing}) — run \`node ${SERVER_ENTRY}\`.`);
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
manifest.scripts = {
|
|
50
|
+
...manifest.scripts,
|
|
51
|
+
start: START_SCRIPT
|
|
52
|
+
};
|
|
53
|
+
await writeJson(manifestPath, manifest);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* A missing scaffold source means a broken install (or an `AAI_TEMPLATES_DIR`
|
|
58
|
+
* pointed somewhere wrong), not anything the user did — so the message names
|
|
59
|
+
* the file and where it was looked for.
|
|
60
|
+
*
|
|
61
|
+
* Built here and thrown by the caller, the shape `build.ts` uses for the same
|
|
62
|
+
* reason: `useErrorCause` reads a `throw new Error` inside a `catch` as
|
|
63
|
+
* dropping the cause, and cannot see that `CliError` takes its options fourth.
|
|
64
|
+
*/
|
|
65
|
+
function scaffoldMissingError(source, err) {
|
|
66
|
+
return new CliError("scaffold_missing", `Could not read the scaffold's ${SERVER_ENTRY} at ${source}: ${errorMessage(err)}`, "Reinstall @alexkroman1/aai-cli.", { cause: err });
|
|
67
|
+
}
|
|
68
|
+
async function executeEject(opts) {
|
|
69
|
+
const { cwd, force } = opts;
|
|
70
|
+
const target = path.join(cwd, SERVER_ENTRY);
|
|
71
|
+
const overwritten = await fileExists(target);
|
|
72
|
+
if (overwritten && !force) throw new CliError("server_exists", `${SERVER_ENTRY} already exists — this project can already be self-hosted.`, `Run \`npm start\`, or re-run with --force to replace it with the current scaffold's copy.`);
|
|
73
|
+
const source = path.join(scaffoldDir(), SERVER_ENTRY);
|
|
74
|
+
try {
|
|
75
|
+
await fs.copyFile(source, target);
|
|
76
|
+
} catch (err) {
|
|
77
|
+
throw scaffoldMissingError(source, err);
|
|
78
|
+
}
|
|
79
|
+
const addedStartScript = await ensureStartScript(cwd);
|
|
80
|
+
log.success(`Wrote ${SERVER_ENTRY}${overwritten ? " (replaced)" : ""}`);
|
|
81
|
+
if (await fileExists(path.join(cwd, "client.tsx"))) log.info("This project has a custom UI: run `aai build` first so it is served.");
|
|
82
|
+
log.info("Next: npm start (PORT and HOST override the 127.0.0.1:3000 default)");
|
|
83
|
+
return ok({
|
|
84
|
+
file: target,
|
|
85
|
+
overwritten,
|
|
86
|
+
addedStartScript
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//#endregion
|
|
90
|
+
export { executeEject };
|
package/dist/eject.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `aai eject` — retrofit the self-hosted entrypoint into an existing project.
|
|
3
|
+
*
|
|
4
|
+
* Every project scaffolded from this CLI version onwards already has
|
|
5
|
+
* `server.mjs` and an `npm start` script: the scaffold ships them, so
|
|
6
|
+
* self-hosting is the default rather than something to opt into. This command
|
|
7
|
+
* exists for the projects that predate that — `aai init` before it, or a
|
|
8
|
+
* workspace pulled from a studio that was created earlier — where the files
|
|
9
|
+
* are simply missing.
|
|
10
|
+
*
|
|
11
|
+
* It COPIES from the scaffold rather than writing its own contents. Two
|
|
12
|
+
* definitions of "the self-hosted entrypoint" would drift, and the one nobody
|
|
13
|
+
* runs locally is the one that would rot; this way an ejected project is
|
|
14
|
+
* byte-identical to a freshly scaffolded one.
|
|
15
|
+
*/
|
|
16
|
+
import { type CommandResult } from "./_output.ts";
|
|
17
|
+
/** The file `npm start` runs. Named once — the scaffold ships it under this name. */
|
|
18
|
+
export declare const SERVER_ENTRY = "server.mjs";
|
|
19
|
+
/** The `scripts.start` value that runs {@link SERVER_ENTRY}. */
|
|
20
|
+
export declare const START_SCRIPT = "node server.mjs";
|
|
21
|
+
type EjectData = {
|
|
22
|
+
/** Absolute path of the entrypoint written. */
|
|
23
|
+
file: string;
|
|
24
|
+
/** True when an existing `server.mjs` was replaced (`--force`). */
|
|
25
|
+
overwritten: boolean;
|
|
26
|
+
/** True when `scripts.start` was added to package.json by this run. */
|
|
27
|
+
addedStartScript: boolean;
|
|
28
|
+
};
|
|
29
|
+
export declare function executeEject(opts: {
|
|
30
|
+
cwd: string;
|
|
31
|
+
force?: boolean | undefined;
|
|
32
|
+
}): Promise<CommandResult<EjectData>>;
|
|
33
|
+
export {};
|
|
@@ -75,7 +75,7 @@ function resolveTargetDir(dir) {
|
|
|
75
75
|
}
|
|
76
76
|
/** Publish after init and return deploy metadata if successful. */
|
|
77
77
|
async function tryPublish(cwd, server) {
|
|
78
|
-
const { executePublish } = await import("./studio-
|
|
78
|
+
const { executePublish } = await import("./studio-DPDFKZ9i.mjs");
|
|
79
79
|
try {
|
|
80
80
|
const result = await executePublish({
|
|
81
81
|
cwd,
|
|
@@ -94,7 +94,7 @@ async function tryPublish(cwd, server) {
|
|
|
94
94
|
}
|
|
95
95
|
/** Scaffold the project, optionally showing a spinner. */
|
|
96
96
|
async function scaffoldProject(dir, cwd, template, silent) {
|
|
97
|
-
const { runInit } = await import("./_init-
|
|
97
|
+
const { runInit } = await import("./_init-5hHkHe-V.mjs");
|
|
98
98
|
const s = silent ? void 0 : p.spinner();
|
|
99
99
|
s?.start(`Creating ${dir}`);
|
|
100
100
|
await runInit({
|
package/dist/scaffold/CLAUDE.md
CHANGED
|
@@ -54,6 +54,29 @@ The scaffold's `package.json` exposes `dev`, `build`, `test`, and `deploy`
|
|
|
54
54
|
as `pnpm <name>` shortcuts. Other commands (`init`, `templates`, `delete`, `secret`)
|
|
55
55
|
are CLI-only.
|
|
56
56
|
|
|
57
|
+
## Running it yourself (`npm start`)
|
|
58
|
+
|
|
59
|
+
`server.mjs` serves this agent from a plain Node process — no CLI, no
|
|
60
|
+
bundler, no platform account. It is the deployment counterpart of `aai dev`:
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npm start # http://127.0.0.1:3000
|
|
64
|
+
PORT=8080 HOST=0.0.0.0 npm start # bind every interface, e.g. in a container
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Secrets work the same as everywhere else: `ctx.env` holds the keys declared
|
|
68
|
+
in `.env` (or `.env.example`), and a real environment variable of that name
|
|
69
|
+
wins — so `docker run -e MY_API_KEY=…` needs no `.env` in the image.
|
|
70
|
+
|
|
71
|
+
Two things to know. It binds **loopback by default**, because this server has
|
|
72
|
+
no request authentication of its own; set `HOST=0.0.0.0` only behind your own
|
|
73
|
+
proxy or auth. And with a custom `client.tsx`, run `npm run build` first —
|
|
74
|
+
otherwise it serves the default UI and says so at startup.
|
|
75
|
+
|
|
76
|
+
Deleting `server.mjs` costs nothing: `aai dev`, `aai publish` and the managed
|
|
77
|
+
platform never read it. `run_code` is the one feature that does not follow —
|
|
78
|
+
it needs the platform's sandbox and refuses outside one.
|
|
79
|
+
|
|
57
80
|
## Project structure
|
|
58
81
|
|
|
59
82
|
```text
|
|
@@ -6,20 +6,21 @@
|
|
|
6
6
|
},
|
|
7
7
|
"scripts": {
|
|
8
8
|
"dev": "aai dev",
|
|
9
|
+
"start": "node server.mjs",
|
|
9
10
|
"build": "aai build",
|
|
10
11
|
"test": "aai test",
|
|
11
12
|
"publish:agent": "aai publish"
|
|
12
13
|
},
|
|
13
14
|
"dependencies": {
|
|
14
|
-
"@alexkroman1/aai": "^5.
|
|
15
|
-
"@alexkroman1/aai-ui": "^5.
|
|
15
|
+
"@alexkroman1/aai": "^5.11.0",
|
|
16
|
+
"@alexkroman1/aai-ui": "^5.11.0",
|
|
16
17
|
"react": "^19.2.8",
|
|
17
18
|
"react-dom": "^19.2.8",
|
|
18
19
|
"tailwindcss": "^4.0.0",
|
|
19
20
|
"zod": "^4.4.3"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"@alexkroman1/aai-cli": "^5.
|
|
23
|
+
"@alexkroman1/aai-cli": "^5.11.0",
|
|
23
24
|
"@tailwindcss/vite": "^4.3.3",
|
|
24
25
|
"@types/node": "^26.1.1",
|
|
25
26
|
"@types/react": "^19.2.17",
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Self-hosted entrypoint — this is what `npm start` runs.
|
|
2
|
+
//
|
|
3
|
+
// It serves the agent over HTTP + WebSocket from your own Node process: no
|
|
4
|
+
// platform account, no CLI at run time, no bundler. `aai dev` is the
|
|
5
|
+
// development counterpart (file watching, Vite, a browser that opens itself);
|
|
6
|
+
// this file is the deployment.
|
|
7
|
+
//
|
|
8
|
+
// npm start # http://127.0.0.1:3000
|
|
9
|
+
// PORT=8080 HOST=0.0.0.0 npm start # bind every interface, e.g. in a container
|
|
10
|
+
//
|
|
11
|
+
// Anything that can run Node can host it: copy the project, install
|
|
12
|
+
// dependencies, provide the secrets, run this file. Deleting it costs nothing
|
|
13
|
+
// — `aai dev`, `aai publish` and the managed platform never read it.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
16
|
+
import { readFile } from "node:fs/promises";
|
|
17
|
+
import { registerHooks } from "node:module";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { parseEnv } from "node:util";
|
|
21
|
+
import { createAgentServer, withHostCredentialFallback } from "@alexkroman1/aai/runtime";
|
|
22
|
+
import { defaultClientDir } from "@alexkroman1/aai-ui/client-dir";
|
|
23
|
+
|
|
24
|
+
const root = path.dirname(fileURLToPath(import.meta.url));
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Teach Node the two non-JavaScript import shapes the `aai` bundler supports,
|
|
28
|
+
* so one `agent.ts` runs unchanged under `aai dev`, `aai publish`, and here.
|
|
29
|
+
*
|
|
30
|
+
* - `import prompt from "./system-prompt.md?raw"` — a Vite convention; Node
|
|
31
|
+
* would look for a file literally named `system-prompt.md?raw`.
|
|
32
|
+
* - `import data from "./data.json"` with no import attribute — TypeScript's
|
|
33
|
+
* `resolveJsonModule` allows it; Node requires `with { type: "json" }` and
|
|
34
|
+
* otherwise fails with ERR_IMPORT_ATTRIBUTE_MISSING. An import that DOES
|
|
35
|
+
* carry the attribute is left to Node, whose own handling is correct.
|
|
36
|
+
*
|
|
37
|
+
* Nothing is transformed beyond that: `.ts` itself needs no help, because Node
|
|
38
|
+
* strips the types natively (this project needs Node 24+). That is why there
|
|
39
|
+
* is no build step here, and no second copy of the agent in JavaScript that
|
|
40
|
+
* could drift from the one you deploy.
|
|
41
|
+
*/
|
|
42
|
+
registerHooks({
|
|
43
|
+
resolve(specifier, context, nextResolve) {
|
|
44
|
+
if (specifier.startsWith(".") && specifier.endsWith("?raw")) {
|
|
45
|
+
// Resolved by hand rather than through nextResolve: the default
|
|
46
|
+
// resolver has no format for `.md` and the query would be lost anyway.
|
|
47
|
+
return {
|
|
48
|
+
url: new URL(specifier, context.parentURL).href,
|
|
49
|
+
format: "module",
|
|
50
|
+
shortCircuit: true,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
return nextResolve(specifier, context);
|
|
54
|
+
},
|
|
55
|
+
load(url, context, nextLoad) {
|
|
56
|
+
const asRaw = url.endsWith("?raw");
|
|
57
|
+
const asBareJson = url.endsWith(".json") && context.importAttributes?.type !== "json";
|
|
58
|
+
if (!(asRaw || asBareJson)) return nextLoad(url, context);
|
|
59
|
+
const text = readFileSync(fileURLToPath(asRaw ? url.slice(0, -"?raw".length) : url), "utf-8");
|
|
60
|
+
return {
|
|
61
|
+
format: "module",
|
|
62
|
+
// `export default <literal>` for both: a JSON document is already a
|
|
63
|
+
// valid JS expression, and JSON.stringify makes any file safe to embed
|
|
64
|
+
// as a string. Emitting `format: "json"` instead would put the import
|
|
65
|
+
// back under the attribute check this exists to satisfy.
|
|
66
|
+
source: `export default ${asRaw ? JSON.stringify(text) : text};`,
|
|
67
|
+
shortCircuit: true,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Imported dynamically, and this is load-bearing: static `import` statements
|
|
73
|
+
// are hoisted and evaluated BEFORE any statement in this file, so an
|
|
74
|
+
// `import agent from "./agent.ts"` at the top would load the agent — and every
|
|
75
|
+
// `?raw` import inside it — before the hooks above were ever registered.
|
|
76
|
+
const { default: agent } = await import("./agent.ts");
|
|
77
|
+
|
|
78
|
+
/** Parse a dotenv-syntax file into a record; `{}` when it does not exist. */
|
|
79
|
+
async function readEnvFile(file) {
|
|
80
|
+
try {
|
|
81
|
+
return parseEnv(await readFile(path.join(root, file), "utf-8"));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
// Absent is normal — `.env` is gitignored, and a container usually has
|
|
84
|
+
// neither file. Unreadable is not: the agent would boot with no
|
|
85
|
+
// credentials and fail later as an opaque provider auth error.
|
|
86
|
+
if (err.code !== "ENOENT") throw err;
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build `ctx.env` — what this agent's own tool code reads.
|
|
93
|
+
*
|
|
94
|
+
* The rule is the one `aai dev` follows: only DECLARED keys are exposed, and a
|
|
95
|
+
* real environment variable wins over the file's value. Nothing else from
|
|
96
|
+
* process.env comes along, so the agent cannot come to depend on a variable
|
|
97
|
+
* (PATH, HOME, …) that will not exist wherever you deploy it.
|
|
98
|
+
*
|
|
99
|
+
* `.env.example` counts as a declaration too, which is what lets a container
|
|
100
|
+
* run with no `.env` at all: the committed file names the secrets the agent
|
|
101
|
+
* needs, and `docker run -e MY_API_KEY=…` supplies the values. Declare
|
|
102
|
+
* `DATABASE_URL` the same way to give your tools `ctx.db`.
|
|
103
|
+
*/
|
|
104
|
+
async function resolveAgentEnv() {
|
|
105
|
+
const declared = { ...(await readEnvFile(".env.example")), ...(await readEnvFile(".env")) };
|
|
106
|
+
const env = {};
|
|
107
|
+
for (const [key, fileValue] of Object.entries(declared)) {
|
|
108
|
+
const value = process.env[key] ?? fileValue;
|
|
109
|
+
// An empty value is worse than a missing one: a provider would try to
|
|
110
|
+
// authenticate with "" rather than report the credential as absent. The
|
|
111
|
+
// example file is full of them by design (`BRAVE_API_KEY=`).
|
|
112
|
+
if (value !== "") env[key] = value;
|
|
113
|
+
}
|
|
114
|
+
return env;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Static assets served at `/`: this project's own UI once `client.tsx` has been
|
|
119
|
+
* built (`npm run build` leaves it in `.aai/client`), otherwise the prebuilt
|
|
120
|
+
* default client that ships inside @alexkroman1/aai-ui.
|
|
121
|
+
*/
|
|
122
|
+
function resolveClientDir() {
|
|
123
|
+
const built = path.join(root, ".aai", "client");
|
|
124
|
+
if (existsSync(path.join(built, "index.html"))) return built;
|
|
125
|
+
if (existsSync(path.join(root, "client.tsx"))) {
|
|
126
|
+
console.warn("client.tsx is not built — serving the default UI. Run `npm run build` first.");
|
|
127
|
+
}
|
|
128
|
+
return defaultClientDir();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const env = await resolveAgentEnv();
|
|
132
|
+
|
|
133
|
+
const server = createAgentServer({
|
|
134
|
+
agent,
|
|
135
|
+
env,
|
|
136
|
+
// Provider credentials may ALSO arrive straight from the environment without
|
|
137
|
+
// being declared, and without becoming ctx.env — the ordinary way to hand
|
|
138
|
+
// ASSEMBLYAI_API_KEY to a container. Anything in `env` still wins.
|
|
139
|
+
providerEnv: withHostCredentialFallback(env),
|
|
140
|
+
clientDir: resolveClientDir(),
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Loopback by default: this server has no request authentication of its own,
|
|
144
|
+
// so exposing it is a deliberate act. Set HOST=0.0.0.0 to bind every interface
|
|
145
|
+
// behind your own proxy or auth. An empty HOST means unset, not "everything".
|
|
146
|
+
const host = process.env.HOST?.trim() || undefined;
|
|
147
|
+
await server.listen(Number(process.env.PORT ?? 3000), host);
|
|
148
|
+
console.log(`${agent.name} listening on http://${host ?? "127.0.0.1"}:${server.port}`);
|
|
149
|
+
|
|
150
|
+
for (const signal of ["SIGINT", "SIGTERM"]) {
|
|
151
|
+
process.once(signal, async () => {
|
|
152
|
+
// close() shuts the runtime down too — no separate runtime.shutdown().
|
|
153
|
+
await server.close();
|
|
154
|
+
process.exit(0);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
@@ -4,7 +4,7 @@ import { n as log, t as fmtUrl } from "./_ui-DfwfDbT-.mjs";
|
|
|
4
4
|
import { s as updateProjectConfig } from "./_config-DMyolIk9.mjs";
|
|
5
5
|
import { t as resolveServerEnv } from "./_server-common-DX8Bfrf5.mjs";
|
|
6
6
|
import { i as resolveDeployTarget } from "./_agent-DS2PUJcl.mjs";
|
|
7
|
-
import { layerScaffold } from "./_templates-
|
|
7
|
+
import { layerScaffold } from "./_templates-DmdG-voA.mjs";
|
|
8
8
|
import { n as apiRequest } from "./_api-client-B-upMGkc.mjs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
@@ -93,19 +93,30 @@ async function collectSourceFiles(dir) {
|
|
|
93
93
|
const files = {};
|
|
94
94
|
const warnings = [];
|
|
95
95
|
if (paths.length > 100) warnings.push(`Project has ${paths.length} files; only the first 100 sync to the studio.`);
|
|
96
|
-
|
|
96
|
+
const read = await Promise.all(paths.slice(0, 100).map(async (rel) => {
|
|
97
97
|
const abs = path.join(dir, rel);
|
|
98
98
|
const st = await stat(abs);
|
|
99
|
-
if (st.size > 256e3) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
99
|
+
if (st.size > 256e3) return {
|
|
100
|
+
ok: false,
|
|
101
|
+
warning: `${rel} is ${st.size} bytes (max ${MAX_STUDIO_FILE_BYTES}) — not synced.`
|
|
102
|
+
};
|
|
103
103
|
const content = decodeUtf8(await readFile(abs));
|
|
104
|
-
if (content === null) {
|
|
105
|
-
|
|
104
|
+
if (content === null) return {
|
|
105
|
+
ok: false,
|
|
106
|
+
warning: `${rel} is not valid UTF-8 (binary file?) — not synced.`
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
rel,
|
|
111
|
+
content
|
|
112
|
+
};
|
|
113
|
+
}));
|
|
114
|
+
for (const entry of read) {
|
|
115
|
+
if (!entry.ok) {
|
|
116
|
+
warnings.push(entry.warning);
|
|
106
117
|
continue;
|
|
107
118
|
}
|
|
108
|
-
files[rel.split(path.sep).join("/")] = content;
|
|
119
|
+
files[entry.rel.split(path.sep).join("/")] = entry.content;
|
|
109
120
|
}
|
|
110
121
|
return {
|
|
111
122
|
files,
|
|
@@ -188,17 +199,40 @@ async function executeList(opts) {
|
|
|
188
199
|
}
|
|
189
200
|
/** Write a pulled file map under `dir`, refusing paths that escape it. */
|
|
190
201
|
async function materializeFiles(dir, files) {
|
|
191
|
-
|
|
202
|
+
const targets = Object.entries(files).map(([rel, content]) => {
|
|
192
203
|
const abs = path.resolve(dir, rel);
|
|
193
204
|
if (abs !== dir && !abs.startsWith(dir + path.sep)) throw new Error(`Pulled file path escapes the project directory: ${rel}`);
|
|
205
|
+
return {
|
|
206
|
+
abs,
|
|
207
|
+
content
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
await Promise.all(targets.map(async ({ abs, content }) => {
|
|
194
211
|
await mkdir(path.dirname(abs), { recursive: true });
|
|
195
212
|
await writeFile(abs, content, "utf-8");
|
|
196
|
-
}
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The hint for a pull that found nothing, which is where the two causes have
|
|
217
|
+
* to be told apart — and only the project LIST can do it. A typo has other
|
|
218
|
+
* projects beside it; an empty list means this login sees no projects at all,
|
|
219
|
+
* i.e. the CLI is authenticated as a different account than the browser the
|
|
220
|
+
* project was created in (the account's key is what decides studio scope —
|
|
221
|
+
* see `resolveBearer` server-side). Naming the visible projects is also the
|
|
222
|
+
* answer to a typo, so the round trip pays for itself either way. Best
|
|
223
|
+
* effort: the list is a second request on an already-failing path, and its
|
|
224
|
+
* own failure must not replace the 404 the user needs to see.
|
|
225
|
+
*/
|
|
226
|
+
async function notFoundHint(serverUrl, apiKey) {
|
|
227
|
+
const projects = await listStudioProjects(serverUrl, apiKey).catch(() => null);
|
|
228
|
+
if (projects === null) return "Run `aai list` to see your projects.";
|
|
229
|
+
if (projects.length === 0) return "This login has no studio projects at all. If yours are in the studio, the CLI is linked to a different account — run `aai login` again, approve it in a browser signed in to the account that owns the project, then `aai list`.";
|
|
230
|
+
return `Your projects: ${projects.slice(0, 10).join(", ")}${projects.length > 10 ? `, and ${projects.length - 10} more` : ""}.`;
|
|
197
231
|
}
|
|
198
232
|
async function executePull(opts) {
|
|
199
233
|
const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
|
|
200
234
|
const remote = await fetchStudioProject(serverUrl, apiKey, opts.project);
|
|
201
|
-
if (!remote) throw new CliError("not_found", `No studio project named "${opts.project}".`,
|
|
235
|
+
if (!remote) throw new CliError("not_found", `No studio project named "${opts.project}".`, await notFoundHint(serverUrl, apiKey));
|
|
202
236
|
const target = path.resolve(opts.cwd, opts.dir ?? opts.project);
|
|
203
237
|
if ((await readdir(target).catch(() => [])).length > 0 && !opts.force) throw new CliError("dir_not_empty", `${target} is not empty.`, "Pull into a fresh directory, or pass --force to overwrite files in place.");
|
|
204
238
|
await materializeFiles(target, remote.files);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aai": "bin.mjs"
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"p-timeout": "^7.0.1",
|
|
39
39
|
"vite": "^8.1.5",
|
|
40
40
|
"zod": "^4.4.3",
|
|
41
|
-
"@alexkroman1/aai": "5.
|
|
42
|
-
"@alexkroman1/aai-ui": "5.
|
|
41
|
+
"@alexkroman1/aai": "5.11.0",
|
|
42
|
+
"@alexkroman1/aai-ui": "5.11.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"playwright": "^1.61.1",
|