@anna-ai/cli 0.1.12 → 0.1.16
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/agent-DUmINbo4.js +372 -0
- package/dist/{apps-CDe6Fjq2.js → apps-Bt9CT5Sl.js} +1 -1
- package/dist/bridge-AJilXBw2.js +3 -0
- package/dist/{bridge-BQUo6ehX.js → bridge-nqQ3-j-t.js} +1 -1
- package/dist/cli.js +78 -9
- package/dist/dev-BRlFgo2I.js +3 -0
- package/dist/{dev-DoY58pBM.js → dev-C6v5yRV2.js} +4 -4
- package/dist/dev-account-DCyjamBa.js +44 -0
- package/dist/dev-app-cache-DAHcq46m.js +175 -0
- package/dist/dev-app-cache-DGF2Kuzd.js +4 -0
- package/dist/{doctor-DP2UB10l.js → doctor-C8MWfLt8.js} +1 -1
- package/dist/executa-dev-4FZ7AJHR.js +259 -0
- package/dist/executa-init-COEmKDOE.js +68 -0
- package/dist/executa-register-66WKIwQQ.js +47 -0
- package/dist/host_upload-C_pGOS6p.js +136 -0
- package/dist/image-bwolX7pa.js +131 -0
- package/dist/mascot-wlYTJqMs.js +218 -0
- package/dist/runner-DmGLdat0.js +322 -0
- package/dist/sampling-CJUDG-mf.js +155 -0
- package/dist/storage-EQJA_0UW.js +316 -0
- package/package.json +1 -1
- package/templates/executa/go/README.md +10 -0
- package/templates/executa/go/executa.json +4 -0
- package/templates/executa/go/go.mod +3 -0
- package/templates/executa/go/main.go +148 -0
- package/templates/executa/node/README.md +12 -0
- package/templates/executa/node/executa.json +4 -0
- package/templates/executa/node/package.json +12 -0
- package/templates/executa/node/plugin.mjs +126 -0
- package/templates/executa/node/sampling-fixture.jsonl +1 -0
- package/templates/executa/python/README.md +23 -0
- package/templates/executa/python/__SLUG_PY___plugin.py +146 -0
- package/templates/executa/python/executa.json +4 -0
- package/templates/executa/python/pyproject.toml +15 -0
- package/templates/executa/python/sampling-fixture.jsonl +4 -0
- package/templates/minimal/bundle/app.js +2 -0
- package/templates/minimal/bundle/index.html +1 -2
- package/dist/bridge-BEHyfpPI.js +0 -3
- package/dist/dev-app-cache-BMfOlTHd.js +0 -93
- package/dist/dev-app-cache-cXvO2XwQ.js +0 -4
- /package/dist/{credentials-CIOYq2Lm.js → credentials-DDqx6XMQ.js} +0 -0
- /package/dist/{fixture-BEu4LXLG.js → fixture-CATHyLLI.js} +0 -0
- /package/dist/{login-dl1Zfny8.js → login-CsIVbrmf.js} +0 -0
- /package/dist/{logout-DablvlFs.js → logout-gfmKQxMj.js} +0 -0
- /package/dist/{server-NXmiWJjX.js → server-D8R6ppOp.js} +0 -0
- /package/dist/{whoami-giXOY415.js → whoami-BS5wy-Nh.js} +0 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
|
|
6
|
+
//#region src/executa/host_upload.ts
|
|
7
|
+
var HostUploadBridge = class {
|
|
8
|
+
mocks = [];
|
|
9
|
+
cachedSession = null;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
this.opts = opts;
|
|
12
|
+
if (opts.mode === "mock" && opts.mockFile) {
|
|
13
|
+
const path = resolve(opts.mockFile);
|
|
14
|
+
if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
15
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
16
|
+
try {
|
|
17
|
+
this.mocks.push(JSON.parse(line));
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async call(method, params) {
|
|
23
|
+
if (method !== "host/uploadFile") throw withCode(new Error(`unsupported host method: ${method}`), -32601);
|
|
24
|
+
if (this.opts.mode === "off") throw withCode(new Error("host upload disabled — rerun without `--no-upload`"), -32201);
|
|
25
|
+
if (this.opts.mode === "mock") return this.mockCall(params);
|
|
26
|
+
return this.realCall(params);
|
|
27
|
+
}
|
|
28
|
+
mockCall(params) {
|
|
29
|
+
const mode = String(params.mode ?? "inline");
|
|
30
|
+
const filename = String(params.filename ?? "");
|
|
31
|
+
const candidates = this.mocks.filter((m) => m.ns === "host" && m.method === "uploadFile");
|
|
32
|
+
const matched = candidates.find((m) => (!m.match?.modeEquals || m.match.modeEquals === mode) && (!m.match?.filenameIncludes || filename.includes(m.match.filenameIncludes))) ?? candidates[0];
|
|
33
|
+
if (matched && matched.result) return matched.result;
|
|
34
|
+
if (mode === "inline") {
|
|
35
|
+
const b64 = String(params.content_b64 ?? "");
|
|
36
|
+
const mime = String(params.mime_type ?? "application/octet-stream");
|
|
37
|
+
const url = `data:${mime};base64,${b64}`;
|
|
38
|
+
const r2Key = `mock/${hashShort(filename + ":" + b64)}/${filename || "blob"}`;
|
|
39
|
+
return {
|
|
40
|
+
download_url: url,
|
|
41
|
+
r2_key: r2Key,
|
|
42
|
+
size_bytes: Math.floor(b64.length * .75),
|
|
43
|
+
expires_at: new Date(Date.now() + 3600 * 1e3).toISOString()
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (mode === "negotiate") {
|
|
47
|
+
const key = `mock/${hashShort(filename + Date.now())}/${filename || "blob"}`;
|
|
48
|
+
return {
|
|
49
|
+
put_url: `https://mock.local/${key}?signature=mock`,
|
|
50
|
+
headers: { "content-type": String(params.mime_type ?? "") },
|
|
51
|
+
r2_key: key,
|
|
52
|
+
expires_at: new Date(Date.now() + 600 * 1e3).toISOString()
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (mode === "confirm") {
|
|
56
|
+
const key = String(params.r2_key ?? "");
|
|
57
|
+
return {
|
|
58
|
+
download_url: `https://mock.local/${key}`,
|
|
59
|
+
r2_key: key,
|
|
60
|
+
size_bytes: 0,
|
|
61
|
+
expires_at: new Date(Date.now() + 3600 * 1e3).toISOString()
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
throw withCode(new Error(`unknown upload mode: ${mode}`), -32203);
|
|
65
|
+
}
|
|
66
|
+
account() {
|
|
67
|
+
const acc = getAccount(this.opts.account);
|
|
68
|
+
if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-upload <fixture>` / `--no-upload`)"), -32201);
|
|
69
|
+
if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32201);
|
|
70
|
+
return acc;
|
|
71
|
+
}
|
|
72
|
+
async mint() {
|
|
73
|
+
if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
|
|
74
|
+
const acc = this.account();
|
|
75
|
+
if (!this.opts.appSlug) throw withCode(new Error("host upload bridge has no app_slug — pass `--app-slug <slug>`"), -32203);
|
|
76
|
+
const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
|
|
77
|
+
const res = await fetch(url, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "content-type": "application/json" },
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
pat: acc.pat,
|
|
82
|
+
kind: "complete",
|
|
83
|
+
app_slug: this.opts.appSlug
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const text = await res.text().catch(() => "");
|
|
88
|
+
throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32207);
|
|
89
|
+
}
|
|
90
|
+
const body = await res.json();
|
|
91
|
+
this.cachedSession = {
|
|
92
|
+
appSessionToken: body.app_session_token,
|
|
93
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
|
|
94
|
+
};
|
|
95
|
+
return this.cachedSession;
|
|
96
|
+
}
|
|
97
|
+
async realCall(params) {
|
|
98
|
+
const acc = this.account();
|
|
99
|
+
const session = await this.mint();
|
|
100
|
+
const mode = String(params.mode ?? "inline");
|
|
101
|
+
const path = mode === "negotiate" ? "/api/v1/copilot/app/upload/negotiate" : mode === "confirm" ? "/api/v1/copilot/app/upload/confirm" : "/api/v1/copilot/app/upload";
|
|
102
|
+
const res = await fetch(`${canonicalHost(acc.host)}${path}`, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: {
|
|
105
|
+
"content-type": "application/json",
|
|
106
|
+
authorization: `Bearer ${session.appSessionToken}`
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(params)
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
const text = await res.text().catch(() => "");
|
|
112
|
+
throw withCode(new Error(`HTTP ${res.status}: ${text}`), httpToUploadCode(res.status));
|
|
113
|
+
}
|
|
114
|
+
return res.json();
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
function withCode(err, code) {
|
|
118
|
+
err.rpcCode = code;
|
|
119
|
+
return err;
|
|
120
|
+
}
|
|
121
|
+
function hashShort(s) {
|
|
122
|
+
return createHash("sha256").update(s).digest("hex").slice(0, 12);
|
|
123
|
+
}
|
|
124
|
+
function httpToUploadCode(status) {
|
|
125
|
+
if (status === 403) return -32201;
|
|
126
|
+
if (status === 429) return -32202;
|
|
127
|
+
if (status === 400) return -32203;
|
|
128
|
+
if (status === 413) return -32204;
|
|
129
|
+
if (status === 415) return -32205;
|
|
130
|
+
if (status === 404) return -32212;
|
|
131
|
+
if (status === 504) return -32208;
|
|
132
|
+
return -32207;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
export { HostUploadBridge };
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { canonicalHost, getAccount } from "./credentials-BTv2IfUZ.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
|
|
5
|
+
//#region src/executa/image.ts
|
|
6
|
+
var ImageBridge = class {
|
|
7
|
+
mocks = [];
|
|
8
|
+
cachedSession = null;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
if (opts.mode === "mock" && opts.mockFile) {
|
|
12
|
+
const path = resolve(opts.mockFile);
|
|
13
|
+
if (existsSync(path)) for (const line of readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
14
|
+
if (!line.trim() || line.startsWith("#")) continue;
|
|
15
|
+
try {
|
|
16
|
+
this.mocks.push(JSON.parse(line));
|
|
17
|
+
} catch {}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async generate(req) {
|
|
22
|
+
if (this.opts.mode === "off") throw withCode(new Error("image generation disabled — rerun without `--no-image`"), -32101);
|
|
23
|
+
if (this.opts.mode === "mock") return this.pickMock("generate", req.prompt);
|
|
24
|
+
return this.realCall("/api/v1/copilot/app/image/generate", req);
|
|
25
|
+
}
|
|
26
|
+
async edit(req) {
|
|
27
|
+
if (this.opts.mode === "off") throw withCode(new Error("image edit disabled — rerun without `--no-image`"), -32101);
|
|
28
|
+
if (this.opts.mode === "mock") return this.pickMock("edit", req.prompt);
|
|
29
|
+
return this.realCall("/api/v1/copilot/app/image/edit", req);
|
|
30
|
+
}
|
|
31
|
+
pickMock(method, prompt) {
|
|
32
|
+
const wantedMethod = method === "generate" ? "generate" : "edit";
|
|
33
|
+
const candidates = this.mocks.filter((m) => m.ns === "image" && m.method === wantedMethod);
|
|
34
|
+
const matched = candidates.find((m) => m.match?.promptIncludes && prompt.includes(m.match.promptIncludes)) ?? candidates[0];
|
|
35
|
+
if (matched && matched.result) return normaliseImageResult(matched.result);
|
|
36
|
+
return {
|
|
37
|
+
images: [{
|
|
38
|
+
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=",
|
|
39
|
+
mimeType: "image/png"
|
|
40
|
+
}],
|
|
41
|
+
model: "mock-image-model"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
account() {
|
|
45
|
+
const acc = getAccount(this.opts.account);
|
|
46
|
+
if (!acc) throw withCode(new Error("no PAT on disk — run `anna-app login --host <nexus-url>` first (or pass `--mock-image <fixture>` / `--no-image`)"), -32101);
|
|
47
|
+
if (acc.expires_at && acc.expires_at < Math.floor(Date.now() / 1e3)) throw withCode(new Error("PAT expired — run `anna-app login` again"), -32101);
|
|
48
|
+
return acc;
|
|
49
|
+
}
|
|
50
|
+
async mint() {
|
|
51
|
+
if (this.cachedSession && this.cachedSession.expiresAt - 30 > Math.floor(Date.now() / 1e3)) return this.cachedSession;
|
|
52
|
+
const acc = this.account();
|
|
53
|
+
if (!this.opts.appSlug) throw withCode(new Error("image bridge has no app_slug — pass `--app-slug <slug>` to `anna-app executa dev` so nexus can attribute the spend"), -32104);
|
|
54
|
+
const url = `${canonicalHost(acc.host)}/api/v1/anna-apps/dev/session/mint`;
|
|
55
|
+
const res = await fetch(url, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
headers: { "content-type": "application/json" },
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
pat: acc.pat,
|
|
60
|
+
kind: "complete",
|
|
61
|
+
app_slug: this.opts.appSlug
|
|
62
|
+
})
|
|
63
|
+
});
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
const text = await res.text().catch(() => "");
|
|
66
|
+
throw withCode(new Error(`session.mint failed: HTTP ${res.status} ${text}`), -32103);
|
|
67
|
+
}
|
|
68
|
+
const body = await res.json();
|
|
69
|
+
this.cachedSession = {
|
|
70
|
+
appSessionToken: body.app_session_token,
|
|
71
|
+
expiresAt: Math.floor(Date.now() / 1e3) + (body.expires_in || 600)
|
|
72
|
+
};
|
|
73
|
+
return this.cachedSession;
|
|
74
|
+
}
|
|
75
|
+
async realCall(path, body) {
|
|
76
|
+
const acc = this.account();
|
|
77
|
+
const session = await this.mint();
|
|
78
|
+
const res = await fetch(`${canonicalHost(acc.host)}${path}`, {
|
|
79
|
+
method: "POST",
|
|
80
|
+
headers: {
|
|
81
|
+
"content-type": "application/json",
|
|
82
|
+
authorization: `Bearer ${session.appSessionToken}`
|
|
83
|
+
},
|
|
84
|
+
body: JSON.stringify(body)
|
|
85
|
+
});
|
|
86
|
+
if (!res.ok) {
|
|
87
|
+
const text = await res.text().catch(() => "");
|
|
88
|
+
throw withCode(
|
|
89
|
+
new Error(`HTTP ${res.status}: ${text}`),
|
|
90
|
+
// Map common HTTP statuses to canonical image codes; otherwise
|
|
91
|
+
// let the host's body propagate.
|
|
92
|
+
httpToImageCode(res.status)
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const out = await res.json();
|
|
96
|
+
return normaliseImageResult(out);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
function withCode(err, code) {
|
|
100
|
+
err.rpcCode = code;
|
|
101
|
+
return err;
|
|
102
|
+
}
|
|
103
|
+
function httpToImageCode(status) {
|
|
104
|
+
if (status === 403) return -32101;
|
|
105
|
+
if (status === 429) return -32102;
|
|
106
|
+
if (status === 400) return -32104;
|
|
107
|
+
if (status === 504) return -32105;
|
|
108
|
+
return -32103;
|
|
109
|
+
}
|
|
110
|
+
function normaliseImageResult(raw) {
|
|
111
|
+
if (!raw || typeof raw !== "object") return {
|
|
112
|
+
images: [],
|
|
113
|
+
model: "unknown"
|
|
114
|
+
};
|
|
115
|
+
const o = raw;
|
|
116
|
+
const images = Array.isArray(o.images) ? o.images.map((img) => ({
|
|
117
|
+
url: String(img.url ?? ""),
|
|
118
|
+
mimeType: img.mimeType ? String(img.mimeType) : void 0,
|
|
119
|
+
width: typeof img.width === "number" ? img.width : void 0,
|
|
120
|
+
height: typeof img.height === "number" ? img.height : void 0
|
|
121
|
+
})) : [];
|
|
122
|
+
return {
|
|
123
|
+
images,
|
|
124
|
+
model: typeof o.model === "string" ? o.model : void 0,
|
|
125
|
+
quota_used: o.quota_used && typeof o.quota_used === "object" ? o.quota_used : void 0,
|
|
126
|
+
_meta: o._meta && typeof o._meta === "object" ? o._meta : void 0
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
//#endregion
|
|
131
|
+
export { ImageBridge };
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import kleur from "kleur";
|
|
2
|
+
|
|
3
|
+
//#region src/mascot.ts
|
|
4
|
+
const FACE = [
|
|
5
|
+
255,
|
|
6
|
+
227,
|
|
7
|
+
234
|
|
8
|
+
];
|
|
9
|
+
const EYE = [
|
|
10
|
+
122,
|
|
11
|
+
90,
|
|
12
|
+
101
|
|
13
|
+
];
|
|
14
|
+
const GEO = (() => {
|
|
15
|
+
const VX0 = 252, VY0 = 280, VW = 520;
|
|
16
|
+
const n = (x, y) => [(x - VX0) / VW, (y - VY0) / VW];
|
|
17
|
+
const r = (v) => v / VW;
|
|
18
|
+
const [fcx, fcy] = n(512, 540);
|
|
19
|
+
const [lex, ley] = n(462, 520);
|
|
20
|
+
const [rex, rey] = n(562, 520);
|
|
21
|
+
return {
|
|
22
|
+
face: {
|
|
23
|
+
cx: fcx,
|
|
24
|
+
cy: fcy,
|
|
25
|
+
r: r(260)
|
|
26
|
+
},
|
|
27
|
+
eyeL: {
|
|
28
|
+
cx: lex,
|
|
29
|
+
cy: ley,
|
|
30
|
+
r: r(34)
|
|
31
|
+
},
|
|
32
|
+
eyeR: {
|
|
33
|
+
cx: rex,
|
|
34
|
+
cy: rey,
|
|
35
|
+
r: r(34)
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
/** Sample the SVG at normalized (x, y) ∈ [0, 1]². Returns null for empty. */
|
|
40
|
+
function sampleSvg(x, y) {
|
|
41
|
+
const inCircle = (cx, cy, r) => {
|
|
42
|
+
const dx = x - cx, dy = y - cy;
|
|
43
|
+
return dx * dx + dy * dy <= r * r;
|
|
44
|
+
};
|
|
45
|
+
let c = null;
|
|
46
|
+
if (inCircle(GEO.face.cx, GEO.face.cy, GEO.face.r)) c = FACE;
|
|
47
|
+
return c;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 2×2 supersampled pixel using **majority vote** (not averaging).
|
|
51
|
+
* Anti-aliases silhouette edges (filled vs empty) while keeping every
|
|
52
|
+
* pixel a pure palette color — no muddy blends between the pink face and
|
|
53
|
+
* the dark eyes. Ties resolve in favor of the topmost layer encountered.
|
|
54
|
+
*/
|
|
55
|
+
function pixel(px, py, w, h) {
|
|
56
|
+
const counts = new Map();
|
|
57
|
+
let nulls = 0;
|
|
58
|
+
for (const oy of [.25, .75]) for (const ox of [.25, .75]) {
|
|
59
|
+
const s = sampleSvg((px + ox) / w, (py + oy) / h);
|
|
60
|
+
if (!s) {
|
|
61
|
+
nulls++;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const key = `${s[0]},${s[1]},${s[2]}`;
|
|
65
|
+
const entry = counts.get(key);
|
|
66
|
+
if (entry) entry.n++;
|
|
67
|
+
else counts.set(key, {
|
|
68
|
+
color: s,
|
|
69
|
+
n: 1
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (counts.size === 0) return null;
|
|
73
|
+
let best = null;
|
|
74
|
+
for (const e of counts.values()) if (!best || e.n > best.n) best = e;
|
|
75
|
+
if (nulls > (best?.n ?? 0)) return null;
|
|
76
|
+
return best.color;
|
|
77
|
+
}
|
|
78
|
+
const FG = (c) => `\x1b[38;2;${c[0]};${c[1]};${c[2]}m`;
|
|
79
|
+
const BG = (c) => `\x1b[48;2;${c[0]};${c[1]};${c[2]}m`;
|
|
80
|
+
const BG_DEFAULT = "\x1B[49m";
|
|
81
|
+
const RESET = "\x1B[0m";
|
|
82
|
+
function eq(a, b) {
|
|
83
|
+
if (a === b) return true;
|
|
84
|
+
if (!a || !b) return false;
|
|
85
|
+
return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Rasterize the face into a list of half-block lines.
|
|
89
|
+
* `cols` is character width; pixel rows are derived from the cell aspect
|
|
90
|
+
* so each pixel covers an (approximately) square area on screen.
|
|
91
|
+
*/
|
|
92
|
+
function renderFace(cols, cellAspect) {
|
|
93
|
+
const W = cols;
|
|
94
|
+
const charRows = Math.max(1, Math.round(cols / cellAspect));
|
|
95
|
+
const H = charRows * 2;
|
|
96
|
+
const eyeRow = Math.round(GEO.eyeL.cy * charRows - .5);
|
|
97
|
+
const eyeColL = Math.max(0, Math.round(GEO.eyeL.cx * W) - 1);
|
|
98
|
+
const eyeColR = Math.max(0, Math.round(GEO.eyeR.cx * W) - 1);
|
|
99
|
+
const isEyeCell = (cx, cy) => cy === eyeRow && (cx >= eyeColL && cx < eyeColL + 2 || cx >= eyeColR && cx < eyeColR + 2);
|
|
100
|
+
const rows = [];
|
|
101
|
+
for (let cy = 0; cy < charRows; cy++) {
|
|
102
|
+
let line = "";
|
|
103
|
+
let lastFg = null;
|
|
104
|
+
let lastBg = null;
|
|
105
|
+
let lastBgDefault = true;
|
|
106
|
+
for (let cx = 0; cx < W; cx++) {
|
|
107
|
+
if (isEyeCell(cx, cy)) {
|
|
108
|
+
if (!eq(lastFg, EYE)) {
|
|
109
|
+
line += FG(EYE);
|
|
110
|
+
lastFg = EYE;
|
|
111
|
+
}
|
|
112
|
+
if (lastBgDefault || !eq(lastBg, FACE)) {
|
|
113
|
+
line += BG(FACE);
|
|
114
|
+
lastBg = FACE;
|
|
115
|
+
lastBgDefault = false;
|
|
116
|
+
}
|
|
117
|
+
line += "█";
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const top = pixel(cx, cy * 2, W, H);
|
|
121
|
+
const bot = pixel(cx, cy * 2 + 1, W, H);
|
|
122
|
+
if (!top && !bot) {
|
|
123
|
+
if (!lastBgDefault) {
|
|
124
|
+
line += BG_DEFAULT;
|
|
125
|
+
lastBgDefault = true;
|
|
126
|
+
lastBg = null;
|
|
127
|
+
}
|
|
128
|
+
line += " ";
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (top && bot) {
|
|
132
|
+
if (!eq(lastFg, top)) {
|
|
133
|
+
line += FG(top);
|
|
134
|
+
lastFg = top;
|
|
135
|
+
}
|
|
136
|
+
if (lastBgDefault || !eq(lastBg, bot)) {
|
|
137
|
+
line += BG(bot);
|
|
138
|
+
lastBg = bot;
|
|
139
|
+
lastBgDefault = false;
|
|
140
|
+
}
|
|
141
|
+
line += "▀";
|
|
142
|
+
} else if (top) {
|
|
143
|
+
if (!eq(lastFg, top)) {
|
|
144
|
+
line += FG(top);
|
|
145
|
+
lastFg = top;
|
|
146
|
+
}
|
|
147
|
+
if (!lastBgDefault) {
|
|
148
|
+
line += BG_DEFAULT;
|
|
149
|
+
lastBgDefault = true;
|
|
150
|
+
lastBg = null;
|
|
151
|
+
}
|
|
152
|
+
line += "▀";
|
|
153
|
+
} else {
|
|
154
|
+
if (!eq(lastFg, bot)) {
|
|
155
|
+
line += FG(bot);
|
|
156
|
+
lastFg = bot;
|
|
157
|
+
}
|
|
158
|
+
if (!lastBgDefault) {
|
|
159
|
+
line += BG_DEFAULT;
|
|
160
|
+
lastBgDefault = true;
|
|
161
|
+
lastBg = null;
|
|
162
|
+
}
|
|
163
|
+
line += "▄";
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
rows.push(line + RESET);
|
|
167
|
+
}
|
|
168
|
+
return rows;
|
|
169
|
+
}
|
|
170
|
+
function mascotEnabled() {
|
|
171
|
+
if (process.env.ANNA_CLI_NO_MASCOT) return false;
|
|
172
|
+
if (process.env.NO_COLOR) return false;
|
|
173
|
+
if (process.env.ANNA_CLI_FORCE_MASCOT) return true;
|
|
174
|
+
return Boolean(process.stdout.isTTY);
|
|
175
|
+
}
|
|
176
|
+
function pickWidth() {
|
|
177
|
+
const override = Number.parseInt(process.env.ANNA_CLI_MASCOT_WIDTH ?? "", 10);
|
|
178
|
+
if (Number.isFinite(override) && override >= 10 && override <= 120) return override;
|
|
179
|
+
const cols = process.stdout.columns || 80;
|
|
180
|
+
if (cols < 60) return 10;
|
|
181
|
+
if (cols >= 120) return 16;
|
|
182
|
+
return 13;
|
|
183
|
+
}
|
|
184
|
+
function pickAspect() {
|
|
185
|
+
const v = Number.parseFloat(process.env.ANNA_CLI_MASCOT_ASPECT ?? "");
|
|
186
|
+
if (Number.isFinite(v) && v >= 1.2 && v <= 3) return v;
|
|
187
|
+
return 2.1;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Render the mascot as a multi-line string. Always returns the colored art
|
|
191
|
+
* (callers decide whether to print based on `mascotEnabled`).
|
|
192
|
+
*/
|
|
193
|
+
function renderMascot(message) {
|
|
194
|
+
const cols = pickWidth();
|
|
195
|
+
const aspect = pickAspect();
|
|
196
|
+
const face = renderFace(cols, aspect);
|
|
197
|
+
const pad = " ".repeat(2);
|
|
198
|
+
const label = kleur.magenta().bold("Anna");
|
|
199
|
+
const tagline = kleur.gray("Anna App developer CLI");
|
|
200
|
+
const textLines = [`${label} ${tagline}`];
|
|
201
|
+
if (message) textLines.push(`${kleur.gray("›")} ${message}`);
|
|
202
|
+
const start = Math.max(0, Math.floor((face.length - textLines.length) / 2));
|
|
203
|
+
const lines = [];
|
|
204
|
+
for (let i = 0; i < face.length; i++) {
|
|
205
|
+
const tIdx = i - start;
|
|
206
|
+
const text = tIdx >= 0 && tIdx < textLines.length ? " " + textLines[tIdx] : "";
|
|
207
|
+
lines.push(pad + face[i] + text);
|
|
208
|
+
}
|
|
209
|
+
return lines.join("\n");
|
|
210
|
+
}
|
|
211
|
+
/** Print the mascot to stderr if enabled. Stderr keeps stdout clean for piping. */
|
|
212
|
+
function printMascot(message) {
|
|
213
|
+
if (!mascotEnabled()) return;
|
|
214
|
+
process.stderr.write(renderMascot(message) + "\n\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
//#endregion
|
|
218
|
+
export { printMascot };
|