@alexkroman1/aai-cli 5.6.0 → 5.7.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.
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as readProjectConfig, i as readGlobalConfig, n as ensureApiKey, o as serverOrigin, t as approveServer } from "./_config-PIC0nzRu.mjs";
2
+ import { a as readProjectConfig, i as readGlobalConfig, n as ensureApiKey, o as serverOrigin, t as approveServer } from "./_config-Y5V-5Krn.mjs";
3
3
  import { existsSync } from "node:fs";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
@@ -67,6 +67,25 @@ function resolveServerUrl(explicit, configUrl, approvedOrigins = []) {
67
67
  If you do intend to use that server, re-run with --server ${origin} to approve it.`);
68
68
  }
69
69
  /**
70
+ * Reject a repo-supplied slug that isn't the platform's slug shape.
71
+ *
72
+ * Enforced before a slug is ever interpolated into a URL path.
73
+ * `.aai/project.json` is part of the working tree, so a cloned repo controls
74
+ * this value, and callers pair it with the user's API key — `aai publish`
75
+ * hands it to `syncEnvSecrets`, which PUTs the whole `.env` to
76
+ * `${serverUrl}/${slug}/secret`. A hostile `"slug": "x/../admin"` must not
77
+ * steer that request to a path of the repo's choosing.
78
+ *
79
+ * Lives here, at the single point where repo-controlled config becomes a
80
+ * credentialed target, rather than at each call site: the check used to
81
+ * exist only in `getServerInfo` (secret/storage/delete), so `publish` — the
82
+ * command users actually run — had no guard at all.
83
+ */
84
+ function assertValidConfigSlug(slug) {
85
+ if (slug === void 0 || VALID_SLUG_RE.test(slug)) return;
86
+ throw new Error(`Invalid slug in .aai/project.json: ${JSON.stringify(slug)}\n Expected lowercase letters, digits, \`-\`, \`_\` (2-64 chars). Fix or delete the file — \`aai publish\` will create a fresh deployment.`);
87
+ }
88
+ /**
70
89
  * Resolve everything needed to talk to the platform: project config (null if
71
90
  * the project has never been deployed), server URL, and API key.
72
91
  */
@@ -74,6 +93,7 @@ async function resolveDeployTarget(cwd, explicitServer) {
74
93
  const [config, globalConfig] = await Promise.all([readProjectConfig(cwd), readGlobalConfig()]);
75
94
  const serverUrl = resolveServerUrl(explicitServer, config?.serverUrl, globalConfig.approvedServers ?? []);
76
95
  if (explicitServer) await approveServer(serverUrl);
96
+ assertValidConfigSlug(config?.slug);
77
97
  return {
78
98
  config,
79
99
  serverUrl,
@@ -84,7 +104,6 @@ async function resolveDeployTarget(cwd, explicitServer) {
84
104
  async function getServerInfo(cwd, explicitServer) {
85
105
  const { config, serverUrl, apiKey } = await resolveDeployTarget(cwd, explicitServer);
86
106
  if (!config?.slug) throw new Error("This project has no deployed agent — run `aai publish` first");
87
- if (!VALID_SLUG_RE.test(config.slug)) throw new Error(`Invalid slug in .aai/project.json: ${JSON.stringify(config.slug)}\n Expected lowercase letters, digits, \`-\`, \`_\` (2-64 chars). Fix the file or run \`aai publish\` to create a fresh deployment.`);
88
107
  return {
89
108
  serverUrl,
90
109
  slug: config.slug,
@@ -34,11 +34,65 @@ async function apiRequest(url, opts) {
34
34
  throw toApiError(err, url, opts);
35
35
  }
36
36
  }
37
+ /**
38
+ * Collapse `{"error": "..."}` payloads embedded in a message into their own
39
+ * text. Studio Publish runs the real `aai deploy` inside the sandbox, so its
40
+ * failures arrive wrapped twice and stringifying them produced a
41
+ * triple-escaped wall of JSON around one actionable sentence.
42
+ */
43
+ function unwrapEmbeddedErrors(message, depth = 0) {
44
+ if (depth > 3) return message;
45
+ const start = message.indexOf("{\"error\"");
46
+ if (start === -1) return message;
47
+ const json = message.slice(start);
48
+ try {
49
+ const inner = JSON.parse(json).error;
50
+ if (typeof inner !== "string") return message;
51
+ return unwrapEmbeddedErrors(message.slice(0, start) + inner, depth + 1);
52
+ } catch {
53
+ return message;
54
+ }
55
+ }
56
+ /** The messages of a Zod issue tree, deduped and flattened. */
57
+ function zodIssueMessages(value) {
58
+ if (Array.isArray(value)) return value.flatMap(zodIssueMessages);
59
+ if (value === null || typeof value !== "object") return [];
60
+ const node = value;
61
+ const nested = zodIssueMessages(node.issues);
62
+ if (nested.length > 0) return nested;
63
+ return typeof node.message === "string" ? [node.message] : [];
64
+ }
65
+ /**
66
+ * A human-readable one-liner for a server error body.
67
+ *
68
+ * Servers answer with `{ error }`, or with a serialized ZodError whose useful
69
+ * part is buried several levels down. Dumping the raw JSON turned a
70
+ * one-character mistake (`aai secret put MY-KEY`) into a 515-character escaped
71
+ * blob, so the shapes we actually emit are unwrapped here and anything else
72
+ * falls back to the raw body rather than being dropped.
73
+ */
74
+ function describeErrorBody(data) {
75
+ if (typeof data === "string") return data;
76
+ if (data === null || typeof data !== "object") return JSON.stringify(data ?? "");
77
+ const error = data.error;
78
+ if (typeof error === "string") return unwrapEmbeddedErrors(error);
79
+ if (error !== null && typeof error === "object") {
80
+ const { message } = error;
81
+ if (typeof message === "string") {
82
+ try {
83
+ const issues = zodIssueMessages(JSON.parse(message));
84
+ if (issues.length > 0) return [...new Set(issues)].join("; ");
85
+ } catch {}
86
+ return message;
87
+ }
88
+ }
89
+ return JSON.stringify(data);
90
+ }
37
91
  /** Format an ofetch failure into a descriptive, action-centric error. */
38
92
  function toApiError(err, url, opts) {
39
93
  if (err instanceof FetchError && err.statusCode !== void 0) {
40
94
  const status = err.statusCode;
41
- const body = typeof err.data === "string" ? err.data : JSON.stringify(err.data ?? "");
95
+ const body = describeErrorBody(err.data);
42
96
  const hint = status === 401 ? HINT_INVALID_API_KEY : opts.hints?.[status];
43
97
  return /* @__PURE__ */ new Error(`${opts.action} failed (HTTP ${status}): ${body}${hint ? `\n ${hint}` : ""}`);
44
98
  }
@@ -41,3 +41,13 @@ export type ApiRequestOptions = {
41
41
  * failure (the 401 hint is always included; pass more via `hints`).
42
42
  */
43
43
  export declare function apiRequest<T = unknown>(url: string, opts: ApiRequestOptions): Promise<T>;
44
+ /**
45
+ * A human-readable one-liner for a server error body.
46
+ *
47
+ * Servers answer with `{ error }`, or with a serialized ZodError whose useful
48
+ * part is buried several levels down. Dumping the raw JSON turned a
49
+ * one-character mistake (`aai secret put MY-KEY`) into a 515-character escaped
50
+ * blob, so the shapes we actually emit are unwrapped here and anything else
51
+ * falls back to the raw body rather than being dropped.
52
+ */
53
+ export declare function describeErrorBody(data: unknown): string;
@@ -1,3 +1,4 @@
1
+ import type { ArgsDef, CommandDef } from "citty";
1
2
  import { type CommandResult, type OutputMode } from "./_output.ts";
2
3
  /** Shared arg definitions for citty commands. */
3
4
  export declare const sharedArgs: {
@@ -16,6 +17,31 @@ export declare const sharedArgs: {
16
17
  readonly description: "Output JSON (auto-detected in non-TTY)";
17
18
  };
18
19
  };
20
+ /**
21
+ * Flags in `rawArgs` that `argsDef` doesn't declare, in the form the user
22
+ * typed them.
23
+ *
24
+ * citty silently drops an unrecognized flag, so `aai push --serverr=http://x`
25
+ * exited 0 having pushed to the DEFAULT server — production, for an installed
26
+ * CLI — as if the flag had been honoured. Since `--server` is what decides
27
+ * where the API key and secret values are sent, a typo quietly retargeting it
28
+ * is worth failing on.
29
+ */
30
+ export declare function findUnknownFlags(rawArgs: string[], argsDef: ArgsDef): string[];
31
+ /**
32
+ * Any command in the tree, regardless of its args shape — the walk below only
33
+ * reads `subCommands` and `args`, and the concrete generics differ per command.
34
+ */
35
+ type AnyCommandDef = CommandDef<ArgsDef>;
36
+ /**
37
+ * Unknown flags in `argv` for whichever (possibly nested) subcommand it
38
+ * selects — `[]` when everything is declared.
39
+ *
40
+ * Walks the real command tree rather than re-listing flags, so this cannot
41
+ * drift from what the commands accept. An unknown SUBCOMMAND is not reported:
42
+ * citty already answers that with usage text and a non-zero exit.
43
+ */
44
+ export declare function unknownFlagsForArgv(root: AnyCommandDef, argv: string[]): Promise<string[]>;
19
45
  /** Shared command setup: resolve cwd, optionally require agent.ts. */
20
46
  export declare function setup(opts?: {
21
47
  agent?: boolean;
@@ -36,3 +62,4 @@ export declare function setup(opts?: {
36
62
  export declare function runCommand(args: {
37
63
  json?: boolean | undefined;
38
64
  }, fn: (mode: OutputMode) => Promise<CommandResult<unknown>>): Promise<void>;
65
+ export {};
@@ -1,8 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { a as unwrapCancel, n as log, o as CliError } from "./_ui-8kOEB-JH.mjs";
2
+ import { n as log, o as CliError } from "./_ui-8kOEB-JH.mjs";
3
3
  import { a as errorMessage, c as readJson, d as writeJson } from "./_utils-Ch0J4s6a.mjs";
4
+ import { mkdtempSync } from "node:fs";
4
5
  import path from "node:path";
5
- import * as p from "@clack/prompts";
6
+ import { tmpdir } from "node:os";
6
7
  import envPaths from "env-paths";
7
8
  import { z } from "zod";
8
9
  //#region _config.ts
@@ -42,8 +43,19 @@ const ProjectConfigSchema = z.object({
42
43
  function getConfigDir() {
43
44
  const override = process.env.AAI_CONFIG_DIR?.trim();
44
45
  if (override) return override;
46
+ if (process.env.VITEST) return testConfigDir();
45
47
  return envPaths("aai", { suffix: "" }).config;
46
48
  }
49
+ /**
50
+ * Per-process throwaway config dir used only under vitest. Memoized: callers
51
+ * read-modify-write the same config across calls, so a fresh dir per call
52
+ * would silently drop what the previous one wrote.
53
+ */
54
+ let _testConfigDir;
55
+ function testConfigDir() {
56
+ _testConfigDir ??= mkdtempSync(path.join(tmpdir(), "aai-vitest-config-"));
57
+ return _testConfigDir;
58
+ }
47
59
  async function readProjectConfig(agentDir) {
48
60
  const file = path.join(agentDir, ".aai", "project.json");
49
61
  let data;
@@ -132,6 +144,20 @@ async function trySaveApiKey(dir, config, apiKey) {
132
144
  log.warn(`Couldn't save your API key to ${path.join(dir, "config.json")}: ${errorMessage(err)} — you'll be prompted again next run.`);
133
145
  }
134
146
  }
147
+ /**
148
+ * The credential every platform command runs on.
149
+ *
150
+ * Two sources, in order: the key `aai login` saved, then
151
+ * `ASSEMBLYAI_API_KEY` for non-interactive callers (CI, scripts, the eval
152
+ * harnesses).
153
+ *
154
+ * There is deliberately NO "paste a key" prompt. Pasting one produced a
155
+ * half-configured CLI — able to push and publish while linked to no account
156
+ * the user could see in the studio — and it made `aai login`, which is the
157
+ * real onboarding path, optional in practice. It was also the riskier code
158
+ * path: a hidden password prompt reads stdin, so a piped invocation could
159
+ * have its input eaten and persisted as the API key.
160
+ */
135
161
  async function ensureApiKey(configDir) {
136
162
  const dir = configDir ?? getConfigDir();
137
163
  const config = await readGlobalConfig(dir);
@@ -141,10 +167,7 @@ async function ensureApiKey(configDir) {
141
167
  await trySaveApiKey(dir, config, envKey);
142
168
  return envKey;
143
169
  }
144
- if (!process.stdin.isTTY) throw new CliError("no_api_key", "No API key configured and no TTY to prompt for one.", "Set the ASSEMBLYAI_API_KEY environment variable, or run `aai login` interactively once to save a key.");
145
- const apiKey = unwrapCancel(await p.password({ message: "Enter your AssemblyAI API key" }), "Setup cancelled");
146
- await trySaveApiKey(dir, config, apiKey);
147
- return apiKey;
170
+ throw new CliError("not_logged_in", "You're not logged in.", "Run `aai login` to link your account, or set ASSEMBLYAI_API_KEY for non-interactive use.");
148
171
  }
149
172
  //#endregion
150
- export { readProjectConfig as a, writeGlobalConfig as c, readGlobalConfig as i, ensureApiKey as n, serverOrigin as o, getConfigDir as r, updateProjectConfig as s, approveServer as t };
173
+ export { readProjectConfig as a, writeGlobalConfig as c, readGlobalConfig as i, writeProjectConfig as l, ensureApiKey as n, serverOrigin as o, getConfigDir as r, updateProjectConfig as s, approveServer as t };
package/dist/_config.d.ts CHANGED
@@ -61,5 +61,19 @@ export declare function serverOrigin(url: string): string | null;
61
61
  export declare function approveServer(url: string, configDir?: string): Promise<void>;
62
62
  export declare function readGlobalConfig(configDir?: string): Promise<GlobalConfig>;
63
63
  export declare function writeGlobalConfig(configDir: string, data: GlobalConfig): Promise<void>;
64
+ /**
65
+ * The credential every platform command runs on.
66
+ *
67
+ * Two sources, in order: the key `aai login` saved, then
68
+ * `ASSEMBLYAI_API_KEY` for non-interactive callers (CI, scripts, the eval
69
+ * harnesses).
70
+ *
71
+ * There is deliberately NO "paste a key" prompt. Pasting one produced a
72
+ * half-configured CLI — able to push and publish while linked to no account
73
+ * the user could see in the studio — and it made `aai login`, which is the
74
+ * real onboarding path, optional in practice. It was also the riskier code
75
+ * path: a hidden password prompt reads stdin, so a piped invocation could
76
+ * have its input eaten and persisted as the API key.
77
+ */
64
78
  export declare function ensureApiKey(configDir?: string): Promise<string>;
65
79
  export {};
@@ -4,7 +4,7 @@ import { a as errorMessage, r as errorCode } from "./_utils-Ch0J4s6a.mjs";
4
4
  import { n as fallbackHtmlPlugin } from "./client-bundler-yiWoXrgb.mjs";
5
5
  import { buildWorker } from "./worker-bundler.mjs";
6
6
  import { n as createWorkerEvaluator } from "./_bundler-Cjaxa2wi.mjs";
7
- import { n as ensureApiKey } from "./_config-PIC0nzRu.mjs";
7
+ import { n as ensureApiKey } from "./_config-Y5V-5Krn.mjs";
8
8
  import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
9
9
  import { createRequire } from "node:module";
10
10
  import { existsSync } from "node:fs";
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { c as readJson, d as writeJson, s as isEexist } from "./_utils-Ch0J4s6a.mjs";
3
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-BB1ZaZsX.mjs";
4
- import { REPO_URL, downloadAndMergeTemplate } from "./_templates-jmjgdgcd.mjs";
3
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-2nVugrN3.mjs";
4
+ import { REPO_URL, downloadAndMergeTemplate } from "./_templates-Bt6u9_68.mjs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
7
7
  //#region _init.ts
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { n as getServerInfo } from "./_agent-BB1ZaZsX.mjs";
3
- import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-B6_uvuLs.mjs";
2
+ import { n as getServerInfo } from "./_agent-2nVugrN3.mjs";
3
+ import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-B-upMGkc.mjs";
4
4
  //#region _slug-api.ts
5
5
  /**
6
6
  * Authenticated request against a deployed agent's slug-scoped resource
package/dist/_studio.d.ts CHANGED
@@ -17,14 +17,23 @@ export declare const MAX_STUDIO_FILES = 100;
17
17
  /**
18
18
  * Walk a local project into the path→content record a workspace stores —
19
19
  * the CLI-side twin of the guest's `snapshotWorkspace`: same ignored
20
- * directories, same caps, oversized files skipped with a warning rather
21
- * than failing the whole push.
20
+ * directories, same caps, oversized and non-text files skipped with a
21
+ * warning rather than failing the whole push.
22
22
  */
23
23
  export declare function collectSourceFiles(dir: string): Promise<{
24
24
  files: Record<string, string>;
25
25
  warnings: string[];
26
26
  }>;
27
- /** A studio project name derived from a directory name, or null if unusable. */
27
+ /**
28
+ * A studio project name derived from a directory name, or null if unusable.
29
+ *
30
+ * A `-preview` suffix is deliberately unusable. Publishing a project deploys
31
+ * it under the project's own name, so a `*-preview` project would claim a
32
+ * slug the studio's orphan-preview sweep reaps hourly — deleting the agent,
33
+ * its app-database schema, and its secrets on a schedule the user never
34
+ * asked for. Refusing the name is recoverable (rename the directory); losing
35
+ * a published agent to the reaper is not.
36
+ */
28
37
  export declare function projectNameFromDir(dir: string): string | null;
29
38
  /** The shareable studio URL for a project — what every command prints. */
30
39
  export declare function studioProjectUrl(serverUrl: string, project: string): string;
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as errorMessage } from "./_utils-Ch0J4s6a.mjs";
3
- import { t as getMonorepoRoot } from "./_agent-BB1ZaZsX.mjs";
3
+ import { t as getMonorepoRoot } from "./_agent-2nVugrN3.mjs";
4
4
  import { existsSync } from "node:fs";
5
5
  import path from "node:path";
6
6
  import fs from "node:fs/promises";
package/dist/cli.mjs CHANGED
@@ -23,6 +23,95 @@ const sharedArgs = {
23
23
  description: "Output JSON (auto-detected in non-TTY)"
24
24
  }
25
25
  };
26
+ /** Flags citty handles itself, so no command declares them. */
27
+ const BUILTIN_FLAGS = /* @__PURE__ */ new Set([
28
+ "help",
29
+ "h",
30
+ "version",
31
+ "v"
32
+ ]);
33
+ /**
34
+ * Flags in `rawArgs` that `argsDef` doesn't declare, in the form the user
35
+ * typed them.
36
+ *
37
+ * citty silently drops an unrecognized flag, so `aai push --serverr=http://x`
38
+ * exited 0 having pushed to the DEFAULT server — production, for an installed
39
+ * CLI — as if the flag had been honoured. Since `--server` is what decides
40
+ * where the API key and secret values are sent, a typo quietly retargeting it
41
+ * is worth failing on.
42
+ */
43
+ function findUnknownFlags(rawArgs, argsDef) {
44
+ const declared = declaredFlagNames(argsDef);
45
+ const unknown = [];
46
+ for (const raw of rawArgs) {
47
+ if (raw === "--") break;
48
+ const token = flagToken(raw);
49
+ if (token && !declared.has(canonicalFlag(token))) unknown.push(token);
50
+ }
51
+ return unknown;
52
+ }
53
+ /**
54
+ * A flag's comparison key: dashes dropped and lower-cased, with a leading
55
+ * `no` negation removed.
56
+ *
57
+ * citty accepts both `--allowMissingSecrets` and `--allow-missing-secrets` for
58
+ * an arg declared as `allowMissingSecrets`, and the guest's in-sandbox Publish
59
+ * spawns the kebab-case spelling. Comparing the literal text rejected
60
+ * `--allow-missing-secrets` as unknown and broke Publish for every studio
61
+ * user, so both sides are normalized to one key.
62
+ */
63
+ function canonicalFlag(flag) {
64
+ return flag.replace(/^--?/, "").replace(/^no-/, "").replace(/-/g, "").toLowerCase();
65
+ }
66
+ /** Every flag name and alias `argsDef` declares, plus citty's built-ins. */
67
+ function declaredFlagNames(argsDef) {
68
+ const declared = new Set([...BUILTIN_FLAGS].map(canonicalFlag));
69
+ for (const [name, def] of Object.entries(argsDef)) {
70
+ const { type, alias } = def;
71
+ if (type === "positional") continue;
72
+ declared.add(canonicalFlag(name));
73
+ if (typeof alias === "string") declared.add(canonicalFlag(alias));
74
+ else if (Array.isArray(alias)) for (const a of alias) declared.add(canonicalFlag(a));
75
+ }
76
+ return declared;
77
+ }
78
+ /** The flag part of `raw` (`--server` from `--server=x`), or null if positional. */
79
+ function flagToken(raw) {
80
+ if (!raw.startsWith("-") || raw === "-" || /^-\d/.test(raw)) return null;
81
+ const eq = raw.indexOf("=");
82
+ return eq === -1 ? raw : raw.slice(0, eq);
83
+ }
84
+ /** Resolve a possibly-nested `Resolvable` citty field. */
85
+ async function resolve(value) {
86
+ return typeof value === "function" ? await value() : await value;
87
+ }
88
+ /**
89
+ * Unknown flags in `argv` for whichever (possibly nested) subcommand it
90
+ * selects — `[]` when everything is declared.
91
+ *
92
+ * Walks the real command tree rather than re-listing flags, so this cannot
93
+ * drift from what the commands accept. An unknown SUBCOMMAND is not reported:
94
+ * citty already answers that with usage text and a non-zero exit.
95
+ */
96
+ async function unknownFlagsForArgv(root, argv) {
97
+ let cmd = root;
98
+ let i = 0;
99
+ for (; i < argv.length; i++) {
100
+ const token = argv[i];
101
+ if (token === void 0 || token.startsWith("-")) break;
102
+ const subCommands = await resolve(cmd.subCommands);
103
+ const next = subCommands?.[token];
104
+ if (next === void 0) {
105
+ if (subCommands && Object.keys(subCommands).length > 0) return [];
106
+ break;
107
+ }
108
+ const resolved = await resolve(next);
109
+ if (resolved === void 0) break;
110
+ cmd = resolved;
111
+ }
112
+ const argsDef = await resolve(cmd.args) ?? {};
113
+ return findUnknownFlags(argv.slice(i), argsDef);
114
+ }
26
115
  /** Shared command setup: resolve cwd, optionally require agent.ts. */
27
116
  async function setup(opts) {
28
117
  const cwd = resolveCwd();
@@ -76,7 +165,7 @@ const list = defineCommand({
76
165
  async run({ args }) {
77
166
  await runCommand(args, async () => {
78
167
  const cwd = resolveCwd();
79
- const { executeList } = await import("./studio-CsRn2J1a.mjs");
168
+ const { executeList } = await import("./studio-sXvYUxr5.mjs");
80
169
  return executeList({
81
170
  cwd,
82
171
  server: args.server
@@ -111,7 +200,7 @@ const pull = defineCommand({
111
200
  async run({ args }) {
112
201
  await runCommand(args, async () => {
113
202
  const cwd = resolveCwd();
114
- const { executePull } = await import("./studio-CsRn2J1a.mjs");
203
+ const { executePull } = await import("./studio-sXvYUxr5.mjs");
115
204
  return executePull({
116
205
  cwd,
117
206
  project: args.project,
@@ -139,7 +228,7 @@ const push = defineCommand({
139
228
  async run({ args }) {
140
229
  await runCommand(args, async () => {
141
230
  const cwd = await setup({ agent: true });
142
- const { executePush } = await import("./studio-CsRn2J1a.mjs");
231
+ const { executePush } = await import("./studio-sXvYUxr5.mjs");
143
232
  return executePush({
144
233
  cwd,
145
234
  server: args.server,
@@ -169,7 +258,7 @@ const publish = defineCommand({
169
258
  async run({ args }) {
170
259
  await runCommand(args, async () => {
171
260
  const cwd = await setup({ agent: true });
172
- const { executePublish } = await import("./studio-CsRn2J1a.mjs");
261
+ const { executePublish } = await import("./studio-sXvYUxr5.mjs");
173
262
  return executePublish({
174
263
  cwd,
175
264
  server: args.server,
@@ -227,7 +316,7 @@ const init = defineCommand({
227
316
  },
228
317
  async run({ args }) {
229
318
  await runCommand(args, async (mode) => {
230
- const { executeInit } = await import("./init-BGlOhIOl.mjs");
319
+ const { executeInit } = await import("./init-BT-IU9AR.mjs");
231
320
  return executeInit({
232
321
  dir: args.dir,
233
322
  force: args.force,
@@ -256,7 +345,7 @@ const dev = defineCommand({
256
345
  async run({ args }) {
257
346
  await runCommand(args, async () => {
258
347
  const cwd = await setup({ agent: true });
259
- const { executeDev } = await import("./dev-C5P8amSg.mjs");
348
+ const { executeDev } = await import("./dev-C4KyxouE.mjs");
260
349
  return executeDev({
261
350
  cwd,
262
351
  port: args.port
@@ -331,7 +420,7 @@ const deploy = defineCommand({
331
420
  async run({ args }) {
332
421
  await runCommand(args, async () => {
333
422
  const cwd = await setup({ agent: true });
334
- const { executeDeploy } = await import("./deploy-D-O-Q_mW.mjs");
423
+ const { executeDeploy } = await import("./deploy-Cp-wgME3.mjs");
335
424
  return executeDeploy({
336
425
  cwd,
337
426
  server: args.server,
@@ -354,7 +443,7 @@ const del = defineCommand({
354
443
  async run({ args }) {
355
444
  await runCommand(args, async () => {
356
445
  const cwd = await setup();
357
- const { executeDelete } = await import("./delete-BaHgd9cN.mjs");
446
+ const { executeDelete } = await import("./delete-DRNfvczK.mjs");
358
447
  return executeDelete({
359
448
  cwd,
360
449
  server: args.server
@@ -385,7 +474,7 @@ const secret = defineCommand({
385
474
  async run({ args }) {
386
475
  await runCommand(args, async (mode) => {
387
476
  const cwd = await setup();
388
- const { executeSecretPut, NO_INPUT, readStdin } = await import("./secret-_bHo4rqt.mjs");
477
+ const { executeSecretPut, NO_INPUT, readStdin } = await import("./secret-Dr0qnxeb.mjs");
389
478
  const value = mode === "json" ? await readStdin() : void 0;
390
479
  if (mode === "json" && !value) throw new CliError(...NO_INPUT);
391
480
  return executeSecretPut(cwd, args.name, value, args.server);
@@ -409,7 +498,7 @@ const secret = defineCommand({
409
498
  async run({ args }) {
410
499
  await runCommand(args, async () => {
411
500
  const cwd = await setup();
412
- const { executeSecretDelete } = await import("./secret-_bHo4rqt.mjs");
501
+ const { executeSecretDelete } = await import("./secret-Dr0qnxeb.mjs");
413
502
  return executeSecretDelete(cwd, args.name, args.server);
414
503
  });
415
504
  }
@@ -426,7 +515,7 @@ const secret = defineCommand({
426
515
  async run({ args }) {
427
516
  await runCommand(args, async () => {
428
517
  const cwd = await setup();
429
- const { executeSecretList } = await import("./secret-_bHo4rqt.mjs");
518
+ const { executeSecretList } = await import("./secret-Dr0qnxeb.mjs");
430
519
  return executeSecretList(cwd, args.server);
431
520
  });
432
521
  }
@@ -461,7 +550,7 @@ const storage = defineCommand({
461
550
  },
462
551
  async run({ args }) {
463
552
  await runCommand(args, async () => {
464
- const { executeStorageStatus } = await import("./storage-MWiVx_mV.mjs");
553
+ const { executeStorageStatus } = await import("./storage-CoQB8d-u.mjs");
465
554
  return executeStorageStatus(resolveStorageCwd(args.dir), args.server);
466
555
  });
467
556
  }
@@ -478,7 +567,7 @@ const storage = defineCommand({
478
567
  },
479
568
  async run({ args }) {
480
569
  await runCommand(args, async () => {
481
- const { executeStorageEnable } = await import("./storage-MWiVx_mV.mjs");
570
+ const { executeStorageEnable } = await import("./storage-CoQB8d-u.mjs");
482
571
  return executeStorageEnable(resolveStorageCwd(args.dir), args.server);
483
572
  });
484
573
  }
@@ -500,7 +589,7 @@ const storage = defineCommand({
500
589
  },
501
590
  async run({ args }) {
502
591
  await runCommand(args, async () => {
503
- const { executeStorageDisable } = await import("./storage-MWiVx_mV.mjs");
592
+ const { executeStorageDisable } = await import("./storage-CoQB8d-u.mjs");
504
593
  return executeStorageDisable(resolveStorageCwd(args.dir), {
505
594
  server: args.server,
506
595
  force: args.force
@@ -521,7 +610,7 @@ const login = defineCommand({
521
610
  },
522
611
  async run({ args }) {
523
612
  await runCommand(args, async () => {
524
- const { executeLogin } = await import("./login-B6IhXski.mjs");
613
+ const { executeLogin } = await import("./login-AA_UdRI-.mjs");
525
614
  return executeLogin({ server: args.server });
526
615
  });
527
616
  }
@@ -534,7 +623,7 @@ const templates = defineCommand({
534
623
  args: { json: sharedArgs.json },
535
624
  async run({ args }) {
536
625
  await runCommand(args, async (mode) => {
537
- const { listTemplates } = await import("./_templates-jmjgdgcd.mjs");
626
+ const { listTemplates } = await import("./_templates-Bt6u9_68.mjs");
538
627
  const names = await listTemplates();
539
628
  if (mode === "human") {
540
629
  for (const name of names) log.message(name);
@@ -586,7 +675,21 @@ if (process.env.VITEST !== "true") {
586
675
  }
587
676
  process.argv.splice(2, 0, "publish");
588
677
  };
589
- runDefault().then(() => runMain(mainCommand)).catch((err) => {
678
+ /**
679
+ * Refuse an unrecognized flag instead of ignoring it.
680
+ *
681
+ * citty drops one silently, so `aai push --serverr=http://x` exited 0 having
682
+ * pushed to the DEFAULT server. `--server` decides where the API key and
683
+ * secret values go, so a typo that quietly retargets it is worth failing on.
684
+ */
685
+ const assertKnownFlags = async () => {
686
+ const unknown = await unknownFlagsForArgv(mainCommand, process.argv.slice(2));
687
+ if (unknown.length === 0) return;
688
+ log.error(`Unknown ${unknown.length === 1 ? "option" : "options"}: ${unknown.join(", ")}`);
689
+ log.info("Run `aai <command> --help` to see the options it accepts.");
690
+ process.exit(1);
691
+ };
692
+ runDefault().then(assertKnownFlags).then(() => runMain(mainCommand)).catch((err) => {
590
693
  log.error(errorMessage(err));
591
694
  process.exitCode = 1;
592
695
  });
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as log, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { i as resolveDeployTarget, n as getServerInfo } from "./_agent-BB1ZaZsX.mjs";
4
- import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-B6_uvuLs.mjs";
3
+ import { l as writeProjectConfig } from "./_config-Y5V-5Krn.mjs";
4
+ import { i as resolveDeployTarget, n as getServerInfo } from "./_agent-2nVugrN3.mjs";
5
+ import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-B-upMGkc.mjs";
5
6
  //#region delete.ts
6
7
  async function runDelete(opts) {
7
8
  await apiRequest(`${opts.url}/${opts.slug}`, {
@@ -32,6 +33,7 @@ async function executeDelete(opts) {
32
33
  hints: { 404: "Run `aai list` to see your projects." }
33
34
  });
34
35
  log.success(`Deleted ${project}`);
36
+ await writeProjectConfig(cwd, { serverUrl });
35
37
  return ok({
36
38
  project,
37
39
  ...config.slug ? { slug: config.slug } : {}
@@ -2,11 +2,11 @@
2
2
  import { n as log, t as fmtUrl, u as ok } from "./_ui-8kOEB-JH.mjs";
3
3
  import { a as errorMessage } from "./_utils-Ch0J4s6a.mjs";
4
4
  import { t as buildAgentBundle } from "./_bundler-Cjaxa2wi.mjs";
5
- import { s as updateProjectConfig } from "./_config-PIC0nzRu.mjs";
5
+ import { s as updateProjectConfig } from "./_config-Y5V-5Krn.mjs";
6
6
  import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
7
- import { i as resolveDeployTarget } from "./_agent-BB1ZaZsX.mjs";
7
+ import { i as resolveDeployTarget } from "./_agent-2nVugrN3.mjs";
8
8
  import { assertTypechecks } from "./_typecheck-gate-DvE8S3aQ.mjs";
9
- import { n as apiRequest } from "./_api-client-B6_uvuLs.mjs";
9
+ import { n as apiRequest } from "./_api-client-B-upMGkc.mjs";
10
10
  import { gzipSync } from "node:zlib";
11
11
  //#region _deploy.ts
12
12
  async function runDeploy(opts) {
@@ -11,7 +11,7 @@ import { styleText } from "node:util";
11
11
  async function executeDev(opts) {
12
12
  const port = parsePort(opts.port);
13
13
  const agentName = path.basename(path.resolve(opts.cwd));
14
- const { startDevServer } = await import("./_dev-server-CGptLIAm.mjs");
14
+ const { startDevServer } = await import("./_dev-server-BB5N8kdh.mjs");
15
15
  let cleanup;
16
16
  let shuttingDown = false;
17
17
  const onSignal = () => {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as unwrapCancel, n as log, u as ok } from "./_ui-8kOEB-JH.mjs";
3
3
  import { a as errorMessage, c as readJson, l as resolveCwd, o as fileExists, t as AGENT_ENTRY } from "./_utils-Ch0J4s6a.mjs";
4
- import { r as isDevMode, t as getMonorepoRoot } from "./_agent-BB1ZaZsX.mjs";
4
+ import { r as isDevMode, t as getMonorepoRoot } from "./_agent-2nVugrN3.mjs";
5
5
  import path from "node:path";
6
6
  import { styleText } from "node:util";
7
7
  import * as p from "@clack/prompts";
@@ -74,7 +74,7 @@ function resolveTargetDir(dir) {
74
74
  }
75
75
  /** Publish after init and return deploy metadata if successful. */
76
76
  async function tryPublish(cwd, server) {
77
- const { executePublish } = await import("./studio-CsRn2J1a.mjs");
77
+ const { executePublish } = await import("./studio-sXvYUxr5.mjs");
78
78
  try {
79
79
  const result = await executePublish({
80
80
  cwd,
@@ -93,7 +93,7 @@ async function tryPublish(cwd, server) {
93
93
  }
94
94
  /** Scaffold the project, optionally showing a spinner. */
95
95
  async function scaffoldProject(dir, cwd, template, silent) {
96
- const { runInit } = await import("./_init-BPFM4uBH.mjs");
96
+ const { runInit } = await import("./_init-D7JIT-IJ.mjs");
97
97
  const s = silent ? void 0 : p.spinner();
98
98
  s?.start(`Creating ${dir}`);
99
99
  await runInit({
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as log, o as CliError, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { c as writeGlobalConfig, i as readGlobalConfig, r as getConfigDir, t as approveServer } from "./_config-PIC0nzRu.mjs";
4
- import { a as resolveServerUrl } from "./_agent-BB1ZaZsX.mjs";
3
+ import { c as writeGlobalConfig, i as readGlobalConfig, r as getConfigDir, t as approveServer } from "./_config-Y5V-5Krn.mjs";
4
+ import { a as resolveServerUrl } from "./_agent-2nVugrN3.mjs";
5
5
  import { spawn } from "node:child_process";
6
6
  import { setTimeout } from "node:timers/promises";
7
7
  import { randomBytes } from "node:crypto";
@@ -35,6 +35,73 @@ async function jsonBody(res, what) {
35
35
  if (body === null) throw new CliError("login_failed", `${what} returned an invalid response`);
36
36
  return body;
37
37
  }
38
+ /**
39
+ * Fetch, turning a transport failure into an error that names the server.
40
+ *
41
+ * A connection failure here is undici's bare `TypeError: fetch failed` — no
42
+ * URL, no cause worth printing — and `aai login` is exactly where it is least
43
+ * diagnosable: the target is resolved (dev mode pins `localhost:8080`
44
+ * whenever the CLI itself lives in the monorepo, whatever the cwd), so the
45
+ * user cannot tell which server was unreachable, or that a local one was
46
+ * expected at all. `apiRequest` has always said "could not reach <url>";
47
+ * login used raw fetch and said nothing.
48
+ */
49
+ async function reachable(fetchFn, url, serverUrl) {
50
+ try {
51
+ return await fetchFn(url);
52
+ } catch (err) {
53
+ throw unreachableError(serverUrl, err);
54
+ }
55
+ }
56
+ /**
57
+ * "Could not reach <server>", with advice chosen by the kind of host and the
58
+ * original transport failure preserved as the cause.
59
+ */
60
+ function unreachableError(serverUrl, cause) {
61
+ const hint = isLoopback(serverUrl) ? "That's a local server — start it with `pnpm dev:aai-server`, or pass `--server <url>` to log in elsewhere. (A CLI installed from the monorepo targets localhost by default; set AAI_NO_DEV=1 to use the hosted platform.)" : "Check your network connection and verify the server URL is correct.";
62
+ return new CliError("login_unreachable", `Could not reach ${serverUrl}.`, hint, { cause });
63
+ }
64
+ /** Whether `url`'s host is loopback — used only to pick a better hint. */
65
+ function isLoopback(url) {
66
+ try {
67
+ const { hostname } = new URL(url);
68
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
69
+ } catch {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Poll the exchange until the browser approves the link, or the deadline.
75
+ *
76
+ * A transport failure is retried rather than fatal: the user is off in a
77
+ * browser approving, and a dev server reloading in that window would
78
+ * otherwise lose a login that was about to succeed. The last one is
79
+ * remembered, so a server that never comes back is reported as unreachable
80
+ * rather than as "you didn't approve in time" — which would blame the user
81
+ * for someone else's outage.
82
+ */
83
+ async function pollForGrant(fetchFn, serverUrl, code, opts) {
84
+ let lastTransportError;
85
+ for (;;) {
86
+ let res = null;
87
+ try {
88
+ res = await fetchFn(`${serverUrl}/studio/cli-link/exchange`, {
89
+ method: "POST",
90
+ headers: { "Content-Type": "application/json" },
91
+ body: JSON.stringify({ code })
92
+ });
93
+ lastTransportError = void 0;
94
+ } catch (err) {
95
+ lastTransportError = err;
96
+ }
97
+ if (res && res.status !== 404) return await jsonBody(res, "Linking your account");
98
+ if (Date.now() >= opts.deadline) {
99
+ if (lastTransportError !== void 0) throw unreachableError(serverUrl, lastTransportError);
100
+ throw new CliError("login_timeout", "Timed out waiting for the link to be approved in the browser.", "Run `aai login` again and approve the link within five minutes.");
101
+ }
102
+ await setTimeout(opts.intervalMs);
103
+ }
104
+ }
38
105
  function requireTty() {
39
106
  if (!process.stdin.isTTY) throw new CliError("login_interactive", "`aai login` is interactive and needs a TTY.", "Non-interactive setups can set the ASSEMBLYAI_API_KEY environment variable instead.");
40
107
  }
@@ -77,29 +144,17 @@ async function executeLogin(opts, deps = {}) {
77
144
  const globalConfig = await readGlobalConfig();
78
145
  const serverUrl = resolveServerUrl(opts.server, void 0, globalConfig.approvedServers ?? []);
79
146
  if (opts.server) await approveServer(serverUrl);
80
- if ((await jsonBody(await fetchFn(`${serverUrl}/studio/auth`), "Reading the server's login configuration")).mode === "none") throw new CliError("login_unavailable", "This server has no browser login configured, so there is no account to link.", "Set the ASSEMBLYAI_API_KEY environment variable, or run any platform command to be prompted for a key.");
147
+ if ((await jsonBody(await reachable(fetchFn, `${serverUrl}/studio/auth`, serverUrl), "Reading the server's login configuration")).mode === "none") throw new CliError("login_unavailable", "This server has no browser login configured, so there is no account to link.", "Set the ASSEMBLYAI_API_KEY environment variable instead it's the only other way to authenticate.");
81
148
  const code = randomBytes(32).toString("base64url");
82
149
  const linkUrl = `${serverUrl}/?cli-link=${code}`;
83
150
  log.info(`Opening the browser to link your account…\n ${linkUrl}`);
84
151
  log.info(`Confirmation code: ${linkConfirmationCode(code)}`);
85
152
  log.info("Approve the link in the browser (sign in there first if you need to) — the approval page shows the same code.");
86
153
  (deps.openBrowser ?? defaultOpenBrowser)(linkUrl);
87
- const pollInterval = deps.pollIntervalMs ?? LINK_POLL_INTERVAL_MS;
88
- const deadline = Date.now() + (deps.timeoutMs ?? LINK_TIMEOUT_MS);
89
- let granted;
90
- for (;;) {
91
- const res = await fetchFn(`${serverUrl}/studio/cli-link/exchange`, {
92
- method: "POST",
93
- headers: { "Content-Type": "application/json" },
94
- body: JSON.stringify({ code })
95
- });
96
- if (res.status !== 404) {
97
- granted = await jsonBody(res, "Linking your account");
98
- break;
99
- }
100
- if (Date.now() >= deadline) throw new CliError("login_timeout", "Timed out waiting for the link to be approved in the browser.", "Run `aai login` again and approve the link within five minutes.");
101
- await setTimeout(pollInterval);
102
- }
154
+ const granted = await pollForGrant(fetchFn, serverUrl, code, {
155
+ intervalMs: deps.pollIntervalMs ?? LINK_POLL_INTERVAL_MS,
156
+ deadline: Date.now() + (deps.timeoutMs ?? LINK_TIMEOUT_MS)
157
+ });
103
158
  if (!granted.apiKey) throw new CliError("login_failed", "Linking your account did not return an API key.");
104
159
  const dir = getConfigDir();
105
160
  await writeGlobalConfig(dir, {
@@ -11,15 +11,15 @@
11
11
  "publish:agent": "aai publish"
12
12
  },
13
13
  "dependencies": {
14
- "@alexkroman1/aai": "^5.6.0",
15
- "@alexkroman1/aai-ui": "^5.6.0",
14
+ "@alexkroman1/aai": "^5.7.0",
15
+ "@alexkroman1/aai-ui": "^5.7.0",
16
16
  "react": "^19.2.8",
17
17
  "react-dom": "^19.2.8",
18
18
  "tailwindcss": "^4.0.0",
19
19
  "zod": "^4.4.3"
20
20
  },
21
21
  "devDependencies": {
22
- "@alexkroman1/aai-cli": "^5.6.0",
22
+ "@alexkroman1/aai-cli": "^5.7.0",
23
23
  "@tailwindcss/vite": "^4.3.3",
24
24
  "@types/node": "^26.1.1",
25
25
  "@types/react": "^19.2.17",
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { a as unwrapCancel, n as log, s as fail, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { t as slugRequest } from "./_slug-api-dcaQ_Uho.mjs";
3
+ import { t as slugRequest } from "./_slug-api-CGJSST9B.mjs";
4
4
  import * as p from "@clack/prompts";
5
5
  import { text } from "node:stream/consumers";
6
6
  //#region secret.ts
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as log, s as fail, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { t as slugRequest } from "./_slug-api-dcaQ_Uho.mjs";
3
+ import { t as slugRequest } from "./_slug-api-CGJSST9B.mjs";
4
4
  import * as p from "@clack/prompts";
5
5
  //#region storage.ts
6
6
  async function storageRequest(cwd, init, server) {
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { n as log, o as CliError, t as fmtUrl, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { s as updateProjectConfig } from "./_config-PIC0nzRu.mjs";
3
+ import { s as updateProjectConfig } from "./_config-Y5V-5Krn.mjs";
4
4
  import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.mjs";
5
- import { i as resolveDeployTarget } from "./_agent-BB1ZaZsX.mjs";
6
- import { layerScaffold } from "./_templates-jmjgdgcd.mjs";
7
- import { n as apiRequest } from "./_api-client-B6_uvuLs.mjs";
5
+ import { i as resolveDeployTarget } from "./_agent-2nVugrN3.mjs";
6
+ import { layerScaffold } from "./_templates-Bt6u9_68.mjs";
7
+ import { n as apiRequest } from "./_api-client-B-upMGkc.mjs";
8
8
  import path from "node:path";
9
9
  import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
10
- import { VALID_SLUG_RE } from "@alexkroman1/aai/utils";
10
+ import { PREVIEW_SLUG_SUFFIX, VALID_SLUG_RE } from "@alexkroman1/aai/utils";
11
11
  //#region _studio.ts
12
12
  /**
13
13
  * Internals of the studio-workspace commands (`aai list/pull/push/publish`):
@@ -60,10 +60,32 @@ async function walkProject(dir, current = dir) {
60
60
  return out.sort((a, b) => a.localeCompare(b));
61
61
  }
62
62
  /**
63
+ * Decode `buf` as UTF-8, or null when it isn't valid UTF-8.
64
+ *
65
+ * `fatal` makes an invalid sequence throw instead of becoming U+FFFD, which
66
+ * is the whole point: a workspace is a JSON path→string map and cannot carry
67
+ * arbitrary bytes, so a lossy read turned a pushed PNG into replacement
68
+ * characters while reporting success — and a later `aai pull` wrote the
69
+ * mangled version back over the local original. `ignoreBOM` keeps a leading
70
+ * U+FEFF in the string; without it the decoder strips the BOM and the check
71
+ * meant to stop corruption would quietly perform some of its own.
72
+ */
73
+ const UTF8_STRICT = new TextDecoder("utf-8", {
74
+ fatal: true,
75
+ ignoreBOM: true
76
+ });
77
+ function decodeUtf8(buf) {
78
+ try {
79
+ return UTF8_STRICT.decode(buf);
80
+ } catch {
81
+ return null;
82
+ }
83
+ }
84
+ /**
63
85
  * Walk a local project into the path→content record a workspace stores —
64
86
  * the CLI-side twin of the guest's `snapshotWorkspace`: same ignored
65
- * directories, same caps, oversized files skipped with a warning rather
66
- * than failing the whole push.
87
+ * directories, same caps, oversized and non-text files skipped with a
88
+ * warning rather than failing the whole push.
67
89
  */
68
90
  async function collectSourceFiles(dir) {
69
91
  const paths = await walkProject(dir);
@@ -77,16 +99,31 @@ async function collectSourceFiles(dir) {
77
99
  warnings.push(`${rel} is ${st.size} bytes (max ${MAX_STUDIO_FILE_BYTES}) — not synced.`);
78
100
  continue;
79
101
  }
80
- files[rel.split(path.sep).join("/")] = await readFile(abs, "utf-8");
102
+ const content = decodeUtf8(await readFile(abs));
103
+ if (content === null) {
104
+ warnings.push(`${rel} is not valid UTF-8 (binary file?) — not synced.`);
105
+ continue;
106
+ }
107
+ files[rel.split(path.sep).join("/")] = content;
81
108
  }
82
109
  return {
83
110
  files,
84
111
  warnings
85
112
  };
86
113
  }
87
- /** A studio project name derived from a directory name, or null if unusable. */
114
+ /**
115
+ * A studio project name derived from a directory name, or null if unusable.
116
+ *
117
+ * A `-preview` suffix is deliberately unusable. Publishing a project deploys
118
+ * it under the project's own name, so a `*-preview` project would claim a
119
+ * slug the studio's orphan-preview sweep reaps hourly — deleting the agent,
120
+ * its app-database schema, and its secrets on a schedule the user never
121
+ * asked for. Refusing the name is recoverable (rename the directory); losing
122
+ * a published agent to the reaper is not.
123
+ */
88
124
  function projectNameFromDir(dir) {
89
125
  const name = path.basename(dir).toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-_]+|[-_]+$/g, "").slice(0, 64);
126
+ if (name.endsWith(PREVIEW_SLUG_SUFFIX)) return null;
90
127
  return VALID_SLUG_RE.test(name) ? name : null;
91
128
  }
92
129
  /** The shareable studio URL for a project — what every command prints. */
@@ -217,7 +254,8 @@ async function pushProject(opts) {
217
254
  created: result.created,
218
255
  serverUrl,
219
256
  apiKey,
220
- slug
257
+ slug,
258
+ warnings
221
259
  };
222
260
  }
223
261
  async function executePush(opts) {
@@ -227,7 +265,8 @@ async function executePush(opts) {
227
265
  return ok({
228
266
  project: pushed.project,
229
267
  created: pushed.created,
230
- url
268
+ url,
269
+ ...pushed.warnings.length > 0 ? { warnings: pushed.warnings } : {}
231
270
  });
232
271
  }
233
272
  /**
@@ -260,6 +299,7 @@ async function executePublish(opts) {
260
299
  if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, pushed.slug);
261
300
  log.step(`Publishing ${project} (builds in the project's sandbox)…`);
262
301
  const result = await publishStudioProject(serverUrl, apiKey, project);
302
+ 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.");
263
303
  if (result.output.trim()) log.message(result.output.trim());
264
304
  await updateProjectConfig(opts.cwd, {
265
305
  serverUrl,
@@ -277,7 +317,8 @@ async function executePublish(opts) {
277
317
  slug: result.slug,
278
318
  url: agentUrl,
279
319
  studioUrl,
280
- output: result.output
320
+ output: result.output,
321
+ ...pushed.warnings.length > 0 ? { warnings: pushed.warnings } : {}
281
322
  });
282
323
  }
283
324
  //#endregion
package/dist/studio.d.ts CHANGED
@@ -37,6 +37,7 @@ export declare function executePush(opts: {
37
37
  project: string;
38
38
  created: boolean;
39
39
  url: string;
40
+ warnings?: string[];
40
41
  }>>;
41
42
  export declare function executePublish(opts: {
42
43
  cwd: string;
@@ -49,4 +50,5 @@ export declare function executePublish(opts: {
49
50
  url: string;
50
51
  studioUrl: string;
51
52
  output: string;
53
+ warnings?: string[];
52
54
  }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "5.6.0",
3
+ "version": "5.7.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "aai": "dist/cli.mjs"
@@ -37,8 +37,8 @@
37
37
  "p-timeout": "^7.0.1",
38
38
  "vite": "^8.1.5",
39
39
  "zod": "^4.4.3",
40
- "@alexkroman1/aai": "5.6.0",
41
- "@alexkroman1/aai-ui": "5.6.0"
40
+ "@alexkroman1/aai": "5.7.0",
41
+ "@alexkroman1/aai-ui": "5.7.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "playwright": "^1.61.1",