@alexkroman1/aai-cli 5.5.1 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -7
- package/dist/{_agent-DMyOab9_.mjs → _agent-BB1ZaZsX.mjs} +3 -3
- package/dist/_agent.d.ts +3 -1
- package/dist/{_api-client-MenP4-O7.mjs → _api-client-B6_uvuLs.mjs} +1 -0
- package/dist/_api-client.d.ts +6 -0
- package/dist/_cli-common.d.ts +38 -0
- package/dist/{_config-5AEqhh-O.mjs → _config-PIC0nzRu.mjs} +29 -3
- package/dist/_config.d.ts +9 -1
- package/dist/_deploy.d.ts +7 -0
- package/dist/{_dev-server-vV05Fnki.mjs → _dev-server-CGptLIAm.mjs} +2 -2
- package/dist/{_init-BZ9t_Kz-.mjs → _init-BPFM4uBH.mjs} +3 -3
- package/dist/{_slug-api-DaqQJHk8.mjs → _slug-api-dcaQ_Uho.mjs} +2 -2
- package/dist/_studio-commands.d.ts +73 -0
- package/dist/_studio.d.ts +55 -0
- package/dist/{_templates-Bv8CR800.mjs → _templates-jmjgdgcd.mjs} +19 -8
- package/dist/_templates.d.ts +9 -0
- package/dist/{_typecheck-gate-9IHWDnl1.mjs → _typecheck-gate-DvE8S3aQ.mjs} +1 -1
- package/dist/{build-D_PgQOD4.mjs → build-BXwDB78d.mjs} +1 -1
- package/dist/cli.mjs +163 -34
- package/dist/delete-BaHgd9cN.mjs +51 -0
- package/dist/delete.d.ts +9 -2
- package/dist/{deploy-1eaXcfUw.mjs → deploy-D-O-Q_mW.mjs} +7 -5
- package/dist/deploy.d.ts +2 -0
- package/dist/{dev-gVNdGFYY.mjs → dev-C5P8amSg.mjs} +1 -1
- package/dist/{init-DoU4_txp.mjs → init-BGlOhIOl.mjs} +15 -14
- package/dist/login-B6IhXski.mjs +117 -0
- package/dist/login.d.ts +33 -20
- package/dist/scaffold/package.json +4 -4
- package/dist/{secret-CGAIAbUx.mjs → secret-_bHo4rqt.mjs} +1 -1
- package/dist/{storage-CnhOayhm.mjs → storage-MWiVx_mV.mjs} +1 -1
- package/dist/studio-CsRn2J1a.mjs +284 -0
- package/dist/studio.d.ts +52 -0
- package/package.json +4 -4
- package/dist/delete-DXilFBb1.mjs +0 -29
- package/dist/login-C59ZHzuO.mjs +0 -109
package/dist/login.d.ts
CHANGED
|
@@ -1,31 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `aai login` —
|
|
3
|
-
* account's AssemblyAI API key stored in
|
|
4
|
-
* `ensureApiKey` reads), so every other
|
|
5
|
-
* key was acquired.
|
|
2
|
+
* `aai login` — link the CLI to an account that is ALREADY signed in to the
|
|
3
|
+
* browser studio, ending with the account's AssemblyAI API key stored in
|
|
4
|
+
* the global config (the same slot `ensureApiKey` reads), so every other
|
|
5
|
+
* command is untouched by how the key was acquired.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* Unlike the browser, the CLI needs the RAW key: `aai dev` runs the
|
|
22
|
-
* provider pipeline in-process on it.
|
|
7
|
+
* The CLI deliberately performs no sign-in of its own — it cannot create an
|
|
8
|
+
* account, and it never sees a session token. Device-link flow:
|
|
9
|
+
* 1. `GET /studio/auth` — fail fast when the server has no browser login
|
|
10
|
+
* configured (nobody could ever approve the link).
|
|
11
|
+
* 2. Mint an unguessable one-shot code (32 random bytes, base64url) and
|
|
12
|
+
* open the browser at `<server>/?cli-link=<code>`. The studio — where
|
|
13
|
+
* the user signs in with GitHub (or the local-dev login) if they aren't
|
|
14
|
+
* already — shows a "link the CLI to this account?" approval displaying
|
|
15
|
+
* the same short confirmation code this terminal printed, so a phished
|
|
16
|
+
* approval link has a visible mismatch (no terminal to match against).
|
|
17
|
+
* 3. Poll `POST /studio/cli-link/exchange` with the code. Approval grants
|
|
18
|
+
* the code ONE exchange for the account's stored API key, which is
|
|
19
|
+
* saved locally. An account with no stored key can't approve — the
|
|
20
|
+
* studio's own onboarding gate runs first — so the CLI never sets keys.
|
|
23
21
|
*/
|
|
24
22
|
import { type CommandResult } from "./_output.ts";
|
|
25
23
|
export type LoginDeps = {
|
|
26
24
|
/** Test seam — never set outside tests. */
|
|
27
25
|
fetchFn?: typeof globalThis.fetch;
|
|
26
|
+
/** Test seam — never set outside tests. */
|
|
27
|
+
openBrowser?: (url: string) => void;
|
|
28
|
+
/** Test seam — never set outside tests. */
|
|
29
|
+
pollIntervalMs?: number;
|
|
30
|
+
/** Test seam — never set outside tests. */
|
|
31
|
+
timeoutMs?: number;
|
|
28
32
|
};
|
|
33
|
+
/**
|
|
34
|
+
* Human-matchable confirmation derived from the link code — printed in the
|
|
35
|
+
* terminal AND shown on the browser approval gate (aai-studio-client's
|
|
36
|
+
* cli-link.ts derives the same value; keep the two in lockstep). Not a
|
|
37
|
+
* secret: both ends already hold the full code. It exists so someone who
|
|
38
|
+
* lands on an approval page they didn't cause has a concrete mismatch to
|
|
39
|
+
* notice ("what terminal?") instead of a bare Approve button.
|
|
40
|
+
*/
|
|
41
|
+
export declare function linkConfirmationCode(code: string): string;
|
|
29
42
|
export declare function executeLogin(opts: {
|
|
30
43
|
server?: string | undefined;
|
|
31
44
|
}, deps?: LoginDeps): Promise<CommandResult<{
|
|
@@ -8,18 +8,18 @@
|
|
|
8
8
|
"dev": "aai dev",
|
|
9
9
|
"build": "aai build",
|
|
10
10
|
"test": "aai test",
|
|
11
|
-
"
|
|
11
|
+
"publish:agent": "aai publish"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@alexkroman1/aai": "^5.
|
|
15
|
-
"@alexkroman1/aai-ui": "^5.
|
|
14
|
+
"@alexkroman1/aai": "^5.6.0",
|
|
15
|
+
"@alexkroman1/aai-ui": "^5.6.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.
|
|
22
|
+
"@alexkroman1/aai-cli": "^5.6.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-
|
|
3
|
+
import { t as slugRequest } from "./_slug-api-dcaQ_Uho.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-
|
|
3
|
+
import { t as slugRequest } from "./_slug-api-dcaQ_Uho.mjs";
|
|
4
4
|
import * as p from "@clack/prompts";
|
|
5
5
|
//#region storage.ts
|
|
6
6
|
async function storageRequest(cwd, init, server) {
|
|
@@ -0,0 +1,284 @@
|
|
|
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-PIC0nzRu.mjs";
|
|
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";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
10
|
+
import { 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
|
+
* Walk a local project into the path→content record a workspace stores —
|
|
64
|
+
* 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.
|
|
67
|
+
*/
|
|
68
|
+
async function collectSourceFiles(dir) {
|
|
69
|
+
const paths = await walkProject(dir);
|
|
70
|
+
const files = {};
|
|
71
|
+
const warnings = [];
|
|
72
|
+
if (paths.length > 100) warnings.push(`Project has ${paths.length} files; only the first 100 sync to the studio.`);
|
|
73
|
+
for (const rel of paths.slice(0, 100)) {
|
|
74
|
+
const abs = path.join(dir, rel);
|
|
75
|
+
const st = await stat(abs);
|
|
76
|
+
if (st.size > 256e3) {
|
|
77
|
+
warnings.push(`${rel} is ${st.size} bytes (max ${MAX_STUDIO_FILE_BYTES}) — not synced.`);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
files[rel.split(path.sep).join("/")] = await readFile(abs, "utf-8");
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
files,
|
|
84
|
+
warnings
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/** A studio project name derived from a directory name, or null if unusable. */
|
|
88
|
+
function projectNameFromDir(dir) {
|
|
89
|
+
const name = path.basename(dir).toLowerCase().replace(/[^a-z0-9-_]+/g, "-").replace(/-{2,}/g, "-").replace(/^[-_]+|[-_]+$/g, "").slice(0, 64);
|
|
90
|
+
return VALID_SLUG_RE.test(name) ? name : null;
|
|
91
|
+
}
|
|
92
|
+
/** The shareable studio URL for a project — what every command prints. */
|
|
93
|
+
function studioProjectUrl(serverUrl, project) {
|
|
94
|
+
return `${serverUrl}/studio/chat/${project}`;
|
|
95
|
+
}
|
|
96
|
+
function listStudioProjects(serverUrl, apiKey) {
|
|
97
|
+
return apiRequest(`${serverUrl}/studio/projects`, {
|
|
98
|
+
apiKey,
|
|
99
|
+
action: "list"
|
|
100
|
+
}).then((res) => res.projects);
|
|
101
|
+
}
|
|
102
|
+
/** Fetch a project, or null when it doesn't exist (the push existence probe). */
|
|
103
|
+
function fetchStudioProject(serverUrl, apiKey, project) {
|
|
104
|
+
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}`, {
|
|
105
|
+
apiKey,
|
|
106
|
+
action: "pull",
|
|
107
|
+
allow404: true
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** `PUT /studio/projects/:project/source` — the atomic whole-tree push. */
|
|
111
|
+
function pushStudioSource(serverUrl, apiKey, project, body) {
|
|
112
|
+
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/source`, {
|
|
113
|
+
apiKey,
|
|
114
|
+
action: "push",
|
|
115
|
+
method: "PUT",
|
|
116
|
+
body,
|
|
117
|
+
hints: { 409: "The studio has newer changes. Run `aai pull` to fetch them, or `aai push --force` to overwrite." }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
/** `POST /studio/projects/:project/deploy` — Publish, in the project's sandbox. */
|
|
121
|
+
function publishStudioProject(serverUrl, apiKey, project) {
|
|
122
|
+
return apiRequest(`${serverUrl}/studio/projects/${encodeURIComponent(project)}/deploy`, {
|
|
123
|
+
apiKey,
|
|
124
|
+
action: "publish",
|
|
125
|
+
method: "POST",
|
|
126
|
+
retry: 0
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region studio.ts
|
|
131
|
+
/**
|
|
132
|
+
* The studio-workspace commands: `aai list`, `aai pull`, `aai push`,
|
|
133
|
+
* `aai publish`.
|
|
134
|
+
*
|
|
135
|
+
* One model: a studio project's workspace and a local project directory are
|
|
136
|
+
* the same file tree. `pull` materializes the workspace locally (completing
|
|
137
|
+
* it into a runnable project with the scaffold), `push` replaces the
|
|
138
|
+
* workspace with the local tree (fast-forward-checked, so studio edits are
|
|
139
|
+
* never silently overwritten), and `publish` pushes then ships the
|
|
140
|
+
* workspace to production through the studio's Publish route — the same
|
|
141
|
+
* in-sandbox deploy the Publish button runs. There is no other path to
|
|
142
|
+
* production.
|
|
143
|
+
*/
|
|
144
|
+
async function executeList(opts) {
|
|
145
|
+
const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
|
|
146
|
+
const projects = await listStudioProjects(serverUrl, apiKey);
|
|
147
|
+
if (projects.length === 0) log.info("No studio projects yet. Push one with `aai push`, or create one in the studio.");
|
|
148
|
+
for (const name of projects) log.message(`${name} ${fmtUrl(studioProjectUrl(serverUrl, name))}`);
|
|
149
|
+
return ok({ projects });
|
|
150
|
+
}
|
|
151
|
+
/** Write a pulled file map under `dir`, refusing paths that escape it. */
|
|
152
|
+
async function materializeFiles(dir, files) {
|
|
153
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
154
|
+
const abs = path.resolve(dir, rel);
|
|
155
|
+
if (abs !== dir && !abs.startsWith(dir + path.sep)) throw new Error(`Pulled file path escapes the project directory: ${rel}`);
|
|
156
|
+
await mkdir(path.dirname(abs), { recursive: true });
|
|
157
|
+
await writeFile(abs, content, "utf-8");
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function executePull(opts) {
|
|
161
|
+
const { serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
|
|
162
|
+
const remote = await fetchStudioProject(serverUrl, apiKey, opts.project);
|
|
163
|
+
if (!remote) throw new CliError("not_found", `No studio project named "${opts.project}".`, "Run `aai list` to see your projects.");
|
|
164
|
+
const target = path.resolve(opts.cwd, opts.dir ?? opts.project);
|
|
165
|
+
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.");
|
|
166
|
+
await materializeFiles(target, remote.files);
|
|
167
|
+
await layerScaffold(target);
|
|
168
|
+
await updateProjectConfig(target, {
|
|
169
|
+
serverUrl,
|
|
170
|
+
studioProject: opts.project,
|
|
171
|
+
studioSourceHash: remote.sourceHash,
|
|
172
|
+
...remote.deployedSlug ? { slug: remote.deployedSlug } : {}
|
|
173
|
+
});
|
|
174
|
+
const count = Object.keys(remote.files).length;
|
|
175
|
+
log.success(`Pulled ${opts.project} (${count} files) into ${target}`);
|
|
176
|
+
log.info(`Next: cd ${opts.dir ?? opts.project} && pnpm install && aai dev`);
|
|
177
|
+
log.info(`Studio: ${fmtUrl(studioProjectUrl(serverUrl, opts.project))}`);
|
|
178
|
+
return ok({
|
|
179
|
+
project: opts.project,
|
|
180
|
+
dir: target,
|
|
181
|
+
files: count
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The shared push core: collect local source, resolve (or mint) the linked
|
|
186
|
+
* project, sync atomically, record the new fast-forward token.
|
|
187
|
+
*/
|
|
188
|
+
async function pushProject(opts) {
|
|
189
|
+
const { config, serverUrl, apiKey } = await resolveDeployTarget(opts.cwd, opts.server);
|
|
190
|
+
const { files, warnings } = await collectSourceFiles(opts.cwd);
|
|
191
|
+
for (const warning of warnings) log.warn(warning);
|
|
192
|
+
if (Object.keys(files).length === 0) throw new Error("Nothing to push — this directory has no project files.");
|
|
193
|
+
let project = config?.studioProject;
|
|
194
|
+
let baseHash = config?.studioSourceHash;
|
|
195
|
+
let slug = config?.slug;
|
|
196
|
+
if (!project) {
|
|
197
|
+
project = slug ?? projectNameFromDir(opts.cwd) ?? void 0;
|
|
198
|
+
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.`);
|
|
199
|
+
const existing = await fetchStudioProject(serverUrl, apiKey, project);
|
|
200
|
+
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.`);
|
|
201
|
+
baseHash = existing?.sourceHash;
|
|
202
|
+
slug ??= existing?.deployedSlug;
|
|
203
|
+
}
|
|
204
|
+
const result = await pushStudioSource(serverUrl, apiKey, project, {
|
|
205
|
+
files,
|
|
206
|
+
...opts.force ? {} : { baseHash }
|
|
207
|
+
});
|
|
208
|
+
await updateProjectConfig(opts.cwd, {
|
|
209
|
+
serverUrl,
|
|
210
|
+
studioProject: project,
|
|
211
|
+
studioSourceHash: result.sourceHash,
|
|
212
|
+
...slug ? { slug } : {}
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
project,
|
|
216
|
+
sourceHash: result.sourceHash,
|
|
217
|
+
created: result.created,
|
|
218
|
+
serverUrl,
|
|
219
|
+
apiKey,
|
|
220
|
+
slug
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
async function executePush(opts) {
|
|
224
|
+
const pushed = await pushProject(opts);
|
|
225
|
+
const url = studioProjectUrl(pushed.serverUrl, pushed.project);
|
|
226
|
+
log.success(`${pushed.created ? "Created" : "Synced"} studio project ${pushed.project} — ${fmtUrl(url)}`);
|
|
227
|
+
return ok({
|
|
228
|
+
project: pushed.project,
|
|
229
|
+
created: pushed.created,
|
|
230
|
+
url
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Mirror `.env` into the deployed agent's secrets (the same `/:slug/secret`
|
|
235
|
+
* routes `aai secret` uses). Secrets are merged into the agent env at
|
|
236
|
+
* deploy time, which is why publish syncs them BEFORE deploying when the
|
|
237
|
+
* slug is already known.
|
|
238
|
+
*/
|
|
239
|
+
async function syncEnvSecrets(cwd, serverUrl, apiKey, slug) {
|
|
240
|
+
const env = await resolveServerEnv(cwd);
|
|
241
|
+
const names = Object.keys(env);
|
|
242
|
+
if (names.length === 0) return [];
|
|
243
|
+
await apiRequest(`${serverUrl}/${slug}/secret`, {
|
|
244
|
+
apiKey,
|
|
245
|
+
action: "secret",
|
|
246
|
+
method: "PUT",
|
|
247
|
+
body: env
|
|
248
|
+
});
|
|
249
|
+
log.info(`Synced ${names.length} secret${names.length === 1 ? "" : "s"} from .env`);
|
|
250
|
+
return names;
|
|
251
|
+
}
|
|
252
|
+
async function executePublish(opts) {
|
|
253
|
+
if (!opts.skipTypecheck) {
|
|
254
|
+
const { assertTypechecks } = await import("./_typecheck-gate-DvE8S3aQ.mjs");
|
|
255
|
+
await assertTypechecks(opts.cwd);
|
|
256
|
+
}
|
|
257
|
+
const pushed = await pushProject(opts);
|
|
258
|
+
const { project, serverUrl, apiKey } = pushed;
|
|
259
|
+
const hadSlug = pushed.slug !== void 0;
|
|
260
|
+
if (pushed.slug) await syncEnvSecrets(opts.cwd, serverUrl, apiKey, pushed.slug);
|
|
261
|
+
log.step(`Publishing ${project} (builds in the project's sandbox)…`);
|
|
262
|
+
const result = await publishStudioProject(serverUrl, apiKey, project);
|
|
263
|
+
if (result.output.trim()) log.message(result.output.trim());
|
|
264
|
+
await updateProjectConfig(opts.cwd, {
|
|
265
|
+
serverUrl,
|
|
266
|
+
slug: result.slug
|
|
267
|
+
});
|
|
268
|
+
if (!hadSlug) {
|
|
269
|
+
if ((await syncEnvSecrets(opts.cwd, serverUrl, apiKey, result.slug)).length > 0) log.info("They apply on the next `aai publish`.");
|
|
270
|
+
}
|
|
271
|
+
const agentUrl = `${serverUrl}/${result.slug}`;
|
|
272
|
+
const studioUrl = studioProjectUrl(serverUrl, project);
|
|
273
|
+
log.success(`Published ${fmtUrl(agentUrl)}`);
|
|
274
|
+
log.info(`Edit in studio: ${fmtUrl(studioUrl)}`);
|
|
275
|
+
return ok({
|
|
276
|
+
project,
|
|
277
|
+
slug: result.slug,
|
|
278
|
+
url: agentUrl,
|
|
279
|
+
studioUrl,
|
|
280
|
+
output: result.output
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
//#endregion
|
|
284
|
+
export { executeList, executePublish, executePull, executePush };
|
package/dist/studio.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
}>>;
|
|
41
|
+
export declare function executePublish(opts: {
|
|
42
|
+
cwd: string;
|
|
43
|
+
server?: string | undefined;
|
|
44
|
+
force?: boolean | undefined;
|
|
45
|
+
skipTypecheck?: boolean | undefined;
|
|
46
|
+
}): Promise<CommandResult<{
|
|
47
|
+
project: string;
|
|
48
|
+
slug: string;
|
|
49
|
+
url: string;
|
|
50
|
+
studioUrl: string;
|
|
51
|
+
output: string;
|
|
52
|
+
}>>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alexkroman1/aai-cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.6.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.
|
|
41
|
-
"@alexkroman1/aai-ui": "5.
|
|
40
|
+
"@alexkroman1/aai": "5.6.0",
|
|
41
|
+
"@alexkroman1/aai-ui": "5.6.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.
|
|
48
|
+
"aai-templates": "0.3.4"
|
|
49
49
|
},
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"vitest": "^4.1.10"
|
package/dist/delete-DXilFBb1.mjs
DELETED
|
@@ -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 };
|
package/dist/login-C59ZHzuO.mjs
DELETED
|
@@ -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 };
|