@anna-ai/cli 0.1.19 → 0.1.22

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 (63) hide show
  1. package/dist/_lifecycle-shared-sbea9HtH.js +65 -0
  2. package/dist/{agent-DUmINbo4.js → agent-Br6zY2qw.js} +1 -1
  3. package/dist/app-bundle-upload-DuLalcSt.js +213 -0
  4. package/dist/app-cache-BEM653Th.js +53 -0
  5. package/dist/apps-B1Nd8l_t.js +221 -0
  6. package/dist/apps-BTn9EN0x.js +53 -0
  7. package/dist/apps-cut-DtEkddIk.js +83 -0
  8. package/dist/apps-destructive-DSTrcFUP.js +104 -0
  9. package/dist/apps-discard-y3_IwcbQ.js +44 -0
  10. package/dist/apps-grants-BGWlpee0.js +34 -0
  11. package/dist/apps-publish-CaTCanDu.js +265 -0
  12. package/dist/apps-publish-DfZTOxBJ.js +14 -0
  13. package/dist/apps-push-B9XT2uwF.js +127 -0
  14. package/dist/apps-release-BLH9XSxB.js +135 -0
  15. package/dist/apps-status-DQ9RvlME.js +58 -0
  16. package/dist/apps-submit-review-DLwCxeAs.js +45 -0
  17. package/dist/apps-sync-meta-D9eKMMUp.js +72 -0
  18. package/dist/apps-versions-2Tmk0nsx.js +43 -0
  19. package/dist/bridge-Id8K8gr-.js +3 -0
  20. package/dist/bundled-executas-BNOKw4kv.js +161 -0
  21. package/dist/bundled-executas-CNaV2C_O.js +5 -0
  22. package/dist/cli.js +346 -23
  23. package/dist/client-Dn9zThOd.js +150 -0
  24. package/dist/confirm-DxHkk9Wn.js +37 -0
  25. package/dist/dev-BS_8yoSm.js +3 -0
  26. package/dist/{dev-b1j-dEM2.js → dev-E7mqXj5S.js} +95 -26
  27. package/dist/{dev-app-cache-3Pfesngr.js → dev-app-cache-D-r6ZpEk.js} +11 -2
  28. package/dist/{doctor-CgJYokiR.js → doctor-DKrt-Kda.js} +1 -1
  29. package/dist/executa-cache-BFoUtb4J.js +86 -0
  30. package/dist/executa-cache-WBkCLic7.js +4 -0
  31. package/dist/executa-destructive-COQE4Xqi.js +104 -0
  32. package/dist/{executa-dev-BeC6a8S8.js → executa-dev-BvS9zTpO.js} +11 -11
  33. package/dist/executa-publish-B88_9gbp.js +9 -0
  34. package/dist/executa-publish-Ca5V7MyA.js +258 -0
  35. package/dist/executa-reads-CQ6S8gHY.js +107 -0
  36. package/dist/executas-Cep6KEo0.js +109 -0
  37. package/dist/manifest-DGwRap2i.js +188 -0
  38. package/dist/publish-C1wcf-qI.js +58 -0
  39. package/dist/{server-BgJGmEpv.js → server-_IG8Igje.js} +200 -2
  40. package/dist/{storage-EQJA_0UW.js → storage-CTkApNQ9.js} +1 -1
  41. package/dist/token-B9JUPelx.js +87 -0
  42. package/dist/working-orchestration-Dw9u1Vq0.js +190 -0
  43. package/package.json +3 -3
  44. package/templates/executa/go/executa.json +5 -0
  45. package/templates/executa/node/executa.json +5 -0
  46. package/templates/executa/python/executa.json +5 -0
  47. package/dist/apps-ClgEOdKD.js +0 -44
  48. package/dist/bridge-B3Vwr4cg.js +0 -3
  49. package/dist/dev-D8o7xi0W.js +0 -3
  50. package/dist/dev-app-cache-CZ1UjMz0.js +0 -4
  51. /package/dist/{bridge-mkb_EM-y.js → bridge-BuklhzeE.js} +0 -0
  52. /package/dist/{credentials-DDqx6XMQ.js → credentials-DklPMD22.js} +0 -0
  53. /package/dist/{dev-account-DCyjamBa.js → dev-account-qRaET1Cp.js} +0 -0
  54. /package/dist/{executa-init-COEmKDOE.js → executa-init-Jp-h9OI7.js} +0 -0
  55. /package/dist/{executa-register-66WKIwQQ.js → executa-register-CulDtwYZ.js} +0 -0
  56. /package/dist/{fixture-CATHyLLI.js → fixture-CYwxbiQD.js} +0 -0
  57. /package/dist/{host_upload-C_pGOS6p.js → host_upload-GXVkDM5M.js} +0 -0
  58. /package/dist/{image-bwolX7pa.js → image-DduR91n5.js} +0 -0
  59. /package/dist/{login-CsIVbrmf.js → login-BGqFjQwH.js} +0 -0
  60. /package/dist/{logout-gfmKQxMj.js → logout-CGIRKH3y.js} +0 -0
  61. /package/dist/{runner-DmGLdat0.js → runner-B-hIqx5L.js} +0 -0
  62. /package/dist/{sampling-CJUDG-mf.js → sampling-CXke7hq1.js} +0 -0
  63. /package/dist/{whoami-BS5wy-Nh.js → whoami-BoFLEUcp.js} +0 -0
@@ -0,0 +1,65 @@
1
+ import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
2
+ import { CliError, NexusClient, ScopeError } from "./client-Dn9zThOd.js";
3
+ import { dim, red, yellow } from "kleur/colors";
4
+
5
+ //#region src/commands/_lifecycle-shared.ts
6
+ function resolveClient(opts = {}) {
7
+ const envHost = process.env.ANNA_APP_HOST;
8
+ const envPat = process.env.ANNA_APP_PAT;
9
+ if (envHost && envPat) {
10
+ const host = canonicalHost(opts.host ?? envHost);
11
+ return {
12
+ client: new NexusClient({
13
+ host,
14
+ pat: envPat
15
+ }),
16
+ host,
17
+ source: "env"
18
+ };
19
+ }
20
+ const acc = getAccount(opts.account ?? opts.host);
21
+ if (!acc) throw new CliError("no PAT on disk and ANNA_APP_PAT not set — run `anna-app login --host <url>` first.", 2);
22
+ return {
23
+ client: new NexusClient({
24
+ host: acc.host,
25
+ pat: acc.pat
26
+ }),
27
+ host: acc.host,
28
+ source: "credentials"
29
+ };
30
+ }
31
+ /**
32
+ * Print an error from a CLI command and return its exit code.
33
+ *
34
+ * `ScopeError` gets the friendly multi-line hint from design §6.2.
35
+ * `CliError` prints its message + carries a numeric exit code.
36
+ * Everything else maps to exit 1 with the bare message.
37
+ */
38
+ function reportError(e) {
39
+ if (e instanceof ScopeError) {
40
+ const need = e.needed.length > 0 ? e.needed.join(", ") : "(unknown)";
41
+ const have = e.have.length > 0 ? e.have.join(", ") : "(none)";
42
+ const missing = e.missing.length > 0 ? e.missing.join(", ") : need;
43
+ console.error(red(`✗ this command needs PAT scope: ${missing}`) + "\n" + dim(` current PAT scopes: [${have}]`) + "\n" + yellow(` Mint a new token with:
44
+ anna-app login --host <nexus-url> --scopes ${need}`) + "\n" + dim(" Existing tokens cannot be upgraded — old token continues to work for its current scopes."));
45
+ return e.exitCode;
46
+ }
47
+ if (e instanceof CliError) {
48
+ console.error(red(`✗ ${e.message}`));
49
+ return e.exitCode;
50
+ }
51
+ const msg = e?.message ?? String(e);
52
+ console.error(red(`✗ ${msg}`));
53
+ return 1;
54
+ }
55
+ /** Wrap an async command body so unexpected throws become exit codes. */
56
+ async function withErrorHandling(fn) {
57
+ try {
58
+ return await fn();
59
+ } catch (e) {
60
+ return reportError(e);
61
+ }
62
+ }
63
+
64
+ //#endregion
65
+ export { resolveClient, withErrorHandling };
@@ -1,5 +1,5 @@
1
1
  import { canonicalHost } from "./credentials-BTv2IfUZ.js";
2
- import { hostOf, mintAppSession, requireAccount, withCode } from "./dev-account-DCyjamBa.js";
2
+ import { hostOf, mintAppSession, requireAccount, withCode } from "./dev-account-qRaET1Cp.js";
3
3
  import { resolve } from "node:path";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
 
@@ -0,0 +1,213 @@
1
+ import { finalizeBundle, finalizeWorkingBundle, getBundle, initBundle, initWorkingBundle, uploadBundleFile, uploadWorkingBundleFile } from "./apps-B1Nd8l_t.js";
2
+ import { CliError } from "./client-Dn9zThOd.js";
3
+ import { canonicalize } from "./executa-publish-Ca5V7MyA.js";
4
+ import { join, relative, sep } from "node:path";
5
+ import { readFileSync, readdirSync, statSync } from "node:fs";
6
+ import { createHash } from "node:crypto";
7
+
8
+ //#region src/publish/app-bundle-upload.ts
9
+ const CONTENT_TYPES = {
10
+ html: "text/html",
11
+ htm: "text/html",
12
+ css: "text/css",
13
+ js: "text/javascript",
14
+ mjs: "text/javascript",
15
+ json: "application/json",
16
+ map: "application/json",
17
+ webmanifest: "application/manifest+json",
18
+ wasm: "application/wasm",
19
+ png: "image/png",
20
+ jpg: "image/jpeg",
21
+ jpeg: "image/jpeg",
22
+ svg: "image/svg+xml",
23
+ webp: "image/webp",
24
+ avif: "image/avif",
25
+ ico: "image/x-icon",
26
+ woff: "font/woff",
27
+ woff2: "font/woff2",
28
+ txt: "text/plain",
29
+ md: "text/markdown"
30
+ };
31
+ const MAX_TOTAL_BYTES = 50 * 1024 * 1024;
32
+ const MAX_FILE_BYTES = 10 * 1024 * 1024;
33
+ const MAX_FILES = 2e3;
34
+ const UPLOAD_PARALLEL = 4;
35
+ function guessContentType(name) {
36
+ const m = /\.([a-z0-9]+)$/i.exec(name);
37
+ if (!m) return "application/octet-stream";
38
+ return CONTENT_TYPES[m[1].toLowerCase()] ?? "application/octet-stream";
39
+ }
40
+ /** Recursively collect files under `bundleDir` with stable POSIX paths. */
41
+ function collectFiles(bundleDir) {
42
+ const out = [];
43
+ const walk = (base) => {
44
+ for (const ent of readdirSync(base, { withFileTypes: true })) {
45
+ const abs = join(base, ent.name);
46
+ if (ent.isDirectory()) walk(abs);
47
+ else if (ent.isFile()) {
48
+ const rel = relative(bundleDir, abs).split(sep).join("/");
49
+ const bytes = readFileSync(abs);
50
+ out.push({
51
+ relativePath: rel,
52
+ bytes,
53
+ entry: {
54
+ sha256: createHash("sha256").update(bytes).digest("hex"),
55
+ byte_size: bytes.byteLength,
56
+ content_type: guessContentType(rel)
57
+ }
58
+ });
59
+ }
60
+ }
61
+ };
62
+ walk(bundleDir);
63
+ out.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
64
+ return out;
65
+ }
66
+ /**
67
+ * Compute the same manifest fingerprint the server stores as
68
+ * `sha256_manifest` (sha256 of canonical JSON of the sorted file_map with
69
+ * each entry reduced to {sha256, byte_size, content_type}).
70
+ */
71
+ function manifestFingerprint(files) {
72
+ const obj = {};
73
+ for (const f of [...files].sort((a, b) => a.relativePath.localeCompare(b.relativePath))) obj[f.relativePath] = {
74
+ sha256: f.entry.sha256,
75
+ byte_size: f.entry.byte_size,
76
+ content_type: f.entry.content_type
77
+ };
78
+ return createHash("sha256").update(canonicalize(obj)).digest("hex");
79
+ }
80
+ /**
81
+ * Upload + finalize the UI bundle for one app version. Returns
82
+ * `{ status: "skipped" }` when the version's bundle is already finalized
83
+ * with identical content.
84
+ */
85
+ async function uploadAppBundle(params) {
86
+ const { client, appId, versionId, bundleDir, entryPath, bundleFormat = "static-spa", externalOrigins = [], log } = params;
87
+ const files = collectFiles(bundleDir);
88
+ if (files.length === 0) throw new CliError(`bundle directory is empty: ${bundleDir}`, 4);
89
+ if (files.length > MAX_FILES) throw new CliError(`too many bundle files (${files.length} > ${MAX_FILES})`, 4);
90
+ const totalBytes = files.reduce((s, f) => s + f.entry.byte_size, 0);
91
+ if (totalBytes > MAX_TOTAL_BYTES) throw new CliError(`bundle is ${(totalBytes / 1024 / 1024).toFixed(2)} MB — exceeds 50 MB limit`, 4);
92
+ const tooBig = files.find((f) => f.entry.byte_size > MAX_FILE_BYTES);
93
+ if (tooBig) throw new CliError(`bundle file exceeds 10 MB: ${tooBig.relativePath} (${(tooBig.entry.byte_size / 1024 / 1024).toFixed(2)} MB)`, 4);
94
+ if (!files.some((f) => f.relativePath === entryPath)) throw new CliError(`manifest entry "${entryPath}" not found in bundle dir ${bundleDir}`, 4);
95
+ const fingerprint = manifestFingerprint(files);
96
+ const existing = await getBundle(client, appId, versionId);
97
+ if (existing && existing.status === "bundle_ready") {
98
+ if (existing.sha256_manifest === fingerprint) {
99
+ log?.(` bundle unchanged (${files.length} files, sha ${fingerprint.slice(0, 12)}…) — skipped`);
100
+ return {
101
+ status: "skipped",
102
+ reason: "unchanged",
103
+ detail: existing
104
+ };
105
+ }
106
+ throw new CliError("version already has a finalized bundle with different content; bump the app version (e.g. --bump patch) to upload a new bundle", 4);
107
+ }
108
+ log?.(` uploading UI bundle: ${files.length} files, ${(totalBytes / 1024).toFixed(1)} KB`);
109
+ const fileMap = {};
110
+ for (const f of files) fileMap[f.relativePath] = f.entry;
111
+ const initResp = await initBundle(client, appId, versionId, {
112
+ file_map: fileMap,
113
+ total_size_bytes: totalBytes,
114
+ file_count: files.length,
115
+ bundle_format: bundleFormat,
116
+ entry_path: entryPath,
117
+ external_origins: externalOrigins
118
+ });
119
+ const proxyByPath = new Map(initResp.files.map((p) => [p.relative_path, p.proxy_upload_url]));
120
+ const queue = [...files];
121
+ const uploadOne = async () => {
122
+ for (;;) {
123
+ const f = queue.shift();
124
+ if (!f) return;
125
+ const proxyUrl = proxyByPath.get(f.relativePath);
126
+ if (!proxyUrl) throw new CliError(`server did not return an upload URL for ${f.relativePath}`, 6);
127
+ const ab = f.bytes.buffer.slice(f.bytes.byteOffset, f.bytes.byteOffset + f.bytes.byteLength);
128
+ const blob = new Blob([ab], { type: f.entry.content_type });
129
+ await uploadBundleFile(client, appId, versionId, f.relativePath, blob);
130
+ }
131
+ };
132
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_PARALLEL, files.length) }, uploadOne));
133
+ const detail = await finalizeBundle(client, appId, versionId, false);
134
+ log?.(` ✓ bundle finalized (${detail.file_count} files, status=${detail.status})`);
135
+ return {
136
+ status: "uploaded",
137
+ detail,
138
+ fileCount: files.length,
139
+ totalBytes
140
+ };
141
+ }
142
+ /** True when `path` is an existing directory. */
143
+ function isExistingDir(path) {
144
+ try {
145
+ return statSync(path).isDirectory();
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+ /**
151
+ * Stage + finalize the working (mutable) UI bundle. When `knownFingerprint`
152
+ * matches the freshly-computed manifest fingerprint, skips the upload
153
+ * (the working bundle already reflects these bytes).
154
+ */
155
+ async function uploadWorkingBundle(params) {
156
+ const { client, appId, bundleDir, entryPath, bundleFormat = "static-spa", externalOrigins = [], knownFingerprint, log } = params;
157
+ const files = collectFiles(bundleDir);
158
+ if (files.length === 0) throw new CliError(`bundle directory is empty: ${bundleDir}`, 4);
159
+ if (files.length > MAX_FILES) throw new CliError(`too many bundle files (${files.length} > ${MAX_FILES})`, 4);
160
+ const totalBytes = files.reduce((s, f) => s + f.entry.byte_size, 0);
161
+ if (totalBytes > MAX_TOTAL_BYTES) throw new CliError(`bundle is ${(totalBytes / 1024 / 1024).toFixed(2)} MB — exceeds 50 MB limit`, 4);
162
+ const tooBig = files.find((f) => f.entry.byte_size > MAX_FILE_BYTES);
163
+ if (tooBig) throw new CliError(`bundle file exceeds 10 MB: ${tooBig.relativePath} (${(tooBig.entry.byte_size / 1024 / 1024).toFixed(2)} MB)`, 4);
164
+ if (!files.some((f) => f.relativePath === entryPath)) throw new CliError(`manifest entry "${entryPath}" not found in bundle dir ${bundleDir}`, 4);
165
+ const fingerprint = manifestFingerprint(files);
166
+ if (knownFingerprint && knownFingerprint === fingerprint) {
167
+ log?.(` working bundle unchanged (${files.length} files, sha ${fingerprint.slice(0, 12)}…) — skipped`);
168
+ return {
169
+ status: "skipped",
170
+ fileCount: files.length,
171
+ totalBytes,
172
+ fingerprint,
173
+ bundleStatus: "ready"
174
+ };
175
+ }
176
+ log?.(` staging working bundle: ${files.length} files, ${(totalBytes / 1024).toFixed(1)} KB`);
177
+ const fileMap = {};
178
+ for (const f of files) fileMap[f.relativePath] = f.entry;
179
+ const initResp = await initWorkingBundle(client, appId, {
180
+ file_map: fileMap,
181
+ total_size_bytes: totalBytes,
182
+ file_count: files.length,
183
+ bundle_format: bundleFormat,
184
+ entry_path: entryPath,
185
+ external_origins: externalOrigins
186
+ });
187
+ const proxyByPath = new Map(initResp.files.map((p) => [p.relative_path, p.proxy_upload_url]));
188
+ const queue = [...files];
189
+ const uploadOne = async () => {
190
+ for (;;) {
191
+ const f = queue.shift();
192
+ if (!f) return;
193
+ const proxyUrl = proxyByPath.get(f.relativePath);
194
+ if (!proxyUrl) throw new CliError(`server did not return an upload URL for ${f.relativePath}`, 6);
195
+ const ab = f.bytes.buffer.slice(f.bytes.byteOffset, f.bytes.byteOffset + f.bytes.byteLength);
196
+ const blob = new Blob([ab], { type: f.entry.content_type });
197
+ await uploadWorkingBundleFile(client, appId, f.relativePath, blob);
198
+ }
199
+ };
200
+ await Promise.all(Array.from({ length: Math.min(UPLOAD_PARALLEL, files.length) }, uploadOne));
201
+ const detail = await finalizeWorkingBundle(client, appId, false);
202
+ log?.(` ✓ working bundle staged (${detail.file_count} files, status=${detail.bundle_status})`);
203
+ return {
204
+ status: "uploaded",
205
+ fileCount: files.length,
206
+ totalBytes,
207
+ fingerprint,
208
+ bundleStatus: detail.bundle_status
209
+ };
210
+ }
211
+
212
+ //#endregion
213
+ export { isExistingDir, uploadAppBundle, uploadWorkingBundle };
@@ -0,0 +1,53 @@
1
+ import { canonicalHost } from "./credentials-BTv2IfUZ.js";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
4
+
5
+ //#region src/identity/app-cache.ts
6
+ const CACHE_DIR = ".anna";
7
+ const CACHE_FILE = "app.json";
8
+ function appCachePath(cwd) {
9
+ return resolve(cwd, CACHE_DIR, CACHE_FILE);
10
+ }
11
+ function readAppIdentity(cwd) {
12
+ const p = appCachePath(cwd);
13
+ if (!existsSync(p)) return null;
14
+ try {
15
+ const raw = JSON.parse(readFileSync(p, "utf-8"));
16
+ if (typeof raw.host === "string" && typeof raw.slug === "string" && typeof raw.app_id === "number") return {
17
+ $schema: "anna-app-identity/v1",
18
+ host: canonicalHost(raw.host),
19
+ app_id: raw.app_id,
20
+ slug: raw.slug,
21
+ first_published_at: raw.first_published_at
22
+ };
23
+ } catch {}
24
+ return null;
25
+ }
26
+ function writeAppIdentity(cwd, identity) {
27
+ const p = appCachePath(cwd);
28
+ mkdirSync(dirname(p), { recursive: true });
29
+ const tmp = `${p}.tmp.${process.pid}`;
30
+ writeFileSync(tmp, JSON.stringify({
31
+ ...identity,
32
+ host: canonicalHost(identity.host)
33
+ }, null, 2) + "\n", "utf-8");
34
+ try {
35
+ renameSync(tmp, p);
36
+ } catch (e) {
37
+ try {
38
+ unlinkSync(tmp);
39
+ } catch {}
40
+ throw e;
41
+ }
42
+ }
43
+ function appCacheMatches(identity, host, slug) {
44
+ if (!identity) return false;
45
+ return identity.host === canonicalHost(host) && identity.slug === slug;
46
+ }
47
+ const _internal = {
48
+ CACHE_DIR,
49
+ CACHE_FILE: join(CACHE_DIR, CACHE_FILE)
50
+ };
51
+
52
+ //#endregion
53
+ export { appCacheMatches, readAppIdentity, writeAppIdentity };
@@ -0,0 +1,221 @@
1
+ //#region src/api/apps.ts
2
+ async function findAppBySlug(client, slug) {
3
+ const r = await client.request({
4
+ path: "/api/v1/developer/apps",
5
+ query: { slug },
6
+ allowStatuses: [404]
7
+ });
8
+ if (r.status === 404) return null;
9
+ if (Array.isArray(r.data) && r.data.length > 0) return r.data[0] ?? null;
10
+ return null;
11
+ }
12
+ async function listMyApps(client) {
13
+ const r = await client.request({ path: "/api/v1/developer/apps" });
14
+ return Array.isArray(r.data) ? r.data : [];
15
+ }
16
+ async function getApp(client, appId) {
17
+ const r = await client.request({ path: `/api/v1/developer/apps/${appId}` });
18
+ return r.data;
19
+ }
20
+ async function createApp(client, body) {
21
+ const r = await client.request({
22
+ method: "POST",
23
+ path: "/api/v1/developer/apps",
24
+ body
25
+ });
26
+ return r.data;
27
+ }
28
+ async function patchApp(client, appId, body) {
29
+ const r = await client.request({
30
+ method: "PATCH",
31
+ path: `/api/v1/developer/apps/${appId}`,
32
+ body
33
+ });
34
+ return r.data;
35
+ }
36
+ async function listVersions(client, appId) {
37
+ const r = await client.request({ path: `/api/v1/developer/apps/${appId}/versions` });
38
+ return Array.isArray(r.data) ? r.data : [];
39
+ }
40
+ async function createVersion(client, appId, body, contentHash) {
41
+ const r = await client.request({
42
+ method: "POST",
43
+ path: `/api/v1/developer/apps/${appId}/versions`,
44
+ query: contentHash ? { content_hash: contentHash } : void 0,
45
+ body
46
+ });
47
+ return {
48
+ version: r.data,
49
+ idempotentHit: r.idempotentHit
50
+ };
51
+ }
52
+ async function submitForReview(client, appId) {
53
+ const r = await client.request({
54
+ method: "POST",
55
+ path: `/api/v1/developer/apps/${appId}/submit-review`
56
+ });
57
+ return r.data;
58
+ }
59
+ /**
60
+ * Freeze & publish an existing immutable version (`publish_version`):
61
+ * marks `is_latest`, sets `published_at`, freezes executa dependency
62
+ * snapshots and flips the app to PUBLISHED. Idempotent on the server
63
+ * (re-running on the already-latest version is harmless).
64
+ */
65
+ async function publishVersion(client, appId, versionId) {
66
+ const r = await client.request({
67
+ method: "POST",
68
+ path: `/api/v1/developer/apps/${appId}/versions/${versionId}/publish`
69
+ });
70
+ return r.data;
71
+ }
72
+ async function unpublishApp(client, appId) {
73
+ const r = await client.request({
74
+ method: "POST",
75
+ path: `/api/v1/developer/apps/${appId}/unpublish`
76
+ });
77
+ return r.data;
78
+ }
79
+ async function archiveApp(client, appId) {
80
+ const r = await client.request({
81
+ method: "POST",
82
+ path: `/api/v1/developer/apps/${appId}/archive`
83
+ });
84
+ return r.data;
85
+ }
86
+ async function unarchiveApp(client, appId) {
87
+ const r = await client.request({
88
+ method: "POST",
89
+ path: `/api/v1/developer/apps/${appId}/unarchive`
90
+ });
91
+ return r.data;
92
+ }
93
+ async function deleteApp(client, appId) {
94
+ await client.request({
95
+ method: "DELETE",
96
+ path: `/api/v1/developer/apps/${appId}`,
97
+ allowStatuses: [204]
98
+ });
99
+ }
100
+ async function getAppGrants(client, appId) {
101
+ const r = await client.request({
102
+ path: `/api/v1/apps/${appId}/grants`,
103
+ allowStatuses: [404]
104
+ });
105
+ return r.status === 404 ? null : r.data;
106
+ }
107
+ /** GET the bundle for a version, or `null` when none is initialized (404). */
108
+ async function getBundle(client, appId, versionId) {
109
+ const r = await client.request({
110
+ path: `/api/v1/developer/apps/${appId}/versions/${versionId}/bundle`,
111
+ allowStatuses: [404]
112
+ });
113
+ return r.status === 404 ? null : r.data;
114
+ }
115
+ async function initBundle(client, appId, versionId, body) {
116
+ const r = await client.request({
117
+ method: "POST",
118
+ path: `/api/v1/developer/apps/${appId}/versions/${versionId}/bundle/init`,
119
+ body
120
+ });
121
+ return r.data;
122
+ }
123
+ /** Upload one file via the same-origin proxy endpoint (multipart). */
124
+ async function uploadBundleFile(client, appId, versionId, relativePath, blob) {
125
+ await client.request({
126
+ method: "POST",
127
+ path: `/api/v1/developer/apps/${appId}/versions/${versionId}/bundle/file`,
128
+ multipart: { fields: {
129
+ relative_path: relativePath,
130
+ file: blob
131
+ } },
132
+ allowStatuses: [201]
133
+ });
134
+ }
135
+ async function finalizeBundle(client, appId, versionId, skipRemoteCheck = false) {
136
+ const r = await client.request({
137
+ method: "POST",
138
+ path: `/api/v1/developer/apps/${appId}/versions/${versionId}/bundle/finalize`,
139
+ body: { skip_remote_check: skipRemoteCheck }
140
+ });
141
+ return r.data;
142
+ }
143
+ /**
144
+ * Upsert the single mutable working draft for an app. `ifMatch` sends an
145
+ * `If-Match: <revision>` optimistic-lock header (stale → 409). Returns
146
+ * `unchanged: true` (server-side dedupe on `content_hash`) without bumping
147
+ * the revision.
148
+ */
149
+ async function upsertWorkingDraft(client, appId, body, ifMatch) {
150
+ const r = await client.request({
151
+ method: "PUT",
152
+ path: `/api/v1/developer/apps/${appId}/working`,
153
+ body,
154
+ headers: ifMatch !== void 0 ? { "If-Match": String(ifMatch) } : void 0
155
+ });
156
+ const unchanged = r.data.unchanged || r.idempotentHit;
157
+ return {
158
+ ...r.data,
159
+ unchanged
160
+ };
161
+ }
162
+ /** GET the working draft, or `null` when none exists yet (404). */
163
+ async function getWorkingDraft(client, appId) {
164
+ const r = await client.request({
165
+ path: `/api/v1/developer/apps/${appId}/working`,
166
+ allowStatuses: [404]
167
+ });
168
+ return r.status === 404 ? null : r.data;
169
+ }
170
+ /** Discard the working draft (idempotent). */
171
+ async function deleteWorkingDraft(client, appId) {
172
+ await client.request({
173
+ method: "DELETE",
174
+ path: `/api/v1/developer/apps/${appId}/working`,
175
+ allowStatuses: [204, 404]
176
+ });
177
+ }
178
+ /**
179
+ * Snapshot the working draft into an immutable `AnnaAppVersion` and freeze
180
+ * executa deps. `allow409` lets the caller inspect a duplicate-version 409
181
+ * (`{ conflict: true }`) instead of throwing.
182
+ */
183
+ async function cutWorkingDraft(client, appId, body) {
184
+ const r = await client.request({
185
+ method: "POST",
186
+ path: `/api/v1/developer/apps/${appId}/working/cut`,
187
+ body
188
+ });
189
+ return r.data;
190
+ }
191
+ async function initWorkingBundle(client, appId, body) {
192
+ const r = await client.request({
193
+ method: "POST",
194
+ path: `/api/v1/developer/apps/${appId}/working/bundle/init`,
195
+ body
196
+ });
197
+ return r.data;
198
+ }
199
+ /** Upload one working-bundle file via the same-origin proxy (multipart). */
200
+ async function uploadWorkingBundleFile(client, appId, relativePath, blob) {
201
+ await client.request({
202
+ method: "POST",
203
+ path: `/api/v1/developer/apps/${appId}/working/bundle/file`,
204
+ multipart: { fields: {
205
+ relative_path: relativePath,
206
+ file: blob
207
+ } },
208
+ allowStatuses: [201]
209
+ });
210
+ }
211
+ async function finalizeWorkingBundle(client, appId, skipRemoteCheck = false) {
212
+ const r = await client.request({
213
+ method: "POST",
214
+ path: `/api/v1/developer/apps/${appId}/working/bundle/finalize`,
215
+ body: { skip_remote_check: skipRemoteCheck }
216
+ });
217
+ return r.data;
218
+ }
219
+
220
+ //#endregion
221
+ export { archiveApp, createApp, createVersion, cutWorkingDraft, deleteApp, deleteWorkingDraft, finalizeBundle, finalizeWorkingBundle, findAppBySlug, getApp, getAppGrants, getBundle, getWorkingDraft, initBundle, initWorkingBundle, listMyApps, listVersions, patchApp, publishVersion, submitForReview, unarchiveApp, unpublishApp, uploadBundleFile, uploadWorkingBundleFile, upsertWorkingDraft };
@@ -0,0 +1,53 @@
1
+ import "./credentials-BTv2IfUZ.js";
2
+ import { listMyApps } from "./apps-B1Nd8l_t.js";
3
+ import "./client-Dn9zThOd.js";
4
+ import { resolveClient, withErrorHandling } from "./_lifecycle-shared-sbea9HtH.js";
5
+ import { bold, cyan, dim, green, magenta, red, yellow } from "kleur/colors";
6
+
7
+ //#region src/commands/apps.ts
8
+ /** Colorize the lifecycle status so the next action is obvious at a glance. */
9
+ function statusTag(status) {
10
+ switch (status.toUpperCase()) {
11
+ case "PUBLISHED": return green("[PUBLISHED]");
12
+ case "APPROVED": return cyan("[APPROVED]");
13
+ case "PENDING_REVIEW": return yellow("[PENDING_REVIEW]");
14
+ case "REJECTED": return red("[REJECTED]");
15
+ case "ARCHIVED": return dim("[ARCHIVED]");
16
+ case "DRAFT": return magenta("[DRAFT]");
17
+ default: return dim(`[${status}]`);
18
+ }
19
+ }
20
+ async function runAppsList(opts) {
21
+ return withErrorHandling(async () => {
22
+ const { client, host } = resolveClient({ account: opts.account });
23
+ const apps = await listMyApps(client);
24
+ if (opts.json) {
25
+ console.log(JSON.stringify({
26
+ host,
27
+ apps: apps.map((a) => ({
28
+ id: a.id,
29
+ slug: a.slug,
30
+ name: a.name,
31
+ status: a.status,
32
+ is_dev: a.is_dev ?? false,
33
+ updated_at: a.updated_at ?? null
34
+ }))
35
+ }, null, 2));
36
+ return 0;
37
+ }
38
+ console.log(bold(cyan("apps you authored on")) + " " + cyan(host));
39
+ if (apps.length === 0) {
40
+ console.log(dim(" (none — run `anna-app apps publish` in a project to create one)"));
41
+ return 0;
42
+ }
43
+ for (const a of apps) {
44
+ const dev = a.is_dev ? " " + yellow("[dev]") : "";
45
+ console.log(` ${statusTag(a.status)}${dev} ${bold(a.slug)} ${dim(`(id=${a.id})`)}`);
46
+ if (a.name && a.name !== a.slug) console.log(` ${dim(a.name)}`);
47
+ }
48
+ return 0;
49
+ });
50
+ }
51
+
52
+ //#endregion
53
+ export { runAppsList };