@rnbsolucoes/axion-code 0.1.69 → 0.1.70

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/npm/bin/axion.mjs CHANGED
@@ -1,478 +1,478 @@
1
- #!/usr/bin/env node
2
- import { spawnSync } from "node:child_process";
3
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
- import https from "node:https";
5
- import { dirname, join, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- const here = dirname(fileURLToPath(import.meta.url));
9
- const root = resolve(here, "..", "..");
10
- const isWindows = process.platform === "win32";
11
- const exeName = isWindows ? "axion-code.exe" : "axion-code";
12
- const platformKey = `${process.platform}-${process.arch}`;
13
- const packagedExePath = join(root, "npm", "releases", platformKey, exeName);
14
- const binDir = isWindows
15
- ? join(process.env.LOCALAPPDATA || join(process.env.USERPROFILE || root, "AppData", "Local"), "AxionCode", "bin")
16
- : join(process.env.HOME || root, ".local", "share", "axion-code", "bin");
17
- const exePath = join(binDir, exeName);
18
- const markerPath = join(binDir, ".axion-code-package-version");
19
- const updateCachePath = join(binDir, ".axion-code-update.json");
20
- const packageJSON = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
21
- const updateSource = packageJSON.name;
22
- const githubSource = "github:rnbsolucoes/Axion-CLI";
23
- const updatePackageURL = "https://raw.githubusercontent.com/rnbsolucoes/Axion-CLI/main/package.json";
24
- const npmRegistryURL = "https://registry.npmjs.org/@rnbsolucoes%2Faxion-code/latest";
25
- const updateCommand = "axion update";
26
- const updateCacheTTL = 12 * 60 * 60 * 1000;
27
-
28
- function findGo() {
29
- if (process.env.AXION_GO) {
30
- return process.env.AXION_GO;
31
- }
32
- if (isWindows && process.env.USERPROFILE) {
33
- const bundled = join(process.env.USERPROFILE, ".codex", "toolchains", "go1.26.4", "go", "bin", "go.exe");
34
- if (existsSync(bundled)) {
35
- return bundled;
36
- }
37
- }
38
- return "go";
39
- }
40
-
41
- function sourceAvailable() {
42
- return existsSync(join(root, "go.mod")) && existsSync(join(root, "cmd", "axion-code"));
43
- }
44
-
45
- function sleep(ms) {
46
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
47
- }
48
-
49
- function markerMatches(expectedMarker) {
50
- try {
51
- return existsSync(exePath) && readFileSync(markerPath, "utf8").trim() === expectedMarker;
52
- } catch {
53
- return false;
54
- }
55
- }
56
-
57
- function copyPackagedBinaryWithRetry(expectedMarker) {
58
- for (let attempt = 0; attempt < 8; attempt += 1) {
59
- try {
60
- copyFileSync(packagedExePath, exePath);
61
- return;
62
- } catch (error) {
63
- const retryable = error && (error.code === "EBUSY" || error.code === "EPERM");
64
- if (!retryable) {
65
- throw error;
66
- }
67
- if (markerMatches(expectedMarker)) {
68
- return;
69
- }
70
- if (attempt === 7) {
71
- throw error;
72
- }
73
- sleep(75 * (attempt + 1));
74
- }
75
- }
76
- }
77
-
78
- function installPackagedBinary() {
79
- if (!existsSync(packagedExePath)) {
80
- return false;
81
- }
82
- const marker = packagedBinaryMarker();
83
- copyPackagedBinaryWithRetry(marker);
84
- if (!isWindows) {
85
- chmodSync(exePath, 0o755);
86
- }
87
- writeFileSync(markerPath, `${marker}\n`, "utf8");
88
- return true;
89
- }
90
-
91
- function buildFromSource() {
92
- const go = findGo();
93
- const result = spawnSync(go, ["build", "-o", exePath, "./cmd/axion-code"], {
94
- cwd: root,
95
- stdio: "inherit",
96
- shell: false
97
- });
98
- if (result.status !== 0) {
99
- return false;
100
- }
101
- writeFileSync(markerPath, `${sourceBinaryMarker()}\n`, "utf8");
102
- return true;
103
- }
104
-
105
- function ensureBinary() {
106
- const marker = existsSync(markerPath) ? readFileSync(markerPath, "utf8").trim() : "";
107
- const desiredMarker = existsSync(packagedExePath) ? packagedBinaryMarker() : sourceBinaryMarker();
108
- if (existsSync(exePath) && marker === desiredMarker) {
109
- return;
110
- }
111
- mkdirSync(binDir, { recursive: true });
112
- if (installPackagedBinary()) {
113
- return;
114
- }
115
- if (sourceAvailable() && buildFromSource()) {
116
- return;
117
- }
118
- console.error("");
119
- console.error(`Axion Code binary for ${platformKey} was not found in this package.`);
120
- console.error("If you are using a source clone, install Go 1.26+ on PATH or set AXION_GO and run:");
121
- console.error(" npm run install:local");
122
- console.error("If you installed from npm, update to a package that includes your platform binary.");
123
- process.exit(1);
124
- }
125
-
126
- function packagedBinaryMarker() {
127
- try {
128
- const stat = statSync(packagedExePath);
129
- return `${packageJSON.version}|${platformKey}|pkg|${stat.size}|${Math.trunc(stat.mtimeMs)}`;
130
- } catch {
131
- return `${packageJSON.version}|${platformKey}|pkg`;
132
- }
133
- }
134
-
135
- function sourceBinaryMarker() {
136
- return `${packageJSON.version}|${platformKey}|source`;
137
- }
138
-
139
- function semverParts(value) {
140
- return String(value || "")
141
- .trim()
142
- .split(/[+-]/)[0]
143
- .split(".")
144
- .slice(0, 3)
145
- .map((part) => Number.parseInt(part, 10) || 0);
146
- }
147
-
148
- function compareVersions(a, b) {
149
- const aa = semverParts(a);
150
- const bb = semverParts(b);
151
- for (let i = 0; i < 3; i += 1) {
152
- if ((aa[i] || 0) > (bb[i] || 0)) return 1;
153
- if ((aa[i] || 0) < (bb[i] || 0)) return -1;
154
- }
155
- return String(a || "").localeCompare(String(b || ""));
156
- }
157
-
158
- function readJSON(path) {
159
- try {
160
- return JSON.parse(readFileSync(path, "utf8"));
161
- } catch {
162
- return null;
163
- }
164
- }
165
-
166
- function writeJSON(path, value) {
167
- try {
168
- mkdirSync(dirname(path), { recursive: true });
169
- writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
170
- } catch {
171
- // Update metadata is advisory; failing to cache must not block startup.
172
- }
173
- }
174
-
175
- function fetchJSON(url, timeoutMs = 1800) {
176
- return new Promise((resolvePromise, rejectPromise) => {
177
- const headers = { "User-Agent": `Axion-Code/${packageJSON.version}` };
178
- if (url.includes("githubusercontent.com")) {
179
- const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
180
- if (token) {
181
- headers.Authorization = `Bearer ${token}`;
182
- }
183
- }
184
- const req = https.get(url, { headers }, (res) => {
185
- if (res.statusCode !== 200) {
186
- res.resume();
187
- rejectPromise(new Error(`status ${res.statusCode}`));
188
- return;
189
- }
190
- let body = "";
191
- res.setEncoding("utf8");
192
- res.on("data", (chunk) => {
193
- body += chunk;
194
- if (body.length > 128 * 1024) {
195
- req.destroy(new Error("version payload too large"));
196
- }
197
- });
198
- res.on("end", () => {
199
- try {
200
- resolvePromise(JSON.parse(body));
201
- } catch (error) {
202
- rejectPromise(error);
203
- }
204
- });
205
- });
206
- req.setTimeout(timeoutMs, () => req.destroy(new Error("version check timeout")));
207
- req.on("error", rejectPromise);
208
- });
209
- }
210
-
211
- async function fetchLatestPackage() {
212
- const errors = [];
213
- for (const candidate of [
214
- { url: npmRegistryURL, source: packageJSON.name },
215
- { url: updatePackageURL, source: githubSource }
216
- ]) {
217
- try {
218
- const remote = await fetchJSON(candidate.url);
219
- const latestVersion = String(remote.version || "").trim();
220
- if (latestVersion) {
221
- return { latestVersion, source: candidate.source };
222
- }
223
- errors.push(`${candidate.url}: missing version`);
224
- } catch (error) {
225
- errors.push(`${candidate.url}: ${error.message}`);
226
- }
227
- }
228
- throw new Error(errors.join("; "));
229
- }
230
-
231
- async function readUpdateInfo({ force = false } = {}) {
232
- const cached = readJSON(updateCachePath);
233
- const now = Date.now();
234
- if (
235
- !force &&
236
- cached?.latestVersion &&
237
- compareVersions(cached.latestVersion, packageJSON.version) >= 0 &&
238
- now - Number(cached.checkedAt || 0) < updateCacheTTL
239
- ) {
240
- return {
241
- currentVersion: packageJSON.version,
242
- latestVersion: cached.latestVersion,
243
- updateAvailable: compareVersions(cached.latestVersion, packageJSON.version) > 0,
244
- source: updateSource,
245
- checkedAt: cached.checkedAt,
246
- fromCache: true
247
- };
248
- }
249
- try {
250
- const remote = await fetchLatestPackage();
251
- const latestVersion = remote.latestVersion;
252
- const info = {
253
- currentVersion: packageJSON.version,
254
- latestVersion,
255
- updateAvailable: latestVersion ? compareVersions(latestVersion, packageJSON.version) > 0 : false,
256
- source: remote.source || updateSource,
257
- checkedAt: now,
258
- fromCache: false
259
- };
260
- if (latestVersion) {
261
- writeJSON(updateCachePath, { latestVersion, checkedAt: now, source: info.source });
262
- }
263
- return info;
264
- } catch (error) {
265
- const usableCachedVersion =
266
- cached?.latestVersion && compareVersions(cached.latestVersion, packageJSON.version) >= 0
267
- ? cached.latestVersion
268
- : "";
269
- return {
270
- currentVersion: packageJSON.version,
271
- latestVersion: usableCachedVersion,
272
- updateAvailable: usableCachedVersion ? compareVersions(usableCachedVersion, packageJSON.version) > 0 : false,
273
- source: cached?.source || updateSource,
274
- checkedAt: cached?.checkedAt || 0,
275
- fromCache: Boolean(usableCachedVersion),
276
- error: error.message
277
- };
278
- }
279
- }
280
-
281
- async function updateCommandHandler(args) {
282
- const checkOnly = args.includes("--check");
283
- const asJSON = args.includes("--json");
284
- const info = await readUpdateInfo({ force: true });
285
- if (checkOnly) {
286
- if (asJSON) {
287
- console.log(JSON.stringify({
288
- current: info.currentVersion,
289
- latest: info.latestVersion || undefined,
290
- update_available: info.updateAvailable,
291
- source: info.source,
292
- command: updateCommand,
293
- checked: Boolean(info.latestVersion),
294
- error: info.error || undefined
295
- }));
296
- return;
297
- }
298
- if (info.updateAvailable) {
299
- console.log(`Update available: ${info.currentVersion} -> ${info.latestVersion}`);
300
- console.log(`Run: ${updateCommand}`);
301
- return;
302
- }
303
- if (info.latestVersion) {
304
- console.log(`Axion Code ${info.currentVersion} is up to date.`);
305
- return;
306
- }
307
- console.log(`Axion Code ${info.currentVersion}. Could not check latest version.`);
308
- if (info.error) {
309
- console.error(`Version check warning: ${info.error}`);
310
- }
311
- return;
312
- }
313
- const source = info.source === packageJSON.name ? packageJSON.name : updateSource;
314
- console.log(`Updating Axion Code from ${source}...`);
315
- // shell:true so Windows resolves npm.cmd (spawning bare "npm" with shell:false
316
- // fails with ENOENT there). Args are fixed constants, so no injection risk.
317
- const result = spawnSync("npm", ["install", "-g", source], {
318
- stdio: "inherit",
319
- shell: true
320
- });
321
- if (result.error) {
322
- console.error(result.error.message);
323
- process.exit(1);
324
- }
325
- if (result.status !== 0) {
326
- process.exit(result.status || 1);
327
- }
328
- console.log("Axion Code update completed. Run axion again.");
329
- }
330
-
331
- function applyTerminalSetup({ auto = false } = {}) {
332
- // Map Shift+Enter and Ctrl+Enter to send a literal newline (LF). Axion's
333
- // composer inserts a line break on LF, while plain Enter (CR) still sends.
334
- // This is needed because terminals do not report Shift+Enter/Ctrl+Enter as
335
- // distinct keys to the app on Windows. In auto mode it stays silent, never
336
- // clobbers an existing user binding, and swallows failures.
337
- if (!isWindows) {
338
- if (!auto) {
339
- console.log("axion terminal-setup currently automates Windows Terminal.");
340
- console.log("On other terminals, bind Shift+Enter (and/or Ctrl+Enter) to send a newline (\\n).");
341
- }
342
- return { changed: false, reason: "unsupported" };
343
- }
344
- const localAppData = process.env.LOCALAPPDATA || join(process.env.USERPROFILE || root, "AppData", "Local");
345
- const candidates = [
346
- join(localAppData, "Packages", "Microsoft.WindowsTerminal_8wekyb3d8bbwe", "LocalState", "settings.json"),
347
- join(localAppData, "Packages", "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", "LocalState", "settings.json"),
348
- join(localAppData, "Microsoft", "Windows Terminal", "settings.json")
349
- ];
350
- const settingsPath = candidates.find((p) => existsSync(p));
351
- if (!settingsPath) {
352
- if (!auto) {
353
- console.log("Windows Terminal settings.json not found.");
354
- console.log("If you use another terminal, bind Shift+Enter / Ctrl+Enter to send a newline (\\n).");
355
- }
356
- return { changed: false, reason: "not-found" };
357
- }
358
- let settings;
359
- try {
360
- settings = JSON.parse(readFileSync(settingsPath, "utf8"));
361
- } catch (err) {
362
- if (!auto) {
363
- console.error(`Could not parse ${settingsPath}: ${err.message}`);
364
- process.exit(1);
365
- }
366
- return { changed: false, reason: "parse-error" };
367
- }
368
- if (!Array.isArray(settings.keybindings)) settings.keybindings = [];
369
- const wanted = ["shift+enter", "ctrl+enter"];
370
- // Windows Terminal may normalize an inline {command,keys} binding into an
371
- // id-based one ({id,keys}) plus an entry in `actions`, so resolve both forms.
372
- const effectiveCommand = (k) => {
373
- if (!k) return null;
374
- if (k.command) return k.command;
375
- if (k.id) {
376
- const a = (settings.actions || []).find((x) => x && x.id === k.id);
377
- if (a && a.command) return a.command;
378
- }
379
- return null;
380
- };
381
- const isOurs = (k, keys) => {
382
- if (!k || k.keys !== keys) return false;
383
- const c = effectiveCommand(k);
384
- return c && c.action === "sendInput" && c.input === "\n";
385
- };
386
- if (wanted.every((keys) => settings.keybindings.some((k) => isOurs(k, keys)))) {
387
- if (!auto) console.log("Windows Terminal is already set up: Shift+Enter and Ctrl+Enter insert a newline.");
388
- return { changed: false, reason: "already" };
389
- }
390
- if (auto) {
391
- // Never overwrite a binding the user assigned to these keys themselves.
392
- for (const keys of wanted) {
393
- const existing = settings.keybindings.find((k) => k && k.keys === keys);
394
- if (existing && !isOurs(existing, keys)) return { changed: false, reason: "conflict" };
395
- }
396
- }
397
- settings.keybindings = settings.keybindings.filter((k) => !(k && wanted.includes(k.keys)));
398
- for (const keys of wanted) {
399
- settings.keybindings.push({ command: { action: "sendInput", input: "\n" }, keys });
400
- }
401
- const backup = `${settingsPath}.axion-bak`;
402
- try {
403
- copyFileSync(settingsPath, backup);
404
- } catch {}
405
- try {
406
- writeFileSync(settingsPath, JSON.stringify(settings, null, 4));
407
- } catch (err) {
408
- if (!auto) {
409
- console.error(err.message);
410
- process.exit(1);
411
- }
412
- return { changed: false, reason: "write-error" };
413
- }
414
- if (!auto) {
415
- console.log("Windows Terminal configured: Shift+Enter and Ctrl+Enter now insert a newline in Axion (Enter still sends).");
416
- console.log(`Settings: ${settingsPath}`);
417
- console.log(`Backup: ${backup}`);
418
- console.log("Reopen the terminal tab if the change does not take effect immediately.");
419
- }
420
- return { changed: true, settingsPath, backup };
421
- }
422
-
423
- function terminalSetupHandler() {
424
- applyTerminalSetup({ auto: false });
425
- }
426
-
427
- function maybeFirstRunTerminalSetup() {
428
- if (!isWindows || process.env.AXION_SKIP_TERMINAL_SETUP) return;
429
- const marker = join(binDir, ".axion-terminal-setup-done");
430
- if (existsSync(marker)) return;
431
- try {
432
- const res = applyTerminalSetup({ auto: true });
433
- if (res.changed) {
434
- console.log(
435
- `Axion enabled Shift+Enter and Ctrl+Enter as newline keys in Windows Terminal ` +
436
- `(reopen the tab to apply; backup at ${res.backup}, opt out with AXION_SKIP_TERMINAL_SETUP=1, ` +
437
- `redo with 'axion terminal-setup').`
438
- );
439
- }
440
- } catch {}
441
- try {
442
- mkdirSync(binDir, { recursive: true });
443
- writeFileSync(marker, new Date().toISOString());
444
- } catch {}
445
- }
446
-
447
- const args = process.argv.slice(2);
448
- if (args[0] === "update") {
449
- await updateCommandHandler(args.slice(1));
450
- process.exit(0);
451
- }
452
- if (args[0] === "terminal-setup") {
453
- terminalSetupHandler();
454
- process.exit(0);
455
- }
456
-
457
- maybeFirstRunTerminalSetup();
458
-
459
- const updateInfo = await readUpdateInfo();
460
- ensureBinary();
461
- const run = spawnSync(exePath, process.argv.slice(2), {
462
- stdio: "inherit",
463
- shell: false,
464
- env: {
465
- ...process.env,
466
- AXION_CODE_VERSION: packageJSON.version,
467
- AXION_CODE_LATEST_VERSION: updateInfo.latestVersion || "",
468
- AXION_CODE_UPDATE_AVAILABLE: updateInfo.updateAvailable ? "1" : "0",
469
- AXION_CODE_UPDATE_COMMAND: updateCommand,
470
- AXION_CODE_UPDATE_SOURCE: updateSource
471
- }
472
- });
473
-
474
- if (run.error) {
475
- console.error(run.error.message);
476
- process.exit(1);
477
- }
478
- process.exit(run.status ?? 0);
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
4
+ import https from "node:https";
5
+ import { dirname, join, resolve } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const here = dirname(fileURLToPath(import.meta.url));
9
+ const root = resolve(here, "..", "..");
10
+ const isWindows = process.platform === "win32";
11
+ const exeName = isWindows ? "axion-code.exe" : "axion-code";
12
+ const platformKey = `${process.platform}-${process.arch}`;
13
+ const packagedExePath = join(root, "npm", "releases", platformKey, exeName);
14
+ const binDir = isWindows
15
+ ? join(process.env.LOCALAPPDATA || join(process.env.USERPROFILE || root, "AppData", "Local"), "AxionCode", "bin")
16
+ : join(process.env.HOME || root, ".local", "share", "axion-code", "bin");
17
+ const exePath = join(binDir, exeName);
18
+ const markerPath = join(binDir, ".axion-code-package-version");
19
+ const updateCachePath = join(binDir, ".axion-code-update.json");
20
+ const packageJSON = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
21
+ const updateSource = packageJSON.name;
22
+ const githubSource = "github:rnbsolucoes/Axion-CLI";
23
+ const updatePackageURL = "https://raw.githubusercontent.com/rnbsolucoes/Axion-CLI/main/package.json";
24
+ const npmRegistryURL = "https://registry.npmjs.org/@rnbsolucoes%2Faxion-code/latest";
25
+ const updateCommand = "axion update";
26
+ const updateCacheTTL = 12 * 60 * 60 * 1000;
27
+
28
+ function findGo() {
29
+ if (process.env.AXION_GO) {
30
+ return process.env.AXION_GO;
31
+ }
32
+ if (isWindows && process.env.USERPROFILE) {
33
+ const bundled = join(process.env.USERPROFILE, ".codex", "toolchains", "go1.26.4", "go", "bin", "go.exe");
34
+ if (existsSync(bundled)) {
35
+ return bundled;
36
+ }
37
+ }
38
+ return "go";
39
+ }
40
+
41
+ function sourceAvailable() {
42
+ return existsSync(join(root, "go.mod")) && existsSync(join(root, "cmd", "axion-code"));
43
+ }
44
+
45
+ function sleep(ms) {
46
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
47
+ }
48
+
49
+ function markerMatches(expectedMarker) {
50
+ try {
51
+ return existsSync(exePath) && readFileSync(markerPath, "utf8").trim() === expectedMarker;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function copyPackagedBinaryWithRetry(expectedMarker) {
58
+ for (let attempt = 0; attempt < 8; attempt += 1) {
59
+ try {
60
+ copyFileSync(packagedExePath, exePath);
61
+ return;
62
+ } catch (error) {
63
+ const retryable = error && (error.code === "EBUSY" || error.code === "EPERM");
64
+ if (!retryable) {
65
+ throw error;
66
+ }
67
+ if (markerMatches(expectedMarker)) {
68
+ return;
69
+ }
70
+ if (attempt === 7) {
71
+ throw error;
72
+ }
73
+ sleep(75 * (attempt + 1));
74
+ }
75
+ }
76
+ }
77
+
78
+ function installPackagedBinary() {
79
+ if (!existsSync(packagedExePath)) {
80
+ return false;
81
+ }
82
+ const marker = packagedBinaryMarker();
83
+ copyPackagedBinaryWithRetry(marker);
84
+ if (!isWindows) {
85
+ chmodSync(exePath, 0o755);
86
+ }
87
+ writeFileSync(markerPath, `${marker}\n`, "utf8");
88
+ return true;
89
+ }
90
+
91
+ function buildFromSource() {
92
+ const go = findGo();
93
+ const result = spawnSync(go, ["build", "-o", exePath, "./cmd/axion-code"], {
94
+ cwd: root,
95
+ stdio: "inherit",
96
+ shell: false
97
+ });
98
+ if (result.status !== 0) {
99
+ return false;
100
+ }
101
+ writeFileSync(markerPath, `${sourceBinaryMarker()}\n`, "utf8");
102
+ return true;
103
+ }
104
+
105
+ function ensureBinary() {
106
+ const marker = existsSync(markerPath) ? readFileSync(markerPath, "utf8").trim() : "";
107
+ const desiredMarker = existsSync(packagedExePath) ? packagedBinaryMarker() : sourceBinaryMarker();
108
+ if (existsSync(exePath) && marker === desiredMarker) {
109
+ return;
110
+ }
111
+ mkdirSync(binDir, { recursive: true });
112
+ if (installPackagedBinary()) {
113
+ return;
114
+ }
115
+ if (sourceAvailable() && buildFromSource()) {
116
+ return;
117
+ }
118
+ console.error("");
119
+ console.error(`Axion Code binary for ${platformKey} was not found in this package.`);
120
+ console.error("If you are using a source clone, install Go 1.26+ on PATH or set AXION_GO and run:");
121
+ console.error(" npm run install:local");
122
+ console.error("If you installed from npm, update to a package that includes your platform binary.");
123
+ process.exit(1);
124
+ }
125
+
126
+ function packagedBinaryMarker() {
127
+ try {
128
+ const stat = statSync(packagedExePath);
129
+ return `${packageJSON.version}|${platformKey}|pkg|${stat.size}|${Math.trunc(stat.mtimeMs)}`;
130
+ } catch {
131
+ return `${packageJSON.version}|${platformKey}|pkg`;
132
+ }
133
+ }
134
+
135
+ function sourceBinaryMarker() {
136
+ return `${packageJSON.version}|${platformKey}|source`;
137
+ }
138
+
139
+ function semverParts(value) {
140
+ return String(value || "")
141
+ .trim()
142
+ .split(/[+-]/)[0]
143
+ .split(".")
144
+ .slice(0, 3)
145
+ .map((part) => Number.parseInt(part, 10) || 0);
146
+ }
147
+
148
+ function compareVersions(a, b) {
149
+ const aa = semverParts(a);
150
+ const bb = semverParts(b);
151
+ for (let i = 0; i < 3; i += 1) {
152
+ if ((aa[i] || 0) > (bb[i] || 0)) return 1;
153
+ if ((aa[i] || 0) < (bb[i] || 0)) return -1;
154
+ }
155
+ return String(a || "").localeCompare(String(b || ""));
156
+ }
157
+
158
+ function readJSON(path) {
159
+ try {
160
+ return JSON.parse(readFileSync(path, "utf8"));
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ function writeJSON(path, value) {
167
+ try {
168
+ mkdirSync(dirname(path), { recursive: true });
169
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`, "utf8");
170
+ } catch {
171
+ // Update metadata is advisory; failing to cache must not block startup.
172
+ }
173
+ }
174
+
175
+ function fetchJSON(url, timeoutMs = 1800) {
176
+ return new Promise((resolvePromise, rejectPromise) => {
177
+ const headers = { "User-Agent": `Axion-Code/${packageJSON.version}` };
178
+ if (url.includes("githubusercontent.com")) {
179
+ const token = process.env.GH_TOKEN || process.env.GITHUB_TOKEN || "";
180
+ if (token) {
181
+ headers.Authorization = `Bearer ${token}`;
182
+ }
183
+ }
184
+ const req = https.get(url, { headers }, (res) => {
185
+ if (res.statusCode !== 200) {
186
+ res.resume();
187
+ rejectPromise(new Error(`status ${res.statusCode}`));
188
+ return;
189
+ }
190
+ let body = "";
191
+ res.setEncoding("utf8");
192
+ res.on("data", (chunk) => {
193
+ body += chunk;
194
+ if (body.length > 128 * 1024) {
195
+ req.destroy(new Error("version payload too large"));
196
+ }
197
+ });
198
+ res.on("end", () => {
199
+ try {
200
+ resolvePromise(JSON.parse(body));
201
+ } catch (error) {
202
+ rejectPromise(error);
203
+ }
204
+ });
205
+ });
206
+ req.setTimeout(timeoutMs, () => req.destroy(new Error("version check timeout")));
207
+ req.on("error", rejectPromise);
208
+ });
209
+ }
210
+
211
+ async function fetchLatestPackage() {
212
+ const errors = [];
213
+ for (const candidate of [
214
+ { url: npmRegistryURL, source: packageJSON.name },
215
+ { url: updatePackageURL, source: githubSource }
216
+ ]) {
217
+ try {
218
+ const remote = await fetchJSON(candidate.url);
219
+ const latestVersion = String(remote.version || "").trim();
220
+ if (latestVersion) {
221
+ return { latestVersion, source: candidate.source };
222
+ }
223
+ errors.push(`${candidate.url}: missing version`);
224
+ } catch (error) {
225
+ errors.push(`${candidate.url}: ${error.message}`);
226
+ }
227
+ }
228
+ throw new Error(errors.join("; "));
229
+ }
230
+
231
+ async function readUpdateInfo({ force = false } = {}) {
232
+ const cached = readJSON(updateCachePath);
233
+ const now = Date.now();
234
+ if (
235
+ !force &&
236
+ cached?.latestVersion &&
237
+ compareVersions(cached.latestVersion, packageJSON.version) >= 0 &&
238
+ now - Number(cached.checkedAt || 0) < updateCacheTTL
239
+ ) {
240
+ return {
241
+ currentVersion: packageJSON.version,
242
+ latestVersion: cached.latestVersion,
243
+ updateAvailable: compareVersions(cached.latestVersion, packageJSON.version) > 0,
244
+ source: updateSource,
245
+ checkedAt: cached.checkedAt,
246
+ fromCache: true
247
+ };
248
+ }
249
+ try {
250
+ const remote = await fetchLatestPackage();
251
+ const latestVersion = remote.latestVersion;
252
+ const info = {
253
+ currentVersion: packageJSON.version,
254
+ latestVersion,
255
+ updateAvailable: latestVersion ? compareVersions(latestVersion, packageJSON.version) > 0 : false,
256
+ source: remote.source || updateSource,
257
+ checkedAt: now,
258
+ fromCache: false
259
+ };
260
+ if (latestVersion) {
261
+ writeJSON(updateCachePath, { latestVersion, checkedAt: now, source: info.source });
262
+ }
263
+ return info;
264
+ } catch (error) {
265
+ const usableCachedVersion =
266
+ cached?.latestVersion && compareVersions(cached.latestVersion, packageJSON.version) >= 0
267
+ ? cached.latestVersion
268
+ : "";
269
+ return {
270
+ currentVersion: packageJSON.version,
271
+ latestVersion: usableCachedVersion,
272
+ updateAvailable: usableCachedVersion ? compareVersions(usableCachedVersion, packageJSON.version) > 0 : false,
273
+ source: cached?.source || updateSource,
274
+ checkedAt: cached?.checkedAt || 0,
275
+ fromCache: Boolean(usableCachedVersion),
276
+ error: error.message
277
+ };
278
+ }
279
+ }
280
+
281
+ async function updateCommandHandler(args) {
282
+ const checkOnly = args.includes("--check");
283
+ const asJSON = args.includes("--json");
284
+ const info = await readUpdateInfo({ force: true });
285
+ if (checkOnly) {
286
+ if (asJSON) {
287
+ console.log(JSON.stringify({
288
+ current: info.currentVersion,
289
+ latest: info.latestVersion || undefined,
290
+ update_available: info.updateAvailable,
291
+ source: info.source,
292
+ command: updateCommand,
293
+ checked: Boolean(info.latestVersion),
294
+ error: info.error || undefined
295
+ }));
296
+ return;
297
+ }
298
+ if (info.updateAvailable) {
299
+ console.log(`Update available: ${info.currentVersion} -> ${info.latestVersion}`);
300
+ console.log(`Run: ${updateCommand}`);
301
+ return;
302
+ }
303
+ if (info.latestVersion) {
304
+ console.log(`Axion Code ${info.currentVersion} is up to date.`);
305
+ return;
306
+ }
307
+ console.log(`Axion Code ${info.currentVersion}. Could not check latest version.`);
308
+ if (info.error) {
309
+ console.error(`Version check warning: ${info.error}`);
310
+ }
311
+ return;
312
+ }
313
+ const source = info.source === packageJSON.name ? packageJSON.name : updateSource;
314
+ console.log(`Updating Axion Code from ${source}...`);
315
+ // shell:true so Windows resolves npm.cmd (spawning bare "npm" with shell:false
316
+ // fails with ENOENT there). Args are fixed constants, so no injection risk.
317
+ const result = spawnSync("npm", ["install", "-g", source], {
318
+ stdio: "inherit",
319
+ shell: true
320
+ });
321
+ if (result.error) {
322
+ console.error(result.error.message);
323
+ process.exit(1);
324
+ }
325
+ if (result.status !== 0) {
326
+ process.exit(result.status || 1);
327
+ }
328
+ console.log("Axion Code update completed. Run axion again.");
329
+ }
330
+
331
+ function applyTerminalSetup({ auto = false } = {}) {
332
+ // Map Shift+Enter and Ctrl+Enter to send a literal newline (LF). Axion's
333
+ // composer inserts a line break on LF, while plain Enter (CR) still sends.
334
+ // This is needed because terminals do not report Shift+Enter/Ctrl+Enter as
335
+ // distinct keys to the app on Windows. In auto mode it stays silent, never
336
+ // clobbers an existing user binding, and swallows failures.
337
+ if (!isWindows) {
338
+ if (!auto) {
339
+ console.log("axion terminal-setup currently automates Windows Terminal.");
340
+ console.log("On other terminals, bind Shift+Enter (and/or Ctrl+Enter) to send a newline (\\n).");
341
+ }
342
+ return { changed: false, reason: "unsupported" };
343
+ }
344
+ const localAppData = process.env.LOCALAPPDATA || join(process.env.USERPROFILE || root, "AppData", "Local");
345
+ const candidates = [
346
+ join(localAppData, "Packages", "Microsoft.WindowsTerminal_8wekyb3d8bbwe", "LocalState", "settings.json"),
347
+ join(localAppData, "Packages", "Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe", "LocalState", "settings.json"),
348
+ join(localAppData, "Microsoft", "Windows Terminal", "settings.json")
349
+ ];
350
+ const settingsPath = candidates.find((p) => existsSync(p));
351
+ if (!settingsPath) {
352
+ if (!auto) {
353
+ console.log("Windows Terminal settings.json not found.");
354
+ console.log("If you use another terminal, bind Shift+Enter / Ctrl+Enter to send a newline (\\n).");
355
+ }
356
+ return { changed: false, reason: "not-found" };
357
+ }
358
+ let settings;
359
+ try {
360
+ settings = JSON.parse(readFileSync(settingsPath, "utf8"));
361
+ } catch (err) {
362
+ if (!auto) {
363
+ console.error(`Could not parse ${settingsPath}: ${err.message}`);
364
+ process.exit(1);
365
+ }
366
+ return { changed: false, reason: "parse-error" };
367
+ }
368
+ if (!Array.isArray(settings.keybindings)) settings.keybindings = [];
369
+ const wanted = ["shift+enter", "ctrl+enter"];
370
+ // Windows Terminal may normalize an inline {command,keys} binding into an
371
+ // id-based one ({id,keys}) plus an entry in `actions`, so resolve both forms.
372
+ const effectiveCommand = (k) => {
373
+ if (!k) return null;
374
+ if (k.command) return k.command;
375
+ if (k.id) {
376
+ const a = (settings.actions || []).find((x) => x && x.id === k.id);
377
+ if (a && a.command) return a.command;
378
+ }
379
+ return null;
380
+ };
381
+ const isOurs = (k, keys) => {
382
+ if (!k || k.keys !== keys) return false;
383
+ const c = effectiveCommand(k);
384
+ return c && c.action === "sendInput" && c.input === "\n";
385
+ };
386
+ if (wanted.every((keys) => settings.keybindings.some((k) => isOurs(k, keys)))) {
387
+ if (!auto) console.log("Windows Terminal is already set up: Shift+Enter and Ctrl+Enter insert a newline.");
388
+ return { changed: false, reason: "already" };
389
+ }
390
+ if (auto) {
391
+ // Never overwrite a binding the user assigned to these keys themselves.
392
+ for (const keys of wanted) {
393
+ const existing = settings.keybindings.find((k) => k && k.keys === keys);
394
+ if (existing && !isOurs(existing, keys)) return { changed: false, reason: "conflict" };
395
+ }
396
+ }
397
+ settings.keybindings = settings.keybindings.filter((k) => !(k && wanted.includes(k.keys)));
398
+ for (const keys of wanted) {
399
+ settings.keybindings.push({ command: { action: "sendInput", input: "\n" }, keys });
400
+ }
401
+ const backup = `${settingsPath}.axion-bak`;
402
+ try {
403
+ copyFileSync(settingsPath, backup);
404
+ } catch {}
405
+ try {
406
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 4));
407
+ } catch (err) {
408
+ if (!auto) {
409
+ console.error(err.message);
410
+ process.exit(1);
411
+ }
412
+ return { changed: false, reason: "write-error" };
413
+ }
414
+ if (!auto) {
415
+ console.log("Windows Terminal configured: Shift+Enter and Ctrl+Enter now insert a newline in Axion (Enter still sends).");
416
+ console.log(`Settings: ${settingsPath}`);
417
+ console.log(`Backup: ${backup}`);
418
+ console.log("Reopen the terminal tab if the change does not take effect immediately.");
419
+ }
420
+ return { changed: true, settingsPath, backup };
421
+ }
422
+
423
+ function terminalSetupHandler() {
424
+ applyTerminalSetup({ auto: false });
425
+ }
426
+
427
+ function maybeFirstRunTerminalSetup() {
428
+ if (!isWindows || process.env.AXION_SKIP_TERMINAL_SETUP) return;
429
+ const marker = join(binDir, ".axion-terminal-setup-done");
430
+ if (existsSync(marker)) return;
431
+ try {
432
+ const res = applyTerminalSetup({ auto: true });
433
+ if (res.changed) {
434
+ console.log(
435
+ `Axion enabled Shift+Enter and Ctrl+Enter as newline keys in Windows Terminal ` +
436
+ `(reopen the tab to apply; backup at ${res.backup}, opt out with AXION_SKIP_TERMINAL_SETUP=1, ` +
437
+ `redo with 'axion terminal-setup').`
438
+ );
439
+ }
440
+ } catch {}
441
+ try {
442
+ mkdirSync(binDir, { recursive: true });
443
+ writeFileSync(marker, new Date().toISOString());
444
+ } catch {}
445
+ }
446
+
447
+ const args = process.argv.slice(2);
448
+ if (args[0] === "update") {
449
+ await updateCommandHandler(args.slice(1));
450
+ process.exit(0);
451
+ }
452
+ if (args[0] === "terminal-setup") {
453
+ terminalSetupHandler();
454
+ process.exit(0);
455
+ }
456
+
457
+ maybeFirstRunTerminalSetup();
458
+
459
+ const updateInfo = await readUpdateInfo();
460
+ ensureBinary();
461
+ const run = spawnSync(exePath, process.argv.slice(2), {
462
+ stdio: "inherit",
463
+ shell: false,
464
+ env: {
465
+ ...process.env,
466
+ AXION_CODE_VERSION: packageJSON.version,
467
+ AXION_CODE_LATEST_VERSION: updateInfo.latestVersion || "",
468
+ AXION_CODE_UPDATE_AVAILABLE: updateInfo.updateAvailable ? "1" : "0",
469
+ AXION_CODE_UPDATE_COMMAND: updateCommand,
470
+ AXION_CODE_UPDATE_SOURCE: updateSource
471
+ }
472
+ });
473
+
474
+ if (run.error) {
475
+ console.error(run.error.message);
476
+ process.exit(1);
477
+ }
478
+ process.exit(run.status ?? 0);