@alexkroman1/aai-cli 5.5.1 → 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.
Files changed (36) hide show
  1. package/README.md +16 -7
  2. package/dist/{_agent-DMyOab9_.mjs → _agent-2nVugrN3.mjs} +22 -3
  3. package/dist/_agent.d.ts +3 -1
  4. package/dist/_api-client-B-upMGkc.mjs +104 -0
  5. package/dist/_api-client.d.ts +16 -0
  6. package/dist/_cli-common.d.ts +65 -0
  7. package/dist/{_config-5AEqhh-O.mjs → _config-Y5V-5Krn.mjs} +58 -9
  8. package/dist/_config.d.ts +23 -1
  9. package/dist/_deploy.d.ts +7 -0
  10. package/dist/{_dev-server-vV05Fnki.mjs → _dev-server-BB5N8kdh.mjs} +2 -2
  11. package/dist/{_init-BZ9t_Kz-.mjs → _init-D7JIT-IJ.mjs} +3 -3
  12. package/dist/{_slug-api-DaqQJHk8.mjs → _slug-api-CGJSST9B.mjs} +2 -2
  13. package/dist/_studio-commands.d.ts +73 -0
  14. package/dist/_studio.d.ts +64 -0
  15. package/dist/{_templates-Bv8CR800.mjs → _templates-Bt6u9_68.mjs} +19 -8
  16. package/dist/_templates.d.ts +9 -0
  17. package/dist/{_typecheck-gate-9IHWDnl1.mjs → _typecheck-gate-DvE8S3aQ.mjs} +1 -1
  18. package/dist/{build-D_PgQOD4.mjs → build-BXwDB78d.mjs} +1 -1
  19. package/dist/cli.mjs +264 -32
  20. package/dist/delete-DRNfvczK.mjs +53 -0
  21. package/dist/delete.d.ts +9 -2
  22. package/dist/{deploy-1eaXcfUw.mjs → deploy-Cp-wgME3.mjs} +7 -5
  23. package/dist/deploy.d.ts +2 -0
  24. package/dist/{dev-gVNdGFYY.mjs → dev-C4KyxouE.mjs} +1 -1
  25. package/dist/{init-DoU4_txp.mjs → init-BT-IU9AR.mjs} +15 -14
  26. package/dist/login-AA_UdRI-.mjs +172 -0
  27. package/dist/login.d.ts +33 -20
  28. package/dist/scaffold/package.json +4 -4
  29. package/dist/{secret-CGAIAbUx.mjs → secret-Dr0qnxeb.mjs} +1 -1
  30. package/dist/{storage-CnhOayhm.mjs → storage-CoQB8d-u.mjs} +1 -1
  31. package/dist/studio-sXvYUxr5.mjs +325 -0
  32. package/dist/studio.d.ts +54 -0
  33. package/package.json +4 -4
  34. package/dist/_api-client-MenP4-O7.mjs +0 -49
  35. package/dist/delete-DXilFBb1.mjs +0 -29
  36. package/dist/login-C59ZHzuO.mjs +0 -109
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
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-Y5V-5Krn.mjs";
4
+ import { t as resolveServerEnv } from "./_server-common-CnaP_Urf.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
+ import path from "node:path";
9
+ import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
10
+ import { PREVIEW_SLUG_SUFFIX, VALID_SLUG_RE } from "@alexkroman1/aai/utils";
11
+ //#region _studio.ts
12
+ /**
13
+ * Internals of the studio-workspace commands (`aai list/pull/push/publish`):
14
+ * the local source-file walk and the thin clients for the platform's
15
+ * `/studio/projects` routes. The workspace is the single source of truth —
16
+ * these helpers only move files between a local directory and the project's
17
+ * workspace row; production deploys happen exclusively through the studio's
18
+ * Publish route (which runs the deploy machinery in the project's sandbox).
19
+ */
20
+ /**
21
+ * Mirrors of the studio workspace caps (`aai-studio-server/studio-limits.ts`)
22
+ * — by value, like `aai-guest/limits.ts`, since the CLI cannot depend on the
23
+ * private server package. The server re-validates every push; these exist so
24
+ * an oversized file is a named warning locally instead of a rejected upload.
25
+ */
26
+ const MAX_STUDIO_FILE_BYTES = 256e3;
27
+ /**
28
+ * What never syncs to a workspace — the same set the guest's snapshot skips
29
+ * (`aai-guest/studio-workspace-fs.ts`), plus what only exists locally:
30
+ * secrets (`.env` rides the secret routes, never a workspace row) and
31
+ * lockfiles (regenerated by install, noise in the studio editor).
32
+ */
33
+ const IGNORED_DIRS = /* @__PURE__ */ new Set([
34
+ "node_modules",
35
+ ".git",
36
+ "dist",
37
+ ".aai"
38
+ ]);
39
+ const IGNORED_FILES = [
40
+ /^\.env(\..+)?$/,
41
+ /^pnpm-lock\.yaml$/,
42
+ /^package-lock\.json$/,
43
+ /^yarn\.lock$/,
44
+ /^\.DS_Store$/
45
+ ];
46
+ function isIgnoredFile(name) {
47
+ return IGNORED_FILES.some((re) => re.test(name));
48
+ }
49
+ /** Project-relative paths of every syncable file under `dir`, sorted. */
50
+ async function walkProject(dir, current = dir) {
51
+ const out = [];
52
+ const entries = await readdir(current, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ if (entry.isSymbolicLink()) continue;
55
+ const abs = path.join(current, entry.name);
56
+ if (entry.isDirectory()) {
57
+ if (!IGNORED_DIRS.has(entry.name)) out.push(...await walkProject(dir, abs));
58
+ } else if (entry.isFile() && !isIgnoredFile(entry.name)) out.push(path.relative(dir, abs));
59
+ }
60
+ return out.sort((a, b) => a.localeCompare(b));
61
+ }
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
+ /**
85
+ * Walk a local project into the path→content record a workspace stores —
86
+ * the CLI-side twin of the guest's `snapshotWorkspace`: same ignored
87
+ * directories, same caps, oversized and non-text files skipped with a
88
+ * warning rather than failing the whole push.
89
+ */
90
+ async function collectSourceFiles(dir) {
91
+ const paths = await walkProject(dir);
92
+ const files = {};
93
+ const warnings = [];
94
+ if (paths.length > 100) warnings.push(`Project has ${paths.length} files; only the first 100 sync to the studio.`);
95
+ for (const rel of paths.slice(0, 100)) {
96
+ const abs = path.join(dir, rel);
97
+ const st = await stat(abs);
98
+ if (st.size > 256e3) {
99
+ warnings.push(`${rel} is ${st.size} bytes (max ${MAX_STUDIO_FILE_BYTES}) — not synced.`);
100
+ continue;
101
+ }
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;
108
+ }
109
+ return {
110
+ files,
111
+ warnings
112
+ };
113
+ }
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
+ */
124
+ function projectNameFromDir(dir) {
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;
127
+ return VALID_SLUG_RE.test(name) ? name : null;
128
+ }
129
+ /** The shareable studio URL for a project — what every command prints. */
130
+ function studioProjectUrl(serverUrl, project) {
131
+ return `${serverUrl}/studio/chat/${project}`;
132
+ }
133
+ function listStudioProjects(serverUrl, apiKey) {
134
+ return apiRequest(`${serverUrl}/studio/projects`, {
135
+ apiKey,
136
+ action: "list"
137
+ }).then((res) => res.projects);
138
+ }
139
+ /** Fetch a project, or null when it doesn't exist (the push existence probe). */
140
+ function fetchStudioProject(serverUrl, apiKey, project) {
141
+ return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}`, {
142
+ apiKey,
143
+ action: "pull",
144
+ allow404: true
145
+ });
146
+ }
147
+ /** `PUT /studio/projects/:project/source` — the atomic whole-tree push. */
148
+ function pushStudioSource(serverUrl, apiKey, project, body) {
149
+ return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/source`, {
150
+ apiKey,
151
+ action: "push",
152
+ method: "PUT",
153
+ body,
154
+ hints: { 409: "The studio has newer changes. Run `aai pull` to fetch them, or `aai push --force` to overwrite." }
155
+ });
156
+ }
157
+ /** `POST /studio/projects/:project/deploy` — Publish, in the project's sandbox. */
158
+ function publishStudioProject(serverUrl, apiKey, project) {
159
+ return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/deploy`, {
160
+ apiKey,
161
+ action: "publish",
162
+ method: "POST",
163
+ retry: 0
164
+ });
165
+ }
166
+ //#endregion
167
+ //#region studio.ts
168
+ /**
169
+ * The studio-workspace commands: `aai list`, `aai pull`, `aai push`,
170
+ * `aai publish`.
171
+ *
172
+ * One model: a studio project's workspace and a local project directory are
173
+ * the same file tree. `pull` materializes the workspace locally (completing
174
+ * it into a runnable project with the scaffold), `push` replaces the
175
+ * workspace with the local tree (fast-forward-checked, so studio edits are
176
+ * never silently overwritten), and `publish` pushes then ships the
177
+ * workspace to production through the studio's Publish route — the same
178
+ * in-sandbox deploy the Publish button runs. There is no other path to
179
+ * production.
180
+ */
181
+ async function executeList(opts) {
182
+ const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
183
+ const projects = await listStudioProjects(serverUrl, apiKey);
184
+ if (projects.length === 0) log.info("No studio projects yet. Push one with `aai push`, or create one in the studio.");
185
+ for (const name of projects) log.message(`${name} ${fmtUrl(studioProjectUrl(serverUrl, name))}`);
186
+ return ok({ projects });
187
+ }
188
+ /** Write a pulled file map under `dir`, refusing paths that escape it. */
189
+ async function materializeFiles(dir, files) {
190
+ for (const [rel, content] of Object.entries(files)) {
191
+ const abs = path.resolve(dir, rel);
192
+ if (abs !== dir && !abs.startsWith(dir + path.sep)) throw new Error(`Pulled file path escapes the project directory: ${rel}`);
193
+ await mkdir(path.dirname(abs), { recursive: true });
194
+ await writeFile(abs, content, "utf-8");
195
+ }
196
+ }
197
+ async function executePull(opts) {
198
+ const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
199
+ const remote = await fetchStudioProject(serverUrl, apiKey, opts.project);
200
+ if (!remote) throw new CliError("not_found", `No studio project named "${opts.project}".`, "Run `aai list` to see your projects.");
201
+ const target = path.resolve(opts.cwd, opts.dir ?? opts.project);
202
+ 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.");
203
+ await materializeFiles(target, remote.files);
204
+ await layerScaffold(target);
205
+ await updateProjectConfig(target, {
206
+ serverUrl,
207
+ studioProject: opts.project,
208
+ studioSourceHash: remote.sourceHash,
209
+ ...remote.deployedSlug ? { slug: remote.deployedSlug } : {}
210
+ });
211
+ const count = Object.keys(remote.files).length;
212
+ log.success(`Pulled ${opts.project} (${count} files) into ${target}`);
213
+ log.info(`Next: cd ${opts.dir ?? opts.project} && pnpm install && aai dev`);
214
+ log.info(`Studio: ${fmtUrl(studioProjectUrl(serverUrl, opts.project))}`);
215
+ return ok({
216
+ project: opts.project,
217
+ dir: target,
218
+ files: count
219
+ });
220
+ }
221
+ /**
222
+ * The shared push core: collect local source, resolve (or mint) the linked
223
+ * project, sync atomically, record the new fast-forward token.
224
+ */
225
+ async function pushProject(opts) {
226
+ const { config, serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
227
+ const { files, warnings } = await collectSourceFiles(opts.cwd);
228
+ for (const warning of warnings) log.warn(warning);
229
+ if (Object.keys(files).length === 0) throw new Error("Nothing to push — this directory has no project files.");
230
+ let project = config?.studioProject;
231
+ let baseHash = config?.studioSourceHash;
232
+ let slug = config?.slug;
233
+ if (!project) {
234
+ project = slug ?? projectNameFromDir(opts.cwd) ?? void 0;
235
+ if (!project) throw new Error(`Can't derive a project name from ${path.basename(opts.cwd)} — rename the directory or run \`aai pull <project>\` to link an existing one.`);
236
+ const existing = await fetchStudioProject(serverUrl, apiKey, project);
237
+ if (existing && !opts.force) throw new CliError("project_exists", `Your studio already has a project named "${project}".`, `Run \`aai pull ${project}\` to link this directory to it, or \`aai push --force\` to overwrite it.`);
238
+ baseHash = existing?.sourceHash;
239
+ slug ??= existing?.deployedSlug;
240
+ }
241
+ const result = await pushStudioSource(serverUrl, apiKey, project, {
242
+ files,
243
+ ...opts.force ? {} : { baseHash }
244
+ });
245
+ await updateProjectConfig(opts.cwd, {
246
+ serverUrl,
247
+ studioProject: project,
248
+ studioSourceHash: result.sourceHash,
249
+ ...slug ? { slug } : {}
250
+ });
251
+ return {
252
+ project,
253
+ sourceHash: result.sourceHash,
254
+ created: result.created,
255
+ serverUrl,
256
+ apiKey,
257
+ slug,
258
+ warnings
259
+ };
260
+ }
261
+ async function executePush(opts) {
262
+ const pushed = await pushProject(opts);
263
+ const url = studioProjectUrl(pushed.serverUrl, pushed.project);
264
+ log.success(`${pushed.created ? "Created" : "Synced"} studio project ${pushed.project} — ${fmtUrl(url)}`);
265
+ return ok({
266
+ project: pushed.project,
267
+ created: pushed.created,
268
+ url,
269
+ ...pushed.warnings.length > 0 ? { warnings: pushed.warnings } : {}
270
+ });
271
+ }
272
+ /**
273
+ * Mirror `.env` into the deployed agent's secrets (the same `/:slug/secret`
274
+ * routes `aai secret` uses). Secrets are merged into the agent env at
275
+ * deploy time, which is why publish syncs them BEFORE deploying when the
276
+ * slug is already known.
277
+ */
278
+ async function syncEnvSecrets(cwd, serverUrl, apiKey, slug) {
279
+ const env = await resolveServerEnv(cwd);
280
+ const names = Object.keys(env);
281
+ if (names.length === 0) return [];
282
+ await apiRequest(`${serverUrl}/${slug}/secret`, {
283
+ apiKey,
284
+ action: "secret",
285
+ method: "PUT",
286
+ body: env
287
+ });
288
+ log.info(`Synced ${names.length} secret${names.length === 1 ? "" : "s"} from .env`);
289
+ return names;
290
+ }
291
+ async function executePublish(opts) {
292
+ if (!opts.skipTypecheck) {
293
+ const { assertTypechecks } = await import("./_typecheck-gate-DvE8S3aQ.mjs");
294
+ await assertTypechecks(opts.cwd);
295
+ }
296
+ const pushed = await pushProject(opts);
297
+ const { project, serverUrl, apiKey } = pushed;
298
+ const hadSlug = pushed.slug !== void 0;
299
+ if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, pushed.slug);
300
+ log.step(`Publishing ${project} (builds in the project's sandbox)…`);
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.");
303
+ if (result.output.trim()) log.message(result.output.trim());
304
+ await updateProjectConfig(opts.cwd, {
305
+ serverUrl,
306
+ slug: result.slug
307
+ });
308
+ if (!hadSlug) {
309
+ if ((await syncEnvSecrets(opts.cwd, serverUrl, apiKey, result.slug)).length > 0) log.info("They apply on the next `aai publish`.");
310
+ }
311
+ const agentUrl = `${serverUrl}/${result.slug}`;
312
+ const studioUrl = studioProjectUrl(serverUrl, project);
313
+ log.success(`Published ${fmtUrl(agentUrl)}`);
314
+ log.info(`Edit in studio: ${fmtUrl(studioUrl)}`);
315
+ return ok({
316
+ project,
317
+ slug: result.slug,
318
+ url: agentUrl,
319
+ studioUrl,
320
+ output: result.output,
321
+ ...pushed.warnings.length > 0 ? { warnings: pushed.warnings } : {}
322
+ });
323
+ }
324
+ //#endregion
325
+ export { executeList, executePublish, executePull, executePush };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The studio-workspace commands: `aai list`, `aai pull`, `aai push`,
3
+ * `aai publish`.
4
+ *
5
+ * One model: a studio project's workspace and a local project directory are
6
+ * the same file tree. `pull` materializes the workspace locally (completing
7
+ * it into a runnable project with the scaffold), `push` replaces the
8
+ * workspace with the local tree (fast-forward-checked, so studio edits are
9
+ * never silently overwritten), and `publish` pushes then ships the
10
+ * workspace to production through the studio's Publish route — the same
11
+ * in-sandbox deploy the Publish button runs. There is no other path to
12
+ * production.
13
+ */
14
+ import { type CommandResult } from "./_output.ts";
15
+ export declare function executeList(opts: {
16
+ cwd: string;
17
+ server?: string | undefined;
18
+ }): Promise<CommandResult<{
19
+ projects: string[];
20
+ }>>;
21
+ export declare function executePull(opts: {
22
+ cwd: string;
23
+ project: string;
24
+ dir?: string | undefined;
25
+ force?: boolean | undefined;
26
+ server?: string | undefined;
27
+ }): Promise<CommandResult<{
28
+ project: string;
29
+ dir: string;
30
+ files: number;
31
+ }>>;
32
+ export declare function executePush(opts: {
33
+ cwd: string;
34
+ server?: string | undefined;
35
+ force?: boolean | undefined;
36
+ }): Promise<CommandResult<{
37
+ project: string;
38
+ created: boolean;
39
+ url: string;
40
+ warnings?: string[];
41
+ }>>;
42
+ export declare function executePublish(opts: {
43
+ cwd: string;
44
+ server?: string | undefined;
45
+ force?: boolean | undefined;
46
+ skipTypecheck?: boolean | undefined;
47
+ }): Promise<CommandResult<{
48
+ project: string;
49
+ slug: string;
50
+ url: string;
51
+ studioUrl: string;
52
+ output: string;
53
+ warnings?: string[];
54
+ }>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexkroman1/aai-cli",
3
- "version": "5.5.1",
3
+ "version": "5.7.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "aai": "dist/cli.mjs"
@@ -37,15 +37,15 @@
37
37
  "p-timeout": "^7.0.1",
38
38
  "vite": "^8.1.5",
39
39
  "zod": "^4.4.3",
40
- "@alexkroman1/aai": "5.5.1",
41
- "@alexkroman1/aai-ui": "5.5.1"
40
+ "@alexkroman1/aai": "5.7.0",
41
+ "@alexkroman1/aai-ui": "5.7.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "playwright": "^1.61.1",
45
45
  "tsdown": "^0.22.13",
46
46
  "verdaccio": "^6.8.0",
47
47
  "vitest": "^4.1.10",
48
- "aai-templates": "0.3.3"
48
+ "aai-templates": "0.3.4"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "vitest": "^4.1.10"
@@ -1,49 +0,0 @@
1
- #!/usr/bin/env node
2
- import { FetchError, ofetch } from "ofetch";
3
- //#region _api-client.ts
4
- /**
5
- * Shared HTTP helper for platform API calls (deploy, delete, secrets).
6
- *
7
- * Built on ofetch: JSON bodies are serialized (with Content-Type set) and
8
- * responses parsed automatically, and transient failures (network errors,
9
- * 5xx/429) are retried before surfacing an error.
10
- */
11
- const HINT_INVALID_API_KEY = "Your API key may be invalid. Run `aai` to re-enter your AssemblyAI API key.";
12
- /** 404 hint for requests scoped to a deployed agent's slug. */
13
- const HINT_NOT_DEPLOYED = "The agent may not be deployed. Check `.aai/project.json` for the correct slug.";
14
- /**
15
- * Send an authenticated request to the platform API and return the parsed
16
- * JSON response. Throws a descriptive error with status-specific hints on
17
- * failure (the 401 hint is always included; pass more via `hints`).
18
- */
19
- async function apiRequest(url, opts) {
20
- const client = opts.fetch ? ofetch.create({}, { fetch: opts.fetch }) : ofetch;
21
- try {
22
- return await client(url, {
23
- method: opts.method ?? "GET",
24
- headers: {
25
- Authorization: `Bearer ${opts.apiKey}`,
26
- ...opts.headers
27
- },
28
- ...opts.body !== void 0 ? { body: opts.body } : {},
29
- retry: opts.retry ?? 2,
30
- retryDelay: opts.retryDelay ?? 300
31
- });
32
- } catch (err) {
33
- throw toApiError(err, url, opts);
34
- }
35
- }
36
- /** Format an ofetch failure into a descriptive, action-centric error. */
37
- function toApiError(err, url, opts) {
38
- if (err instanceof FetchError && err.statusCode !== void 0) {
39
- const status = err.statusCode;
40
- const body = typeof err.data === "string" ? err.data : JSON.stringify(err.data ?? "");
41
- const hint = status === 401 ? HINT_INVALID_API_KEY : opts.hints?.[status];
42
- return /* @__PURE__ */ new Error(`${opts.action} failed (HTTP ${status}): ${body}${hint ? `\n ${hint}` : ""}`);
43
- }
44
- const hint = "Check your network connection and verify the server URL is correct.";
45
- const cause = err instanceof FetchError && err.cause !== void 0 ? err.cause : err;
46
- return new Error(`${opts.action} failed: could not reach ${url}\n ${hint}`, { cause });
47
- }
48
- //#endregion
49
- export { apiRequest as n, HINT_NOT_DEPLOYED as t };
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env node
2
- import { n as log, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { n as getServerInfo } from "./_agent-DMyOab9_.mjs";
4
- import { n as apiRequest, t as HINT_NOT_DEPLOYED } from "./_api-client-MenP4-O7.mjs";
5
- //#region delete.ts
6
- async function runDelete(opts) {
7
- await apiRequest(`${opts.url}/${opts.slug}`, {
8
- method: "DELETE",
9
- apiKey: opts.apiKey,
10
- action: "delete",
11
- hints: { 404: HINT_NOT_DEPLOYED },
12
- ...opts.fetch ? { fetch: opts.fetch } : {}
13
- });
14
- }
15
- /** Execute delete and return structured result. */
16
- async function executeDelete(opts) {
17
- const { cwd } = opts;
18
- const { serverUrl, slug, apiKey } = await getServerInfo(cwd, opts.server);
19
- log.step(`Deleting ${slug}`);
20
- await runDelete({
21
- url: serverUrl,
22
- slug,
23
- apiKey
24
- });
25
- log.success(`Deleted ${serverUrl}/${slug}`);
26
- return ok({ slug });
27
- }
28
- //#endregion
29
- export { executeDelete };
@@ -1,109 +0,0 @@
1
- #!/usr/bin/env node
2
- import { a as unwrapCancel, n as log, o as CliError, u as ok } from "./_ui-8kOEB-JH.mjs";
3
- import { i as readGlobalConfig, r as getConfigDir, s as writeGlobalConfig, t as approveServer } from "./_config-5AEqhh-O.mjs";
4
- import { a as resolveServerUrl } from "./_agent-DMyOab9_.mjs";
5
- import * as p from "@clack/prompts";
6
- //#region login.ts
7
- /**
8
- * `aai login` — email sign-in against the platform, ending with the
9
- * account's AssemblyAI API key stored in the global config (the same slot
10
- * `ensureApiKey` reads), so every other command is untouched by how the
11
- * key was acquired.
12
- *
13
- * Flow (the CLI mirror of the browser studio's two gates):
14
- * 1. `GET /studio/auth` names the login mode.
15
- * - `supabase`: Supabase email OTP — `POST /auth/v1/otp` emails a
16
- * one-time code, the user types it here, `POST /auth/v1/verify`
17
- * returns the session. The magic LINK in the same email targets the
18
- * browser; the CLI uses the code because a terminal has no redirect
19
- * to land on. (The Supabase email template must include the
20
- * `{{ .Token }}` code for this to work.)
21
- * - `dev`: mint the same self-describing dev token the studio's local
22
- * login mints — nothing is emailed anywhere.
23
- * 2. `GET /studio/account` — when no key is on file yet, prompt for one
24
- * and `PUT /studio/account/key` (the same mandatory onboarding step
25
- * the browser shows after sign-in).
26
- * 3. `GET /studio/account/key` — fetch the key and save it locally.
27
- * Unlike the browser, the CLI needs the RAW key: `aai dev` runs the
28
- * provider pipeline in-process on it.
29
- */
30
- async function jsonBody(res, what) {
31
- const body = await res.json().catch(() => null);
32
- if (!res.ok) throw new CliError("login_failed", `${what} failed: ${body?.error ?? body?.msg ?? `HTTP ${res.status}`}`);
33
- if (body === null) throw new CliError("login_failed", `${what} returned an invalid response`);
34
- return body;
35
- }
36
- function requireTty() {
37
- 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.");
38
- }
39
- /** The same self-describing token the studio's local-dev login mints. */
40
- function mintDevToken(email) {
41
- return `dev.${Buffer.from(JSON.stringify({
42
- id: `dev:${email}`,
43
- email
44
- })).toString("base64url").replace(/=+$/, "")}.dev`;
45
- }
46
- /** Supabase email OTP: send the code, prompt for it, verify to a session. */
47
- async function supabaseSession(auth, email, fetchFn) {
48
- const base = auth.supabaseUrl.replace(/\/+$/, "");
49
- const headers = {
50
- apikey: auth.supabasePublishableKey,
51
- "Content-Type": "application/json"
52
- };
53
- await jsonBody(await fetchFn(`${base}/auth/v1/otp`, {
54
- method: "POST",
55
- headers,
56
- body: JSON.stringify({
57
- email,
58
- create_user: true
59
- })
60
- }), "Sending the sign-in code");
61
- log.info(`Sent a sign-in code to ${email}.`);
62
- const code = unwrapCancel(await p.text({ message: "Enter the code from your email" }), "Login cancelled").trim();
63
- const session = await jsonBody(await fetchFn(`${base}/auth/v1/verify`, {
64
- method: "POST",
65
- headers,
66
- body: JSON.stringify({
67
- type: "email",
68
- email,
69
- token: code
70
- })
71
- }), "Verifying the code");
72
- if (!session.access_token) throw new CliError("login_failed", "Verifying the code did not return a session.");
73
- return session.access_token;
74
- }
75
- async function executeLogin(opts, deps = {}) {
76
- const fetchFn = deps.fetchFn ?? globalThis.fetch;
77
- requireTty();
78
- const globalConfig = await readGlobalConfig();
79
- const serverUrl = resolveServerUrl(opts.server, void 0, globalConfig.approvedServers ?? []);
80
- if (opts.server) await approveServer(serverUrl);
81
- const auth = await jsonBody(await fetchFn(`${serverUrl}/studio/auth`), "Reading the server's login configuration");
82
- if (auth.mode === "none") throw new CliError("login_unavailable", "This server has no browser/email login configured.", "Set the ASSEMBLYAI_API_KEY environment variable, or run any platform command to be prompted for a key.");
83
- const email = unwrapCancel(await p.text({ message: "Email address" }), "Login cancelled").trim();
84
- const bearer = { Authorization: `Bearer ${auth.mode === "dev" ? mintDevToken(email) : await supabaseSession(auth, email, fetchFn)}` };
85
- if (!(await jsonBody(await fetchFn(`${serverUrl}/studio/account`, { headers: bearer }), "Loading your account")).hasKey) {
86
- const newKey = unwrapCancel(await p.password({ message: "Enter your AssemblyAI API key (stored with your account)" }), "Login cancelled").trim();
87
- await jsonBody(await fetchFn(`${serverUrl}/studio/account/key`, {
88
- method: "PUT",
89
- headers: {
90
- ...bearer,
91
- "Content-Type": "application/json"
92
- },
93
- body: JSON.stringify({ apiKey: newKey })
94
- }), "Saving your API key");
95
- }
96
- const { apiKey } = await jsonBody(await fetchFn(`${serverUrl}/studio/account/key`, { headers: bearer }), "Fetching your API key");
97
- const dir = getConfigDir();
98
- await writeGlobalConfig(dir, {
99
- ...await readGlobalConfig(dir),
100
- apiKey
101
- });
102
- log.success(`Signed in as ${email} — your API key is saved for future commands.`);
103
- return ok({
104
- email,
105
- server: serverUrl
106
- });
107
- }
108
- //#endregion
109
- export { executeLogin };