@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.
Files changed (44) hide show
  1. package/dist/{_agent-DS2PUJcl.mjs → _agent-JGaAJ5j6.mjs} +3 -2
  2. package/dist/_agent.d.ts +1 -0
  3. package/dist/{_bundler-DC17suWN.mjs → _bundler-BvfPzf8J.mjs} +28 -9
  4. package/dist/_bundler.d.ts +16 -2
  5. package/dist/{_config-DMyolIk9.mjs → _config-riTmrwxX.mjs} +2 -2
  6. package/dist/_deploy.d.ts +0 -10
  7. package/dist/_dev-restart.d.ts +59 -0
  8. package/dist/{_dev-server-B3ivqyEd.mjs → _dev-server-BgXXSijx.mjs} +178 -89
  9. package/dist/_dev-server.d.ts +17 -0
  10. package/dist/{_init-CYLvYU-B.mjs → _init-Ca5Eg5pn.mjs} +2 -2
  11. package/dist/_preflight-BtfaYtbE.mjs +46 -0
  12. package/dist/_preflight.d.ts +57 -0
  13. package/dist/_slug-api-DXm7s9X5.mjs +50 -0
  14. package/dist/_slug-api.d.ts +16 -0
  15. package/dist/_studio-CXoY_Hsg.mjs +94 -0
  16. package/dist/_studio.d.ts +19 -16
  17. package/dist/{_templates-BWJOiWOO.mjs → _templates-CKYDgGyY.mjs} +18 -6
  18. package/dist/_templates.d.ts +10 -0
  19. package/dist/{_typecheck-gate-DB-PY0A3.mjs → _typecheck-gate-4v9UBZHh.mjs} +1 -1
  20. package/dist/{_ui-DfwfDbT-.mjs → _ui-u7T4YooX.mjs} +5 -1
  21. package/dist/_ui.d.ts +2 -0
  22. package/dist/{build-CACFbdQ4.mjs → build-BTOyN3H5.mjs} +4 -4
  23. package/dist/cli.d.ts +2 -1
  24. package/dist/cli.mjs +93 -32
  25. package/dist/{delete-DEZ7u3u4.mjs → delete-D3gZdSs-.mjs} +3 -3
  26. package/dist/{deploy-Ch0d_jje.mjs → deploy-Df6e7YFn.mjs} +21 -21
  27. package/dist/deploy.d.ts +0 -2
  28. package/dist/{dev-CdeRcYiQ.mjs → dev-DqIUtFfS.mjs} +2 -2
  29. package/dist/eject-DSldO60O.mjs +90 -0
  30. package/dist/eject.d.ts +33 -0
  31. package/dist/{init-GQsNWkLJ.mjs → init-B-04lRSV.mjs} +4 -4
  32. package/dist/{login-BeFUiU6M.mjs → login-CtimAsZk.mjs} +4 -15
  33. package/dist/login.d.ts +0 -9
  34. package/dist/project-config.d.ts +23 -0
  35. package/dist/project-config.mjs +3 -0
  36. package/dist/scaffold/CLAUDE.md +23 -0
  37. package/dist/scaffold/package.json +4 -3
  38. package/dist/scaffold/server.mjs +156 -0
  39. package/dist/{secret-4_dYyrpA.mjs → secret-LnFdg8SB.mjs} +7 -7
  40. package/dist/{storage-BTfErOOW.mjs → storage-Cjj3Pv6i.mjs} +2 -2
  41. package/dist/{studio-LNvXtWak.mjs → studio-Rl5Po2M6.mjs} +36 -169
  42. package/dist/{test-C-V98oC-.mjs → test-BtOzTcXE.mjs} +1 -1
  43. package/package.json +8 -3
  44. package/dist/_slug-api-fRNNR8tz.mjs +0 -27
@@ -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.10.1",
15
- "@alexkroman1/aai-ui": "^5.10.1",
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.10.1",
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, o as unwrapCancel } from "./_ui-DfwfDbT-.mjs";
4
- import { t as slugRequest } from "./_slug-api-fRNNR8tz.mjs";
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 { slug } = await slugRequest(cwd, "/secret", {
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 ${slug}`);
37
+ log.success(`Set ${name} for ${target}`);
38
38
  return ok({ name });
39
39
  }
40
40
  async function executeSecretDelete(cwd, name, server) {
41
- const { slug } = await slugRequest(cwd, `/secret/${encodeURIComponent(name)}`, {
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 ${slug}`);
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 slugRequest(cwd, "/secret", { action: "secret" }, server);
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-DfwfDbT-.mjs";
4
- import { t as slugRequest } from "./_slug-api-fRNNR8tz.mjs";
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-DfwfDbT-.mjs";
4
- import { s as updateProjectConfig } from "./_config-DMyolIk9.mjs";
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-DS2PUJcl.mjs";
7
- import { layerScaffold } from "./_templates-BWJOiWOO.mjs";
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, readFile, readdir, stat, writeFile } from "node:fs/promises";
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
- for (const [rel, content] of Object.entries(files)) {
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}".`, "Run `aai list` to see your projects.");
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, slug) {
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}/${slug}/secret`, {
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-DB-PY0A3.mjs");
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, pushed.slug);
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-DfwfDbT-.mjs";
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.10.1",
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.10.1",
42
- "@alexkroman1/aai-ui": "5.10.1"
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 };