@timurproko/a1-install 0.2.1-dev.599

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Timur Prokopiev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @timurproko/a1-install
2
+
3
+ Dependency-free installer for [`@timurproko/a1`](https://www.npmjs.com/package/@timurproko/a1).
4
+
5
+ ```sh
6
+ npx -y @timurproko/a1-install
7
+ ```
8
+
9
+ Development channel:
10
+
11
+ ```sh
12
+ npx -y @timurproko/a1-install --develop
13
+ ```
14
+
15
+ Exact development version:
16
+
17
+ ```sh
18
+ npx -y @timurproko/a1-install --version 0.1.8-dev.107
19
+ ```
@@ -0,0 +1,629 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { access, lstat, readFile, realpath } from "node:fs/promises";
5
+ import { existsSync, realpathSync } from "node:fs";
6
+ import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+
9
+ const APPLICATION_PACKAGE = "@timurproko/a1";
10
+ const COMMAND = "a1";
11
+ const PROGRESS_WIDTH = 40;
12
+ const DIAGNOSTIC_LIMIT = 8_000;
13
+ const PHASES = new Set([
14
+ "Preparing",
15
+ "Resolving version",
16
+ "Resolving packages",
17
+ "Downloading packages",
18
+ "Installing",
19
+ "Activating",
20
+ "Verifying",
21
+ ]);
22
+ const STABLE_VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/u;
23
+ const DEVELOPMENT_VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)-dev\.[1-9]\d*$/u;
24
+ const VERSION_PATTERN = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-dev\.[1-9]\d*)?$/u;
25
+
26
+ class InstallationFailure extends Error {
27
+ constructor(reason, details = "", exitCode = 1) {
28
+ super(reason);
29
+ this.name = "InstallationFailure";
30
+ this.reason = reason;
31
+ this.details = details;
32
+ this.exitCode = exitCode;
33
+ }
34
+ }
35
+
36
+ class InstallationCancelled extends InstallationFailure {
37
+ constructor() {
38
+ super("installation cancelled", "", 130);
39
+ this.name = "InstallationCancelled";
40
+ }
41
+ }
42
+
43
+ export function parseArguments(argv) {
44
+ if (!Array.isArray(argv) || argv.length > 8 || argv.some(value => typeof value !== "string" || value.length > 128)) {
45
+ throw new InstallationFailure("invalid command arguments", "", 2);
46
+ }
47
+ let target = { kind: "stable" };
48
+ let verbose = false;
49
+ let help = false;
50
+ for (let index = 0; index < argv.length; index += 1) {
51
+ const value = argv[index];
52
+ if (value === "--help" || value === "-h") {
53
+ help = true;
54
+ continue;
55
+ }
56
+ if (value === "--verbose") {
57
+ if (verbose) throw new InstallationFailure("--verbose was supplied more than once", "", 2);
58
+ verbose = true;
59
+ continue;
60
+ }
61
+ if (value === "--develop") {
62
+ if (target.kind === "version") throw new InstallationFailure("--develop cannot be combined with --version", "", 2);
63
+ if (target.kind !== "stable") throw new InstallationFailure("--develop was supplied more than once", "", 2);
64
+ target = { kind: "develop" };
65
+ continue;
66
+ }
67
+ if (value === "--version") {
68
+ const version = argv[index + 1];
69
+ if (version === undefined || version.startsWith("-")) {
70
+ throw new InstallationFailure("--version requires an exact development version", "", 2);
71
+ }
72
+ if (!DEVELOPMENT_VERSION_PATTERN.test(version)) {
73
+ throw new InstallationFailure("--version requires an exact development version", "", 2);
74
+ }
75
+ if (target.kind === "develop") throw new InstallationFailure("--develop cannot be combined with --version", "", 2);
76
+ if (target.kind !== "stable") throw new InstallationFailure("--version was supplied more than once", "", 2);
77
+ target = { kind: "version", version };
78
+ index += 1;
79
+ continue;
80
+ }
81
+ if (value?.startsWith("-")) throw new InstallationFailure("unsupported option", "", 2);
82
+ throw new InstallationFailure("unexpected argument", "", 2);
83
+ }
84
+ if (help && argv.some(value => !["--help", "-h"].includes(value))) {
85
+ throw new InstallationFailure("--help cannot be combined with another option", "", 2);
86
+ }
87
+ return { target, verbose, help };
88
+ }
89
+
90
+ export function installerHelp() {
91
+ return [
92
+ "a1-install",
93
+ "a1-install --develop",
94
+ "a1-install --version <x.y.z-dev.n>",
95
+ "a1-install --verbose",
96
+ "",
97
+ ].join("\n");
98
+ }
99
+
100
+ export function renderProgressBar(percent) {
101
+ const bounded = Math.min(100, Math.max(0, Math.round(percent)));
102
+ const filled = Math.round((bounded / 100) * PROGRESS_WIDTH);
103
+ const completed = "\u001b[38;2;138;190;183m";
104
+ const gray = "\u001b[38;2;128;128;128m";
105
+ const track = "\u001b[38;2;102;102;102m";
106
+ return `${completed}${"━".repeat(filled)}${track}${"─".repeat(PROGRESS_WIDTH - filled)}${gray} ${bounded}%\u001b[39m`;
107
+ }
108
+
109
+ export function classifyProgressLine(line, fallback = "Installing") {
110
+ const value = String(line).replace(/\r/g, "").trim();
111
+ if (/npm verbose title npm (?:view|show)\b/iu.test(value)) return "Resolving version";
112
+ if (/npm verbose title npm (?:install|ci)\b/iu.test(value)) return "Resolving packages";
113
+ if (/npm http (?:fetch|cache)\b/iu.test(value)) return "Downloading packages";
114
+ if (/npm info run\b/iu.test(value)) return "Installing";
115
+ return PHASES.has(fallback) ? fallback : "Installing";
116
+ }
117
+
118
+ export function conciseFailure(stage, diagnostics) {
119
+ const text = String(diagnostics ?? "").toLowerCase();
120
+ if (/e401|e403|unauthorized|authentication|forbidden/u.test(text)) return "registry authentication failed";
121
+ if (/e404|etarget|not found|no matching version/u.test(text)) return stage === "resolve" ? "the selected release was not found" : "npm installation failed";
122
+ if (/eacces|eperm|permission denied|access is denied/u.test(text)) return "permission was denied";
123
+ if (/enotfound|eai_again|econnreset|econnrefused|network|socket|timed? ?out/u.test(text)) return "network access failed";
124
+ if (stage === "resolve") return "could not resolve the selected release";
125
+ if (stage === "update") return "existing installation could not be updated";
126
+ if (stage === "activate") return "activation failed";
127
+ return "npm installation failed";
128
+ }
129
+
130
+ export function sanitizeDiagnostic(value) {
131
+ const safe = String(value ?? "")
132
+ .replace(/\u001B\][^\u0007]*(?:\u0007|\u001B\\)/gu, "")
133
+ .replace(/\u001B(?:[@-_]|\[[0-?]*[ -/]*[@-~])/gu, "")
134
+ .replace(/[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/gu, "");
135
+ const lines = safe
136
+ .split(/\n/u)
137
+ .map(line => line.trim())
138
+ .filter(Boolean)
139
+ .filter(line => !/^(?:authorization|token|password|_auth)\s*[:=]/iu.test(line))
140
+ .map(line => line
141
+ .replace(/(https?:\/\/)[^/@\s]+@/giu, "$1[redacted]@")
142
+ .replace(/([?&](?:token|key|auth|password)=)[^&\s]+/giu, "$1[redacted]")
143
+ .replace(/((?:_authToken|authorization|password|token)\s*[:=]\s*)\S+/giu, "$1[redacted]")
144
+ .replace(/(bearer\s+)\S+/giu, "$1[redacted]"));
145
+ return lines.slice(-4).join(" | ").slice(0, 1_000);
146
+ }
147
+
148
+ class ProgressDisplay {
149
+ constructor(output, enabled, options = {}) {
150
+ this.output = output;
151
+ this.enabled = enabled;
152
+ this.interval = options.interval ?? 200;
153
+ this.current = 0;
154
+ this.shown = -1;
155
+ this.label = "Preparing";
156
+ this.timer = null;
157
+ this.visible = false;
158
+ this.maxWidth = PROGRESS_WIDTH + 6 + 1 + 64;
159
+ }
160
+
161
+ set(percent, label, creepTo = percent) {
162
+ if (!this.enabled) return;
163
+ this.stop();
164
+ this.current = Math.max(this.current, percent);
165
+ if (PHASES.has(label)) this.label = label;
166
+ this.shown = -1;
167
+ this.draw();
168
+ if (creepTo <= this.current) return;
169
+ this.timer = setInterval(() => {
170
+ this.current = Math.min(creepTo - 1, this.current + (creepTo - this.current) * 0.04);
171
+ this.draw();
172
+ }, this.interval);
173
+ this.timer.unref?.();
174
+ }
175
+
176
+ draw() {
177
+ const rounded = Math.round(this.current);
178
+ if (rounded === this.shown) return;
179
+ this.shown = rounded;
180
+ if (!this.visible) this.output.write("\u001b[?25l");
181
+ this.visible = true;
182
+ this.output.write(`\r${renderProgressBar(rounded)} ${this.label}`);
183
+ }
184
+
185
+ finish() {
186
+ if (!this.enabled) return;
187
+ this.stop();
188
+ this.current = 100;
189
+ this.shown = -1;
190
+ this.draw();
191
+ this.clear();
192
+ }
193
+
194
+ clear() {
195
+ this.stop();
196
+ if (!this.enabled || !this.visible) return;
197
+ this.output.write(`\r${" ".repeat(this.maxWidth)}\r`);
198
+ this.output.write("\u001b[39m\u001b[?25h");
199
+ this.visible = false;
200
+ this.shown = -1;
201
+ }
202
+
203
+ stop() {
204
+ if (this.timer !== null) clearInterval(this.timer);
205
+ this.timer = null;
206
+ }
207
+ }
208
+
209
+ function createDefaultProcessRunner(environment, platform) {
210
+ const npmCli = resolveNpmCli(environment);
211
+ return {
212
+ npm: async (args, callbacks = {}) => await runChild(process.execPath, [npmCli, ...args], { environment, ...callbacks }),
213
+ node: async (entry, args, callbacks = {}) => await runChild(process.execPath, [entry, ...args], { environment, ...callbacks }),
214
+ };
215
+
216
+ function resolveNpmCli(env) {
217
+ const declared = env.npm_execpath;
218
+ if (declared) {
219
+ const candidate = /npx-cli\.js$/iu.test(declared) ? resolve(dirname(declared), "npm-cli.js") : declared;
220
+ if (existsSync(candidate)) return candidate;
221
+ }
222
+ for (const directory of String(env.PATH ?? "").split(delimiter)) {
223
+ if (!directory) continue;
224
+ const candidates = [
225
+ resolve(directory, "node_modules", "npm", "bin", "npm-cli.js"),
226
+ resolve(directory, "..", "lib", "node_modules", "npm", "bin", "npm-cli.js"),
227
+ ];
228
+ for (const candidate of candidates) if (existsSync(candidate)) return candidate;
229
+ }
230
+ throw new InstallationFailure("npm is unavailable");
231
+ }
232
+ }
233
+
234
+ async function runChild(command, args, options) {
235
+ return await new Promise((resolvePromise, rejectPromise) => {
236
+ const child = spawn(command, args, {
237
+ env: { ...options.environment, NO_UPDATE_NOTIFIER: "1", npm_config_update_notifier: "false" },
238
+ stdio: ["ignore", "pipe", "pipe"],
239
+ windowsHide: true,
240
+ });
241
+ options.onChild?.(child);
242
+ let stdout = "";
243
+ let stderr = "";
244
+ let stdoutPending = "";
245
+ let stderrPending = "";
246
+ const consume = (stream, chunk, callback) => {
247
+ const combined = `${stream}${chunk}`.slice(-DIAGNOSTIC_LIMIT);
248
+ const lines = combined.split(/\r?\n/u);
249
+ const pending = lines.pop() ?? "";
250
+ for (const line of lines) callback?.(line);
251
+ return pending;
252
+ };
253
+ child.stdout?.setEncoding("utf8");
254
+ child.stderr?.setEncoding("utf8");
255
+ child.stdout?.on("data", chunk => {
256
+ stdout = `${stdout}${chunk}`.slice(-DIAGNOSTIC_LIMIT);
257
+ stdoutPending = consume(stdoutPending, chunk, options.onStdoutLine);
258
+ });
259
+ child.stderr?.on("data", chunk => {
260
+ stderr = `${stderr}${chunk}`.slice(-DIAGNOSTIC_LIMIT);
261
+ stderrPending = consume(stderrPending, chunk, options.onStderrLine);
262
+ });
263
+ let settled = false;
264
+ child.once("error", error => {
265
+ if (settled) return;
266
+ settled = true;
267
+ rejectPromise(error);
268
+ });
269
+ child.once("close", (code, signal) => {
270
+ if (settled) return;
271
+ settled = true;
272
+ if (stdoutPending) options.onStdoutLine?.(stdoutPending);
273
+ if (stderrPending) options.onStderrLine?.(stderrPending);
274
+ resolvePromise({ code, signal, stdout, stderr });
275
+ });
276
+ });
277
+ }
278
+
279
+ function targetSpec(target) {
280
+ if (target.kind === "develop") return `${APPLICATION_PACKAGE}@next`;
281
+ if (target.kind === "version") return `${APPLICATION_PACKAGE}@${target.version}`;
282
+ return `${APPLICATION_PACKAGE}@latest`;
283
+ }
284
+
285
+ function updateArguments(target) {
286
+ if (target.kind === "develop") return ["update", "--develop"];
287
+ if (target.kind === "version") return ["update", "--develop", target.version];
288
+ return ["update"];
289
+ }
290
+
291
+ function parseResolvedVersion(stdout, target) {
292
+ const text = String(stdout).trim();
293
+ let value = text;
294
+ try {
295
+ const parsed = JSON.parse(text);
296
+ if (typeof parsed === "string") value = parsed;
297
+ else if (Array.isArray(parsed) && typeof parsed[0] === "string") value = parsed[0];
298
+ } catch {
299
+ // Compatibility: npm can return the plain version when a registry overrides JSON formatting.
300
+ }
301
+ value = value.trim();
302
+ if (!VERSION_PATTERN.test(value)) throw new InstallationFailure("could not resolve the selected release", text);
303
+ if (target.kind === "stable" && !STABLE_VERSION_PATTERN.test(value)) throw new InstallationFailure("could not resolve the selected release", text);
304
+ if (target.kind === "develop" && !DEVELOPMENT_VERSION_PATTERN.test(value)) throw new InstallationFailure("could not resolve the selected release", text);
305
+ if (target.kind === "version" && value !== target.version) throw new InstallationFailure("the selected release was not found", text);
306
+ return value;
307
+ }
308
+
309
+ function packageRootFor(globalRoot) {
310
+ return resolve(globalRoot, ...APPLICATION_PACKAGE.split("/"));
311
+ }
312
+
313
+ async function readExistingManifest(packageRoot) {
314
+ let metadata;
315
+ try { metadata = await lstat(packageRoot); }
316
+ catch (error) {
317
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
318
+ throw new InstallationFailure("existing installation ownership could not be verified", String(error));
319
+ }
320
+ if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
321
+ throw new InstallationFailure("existing installation ownership could not be verified");
322
+ }
323
+ const manifest = await readManifest(packageRoot);
324
+ if (manifest === null) throw new InstallationFailure("existing installation ownership could not be verified");
325
+ return manifest;
326
+ }
327
+
328
+ async function readManifest(packageRoot) {
329
+ try {
330
+ return JSON.parse(await readFile(resolve(packageRoot, "package.json"), "utf8"));
331
+ } catch (error) {
332
+ if (error && typeof error === "object" && error.code === "ENOENT") return null;
333
+ throw new InstallationFailure("the installed package is incomplete", String(error));
334
+ }
335
+ }
336
+
337
+ function launcherPaths(globalRoot, platform) {
338
+ if (platform === "win32") {
339
+ const prefix = dirname(globalRoot);
340
+ return [resolve(prefix, COMMAND), resolve(prefix, `${COMMAND}.cmd`), resolve(prefix, `${COMMAND}.ps1`)];
341
+ }
342
+ const prefix = dirname(dirname(globalRoot));
343
+ return [resolve(prefix, "bin", COMMAND)];
344
+ }
345
+
346
+ async function assertLaunchersAbsent(globalRoot, platform) {
347
+ for (const path of launcherPaths(globalRoot, platform)) {
348
+ try {
349
+ await lstat(path);
350
+ throw new InstallationFailure("existing installation ownership could not be verified");
351
+ } catch (error) {
352
+ if (error && typeof error === "object" && error.code === "ENOENT") continue;
353
+ if (error instanceof InstallationFailure) throw error;
354
+ throw new InstallationFailure("existing installation ownership could not be verified", String(error));
355
+ }
356
+ }
357
+ }
358
+
359
+ async function verifyLaunchers(globalRoot, platform) {
360
+ const paths = launcherPaths(globalRoot, platform);
361
+ const entry = resolve(packageRootFor(globalRoot), "bin", "cli.js");
362
+ const expectedSuffix = `${APPLICATION_PACKAGE}/bin/cli.js`;
363
+ for (const path of paths) {
364
+ try {
365
+ const metadata = await lstat(path);
366
+ if (metadata.isSymbolicLink()) {
367
+ if (normalizePath(await realpath(path), platform) !== normalizePath(entry, platform)) throw new Error("launcher target differs");
368
+ } else if (metadata.isFile()) {
369
+ const content = await readFile(path, "utf8");
370
+ if (content.length > 32_768 || !content.replaceAll("\\", "/").includes(expectedSuffix)) throw new Error("launcher content differs");
371
+ } else throw new Error("not a file");
372
+ } catch (error) {
373
+ throw new InstallationFailure("launcher verification failed", String(error));
374
+ }
375
+ }
376
+ return paths;
377
+ }
378
+
379
+ function normalizePath(value, platform) {
380
+ const normalized = resolve(value);
381
+ return platform === "win32" ? normalized.toLowerCase() : normalized;
382
+ }
383
+
384
+ async function verifyCommandResolution(expected, environment, platform) {
385
+ const names = platform === "win32" ? ["a1.com", "a1.exe", "a1.bat", "a1.cmd", "a1.ps1", "a1"] : ["a1"];
386
+ let selected = null;
387
+ for (const directory of String(environment.PATH ?? "").split(delimiter)) {
388
+ if (!directory) continue;
389
+ for (const name of names) {
390
+ const candidate = resolve(directory, name);
391
+ try {
392
+ await access(candidate);
393
+ selected = candidate;
394
+ break;
395
+ } catch { /* Rationale: keep searching the remaining PATH candidates. */ }
396
+ }
397
+ if (selected) break;
398
+ }
399
+ if (!selected) throw new InstallationFailure("the installed command is not active in this shell");
400
+ const expectedNormalized = new Set(expected.map(path => normalizePath(path, platform)));
401
+ if (!expectedNormalized.has(normalizePath(selected, platform))) {
402
+ // Platform: Unix npm launchers are symlinks; lexical launcher ownership is authoritative,
403
+ // while canonical comparison also accepts shells that return the resolved target.
404
+ try {
405
+ const selectedReal = normalizePath(await realpath(selected), platform);
406
+ const expectedReal = new Set(await Promise.all(expected.map(async path => normalizePath(await realpath(path), platform))));
407
+ if (expectedReal.has(selectedReal)) return;
408
+ } catch { /* Rationale: report the stable public reason below. */ }
409
+ throw new InstallationFailure("another command takes precedence");
410
+ }
411
+ }
412
+
413
+ async function verifyInstalledPackage(packageRoot, targetVersion) {
414
+ const manifest = await readManifest(packageRoot);
415
+ if (manifest === null) throw new InstallationFailure("the installed package is incomplete");
416
+ if (manifest.name !== APPLICATION_PACKAGE) throw new InstallationFailure("the installed package identity is invalid");
417
+ if (manifest.version !== targetVersion) throw new InstallationFailure("the installed version does not match the selected release");
418
+ if (!Array.isArray(manifest.updateActivationContracts) || !manifest.updateActivationContracts.includes("activate-v1")) {
419
+ throw new InstallationFailure("activation failed", "the selected release does not declare the required activation contract");
420
+ }
421
+ return manifest;
422
+ }
423
+
424
+ async function activateInstalled(runner, packageRoot, targetVersion, progress, setActiveChild, isCancelled) {
425
+ const entry = resolve(packageRoot, "bin", "activate.js");
426
+ try { await access(entry); } catch { throw new InstallationFailure("activation failed", "activation entry is missing"); }
427
+ let completed = false;
428
+ let failure = "";
429
+ let relay = Promise.resolve();
430
+ const result = await runner.node(entry, ["--target-version", targetVersion], {
431
+ onChild: setActiveChild,
432
+ onStdoutLine: line => {
433
+ relay = relay.then(async () => {
434
+ let event;
435
+ try { event = JSON.parse(line); } catch { failure ||= "activation returned an invalid event"; return; }
436
+ if (event?.event === "materializing" && Number.isFinite(event.completed) && Number.isFinite(event.total)) {
437
+ const ratio = event.total > 0 ? Math.min(1, event.completed / event.total) : 0;
438
+ progress.set(76 + ratio * 14, "Activating");
439
+ } else if (event?.event === "phase") {
440
+ if (event.phase === "materialized") progress.set(90, "Activating", 93);
441
+ else if (event.phase === "certified") progress.set(94, "Activating", 96);
442
+ else if (event.phase === "active-reference-committed") progress.set(98, "Verifying", 99);
443
+ else failure ||= "activation returned an invalid phase";
444
+ } else if (event?.event === "warmup") {
445
+ if (event.state === "started") progress.set(96, "Activating", 97);
446
+ else if (event.state === "completed") progress.set(97, "Verifying", 98);
447
+ else failure ||= "activation returned an invalid warmup state";
448
+ } else if (event?.event === "completed") completed = true;
449
+ else if (event?.event === "failed" && typeof event.message === "string") failure ||= event.message;
450
+ else failure ||= "activation returned an invalid event";
451
+ });
452
+ },
453
+ });
454
+ await relay;
455
+ setActiveChild(null);
456
+ if (isCancelled()) throw new InstallationCancelled();
457
+ if (result.code !== 0 || failure || !completed) {
458
+ throw new InstallationFailure("activation failed", failure || result.stderr || `exit ${result.code ?? result.signal ?? "unknown"}`);
459
+ }
460
+ }
461
+
462
+ export async function runInstaller(argv, options = {}) {
463
+ const stdout = options.stdout ?? process.stdout;
464
+ const stderr = options.stderr ?? process.stderr;
465
+ const environment = options.environment ?? process.env;
466
+ const platform = options.platform ?? process.platform;
467
+ const parsed = parseArguments(argv);
468
+ if (parsed.help) {
469
+ stdout.write(installerHelp());
470
+ return 0;
471
+ }
472
+ if (!/^v?(?:22\.(?:19|[2-9]\d)|2[3-4]\.\d+)\./u.test(process.version)) {
473
+ throw new InstallationFailure("Node.js 22.19.0 or newer is required");
474
+ }
475
+
476
+ const progress = options.progress ?? new ProgressDisplay(stdout, options.tty ?? stdout.isTTY === true, options.progressOptions);
477
+ const runner = options.runner ?? createDefaultProcessRunner(environment, platform);
478
+ let activeChild = null;
479
+ let killTimer = null;
480
+ let cancelled = options.signal?.aborted === true;
481
+ const stopActiveChild = () => {
482
+ try { activeChild?.kill("SIGTERM"); } catch { /* Rationale: cancellation continues to terminal restoration. */ }
483
+ if (activeChild && killTimer === null) {
484
+ killTimer = setTimeout(() => {
485
+ try { activeChild?.kill("SIGKILL"); } catch { /* Rationale: the owned child may already have exited. */ }
486
+ }, 5_000);
487
+ killTimer.unref?.();
488
+ }
489
+ };
490
+ const setActiveChild = child => {
491
+ activeChild = child;
492
+ if (child === null && killTimer !== null) { clearTimeout(killTimer); killTimer = null; }
493
+ else if (child !== null && cancelled) stopActiveChild();
494
+ };
495
+ const cancel = () => {
496
+ cancelled = true;
497
+ stopActiveChild();
498
+ };
499
+ if (options.handleSignals !== false) {
500
+ process.once("SIGINT", cancel);
501
+ process.once("SIGTERM", cancel);
502
+ }
503
+ options.signal?.addEventListener("abort", cancel, { once: true });
504
+
505
+ try {
506
+ progress.set(3, "Preparing", 10);
507
+ let phase = "Resolving version";
508
+ const resolveResult = await runner.npm([
509
+ "view", targetSpec(parsed.target), "version", "--json", "--loglevel=verbose", "--no-fund", "--no-audit",
510
+ ], {
511
+ onChild: setActiveChild,
512
+ onStderrLine: line => { phase = classifyProgressLine(line, phase); progress.set(10, phase, 18); },
513
+ });
514
+ setActiveChild(null);
515
+ if (cancelled) throw new InstallationCancelled();
516
+ if (resolveResult.code !== 0) throw new InstallationFailure(conciseFailure("resolve", resolveResult.stderr), resolveResult.stderr);
517
+ const targetVersion = parseResolvedVersion(resolveResult.stdout, parsed.target);
518
+ progress.set(18, "Resolving packages", 24);
519
+
520
+ const rootResult = await runner.npm(["root", "--global", "--loglevel=error", "--no-fund", "--no-audit"], { onChild: setActiveChild });
521
+ setActiveChild(null);
522
+ if (cancelled) throw new InstallationCancelled();
523
+ const globalRoot = rootResult.stdout.trim();
524
+ if (rootResult.code !== 0 || !globalRoot || !isAbsolute(globalRoot)) {
525
+ throw new InstallationFailure("the npm global installation cannot be verified", rootResult.stderr || rootResult.stdout);
526
+ }
527
+ const packageRoot = packageRootFor(globalRoot);
528
+ const existing = await readExistingManifest(packageRoot);
529
+ if (existing !== null && existing.name !== APPLICATION_PACKAGE) {
530
+ throw new InstallationFailure("existing installation ownership could not be verified");
531
+ }
532
+
533
+ if (existing !== null) {
534
+ const cli = resolve(packageRoot, "bin", "cli.js");
535
+ try {
536
+ await access(cli);
537
+ const existingLaunchers = await verifyLaunchers(globalRoot, platform);
538
+ await verifyCommandResolution(existingLaunchers, environment, platform);
539
+ } catch (error) {
540
+ if (error instanceof InstallationFailure && error.reason === "another command takes precedence") throw error;
541
+ throw new InstallationFailure("existing installation ownership could not be verified", error instanceof Error ? error.message : String(error));
542
+ }
543
+ progress.set(24, "Installing", 90);
544
+ const updateResult = await runner.node(cli, updateArguments(parsed.target), { onChild: setActiveChild });
545
+ setActiveChild(null);
546
+ if (cancelled) throw new InstallationCancelled();
547
+ if (updateResult.code !== 0) {
548
+ throw new InstallationFailure(conciseFailure("update", `${updateResult.stderr}\n${updateResult.stdout}`), `${updateResult.stderr}\n${updateResult.stdout}`, updateResult.code ?? 1);
549
+ }
550
+ } else {
551
+ await assertLaunchersAbsent(globalRoot, platform);
552
+ let installPhase = "Resolving packages";
553
+ progress.set(24, installPhase, 70);
554
+ const installResult = await runner.npm([
555
+ "install", "--global", "--loglevel=verbose", "--ignore-scripts", "--no-fund", "--no-audit", "--progress=false",
556
+ `${APPLICATION_PACKAGE}@${targetVersion}`,
557
+ ], {
558
+ onChild: setActiveChild,
559
+ onStdoutLine: line => { installPhase = classifyProgressLine(line, installPhase); progress.set(24, installPhase, 70); },
560
+ onStderrLine: line => { installPhase = classifyProgressLine(line, installPhase); progress.set(24, installPhase, 70); },
561
+ });
562
+ setActiveChild(null);
563
+ if (cancelled) throw new InstallationCancelled();
564
+ if (installResult.code !== 0) {
565
+ throw new InstallationFailure(conciseFailure("install", `${installResult.stderr}\n${installResult.stdout}`), `${installResult.stderr}\n${installResult.stdout}`, installResult.code ?? 1);
566
+ }
567
+ progress.set(72, "Installing", 76);
568
+ await verifyInstalledPackage(packageRoot, targetVersion);
569
+ await activateInstalled(runner, packageRoot, targetVersion, progress, setActiveChild, () => cancelled);
570
+ }
571
+
572
+ progress.set(98, "Verifying", 99);
573
+ await verifyInstalledPackage(packageRoot, targetVersion);
574
+ const launchers = await verifyLaunchers(globalRoot, platform);
575
+ await verifyCommandResolution(launchers, environment, platform);
576
+ if (cancelled) throw new InstallationCancelled();
577
+ progress.finish();
578
+ stdout.write("a1 successfully installed\n");
579
+ return 0;
580
+ } catch (error) {
581
+ progress.clear();
582
+ if (cancelled || error instanceof InstallationCancelled) {
583
+ stderr.write("installation cancelled\n");
584
+ return 130;
585
+ }
586
+ const failure = error instanceof InstallationFailure
587
+ ? error
588
+ : new InstallationFailure("unexpected installer failure", error instanceof Error ? error.message : String(error));
589
+ stderr.write(`installation failed: ${failure.reason}\n`);
590
+ if (parsed.verbose) {
591
+ const details = sanitizeDiagnostic(failure.details);
592
+ if (details) stderr.write(`Details: ${details}\n`);
593
+ }
594
+ return failure.exitCode;
595
+ } finally {
596
+ progress.clear();
597
+ if (options.handleSignals !== false) {
598
+ process.removeListener("SIGINT", cancel);
599
+ process.removeListener("SIGTERM", cancel);
600
+ }
601
+ options.signal?.removeEventListener("abort", cancel);
602
+ if (killTimer !== null) clearTimeout(killTimer);
603
+ }
604
+ }
605
+
606
+ export async function main(argv = process.argv.slice(2)) {
607
+ try {
608
+ return await runInstaller(argv);
609
+ } catch (error) {
610
+ const failure = error instanceof InstallationFailure
611
+ ? error
612
+ : new InstallationFailure("unexpected installer failure", error instanceof Error ? error.message : String(error));
613
+ process.stderr.write(`installation failed: ${failure.reason}\n`);
614
+ return failure.exitCode;
615
+ }
616
+ }
617
+
618
+ /** Return whether Node entered through this module or a symlink to it. */
619
+ function isDirectInvocation(entry, moduleUrl) {
620
+ if (!entry) return false;
621
+ const entryPath = resolve(entry);
622
+ try {
623
+ return realpathSync(entryPath) === realpathSync(fileURLToPath(moduleUrl));
624
+ } catch {
625
+ return pathToFileURL(entryPath).href === moduleUrl;
626
+ }
627
+ }
628
+
629
+ if (isDirectInvocation(process.argv[1], import.meta.url)) process.exitCode = await main();
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@timurproko/a1-install",
3
+ "version": "0.2.1-dev.599",
4
+ "description": "Dependency-free installer for a1",
5
+ "type": "module",
6
+ "bin": {
7
+ "a1-install": "bin/a1-install.js"
8
+ },
9
+ "files": [
10
+ "bin/a1-install.js",
11
+ "README.md",
12
+ "LICENSE"
13
+ ],
14
+ "engines": {
15
+ "node": ">=22.19.0 <25"
16
+ },
17
+ "publishConfig": {
18
+ "access": "public"
19
+ },
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/timurproko/a1"
23
+ },
24
+ "author": "Timur Prokopiev",
25
+ "license": "MIT"
26
+ }