@alexkroman1/aai-cli 5.10.1 → 5.12.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-DS2PUJcl.mjs → _agent-JGaAJ5j6.mjs} +3 -2
- package/dist/_agent.d.ts +1 -0
- package/dist/{_bundler-DC17suWN.mjs → _bundler-BvfPzf8J.mjs} +28 -9
- package/dist/_bundler.d.ts +16 -2
- package/dist/{_config-DMyolIk9.mjs → _config-riTmrwxX.mjs} +2 -2
- package/dist/_deploy.d.ts +0 -10
- package/dist/_dev-restart.d.ts +59 -0
- package/dist/{_dev-server-B3ivqyEd.mjs → _dev-server-BgXXSijx.mjs} +178 -89
- package/dist/_dev-server.d.ts +17 -0
- package/dist/{_init-CYLvYU-B.mjs → _init-Ca5Eg5pn.mjs} +2 -2
- package/dist/_preflight-BtfaYtbE.mjs +46 -0
- package/dist/_preflight.d.ts +57 -0
- package/dist/_slug-api-DXm7s9X5.mjs +50 -0
- package/dist/_slug-api.d.ts +16 -0
- package/dist/_studio-CXoY_Hsg.mjs +94 -0
- package/dist/_studio.d.ts +19 -16
- package/dist/{_templates-BWJOiWOO.mjs → _templates-CKYDgGyY.mjs} +18 -6
- package/dist/_templates.d.ts +10 -0
- package/dist/{_typecheck-gate-DB-PY0A3.mjs → _typecheck-gate-4v9UBZHh.mjs} +1 -1
- package/dist/{_ui-DfwfDbT-.mjs → _ui-u7T4YooX.mjs} +5 -1
- package/dist/_ui.d.ts +2 -0
- package/dist/{build-CACFbdQ4.mjs → build-BTOyN3H5.mjs} +4 -4
- package/dist/cli.d.ts +2 -1
- package/dist/cli.mjs +93 -32
- package/dist/{delete-DEZ7u3u4.mjs → delete-D3gZdSs-.mjs} +3 -3
- package/dist/{deploy-Ch0d_jje.mjs → deploy-Df6e7YFn.mjs} +21 -21
- package/dist/deploy.d.ts +0 -2
- package/dist/{dev-CdeRcYiQ.mjs → dev-DqIUtFfS.mjs} +2 -2
- package/dist/eject-DSldO60O.mjs +90 -0
- package/dist/eject.d.ts +33 -0
- package/dist/{init-GQsNWkLJ.mjs → init-B-04lRSV.mjs} +4 -4
- package/dist/{login-BeFUiU6M.mjs → login-CtimAsZk.mjs} +4 -15
- package/dist/login.d.ts +0 -9
- package/dist/project-config.d.ts +23 -0
- package/dist/project-config.mjs +3 -0
- package/dist/scaffold/CLAUDE.md +23 -0
- package/dist/scaffold/package.json +4 -3
- package/dist/scaffold/server.mjs +156 -0
- package/dist/{secret-4_dYyrpA.mjs → secret-LnFdg8SB.mjs} +7 -7
- package/dist/{storage-BTfErOOW.mjs → storage-Cjj3Pv6i.mjs} +2 -2
- package/dist/{studio-LNvXtWak.mjs → studio-Rl5Po2M6.mjs} +36 -169
- package/dist/{test-C-V98oC-.mjs → test-BtOzTcXE.mjs} +1 -1
- package/package.json +8 -3
- package/dist/_slug-api-fRNNR8tz.mjs +0 -27
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.12.0",
|
|
16
|
+
"@alexkroman1/aai-ui": "^5.12.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.12.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
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
|
|
3
|
-
import { n as log,
|
|
4
|
-
import { t as
|
|
3
|
+
import { n as log, s as unwrapCancel } from "./_ui-u7T4YooX.mjs";
|
|
4
|
+
import { t as secretRequest } from "./_slug-api-DXm7s9X5.mjs";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
import { text } from "node:stream/consumers";
|
|
7
7
|
//#region secret.ts
|
|
@@ -29,24 +29,24 @@ async function executeSecretPut(cwd, name, value, server) {
|
|
|
29
29
|
if (!result) return fail(...NO_INPUT);
|
|
30
30
|
secretValue = result;
|
|
31
31
|
}
|
|
32
|
-
const {
|
|
32
|
+
const { target } = await secretRequest(cwd, "", {
|
|
33
33
|
method: "PUT",
|
|
34
34
|
body: { [name]: secretValue },
|
|
35
35
|
action: "secret"
|
|
36
36
|
}, server);
|
|
37
|
-
log.success(`Set ${name} for ${
|
|
37
|
+
log.success(`Set ${name} for ${target}`);
|
|
38
38
|
return ok({ name });
|
|
39
39
|
}
|
|
40
40
|
async function executeSecretDelete(cwd, name, server) {
|
|
41
|
-
const {
|
|
41
|
+
const { target } = await secretRequest(cwd, `/${encodeURIComponent(name)}`, {
|
|
42
42
|
method: "DELETE",
|
|
43
43
|
action: "secret"
|
|
44
44
|
}, server);
|
|
45
|
-
log.success(`Deleted ${name} from ${
|
|
45
|
+
log.success(`Deleted ${name} from ${target}`);
|
|
46
46
|
return ok({ name });
|
|
47
47
|
}
|
|
48
48
|
async function executeSecretList(cwd, server) {
|
|
49
|
-
const { data: { vars } } = await
|
|
49
|
+
const { data: { vars } } = await secretRequest(cwd, "", { action: "secret" }, server);
|
|
50
50
|
if (vars.length === 0) log.info("No secrets set. Use `aai secret put <name>` to add one.");
|
|
51
51
|
else {
|
|
52
52
|
log.message(`${vars.length} secret${vars.length === 1 ? "" : "s"}:`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
|
|
3
|
-
import { n as log } from "./_ui-
|
|
4
|
-
import {
|
|
3
|
+
import { n as log } from "./_ui-u7T4YooX.mjs";
|
|
4
|
+
import { n as slugRequest } from "./_slug-api-DXm7s9X5.mjs";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
//#region storage.ts
|
|
7
7
|
async function storageRequest(cwd, init, server) {
|
|
@@ -1,170 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, t as CliError } from "./_output-CC300DzW.mjs";
|
|
3
|
-
import { n as log, t as fmtUrl } from "./_ui-
|
|
4
|
-
import { s as updateProjectConfig } from "./_config-
|
|
3
|
+
import { n as log, t as fmtUrl } from "./_ui-u7T4YooX.mjs";
|
|
4
|
+
import { s as updateProjectConfig } from "./_config-riTmrwxX.mjs";
|
|
5
5
|
import { t as resolveServerEnv } from "./_server-common-DX8Bfrf5.mjs";
|
|
6
|
-
import { i as resolveDeployTarget } from "./_agent-
|
|
7
|
-
import { layerScaffold } from "./_templates-
|
|
6
|
+
import { i as resolveDeployTarget } from "./_agent-JGaAJ5j6.mjs";
|
|
7
|
+
import { layerScaffold } from "./_templates-CKYDgGyY.mjs";
|
|
8
8
|
import { n as apiRequest } from "./_api-client-B-upMGkc.mjs";
|
|
9
|
+
import { a as publishStudioProject, i as projectNameFromDir, n as fetchStudioProject, o as pushStudioSource, r as listStudioProjects, s as studioProjectUrl, t as collectSourceFiles } from "./_studio-CXoY_Hsg.mjs";
|
|
9
10
|
import path from "node:path";
|
|
10
|
-
import { mkdir,
|
|
11
|
-
import { PREVIEW_SLUG_SUFFIX, VALID_SLUG_RE } from "@alexkroman1/aai/utils";
|
|
12
|
-
//#region _studio.ts
|
|
13
|
-
/**
|
|
14
|
-
* Internals of the studio-workspace commands (`aai list/pull/push/publish`):
|
|
15
|
-
* the local source-file walk and the thin clients for the platform's
|
|
16
|
-
* `/studio/projects` routes. The workspace is the single source of truth —
|
|
17
|
-
* these helpers only move files between a local directory and the project's
|
|
18
|
-
* workspace row; production deploys happen exclusively through the studio's
|
|
19
|
-
* Publish route (which runs the deploy machinery in the project's sandbox).
|
|
20
|
-
*/
|
|
21
|
-
/**
|
|
22
|
-
* Mirrors of the studio workspace caps (`aai-studio-server/studio-limits.ts`)
|
|
23
|
-
* — by value, like `aai-guest/limits.ts`, since the CLI cannot depend on the
|
|
24
|
-
* private server package. The server re-validates every push; these exist so
|
|
25
|
-
* an oversized file is a named warning locally instead of a rejected upload.
|
|
26
|
-
*/
|
|
27
|
-
const MAX_STUDIO_FILE_BYTES = 256e3;
|
|
28
|
-
/**
|
|
29
|
-
* What never syncs to a workspace — the same set the guest's snapshot skips
|
|
30
|
-
* (`aai-guest/studio-workspace-fs.ts`), plus what only exists locally:
|
|
31
|
-
* secrets (`.env` rides the secret routes, never a workspace row) and
|
|
32
|
-
* lockfiles (regenerated by install, noise in the studio editor).
|
|
33
|
-
*/
|
|
34
|
-
const IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
35
|
-
"node_modules",
|
|
36
|
-
".git",
|
|
37
|
-
"dist",
|
|
38
|
-
".aai"
|
|
39
|
-
]);
|
|
40
|
-
const IGNORED_FILES = [
|
|
41
|
-
/^\.env(\..+)?$/,
|
|
42
|
-
/^pnpm-lock\.yaml$/,
|
|
43
|
-
/^package-lock\.json$/,
|
|
44
|
-
/^yarn\.lock$/,
|
|
45
|
-
/^\.DS_Store$/
|
|
46
|
-
];
|
|
47
|
-
function isIgnoredFile(name) {
|
|
48
|
-
return IGNORED_FILES.some((re) => re.test(name));
|
|
49
|
-
}
|
|
50
|
-
/** Project-relative paths of every syncable file under `dir`, sorted. */
|
|
51
|
-
async function walkProject(dir, current = dir) {
|
|
52
|
-
const out = [];
|
|
53
|
-
const entries = await readdir(current, { withFileTypes: true });
|
|
54
|
-
for (const entry of entries) {
|
|
55
|
-
if (entry.isSymbolicLink()) continue;
|
|
56
|
-
const abs = path.join(current, entry.name);
|
|
57
|
-
if (entry.isDirectory()) {
|
|
58
|
-
if (!IGNORED_DIRS.has(entry.name)) out.push(...await walkProject(dir, abs));
|
|
59
|
-
} else if (entry.isFile() && !isIgnoredFile(entry.name)) out.push(path.relative(dir, abs));
|
|
60
|
-
}
|
|
61
|
-
return out.sort((a, b) => a.localeCompare(b));
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Decode `buf` as UTF-8, or null when it isn't valid UTF-8.
|
|
65
|
-
*
|
|
66
|
-
* `fatal` makes an invalid sequence throw instead of becoming U+FFFD, which
|
|
67
|
-
* is the whole point: a workspace is a JSON path→string map and cannot carry
|
|
68
|
-
* arbitrary bytes, so a lossy read turned a pushed PNG into replacement
|
|
69
|
-
* characters while reporting success — and a later `aai pull` wrote the
|
|
70
|
-
* mangled version back over the local original. `ignoreBOM` keeps a leading
|
|
71
|
-
* U+FEFF in the string; without it the decoder strips the BOM and the check
|
|
72
|
-
* meant to stop corruption would quietly perform some of its own.
|
|
73
|
-
*/
|
|
74
|
-
const UTF8_STRICT = new TextDecoder("utf-8", {
|
|
75
|
-
fatal: true,
|
|
76
|
-
ignoreBOM: true
|
|
77
|
-
});
|
|
78
|
-
function decodeUtf8(buf) {
|
|
79
|
-
try {
|
|
80
|
-
return UTF8_STRICT.decode(buf);
|
|
81
|
-
} catch {
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
/**
|
|
86
|
-
* Walk a local project into the path→content record a workspace stores —
|
|
87
|
-
* the CLI-side twin of the guest's `snapshotWorkspace`: same ignored
|
|
88
|
-
* directories, same caps, oversized and non-text files skipped with a
|
|
89
|
-
* warning rather than failing the whole push.
|
|
90
|
-
*/
|
|
91
|
-
async function collectSourceFiles(dir) {
|
|
92
|
-
const paths = await walkProject(dir);
|
|
93
|
-
const files = {};
|
|
94
|
-
const warnings = [];
|
|
95
|
-
if (paths.length > 100) warnings.push(`Project has ${paths.length} files; only the first 100 sync to the studio.`);
|
|
96
|
-
for (const rel of paths.slice(0, 100)) {
|
|
97
|
-
const abs = path.join(dir, rel);
|
|
98
|
-
const st = await stat(abs);
|
|
99
|
-
if (st.size > 256e3) {
|
|
100
|
-
warnings.push(`${rel} is ${st.size} bytes (max ${MAX_STUDIO_FILE_BYTES}) — not synced.`);
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
const content = decodeUtf8(await readFile(abs));
|
|
104
|
-
if (content === null) {
|
|
105
|
-
warnings.push(`${rel} is not valid UTF-8 (binary file?) — not synced.`);
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
files[rel.split(path.sep).join("/")] = content;
|
|
109
|
-
}
|
|
110
|
-
return {
|
|
111
|
-
files,
|
|
112
|
-
warnings
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* A studio project name derived from a directory name, or null if unusable.
|
|
117
|
-
*
|
|
118
|
-
* A `-preview` suffix is deliberately unusable. Publishing a project deploys
|
|
119
|
-
* it under the project's own name, so a `*-preview` project would claim a
|
|
120
|
-
* slug the studio's orphan-preview sweep reaps hourly — deleting the agent,
|
|
121
|
-
* its app-database schema, and its secrets on a schedule the user never
|
|
122
|
-
* asked for. Refusing the name is recoverable (rename the directory); losing
|
|
123
|
-
* a published agent to the reaper is not.
|
|
124
|
-
*/
|
|
125
|
-
function projectNameFromDir(dir) {
|
|
126
|
-
const name = path.basename(dir).toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-_]+|[-_]+$/g, "").slice(0, 64);
|
|
127
|
-
if (name.endsWith(PREVIEW_SLUG_SUFFIX)) return null;
|
|
128
|
-
return VALID_SLUG_RE.test(name) ? name : null;
|
|
129
|
-
}
|
|
130
|
-
/** The shareable studio URL for a project — what every command prints. */
|
|
131
|
-
function studioProjectUrl(serverUrl, project) {
|
|
132
|
-
return `${serverUrl}/studio/chat/${project}`;
|
|
133
|
-
}
|
|
134
|
-
function listStudioProjects(serverUrl, apiKey) {
|
|
135
|
-
return apiRequest(`${serverUrl}/studio/projects`, {
|
|
136
|
-
apiKey,
|
|
137
|
-
action: "list"
|
|
138
|
-
}).then((res) => res.projects);
|
|
139
|
-
}
|
|
140
|
-
/** Fetch a project, or null when it doesn't exist (the push existence probe). */
|
|
141
|
-
function fetchStudioProject(serverUrl, apiKey, project) {
|
|
142
|
-
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}`, {
|
|
143
|
-
apiKey,
|
|
144
|
-
action: "pull",
|
|
145
|
-
allow404: true
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
/** `PUT /studio/projects/:project/source` — the atomic whole-tree push. */
|
|
149
|
-
function pushStudioSource(serverUrl, apiKey, project, body) {
|
|
150
|
-
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/source`, {
|
|
151
|
-
apiKey,
|
|
152
|
-
action: "push",
|
|
153
|
-
method: "PUT",
|
|
154
|
-
body,
|
|
155
|
-
hints: { 409: "The studio has newer changes. Run `aai pull` to fetch them, or `aai push --force` to overwrite." }
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
/** `POST /studio/projects/:project/deploy` — Publish, in the project's sandbox. */
|
|
159
|
-
function publishStudioProject(serverUrl, apiKey, project) {
|
|
160
|
-
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/deploy`, {
|
|
161
|
-
apiKey,
|
|
162
|
-
action: "publish",
|
|
163
|
-
method: "POST",
|
|
164
|
-
retry: 0
|
|
165
|
-
});
|
|
166
|
-
}
|
|
167
|
-
//#endregion
|
|
11
|
+
import { mkdir, readdir, writeFile } from "node:fs/promises";
|
|
168
12
|
//#region studio.ts
|
|
169
13
|
/**
|
|
170
14
|
* The studio-workspace commands: `aai list`, `aai pull`, `aai push`,
|
|
@@ -188,17 +32,40 @@ async function executeList(opts) {
|
|
|
188
32
|
}
|
|
189
33
|
/** Write a pulled file map under `dir`, refusing paths that escape it. */
|
|
190
34
|
async function materializeFiles(dir, files) {
|
|
191
|
-
|
|
35
|
+
const targets = Object.entries(files).map(([rel, content]) => {
|
|
192
36
|
const abs = path.resolve(dir, rel);
|
|
193
37
|
if (abs !== dir && !abs.startsWith(dir + path.sep)) throw new Error(`Pulled file path escapes the project directory: ${rel}`);
|
|
38
|
+
return {
|
|
39
|
+
abs,
|
|
40
|
+
content
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
await Promise.all(targets.map(async ({ abs, content }) => {
|
|
194
44
|
await mkdir(path.dirname(abs), { recursive: true });
|
|
195
45
|
await writeFile(abs, content, "utf-8");
|
|
196
|
-
}
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The hint for a pull that found nothing, which is where the two causes have
|
|
50
|
+
* to be told apart — and only the project LIST can do it. A typo has other
|
|
51
|
+
* projects beside it; an empty list means this login sees no projects at all,
|
|
52
|
+
* i.e. the CLI is authenticated as a different account than the browser the
|
|
53
|
+
* project was created in (the account's key is what decides studio scope —
|
|
54
|
+
* see `resolveBearer` server-side). Naming the visible projects is also the
|
|
55
|
+
* answer to a typo, so the round trip pays for itself either way. Best
|
|
56
|
+
* effort: the list is a second request on an already-failing path, and its
|
|
57
|
+
* own failure must not replace the 404 the user needs to see.
|
|
58
|
+
*/
|
|
59
|
+
async function notFoundHint(serverUrl, apiKey) {
|
|
60
|
+
const projects = await listStudioProjects(serverUrl, apiKey).catch(() => null);
|
|
61
|
+
if (projects === null) return "Run `aai list` to see your projects.";
|
|
62
|
+
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`.";
|
|
63
|
+
return `Your projects: ${projects.slice(0, 10).join(", ")}${projects.length > 10 ? `, and ${projects.length - 10} more` : ""}.`;
|
|
197
64
|
}
|
|
198
65
|
async function executePull(opts) {
|
|
199
66
|
const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
|
|
200
67
|
const remote = await fetchStudioProject(serverUrl, apiKey, opts.project);
|
|
201
|
-
if (!remote) throw new CliError("not_found", `No studio project named "${opts.project}".`,
|
|
68
|
+
if (!remote) throw new CliError("not_found", `No studio project named "${opts.project}".`, await notFoundHint(serverUrl, apiKey));
|
|
202
69
|
const target = path.resolve(opts.cwd, opts.dir ?? opts.project);
|
|
203
70
|
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
71
|
await materializeFiles(target, remote.files);
|
|
@@ -276,11 +143,11 @@ async function executePush(opts) {
|
|
|
276
143
|
* deploy time, which is why publish syncs them BEFORE deploying when the
|
|
277
144
|
* slug is already known.
|
|
278
145
|
*/
|
|
279
|
-
async function syncEnvSecrets(cwd, serverUrl, apiKey,
|
|
146
|
+
async function syncEnvSecrets(cwd, serverUrl, apiKey, project) {
|
|
280
147
|
const env = await resolveServerEnv(cwd);
|
|
281
148
|
const names = Object.keys(env);
|
|
282
149
|
if (names.length === 0) return [];
|
|
283
|
-
await apiRequest(`${serverUrl}/${
|
|
150
|
+
await apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/secret`, {
|
|
284
151
|
apiKey,
|
|
285
152
|
action: "secret",
|
|
286
153
|
method: "PUT",
|
|
@@ -291,13 +158,13 @@ async function syncEnvSecrets(cwd, serverUrl, apiKey, slug) {
|
|
|
291
158
|
}
|
|
292
159
|
async function executePublish(opts) {
|
|
293
160
|
if (!opts.skipTypecheck) {
|
|
294
|
-
const { assertTypechecks } = await import("./_typecheck-gate-
|
|
161
|
+
const { assertTypechecks } = await import("./_typecheck-gate-4v9UBZHh.mjs");
|
|
295
162
|
await assertTypechecks(opts.cwd);
|
|
296
163
|
}
|
|
297
164
|
const pushed = await pushProject(opts);
|
|
298
165
|
const { project, serverUrl, apiKey } = pushed;
|
|
299
166
|
const hadSlug = pushed.slug !== void 0;
|
|
300
|
-
if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey,
|
|
167
|
+
if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, project);
|
|
301
168
|
log.step(`Publishing ${project} (builds in the project's sandbox)…`);
|
|
302
169
|
const result = await publishStudioProject(serverUrl, apiKey, project);
|
|
303
170
|
if (typeof result?.slug !== "string" || typeof result?.output !== "string") throw new CliError("bad_publish_response", `Unexpected response from the publish route at ${serverUrl}.`, "Check that --server points at an aai platform server, then try again.");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { a as ok, n as fail } from "./_output-CC300DzW.mjs";
|
|
3
|
-
import { n as log } from "./_ui-
|
|
3
|
+
import { n as log } from "./_ui-u7T4YooX.mjs";
|
|
4
4
|
import { a as errorMessage, n as binFromPackageJson, r as errorCode } from "./_utils-8KKw-bzi.mjs";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import { existsSync } from "node:fs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.12.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aai": "bin.mjs"
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"@dev/source": "./typecheck.ts",
|
|
21
21
|
"types": "./dist/typecheck.d.ts",
|
|
22
22
|
"import": "./dist/typecheck.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./project-config": {
|
|
25
|
+
"@dev/source": "./project-config.ts",
|
|
26
|
+
"types": "./dist/project-config.d.ts",
|
|
27
|
+
"import": "./dist/project-config.mjs"
|
|
23
28
|
}
|
|
24
29
|
},
|
|
25
30
|
"files": [
|
|
@@ -38,8 +43,8 @@
|
|
|
38
43
|
"p-timeout": "^7.0.1",
|
|
39
44
|
"vite": "^8.1.5",
|
|
40
45
|
"zod": "^4.4.3",
|
|
41
|
-
"@alexkroman1/aai": "5.
|
|
42
|
-
"@alexkroman1/aai-ui": "5.
|
|
46
|
+
"@alexkroman1/aai": "5.12.0",
|
|
47
|
+
"@alexkroman1/aai-ui": "5.12.0"
|
|
43
48
|
},
|
|
44
49
|
"devDependencies": {
|
|
45
50
|
"playwright": "^1.61.1",
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { n as getServerInfo } from "./_agent-DS2PUJcl.mjs";
|
|
3
|
-
import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-B-upMGkc.mjs";
|
|
4
|
-
//#region _slug-api.ts
|
|
5
|
-
/**
|
|
6
|
-
* Authenticated request against a deployed agent's slug-scoped resource
|
|
7
|
-
* (`${serverUrl}/${slug}${resourcePath}`) — the one shape every per-agent
|
|
8
|
-
* command (secret, storage) shares, including the standard "not deployed"
|
|
9
|
-
* 404 hint.
|
|
10
|
-
*
|
|
11
|
-
* Its own module (rather than living beside `getServerInfo` in `_agent.ts`)
|
|
12
|
-
* so tests can mock `_agent.ts`/`_api-client.ts` while this composition
|
|
13
|
-
* stays real — an intra-module call would bypass those mocks.
|
|
14
|
-
*/
|
|
15
|
-
async function slugRequest(cwd, resourcePath, init, server) {
|
|
16
|
-
const { serverUrl, slug, apiKey } = await getServerInfo(cwd, server);
|
|
17
|
-
return {
|
|
18
|
-
data: await apiRequest(`${serverUrl}/${slug}${resourcePath}`, {
|
|
19
|
-
...init,
|
|
20
|
-
apiKey,
|
|
21
|
-
hints: { 404: HINT_NOT_DEPLOYED }
|
|
22
|
-
}),
|
|
23
|
-
slug
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
//#endregion
|
|
27
|
-
export { slugRequest as t };
|