@yuanchuan/aivo 0.14.1 → 0.14.3

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
@@ -2,25 +2,39 @@
2
2
 
3
3
  const { spawn } = require("node:child_process");
4
4
  const os = require("node:os");
5
- const { getInstalledBinaryPath } = require("../lib/paths");
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
- const child = spawn(binaryPath, process.argv.slice(2), {
10
- stdio: "inherit"
11
- });
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
+ }
12
17
 
13
- child.on("exit", (code, signal) => {
14
- if (signal) {
15
- process.exitCode = 128 + (os.constants.signals[signal] || 1);
16
- return;
17
- }
18
+ process.exit(code ?? 1);
19
+ });
20
+ }
18
21
 
19
- process.exit(code ?? 1);
20
- });
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
+ });
21
34
 
22
- child.on("error", () => {
23
- console.error("aivo binary is not installed.");
24
- console.error("Reinstall with: npm install -g @yuanchuan/aivo");
25
- process.exit(1);
26
- });
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.1",
3
+ "version": "0.14.3",
4
4
  "description": "npm wrapper for the aivo CLI",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -11,17 +11,22 @@ const { resolvePlatformAsset } = require("../lib/platform");
11
11
  const pkg = require(path.join(getPackageRoot(), "package.json"));
12
12
 
13
13
  const MAX_REDIRECTS = 5;
14
+ const REPAIR_COMMAND = "npm install -g @yuanchuan/aivo@latest";
14
15
 
15
- async function main() {
16
- if (process.env.AIVO_SKIP_POSTINSTALL === "1") {
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") {
17
24
  return;
18
25
  }
19
26
 
20
- const { assetName } = resolvePlatformAsset();
27
+ const { assetName } = resolvePlatformAsset(platform, arch);
21
28
  const version = pkg.version;
22
- const baseUrl =
23
- process.env.AIVO_INSTALL_BASE_URL ||
24
- `https://github.com/yuanchuan/aivo/releases/download/v${version}`;
29
+ const baseUrl = env.AIVO_INSTALL_BASE_URL || getReleaseBaseUrl(version);
25
30
  const checksumUrl = `${baseUrl}/${assetName}.sha256`;
26
31
  const binaryUrl = `${baseUrl}/${assetName}`;
27
32
 
@@ -41,21 +46,70 @@ async function main() {
41
46
  }
42
47
 
43
48
  const nativeDir = getNativeDir();
44
- const binaryPath = getInstalledBinaryPath();
45
- fs.mkdirSync(nativeDir, { recursive: true });
46
- fs.writeFileSync(binaryPath, binary);
49
+ const binaryPath = getInstalledBinaryPath(platform, arch);
50
+ installBinary({
51
+ binary,
52
+ binaryPath,
53
+ nativeDir,
54
+ platform,
55
+ fsImpl
56
+ });
47
57
 
48
- if (process.platform !== "win32") {
49
- 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.");
50
61
  }
51
-
52
- console.log(`Installed aivo ${version} (${assetName})`);
53
62
  }
54
63
 
55
64
  function downloadText(url) {
56
65
  return downloadBuffer(url).then((buffer) => buffer.toString("utf8"));
57
66
  }
58
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
+
59
113
  function downloadBuffer(url, redirectCount = 0) {
60
114
  return new Promise((resolve, reject) => {
61
115
  const proto = url.startsWith("https://") ? https : require("node:http");
@@ -106,7 +160,19 @@ function downloadBuffer(url, redirectCount = 0) {
106
160
  });
107
161
  }
108
162
 
109
- main().catch((error) => {
110
- console.error(error.message);
111
- process.exitCode = 1;
112
- });
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
+ };