@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.
- package/dist/_lifecycle-shared-sbea9HtH.js +65 -0
- package/dist/{agent-DUmINbo4.js → agent-Br6zY2qw.js} +1 -1
- package/dist/app-bundle-upload-DuLalcSt.js +213 -0
- package/dist/app-cache-BEM653Th.js +53 -0
- package/dist/apps-B1Nd8l_t.js +221 -0
- package/dist/apps-BTn9EN0x.js +53 -0
- package/dist/apps-cut-DtEkddIk.js +83 -0
- package/dist/apps-destructive-DSTrcFUP.js +104 -0
- package/dist/apps-discard-y3_IwcbQ.js +44 -0
- package/dist/apps-grants-BGWlpee0.js +34 -0
- package/dist/apps-publish-CaTCanDu.js +265 -0
- package/dist/apps-publish-DfZTOxBJ.js +14 -0
- package/dist/apps-push-B9XT2uwF.js +127 -0
- package/dist/apps-release-BLH9XSxB.js +135 -0
- package/dist/apps-status-DQ9RvlME.js +58 -0
- package/dist/apps-submit-review-DLwCxeAs.js +45 -0
- package/dist/apps-sync-meta-D9eKMMUp.js +72 -0
- package/dist/apps-versions-2Tmk0nsx.js +43 -0
- package/dist/bridge-Id8K8gr-.js +3 -0
- package/dist/bundled-executas-BNOKw4kv.js +161 -0
- package/dist/bundled-executas-CNaV2C_O.js +5 -0
- package/dist/cli.js +346 -23
- package/dist/client-Dn9zThOd.js +150 -0
- package/dist/confirm-DxHkk9Wn.js +37 -0
- package/dist/dev-BS_8yoSm.js +3 -0
- package/dist/{dev-b1j-dEM2.js → dev-E7mqXj5S.js} +95 -26
- package/dist/{dev-app-cache-3Pfesngr.js → dev-app-cache-D-r6ZpEk.js} +11 -2
- package/dist/{doctor-CgJYokiR.js → doctor-DKrt-Kda.js} +1 -1
- package/dist/executa-cache-BFoUtb4J.js +86 -0
- package/dist/executa-cache-WBkCLic7.js +4 -0
- package/dist/executa-destructive-COQE4Xqi.js +104 -0
- package/dist/{executa-dev-BeC6a8S8.js → executa-dev-BvS9zTpO.js} +11 -11
- package/dist/executa-publish-B88_9gbp.js +9 -0
- package/dist/executa-publish-Ca5V7MyA.js +258 -0
- package/dist/executa-reads-CQ6S8gHY.js +107 -0
- package/dist/executas-Cep6KEo0.js +109 -0
- package/dist/manifest-DGwRap2i.js +188 -0
- package/dist/publish-C1wcf-qI.js +58 -0
- package/dist/{server-BgJGmEpv.js → server-_IG8Igje.js} +200 -2
- package/dist/{storage-EQJA_0UW.js → storage-CTkApNQ9.js} +1 -1
- package/dist/token-B9JUPelx.js +87 -0
- package/dist/working-orchestration-Dw9u1Vq0.js +190 -0
- package/package.json +3 -3
- package/templates/executa/go/executa.json +5 -0
- package/templates/executa/node/executa.json +5 -0
- package/templates/executa/python/executa.json +5 -0
- package/dist/apps-ClgEOdKD.js +0 -44
- package/dist/bridge-B3Vwr4cg.js +0 -3
- package/dist/dev-D8o7xi0W.js +0 -3
- package/dist/dev-app-cache-CZ1UjMz0.js +0 -4
- /package/dist/{bridge-mkb_EM-y.js → bridge-BuklhzeE.js} +0 -0
- /package/dist/{credentials-DDqx6XMQ.js → credentials-DklPMD22.js} +0 -0
- /package/dist/{dev-account-DCyjamBa.js → dev-account-qRaET1Cp.js} +0 -0
- /package/dist/{executa-init-COEmKDOE.js → executa-init-Jp-h9OI7.js} +0 -0
- /package/dist/{executa-register-66WKIwQQ.js → executa-register-CulDtwYZ.js} +0 -0
- /package/dist/{fixture-CATHyLLI.js → fixture-CYwxbiQD.js} +0 -0
- /package/dist/{host_upload-C_pGOS6p.js → host_upload-GXVkDM5M.js} +0 -0
- /package/dist/{image-bwolX7pa.js → image-DduR91n5.js} +0 -0
- /package/dist/{login-CsIVbrmf.js → login-BGqFjQwH.js} +0 -0
- /package/dist/{logout-gfmKQxMj.js → logout-CGIRKH3y.js} +0 -0
- /package/dist/{runner-DmGLdat0.js → runner-B-hIqx5L.js} +0 -0
- /package/dist/{sampling-CJUDG-mf.js → sampling-CXke7hq1.js} +0 -0
- /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-
|
|
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 };
|