@yuanchuan/aivo 0.14.0 → 0.14.2

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/README.md CHANGED
@@ -9,17 +9,18 @@ npm install -g @yuanchuan/aivo
9
9
  ```
10
10
 
11
11
  This package downloads the matching prebuilt binary for your platform during install.
12
+ If `aivo` is not recognized right away on Windows, open a new terminal and try again.
12
13
 
13
14
  ## Update
14
15
 
15
16
  ```bash
16
- npm install -g @yuanchuan/aivo@latest
17
+ aivo update
17
18
  ```
18
19
 
19
- Or:
20
+ If the npm-managed install needs repair, run:
20
21
 
21
22
  ```bash
22
- npm update -g @yuanchuan/aivo
23
+ npm install -g @yuanchuan/aivo@latest
23
24
  ```
24
25
 
25
26
  ## Usage
package/bin/aivo.js CHANGED
@@ -1,31 +1,40 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { spawn } = require("node:child_process");
4
- const fs = require("node:fs");
5
- const { getInstalledBinaryPath } = require("../lib/paths");
4
+ const os = require("node:os");
5
+ const { getInstalledBinaryPath, getPackageRoot } = require("../lib/paths");
6
+ const { formatLaunchError } = require("../lib/launcher");
7
+ const { shouldDelegateWindowsNpmUpdate, spawnWindowsNpmUpdate } = require("../lib/update");
6
8
 
7
- const binaryPath = getInstalledBinaryPath();
9
+ const args = process.argv.slice(2);
8
10
 
9
- if (!fs.existsSync(binaryPath)) {
10
- console.error("aivo binary is not installed.");
11
- console.error("Reinstall with: npm install -g @yuanchuan/aivo");
12
- process.exit(1);
13
- }
14
-
15
- const child = spawn(binaryPath, process.argv.slice(2), {
16
- stdio: "inherit"
17
- });
11
+ function forwardExit(child) {
12
+ child.on("exit", (code, signal) => {
13
+ if (signal) {
14
+ process.exitCode = 128 + (os.constants.signals[signal] || 1);
15
+ return;
16
+ }
18
17
 
19
- child.on("exit", (code, signal) => {
20
- if (signal) {
21
- process.kill(process.pid, signal);
22
- return;
23
- }
18
+ process.exit(code ?? 1);
19
+ });
20
+ }
24
21
 
25
- process.exit(code ?? 1);
26
- });
22
+ if (shouldDelegateWindowsNpmUpdate(args, { packageRoot: getPackageRoot() })) {
23
+ const child = spawnWindowsNpmUpdate(spawn);
24
+ forwardExit(child);
25
+ child.on("error", (error) => {
26
+ console.error(`Failed to launch npm.cmd for update: ${error.message}`);
27
+ process.exit(1);
28
+ });
29
+ } else {
30
+ const binaryPath = getInstalledBinaryPath();
31
+ const child = spawn(binaryPath, args, {
32
+ stdio: "inherit"
33
+ });
27
34
 
28
- child.on("error", (error) => {
29
- console.error(`Failed to launch aivo: ${error.message}`);
30
- process.exit(1);
31
- });
35
+ forwardExit(child);
36
+ child.on("error", (error) => {
37
+ console.error(formatLaunchError(error, binaryPath));
38
+ process.exit(1);
39
+ });
40
+ }
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ const REPAIR_COMMAND = "npm install -g @yuanchuan/aivo@latest";
4
+
5
+ function formatLaunchError(error, binaryPath, platform = process.platform) {
6
+ const lines = [
7
+ `aivo binary is not available at ${binaryPath}.`,
8
+ "This usually means the npm postinstall download did not complete.",
9
+ "If you installed with --ignore-scripts, reinstall without it.",
10
+ `Repair with: ${REPAIR_COMMAND}`
11
+ ];
12
+
13
+ if (error && error.code) {
14
+ lines.push(`Launch error: ${error.code}`);
15
+ }
16
+
17
+ if (platform === "win32") {
18
+ lines.push("If you just installed aivo, open a new terminal and try again.");
19
+ }
20
+
21
+ return lines.join("\n");
22
+ }
23
+
24
+ module.exports = {
25
+ REPAIR_COMMAND,
26
+ formatLaunchError
27
+ };
package/lib/update.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+
3
+ const WINDOWS_NPM_UPDATE_COMMAND = "npm.cmd install -g @yuanchuan/aivo@latest";
4
+
5
+ function normalizePathForMatch(value) {
6
+ return String(value || "")
7
+ .replace(/\\/g, "/")
8
+ .replace(/^\/\/\?\//, "")
9
+ .toLowerCase();
10
+ }
11
+
12
+ function isNpmManagedPackageRoot(packageRoot) {
13
+ const normalized = normalizePathForMatch(packageRoot);
14
+ return normalized.includes("/node_modules/@yuanchuan/aivo");
15
+ }
16
+
17
+ function shouldDelegateWindowsNpmUpdate(argv, options = {}) {
18
+ const platform = options.platform || process.platform;
19
+ const packageRoot = options.packageRoot;
20
+
21
+ if (platform !== "win32") {
22
+ return false;
23
+ }
24
+
25
+ if (!isNpmManagedPackageRoot(packageRoot)) {
26
+ return false;
27
+ }
28
+
29
+ if (!Array.isArray(argv) || argv[0] !== "update") {
30
+ return false;
31
+ }
32
+
33
+ const rest = argv.slice(1);
34
+ if (rest.length === 0) {
35
+ return true;
36
+ }
37
+
38
+ return rest.every((arg) => arg === "--no-color");
39
+ }
40
+
41
+ function spawnWindowsNpmUpdate(spawnImpl, options = {}) {
42
+ const comspec = options.comspec || process.env.ComSpec || process.env.COMSPEC || "cmd.exe";
43
+ return spawnImpl(comspec, ["/d", "/s", "/c", WINDOWS_NPM_UPDATE_COMMAND], {
44
+ stdio: "inherit"
45
+ });
46
+ }
47
+
48
+ module.exports = {
49
+ WINDOWS_NPM_UPDATE_COMMAND,
50
+ isNpmManagedPackageRoot,
51
+ normalizePathForMatch,
52
+ shouldDelegateWindowsNpmUpdate,
53
+ spawnWindowsNpmUpdate
54
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yuanchuan/aivo",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "npm wrapper for the aivo CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -10,56 +10,116 @@ const { resolvePlatformAsset } = require("../lib/platform");
10
10
 
11
11
  const pkg = require(path.join(getPackageRoot(), "package.json"));
12
12
 
13
- async function main() {
14
- if (process.env.AIVO_SKIP_POSTINSTALL === "1") {
13
+ const MAX_REDIRECTS = 5;
14
+ const REPAIR_COMMAND = "npm install -g @yuanchuan/aivo@latest";
15
+
16
+ async function main(options = {}) {
17
+ const platform = options.platform || process.platform;
18
+ const arch = options.arch || process.arch;
19
+ const env = options.env || process.env;
20
+ const fsImpl = options.fsImpl || fs;
21
+ const logger = options.logger || console;
22
+
23
+ if (env.AIVO_SKIP_POSTINSTALL === "1") {
15
24
  return;
16
25
  }
17
26
 
18
- const { assetName } = resolvePlatformAsset();
27
+ const { assetName } = resolvePlatformAsset(platform, arch);
19
28
  const version = pkg.version;
20
- const baseUrl =
21
- process.env.AIVO_INSTALL_BASE_URL ||
22
- `https://github.com/yuanchuan/aivo/releases/download/v${version}`;
29
+ const baseUrl = env.AIVO_INSTALL_BASE_URL || getReleaseBaseUrl(version);
23
30
  const checksumUrl = `${baseUrl}/${assetName}.sha256`;
24
31
  const binaryUrl = `${baseUrl}/${assetName}`;
25
32
 
26
- const checksumText = await downloadText(checksumUrl);
27
- const expectedSha = parseChecksumText(checksumText, assetName);
33
+ const [checksumText, binary] = await Promise.all([
34
+ downloadText(checksumUrl),
35
+ downloadBuffer(binaryUrl)
36
+ ]);
28
37
 
38
+ const expectedSha = parseChecksumText(checksumText, assetName);
29
39
  if (!expectedSha) {
30
40
  throw new Error(`Checksum asset for ${assetName} could not be parsed.`);
31
41
  }
32
42
 
33
- const binary = await downloadBuffer(binaryUrl);
34
43
  const actualSha = sha256(binary);
35
44
  if (actualSha !== expectedSha) {
36
45
  throw new Error(`Checksum verification failed for ${assetName}.`);
37
46
  }
38
47
 
39
48
  const nativeDir = getNativeDir();
40
- const binaryPath = getInstalledBinaryPath();
41
- fs.mkdirSync(nativeDir, { recursive: true });
42
- fs.writeFileSync(binaryPath, binary);
49
+ const binaryPath = getInstalledBinaryPath(platform, arch);
50
+ installBinary({
51
+ binary,
52
+ binaryPath,
53
+ nativeDir,
54
+ platform,
55
+ fsImpl
56
+ });
43
57
 
44
- if (process.platform !== "win32") {
45
- fs.chmodSync(binaryPath, 0o755);
58
+ logger.log(`Installed aivo ${version} (${assetName})`);
59
+ if (platform === "win32") {
60
+ logger.log("If `aivo` is not recognized yet, open a new terminal and try again.");
46
61
  }
47
-
48
- console.log(`Installed aivo ${version} (${assetName})`);
49
62
  }
50
63
 
51
64
  function downloadText(url) {
52
65
  return downloadBuffer(url).then((buffer) => buffer.toString("utf8"));
53
66
  }
54
67
 
68
+ function getReleaseBaseUrl(version) {
69
+ return `https://github.com/yuanchuan/aivo/releases/download/v${version}`;
70
+ }
71
+
72
+ function installBinary({ binary, binaryPath, nativeDir, platform, fsImpl = fs }) {
73
+ const tempPath = path.join(
74
+ nativeDir,
75
+ `${path.basename(binaryPath)}.tmp-${process.pid}-${Date.now()}`
76
+ );
77
+
78
+ fsImpl.mkdirSync(nativeDir, { recursive: true });
79
+
80
+ try {
81
+ fsImpl.writeFileSync(tempPath, binary);
82
+ if (platform !== "win32") {
83
+ fsImpl.chmodSync(tempPath, 0o755);
84
+ }
85
+ fsImpl.renameSync(tempPath, binaryPath);
86
+ } catch (error) {
87
+ cleanupTempFile(fsImpl, tempPath);
88
+ throw new Error(`Failed to install ${path.basename(binaryPath)}: ${error.message}`);
89
+ }
90
+ }
91
+
92
+ function cleanupTempFile(fsImpl, tempPath) {
93
+ if (typeof fsImpl.rmSync === "function") {
94
+ fsImpl.rmSync(tempPath, { force: true });
95
+ return;
96
+ }
97
+
98
+ try {
99
+ fsImpl.unlinkSync(tempPath);
100
+ } catch {
101
+ // ignore cleanup failures
102
+ }
103
+ }
104
+
105
+ function formatInstallError(error, platform = process.platform) {
106
+ const lines = [error.message, `Repair with: ${REPAIR_COMMAND}`];
107
+ if (platform === "win32") {
108
+ lines.push("If you just installed aivo, open a new terminal and try again.");
109
+ }
110
+ return lines.join("\n");
111
+ }
112
+
55
113
  function downloadBuffer(url, redirectCount = 0) {
56
114
  return new Promise((resolve, reject) => {
57
- const request = https.get(
115
+ const proto = url.startsWith("https://") ? https : require("node:http");
116
+ const request = proto.get(
58
117
  url,
59
118
  {
60
119
  headers: {
61
120
  "User-Agent": "@yuanchuan/aivo-installer"
62
- }
121
+ },
122
+ timeout: 30_000
63
123
  },
64
124
  (response) => {
65
125
  const status = response.statusCode || 0;
@@ -68,10 +128,15 @@ function downloadBuffer(url, redirectCount = 0) {
68
128
  status >= 300 &&
69
129
  status < 400 &&
70
130
  response.headers.location &&
71
- redirectCount < 5
131
+ redirectCount < MAX_REDIRECTS
72
132
  ) {
73
133
  response.resume();
74
- resolve(downloadBuffer(response.headers.location, redirectCount + 1));
134
+ const location = response.headers.location;
135
+ if (!location.startsWith("https://") && !location.startsWith("http://")) {
136
+ reject(new Error(`Invalid redirect URL: ${location}`));
137
+ return;
138
+ }
139
+ resolve(downloadBuffer(location, redirectCount + 1));
75
140
  return;
76
141
  }
77
142
 
@@ -87,11 +152,27 @@ function downloadBuffer(url, redirectCount = 0) {
87
152
  }
88
153
  );
89
154
 
155
+ request.on("timeout", () => {
156
+ request.destroy();
157
+ reject(new Error(`Download timed out: ${url}`));
158
+ });
90
159
  request.on("error", reject);
91
160
  });
92
161
  }
93
162
 
94
- main().catch((error) => {
95
- console.error(error.message);
96
- process.exitCode = 1;
97
- });
163
+ if (require.main === module) {
164
+ main().catch((error) => {
165
+ console.error(formatInstallError(error));
166
+ process.exitCode = 1;
167
+ });
168
+ }
169
+
170
+ module.exports = {
171
+ REPAIR_COMMAND,
172
+ downloadBuffer,
173
+ downloadText,
174
+ formatInstallError,
175
+ getReleaseBaseUrl,
176
+ installBinary,
177
+ main
178
+ };