@anna-ai/cli 0.1.9 → 0.1.11

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.
@@ -0,0 +1,366 @@
1
+ import { dirname, isAbsolute, resolve } from "node:path";
2
+ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
3
+ import { bold, cyan, dim, green, red, yellow } from "kleur/colors";
4
+ import { parse } from "smol-toml";
5
+
6
+ //#region src/commands/dev.ts
7
+ async function runDev(opts) {
8
+ const cwd = resolve(opts.cwd);
9
+ const manifestPath = isAbsolute(opts.manifestPath) ? opts.manifestPath : resolve(cwd, opts.manifestPath);
10
+ if (!existsSync(manifestPath)) {
11
+ console.error(red(`✗ manifest not found: ${manifestPath}`));
12
+ return 2;
13
+ }
14
+ let manifest;
15
+ try {
16
+ manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
17
+ } catch (e) {
18
+ console.error(red(`✗ manifest is not valid json: ${e.message}`));
19
+ return 2;
20
+ }
21
+ const slug = opts.slug ?? deriveSlug(manifest, manifestPath);
22
+ const bundleDir = opts.bundleDir ? resolve(cwd, opts.bundleDir) : resolve(dirname(manifestPath), "bundle");
23
+ const bundleEntry = manifest.ui?.bundle?.entry ?? "index.html";
24
+ if (!existsSync(bundleDir)) {
25
+ console.error(red(`✗ bundle dir not found: ${bundleDir}`));
26
+ return 2;
27
+ }
28
+ if (!existsSync(resolve(bundleDir, bundleEntry))) {
29
+ console.error(red(`✗ bundle entry "${bundleEntry}" not found under ${bundleDir}`));
30
+ return 2;
31
+ }
32
+ const matrixNexusRoot = await resolveMatrixNexusRoot(opts.matrixNexusRoot, cwd);
33
+ const mode = matrixNexusRoot ? "nexus-source" : "uvx";
34
+ const { PythonBridge, PINNED_RUNTIME_VERSION } = await import("./bridge-BIO7ilgO.js");
35
+ const { HarnessServer } = await import("./server-Cd5Lo-2v.js");
36
+ const bridge = new PythonBridge({
37
+ mode,
38
+ matrixNexusRoot: matrixNexusRoot ?? void 0,
39
+ onStderr: (line) => process.stderr.write(dim(`[bridge] ${line}\n`))
40
+ });
41
+ console.log(bold(cyan("anna-app dev")));
42
+ console.log(` manifest ${dim(manifestPath)}`);
43
+ console.log(` bundle ${dim(`${bundleDir}/${bundleEntry}`)}`);
44
+ if (mode === "nexus-source") {
45
+ console.log(` matrix-nexus root ${dim(matrixNexusRoot)}`);
46
+ console.log(` runtime ${dim("nexus-source (uv run)")}`);
47
+ } else console.log(` runtime ${dim(`uvx anna-app-runtime-local@${PINNED_RUNTIME_VERSION}`)}`);
48
+ console.log(` spawning python bridge…`);
49
+ try {
50
+ await bridge.start();
51
+ } catch (e) {
52
+ console.error(red(`✗ bridge failed to start: ${e.message}`));
53
+ if (mode === "uvx") console.error(dim(" (try: install uv from https://docs.astral.sh/uv/, or pass --matrix-nexus-root to use a checkout)"));
54
+ else console.error(dim(" (try: cd matrix-nexus && uv sync; ensure `uv` is on PATH; or unset --matrix-nexus-root to use uvx)"));
55
+ return 2;
56
+ }
57
+ try {
58
+ const pong = await bridge.call("ping");
59
+ if (!pong.pong) throw new Error("bridge ping returned non-pong");
60
+ } catch (e) {
61
+ console.error(red(`✗ bridge ping failed: ${e.message}`));
62
+ await bridge.stop();
63
+ return 2;
64
+ }
65
+ console.log(green(" ✓ bridge ready"));
66
+ const executas = opts.executas ?? autoDiscoverExecutas(dirname(manifestPath));
67
+ if (executas.length > 0) console.log(` executas ${dim(executas.map((e) => e.tool_id).join(", "))}`);
68
+ const llm = opts.noLlm ? { mode: "off" } : opts.mockLlm ? {
69
+ mode: "mock",
70
+ mockFile: opts.mockLlm
71
+ } : {
72
+ mode: "real",
73
+ account: opts.llmAccount,
74
+ appId: opts.llmAppId
75
+ };
76
+ console.log(` llm bridge ${dim(llm.mode === "off" ? "disabled (--no-llm)" : llm.mode === "mock" ? `mock (${opts.mockLlm})` : `real${opts.llmAccount ? ` [${opts.llmAccount}]` : ""}`)}`);
77
+ const server = new HarnessServer({
78
+ slug,
79
+ manifest,
80
+ bundleDir,
81
+ bundleEntry,
82
+ view: opts.view,
83
+ matrixNexusRoot,
84
+ userId: opts.userId,
85
+ port: opts.port,
86
+ executas,
87
+ watch: !opts.noWatch,
88
+ llm
89
+ }, bridge);
90
+ try {
91
+ await server.listen();
92
+ } catch (e) {
93
+ const msg = e.code === "EADDRINUSE" ? `port ${opts.port} already in use` : e.message;
94
+ console.error(red(`✗ harness failed to listen: ${msg}`));
95
+ await bridge.stop();
96
+ return 2;
97
+ }
98
+ console.log(` ${green("✓")} dashboard ${cyan(`http://localhost:${opts.port}/`)}`);
99
+ console.log(yellow(" press Ctrl+C to stop"));
100
+ const shutdown = async () => {
101
+ console.log(dim("\n shutting down…"));
102
+ try {
103
+ await server.close();
104
+ await bridge.stop();
105
+ } finally {
106
+ process.exit(0);
107
+ }
108
+ };
109
+ process.once("SIGINT", shutdown);
110
+ process.once("SIGTERM", shutdown);
111
+ await new Promise(() => {});
112
+ return 0;
113
+ }
114
+ function deriveSlug(manifest, path) {
115
+ const fromMan = manifest.slug ?? manifest.name;
116
+ if (typeof fromMan === "string" && fromMan) return fromMan;
117
+ return dirname(path).split(/[\\/]/).pop() || "anna-app-dev";
118
+ }
119
+ function autoDiscoverExecutas(manifestDir) {
120
+ const root = resolve(manifestDir, "executas");
121
+ if (!existsSync(root)) return [];
122
+ const found = [];
123
+ const seen = new Map();
124
+ for (const name of readdirSync(root)) {
125
+ if (name.startsWith(".")) continue;
126
+ const dir = resolve(root, name);
127
+ let st;
128
+ try {
129
+ st = statSync(dir);
130
+ } catch {
131
+ continue;
132
+ }
133
+ if (!st.isDirectory()) continue;
134
+ const result = discoverOne(dir, name);
135
+ if (result === "skip" || result === "disabled") continue;
136
+ if (result instanceof Error) {
137
+ console.warn(yellow(`⚠ skipping executa ${name}: ${result.message}`));
138
+ continue;
139
+ }
140
+ const prior = seen.get(result.tool_id);
141
+ if (prior) {
142
+ console.warn(yellow(`⚠ skipping executa ${name}: tool_id "${result.tool_id}" already provided by ${prior} (toggle "enabled": false in executa.json to silence)`));
143
+ continue;
144
+ }
145
+ seen.set(result.tool_id, name);
146
+ found.push(result);
147
+ }
148
+ return found;
149
+ }
150
+ function discoverOne(dir, name, opts = { respectEnabled: true }) {
151
+ const cfgPath = resolve(dir, "executa.json");
152
+ if (existsSync(cfgPath)) {
153
+ let cfg;
154
+ try {
155
+ cfg = JSON.parse(readFileSync(cfgPath, "utf-8"));
156
+ } catch (err) {
157
+ return new Error(`failed to parse executa.json (${err.message})`);
158
+ }
159
+ if (cfg.enabled === false && opts.respectEnabled !== false) return "disabled";
160
+ if (!cfg.tool_id) return new Error("executa.json missing required `tool_id`");
161
+ if (!cfg.type) return new Error("executa.json missing required `type`");
162
+ if (cfg.command && cfg.command.length > 0) return {
163
+ tool_id: cfg.tool_id,
164
+ project_dir: dir,
165
+ command: cfg.command
166
+ };
167
+ const cmd = defaultCommand(cfg.type, dir, cfg.tool_id);
168
+ if (cmd instanceof Error) return cmd;
169
+ return {
170
+ tool_id: cfg.tool_id,
171
+ project_dir: dir,
172
+ command: cmd
173
+ };
174
+ }
175
+ if (existsSync(resolve(dir, "pyproject.toml"))) return discoverPython(dir, name);
176
+ if (existsSync(resolve(dir, "package.json"))) return discoverNode(dir, name);
177
+ if (existsSync(resolve(dir, "go.mod"))) return new Error("Go executa requires an explicit executa.json with `tool_id` and `type: \"go\"` (go.mod has no script-name field)");
178
+ const binCandidate = resolve(dir, "bin", name);
179
+ if (existsSync(binCandidate)) return {
180
+ tool_id: name,
181
+ project_dir: dir,
182
+ command: [binCandidate]
183
+ };
184
+ return "skip";
185
+ }
186
+ function defaultCommand(type, dir, toolId) {
187
+ switch (type) {
188
+ case "python": return [
189
+ "uv",
190
+ "run",
191
+ "--project",
192
+ dir,
193
+ toolId
194
+ ];
195
+ case "node": {
196
+ const pkg = readPackageJson(dir);
197
+ if (!pkg) return new Error("type=node but no package.json found");
198
+ const entry = nodeEntry(pkg, toolId, dir);
199
+ if (entry instanceof Error) return entry;
200
+ return ["node", entry];
201
+ }
202
+ case "go": return [
203
+ "go",
204
+ "run",
205
+ "."
206
+ ];
207
+ case "binary": {
208
+ const guess = resolve(dir, "bin", toolId);
209
+ if (!existsSync(guess)) return new Error(`type=binary but no executable at ${guess}; specify "command" in executa.json explicitly`);
210
+ return [guess];
211
+ }
212
+ default: return new Error(`unknown type "${type}" — expected python | node | go | binary`);
213
+ }
214
+ }
215
+ function discoverPython(dir, name) {
216
+ const py = resolve(dir, "pyproject.toml");
217
+ let body;
218
+ try {
219
+ body = readFileSync(py, "utf-8");
220
+ } catch {
221
+ return new Error("could not read pyproject.toml");
222
+ }
223
+ let parsed;
224
+ try {
225
+ parsed = parse(body);
226
+ } catch (err) {
227
+ return new Error(`failed to parse pyproject.toml (${err.message})`);
228
+ }
229
+ const scripts = parsed?.project?.scripts;
230
+ if (!scripts || typeof scripts !== "object") return new Error("no [project.scripts] entry in pyproject.toml");
231
+ const keys = Object.keys(scripts);
232
+ if (keys.length === 0) return new Error("[project.scripts] is empty");
233
+ const toolId = keys[0];
234
+ return {
235
+ tool_id: toolId,
236
+ project_dir: dir,
237
+ command: [
238
+ "uv",
239
+ "run",
240
+ "--project",
241
+ dir,
242
+ toolId
243
+ ]
244
+ };
245
+ }
246
+ function discoverNode(dir, name) {
247
+ const pkg = readPackageJson(dir);
248
+ if (!pkg) return new Error("could not read package.json");
249
+ const toolId = (typeof pkg.executa === "object" && pkg.executa ? pkg.executa.tool_id : void 0) ?? firstBinKey(pkg) ?? (typeof pkg.name === "string" ? pkg.name : void 0);
250
+ if (!toolId) return new Error("package.json missing tool_id — set `executa.tool_id`, `bin`, or `name`");
251
+ const entry = nodeEntry(pkg, toolId, dir);
252
+ if (entry instanceof Error) return entry;
253
+ return {
254
+ tool_id: toolId,
255
+ project_dir: dir,
256
+ command: ["node", entry]
257
+ };
258
+ }
259
+ function readPackageJson(dir) {
260
+ const p = resolve(dir, "package.json");
261
+ if (!existsSync(p)) return null;
262
+ try {
263
+ return JSON.parse(readFileSync(p, "utf-8"));
264
+ } catch {
265
+ return null;
266
+ }
267
+ }
268
+ function firstBinKey(pkg) {
269
+ if (typeof pkg.bin === "string") return typeof pkg.name === "string" ? pkg.name : void 0;
270
+ if (pkg.bin && typeof pkg.bin === "object") {
271
+ const keys = Object.keys(pkg.bin);
272
+ return keys[0];
273
+ }
274
+ return void 0;
275
+ }
276
+ function nodeEntry(pkg, toolId, dir) {
277
+ let rel;
278
+ if (typeof pkg.bin === "string") rel = pkg.bin;
279
+ else if (pkg.bin && typeof pkg.bin === "object") {
280
+ const obj = pkg.bin;
281
+ const v = obj[toolId];
282
+ if (typeof v === "string") rel = v;
283
+ else {
284
+ const first = Object.values(obj).find((x) => typeof x === "string");
285
+ if (typeof first === "string") rel = first;
286
+ }
287
+ }
288
+ if (!rel && typeof pkg.main === "string") rel = pkg.main;
289
+ if (!rel && typeof pkg.module === "string") rel = pkg.module;
290
+ if (!rel) return new Error("package.json has no usable entry — set `bin`, `main`, or override `command` in executa.json");
291
+ const abs = isAbsolute(rel) ? rel : resolve(dir, rel);
292
+ if (!existsSync(abs)) return new Error(`node entry not found: ${abs}`);
293
+ return abs;
294
+ }
295
+ /**
296
+ * Parse `--executa <spec>` flag values from CLI. Spec syntax (comma-separated
297
+ * key=value pairs, all keys lowercase):
298
+ *
299
+ * --executa dir=./executas/foo,type=node
300
+ * --executa dir=/abs/path,tool_id=tool-x,command="node plugin.js"
301
+ *
302
+ * Required: `dir`. Optional: `tool_id`, `type`, `command` (space-split unless
303
+ * already an array via JSON), `cwd`. When `tool_id`/`type`/`command` are
304
+ * omitted, falls back to the same rules as `executa.json` discovery.
305
+ */
306
+ function parseExecutaSpec(spec, cwd) {
307
+ const parts = spec.split(",").map((s) => s.trim()).filter(Boolean);
308
+ const kv = {};
309
+ for (const p of parts) {
310
+ const eq = p.indexOf("=");
311
+ if (eq === -1) return new Error(`bad --executa spec part: "${p}"`);
312
+ kv[p.slice(0, eq).trim().toLowerCase()] = p.slice(eq + 1).trim();
313
+ }
314
+ const dirRaw = kv.dir;
315
+ if (!dirRaw) return new Error("--executa spec missing `dir=`");
316
+ const dir = isAbsolute(dirRaw) ? dirRaw : resolve(cwd, dirRaw);
317
+ if (!existsSync(dir)) return new Error(`--executa dir not found: ${dir}`);
318
+ if (!kv.tool_id && !kv.type && !kv.command) {
319
+ const result = discoverOne(dir, dir.split(/[\\/]/).pop() ?? dir, { respectEnabled: false });
320
+ if (result === "skip") return new Error(`nothing discoverable at ${dir} (no executa.json / pyproject.toml / package.json / bin/)`);
321
+ if (result === "disabled") return new Error(`executa.json at ${dir} has "enabled": false`);
322
+ if (result instanceof Error) return result;
323
+ return result;
324
+ }
325
+ const tool_id = kv.tool_id;
326
+ if (!tool_id) return new Error("--executa spec missing `tool_id=`");
327
+ let command;
328
+ if (kv.command) if (kv.command.startsWith("[")) try {
329
+ const arr = JSON.parse(kv.command);
330
+ if (!Array.isArray(arr) || !arr.every((x) => typeof x === "string")) return new Error("--executa command JSON must be a string array");
331
+ command = arr;
332
+ } catch (err) {
333
+ return new Error(`--executa command JSON invalid: ${err.message}`);
334
+ }
335
+ else command = kv.command.split(/\s+/);
336
+ else if (kv.type) {
337
+ const c = defaultCommand(kv.type, dir, tool_id);
338
+ if (c instanceof Error) return c;
339
+ command = c;
340
+ }
341
+ return {
342
+ tool_id,
343
+ project_dir: dir,
344
+ command
345
+ };
346
+ }
347
+ async function resolveMatrixNexusRoot(explicit, cwd) {
348
+ const candidates = [explicit, process.env.ANNA_NEXUS_ROOT];
349
+ let dir = cwd;
350
+ while (true) {
351
+ candidates.push(dir);
352
+ const parent = dirname(dir);
353
+ if (parent === dir) break;
354
+ dir = parent;
355
+ }
356
+ candidates.push(resolve(cwd, "..", "matrix-nexus"));
357
+ for (const c of candidates) {
358
+ if (!c) continue;
359
+ const abs = isAbsolute(c) ? c : resolve(cwd, c);
360
+ if (existsSync(resolve(abs, "packages/anna-app-runtime-local/pyproject.toml"))) return abs;
361
+ }
362
+ return null;
363
+ }
364
+
365
+ //#endregion
366
+ export { parseExecutaSpec, runDev };
@@ -1,8 +1,8 @@
1
- import { PINNED_RUNTIME_VERSION } from "./bridge-CBcQUQGU.js";
1
+ import { PINNED_RUNTIME_VERSION } from "./bridge-Cpm3D2Wk.js";
2
2
  import { dirname, isAbsolute, resolve } from "node:path";
3
3
  import { existsSync, statSync } from "node:fs";
4
- import { bold, dim, green, red, yellow } from "kleur/colors";
5
4
  import { spawnSync } from "node:child_process";
5
+ import { bold, dim, green, red, yellow } from "kleur/colors";
6
6
  import { homedir } from "node:os";
7
7
 
8
8
  //#region src/commands/doctor.ts
@@ -0,0 +1,102 @@
1
+ import { canonicalHost, saveAccount } from "./credentials-ggdaz_-7.js";
2
+
3
+ //#region src/commands/login.ts
4
+ const POLL_TIMEOUT_MS = 12 * 60 * 1e3;
5
+ function envBool(v) {
6
+ return !!v && /^(1|true|yes|on)$/i.test(v);
7
+ }
8
+ async function tryOpenBrowser(url) {
9
+ if (envBool(process.env.ANNA_LOGIN_NO_OPEN)) return;
10
+ const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
11
+ try {
12
+ const { spawn } = await import("node:child_process");
13
+ spawn(opener, [url], {
14
+ detached: true,
15
+ stdio: "ignore"
16
+ }).unref();
17
+ } catch {}
18
+ }
19
+ async function runLogin(opts) {
20
+ const host = canonicalHost(opts.host);
21
+ process.stdout.write(`▸ logging in to ${host}\n`);
22
+ let startResp;
23
+ try {
24
+ const res = await fetch(`${host}/api/v1/anna-apps/dev/login/start`, {
25
+ method: "POST",
26
+ headers: { "content-type": "application/json" }
27
+ });
28
+ if (!res.ok) {
29
+ console.error(`✗ /dev/login/start failed: HTTP ${res.status}`);
30
+ return 1;
31
+ }
32
+ startResp = await res.json();
33
+ } catch (e) {
34
+ console.error(`✗ cannot reach nexus: ${e.message}`);
35
+ return 1;
36
+ }
37
+ const verifyUrl = `${host}${startResp.verification_uri_complete}`;
38
+ process.stdout.write(`\n user_code: \x1b[1m${startResp.user_code}\x1b[0m\n`);
39
+ process.stdout.write(` open: ${verifyUrl}\n`);
40
+ process.stdout.write(` (waiting up to ${Math.round(startResp.expires_in / 60)} min)\n\n`);
41
+ if (!opts.noBrowser) await tryOpenBrowser(verifyUrl);
42
+ const interval = Math.max(2, Math.min(15, startResp.interval || 5));
43
+ const deadline = Date.now() + Math.min(POLL_TIMEOUT_MS, startResp.expires_in * 1e3);
44
+ const spinner = [
45
+ "⠋",
46
+ "⠙",
47
+ "⠹",
48
+ "⠸",
49
+ "⠼",
50
+ "⠴",
51
+ "⠦",
52
+ "⠧",
53
+ "⠇",
54
+ "⠏"
55
+ ];
56
+ let frame = 0;
57
+ while (Date.now() < deadline) {
58
+ process.stdout.write(`\r ${spinner[frame++ % spinner.length]} polling...`);
59
+ await new Promise((r) => setTimeout(r, interval * 1e3));
60
+ let resp;
61
+ try {
62
+ resp = await fetch(`${host}/api/v1/anna-apps/dev/login/poll`, {
63
+ method: "POST",
64
+ headers: { "content-type": "application/json" },
65
+ body: JSON.stringify({ device_code: startResp.device_code })
66
+ });
67
+ } catch (e) {
68
+ process.stdout.write("\n");
69
+ console.error(`✗ poll failed: ${e.message}`);
70
+ return 1;
71
+ }
72
+ if (resp.status === 202) continue;
73
+ if (resp.status === 410) {
74
+ process.stdout.write("\n✗ device code expired\n");
75
+ return 1;
76
+ }
77
+ if (resp.status === 200) {
78
+ process.stdout.write("\n");
79
+ const ok = await resp.json();
80
+ const now = Math.floor(Date.now() / 1e3);
81
+ saveAccount({
82
+ host,
83
+ user_id: null,
84
+ pat: ok.pat,
85
+ issued_at: now,
86
+ expires_at: now + (ok.expires_in || 0),
87
+ scopes: ok.scopes
88
+ });
89
+ console.log(`✓ logged in. PAT saved to ~/.config/anna/credentials.json`);
90
+ console.log(` expires in ~${Math.round((ok.expires_in || 0) / 86400)}d`);
91
+ return 0;
92
+ }
93
+ process.stdout.write("\n");
94
+ console.error(`✗ unexpected status: HTTP ${resp.status}`);
95
+ return 1;
96
+ }
97
+ process.stdout.write("\n✗ timed out waiting for approval\n");
98
+ return 1;
99
+ }
100
+
101
+ //#endregion
102
+ export { runLogin };
@@ -0,0 +1,23 @@
1
+ import { canonicalHost, getAccount, readCredentials, removeAccount } from "./credentials-ggdaz_-7.js";
2
+
3
+ //#region src/commands/logout.ts
4
+ async function runLogout(opts) {
5
+ if (opts.all) {
6
+ const data = readCredentials();
7
+ const n = Object.keys(data.accounts).length;
8
+ for (const k of Object.keys(data.accounts)) removeAccount(k);
9
+ console.log(`✓ removed ${n} account(s)`);
10
+ return 0;
11
+ }
12
+ const acc = getAccount(opts.host);
13
+ if (!acc) {
14
+ console.error(opts.host ? `✗ no account for ${canonicalHost(opts.host)}` : "✗ no current account");
15
+ return 1;
16
+ }
17
+ removeAccount(acc.host);
18
+ console.log(`✓ removed account ${acc.host}`);
19
+ return 0;
20
+ }
21
+
22
+ //#endregion
23
+ export { runLogout };