@anna-ai/cli 0.1.12 → 0.1.14

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 (40) hide show
  1. package/dist/agent-DUmINbo4.js +372 -0
  2. package/dist/{apps-CDe6Fjq2.js → apps-BEJUn9Ws.js} +1 -1
  3. package/dist/bridge-C0DWb5eQ.js +3 -0
  4. package/dist/cli.js +72 -9
  5. package/dist/{dev-DoY58pBM.js → dev-BfLGxpiT.js} +4 -4
  6. package/dist/dev-C81H9c9_.js +3 -0
  7. package/dist/dev-account-DCyjamBa.js +44 -0
  8. package/dist/{dev-app-cache-cXvO2XwQ.js → dev-app-cache-CZ8lIKiw.js} +1 -1
  9. package/dist/{doctor-DP2UB10l.js → doctor-B3u0edUg.js} +1 -1
  10. package/dist/executa-dev-BhouP8jh.js +212 -0
  11. package/dist/executa-init-COEmKDOE.js +68 -0
  12. package/dist/executa-register-66WKIwQQ.js +47 -0
  13. package/dist/mascot-wlYTJqMs.js +218 -0
  14. package/dist/runner-Bral1LFW.js +279 -0
  15. package/dist/sampling-3EfSlDHM.js +155 -0
  16. package/dist/storage-CnWTZqq_.js +316 -0
  17. package/package.json +1 -1
  18. package/templates/executa/go/README.md +10 -0
  19. package/templates/executa/go/executa.json +4 -0
  20. package/templates/executa/go/go.mod +3 -0
  21. package/templates/executa/go/main.go +148 -0
  22. package/templates/executa/node/README.md +12 -0
  23. package/templates/executa/node/executa.json +4 -0
  24. package/templates/executa/node/package.json +12 -0
  25. package/templates/executa/node/plugin.mjs +126 -0
  26. package/templates/executa/node/sampling-fixture.jsonl +1 -0
  27. package/templates/executa/python/README.md +23 -0
  28. package/templates/executa/python/__SLUG_PY___plugin.py +146 -0
  29. package/templates/executa/python/executa.json +4 -0
  30. package/templates/executa/python/pyproject.toml +15 -0
  31. package/templates/executa/python/sampling-fixture.jsonl +4 -0
  32. package/dist/bridge-BEHyfpPI.js +0 -3
  33. /package/dist/{bridge-BQUo6ehX.js → bridge-D6YyP9DM.js} +0 -0
  34. /package/dist/{credentials-CIOYq2Lm.js → credentials-DDqx6XMQ.js} +0 -0
  35. /package/dist/{dev-app-cache-BMfOlTHd.js → dev-app-cache-C3D1Sp_V.js} +0 -0
  36. /package/dist/{fixture-BEu4LXLG.js → fixture-CATHyLLI.js} +0 -0
  37. /package/dist/{login-dl1Zfny8.js → login-CsIVbrmf.js} +0 -0
  38. /package/dist/{logout-DablvlFs.js → logout-gfmKQxMj.js} +0 -0
  39. /package/dist/{server-NXmiWJjX.js → server-q6nKCeEV.js} +0 -0
  40. /package/dist/{whoami-giXOY415.js → whoami-BS5wy-Nh.js} +0 -0
@@ -0,0 +1,212 @@
1
+ import { parseExecutaSpec } from "./dev-BfLGxpiT.js";
2
+ import { isAbsolute, resolve } from "node:path";
3
+ import { existsSync } from "node:fs";
4
+ import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
5
+ import * as readline from "node:readline";
6
+ import { createInterface as createInterface$1 } from "node:readline/promises";
7
+
8
+ //#region src/commands/executa-dev.ts
9
+ /**
10
+ * Mutable hook the REPL installs while it's waiting at a prompt, so
11
+ * out-of-band stderr lines from the executa subprocess don't garble
12
+ * the prompt. When set, the stderr sink clears the current readline
13
+ * line on stdout, prints the message, then asks readline to redraw
14
+ * the prompt + any pending user input.
15
+ */
16
+ let activeRepl = null;
17
+ async function runExecutaDev(opts) {
18
+ const cwd = process.cwd();
19
+ const dir = opts.dir ? isAbsolute(opts.dir) ? opts.dir : resolve(cwd, opts.dir) : cwd;
20
+ if (!existsSync(dir)) {
21
+ console.error(red(`✗ dir not found: ${dir}`));
22
+ return 2;
23
+ }
24
+ const specStr = opts.spec ? prependDirIfMissing(opts.spec, dir) : `dir=${dir}`;
25
+ const parsed = parseExecutaSpec(specStr, cwd);
26
+ if (parsed instanceof Error) {
27
+ console.error(red(`✗ ${parsed.message}`));
28
+ return 2;
29
+ }
30
+ if (!parsed.command || parsed.command.length === 0) {
31
+ console.error(red(`✗ could not derive a launch command for ${dir}`));
32
+ return 2;
33
+ }
34
+ const oneShot = !!(opts.describe || opts.health || opts.invoke);
35
+ const quiet = oneShot && (opts.json ?? false);
36
+ const { SamplingBridge } = await import("./sampling-3EfSlDHM.js");
37
+ const sampling = opts.noSampling ? new SamplingBridge({ mode: "off" }) : opts.mockSampling ? new SamplingBridge({
38
+ mode: "mock",
39
+ mockFile: opts.mockSampling
40
+ }) : opts.appSlug ? new SamplingBridge({
41
+ mode: "real",
42
+ account: opts.samplingAccount,
43
+ appSlug: opts.appSlug
44
+ }) : new SamplingBridge({ mode: "off" });
45
+ const { AgentBridge } = await import("./agent-DUmINbo4.js");
46
+ const agent = opts.noAgent ? new AgentBridge({ mode: "off" }) : opts.mockAgent ? new AgentBridge({
47
+ mode: "mock",
48
+ mockFile: opts.mockAgent
49
+ }) : opts.appSlug ? new AgentBridge({
50
+ mode: "real",
51
+ account: opts.agentAccount ?? opts.samplingAccount,
52
+ appSlug: opts.appSlug
53
+ }) : new AgentBridge({ mode: "off" });
54
+ const { StorageBridge } = await import("./storage-CnWTZqq_.js");
55
+ const storageMode = opts.storage ?? (opts.mockStorage ? "mock" : "memory");
56
+ const storage = new StorageBridge({
57
+ mode: storageMode,
58
+ mockFile: opts.mockStorage,
59
+ account: opts.storageAccount ?? opts.samplingAccount,
60
+ appSlug: opts.appSlug,
61
+ scopes: opts.storageScopes ? opts.storageScopes.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
62
+ pluginName: parsed.tool_id
63
+ });
64
+ const { ExecutaRunner } = await import("./runner-Bral1LFW.js");
65
+ const runner = new ExecutaRunner({
66
+ command: parsed.command,
67
+ cwd: parsed.project_dir,
68
+ sampling,
69
+ agent,
70
+ storage,
71
+ onStderr: (line) => {
72
+ const text = quiet ? `${line}\n` : dim(`[executa] ${line}\n`);
73
+ writeStderrCooperative(text);
74
+ }
75
+ });
76
+ if (!quiet) {
77
+ console.log(bold(cyan("anna-app executa dev")));
78
+ console.log(` tool_id ${dim(parsed.tool_id)}`);
79
+ console.log(` dir ${dim(parsed.project_dir)}`);
80
+ console.log(` command ${dim(parsed.command.join(" "))}`);
81
+ console.log(` sampling ${dim(opts.noSampling ? "disabled (--no-sampling)" : opts.mockSampling ? `mock (${opts.mockSampling})` : opts.appSlug ? `real → app_slug=${opts.appSlug}` : "disabled (no fixture, no --app-slug)")}`);
82
+ console.log(` agent ${dim(opts.noAgent ? "disabled (--no-agent)" : opts.mockAgent ? `mock (${opts.mockAgent})` : opts.appSlug ? `real → app_slug=${opts.appSlug}` : "disabled (no fixture, no --app-slug)")}`);
83
+ console.log(` storage ${dim(storageMode === "mock" ? `mock (${opts.mockStorage ?? "<no-fixture>"})` : storageMode === "real" ? `real → app_slug=${opts.appSlug ?? "<unset>"}` : storageMode)}`);
84
+ }
85
+ let init;
86
+ try {
87
+ init = await runner.start();
88
+ } catch (e) {
89
+ console.error(red(`✗ executa failed to start: ${e.message}`));
90
+ return 2;
91
+ }
92
+ if (!quiet) console.log(` ${green("✓")} negotiated protocol ${bold(init.protocolVersion)}`);
93
+ let exitCode = 0;
94
+ try {
95
+ if (oneShot) exitCode = await runOneShot(runner, opts, quiet);
96
+ else exitCode = await runRepl(runner);
97
+ } finally {
98
+ await runner.stop();
99
+ }
100
+ return exitCode;
101
+ }
102
+ function prependDirIfMissing(spec, dir) {
103
+ if (/\bdir\s*=/.test(spec)) return spec;
104
+ return `dir=${dir},${spec}`;
105
+ }
106
+ async function runOneShot(runner, opts, quiet) {
107
+ const print = (v) => {
108
+ process.stdout.write(`${JSON.stringify(v, null, quiet ? 0 : 2)}\n`);
109
+ };
110
+ try {
111
+ if (opts.describe) {
112
+ print(await runner.describe());
113
+ return 0;
114
+ }
115
+ if (opts.health) {
116
+ print(await runner.health());
117
+ return 0;
118
+ }
119
+ if (opts.invoke) {
120
+ let args = {};
121
+ if (opts.args) try {
122
+ const parsed = JSON.parse(opts.args);
123
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("--args must be a JSON object");
124
+ args = parsed;
125
+ } catch (e) {
126
+ console.error(red(`✗ --args: ${e.message}`));
127
+ return 2;
128
+ }
129
+ print(await runner.invoke(opts.invoke, args));
130
+ return 0;
131
+ }
132
+ return 0;
133
+ } catch (e) {
134
+ console.error(red(`✗ ${e.message}`));
135
+ return 1;
136
+ }
137
+ }
138
+ async function runRepl(runner) {
139
+ const rl = createInterface$1({
140
+ input: process.stdin,
141
+ output: process.stdout
142
+ });
143
+ console.log(yellow(" REPL ready. Commands: describe | health | invoke <tool> <json?> | quit"));
144
+ activeRepl = { rl };
145
+ for (;;) {
146
+ let line;
147
+ try {
148
+ line = (await rl.question(cyan("executa> "))).trim();
149
+ } catch {
150
+ break;
151
+ }
152
+ if (!line) continue;
153
+ if (line === "quit" || line === "exit" || line === ".q") break;
154
+ try {
155
+ if (line === "describe") {
156
+ const out = await runner.describe();
157
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
158
+ } else if (line === "health") {
159
+ const out = await runner.health();
160
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
161
+ } else if (line.startsWith("invoke")) {
162
+ const rest = line.slice(6).trim();
163
+ const sp = rest.indexOf(" ");
164
+ const tool = sp === -1 ? rest : rest.slice(0, sp);
165
+ const argsStr = sp === -1 ? "{}" : rest.slice(sp + 1).trim() || "{}";
166
+ if (!tool) {
167
+ console.error(red("usage: invoke <tool> <json-args?>"));
168
+ continue;
169
+ }
170
+ let args;
171
+ try {
172
+ const parsed = JSON.parse(argsStr);
173
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("args must be a JSON object");
174
+ args = parsed;
175
+ } catch (e) {
176
+ console.error(red(`bad args: ${e.message}`));
177
+ continue;
178
+ }
179
+ const out = await runner.invoke(tool, args);
180
+ process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
181
+ } else console.error(yellow(`unknown command: ${line}`));
182
+ } catch (e) {
183
+ console.error(red(`✗ ${e.message}`));
184
+ }
185
+ }
186
+ activeRepl = null;
187
+ rl.close();
188
+ return 0;
189
+ }
190
+ /**
191
+ * Write to stderr without trampling the REPL prompt. When the REPL is
192
+ * idle (no `activeRepl`) or stdout is not a TTY, this is a plain
193
+ * `process.stderr.write`. When the REPL is at a prompt, we clear the
194
+ * prompt line on stdout, emit the stderr text, then redraw the prompt
195
+ * with any user input the reader had already buffered.
196
+ */
197
+ function writeStderrCooperative(text) {
198
+ const repl = activeRepl;
199
+ if (!repl || !process.stdout.isTTY) {
200
+ process.stderr.write(text);
201
+ return;
202
+ }
203
+ readline.cursorTo(process.stdout, 0);
204
+ readline.clearLine(process.stdout, 0);
205
+ process.stderr.write(text);
206
+ const rl = repl.rl;
207
+ if (typeof rl._refreshLine === "function") rl._refreshLine();
208
+ else process.stdout.write(cyan("executa> "));
209
+ }
210
+
211
+ //#endregion
212
+ export { runExecutaDev };
@@ -0,0 +1,68 @@
1
+ import { printMascot } from "./mascot-wlYTJqMs.js";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
4
+ import { fileURLToPath } from "node:url";
5
+ import kleur from "kleur";
6
+
7
+ //#region src/commands/executa-init.ts
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ function templateRoot(template) {
10
+ for (const cand of [resolve(here, "..", "..", "templates", "executa", template), resolve(here, "..", "templates", "executa", template)]) if (existsSync(cand)) return cand;
11
+ throw new Error(`executa template not found: ${template}`);
12
+ }
13
+ function substitute(content, slug, toolId) {
14
+ const slugPy = slug.replace(/-/g, "_");
15
+ return content.replace(/__SLUG_PY__/g, slugPy).replace(/__SLUG__/g, slug).replace(/__TOOL_ID__/g, toolId);
16
+ }
17
+ function copyDirWithSubst(src, dst, slug, toolId) {
18
+ mkdirSync(dst, { recursive: true });
19
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
20
+ const s = join(src, entry.name);
21
+ const d = join(dst, substitute(entry.name, slug, toolId));
22
+ if (entry.isDirectory()) copyDirWithSubst(s, d, slug, toolId);
23
+ else if (entry.isFile()) {
24
+ const stat = statSync(s);
25
+ if (stat.size < 256 * 1024) {
26
+ const buf = readFileSync(s);
27
+ if (!buf.includes(0)) {
28
+ writeFileSync(d, substitute(buf.toString("utf-8"), slug, toolId), "utf-8");
29
+ continue;
30
+ }
31
+ }
32
+ cpSync(s, d);
33
+ }
34
+ }
35
+ }
36
+ function runExecutaInit(opts) {
37
+ const target = resolve(process.cwd(), opts.targetDir);
38
+ if (existsSync(target) && !opts.force) {
39
+ if (readdirSync(target).filter((n) => !n.startsWith(".")).length > 0) {
40
+ console.error(kleur.red(`✗ target dir not empty: ${target} (use --force to override)`));
41
+ return 1;
42
+ }
43
+ }
44
+ if (!/^[a-z][a-z0-9-]{1,40}$/.test(opts.slug)) {
45
+ console.error(kleur.red(`✗ invalid slug "${opts.slug}": must match /^[a-z][a-z0-9-]{1,40}$/`));
46
+ return 1;
47
+ }
48
+ const toolId = opts.toolId ?? `tool-dev-${opts.slug}`;
49
+ if (!/^[a-z][a-z0-9-]{1,80}$/.test(toolId)) {
50
+ console.error(kleur.red(`✗ invalid tool_id "${toolId}": must match /^[a-z][a-z0-9-]{1,80}$/`));
51
+ return 1;
52
+ }
53
+ let tplRoot;
54
+ try {
55
+ tplRoot = templateRoot(opts.template);
56
+ } catch (e) {
57
+ console.error(kleur.red(`✗ ${e.message}`));
58
+ return 1;
59
+ }
60
+ copyDirWithSubst(tplRoot, target, opts.slug, toolId);
61
+ printMascot(`scaffolded an executa — happy hacking!`);
62
+ console.log(kleur.green(`✓ scaffolded "${opts.slug}" (${opts.template}) at ${target}`));
63
+ console.log(kleur.gray(` next: cd ${opts.targetDir} && anna-app executa dev --describe`));
64
+ return 0;
65
+ }
66
+
67
+ //#endregion
68
+ export { runExecutaInit };
@@ -0,0 +1,47 @@
1
+ import { getAccount } from "./credentials-BTv2IfUZ.js";
2
+ import { bold, cyan, dim, green, red } from "kleur/colors";
3
+
4
+ //#region src/commands/executa-register.ts
5
+ async function runExecutaRegister(opts) {
6
+ const acc = getAccount(opts.account);
7
+ if (!acc) {
8
+ console.error(red("✗ no PAT on disk — run `anna-app login --host <nexus-url>` first."));
9
+ return 2;
10
+ }
11
+ const body = {
12
+ pat: acc.pat,
13
+ tool_id: opts.toolId,
14
+ slug: opts.slug,
15
+ name: opts.name
16
+ };
17
+ const url = `${acc.host.replace(/\/$/, "")}/api/v1/anna-apps/dev/executas/register`;
18
+ let res;
19
+ try {
20
+ res = await fetch(url, {
21
+ method: "POST",
22
+ headers: { "content-type": "application/json" },
23
+ body: JSON.stringify(body)
24
+ });
25
+ } catch (e) {
26
+ console.error(red(`✗ network error: ${e.message}`));
27
+ return 2;
28
+ }
29
+ if (res.status === 404) {
30
+ console.error(red("✗ your nexus does not expose POST /api/v1/anna-apps/dev/executas/register — upgrade matrix-nexus to a version that ships this endpoint."));
31
+ return 2;
32
+ }
33
+ if (!res.ok) {
34
+ const text = await res.text().catch(() => "");
35
+ console.error(red(`✗ HTTP ${res.status}: ${text}`));
36
+ return 1;
37
+ }
38
+ const out = await res.json();
39
+ console.log(`${green(out.created ? "✓ registered" : "✓ exists")} ${bold(out.slug)} ${dim(`(app_id=${out.app_id}, kind=${out.kind})`)}`);
40
+ console.log(` tool_id ${dim(out.tool_id)}`);
41
+ console.log(` name ${dim(out.name)}`);
42
+ console.log(` host ${cyan(acc.host)}`);
43
+ return 0;
44
+ }
45
+
46
+ //#endregion
47
+ export { runExecutaRegister };
@@ -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 };