astragate 0.0.0-stage → 0.2.0

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/index.js ADDED
@@ -0,0 +1,2360 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import process5 from "process";
5
+ import { Argument, Command } from "commander";
6
+
7
+ // src/errors.ts
8
+ var AstraError = class extends Error {
9
+ hint;
10
+ constructor(message, hint) {
11
+ super(message);
12
+ this.name = "AstraError";
13
+ this.hint = hint;
14
+ }
15
+ };
16
+
17
+ // src/lib/http.ts
18
+ var ApiError = class extends Error {
19
+ status;
20
+ constructor(message, status) {
21
+ super(message);
22
+ this.name = "ApiError";
23
+ this.status = status;
24
+ }
25
+ };
26
+ function baseUrlFor(host, port) {
27
+ let h = host.trim();
28
+ if (!h || h === "0.0.0.0" || h === "*" || h === "::") h = "127.0.0.1";
29
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
30
+ return `http://${h.includes(":") ? `[${h}]` : h}:${port}`;
31
+ }
32
+ function adminHeaders(mutating, runtimeToken) {
33
+ const headers = { Accept: "application/json" };
34
+ if (mutating) headers["X-Astra-Admin"] = "1";
35
+ if (runtimeToken) headers["X-Astra-Runtime-Token"] = runtimeToken;
36
+ return headers;
37
+ }
38
+ async function apiRequest(options) {
39
+ const { base, path: path13, method = "GET", body, runtimeToken, timeoutMs = 1e4 } = options;
40
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
41
+ if (!doFetch) {
42
+ throw new AstraError("fetch is unavailable. The Astra CLI requires Node.js 18+.");
43
+ }
44
+ const mutating = method !== "GET";
45
+ const headers = adminHeaders(mutating, runtimeToken);
46
+ if (body !== void 0) headers["Content-Type"] = "application/json";
47
+ let response;
48
+ try {
49
+ response = await doFetch(base + path13, {
50
+ method,
51
+ headers,
52
+ body: body === void 0 ? void 0 : JSON.stringify(body),
53
+ signal: AbortSignal.timeout(timeoutMs)
54
+ });
55
+ } catch (err) {
56
+ throw new AstraError(
57
+ `Cannot reach the Astra server at ${base} (${describeFetchError(err)}).`,
58
+ "Start it with `astra start`."
59
+ );
60
+ }
61
+ const text = await response.text();
62
+ let payload;
63
+ if (text.trim()) {
64
+ try {
65
+ payload = JSON.parse(text);
66
+ } catch {
67
+ payload = void 0;
68
+ }
69
+ }
70
+ if (!response.ok) {
71
+ const serverMessage = readErrorMessage(payload);
72
+ throw new ApiError(
73
+ serverMessage ?? `${response.status} ${response.statusText || "request failed"}`,
74
+ response.status
75
+ );
76
+ }
77
+ return payload;
78
+ }
79
+ function readErrorMessage(payload) {
80
+ if (payload && typeof payload === "object" && "error" in payload) {
81
+ const e = payload.error;
82
+ if (typeof e === "string" && e) return e;
83
+ }
84
+ return void 0;
85
+ }
86
+ function describeFetchError(err) {
87
+ if (err instanceof Error) {
88
+ if (err.name === "TimeoutError" || err.name === "AbortError") return "timed out";
89
+ return err.message;
90
+ }
91
+ return String(err);
92
+ }
93
+ async function checkHealth(base, fetchImpl, timeoutMs = 2500) {
94
+ const doFetch = fetchImpl ?? globalThis.fetch;
95
+ if (!doFetch) return false;
96
+ try {
97
+ const res = await doFetch(`${base}/api/health`, { signal: AbortSignal.timeout(timeoutMs) });
98
+ if (!res.ok) return false;
99
+ const payload = await res.json();
100
+ return payload.status === "ok";
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+ async function shutdownRequest(base, runtimeToken, fetchImpl) {
106
+ await apiRequest({
107
+ base,
108
+ path: "/api/admin/shutdown",
109
+ method: "POST",
110
+ runtimeToken,
111
+ timeoutMs: 5e3,
112
+ fetchImpl
113
+ });
114
+ }
115
+
116
+ // src/ui.ts
117
+ import process2 from "process";
118
+ var useColor = process2.stdout.isTTY === true && process2.env.NO_COLOR !== "1" && process2.env.TERM !== "dumb";
119
+ function wrap(open) {
120
+ return (text) => useColor ? `\x1B[${open}m${text}\x1B[0m` : text;
121
+ }
122
+ var style = {
123
+ bold: wrap("1"),
124
+ dim: wrap("2"),
125
+ green: wrap("32"),
126
+ yellow: wrap("33"),
127
+ red: wrap("31"),
128
+ cyan: wrap("36")
129
+ };
130
+ function info(message) {
131
+ console.log(message);
132
+ }
133
+ function success(message) {
134
+ console.log(style.green(message));
135
+ }
136
+ function warn(message) {
137
+ console.log(style.yellow(message));
138
+ }
139
+ function fail(message) {
140
+ console.error(style.red(message));
141
+ }
142
+ function printTable(rows, headers) {
143
+ const all = headers ? [headers, ...rows] : rows;
144
+ if (all.length === 0) return;
145
+ const widths = [];
146
+ for (const row of all) {
147
+ row.forEach((cell, i) => {
148
+ widths[i] = Math.max(widths[i] ?? 0, cell.length);
149
+ });
150
+ }
151
+ const line = (row) => row.map((cell, i) => cell + " ".repeat((widths[i] ?? 0) - cell.length)).join(" ");
152
+ if (headers) console.log(style.bold(line(headers)));
153
+ for (const row of rows) console.log(line(row));
154
+ }
155
+
156
+ // src/lib/platform.ts
157
+ var RID_PREFIX = { darwin: "osx", linux: "linux", win32: "win" };
158
+ var SUPPORTED_ARCHES = ["x64", "arm64"];
159
+ function platformInfo(platform = process.platform, arch = process.arch) {
160
+ const ridPrefix = RID_PREFIX[platform];
161
+ if (!ridPrefix || !SUPPORTED_ARCHES.includes(arch)) {
162
+ throw new AstraError(
163
+ `Astra does not ship binaries for ${platform}/${arch}.`,
164
+ "Supported platforms: macOS (x64, arm64), Linux (x64, arm64), Windows (x64, arm64)."
165
+ );
166
+ }
167
+ const p = platform;
168
+ const dir = `${p}-${arch}`;
169
+ return {
170
+ platform: p,
171
+ arch,
172
+ rid: `${ridPrefix}-${arch}`,
173
+ serverPackage: `@aidotnet/server-${dir}`,
174
+ desktopPackage: `@aidotnet/desktop-${dir}`,
175
+ serverDir: `server-${dir}`,
176
+ desktopDir: `desktop-${dir}`,
177
+ binaryName: p === "win32" ? "astra-server.exe" : "astra-server"
178
+ };
179
+ }
180
+ var CLIENT_KINDS = [
181
+ "codex",
182
+ "claude-code",
183
+ "gemini-cli",
184
+ "opencode",
185
+ "claude-desktop",
186
+ "grok-build"
187
+ ];
188
+
189
+ // src/commands/manage.ts
190
+ import process3 from "process";
191
+
192
+ // ../packages/update-core/src/manifest.ts
193
+ var DEFAULT_FEED_URL = "https://astra-gate.si/api/client-releases";
194
+ var DEFAULT_CHANNEL = "stable";
195
+ var UpdateFeedError = class extends Error {
196
+ constructor(message) {
197
+ super(message);
198
+ this.name = "UpdateFeedError";
199
+ }
200
+ };
201
+ async function fetchManifest(o = {}) {
202
+ const feed = (o.feedUrl ?? DEFAULT_FEED_URL).trim();
203
+ const channel = (o.channel ?? DEFAULT_CHANNEL).trim() || DEFAULT_CHANNEL;
204
+ const url = `${feed.replace(/\/+$/, "")}/${channel}/latest.json`;
205
+ const doFetch = o.fetchImpl ?? globalThis.fetch;
206
+ if (!doFetch) throw new UpdateFeedError("fetch is unavailable (Node.js 18+ required).");
207
+ let response;
208
+ try {
209
+ response = await doFetch(url, {
210
+ headers: {
211
+ Accept: "application/json",
212
+ ...o.userAgent ? { "User-Agent": o.userAgent } : {}
213
+ },
214
+ signal: AbortSignal.timeout(o.timeoutMs ?? 15e3)
215
+ });
216
+ } catch (err) {
217
+ const reason = err instanceof Error ? err.name === "TimeoutError" || err.name === "AbortError" ? "timed out" : err.message : String(err);
218
+ throw new UpdateFeedError(`Cannot reach the update feed at ${url} (${reason}).`);
219
+ }
220
+ if (!response.ok) {
221
+ throw new UpdateFeedError(`Update feed responded ${response.status} ${response.statusText} for ${url}.`);
222
+ }
223
+ let manifest;
224
+ try {
225
+ manifest = await response.json();
226
+ } catch (err) {
227
+ throw new UpdateFeedError(`Update feed returned invalid JSON (${err instanceof Error ? err.message : String(err)}).`);
228
+ }
229
+ if (!manifest || typeof manifest.version !== "string" || manifest.version.trim() === "") {
230
+ throw new UpdateFeedError("Update manifest has no version.");
231
+ }
232
+ return manifest;
233
+ }
234
+
235
+ // ../packages/update-core/src/version.ts
236
+ function parseVersion(v) {
237
+ if (!v) return null;
238
+ const core = v.trim().split("-", 1)[0].split("+", 1)[0].replace(/^v/, "");
239
+ const m = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?$/.exec(core);
240
+ if (!m) return null;
241
+ return [Number(m[1]), Number(m[2] ?? 0), Number(m[3] ?? 0)];
242
+ }
243
+ function isNewerVersion(candidate, current) {
244
+ const c = parseVersion(candidate);
245
+ const cur = parseVersion(current);
246
+ if (!c || !cur) return false;
247
+ for (let i = 0; i < 3; i++) {
248
+ const a = c[i];
249
+ const b = cur[i];
250
+ if (a !== b) return a > b;
251
+ }
252
+ return false;
253
+ }
254
+ function majorOf(v) {
255
+ const m = /^\s*(\d+)(?:\.|$)/.exec(v ?? "");
256
+ const group = m?.[1];
257
+ return group ? Number.parseInt(group, 10) : null;
258
+ }
259
+ function isApiVersionCompatible(serverApiVersion, expectedMajor) {
260
+ const major = majorOf(serverApiVersion);
261
+ return major !== null && major === expectedMajor;
262
+ }
263
+
264
+ // ../packages/update-core/src/state.ts
265
+ import fs from "fs";
266
+ import path from "path";
267
+ var MAX_CONSECUTIVE_FAILURES = 2;
268
+ var KNOWN_PHASES = [
269
+ "idle",
270
+ "checking",
271
+ "downloading",
272
+ "swapping",
273
+ "restarting",
274
+ "rolling-back",
275
+ "failed"
276
+ ];
277
+ function initialState(now = () => /* @__PURE__ */ new Date()) {
278
+ return { phase: "idle", failedCount: 0, updatedAt: now().toISOString() };
279
+ }
280
+ function readUpdateState(file) {
281
+ let text;
282
+ try {
283
+ text = fs.readFileSync(file, "utf8");
284
+ } catch {
285
+ return null;
286
+ }
287
+ if (!text.trim()) return null;
288
+ try {
289
+ const raw = JSON.parse(text);
290
+ if (!raw || typeof raw !== "object" || !KNOWN_PHASES.includes(raw.phase)) return null;
291
+ return {
292
+ phase: raw.phase,
293
+ ...raw.targetVersion !== void 0 ? { targetVersion: raw.targetVersion } : {},
294
+ ...raw.previousVersion !== void 0 ? { previousVersion: raw.previousVersion } : {},
295
+ ...raw.step !== void 0 ? { step: raw.step } : {},
296
+ ...raw.error !== void 0 ? { error: raw.error } : {},
297
+ failedCount: typeof raw.failedCount === "number" ? raw.failedCount : 0,
298
+ updatedAt: typeof raw.updatedAt === "string" ? raw.updatedAt : (/* @__PURE__ */ new Date()).toISOString()
299
+ };
300
+ } catch {
301
+ return null;
302
+ }
303
+ }
304
+ function writeUpdateState(file, state, now = () => /* @__PURE__ */ new Date()) {
305
+ const payload = { ...state, updatedAt: now().toISOString() };
306
+ fs.mkdirSync(path.dirname(file), { recursive: true });
307
+ const tmp = `${file}.tmp`;
308
+ fs.writeFileSync(tmp, `${JSON.stringify(payload, null, 2)}
309
+ `);
310
+ fs.renameSync(tmp, file);
311
+ }
312
+
313
+ // ../packages/update-core/src/paths.ts
314
+ import path2 from "path";
315
+ function updatePaths(home) {
316
+ return {
317
+ stagingDir: path2.join(home, "updates", "staging"),
318
+ backupsDir: path2.join(home, "backups"),
319
+ serverDir: path2.join(home, "server"),
320
+ stateFile: path2.join(home, "update-state.json")
321
+ };
322
+ }
323
+ function managedServerBinaryPath(serverDir, version, platform) {
324
+ return path2.join(serverDir, `astra-server-${version}${platform === "win32" ? ".exe" : ""}`);
325
+ }
326
+
327
+ // ../packages/update-core/src/npm-runner.ts
328
+ import { spawn } from "child_process";
329
+ import fs2 from "fs";
330
+ import path3 from "path";
331
+ function locateNpm(o = {}) {
332
+ const env = o.env ?? process.env;
333
+ const platform = o.platform ?? process.platform;
334
+ const execPath = o.execPath ?? process.execPath;
335
+ const isNode = /^node(\.exe)?$/i.test(path3.basename(execPath));
336
+ if (isNode) {
337
+ const found = npmBesideNode(execPath, platform);
338
+ if (found) return found;
339
+ }
340
+ const home = env.HOME ?? "";
341
+ const candidates = platform === "win32" ? [path3.join(process.env.APPDATA ?? path3.join(home, "AppData", "Roaming"), "npm", "npm.cmd")] : ["/opt/homebrew/bin/npm", "/usr/local/bin/npm", "/usr/bin/npm"];
342
+ for (const candidate of candidates) {
343
+ if (candidate && fs2.existsSync(candidate)) return { command: candidate, args: [] };
344
+ }
345
+ return { command: platform === "win32" ? "npm.cmd" : "npm", args: [] };
346
+ }
347
+ function npmBesideNode(execPath, platform) {
348
+ const execDir = path3.dirname(execPath);
349
+ const prefix = platform === "win32" ? execDir : path3.basename(execDir) === "bin" ? path3.dirname(execDir) : execDir;
350
+ const cli = path3.join(prefix, "lib", "node_modules", "npm", "bin", "npm-cli.js");
351
+ if (fs2.existsSync(cli)) return { command: execPath, args: [cli] };
352
+ return null;
353
+ }
354
+ function usesShell(command, platform) {
355
+ return platform === "win32" && command.endsWith(".cmd");
356
+ }
357
+ function runCapture(command, args, o = {}) {
358
+ return new Promise((resolve, reject) => {
359
+ const child = spawn(command, [...args], {
360
+ env: o.env,
361
+ windowsHide: true,
362
+ shell: usesShell(command, o.platform ?? process.platform)
363
+ });
364
+ let stdout = "";
365
+ let stderr = "";
366
+ let settled = false;
367
+ const timer = o.timeoutMs ? setTimeout(() => {
368
+ settled = true;
369
+ try {
370
+ child.kill("SIGKILL");
371
+ } catch {
372
+ }
373
+ reject(new Error(`Command timed out after ${o.timeoutMs}ms: ${command} ${args.join(" ")}`));
374
+ }, o.timeoutMs) : null;
375
+ child.stdout?.on("data", (d) => {
376
+ stdout += d.toString();
377
+ });
378
+ child.stderr?.on("data", (d) => {
379
+ stderr += d.toString();
380
+ });
381
+ child.on("error", (err) => {
382
+ if (timer) clearTimeout(timer);
383
+ if (!settled) reject(err);
384
+ });
385
+ child.on("close", (code) => {
386
+ if (timer) clearTimeout(timer);
387
+ if (!settled) resolve({ code, stdout, stderr });
388
+ });
389
+ });
390
+ }
391
+ function npmCommand(o) {
392
+ return o.npm ?? locateNpm(o);
393
+ }
394
+ async function npmInstallIntoPrefix(prefix, spec, o = {}) {
395
+ const npm = npmCommand(o);
396
+ const r = await runCapture(
397
+ npm.command,
398
+ [...npm.args, "install", "--prefix", prefix, "--no-audit", "--no-fund", "--loglevel", "error", spec],
399
+ { env: o.env, platform: o.platform, timeoutMs: o.installTimeoutMs ?? 6e5 }
400
+ );
401
+ if (r.code !== 0) {
402
+ throw new Error(`Failed to install ${spec}: ${r.stderr.trim() || `npm exited with code ${r.code}`}`);
403
+ }
404
+ }
405
+
406
+ // ../packages/update-core/src/install-info.ts
407
+ import fs3 from "fs";
408
+ import path4 from "path";
409
+ function readInstallInfo(home) {
410
+ let text;
411
+ try {
412
+ text = fs3.readFileSync(path4.join(home, "install.json"), "utf8");
413
+ } catch {
414
+ return null;
415
+ }
416
+ if (!text.trim()) return null;
417
+ try {
418
+ const raw = JSON.parse(text);
419
+ if (!raw || typeof raw !== "object") return null;
420
+ const info2 = {};
421
+ for (const key of ["serverPath", "serverVersion", "desktopPath", "desktopVersion", "updatedAt"]) {
422
+ const v = raw[key];
423
+ if (typeof v === "string" && v.length > 0) info2[key] = v;
424
+ }
425
+ return info2;
426
+ } catch {
427
+ return null;
428
+ }
429
+ }
430
+ function writeInstallInfo(home, info2) {
431
+ const file = path4.join(home, "install.json");
432
+ fs3.mkdirSync(path4.dirname(file), { recursive: true });
433
+ fs3.writeFileSync(file, `${JSON.stringify(info2, null, 2)}
434
+ `);
435
+ }
436
+ function applyServerInfo(existing, server) {
437
+ return {
438
+ ...existing ?? {},
439
+ serverPath: server.serverPath,
440
+ ...server.serverVersion ? { serverVersion: server.serverVersion } : {},
441
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
442
+ };
443
+ }
444
+
445
+ // ../packages/update-core/src/staging.ts
446
+ import { createHash } from "crypto";
447
+ import fs4 from "fs";
448
+ import path5 from "path";
449
+ function sha256File(file) {
450
+ const hash = createHash("sha256");
451
+ hash.update(fs4.readFileSync(file));
452
+ return hash.digest("hex");
453
+ }
454
+ async function stageServerBinary(o) {
455
+ const install = o.installIntoPrefix ?? npmInstallIntoPrefix;
456
+ fs4.rmSync(o.stagingDir, { recursive: true, force: true });
457
+ fs4.mkdirSync(o.stagingDir, { recursive: true });
458
+ o.log?.(`Downloading ${o.serverPackage}@${o.version}\u2026`);
459
+ await install(o.stagingDir, `${o.serverPackage}@${o.version}`);
460
+ const binaryName = o.platform === "win32" ? "astra-server.exe" : "astra-server";
461
+ const staged = path5.join(o.stagingDir, "node_modules", ...o.serverPackage.split("/"), "bin", binaryName);
462
+ if (!fs4.existsSync(staged)) {
463
+ fs4.rmSync(o.stagingDir, { recursive: true, force: true });
464
+ throw new Error(`The staged package does not contain a server binary (${staged}).`);
465
+ }
466
+ if (o.sha256) {
467
+ o.log?.("Verifying checksum\u2026");
468
+ const actual = sha256File(staged);
469
+ if (actual.toLowerCase() !== o.sha256.toLowerCase()) {
470
+ fs4.rmSync(o.stagingDir, { recursive: true, force: true });
471
+ throw new Error(`Checksum mismatch for the staged server binary (expected ${o.sha256}, got ${actual}).`);
472
+ }
473
+ }
474
+ return { path: staged, version: o.version };
475
+ }
476
+
477
+ // ../packages/update-core/src/swap.ts
478
+ import fs5 from "fs";
479
+ import path6 from "path";
480
+ var MAX_BACKUPS = 3;
481
+ function backupCurrentBinary(o) {
482
+ if (!o.currentPath || !fs5.existsSync(o.currentPath)) return null;
483
+ const kind = o.kind ?? "server";
484
+ const dir = path6.join(o.backupsDir, kind);
485
+ fs5.mkdirSync(dir, { recursive: true });
486
+ const ext = o.platform === "win32" ? ".exe" : "";
487
+ const stamp = o.currentVersion ? `-${o.currentVersion}` : `-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}`;
488
+ const backupPath = path6.join(dir, `astra-server${stamp}${ext}`);
489
+ fs5.copyFileSync(o.currentPath, backupPath);
490
+ pruneBackups(dir, o.maxBackups ?? MAX_BACKUPS);
491
+ return backupPath;
492
+ }
493
+ function pruneBackups(dir, keep = MAX_BACKUPS) {
494
+ let entries = [];
495
+ try {
496
+ entries = fs5.readdirSync(dir).sort();
497
+ } catch {
498
+ return;
499
+ }
500
+ const excess = entries.length - keep;
501
+ for (let i = 0; i < excess; i++) {
502
+ try {
503
+ fs5.rmSync(path6.join(dir, entries[i]), { force: true });
504
+ } catch {
505
+ }
506
+ }
507
+ }
508
+ function installManagedBinary(o) {
509
+ const target = managedServerBinaryPath(o.serverDir, o.version, o.platform);
510
+ fs5.mkdirSync(o.serverDir, { recursive: true });
511
+ const tmp = `${target}.tmp`;
512
+ fs5.copyFileSync(o.stagedPath, tmp);
513
+ fs5.renameSync(tmp, target);
514
+ if (o.platform !== "win32") fs5.chmodSync(target, 493);
515
+ return target;
516
+ }
517
+ function pruneManagedBinaries(serverDir, currentTarget, keep = 2) {
518
+ let entries = [];
519
+ try {
520
+ entries = fs5.readdirSync(serverDir).filter((f) => /^astra-server-\d/.test(f));
521
+ } catch {
522
+ return;
523
+ }
524
+ const excess = entries.filter((f) => path6.join(serverDir, f) !== currentTarget).length - keep;
525
+ for (let i = 0; i < Math.max(0, excess); i++) {
526
+ try {
527
+ fs5.rmSync(path6.join(serverDir, entries[i]), { force: true });
528
+ } catch {
529
+ }
530
+ }
531
+ }
532
+ function installManagedWebRoot(binaryPath, serverDir) {
533
+ const src = path6.join(path6.dirname(binaryPath), "wwwroot");
534
+ if (!fs5.existsSync(src)) return false;
535
+ const dest = path6.join(serverDir, "wwwroot");
536
+ const tmp = `${dest}.tmp`;
537
+ const prev = `${dest}.prev`;
538
+ fs5.mkdirSync(serverDir, { recursive: true });
539
+ fs5.rmSync(tmp, { recursive: true, force: true });
540
+ fs5.cpSync(src, tmp, { recursive: true });
541
+ fs5.rmSync(prev, { recursive: true, force: true });
542
+ if (fs5.existsSync(dest)) fs5.renameSync(dest, prev);
543
+ fs5.renameSync(tmp, dest);
544
+ return true;
545
+ }
546
+ function commitManagedWebRoot(serverDir) {
547
+ fs5.rmSync(path6.join(serverDir, "wwwroot.prev"), { recursive: true, force: true });
548
+ }
549
+ function restoreManagedWebRoot(serverDir) {
550
+ const dest = path6.join(serverDir, "wwwroot");
551
+ const prev = `${dest}.prev`;
552
+ if (!fs5.existsSync(prev)) return;
553
+ fs5.rmSync(dest, { recursive: true, force: true });
554
+ fs5.renameSync(prev, dest);
555
+ }
556
+
557
+ // ../packages/update-core/src/apply.ts
558
+ import fs6 from "fs";
559
+ var UpdateError = class extends Error {
560
+ constructor(message, hint) {
561
+ super(message);
562
+ this.hint = hint;
563
+ this.name = "UpdateError";
564
+ }
565
+ hint;
566
+ };
567
+ async function applyServerUpdate(o) {
568
+ const paths = updatePaths(o.home);
569
+ const log = o.log ?? (() => {
570
+ });
571
+ const now = o.now ?? (() => /* @__PURE__ */ new Date());
572
+ const state = readUpdateState(paths.stateFile) ?? initialState(now);
573
+ if (!o.force && state.failedCount >= MAX_CONSECUTIVE_FAILURES) {
574
+ throw new UpdateError(
575
+ `The last ${state.failedCount} update attempts failed. Fix the problem and run the update with --retry.`,
576
+ state.error ?? void 0
577
+ );
578
+ }
579
+ if (!isNewerVersion(o.manifest.version, o.currentVersion)) {
580
+ throw new UpdateError(`Already on version ${o.currentVersion} (${o.manifest.version} is not newer).`);
581
+ }
582
+ if (o.manifest.apiVersion && !isApiVersionCompatible(o.manifest.apiVersion, o.expectedApiMajor)) {
583
+ throw new UpdateError(
584
+ `The update targets API major ${o.manifest.apiVersion} but this client expects ${o.expectedApiMajor}. Update the whole Astra install manually (npm install -g astragate).`
585
+ );
586
+ }
587
+ const setState = (phase, step, error) => {
588
+ writeUpdateState(
589
+ paths.stateFile,
590
+ {
591
+ ...state,
592
+ phase,
593
+ targetVersion: o.manifest.version,
594
+ previousVersion: o.currentVersion,
595
+ ...step !== void 0 ? { step } : {},
596
+ ...error !== void 0 ? { error } : { error: null },
597
+ failedCount: state.failedCount
598
+ },
599
+ now
600
+ );
601
+ };
602
+ const previousInstall = readInstallInfo(o.home);
603
+ let swapped = false;
604
+ let webRootSwapped = false;
605
+ let backupPath = null;
606
+ try {
607
+ setState("downloading", "downloading");
608
+ const platformInfo2 = o.manifest.platforms?.[o.platformKey];
609
+ const staged = await stageServerBinary({
610
+ home: o.home,
611
+ stagingDir: paths.stagingDir,
612
+ serverPackage: platformInfo2?.server ?? o.serverPackage,
613
+ version: o.manifest.version,
614
+ sha256: platformInfo2?.serverSha256 ?? null,
615
+ platform: o.platform,
616
+ npm: o.npm,
617
+ installIntoPrefix: o.installIntoPrefix,
618
+ log
619
+ });
620
+ setState("swapping", "swapping");
621
+ const wasRunning = await o.control.isRunning();
622
+ backupPath = backupCurrentBinary({
623
+ currentPath: o.currentServerPath,
624
+ backupsDir: paths.backupsDir,
625
+ currentVersion: o.currentVersion,
626
+ platform: o.platform
627
+ });
628
+ if (backupPath) log(`Backed up the current binary to ${backupPath}`);
629
+ const serverPath = installManagedBinary({
630
+ stagedPath: staged.path,
631
+ serverDir: paths.serverDir,
632
+ version: o.manifest.version,
633
+ platform: o.platform
634
+ });
635
+ writeInstallInfo(o.home, applyServerInfo(previousInstall, { serverPath, serverVersion: o.manifest.version }));
636
+ pruneManagedBinaries(paths.serverDir, serverPath);
637
+ swapped = true;
638
+ setState("restarting", "restarting");
639
+ if (wasRunning) {
640
+ log("Restarting the Astra server\u2026");
641
+ await o.control.stop();
642
+ }
643
+ try {
644
+ webRootSwapped = installManagedWebRoot(staged.path, paths.serverDir);
645
+ } catch (err) {
646
+ log(`Could not update the web UI files: ${err instanceof Error ? err.message : String(err)}`);
647
+ }
648
+ await o.control.start();
649
+ const reported = await o.control.probeVersion();
650
+ if (reported !== o.manifest.version) {
651
+ throw new UpdateError(
652
+ `The server reports ${reported ?? "no version"} after the restart; expected ${o.manifest.version}.`
653
+ );
654
+ }
655
+ fs6.rmSync(paths.stagingDir, { recursive: true, force: true });
656
+ if (webRootSwapped) commitManagedWebRoot(paths.serverDir);
657
+ writeUpdateState(paths.stateFile, { ...initialState(now), phase: "idle" }, now);
658
+ log(`Astra updated to ${o.manifest.version}.`);
659
+ return { from: o.currentVersion, to: o.manifest.version, serverPath };
660
+ } catch (err) {
661
+ const message = err instanceof Error ? err.message : String(err);
662
+ setState("rolling-back", "rolling-back", message);
663
+ if (swapped) await rollback(o, previousInstall, backupPath, webRootSwapped, setState, log);
664
+ const failed = {
665
+ phase: "failed",
666
+ targetVersion: o.manifest.version,
667
+ previousVersion: o.currentVersion,
668
+ error: message,
669
+ failedCount: state.failedCount + 1,
670
+ updatedAt: now().toISOString()
671
+ };
672
+ writeUpdateState(paths.stateFile, failed, now);
673
+ throw err instanceof UpdateError ? err : new UpdateError(message);
674
+ }
675
+ }
676
+ async function rollback(o, previousInstall, backupPath, webRootSwapped, setState, log) {
677
+ const restorePath = previousInstall?.serverPath ?? backupPath;
678
+ try {
679
+ if (restorePath) {
680
+ log(`Rolling back to ${restorePath}`);
681
+ writeInstallInfo(o.home, applyServerInfo(previousInstall, { serverPath: restorePath }));
682
+ }
683
+ if (await o.control.isRunning()) await o.control.stop();
684
+ if (webRootSwapped) restoreManagedWebRoot(updatePaths(o.home).serverDir);
685
+ await o.control.start();
686
+ } catch (rollbackErr) {
687
+ setState("failed", void 0, `Rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`);
688
+ log("Rollback failed \u2014 see the server log. `astra restore-all` still works offline.");
689
+ }
690
+ }
691
+
692
+ // ../packages/update-core/src/check.ts
693
+ async function checkForServerUpdate(o) {
694
+ const doFetch = o.fetchImpl ?? globalThis.fetch;
695
+ if (o.baseUrl && doFetch) {
696
+ try {
697
+ const res = await doFetch(`${o.baseUrl.replace(/\/+$/, "")}/api/update/status`, {
698
+ signal: AbortSignal.timeout(o.timeoutMs ?? 3e3)
699
+ });
700
+ if (res.ok) {
701
+ const status = await res.json();
702
+ return {
703
+ availableVersion: status.available ?? null,
704
+ notes: status.notes ?? null,
705
+ source: "server"
706
+ };
707
+ }
708
+ } catch {
709
+ }
710
+ }
711
+ const manifest = await fetchManifest({
712
+ feedUrl: o.feedUrl ?? DEFAULT_FEED_URL,
713
+ channel: o.channel ?? DEFAULT_CHANNEL,
714
+ timeoutMs: o.timeoutMs,
715
+ fetchImpl: o.fetchImpl,
716
+ userAgent: o.userAgent
717
+ });
718
+ return {
719
+ availableVersion: isNewerVersion(manifest.version, o.currentVersion) ? manifest.version : null,
720
+ notes: manifest.notes ?? null,
721
+ source: "feed",
722
+ manifest
723
+ };
724
+ }
725
+
726
+ // ../packages/update-core/src/desktop-install.ts
727
+ import fs7 from "fs";
728
+ import path7 from "path";
729
+ import { spawn as spawn2 } from "child_process";
730
+ function walkFiles(dir, depth) {
731
+ if (depth < 0) return [];
732
+ let entries;
733
+ try {
734
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
735
+ } catch {
736
+ return [];
737
+ }
738
+ const out = [];
739
+ for (const e of entries) {
740
+ const full = path7.join(dir, e.name);
741
+ if (e.isDirectory()) out.push(...walkFiles(full, depth - 1));
742
+ else if (e.isFile()) out.push(full);
743
+ }
744
+ return out;
745
+ }
746
+ function walkDirs(dir, depth) {
747
+ if (depth < 0) return [];
748
+ let entries;
749
+ try {
750
+ entries = fs7.readdirSync(dir, { withFileTypes: true });
751
+ } catch {
752
+ return [];
753
+ }
754
+ const out = [];
755
+ for (const e of entries) {
756
+ if (e.isDirectory()) {
757
+ const full = path7.join(dir, e.name);
758
+ out.push(full, ...walkDirs(full, depth - 1));
759
+ }
760
+ }
761
+ return out;
762
+ }
763
+ function findDesktopEntry(appDir, platform) {
764
+ if (!fs7.existsSync(appDir)) {
765
+ throw new Error(`The desktop package does not contain an app directory (${appDir}).`);
766
+ }
767
+ if (platform === "darwin") {
768
+ const bundles = walkDirs(appDir, 4).filter((d) => d.endsWith(".app")).sort((a, b) => a.length - b.length);
769
+ const first2 = bundles[0];
770
+ if (first2) return { path: first2, isBundle: true };
771
+ throw new Error("Could not find the .app bundle inside the desktop package.");
772
+ }
773
+ const exes = walkFiles(appDir, 3).filter((f) => platform === "win32" ? /\.exe$/i.test(f) : !/\.(so|png|json|sh|txt|desktop)$/i.test(f)).filter((f) => {
774
+ if (platform === "win32") return !/(crashpad|installer|unins|squirrel)/i.test(f);
775
+ try {
776
+ fs7.accessSync(f, fs7.constants.X_OK);
777
+ return true;
778
+ } catch {
779
+ return false;
780
+ }
781
+ }).sort((a, b) => a.split(path7.sep).length - b.split(path7.sep).length || a.length - b.length);
782
+ const first = exes[0];
783
+ if (first) return { path: first, isBundle: false };
784
+ throw new Error("Could not find the desktop executable inside the desktop package.");
785
+ }
786
+ function macLauncherTarget(home = "") {
787
+ return path7.join(home, "Applications", "Astra.app");
788
+ }
789
+ function winLauncherTarget(home, env = process.env) {
790
+ const base = env.APPDATA || path7.join(home, "AppData", "Roaming");
791
+ return path7.join(base, "Microsoft", "Windows", "Start Menu", "Programs", "Astra.lnk");
792
+ }
793
+ function linuxLauncherTarget(home) {
794
+ return path7.join(home, ".local", "share", "applications", "astra.desktop");
795
+ }
796
+ function renderLinuxDesktopFile(exePath) {
797
+ return `[Desktop Entry]
798
+ Type=Application
799
+ Name=Astra
800
+ Comment=Local AI gateway
801
+ Exec=${exePath}
802
+ Terminal=false
803
+ Categories=Development;Network;
804
+ `;
805
+ }
806
+ function runCapture2(command, args, timeoutMs) {
807
+ return new Promise((resolve, reject) => {
808
+ const child = spawn2(command, [...args], { stdio: "ignore", windowsHide: true, shell: command.endsWith(".cmd") });
809
+ let settled = false;
810
+ const timer = setTimeout(() => {
811
+ settled = true;
812
+ reject(new Error(`${command} timed out`));
813
+ }, timeoutMs);
814
+ child.on("error", (err) => {
815
+ clearTimeout(timer);
816
+ if (!settled) reject(err);
817
+ });
818
+ child.on("close", (code) => {
819
+ clearTimeout(timer);
820
+ if (!settled) resolve({ code, stderr: "" });
821
+ });
822
+ });
823
+ }
824
+ async function createLauncher(entry, platform, home, env = process.env) {
825
+ if (platform === "darwin") {
826
+ const target2 = macLauncherTarget(home);
827
+ fs7.mkdirSync(path7.dirname(target2), { recursive: true });
828
+ fs7.rmSync(target2, { force: true, recursive: true });
829
+ fs7.symlinkSync(entry.path, target2, "dir");
830
+ return target2;
831
+ }
832
+ if (platform === "win32") {
833
+ const target2 = winLauncherTarget(home, env);
834
+ fs7.mkdirSync(path7.dirname(target2), { recursive: true });
835
+ const esc = (s) => s.replace(/'/g, "''");
836
+ const ps = `$s=(New-Object -ComObject WScript.Shell).CreateShortcut('${esc(target2)}');$s.TargetPath='${esc(entry.path)}';$s.Save()`;
837
+ const r = await runCapture2("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", ps], 6e4);
838
+ if (r.code !== 0) throw new Error("Failed to create the Start Menu shortcut.");
839
+ return target2;
840
+ }
841
+ const target = linuxLauncherTarget(home);
842
+ fs7.mkdirSync(path7.dirname(target), { recursive: true });
843
+ fs7.writeFileSync(target, renderLinuxDesktopFile(entry.path));
844
+ return target;
845
+ }
846
+ function removeLauncher(platform, home, env = process.env) {
847
+ if (platform === "darwin") {
848
+ const target2 = macLauncherTarget(home);
849
+ try {
850
+ const st = fs7.lstatSync(target2);
851
+ if (st.isSymbolicLink()) fs7.rmSync(target2);
852
+ } catch {
853
+ }
854
+ return;
855
+ }
856
+ const target = platform === "win32" ? winLauncherTarget(home, env) : linuxLauncherTarget(home);
857
+ fs7.rmSync(target, { force: true });
858
+ }
859
+ async function installDesktopClient(o) {
860
+ const prefix = path7.join(o.home, "desktop");
861
+ const spec = `${o.desktopPackage}@${o.version}`;
862
+ o.log?.(`Installing ${spec} (this may take a minute)\u2026`);
863
+ await npmInstallIntoPrefix(prefix, spec, o.npm);
864
+ const pkgDir = path7.join(prefix, "node_modules", ...o.desktopPackage.split("/"));
865
+ const entry = findDesktopEntry(path7.join(pkgDir, "app"), o.platform);
866
+ const launcher = await createLauncher(entry, o.platform, o.home, o.npm?.env);
867
+ let version = o.version;
868
+ try {
869
+ const pkg = JSON.parse(fs7.readFileSync(path7.join(pkgDir, "package.json"), "utf8"));
870
+ version = pkg.version ?? version;
871
+ } catch {
872
+ }
873
+ return { path: entry.path, version, launcher };
874
+ }
875
+
876
+ // src/lib/paths.ts
877
+ import os from "os";
878
+ import path8 from "path";
879
+ function astraHome(env = process.env) {
880
+ const override = env.ASTRA_HOME?.trim();
881
+ return override ? override : path8.join(os.homedir(), ".astra");
882
+ }
883
+ function homePaths(home) {
884
+ return {
885
+ home,
886
+ configFile: path8.join(home, "config.json"),
887
+ runtimeFile: path8.join(home, "runtime.json"),
888
+ installFile: path8.join(home, "install.json"),
889
+ logsDir: path8.join(home, "logs"),
890
+ serverStdoutLog: path8.join(home, "logs", "server-stdout.log"),
891
+ dbFile: path8.join(home, "astra.db"),
892
+ keysDir: path8.join(home, "keys"),
893
+ desktopPrefix: path8.join(home, "desktop")
894
+ };
895
+ }
896
+
897
+ // src/lib/browser.ts
898
+ import { spawn as spawn3 } from "child_process";
899
+ function openInBrowser(url, platform = process.platform) {
900
+ try {
901
+ if (platform === "darwin") {
902
+ spawn3("open", [url], { detached: true, stdio: "ignore" }).unref();
903
+ return;
904
+ }
905
+ if (platform === "win32") {
906
+ spawn3("rundll32", ["url.dll,FileProtocolHandler", url], {
907
+ detached: true,
908
+ stdio: "ignore",
909
+ windowsHide: true
910
+ }).unref();
911
+ return;
912
+ }
913
+ spawn3("xdg-open", [url], { detached: true, stdio: "ignore" }).unref();
914
+ } catch {
915
+ }
916
+ }
917
+
918
+ // src/lib/logs.ts
919
+ import fs8 from "fs";
920
+ import path9 from "path";
921
+ function findNewestLog(home) {
922
+ const { logsDir, serverStdoutLog } = homePaths(home);
923
+ let candidates = [];
924
+ try {
925
+ candidates = fs8.readdirSync(logsDir).filter((f) => /^astra-\d{8}\.log$/.test(f)).map((f) => path9.join(logsDir, f));
926
+ } catch {
927
+ }
928
+ candidates.sort((a, b) => mtimeOf(b) - mtimeOf(a));
929
+ const newest = candidates[0];
930
+ if (newest) return newest;
931
+ if (fs8.existsSync(serverStdoutLog)) return serverStdoutLog;
932
+ throw new AstraError("No log files found.", "Start the server first: `astra start`.");
933
+ }
934
+ function mtimeOf(p) {
935
+ try {
936
+ return fs8.statSync(p).mtimeMs;
937
+ } catch {
938
+ return 0;
939
+ }
940
+ }
941
+ function tailLines(filePath, lineCount) {
942
+ const text = fs8.readFileSync(filePath, "utf8");
943
+ const lines = text.split(/\r?\n/);
944
+ while (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
945
+ return lines.slice(-lineCount).join("\n");
946
+ }
947
+ function followFile(filePath, write) {
948
+ let pos = fs8.existsSync(filePath) ? fs8.statSync(filePath).size : 0;
949
+ let stopped = false;
950
+ let timer;
951
+ const tick = () => {
952
+ if (stopped) return;
953
+ try {
954
+ const st = fs8.existsSync(filePath) ? fs8.statSync(filePath) : null;
955
+ if (!st) {
956
+ pos = 0;
957
+ } else if (st.size < pos) {
958
+ pos = 0;
959
+ } else if (st.size > pos) {
960
+ const fd = fs8.openSync(filePath, "r");
961
+ try {
962
+ const buf = Buffer.alloc(st.size - pos);
963
+ const bytesRead = fs8.readSync(fd, buf, 0, buf.length, pos);
964
+ pos += bytesRead;
965
+ write(buf.toString("utf8"));
966
+ } finally {
967
+ fs8.closeSync(fd);
968
+ }
969
+ }
970
+ } catch {
971
+ }
972
+ timer = setTimeout(tick, 1e3);
973
+ };
974
+ tick();
975
+ return () => {
976
+ stopped = true;
977
+ if (timer) clearTimeout(timer);
978
+ };
979
+ }
980
+
981
+ // src/lib/server-lifecycle.ts
982
+ import fs11 from "fs";
983
+ import { spawn as spawn5 } from "child_process";
984
+
985
+ // src/lib/json-file.ts
986
+ import fs9 from "fs";
987
+ import path10 from "path";
988
+ function readTextOrNull(filePath) {
989
+ try {
990
+ return fs9.readFileSync(filePath, "utf8");
991
+ } catch (err) {
992
+ const code = err.code;
993
+ if (code === "ENOENT" || code === "EISDIR") return null;
994
+ throw err;
995
+ }
996
+ }
997
+ function readJsonFile(filePath) {
998
+ const text = readTextOrNull(filePath);
999
+ if (text === null || text.trim() === "") return null;
1000
+ try {
1001
+ return JSON.parse(text);
1002
+ } catch {
1003
+ return null;
1004
+ }
1005
+ }
1006
+ function writeJsonFile(filePath, data, mode = 420) {
1007
+ fs9.mkdirSync(path10.dirname(filePath), { recursive: true });
1008
+ fs9.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
1009
+ `, { mode });
1010
+ }
1011
+
1012
+ // src/lib/config-file.ts
1013
+ var DEFAULT_PORT = 17321;
1014
+ var DEFAULT_HOST = "127.0.0.1";
1015
+ function readConfig(home) {
1016
+ return readJsonFile(homePaths(home).configFile) ?? {};
1017
+ }
1018
+ function writeConfig(home, patch) {
1019
+ const next = { ...readConfig(home), ...patch };
1020
+ writeJsonFile(homePaths(home).configFile, next);
1021
+ return next;
1022
+ }
1023
+ function normalizePort(value, fallback = DEFAULT_PORT) {
1024
+ if (value === void 0 || value === null || value === "") return fallback;
1025
+ const n = typeof value === "number" ? value : Number(value);
1026
+ if (!Number.isInteger(n) || n < 1 || n > 65535) {
1027
+ throw new AstraError(`Invalid port: ${String(value)}. Use a number between 1 and 65535.`);
1028
+ }
1029
+ return n;
1030
+ }
1031
+ function normalizeHost(value, fallback = DEFAULT_HOST) {
1032
+ if (value === void 0 || value === null || value === "") return fallback;
1033
+ if (typeof value !== "string") throw new AstraError(`Invalid host: ${String(value)}`);
1034
+ return value;
1035
+ }
1036
+
1037
+ // src/lib/runtime.ts
1038
+ var STARTED_BY_VALUES = ["cli", "desktop", "autostart"];
1039
+ function parseRuntimeJson(text) {
1040
+ let raw;
1041
+ try {
1042
+ raw = JSON.parse(text);
1043
+ } catch {
1044
+ return null;
1045
+ }
1046
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
1047
+ const obj = raw;
1048
+ const { pid, port, host } = obj;
1049
+ if (typeof pid !== "number" || !Number.isInteger(pid) || pid <= 0) return null;
1050
+ if (typeof port !== "number" || !Number.isInteger(port) || port <= 0 || port > 65535) return null;
1051
+ if (typeof host !== "string" || host.trim() === "") return null;
1052
+ const info2 = { pid, port, host };
1053
+ if (typeof obj.version === "string") info2.version = obj.version;
1054
+ if (typeof obj.apiVersion === "string") info2.apiVersion = obj.apiVersion;
1055
+ if (typeof obj.startedBy === "string" && STARTED_BY_VALUES.includes(obj.startedBy)) {
1056
+ info2.startedBy = obj.startedBy;
1057
+ }
1058
+ if (typeof obj.startedAt === "string") info2.startedAt = obj.startedAt;
1059
+ if (typeof obj.runtimeToken === "string") info2.runtimeToken = obj.runtimeToken;
1060
+ return info2;
1061
+ }
1062
+ function readRuntimeInfo(runtimeFile) {
1063
+ const text = readTextOrNull(runtimeFile);
1064
+ return text === null ? null : parseRuntimeJson(text);
1065
+ }
1066
+
1067
+ // src/lib/binary.ts
1068
+ import fs10 from "fs";
1069
+ import path11 from "path";
1070
+ import { createRequire } from "module";
1071
+ import { fileURLToPath } from "url";
1072
+
1073
+ // package.json
1074
+ var package_default = {
1075
+ name: "astragate",
1076
+ version: "0.2.0",
1077
+ description: "Astra \u2014 a local AI gateway. Install, start, stop and manage the astra-server from the terminal.",
1078
+ license: "MIT",
1079
+ type: "module",
1080
+ bin: {
1081
+ astra: "dist/index.js"
1082
+ },
1083
+ files: [
1084
+ "dist"
1085
+ ],
1086
+ engines: {
1087
+ node: ">=18"
1088
+ },
1089
+ keywords: [
1090
+ "astra",
1091
+ "ai",
1092
+ "gateway",
1093
+ "llm",
1094
+ "proxy"
1095
+ ],
1096
+ scripts: {
1097
+ build: "tsup",
1098
+ watch: "tsup --watch",
1099
+ test: "vitest run",
1100
+ typecheck: "tsc --noEmit",
1101
+ lint: "eslint ."
1102
+ },
1103
+ dependencies: {
1104
+ commander: "^12.1.0"
1105
+ },
1106
+ optionalDependencies: {
1107
+ "@aidotnet/server-darwin-arm64": "0.2.0",
1108
+ "@aidotnet/server-darwin-x64": "0.2.0",
1109
+ "@aidotnet/server-linux-arm64": "0.2.0",
1110
+ "@aidotnet/server-linux-x64": "0.2.0",
1111
+ "@aidotnet/server-win32-arm64": "0.2.0",
1112
+ "@aidotnet/server-win32-x64": "0.2.0"
1113
+ },
1114
+ devDependencies: {
1115
+ "@aidotnet/update-core": "workspace:*",
1116
+ "@types/node": "^24.0.0",
1117
+ eslint: "^9.17.0",
1118
+ prettier: "^3.4.2",
1119
+ tsup: "^8.3.5",
1120
+ typescript: "^5.7.2",
1121
+ "typescript-eslint": "^8.18.1",
1122
+ vitest: "^3.0.0"
1123
+ },
1124
+ packageManager: "pnpm@9.15.0"
1125
+ };
1126
+
1127
+ // src/version.ts
1128
+ var VERSION = package_default.version;
1129
+ var PACKAGE_NAME = package_default.name;
1130
+
1131
+ // src/lib/binary.ts
1132
+ function findRepoRoot(startDir, exists) {
1133
+ let dir = path11.resolve(startDir);
1134
+ for (; ; ) {
1135
+ if (exists(path11.join(dir, "src", "Astra.Server"))) return dir;
1136
+ const parent = path11.dirname(dir);
1137
+ if (parent === dir) return null;
1138
+ dir = parent;
1139
+ }
1140
+ }
1141
+ function defaultResolveFrom(baseDir, request) {
1142
+ const require2 = createRequire(path11.join(baseDir, "index.js"));
1143
+ return require2.resolve(request);
1144
+ }
1145
+ function resolveServerBinary(options = {}) {
1146
+ const env = options.env ?? process.env;
1147
+ const exists = options.existsSync ?? ((p) => fs10.existsSync(p));
1148
+ const cwd = options.cwd ?? process.cwd();
1149
+ const plat = platformInfo(options.platform ?? process.platform, options.arch ?? process.arch);
1150
+ const envBin = env.ASTRA_SERVER_BIN?.trim();
1151
+ if (envBin) {
1152
+ if (!exists(envBin)) {
1153
+ throw new AstraError(`ASTRA_SERVER_BIN points to a missing file: ${envBin}`);
1154
+ }
1155
+ return { command: envBin, args: [], serverPath: envBin, source: "env" };
1156
+ }
1157
+ const installPath = options.installServerPath?.trim();
1158
+ if (installPath && exists(installPath)) {
1159
+ return { command: installPath, args: [], serverPath: installPath, source: "install" };
1160
+ }
1161
+ const repoRoot = findRepoRoot(cwd, exists);
1162
+ if (repoRoot) {
1163
+ const binDir = path11.join(repoRoot, "src", "Astra.Server", "bin", "Debug", "net10.0");
1164
+ const native = path11.join(binDir, plat.binaryName);
1165
+ if (exists(native)) return { command: native, args: [], serverPath: native, source: "repo" };
1166
+ const dll = path11.join(binDir, "astra-server.dll");
1167
+ if (exists(dll)) return { command: "dotnet", args: [dll], serverPath: dll, source: "repo" };
1168
+ }
1169
+ const packaged = findPackagedServerBinary(options);
1170
+ if ("path" in packaged) {
1171
+ return { command: packaged.path, args: [], serverPath: packaged.path, source: "package" };
1172
+ }
1173
+ throw new AstraError(
1174
+ `Could not find the Astra server binary. ${packaged.error}`,
1175
+ `Reinstall the CLI (\`npm install -g ${PACKAGE_NAME}\`), or point ASTRA_SERVER_BIN at the server binary.`
1176
+ );
1177
+ }
1178
+ function findPackagedServerBinary(options = {}) {
1179
+ const exists = options.existsSync ?? ((p) => fs10.existsSync(p));
1180
+ const readText = options.readFileSync ?? ((p, _enc) => fs10.readFileSync(p, "utf8"));
1181
+ const baseDir = options.baseDir ?? path11.dirname(fileURLToPath(import.meta.url));
1182
+ const plat = platformInfo(options.platform ?? process.platform, options.arch ?? process.arch);
1183
+ const resolveFrom = options.resolveFrom ?? defaultResolveFrom;
1184
+ try {
1185
+ const pkgJsonPath = resolveFrom(baseDir, `${plat.serverPackage}/package.json`);
1186
+ const pkg = JSON.parse(readText(pkgJsonPath, "utf8"));
1187
+ const binRel = pkg.bin?.["astra-server"];
1188
+ if (!binRel) return { error: `${plat.serverPackage} has no 'astra-server' bin entry.` };
1189
+ const binPath = path11.resolve(path11.dirname(pkgJsonPath), binRel);
1190
+ if (exists(binPath)) return { path: binPath };
1191
+ return { error: `${plat.serverPackage} is installed but its binary is missing (${binPath}).` };
1192
+ } catch {
1193
+ return { error: `${plat.serverPackage} is not installed.` };
1194
+ }
1195
+ }
1196
+
1197
+ // src/lib/process-utils.ts
1198
+ import { spawn as spawn4 } from "child_process";
1199
+ function runCapture3(command, args, options = {}) {
1200
+ return new Promise((resolve, reject) => {
1201
+ const child = spawn4(command, [...args], {
1202
+ cwd: options.cwd,
1203
+ env: options.env,
1204
+ windowsHide: true,
1205
+ shell: options.shell ?? false
1206
+ });
1207
+ let stdout = "";
1208
+ let stderr = "";
1209
+ let settled = false;
1210
+ const timer = options.timeoutMs ? setTimeout(() => {
1211
+ settled = true;
1212
+ try {
1213
+ child.kill("SIGKILL");
1214
+ } catch {
1215
+ }
1216
+ reject(new Error(`Command timed out after ${options.timeoutMs}ms: ${command} ${args.join(" ")}`));
1217
+ }, options.timeoutMs) : null;
1218
+ child.stdout?.on("data", (d) => {
1219
+ stdout += d.toString();
1220
+ });
1221
+ child.stderr?.on("data", (d) => {
1222
+ stderr += d.toString();
1223
+ });
1224
+ if (options.input !== void 0) {
1225
+ child.stdin?.write(options.input);
1226
+ }
1227
+ child.stdin?.end();
1228
+ child.on("error", (err) => {
1229
+ if (timer) clearTimeout(timer);
1230
+ if (!settled) reject(err);
1231
+ });
1232
+ child.on("close", (code) => {
1233
+ if (timer) clearTimeout(timer);
1234
+ if (!settled) resolve({ code, stdout, stderr });
1235
+ });
1236
+ });
1237
+ }
1238
+ function spawnDetached(command, args, stdio) {
1239
+ const child = spawn4(command, [...args], { detached: true, stdio, windowsHide: true });
1240
+ child.unref();
1241
+ return child;
1242
+ }
1243
+ function exitCodeOf(child) {
1244
+ return new Promise((resolve, reject) => {
1245
+ child.on("error", reject);
1246
+ child.on("close", (code) => resolve(code));
1247
+ });
1248
+ }
1249
+ function isPidAlive(pid) {
1250
+ try {
1251
+ process.kill(pid, 0);
1252
+ return true;
1253
+ } catch (err) {
1254
+ return err.code === "EPERM";
1255
+ }
1256
+ }
1257
+ async function terminatePid(pid, platform = process.platform) {
1258
+ if (platform === "win32") {
1259
+ try {
1260
+ await runCapture3("taskkill", ["/PID", String(pid), "/T", "/F"], { timeoutMs: 1e4 });
1261
+ } catch {
1262
+ }
1263
+ return;
1264
+ }
1265
+ try {
1266
+ process.kill(pid, "SIGTERM");
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ async function killPid(pid, platform = process.platform) {
1271
+ if (platform === "win32") {
1272
+ await terminatePid(pid, platform);
1273
+ return;
1274
+ }
1275
+ try {
1276
+ process.kill(pid, "SIGKILL");
1277
+ } catch {
1278
+ }
1279
+ }
1280
+ async function waitForPidExit(pid, timeoutMs, isAlive = isPidAlive) {
1281
+ const deadline = Date.now() + timeoutMs;
1282
+ while (Date.now() < deadline) {
1283
+ if (!isAlive(pid)) return true;
1284
+ await delay(300);
1285
+ }
1286
+ return !isAlive(pid);
1287
+ }
1288
+ function delay(ms) {
1289
+ return new Promise((resolve) => setTimeout(resolve, ms));
1290
+ }
1291
+
1292
+ // src/lib/install-json.ts
1293
+ function readInstallJson(home) {
1294
+ return readJsonFile(homePaths(home).installFile);
1295
+ }
1296
+ function writeInstallJson(home, data) {
1297
+ writeJsonFile(homePaths(home).installFile, data);
1298
+ }
1299
+ function nowIso() {
1300
+ return (/* @__PURE__ */ new Date()).toISOString();
1301
+ }
1302
+ function applyServerInfo2(existing, server) {
1303
+ return {
1304
+ ...existing ?? {},
1305
+ serverPath: server.serverPath,
1306
+ ...server.serverVersion ? { serverVersion: server.serverVersion } : {},
1307
+ updatedAt: nowIso()
1308
+ };
1309
+ }
1310
+ function applyDesktopInfo(existing, desktop) {
1311
+ return {
1312
+ ...existing ?? {},
1313
+ desktopPath: desktop.desktopPath,
1314
+ ...desktop.desktopVersion ? { desktopVersion: desktop.desktopVersion } : {},
1315
+ updatedAt: nowIso()
1316
+ };
1317
+ }
1318
+ function clearDesktopInfo(existing) {
1319
+ const { desktopPath: _dp, desktopVersion: _dv, ...rest } = existing ?? {};
1320
+ return { ...rest, updatedAt: nowIso() };
1321
+ }
1322
+
1323
+ // src/lib/server-lifecycle.ts
1324
+ function currentRuntime(home) {
1325
+ return readRuntimeInfo(homePaths(home).runtimeFile);
1326
+ }
1327
+ function clearRuntimeFile(home) {
1328
+ try {
1329
+ fs11.rmSync(homePaths(home).runtimeFile, { force: true });
1330
+ } catch {
1331
+ }
1332
+ }
1333
+ async function probeRunning(home, fetchImpl) {
1334
+ const rt = currentRuntime(home);
1335
+ if (!rt) return null;
1336
+ if (!isPidAlive(rt.pid)) return null;
1337
+ const base = baseUrlFor(rt.host, rt.port);
1338
+ if (await checkHealth(base, fetchImpl)) return { runtime: rt, base };
1339
+ return null;
1340
+ }
1341
+ async function getServerVersion(command) {
1342
+ try {
1343
+ const r = await runCapture3(command.command, [...command.args, "version"], { timeoutMs: 15e3 });
1344
+ if (r.code !== 0) return void 0;
1345
+ const parsed = JSON.parse(r.stdout.trim());
1346
+ return typeof parsed.version === "string" ? parsed.version : void 0;
1347
+ } catch {
1348
+ return void 0;
1349
+ }
1350
+ }
1351
+ async function waitForHealthy(base, timeoutMs, aborted, fetchImpl) {
1352
+ const deadline = Date.now() + timeoutMs;
1353
+ while (Date.now() < deadline) {
1354
+ if (aborted?.()) return false;
1355
+ if (await checkHealth(base, fetchImpl)) return true;
1356
+ await delay(400);
1357
+ }
1358
+ return false;
1359
+ }
1360
+ async function waitForStartedServer(o) {
1361
+ const isAlive = o.isAlive ?? isPidAlive;
1362
+ const deadline = Date.now() + o.timeoutMs;
1363
+ while (Date.now() < deadline) {
1364
+ if (o.aborted?.()) return null;
1365
+ const rt = o.readRuntime();
1366
+ if (rt && isAlive(rt.pid)) {
1367
+ const base = baseUrlFor(rt.host, rt.port);
1368
+ if (await checkHealth(base, o.fetchImpl)) return { base, runtime: rt };
1369
+ } else if (!rt && await checkHealth(o.fallbackBase, o.fetchImpl)) {
1370
+ return { base: o.fallbackBase, runtime: null };
1371
+ }
1372
+ await delay(o.pollMs ?? 400);
1373
+ }
1374
+ return null;
1375
+ }
1376
+ function openLogFds(home) {
1377
+ const p = homePaths(home);
1378
+ fs11.mkdirSync(p.logsDir, { recursive: true });
1379
+ const fd = fs11.openSync(p.serverStdoutLog, "a");
1380
+ return { out: fd, err: fd, logFile: p.serverStdoutLog };
1381
+ }
1382
+ async function startServer(options = {}) {
1383
+ const home = astraHome();
1384
+ const p = homePaths(home);
1385
+ const cfg = readConfig(home);
1386
+ const port = normalizePort(options.port ?? cfg.port);
1387
+ const host = normalizeHost(options.host ?? cfg.host);
1388
+ if (options.port !== void 0 || options.host !== void 0 || cfg.port === void 0 || cfg.host === void 0) {
1389
+ writeConfig(home, { port, host });
1390
+ }
1391
+ const existing = await probeRunning(home);
1392
+ if (existing) {
1393
+ return {
1394
+ base: existing.base,
1395
+ host: existing.runtime.host,
1396
+ port: existing.runtime.port,
1397
+ alreadyRunning: true,
1398
+ pid: existing.runtime.pid,
1399
+ command: resolveServerBinary({ installServerPath: readInstallJson(home)?.serverPath ?? null }),
1400
+ logFile: p.serverStdoutLog
1401
+ };
1402
+ }
1403
+ if (currentRuntime(home)) clearRuntimeFile(home);
1404
+ const command = resolveServerBinary({ installServerPath: readInstallJson(home)?.serverPath ?? null });
1405
+ const version = await getServerVersion(command);
1406
+ writeInstallJson(
1407
+ home,
1408
+ applyServerInfo2(readInstallJson(home), {
1409
+ serverPath: command.serverPath,
1410
+ serverVersion: version
1411
+ })
1412
+ );
1413
+ const args = [...command.args, "serve", "--port", String(port), "--host", host, "--started-by", "cli"];
1414
+ const base = baseUrlFor(host, port);
1415
+ if (options.foreground) {
1416
+ const child2 = spawn5(command.command, args, { stdio: "inherit", windowsHide: true });
1417
+ void (async () => {
1418
+ if (await waitForHealthy(base, 2e4, () => child2.exitCode !== null)) {
1419
+ }
1420
+ })();
1421
+ const code = await exitCodeOf(child2);
1422
+ if (code !== 0) {
1423
+ throw new AstraError(
1424
+ `The server exited with code ${code}.`,
1425
+ `Check the logs in ${p.logsDir}`
1426
+ );
1427
+ }
1428
+ return { base, host, port, alreadyRunning: false, command, logFile: p.serverStdoutLog };
1429
+ }
1430
+ const fds = openLogFds(home);
1431
+ let child;
1432
+ try {
1433
+ child = spawnDetached(command.command, args, ["ignore", fds.out, fds.err]);
1434
+ } catch (err) {
1435
+ throw new AstraError(
1436
+ `Failed to start the server binary: ${err.message}`,
1437
+ `Binary: ${command.command}`
1438
+ );
1439
+ } finally {
1440
+ try {
1441
+ fs11.closeSync(fds.out);
1442
+ } catch {
1443
+ }
1444
+ }
1445
+ const started = await waitForStartedServer({
1446
+ fallbackBase: base,
1447
+ timeoutMs: 2e4,
1448
+ aborted: () => child.exitCode !== null,
1449
+ readRuntime: () => currentRuntime(home)
1450
+ });
1451
+ if (!started) {
1452
+ if (child.exitCode !== null && child.exitCode !== 0) {
1453
+ throw new AstraError(
1454
+ `The server exited with code ${child.exitCode} before becoming healthy.`,
1455
+ `Check the log: ${p.serverStdoutLog}`
1456
+ );
1457
+ }
1458
+ throw new AstraError(
1459
+ `The server did not become healthy at ${base} within 20 seconds.`,
1460
+ `Check the log: ${p.serverStdoutLog}`
1461
+ );
1462
+ }
1463
+ const rt = started.runtime;
1464
+ return {
1465
+ base: started.base,
1466
+ host: rt?.host ?? host,
1467
+ port: rt?.port ?? port,
1468
+ alreadyRunning: false,
1469
+ pid: rt?.pid ?? child.pid,
1470
+ command,
1471
+ logFile: p.serverStdoutLog
1472
+ };
1473
+ }
1474
+ async function stopServer() {
1475
+ const home = astraHome();
1476
+ const rt = currentRuntime(home);
1477
+ if (!rt) return { stopped: false };
1478
+ if (!isPidAlive(rt.pid)) {
1479
+ clearRuntimeFile(home);
1480
+ return { stopped: false };
1481
+ }
1482
+ const base = baseUrlFor(rt.host, rt.port);
1483
+ if (rt.runtimeToken) {
1484
+ try {
1485
+ await shutdownRequest(base, rt.runtimeToken);
1486
+ } catch {
1487
+ }
1488
+ }
1489
+ if (!await waitForPidExit(rt.pid, 8e3)) {
1490
+ await terminatePid(rt.pid);
1491
+ if (!await waitForPidExit(rt.pid, 3e3)) {
1492
+ await killPid(rt.pid);
1493
+ await waitForPidExit(rt.pid, 2e3);
1494
+ }
1495
+ }
1496
+ clearRuntimeFile(home);
1497
+ return { stopped: true, pid: rt.pid };
1498
+ }
1499
+ async function serverStatus(fetchImpl) {
1500
+ const home = astraHome();
1501
+ const cfg = readConfig(home);
1502
+ const configPort = normalizePort(cfg.port);
1503
+ const configHost = normalizeHost(cfg.host);
1504
+ const rt = currentRuntime(home);
1505
+ if (!rt) return { running: false, staleRuntime: false, configPort, configHost };
1506
+ if (!isPidAlive(rt.pid)) {
1507
+ clearRuntimeFile(home);
1508
+ return { running: false, staleRuntime: true, configPort, configHost };
1509
+ }
1510
+ const base = baseUrlFor(rt.host, rt.port);
1511
+ const healthy = await checkHealth(base, fetchImpl);
1512
+ return { running: healthy, base, runtime: rt, staleRuntime: false, configPort, configHost };
1513
+ }
1514
+ async function ensureServerRunning() {
1515
+ const home = astraHome();
1516
+ const existing = await probeRunning(home);
1517
+ if (existing) return { base: existing.base, runtime: existing.runtime, started: false };
1518
+ const result = await startServer({});
1519
+ return { base: result.base, runtime: currentRuntime(home) ?? void 0, started: true };
1520
+ }
1521
+
1522
+ // src/commands/manage.ts
1523
+ async function runStart(opts) {
1524
+ console.log("Starting Astra\u2026");
1525
+ const result = await startServer(opts);
1526
+ if (result.alreadyRunning) {
1527
+ success(`Astra is already running at ${style.cyan(result.base)} (pid ${result.pid ?? "?"}).`);
1528
+ info(style.dim("Use `astra restart` to reload it."));
1529
+ if (opts.open) openInBrowser(result.base);
1530
+ return;
1531
+ }
1532
+ console.log(` Binary ${result.command.serverPath} ${style.dim(`(${result.command.source})`)}`);
1533
+ console.log(` Address ${style.cyan(result.base)}`);
1534
+ console.log(` Log ${style.dim(result.logFile)}`);
1535
+ if (opts.foreground) return;
1536
+ success(`Astra is running at ${style.cyan(result.base)}`);
1537
+ if (opts.open) openInBrowser(result.base);
1538
+ }
1539
+ async function runStop() {
1540
+ const result = await stopServer();
1541
+ if (!result.stopped) {
1542
+ console.log("Astra is not running.");
1543
+ return;
1544
+ }
1545
+ success(`Astra stopped (pid ${result.pid ?? "?"}).`);
1546
+ }
1547
+ async function runRestart(opts) {
1548
+ const status = await serverStatus();
1549
+ if (status.running || status.runtime) {
1550
+ console.log("Stopping Astra\u2026");
1551
+ await stopServer();
1552
+ }
1553
+ await runStart(opts);
1554
+ }
1555
+ async function runStatus() {
1556
+ const status = await serverStatus();
1557
+ if (!status.running) {
1558
+ if (status.staleRuntime) {
1559
+ console.log(style.dim("Removed a stale runtime.json (its process is gone)."));
1560
+ }
1561
+ console.log(`Astra is not running. Start it with ${style.cyan("astra start")}.`);
1562
+ process3.exitCode = 1;
1563
+ return;
1564
+ }
1565
+ const rt = status.runtime;
1566
+ if (!rt) return;
1567
+ console.log("Astra is running");
1568
+ console.log(` URL ${style.cyan(status.base ?? "")}`);
1569
+ console.log(` PID ${rt.pid}`);
1570
+ const versionLine = [rt.version, rt.apiVersion ? `API ${rt.apiVersion}` : void 0].filter(Boolean).join(" \xB7 ");
1571
+ if (versionLine) console.log(` Version ${versionLine}`);
1572
+ if (rt.startedBy) {
1573
+ console.log(` Started by ${rt.startedBy}${rt.startedAt ? ` at ${rt.startedAt}` : ""}`);
1574
+ }
1575
+ if (status.base) {
1576
+ try {
1577
+ const r = await checkForServerUpdate({ currentVersion: VERSION, baseUrl: status.base, timeoutMs: 2e3 });
1578
+ if (r.availableVersion) {
1579
+ console.log(` Update ${style.cyan(r.availableVersion)} available \u2014 run ${style.cyan("astra update")}`);
1580
+ }
1581
+ } catch {
1582
+ }
1583
+ }
1584
+ }
1585
+ async function runLogs(opts) {
1586
+ const home = astraHome();
1587
+ const logFile = findNewestLog(home);
1588
+ console.error(style.dim(`# ${logFile}`));
1589
+ process3.stdout.write(`${tailLines(logFile, 100)}
1590
+ `);
1591
+ if (opts.follow) {
1592
+ console.error(style.dim("--- following (Ctrl+C to stop) ---"));
1593
+ const stop = followFile(logFile, (chunk) => process3.stdout.write(chunk));
1594
+ process3.on("SIGINT", () => {
1595
+ stop();
1596
+ process3.exit(0);
1597
+ });
1598
+ await new Promise(() => {
1599
+ });
1600
+ }
1601
+ }
1602
+ async function runOpen() {
1603
+ const status = await serverStatus();
1604
+ if (!status.running || !status.base) {
1605
+ fail("Astra is not running.");
1606
+ info(`Start it with ${style.cyan("astra start --open")}.`);
1607
+ process3.exitCode = 1;
1608
+ return;
1609
+ }
1610
+ openInBrowser(status.base);
1611
+ console.log(`Opened ${status.base}`);
1612
+ }
1613
+
1614
+ // src/commands/autostart.ts
1615
+ import os3 from "os";
1616
+ import process4 from "process";
1617
+
1618
+ // src/lib/autostart.ts
1619
+ import fs12 from "fs";
1620
+ import os2 from "os";
1621
+ import path12 from "path";
1622
+ var LAUNCHD_LABEL = "io.astra.server";
1623
+ var SYSTEMD_UNIT_NAME = "astra.service";
1624
+ var SCHTASKS_TASK_NAME = "Astra Server";
1625
+ function xmlEscape(s) {
1626
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1627
+ }
1628
+ function launchdPlistPath(home = os2.homedir()) {
1629
+ return path12.join(home, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
1630
+ }
1631
+ function renderLaunchdPlist(argv, logFile) {
1632
+ return `<?xml version="1.0" encoding="UTF-8"?>
1633
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1634
+ <plist version="1.0">
1635
+ <dict>
1636
+ <key>Label</key>
1637
+ <string>${xmlEscape(LAUNCHD_LABEL)}</string>
1638
+ <key>ProgramArguments</key>
1639
+ <array>
1640
+ ${argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n")}
1641
+ </array>
1642
+ <key>RunAtLoad</key>
1643
+ <true/>
1644
+ <key>KeepAlive</key>
1645
+ <false/>
1646
+ <key>ProcessType</key>
1647
+ <string>Background</string>
1648
+ <key>StandardOutPath</key>
1649
+ <string>${xmlEscape(logFile)}</string>
1650
+ <key>StandardErrorPath</key>
1651
+ <string>${xmlEscape(logFile)}</string>
1652
+ </dict>
1653
+ </plist>
1654
+ `;
1655
+ }
1656
+ function systemdUnitPath(home = os2.homedir()) {
1657
+ return path12.join(home, ".config", "systemd", "user", SYSTEMD_UNIT_NAME);
1658
+ }
1659
+ function systemdQuote(part) {
1660
+ return /[\s"]/.test(part) ? `"${part.replace(/"/g, '\\"')}"` : part;
1661
+ }
1662
+ function renderSystemdUnit(argv) {
1663
+ const exec2 = argv.map(systemdQuote).join(" ");
1664
+ return `[Unit]
1665
+ Description=Astra local AI gateway server
1666
+ After=network.target
1667
+
1668
+ [Service]
1669
+ Type=simple
1670
+ ExecStart=${exec2}
1671
+ Restart=on-failure
1672
+ RestartSec=3
1673
+
1674
+ [Install]
1675
+ WantedBy=default.target
1676
+ `;
1677
+ }
1678
+ function quoteForTr(argv) {
1679
+ const inner = argv.map((a) => /[\s"]/.test(a) ? `"${a}"` : a).join(" ").replace(/"/g, '\\"');
1680
+ return `"${inner}"`;
1681
+ }
1682
+ function schtasksCreateArgs(argv) {
1683
+ return ["/Create", "/F", "/SC", "ONLOGON", "/TN", SCHTASKS_TASK_NAME, "/TR", quoteForTr(argv)];
1684
+ }
1685
+ function schtasksDeleteArgs() {
1686
+ return ["/Delete", "/F", "/TN", SCHTASKS_TASK_NAME];
1687
+ }
1688
+ function schtasksQueryArgs() {
1689
+ return ["/Query", "/TN", SCHTASKS_TASK_NAME];
1690
+ }
1691
+ function autostartArgv(command) {
1692
+ return [command.command, ...command.args, "serve", "--started-by", "autostart"];
1693
+ }
1694
+ async function exec(args) {
1695
+ const [cmd, ...rest] = args;
1696
+ if (!cmd) throw new AstraError("Missing command.");
1697
+ return runCapture3(cmd, rest, { timeoutMs: 3e4 });
1698
+ }
1699
+ function logFileFor(lgHome) {
1700
+ return path12.join(homePaths(lgHome).logsDir, "server-stdout.log");
1701
+ }
1702
+ async function enableAutostartMac(argv, home = os2.homedir(), lgHome = homePaths(home).home) {
1703
+ const plist = launchdPlistPath(home);
1704
+ fs12.mkdirSync(path12.dirname(plist), { recursive: true });
1705
+ fs12.writeFileSync(plist, renderLaunchdPlist(argv, logFileFor(lgHome)));
1706
+ await exec(["launchctl", "unload", plist]).catch(() => void 0);
1707
+ const r = await exec(["launchctl", "load", plist]);
1708
+ if (r.code !== 0) {
1709
+ throw new AstraError("launchctl load failed.", r.stderr.trim() || r.stdout.trim() || void 0);
1710
+ }
1711
+ }
1712
+ async function disableAutostartMac(home = os2.homedir()) {
1713
+ const plist = launchdPlistPath(home);
1714
+ await exec(["launchctl", "unload", plist]).catch(() => void 0);
1715
+ fs12.rmSync(plist, { force: true });
1716
+ }
1717
+ async function autostartStatusMac(home = os2.homedir()) {
1718
+ return fs12.existsSync(launchdPlistPath(home)) ? "enabled" : "disabled";
1719
+ }
1720
+ async function enableAutostartLinux(argv, home = os2.homedir()) {
1721
+ const unit = systemdUnitPath(home);
1722
+ fs12.mkdirSync(path12.dirname(unit), { recursive: true });
1723
+ fs12.writeFileSync(unit, renderSystemdUnit(argv));
1724
+ const reload = await exec(["systemctl", "--user", "daemon-reload"]);
1725
+ if (reload.code !== 0) {
1726
+ throw new AstraError(
1727
+ "systemctl --user daemon-reload failed. Is systemd available (a WSL setup without systemd is not supported)?",
1728
+ reload.stderr.trim() || void 0
1729
+ );
1730
+ }
1731
+ const enable = await exec(["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT_NAME]);
1732
+ if (enable.code !== 0) {
1733
+ throw new AstraError(
1734
+ `systemctl --user enable --now ${SYSTEMD_UNIT_NAME} failed.`,
1735
+ enable.stderr.trim() || void 0
1736
+ );
1737
+ }
1738
+ }
1739
+ async function disableAutostartLinux(home = os2.homedir()) {
1740
+ await exec(["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT_NAME]).catch(() => void 0);
1741
+ fs12.rmSync(systemdUnitPath(home), { force: true });
1742
+ await exec(["systemctl", "--user", "daemon-reload"]).catch(() => void 0);
1743
+ }
1744
+ async function autostartStatusLinux(home = os2.homedir()) {
1745
+ if (!fs12.existsSync(systemdUnitPath(home))) return "disabled";
1746
+ const r = await exec(["systemctl", "--user", "is-enabled", SYSTEMD_UNIT_NAME]).catch(() => null);
1747
+ return r !== null && r.code === 0 && r.stdout.trim() === "enabled" ? "enabled" : "disabled";
1748
+ }
1749
+ async function enableAutostartWindows(argv) {
1750
+ const r = await exec(["schtasks", ...schtasksCreateArgs(argv)]);
1751
+ if (r.code !== 0) {
1752
+ throw new AstraError("schtasks /Create failed.", r.stderr.trim() || void 0);
1753
+ }
1754
+ }
1755
+ async function disableAutostartWindows() {
1756
+ const r = await exec(["schtasks", ...schtasksDeleteArgs()]);
1757
+ if (r.code !== 0 && !/does not exist|no existe/i.test(r.stderr + r.stdout)) {
1758
+ throw new AstraError("schtasks /Delete failed.", r.stderr.trim() || void 0);
1759
+ }
1760
+ }
1761
+ async function autostartStatusWindows() {
1762
+ const r = await exec(["schtasks", ...schtasksQueryArgs()]);
1763
+ return r.code === 0 ? "enabled" : "disabled";
1764
+ }
1765
+
1766
+ // src/commands/autostart.ts
1767
+ async function dispatch(mac, linux, win) {
1768
+ switch (process4.platform) {
1769
+ case "darwin":
1770
+ return mac();
1771
+ case "win32":
1772
+ return win();
1773
+ case "linux":
1774
+ return linux();
1775
+ default:
1776
+ throw new AstraError(`Autostart is not supported on ${process4.platform}.`);
1777
+ }
1778
+ }
1779
+ async function runAutostart(action) {
1780
+ const home = astraHome();
1781
+ if (action === "status") {
1782
+ const s = await dispatch(
1783
+ () => autostartStatusMac(os3.homedir()),
1784
+ () => autostartStatusLinux(os3.homedir()),
1785
+ () => autostartStatusWindows()
1786
+ );
1787
+ console.log(`Autostart is ${s === "enabled" ? style.green("enabled") : "disabled"}.`);
1788
+ return;
1789
+ }
1790
+ if (action === "enable") {
1791
+ const command = resolveServerBinary();
1792
+ const argv = autostartArgv(command);
1793
+ await dispatch(
1794
+ () => enableAutostartMac(argv, os3.homedir(), home),
1795
+ () => enableAutostartLinux(argv, os3.homedir()),
1796
+ () => enableAutostartWindows(argv)
1797
+ );
1798
+ success("Astra will start automatically at login.");
1799
+ } else {
1800
+ await dispatch(
1801
+ () => disableAutostartMac(os3.homedir()),
1802
+ () => disableAutostartLinux(os3.homedir()),
1803
+ () => disableAutostartWindows()
1804
+ );
1805
+ success("Autostart disabled.");
1806
+ }
1807
+ }
1808
+
1809
+ // src/commands/desktop.ts
1810
+ import fs14 from "fs";
1811
+
1812
+ // src/lib/desktop.ts
1813
+ import { spawn as spawn6 } from "child_process";
1814
+ function usesShell2(command) {
1815
+ return process.platform === "win32" && command.endsWith(".cmd");
1816
+ }
1817
+ async function npmInstallGlobal() {
1818
+ const npm = locateNpm();
1819
+ const full = [...npm.args, "install", "-g", `${PACKAGE_NAME}@latest`, "--no-audit", "--no-fund", "--loglevel", "error"];
1820
+ return new Promise((resolve, reject) => {
1821
+ const child = spawn6(npm.command, full, {
1822
+ stdio: "inherit",
1823
+ windowsHide: true,
1824
+ shell: usesShell2(npm.command)
1825
+ });
1826
+ child.on("error", reject);
1827
+ child.on("close", (code) => resolve({ code, stderr: "" }));
1828
+ });
1829
+ }
1830
+
1831
+ // src/lib/desktop-server.ts
1832
+ import fs13 from "fs";
1833
+ function isEphemeralPackagePath(p) {
1834
+ return /[\\/](_npx|dlx)[\\/]/.test(p);
1835
+ }
1836
+ function hasUsableServer(install, cliVersion, exists = (p) => fs13.existsSync(p)) {
1837
+ const current = install?.serverPath?.trim();
1838
+ if (!current || !exists(current) || isEphemeralPackagePath(current)) return false;
1839
+ return !(install?.serverVersion && isNewerVersion(cliVersion, install.serverVersion));
1840
+ }
1841
+ function ensureDesktopServer(o) {
1842
+ const install = readInstallJson(o.home);
1843
+ const current = install?.serverPath;
1844
+ if (current && hasUsableServer(install, o.version)) return { action: "kept", serverPath: current };
1845
+ const packaged = (o.findPackaged ?? (() => findPackagedServerBinary()))();
1846
+ if (!("path" in packaged)) return { action: "missing", reason: packaged.error };
1847
+ let serverPath = packaged.path;
1848
+ let action = "recorded";
1849
+ if (isEphemeralPackagePath(packaged.path)) {
1850
+ const { serverDir } = updatePaths(o.home);
1851
+ serverPath = installManagedBinary({
1852
+ stagedPath: packaged.path,
1853
+ serverDir,
1854
+ version: o.version,
1855
+ platform: o.platform
1856
+ });
1857
+ if (installManagedWebRoot(packaged.path, serverDir)) commitManagedWebRoot(serverDir);
1858
+ pruneManagedBinaries(serverDir, serverPath);
1859
+ action = "copied";
1860
+ }
1861
+ writeInstallJson(
1862
+ o.home,
1863
+ applyServerInfo2(readInstallJson(o.home), { serverPath, serverVersion: o.version })
1864
+ );
1865
+ return { action, serverPath };
1866
+ }
1867
+
1868
+ // src/lib/prompt.ts
1869
+ import readline from "readline";
1870
+ import { Writable } from "stream";
1871
+ function promptHidden(question) {
1872
+ return new Promise((resolve, reject) => {
1873
+ const stdin = process.stdin;
1874
+ if (!stdin.isTTY) {
1875
+ let data = "";
1876
+ stdin.setEncoding("utf8");
1877
+ stdin.on("data", (chunk) => {
1878
+ data += chunk;
1879
+ });
1880
+ stdin.on("end", () => resolve(data.replace(/\r?\n$/, "")));
1881
+ stdin.on("error", reject);
1882
+ return;
1883
+ }
1884
+ process.stdout.write(question);
1885
+ const muted = new Writable({
1886
+ write(_chunk, _encoding, callback) {
1887
+ callback();
1888
+ }
1889
+ });
1890
+ const rl = readline.createInterface({ input: stdin, output: muted, terminal: true });
1891
+ rl.question("", (answer) => {
1892
+ rl.close();
1893
+ process.stdout.write("\n");
1894
+ resolve(answer);
1895
+ });
1896
+ });
1897
+ }
1898
+ function confirmPrompt(question) {
1899
+ return new Promise((resolve) => {
1900
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1901
+ rl.question(question, (answer) => {
1902
+ rl.close();
1903
+ resolve(/^\s*(y|yes)/i.test(answer.trim()));
1904
+ });
1905
+ });
1906
+ }
1907
+
1908
+ // src/commands/desktop.ts
1909
+ async function runInstall(opts) {
1910
+ if (!opts.client) {
1911
+ console.log("The Astra server is installed automatically with the npm package.");
1912
+ console.log(`To install the desktop app, run ${style.cyan("astra install --desktop")}.`);
1913
+ return;
1914
+ }
1915
+ const home = astraHome();
1916
+ await installDesktopAt(home, VERSION);
1917
+ const server = ensureDesktopServer({ home, version: VERSION, platform: platformInfo().platform });
1918
+ if (server.action === "missing") {
1919
+ warn(
1920
+ `The desktop app cannot find an Astra server: ${server.reason} Install the CLI (\`npm i -g ${PACKAGE_NAME}\`) and run \`astra start\` once, or set ASTRA_SERVER_BIN.`
1921
+ );
1922
+ } else if (server.action === "copied") {
1923
+ success(`Astra server ${VERSION} installed to ${server.serverPath}.`);
1924
+ }
1925
+ info("Launch it from your applications, or run `astra open` for the web UI.");
1926
+ }
1927
+ async function runUninstallClient() {
1928
+ const home = astraHome();
1929
+ const install = readInstallJson(home);
1930
+ if (!install?.desktopPath && !fs14.existsSync(homePaths(home).desktopPrefix)) {
1931
+ console.log("The desktop app is not installed.");
1932
+ return;
1933
+ }
1934
+ removeLauncher(platformInfo().platform, home);
1935
+ fs14.rmSync(homePaths(home).desktopPrefix, { recursive: true, force: true });
1936
+ writeInstallJson(home, clearDesktopInfo(readInstallJson(home)));
1937
+ success("Astra desktop app uninstalled.");
1938
+ }
1939
+ async function runUpdate(opts) {
1940
+ const home = astraHome();
1941
+ if (opts.check) {
1942
+ await reportCheck();
1943
+ return;
1944
+ }
1945
+ let manifest;
1946
+ try {
1947
+ manifest = await fetchManifest({ userAgent: `Astra-CLI/${VERSION}` });
1948
+ } catch (err) {
1949
+ if (!opts.client) throw err;
1950
+ manifest = { version: VERSION };
1951
+ }
1952
+ if (!opts.client) {
1953
+ await updateServer(home, manifest, opts.retry ?? false);
1954
+ if (isNewerVersion(manifest.version, VERSION)) {
1955
+ console.log(`Updating the Astra CLI (npm install -g ${PACKAGE_NAME}@latest)\u2026`);
1956
+ const r = await npmInstallGlobal();
1957
+ if (r.code !== 0) {
1958
+ info(`npm install -g ${PACKAGE_NAME}@latest failed \u2014 the server is updated; run \`npm i -g ${PACKAGE_NAME}\` for the CLI.`);
1959
+ } else {
1960
+ success("The new CLI version is used the next time you run `astra`.");
1961
+ }
1962
+ }
1963
+ }
1964
+ const install = readInstallJson(home);
1965
+ const wantsDesktop = opts.client || !!install?.desktopPath || fs14.existsSync(homePaths(home).desktopPrefix);
1966
+ if (wantsDesktop) {
1967
+ const target = isNewerVersion(manifest.version, VERSION) ? manifest.version : VERSION;
1968
+ await installDesktopAt(home, target);
1969
+ }
1970
+ }
1971
+ async function reportCheck() {
1972
+ const status = await serverStatus();
1973
+ const r = await checkForServerUpdate({
1974
+ currentVersion: VERSION,
1975
+ baseUrl: status.running ? status.base ?? void 0 : void 0
1976
+ });
1977
+ if (r.availableVersion) {
1978
+ console.log(`Update available: ${style.cyan(r.availableVersion)} (current ${VERSION}).`);
1979
+ if (r.notes) console.log(style.dim(r.notes));
1980
+ console.log(`Run ${style.cyan("astra update")} to install.`);
1981
+ process.exitCode = 1;
1982
+ } else {
1983
+ success(`Astra is up to date (${VERSION}).`);
1984
+ }
1985
+ }
1986
+ async function installDesktopAt(home, version) {
1987
+ const plat = platformInfo();
1988
+ console.log(`Installing the Astra desktop app ${version} (this may take a minute)\u2026`);
1989
+ const entry = await installDesktopClient({
1990
+ home,
1991
+ version,
1992
+ desktopPackage: plat.desktopPackage,
1993
+ platform: plat.platform
1994
+ });
1995
+ writeInstallJson(
1996
+ home,
1997
+ applyDesktopInfo(readInstallJson(home), {
1998
+ desktopPath: entry.path,
1999
+ desktopVersion: entry.version
2000
+ })
2001
+ );
2002
+ success(`Astra desktop app installed (${entry.version}).`);
2003
+ }
2004
+ async function updateServer(home, manifest, force) {
2005
+ if (!isNewerVersion(manifest.version, VERSION)) {
2006
+ console.log(`Astra server is up to date (${VERSION}).`);
2007
+ return;
2008
+ }
2009
+ const plat = platformInfo();
2010
+ const status = await serverStatus();
2011
+ const ok = await confirmPrompt(
2012
+ `Update the Astra server ${VERSION} \u2192 ${manifest.version}? The gateway restarts and in-flight requests are interrupted. [y/N] `
2013
+ );
2014
+ if (!ok) {
2015
+ console.log("Update cancelled.");
2016
+ return;
2017
+ }
2018
+ const control = {
2019
+ isRunning: async () => await probeRunning(home) !== null,
2020
+ stop: async () => {
2021
+ await stopServer();
2022
+ },
2023
+ start: async () => {
2024
+ await startServer({});
2025
+ },
2026
+ probeVersion: async () => {
2027
+ const rt = currentRuntime(home);
2028
+ if (!rt) return null;
2029
+ try {
2030
+ const v = await apiRequest({
2031
+ base: baseUrlFor(rt.host, rt.port),
2032
+ path: "/api/version"
2033
+ });
2034
+ return v.version ?? null;
2035
+ } catch {
2036
+ return null;
2037
+ }
2038
+ }
2039
+ };
2040
+ await applyServerUpdate({
2041
+ home,
2042
+ manifest,
2043
+ platformKey: `${plat.platform}-${plat.arch}`,
2044
+ serverPackage: plat.serverPackage,
2045
+ platform: plat.platform,
2046
+ currentServerPath: readInstallJson(home)?.serverPath ?? null,
2047
+ currentVersion: VERSION,
2048
+ // The running server's API major is the compatibility anchor (fallback 1).
2049
+ expectedApiMajor: majorOf(status.runtime?.apiVersion) ?? 1,
2050
+ control,
2051
+ force,
2052
+ log: (line) => console.log(` ${line}`)
2053
+ });
2054
+ success(`Astra server updated to ${manifest.version}.`);
2055
+ }
2056
+
2057
+ // src/commands/uninstall.ts
2058
+ import fs15 from "fs";
2059
+ import { spawn as spawn7 } from "child_process";
2060
+ async function restoreAllOffline(purge) {
2061
+ const command = resolveServerBinary();
2062
+ const child = spawn7(
2063
+ command.command,
2064
+ [...command.args, "restore-all", ...purge ? ["--purge"] : []],
2065
+ { stdio: "inherit", windowsHide: true }
2066
+ );
2067
+ const code = await exitCodeOf(child);
2068
+ if (code !== 0) throw new AstraError(`restore-all exited with code ${code}.`);
2069
+ }
2070
+ async function runUninstall(opts) {
2071
+ const home = astraHome();
2072
+ info("Uninstalling Astra\u2026");
2073
+ info("\u2022 Restoring client configurations\u2026");
2074
+ try {
2075
+ await restoreAllOffline(opts.purge === true);
2076
+ } catch (err) {
2077
+ warn(`restore-all failed: ${err.message}`);
2078
+ }
2079
+ info("\u2022 Disabling autostart\u2026");
2080
+ try {
2081
+ await runAutostart("disable");
2082
+ } catch (err) {
2083
+ warn(`Could not disable autostart: ${err.message}`);
2084
+ }
2085
+ info("\u2022 Removing the desktop app\u2026");
2086
+ try {
2087
+ await runUninstallClient();
2088
+ } catch (err) {
2089
+ warn(`Could not remove the desktop app: ${err.message}`);
2090
+ }
2091
+ info("\u2022 Stopping the server\u2026");
2092
+ try {
2093
+ await stopServer();
2094
+ } catch (err) {
2095
+ warn(`Could not stop the server: ${err.message}`);
2096
+ }
2097
+ if (opts.purge) {
2098
+ if (process.stdin.isTTY) {
2099
+ const ok = await confirmPrompt(`Delete all Astra data in ${home}? [y/N] `);
2100
+ if (!ok) {
2101
+ console.log("Skipped data deletion. Run `astra uninstall --purge` again to delete it.");
2102
+ return;
2103
+ }
2104
+ }
2105
+ fs15.rmSync(home, { recursive: true, force: true });
2106
+ success(`Deleted ${home}.`);
2107
+ }
2108
+ success(`Astra uninstalled. Remove the npm package with \`npm uninstall -g ${PACKAGE_NAME}\`.`);
2109
+ }
2110
+
2111
+ // src/commands/admin.ts
2112
+ async function api(path13, options = {}) {
2113
+ const { base, runtime, started } = await ensureServerRunning();
2114
+ if (started) console.log("Server was not running \u2014 started it in the background.");
2115
+ return apiRequest({
2116
+ base,
2117
+ path: path13,
2118
+ method: options.method ?? "GET",
2119
+ body: options.body,
2120
+ runtimeToken: runtime?.runtimeToken
2121
+ });
2122
+ }
2123
+ function fmtBool(v) {
2124
+ if (v === void 0) return "";
2125
+ return v ? "yes" : "no";
2126
+ }
2127
+ function findProvider(providers, idOrName) {
2128
+ const needle = idOrName.toLowerCase();
2129
+ const byId = providers.find((p) => p.id.toLowerCase() === needle);
2130
+ if (byId) return byId;
2131
+ const byName = providers.filter((p) => p.name?.toLowerCase() === needle);
2132
+ if (byName.length === 1) return byName[0];
2133
+ if (byName.length > 1) {
2134
+ throw new AstraError(
2135
+ `Provider name "${idOrName}" is ambiguous.`,
2136
+ `Matching ids: ${byName.map((p) => p.id).join(", ")} \u2014 use an id instead.`
2137
+ );
2138
+ }
2139
+ return void 0;
2140
+ }
2141
+ async function runClientList() {
2142
+ const rows = await api("/api/clients");
2143
+ printTable(
2144
+ rows.map((r) => [
2145
+ r.kind,
2146
+ r.name ?? "",
2147
+ fmtBool(r.enabled),
2148
+ r.status ?? "",
2149
+ r.providerId ?? "",
2150
+ r.selectedModel ?? "",
2151
+ r.availability ?? ""
2152
+ ]),
2153
+ ["KIND", "NAME", "ENABLED", "STATUS", "PROVIDER", "MODEL", "AVAILABILITY"]
2154
+ );
2155
+ }
2156
+ async function runClientStatus(kind) {
2157
+ const rows = await api("/api/clients");
2158
+ const row = rows.find((r) => r.kind === kind);
2159
+ if (!row) throw new AstraError(`Unknown client kind: ${kind}.`);
2160
+ console.log(`Client: ${row.name ?? row.kind} (${row.kind})`);
2161
+ console.log(` Enabled ${fmtBool(row.enabled)}`);
2162
+ if (row.status) console.log(` Status ${row.status}`);
2163
+ if (row.availability) console.log(` Availability ${row.availability}`);
2164
+ if (row.providerId) console.log(` Provider ${row.providerId}`);
2165
+ if (row.selectedModel) console.log(` Model ${row.selectedModel}`);
2166
+ }
2167
+ async function runClientEnable(kind, opts) {
2168
+ const providers = await api("/api/providers");
2169
+ const provider2 = findProvider(providers, opts.provider);
2170
+ if (!provider2) {
2171
+ throw new AstraError(
2172
+ `No provider matches "${opts.provider}".`,
2173
+ "Run `astra provider list` to see available providers."
2174
+ );
2175
+ }
2176
+ await api(`/api/clients/${kind}/enable`, {
2177
+ method: "POST",
2178
+ body: { providerId: provider2.id, ...opts.model ? { model: opts.model } : {} }
2179
+ });
2180
+ success(
2181
+ `Client "${kind}" enabled with provider "${provider2.name ?? provider2.id}"${opts.model ? ` (model: ${opts.model})` : ""}.`
2182
+ );
2183
+ console.log("Restart the client (Codex, Claude Desktop, \u2026) if it does not pick up the change.");
2184
+ }
2185
+ async function runClientDisable(kind) {
2186
+ await api(`/api/clients/${kind}/disable`, { method: "POST", body: {} });
2187
+ success(`Client "${kind}" disabled. Its original configuration was restored.`);
2188
+ }
2189
+ async function runProviderList() {
2190
+ const rows = await api("/api/providers");
2191
+ printTable(
2192
+ rows.map((r) => [
2193
+ r.id,
2194
+ r.name ?? "",
2195
+ r.templateId ?? "",
2196
+ fmtBool(r.enabled),
2197
+ r.modelCount === void 0 ? "" : String(r.modelCount)
2198
+ ]),
2199
+ ["ID", "NAME", "TEMPLATE", "ENABLED", "MODELS"]
2200
+ );
2201
+ }
2202
+ async function runProviderAdd(opts) {
2203
+ const templates = await api("/api/provider-templates");
2204
+ const needle = opts.template.toLowerCase();
2205
+ const template = templates.find(
2206
+ (t) => t.id.toLowerCase() === needle || t.name?.toLowerCase() === needle
2207
+ );
2208
+ if (!template) {
2209
+ throw new AstraError(
2210
+ `Unknown provider template "${opts.template}".`,
2211
+ `Available templates: ${templates.map((t) => t.id).join(", ") || "(none reported)"}`
2212
+ );
2213
+ }
2214
+ const variants = template.variants ?? [];
2215
+ let variantId = opts.variant;
2216
+ if (variants.length > 0 && !variantId) {
2217
+ throw new AstraError(
2218
+ `Template "${template.id}" has variants \u2014 pass --variant.`,
2219
+ `Variants: ${variants.map((v) => v.id).join(", ")}`
2220
+ );
2221
+ }
2222
+ if (variantId && variants.length > 0 && !variants.some((v) => v.id === variantId)) {
2223
+ throw new AstraError(
2224
+ `Unknown variant "${variantId}" for template "${template.id}".`,
2225
+ `Variants: ${variants.map((v) => v.id).join(", ")}`
2226
+ );
2227
+ }
2228
+ if (variants.length === 0) variantId = void 0;
2229
+ const created = await api("/api/providers", {
2230
+ method: "POST",
2231
+ body: {
2232
+ templateId: template.id,
2233
+ ...variantId ? { variantId } : {},
2234
+ ...opts.name ? { name: opts.name } : {},
2235
+ apiKey: opts.key
2236
+ }
2237
+ });
2238
+ success(`Provider added: ${created.name ?? created.id} (id: ${created.id}).`);
2239
+ }
2240
+ async function runProviderRemove(id) {
2241
+ try {
2242
+ await api(`/api/providers/${encodeURIComponent(id)}`, { method: "DELETE" });
2243
+ } catch (err) {
2244
+ if (err instanceof ApiError && err.status === 409) {
2245
+ throw new AstraError(
2246
+ `Provider "${id}" is still bound to one or more clients.`,
2247
+ "Disable the client or bind it to another provider first (`astra client disable <kind>`)."
2248
+ );
2249
+ }
2250
+ throw err;
2251
+ }
2252
+ success(`Provider "${id}" removed.`);
2253
+ }
2254
+
2255
+ // src/commands/config.ts
2256
+ import { spawn as spawn8 } from "child_process";
2257
+ async function runSetPassword() {
2258
+ const password = await promptHidden("Enter new admin password: ");
2259
+ if (!password) throw new AstraError("Password must not be empty.");
2260
+ const confirm = await promptHidden("Repeat password: ");
2261
+ if (confirm !== password) throw new AstraError("Passwords do not match.");
2262
+ const command = resolveServerBinary();
2263
+ const child = spawn8(command.command, [...command.args, "set-password"], {
2264
+ stdio: ["pipe", "inherit", "inherit"],
2265
+ windowsHide: true
2266
+ });
2267
+ child.stdin?.write(`${password}
2268
+ `);
2269
+ child.stdin?.end();
2270
+ const code = await exitCodeOf(child);
2271
+ if (code !== 0) {
2272
+ throw new AstraError(`set-password failed with exit code ${code}.`);
2273
+ }
2274
+ success("Admin password updated.");
2275
+ const status = await serverStatus();
2276
+ if (status.running) {
2277
+ info("Restart the server for the new password to take effect: `astra restart`.");
2278
+ }
2279
+ }
2280
+
2281
+ // src/commands/restore-all.ts
2282
+ import { spawn as spawn9 } from "child_process";
2283
+ async function runRestoreAll(opts) {
2284
+ const command = resolveServerBinary();
2285
+ const args = [...command.args, "restore-all", ...opts.purge ? ["--purge"] : []];
2286
+ const child = spawn9(command.command, args, { stdio: "inherit", windowsHide: true });
2287
+ const code = await exitCodeOf(child);
2288
+ if (code !== 0) {
2289
+ throw new AstraError(`restore-all failed with exit code ${code}.`);
2290
+ }
2291
+ success(
2292
+ opts.purge ? "All client configurations were restored and Astra entries were removed from them." : "All client configurations were restored."
2293
+ );
2294
+ }
2295
+
2296
+ // src/index.ts
2297
+ function addStartOptions(cmd) {
2298
+ return cmd.option("--port <port>", "TCP port to listen on (default 17321)").option("--host <host>", "Address to bind (default 127.0.0.1)").option("--foreground", "Run the server in the foreground instead of detached").option("--open", "Open the web UI in the default browser");
2299
+ }
2300
+ var program = new Command();
2301
+ program.name("astra").description("Astra \u2014 a local AI gateway. Manage the astra-server from the terminal.").version(VERSION, "-v, --version", "Print the CLI version");
2302
+ addStartOptions(program).action((opts) => runStart(opts));
2303
+ addStartOptions(
2304
+ program.command("start").description("Start the Astra server (background by default)")
2305
+ ).action((opts) => runStart(opts));
2306
+ program.command("stop").description("Stop the running Astra server").action(() => runStop());
2307
+ addStartOptions(program.command("restart").description("Restart the Astra server")).action(
2308
+ (opts) => runRestart(opts)
2309
+ );
2310
+ program.command("status").description("Show whether the server is running and its details").action(() => runStatus());
2311
+ program.command("logs").description("Show recent server logs").option("-f, --follow", "Keep printing new log output").action((opts) => runLogs(opts));
2312
+ program.command("open").description("Open the Astra web UI in the default browser").action(() => runOpen());
2313
+ var autostart = program.command("autostart").description("Manage starting Astra at login");
2314
+ autostart.command("enable").description("Enable autostart (LaunchAgent / systemd --user / schtasks)").action(() => runAutostart("enable"));
2315
+ autostart.command("disable").description("Disable autostart").action(() => runAutostart("disable"));
2316
+ autostart.command("status").description("Show whether autostart is enabled").action(() => runAutostart("status"));
2317
+ program.command("install").description("Install optional Astra components").option("--desktop", "Install the Astra desktop app for this platform").option("--client", "Alias of --desktop").action(
2318
+ (opts) => runInstall({ client: opts.desktop || opts.client })
2319
+ );
2320
+ program.command("uninstall").description("Uninstall Astra (restore client configs, stop the server)").option("--desktop", "Only uninstall the desktop app").option("--client", "Alias of --desktop").option("--purge", "Also delete the Astra data directory (~/.astra)").action((opts) => {
2321
+ if (opts.desktop || opts.client) return runUninstallClient();
2322
+ return runUninstall({ purge: opts.purge });
2323
+ });
2324
+ program.command("update").description("Update astra (and the desktop app when installed), then restart the server").option("--desktop", "Only update the desktop app").option("--client", "Alias of --desktop").option("--check", "Only check for updates, do not install").option("--retry", "Retry even after repeated update failures").action(
2325
+ (opts) => runUpdate({ client: opts.desktop || opts.client, check: opts.check, retry: opts.retry })
2326
+ );
2327
+ var client = program.command("client").description("Manage AI clients (Codex, Claude Code, \u2026)");
2328
+ client.command("list").description("List clients and their status").action(() => runClientList());
2329
+ client.command("status").description("Show one client in detail").addArgument(new Argument("<kind>", "Client kind").choices([...CLIENT_KINDS])).action((kind) => runClientStatus(kind));
2330
+ client.command("enable").description("Enable a client and point it at Astra").addArgument(new Argument("<kind>", "Client kind").choices([...CLIENT_KINDS])).requiredOption("--provider <id-or-name>", "Provider id or name to bind").option("--model <model>", "Model to write into the client configuration").action(
2331
+ (kind, opts) => runClientEnable(kind, opts)
2332
+ );
2333
+ client.command("disable").description("Disable a client and restore its original configuration").addArgument(new Argument("<kind>", "Client kind").choices([...CLIENT_KINDS])).action((kind) => runClientDisable(kind));
2334
+ var provider = program.command("provider").description("Manage providers (upstream AI accounts)");
2335
+ provider.command("list").description("List providers").action(() => runProviderList());
2336
+ provider.command("add").description("Add a provider from a template").requiredOption("--template <id>", "Provider template id (see `astra provider templates`)").option("--variant <id>", "Template variant, when the template has variants").requiredOption("--key <apiKey>", "API key for the provider").option("--name <name>", "Display name for the provider").action(
2337
+ (opts) => runProviderAdd(opts)
2338
+ );
2339
+ provider.command("remove").description("Remove a provider").argument("<id>", "Provider id").action((id) => runProviderRemove(id));
2340
+ program.command("config").description("Manage Astra configuration").command("set-password").description("Set the admin password (required before binding to a non-loopback address)").action(() => runSetPassword());
2341
+ program.command("restore-all").description("Restore every managed client configuration offline (no server needed)").option("--purge", "Also remove Astra entries from client configs entirely").action((opts) => runRestoreAll(opts));
2342
+ program.parseAsync(process5.argv).then(() => {
2343
+ }).catch((err) => {
2344
+ if (err instanceof ApiError) {
2345
+ fail(`error: ${err.message} (HTTP ${err.status})`);
2346
+ process5.exitCode = 1;
2347
+ return;
2348
+ }
2349
+ if (err instanceof AstraError) {
2350
+ fail(`error: ${err.message}`);
2351
+ if (err.hint) console.error(` ${err.hint}`);
2352
+ process5.exitCode = 1;
2353
+ return;
2354
+ }
2355
+ fail(`error: ${err instanceof Error ? err.message : String(err)}`);
2356
+ if (process5.env.ASTRA_DEBUG) console.error(err);
2357
+ else console.error("Run with ASTRA_DEBUG=1 for a stack trace.");
2358
+ process5.exitCode = 1;
2359
+ });
2360
+ //# sourceMappingURL=index.js.map