@gaiaworks/gaia-cli 0.0.5 → 0.0.7

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
@@ -1,30 +1,70 @@
1
- # @gaiaworks/gaia-cli
1
+ # @gaiaworks/gaia-cli
2
+
3
+ 这是 `gaia-cli` 的 npm 分发壳包,用于通过 `npx` 下载并运行对应版本的 Go 二进制。
4
+
5
+ ## 使用方式
6
+
7
+ 临时执行:
8
+
9
+ ```bash
10
+ npx @gaiaworks/gaia-cli version
11
+ ```
12
+
13
+ 全局安装:
14
+
15
+ ```bash
16
+ npx @gaiaworks/gaia-cli@latest install
17
+ gaia version
18
+ ```
19
+
20
+ 安装模式:
21
+
22
+ ```bash
23
+ npx @gaiaworks/gaia-cli@latest install
24
+ npx @gaiaworks/gaia-cli@latest install --mode external
25
+ npx @gaiaworks/gaia-cli@latest install --mode internal
26
+ ```
27
+
28
+ 未指定 `--mode` 时默认写入 `external`。安装脚本会执行 `gaia setup --mode <external|internal>`,用于初始化本地 `install_mode` 和 Agent Skills。
29
+
30
+ 安装时会根据 npm 包版本,从 OSS 的同版本目录下载当前平台对应的 `gaia-cli` 二进制。
2
31
 
3
- 这是 `gaia-cli` 的 npm 分发壳包,用于通过 `npx` 下载并运行对应版本的 Go 二进制。
32
+ 升级到 npm `latest` 并保留当前安装模式:
4
33
 
5
- ## 使用方式
34
+ ```bash
35
+ gaia update
36
+ ```
6
37
 
7
- 临时执行:
38
+ 仅检查或安装指定版本:
8
39
 
9
40
  ```bash
10
- npx @gaiaworks/gaia-cli version
41
+ gaia update --check
42
+ gaia update --version 0.0.7
11
43
  ```
12
44
 
13
- 全局安装:
45
+ 更新命令拒绝降级。首个包含该命令的版本仍需通过 `npx ... install` 手工升级一次。
46
+
47
+ 查看脱敏后的当前有效配置,并诊断配置、端点、登录凭据及 Internal 远程授权服务:
14
48
 
15
49
  ```bash
16
- npx @gaiaworks/gaia-cli@latest install
17
- gaia version
50
+ gaia config show
51
+ gaia doctor
52
+ gaia doctor --json
18
53
  ```
19
54
 
20
- 安装模式:
55
+ `config show` 不输出 Token 或 Client Secret 明文。未登录属于警告;配置非法、必填端点缺失或远程授权服务健康检查失败时,`doctor` 返回非零退出码。
56
+
57
+ 卸载全局命令和 Gaia 管理的 Agent Skills,默认保留本地配置、凭据和目标信息:
21
58
 
22
59
  ```bash
23
- npx @gaiaworks/gaia-cli@latest install
24
- npx @gaiaworks/gaia-cli@latest install --mode external
25
- npx @gaiaworks/gaia-cli@latest install --mode internal
60
+ gaia uninstall
26
61
  ```
27
62
 
28
- 未指定 `--mode` 时默认写入 `external`。安装脚本会执行 `gaia setup --mode <external|internal>`,用于初始化本地 `install_mode` 和 Agent Skills。
63
+ 需要同时删除全部本地 Gaia 数据时使用:
29
64
 
30
- 安装时会根据 npm 包版本,从 OSS 的同版本目录下载当前平台对应的 `gaia-cli` 二进制。
65
+ ```bash
66
+ gaia uninstall --purge
67
+ gaia uninstall --purge --yes
68
+ ```
69
+
70
+ `--purge` 删除 `GAIA_CONFIG_DIR` 指向的目录;未设置该环境变量时删除 `~/.gaia`。默认会要求确认,自动化场景可显式添加 `--yes`。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gaiaworks/gaia-cli",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "JavaScript launcher for gaia-cli.",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,
@@ -1,189 +1,206 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { spawnSync } = require('child_process');
7
- const { binaryName, install } = require('./install');
8
-
9
- const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
10
- const packageSpec = `${packageJson.name}@${packageJson.version}`;
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+ const { binaryName, install } = require('./install');
8
+
9
+ const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'));
10
+ const packageSpec = `${packageJson.name}@${packageJson.version}`;
11
11
  const binaryPath = path.join(__dirname, '..', 'bin', binaryName());
12
12
  const shellPathMarker = '# Added by gaia-cli installer';
13
13
 
14
- function parseInstallArgs(args) {
15
- let mode = 'external';
16
- let explicitMode = false;
17
- const npmArgs = [];
18
-
19
- for (let i = 0; i < args.length; i += 1) {
20
- const arg = args[i];
21
- if (arg === '--mode') {
22
- const value = args[i + 1];
23
- if (!value) {
24
- throw new Error('--mode requires a value: external or internal');
25
- }
26
- mode = value;
27
- explicitMode = true;
28
- i += 1;
29
- continue;
30
- }
31
- if (arg.startsWith('--mode=')) {
32
- mode = arg.slice('--mode='.length);
33
- explicitMode = true;
34
- continue;
35
- }
36
- npmArgs.push(arg);
37
- }
38
-
39
- if (mode !== 'external' && mode !== 'internal') {
40
- throw new Error(`Unsupported mode: ${mode}. Expected external or internal.`);
41
- }
42
-
43
- return { explicitMode, mode, npmArgs };
44
- }
45
-
46
- function run(command, args, options = {}) {
47
- const result = spawnSync(command, args, {
48
- stdio: options.stdio || 'inherit',
49
- encoding: 'utf8',
50
- });
51
-
52
- if (result.error) {
53
- console.error(`[gaia-cli] Failed to run ${command}: ${result.error.message}`);
54
- process.exit(1);
55
- }
56
-
57
- if (result.status !== 0) {
58
- process.exit(typeof result.status === 'number' ? result.status : 1);
14
+ function npmInvocation(args) {
15
+ if (process.platform === 'win32') {
16
+ const npmCli = process.env.npm_execpath || path.join(
17
+ path.dirname(process.execPath),
18
+ 'node_modules',
19
+ 'npm',
20
+ 'bin',
21
+ 'npm-cli.js'
22
+ );
23
+ return { command: process.execPath, args: [npmCli, ...args] };
59
24
  }
25
+ return { command: 'npm', args };
60
26
  }
61
-
62
- function runCapture(command, args) {
63
- return spawnSync(command, args, {
64
- stdio: ['ignore', 'pipe', 'pipe'],
65
- encoding: 'utf8',
66
- });
67
- }
68
-
27
+
28
+ function parseInstallArgs(args) {
29
+ let mode = 'external';
30
+ let explicitMode = false;
31
+ const npmArgs = [];
32
+
33
+ for (let i = 0; i < args.length; i += 1) {
34
+ const arg = args[i];
35
+ if (arg === '--mode') {
36
+ const value = args[i + 1];
37
+ if (!value) {
38
+ throw new Error('--mode requires a value: external or internal');
39
+ }
40
+ mode = value;
41
+ explicitMode = true;
42
+ i += 1;
43
+ continue;
44
+ }
45
+ if (arg.startsWith('--mode=')) {
46
+ mode = arg.slice('--mode='.length);
47
+ explicitMode = true;
48
+ continue;
49
+ }
50
+ npmArgs.push(arg);
51
+ }
52
+
53
+ if (mode !== 'external' && mode !== 'internal') {
54
+ throw new Error(`Unsupported mode: ${mode}. Expected external or internal.`);
55
+ }
56
+
57
+ return { explicitMode, mode, npmArgs };
58
+ }
59
+
60
+ function run(command, args, options = {}) {
61
+ const result = spawnSync(command, args, {
62
+ stdio: options.stdio || 'inherit',
63
+ encoding: 'utf8',
64
+ });
65
+
66
+ if (result.error) {
67
+ console.error(`[gaia-cli] Failed to run ${command}: ${result.error.message}`);
68
+ process.exit(1);
69
+ }
70
+
71
+ if (result.status !== 0) {
72
+ process.exit(typeof result.status === 'number' ? result.status : 1);
73
+ }
74
+ }
75
+
76
+ function runCapture(command, args) {
77
+ return spawnSync(command, args, {
78
+ stdio: ['ignore', 'pipe', 'pipe'],
79
+ encoding: 'utf8',
80
+ });
81
+ }
82
+
69
83
  function npmGlobalBinDir() {
70
- const result = runCapture('npm', ['prefix', '-g']);
71
- if (result.error || result.status !== 0) return null;
72
- const prefix = result.stdout.trim();
73
- if (!prefix) return null;
74
- return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
75
- }
76
-
77
- function npmGlobalCommandPath(binDir, platform = process.platform) {
84
+ const npm = npmInvocation(['prefix', '-g']);
85
+ const result = runCapture(npm.command, npm.args);
86
+ if (result.error || result.status !== 0) return null;
87
+ const prefix = result.stdout.trim();
88
+ if (!prefix) return null;
89
+ return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
90
+ }
91
+
92
+ function npmGlobalCommandPath(binDir, platform = process.platform) {
93
+ if (!binDir) return null;
94
+ if (platform === 'win32') return path.win32.join(binDir, 'gaia.cmd');
95
+ return path.posix.join(binDir, 'gaia');
96
+ }
97
+
98
+ function pathContains(dir) {
99
+ if (!dir) return false;
100
+ const entries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
101
+ return entries.some((entry) => path.resolve(entry) === path.resolve(dir));
102
+ }
103
+
104
+ function shellProfilePath(env = process.env) {
105
+ if (process.platform === 'win32') return null;
106
+ const home = env.HOME;
107
+ if (!home) return null;
108
+ const shell = path.basename(env.SHELL || '');
109
+ if (shell === 'zsh') return path.join(home, '.zshrc');
110
+ if (shell === 'bash') return path.join(home, '.bashrc');
111
+ return path.join(home, '.profile');
112
+ }
113
+
114
+ function ensureWindowsPathConfigured(binDir) {
115
+ if (!binDir || process.platform !== 'win32' || pathContains(binDir)) return null;
116
+ const command = [
117
+ "$dir = $args[0]",
118
+ "$current = [Environment]::GetEnvironmentVariable('Path', 'User')",
119
+ "if (-not $current) { $current = '' }",
120
+ "$parts = $current -split ';' | Where-Object { $_ }",
121
+ "if ($parts -notcontains $dir) {",
122
+ " $next = (@($dir) + $parts) -join ';'",
123
+ " [Environment]::SetEnvironmentVariable('Path', $next, 'User')",
124
+ "}",
125
+ ].join("; ");
126
+ const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command, binDir], {
127
+ stdio: ["ignore", "pipe", "pipe"],
128
+ encoding: "utf8",
129
+ });
130
+ if (result.error || result.status !== 0) {
131
+ const detail = result.error ? result.error.message : `${result.stdout || ""}${result.stderr || ""}`.trim();
132
+ throw new Error(`Failed to update user PATH: ${detail}`);
133
+ }
134
+ return "Windows user PATH";
135
+ }
136
+
137
+ function pathExportBlock(binDir) {
138
+ return `${shellPathMarker}\nexport PATH="${binDir}:$PATH"\n`;
139
+ }
140
+
141
+ function ensurePathConfigured(binDir, profilePath = shellProfilePath(), platform = process.platform) {
78
142
  if (!binDir) return null;
79
- if (platform === 'win32') return path.win32.join(binDir, 'gaia.cmd');
80
- return path.posix.join(binDir, 'gaia');
81
- }
82
-
83
- function pathContains(dir) {
84
- if (!dir) return false;
85
- const entries = (process.env.PATH || '').split(path.delimiter).filter(Boolean);
86
- return entries.some((entry) => path.resolve(entry) === path.resolve(dir));
87
- }
88
-
89
- function shellProfilePath(env = process.env) {
90
- if (process.platform === 'win32') return null;
91
- const home = env.HOME;
92
- if (!home) return null;
93
- const shell = path.basename(env.SHELL || '');
94
- if (shell === 'zsh') return path.join(home, '.zshrc');
95
- if (shell === 'bash') return path.join(home, '.bashrc');
96
- return path.join(home, '.profile');
97
- }
98
-
99
- function ensureWindowsPathConfigured(binDir) {
100
- if (!binDir || process.platform !== 'win32' || pathContains(binDir)) return null;
101
- const command = [
102
- "$dir = $args[0]",
103
- "$current = [Environment]::GetEnvironmentVariable('Path', 'User')",
104
- "if (-not $current) { $current = '' }",
105
- "$parts = $current -split ';' | Where-Object { $_ }",
106
- "if ($parts -notcontains $dir) {",
107
- " $next = (@($dir) + $parts) -join ';'",
108
- " [Environment]::SetEnvironmentVariable('Path', $next, 'User')",
109
- "}",
110
- ].join("; ");
111
- const result = spawnSync("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", command, binDir], {
112
- stdio: ["ignore", "pipe", "pipe"],
113
- encoding: "utf8",
114
- });
115
- if (result.error || result.status !== 0) {
116
- const detail = result.error ? result.error.message : `${result.stdout || ""}${result.stderr || ""}`.trim();
117
- throw new Error(`Failed to update user PATH: ${detail}`);
118
- }
119
- return "Windows user PATH";
120
- }
121
-
122
- function pathExportBlock(binDir) {
123
- return `${shellPathMarker}\nexport PATH="${binDir}:$PATH"\n`;
124
- }
125
-
126
- function ensurePathConfigured(binDir, profilePath = shellProfilePath()) {
127
- if (!binDir) return null;
128
- if (process.platform === 'win32') {
143
+ if (platform === 'win32') {
129
144
  return ensureWindowsPathConfigured(binDir);
130
- }
131
- if (!profilePath || pathContains(binDir)) return null;
132
- const existing = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
133
- if (existing.includes(binDir) || existing.includes(shellPathMarker)) return profilePath;
134
- const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
135
- fs.appendFileSync(profilePath, `${prefix}${pathExportBlock(binDir)}`);
136
- return profilePath;
137
- }
138
-
139
- function ensureLocalBinary() {
140
- if (fs.existsSync(binaryPath)) return;
141
- install();
142
- }
143
-
144
- function installWizard(args) {
145
- const { explicitMode, mode, npmArgs } = parseInstallArgs(args);
146
- if (npmArgs.length > 0) {
147
- throw new Error(`Unsupported install arguments: ${npmArgs.join(' ')}`);
145
+ }
146
+ if (!profilePath || pathContains(binDir)) return null;
147
+ const existing = fs.existsSync(profilePath) ? fs.readFileSync(profilePath, 'utf8') : '';
148
+ if (existing.includes(binDir) || existing.includes(shellPathMarker)) return profilePath;
149
+ const prefix = existing && !existing.endsWith('\n') ? '\n' : '';
150
+ fs.appendFileSync(profilePath, `${prefix}${pathExportBlock(binDir)}`);
151
+ return profilePath;
152
+ }
153
+
154
+ function ensureLocalBinary() {
155
+ if (fs.existsSync(binaryPath)) return;
156
+ install();
157
+ }
158
+
159
+ function installWizard(args) {
160
+ const { explicitMode, mode, npmArgs } = parseInstallArgs(args);
161
+ if (npmArgs.length > 0) {
162
+ throw new Error(`Unsupported install arguments: ${npmArgs.join(' ')}`);
148
163
  }
149
164
 
150
165
  ensureLocalBinary();
151
- run('npm', ['install', '-g', packageSpec]);
152
- const globalBinDir = npmGlobalBinDir();
153
- const globalCommand = npmGlobalCommandPath(globalBinDir);
154
- const setupCommand = globalCommand && fs.existsSync(globalCommand) ? globalCommand : binaryPath;
155
- run(setupCommand, ['setup', '--mode', mode], { stdio: 'ignore' });
156
- const profilePath = ensurePathConfigured(globalBinDir);
157
- const suffix = explicitMode ? `当前模式: ${mode}。` : '';
158
- if (profilePath && globalBinDir && !pathContains(globalBinDir)) {
159
- console.error(`[gaia-cli] 安装完成,${suffix}已更新 PATH 配置: ${profilePath}`);
160
- console.error(`[gaia-cli] 重新打开终端后可以执行: gaia version`);
161
- return;
162
- }
163
- if (explicitMode) {
164
- console.error(`[gaia-cli] 安装完成,当前模式: ${mode}。可以执行: gaia version`);
165
- } else {
166
- console.error('[gaia-cli] 安装完成。可以执行: gaia version');
167
- }
168
- }
169
-
170
- if (require.main === module) {
171
- try {
172
- installWizard(process.argv.slice(2));
173
- } catch (error) {
174
- console.error(`[gaia-cli] Install failed: ${error.message}`);
175
- process.exit(1);
176
- }
177
- }
178
-
179
- module.exports = {
180
- ensurePathConfigured,
181
- ensureWindowsPathConfigured,
166
+ const npm = npmInvocation(['install', '-g', packageSpec]);
167
+ run(npm.command, npm.args);
168
+ const globalBinDir = npmGlobalBinDir();
169
+ const globalCommand = npmGlobalCommandPath(globalBinDir);
170
+ const setupCommand = globalCommand && fs.existsSync(globalCommand) ? globalCommand : binaryPath;
171
+ run(setupCommand, ['setup', '--mode', mode], { stdio: 'ignore' });
172
+ const profilePath = ensurePathConfigured(globalBinDir);
173
+ const suffix = explicitMode ? `当前模式: ${mode}。` : '';
174
+ if (profilePath && globalBinDir && !pathContains(globalBinDir)) {
175
+ console.error(`[gaia-cli] 安装完成,${suffix}已更新 PATH 配置: ${profilePath}`);
176
+ console.error(`[gaia-cli] 重新打开终端后可以执行: gaia version`);
177
+ return;
178
+ }
179
+ if (explicitMode) {
180
+ console.error(`[gaia-cli] 安装完成,当前模式: ${mode}。可以执行: gaia version`);
181
+ } else {
182
+ console.error('[gaia-cli] 安装完成。可以执行: gaia version');
183
+ }
184
+ }
185
+
186
+ if (require.main === module) {
187
+ try {
188
+ installWizard(process.argv.slice(2));
189
+ } catch (error) {
190
+ console.error(`[gaia-cli] Install failed: ${error.message}`);
191
+ process.exit(1);
192
+ }
193
+ }
194
+
195
+ module.exports = {
196
+ ensurePathConfigured,
197
+ ensureWindowsPathConfigured,
182
198
  npmGlobalBinDir,
183
199
  npmGlobalCommandPath,
184
- pathContains,
185
- pathExportBlock,
186
- shellProfilePath,
187
- installWizard,
188
- parseInstallArgs,
189
- };
200
+ npmInvocation,
201
+ pathContains,
202
+ pathExportBlock,
203
+ shellProfilePath,
204
+ installWizard,
205
+ parseInstallArgs,
206
+ };
@@ -1,177 +1,177 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const os = require('os');
6
- const path = require('path');
7
- const { spawnSync } = require('child_process');
8
-
9
- const PACKAGE_ROOT = path.resolve(__dirname, '..');
10
- const PACKAGE_JSON = path.join(PACKAGE_ROOT, 'package.json');
11
- const BIN_DIR = path.join(PACKAGE_ROOT, 'bin');
12
- const DOWNLOAD_BASE = process.env.GAIA_CLI_DOWNLOAD_BASE || 'https://assets.gaiaworkforce.com/gaia-cli/releases';
13
- const ALLOWED_DOWNLOAD_HOSTS = new Set([
14
- 'assets.gaiaworkforce.com',
15
- 'gaiafe.oss-cn-shanghai.aliyuncs.com',
16
- ]);
17
-
18
- function readPackageVersion() {
19
- const content = fs.readFileSync(PACKAGE_JSON, 'utf8');
20
- return JSON.parse(content).version;
21
- }
22
-
23
- function normalizeVersion(version) {
24
- return version.startsWith('v') ? version.slice(1) : version;
25
- }
26
-
27
- function platformName(platform = process.platform) {
28
- if (platform === 'darwin') return 'darwin';
29
- if (platform === 'linux') return 'linux';
30
- if (platform === 'win32') return 'windows';
31
- throw new Error(`Unsupported platform: ${platform}`);
32
- }
33
-
34
- function archName(arch = process.arch) {
35
- if (arch === 'x64') return 'amd64';
36
- if (arch === 'arm64') return 'arm64';
37
- throw new Error(`Unsupported architecture: ${arch}`);
38
- }
39
-
40
- function binaryName(platform = process.platform) {
41
- return platform === 'win32' ? 'gaia-cli.exe' : 'gaia-cli';
42
- }
43
-
44
- function archiveName(version, platform = process.platform, arch = process.arch) {
45
- const cleanVersion = normalizeVersion(version);
46
- return `gaia-cli-${cleanVersion}-${platformName(platform)}-${archName(arch)}.tar.gz`;
47
- }
48
-
49
- function releaseTag(version) {
50
- const cleanVersion = normalizeVersion(version);
51
- return `v${cleanVersion}`;
52
- }
53
-
54
- function downloadUrl(version, platform = process.platform, arch = process.arch) {
55
- return `${DOWNLOAD_BASE}/${releaseTag(version)}/${archiveName(version, platform, arch)}`;
56
- }
57
-
58
- function assertAllowedUrl(rawUrl) {
59
- const parsed = new URL(rawUrl);
60
- if (parsed.protocol !== 'https:') {
61
- throw new Error(`Refusing non-HTTPS download URL: ${rawUrl}`);
62
- }
63
- if (!ALLOWED_DOWNLOAD_HOSTS.has(parsed.hostname)) {
64
- throw new Error(`Refusing download host: ${parsed.hostname}`);
65
- }
66
- }
67
-
68
- function ensureDir(dir) {
69
- fs.mkdirSync(dir, { recursive: true });
70
- }
71
-
72
- function run(command, args, options = {}) {
73
- const result = spawnSync(command, args, {
74
- stdio: options.stdio || 'inherit',
75
- cwd: options.cwd || PACKAGE_ROOT,
76
- encoding: 'utf8',
77
- });
78
- if (result.error) {
79
- throw result.error;
80
- }
81
- if (result.status !== 0) {
82
- throw new Error(`${command} ${args.join(' ')} exited with code ${result.status}`);
83
- }
84
- return result;
85
- }
86
-
87
- function downloadWithRedirects(url, destination) {
88
- assertAllowedUrl(url);
89
- const tmpPath = `${destination}.tmp`;
90
- fs.rmSync(tmpPath, { force: true });
91
-
92
- run('curl', [
93
- '--fail',
94
- '--location',
95
- '--show-error',
96
- '--silent',
97
- '--output',
98
- tmpPath,
99
- url,
100
- ]);
101
-
102
- fs.renameSync(tmpPath, destination);
103
- }
104
-
105
- function extractArchive(archivePath, targetDir) {
106
- fs.rmSync(targetDir, { recursive: true, force: true });
107
- ensureDir(targetDir);
108
- run('tar', ['-xzf', archivePath, '-C', targetDir]);
109
- }
110
-
111
- function findDownloadedBinary(targetDir, platform = process.platform) {
112
- const expectedName = binaryName(platform);
113
- const direct = path.join(targetDir, expectedName);
114
- if (fs.existsSync(direct)) return direct;
115
-
116
- const pending = [targetDir];
117
- while (pending.length > 0) {
118
- const currentDir = pending.pop();
119
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
120
- for (const entry of entries) {
121
- const entryPath = path.join(currentDir, entry.name);
122
- if (entry.isDirectory()) {
123
- pending.push(entryPath);
124
- } else if (entry.isFile() && entry.name === expectedName) {
125
- return entryPath;
126
- }
127
- }
128
- }
129
- throw new Error(`Downloaded archive does not contain ${expectedName}`);
130
- }
131
-
132
- function install(options = {}) {
133
- const version = options.version || readPackageVersion();
134
- const url = options.url || downloadUrl(version);
135
- const archivePath = path.join(os.tmpdir(), archiveName(version));
136
- const unpackDir = path.join(os.tmpdir(), `gaia-cli-${process.pid}-${Date.now()}`);
137
- const finalBinary = path.join(BIN_DIR, binaryName());
138
-
139
- console.error(`[gaia-cli] Downloading ${url}`);
140
- ensureDir(BIN_DIR);
141
- downloadWithRedirects(url, archivePath);
142
- extractArchive(archivePath, unpackDir);
143
-
144
- const downloadedBinary = findDownloadedBinary(unpackDir);
145
- fs.copyFileSync(downloadedBinary, finalBinary);
146
- if (process.platform !== 'win32') {
147
- fs.chmodSync(finalBinary, 0o755);
148
- }
149
-
150
- fs.rmSync(archivePath, { force: true });
151
- fs.rmSync(unpackDir, { recursive: true, force: true });
152
- console.error(`[gaia-cli] Installed ${finalBinary}`);
153
- }
154
-
155
- if (require.main === module) {
156
- try {
157
- install();
158
- } catch (error) {
159
- console.error(`[gaia-cli] Failed to install binary: ${error.message}`);
160
- console.error('[gaia-cli] Please check network access to OSS assets or contact the administrator.');
161
- process.exit(1);
162
- }
163
- }
164
-
165
- module.exports = {
166
- ALLOWED_DOWNLOAD_HOSTS,
167
- archName,
168
- archiveName,
169
- assertAllowedUrl,
170
- binaryName,
171
- downloadUrl,
172
- findDownloadedBinary,
173
- install,
174
- normalizeVersion,
175
- platformName,
176
- releaseTag,
177
- };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const os = require('os');
6
+ const path = require('path');
7
+ const { spawnSync } = require('child_process');
8
+
9
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
10
+ const PACKAGE_JSON = path.join(PACKAGE_ROOT, 'package.json');
11
+ const BIN_DIR = path.join(PACKAGE_ROOT, 'bin');
12
+ const DOWNLOAD_BASE = process.env.GAIA_CLI_DOWNLOAD_BASE || 'https://assets.gaiaworkforce.com/gaia-cli/releases';
13
+ const ALLOWED_DOWNLOAD_HOSTS = new Set([
14
+ 'assets.gaiaworkforce.com',
15
+ 'gaiafe.oss-cn-shanghai.aliyuncs.com',
16
+ ]);
17
+
18
+ function readPackageVersion() {
19
+ const content = fs.readFileSync(PACKAGE_JSON, 'utf8');
20
+ return JSON.parse(content).version;
21
+ }
22
+
23
+ function normalizeVersion(version) {
24
+ return version.startsWith('v') ? version.slice(1) : version;
25
+ }
26
+
27
+ function platformName(platform = process.platform) {
28
+ if (platform === 'darwin') return 'darwin';
29
+ if (platform === 'linux') return 'linux';
30
+ if (platform === 'win32') return 'windows';
31
+ throw new Error(`Unsupported platform: ${platform}`);
32
+ }
33
+
34
+ function archName(arch = process.arch) {
35
+ if (arch === 'x64') return 'amd64';
36
+ if (arch === 'arm64') return 'arm64';
37
+ throw new Error(`Unsupported architecture: ${arch}`);
38
+ }
39
+
40
+ function binaryName(platform = process.platform) {
41
+ return platform === 'win32' ? 'gaia-cli.exe' : 'gaia-cli';
42
+ }
43
+
44
+ function archiveName(version, platform = process.platform, arch = process.arch) {
45
+ const cleanVersion = normalizeVersion(version);
46
+ return `gaia-cli-${cleanVersion}-${platformName(platform)}-${archName(arch)}.tar.gz`;
47
+ }
48
+
49
+ function releaseTag(version) {
50
+ const cleanVersion = normalizeVersion(version);
51
+ return `v${cleanVersion}`;
52
+ }
53
+
54
+ function downloadUrl(version, platform = process.platform, arch = process.arch) {
55
+ return `${DOWNLOAD_BASE}/${releaseTag(version)}/${archiveName(version, platform, arch)}`;
56
+ }
57
+
58
+ function assertAllowedUrl(rawUrl) {
59
+ const parsed = new URL(rawUrl);
60
+ if (parsed.protocol !== 'https:') {
61
+ throw new Error(`Refusing non-HTTPS download URL: ${rawUrl}`);
62
+ }
63
+ if (!ALLOWED_DOWNLOAD_HOSTS.has(parsed.hostname)) {
64
+ throw new Error(`Refusing download host: ${parsed.hostname}`);
65
+ }
66
+ }
67
+
68
+ function ensureDir(dir) {
69
+ fs.mkdirSync(dir, { recursive: true });
70
+ }
71
+
72
+ function run(command, args, options = {}) {
73
+ const result = spawnSync(command, args, {
74
+ stdio: options.stdio || 'inherit',
75
+ cwd: options.cwd || PACKAGE_ROOT,
76
+ encoding: 'utf8',
77
+ });
78
+ if (result.error) {
79
+ throw result.error;
80
+ }
81
+ if (result.status !== 0) {
82
+ throw new Error(`${command} ${args.join(' ')} exited with code ${result.status}`);
83
+ }
84
+ return result;
85
+ }
86
+
87
+ function downloadWithRedirects(url, destination) {
88
+ assertAllowedUrl(url);
89
+ const tmpPath = `${destination}.tmp`;
90
+ fs.rmSync(tmpPath, { force: true });
91
+
92
+ run('curl', [
93
+ '--fail',
94
+ '--location',
95
+ '--show-error',
96
+ '--silent',
97
+ '--output',
98
+ tmpPath,
99
+ url,
100
+ ]);
101
+
102
+ fs.renameSync(tmpPath, destination);
103
+ }
104
+
105
+ function extractArchive(archivePath, targetDir) {
106
+ fs.rmSync(targetDir, { recursive: true, force: true });
107
+ ensureDir(targetDir);
108
+ run('tar', ['-xzf', archivePath, '-C', targetDir]);
109
+ }
110
+
111
+ function findDownloadedBinary(targetDir, platform = process.platform) {
112
+ const expectedName = binaryName(platform);
113
+ const direct = path.join(targetDir, expectedName);
114
+ if (fs.existsSync(direct)) return direct;
115
+
116
+ const pending = [targetDir];
117
+ while (pending.length > 0) {
118
+ const currentDir = pending.pop();
119
+ const entries = fs.readdirSync(currentDir, { withFileTypes: true });
120
+ for (const entry of entries) {
121
+ const entryPath = path.join(currentDir, entry.name);
122
+ if (entry.isDirectory()) {
123
+ pending.push(entryPath);
124
+ } else if (entry.isFile() && entry.name === expectedName) {
125
+ return entryPath;
126
+ }
127
+ }
128
+ }
129
+ throw new Error(`Downloaded archive does not contain ${expectedName}`);
130
+ }
131
+
132
+ function install(options = {}) {
133
+ const version = options.version || readPackageVersion();
134
+ const url = options.url || downloadUrl(version);
135
+ const archivePath = path.join(os.tmpdir(), archiveName(version));
136
+ const unpackDir = path.join(os.tmpdir(), `gaia-cli-${process.pid}-${Date.now()}`);
137
+ const finalBinary = path.join(BIN_DIR, binaryName());
138
+
139
+ console.error(`[gaia-cli] Downloading ${url}`);
140
+ ensureDir(BIN_DIR);
141
+ downloadWithRedirects(url, archivePath);
142
+ extractArchive(archivePath, unpackDir);
143
+
144
+ const downloadedBinary = findDownloadedBinary(unpackDir);
145
+ fs.copyFileSync(downloadedBinary, finalBinary);
146
+ if (process.platform !== 'win32') {
147
+ fs.chmodSync(finalBinary, 0o755);
148
+ }
149
+
150
+ fs.rmSync(archivePath, { force: true });
151
+ fs.rmSync(unpackDir, { recursive: true, force: true });
152
+ console.error(`[gaia-cli] Installed ${finalBinary}`);
153
+ }
154
+
155
+ if (require.main === module) {
156
+ try {
157
+ install();
158
+ } catch (error) {
159
+ console.error(`[gaia-cli] Failed to install binary: ${error.message}`);
160
+ console.error('[gaia-cli] Please check network access to OSS assets or contact the administrator.');
161
+ process.exit(1);
162
+ }
163
+ }
164
+
165
+ module.exports = {
166
+ ALLOWED_DOWNLOAD_HOSTS,
167
+ archName,
168
+ archiveName,
169
+ assertAllowedUrl,
170
+ binaryName,
171
+ downloadUrl,
172
+ findDownloadedBinary,
173
+ install,
174
+ normalizeVersion,
175
+ platformName,
176
+ releaseTag,
177
+ };
package/scripts/run.js CHANGED
@@ -1,47 +1,67 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const path = require('path');
6
- const { spawnSync } = require('child_process');
7
-
8
- const { binaryName } = require('./install');
9
-
10
- const PACKAGE_ROOT = path.resolve(__dirname, '..');
11
- const BINARY_PATH = path.join(PACKAGE_ROOT, 'bin', binaryName());
12
-
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const { binaryName } = require('./install');
9
+
10
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
11
+ const BINARY_PATH = path.join(PACKAGE_ROOT, 'bin', binaryName());
12
+
13
13
  function runInstallWizard(args) {
14
- const result = spawnSync(process.execPath, [path.join(__dirname, 'install-wizard.js'), ...args], {
15
- stdio: 'inherit',
16
- });
17
- process.exit(typeof result.status === 'number' ? result.status : 1);
14
+ const result = spawnSync(process.execPath, [path.join(__dirname, 'install-wizard.js'), ...args], {
15
+ stdio: 'inherit',
16
+ });
17
+ process.exit(typeof result.status === 'number' ? result.status : 1);
18
18
  }
19
19
 
20
- function ensureBinary() {
21
- if (fs.existsSync(BINARY_PATH)) return;
22
- const result = spawnSync(process.execPath, [path.join(__dirname, 'install.js')], {
20
+ function runUpdater(args) {
21
+ const result = spawnSync(process.execPath, [path.join(__dirname, 'update.js'), ...args], {
23
22
  stdio: 'inherit',
24
23
  });
25
- if (result.status !== 0) {
26
- process.exit(typeof result.status === 'number' ? result.status : 1);
27
- }
24
+ process.exit(typeof result.status === 'number' ? result.status : 1);
28
25
  }
29
26
 
30
- function runBinary(args) {
31
- ensureBinary();
32
- const result = spawnSync(BINARY_PATH, args, {
27
+ function runUninstaller(args) {
28
+ const result = spawnSync(process.execPath, [path.join(__dirname, 'uninstall.js'), ...args], {
33
29
  stdio: 'inherit',
34
30
  });
35
- if (result.error) {
36
- console.error(`[gaia-cli] Failed to run ${BINARY_PATH}: ${result.error.message}`);
37
- process.exit(1);
38
- }
39
31
  process.exit(typeof result.status === 'number' ? result.status : 1);
40
32
  }
41
-
42
- const args = process.argv.slice(2);
33
+
34
+ function ensureBinary() {
35
+ if (fs.existsSync(BINARY_PATH)) return;
36
+ const result = spawnSync(process.execPath, [path.join(__dirname, 'install.js')], {
37
+ stdio: 'inherit',
38
+ });
39
+ if (result.status !== 0) {
40
+ process.exit(typeof result.status === 'number' ? result.status : 1);
41
+ }
42
+ }
43
+
44
+ function runBinary(args) {
45
+ ensureBinary();
46
+ const result = spawnSync(BINARY_PATH, args, {
47
+ stdio: 'inherit',
48
+ });
49
+ if (result.error) {
50
+ console.error(`[gaia-cli] Failed to run ${BINARY_PATH}: ${result.error.message}`);
51
+ process.exit(1);
52
+ }
53
+ process.exit(typeof result.status === 'number' ? result.status : 1);
54
+ }
55
+
56
+ const args = process.argv.slice(2);
43
57
  if (args[0] === 'install') {
44
58
  runInstallWizard(args.slice(1));
45
59
  }
60
+ if (args[0] === 'update') {
61
+ runUpdater(args.slice(1));
62
+ }
63
+ if (args[0] === 'uninstall') {
64
+ runUninstaller(args.slice(1));
65
+ }
46
66
 
47
67
  runBinary(args);
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+ const readline = require('node:readline/promises');
9
+
10
+ function defaultExecute(command, args) {
11
+ return spawnSync(command, args, {
12
+ stdio: ['ignore', 'pipe', 'pipe'],
13
+ encoding: 'utf8',
14
+ });
15
+ }
16
+
17
+ function npmInvocation(args, platform, env) {
18
+ if (platform === 'win32') {
19
+ const npmCli = env.npm_execpath || path.join(
20
+ path.dirname(process.execPath),
21
+ 'node_modules',
22
+ 'npm',
23
+ 'bin',
24
+ 'npm-cli.js'
25
+ );
26
+ return { command: process.execPath, args: [npmCli, ...args] };
27
+ }
28
+ return { command: 'npm', args };
29
+ }
30
+
31
+ function executeChecked(execute, command, args, action) {
32
+ const result = execute(command, args);
33
+ if (result.error || result.status !== 0) {
34
+ const detail = result.error
35
+ ? result.error.message
36
+ : `${result.stdout || ''}${result.stderr || ''}`.trim();
37
+ throw new Error(`${action} failed${detail ? `: ${detail}` : ''}`);
38
+ }
39
+ }
40
+
41
+ async function defaultConfirm(message) {
42
+ const prompt = readline.createInterface({ input: process.stdin, output: process.stderr });
43
+ try {
44
+ const answer = await prompt.question(`${message} [y/N] `);
45
+ return /^(y|yes)$/i.test(answer.trim());
46
+ } finally {
47
+ prompt.close();
48
+ }
49
+ }
50
+
51
+ async function uninstallCli(args, options = {}) {
52
+ const purge = args.includes('--purge');
53
+ const yes = args.includes('--yes');
54
+ const invalidArgs = args.filter((arg) => arg !== '--purge' && arg !== '--yes');
55
+ if (invalidArgs.length > 0 || yes && !purge || new Set(args).size !== args.length) {
56
+ throw new Error(`unsupported uninstall arguments: ${args.join(' ')}`);
57
+ }
58
+
59
+ const env = options.env || process.env;
60
+ const platform = options.platform || process.platform;
61
+ const packageInfo = options.packageInfo || JSON.parse(
62
+ fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
63
+ );
64
+ const execute = options.execute || defaultExecute;
65
+ const write = options.write || ((message) => console.error(message));
66
+ const confirm = options.confirm || defaultConfirm;
67
+ const runScript = options.runScript || path.join(__dirname, 'run.js');
68
+ const configDir = path.resolve(env.GAIA_CONFIG_DIR || path.join(os.homedir(), '.gaia'));
69
+
70
+ if (purge) {
71
+ const root = path.parse(configDir).root;
72
+ const protectedPaths = [root, path.resolve(os.homedir()), path.resolve(process.cwd())];
73
+ const normalizedConfigDir = platform === 'win32' ? configDir.toLowerCase() : configDir;
74
+ if (protectedPaths.some((protectedPath) => (
75
+ platform === 'win32' ? protectedPath.toLowerCase() : protectedPath
76
+ ) === normalizedConfigDir)) {
77
+ throw new Error(`refusing to purge unsafe Gaia configuration directory: ${configDir}`);
78
+ }
79
+ if (!yes && !await confirm(`Remove all Gaia CLI configuration and credentials at ${configDir}?`)) {
80
+ write('Uninstall cancelled.');
81
+ return { purged: false, cancelled: true, configDir };
82
+ }
83
+ }
84
+
85
+ write('Removing Gaia-managed Agent Skills...');
86
+ executeChecked(
87
+ execute,
88
+ process.execPath,
89
+ [runScript, 'skills', 'remove'],
90
+ 'remove Gaia-managed Agent Skills'
91
+ );
92
+
93
+ write(`Uninstalling ${packageInfo.name}...`);
94
+ const uninstall = npmInvocation(['uninstall', '-g', packageInfo.name], platform, env);
95
+ executeChecked(execute, uninstall.command, uninstall.args, 'uninstall Gaia CLI package');
96
+
97
+ if (purge) {
98
+ fs.rmSync(configDir, { recursive: true, force: true });
99
+ write(`Gaia CLI uninstalled. Configuration removed from ${configDir}`);
100
+ return { purged: true, configDir };
101
+ }
102
+
103
+ write(`Gaia CLI uninstalled. Configuration retained at ${configDir}`);
104
+
105
+ return { purged: false, configDir };
106
+ }
107
+
108
+ if (require.main === module) {
109
+ uninstallCli(process.argv.slice(2)).catch((error) => {
110
+ console.error(`[gaia-cli] Uninstall failed: ${error.message}`);
111
+ process.exitCode = 1;
112
+ });
113
+ }
114
+
115
+ module.exports = { uninstallCli };
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+
9
+ const registry = 'https://registry.npmjs.org';
10
+
11
+ function parseVersion(version) {
12
+ const match = String(version).match(
13
+ /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/
14
+ );
15
+ if (!match) throw new Error(`invalid Gaia CLI version: ${version}`);
16
+ return {
17
+ core: match.slice(1, 4).map(Number),
18
+ prerelease: match[4] ? match[4].split('.') : [],
19
+ };
20
+ }
21
+
22
+ function compareVersions(left, right) {
23
+ const a = parseVersion(left);
24
+ const b = parseVersion(right);
25
+ for (let index = 0; index < a.core.length; index += 1) {
26
+ if (a.core[index] !== b.core[index]) return a.core[index] - b.core[index];
27
+ }
28
+ if (a.prerelease.length === 0) return b.prerelease.length === 0 ? 0 : 1;
29
+ if (b.prerelease.length === 0) return -1;
30
+ const length = Math.max(a.prerelease.length, b.prerelease.length);
31
+ for (let index = 0; index < length; index += 1) {
32
+ const aPart = a.prerelease[index];
33
+ const bPart = b.prerelease[index];
34
+ if (aPart === undefined) return -1;
35
+ if (bPart === undefined) return 1;
36
+ if (aPart === bPart) continue;
37
+ const aNumber = /^\d+$/.test(aPart);
38
+ const bNumber = /^\d+$/.test(bPart);
39
+ if (aNumber && bNumber) return Number(aPart) - Number(bPart);
40
+ if (aNumber) return -1;
41
+ if (bNumber) return 1;
42
+ return aPart.localeCompare(bPart);
43
+ }
44
+ return 0;
45
+ }
46
+
47
+ function defaultExecute(command, args) {
48
+ return spawnSync(command, args, {
49
+ stdio: ['ignore', 'pipe', 'pipe'],
50
+ encoding: 'utf8',
51
+ });
52
+ }
53
+
54
+ function npmInvocation(args, platform, env) {
55
+ if (platform === 'win32') {
56
+ const npmCli = env.npm_execpath || path.join(
57
+ path.dirname(process.execPath),
58
+ 'node_modules',
59
+ 'npm',
60
+ 'bin',
61
+ 'npm-cli.js'
62
+ );
63
+ return { command: process.execPath, args: [npmCli, ...args] };
64
+ }
65
+ return { command: 'npm', args };
66
+ }
67
+
68
+ function executeChecked(execute, command, args, action) {
69
+ const result = execute(command, args);
70
+ if (result.error || result.status !== 0) {
71
+ const detail = result.error
72
+ ? result.error.message
73
+ : `${result.stdout || ''}${result.stderr || ''}`.trim();
74
+ throw new Error(`${action} failed${detail ? `: ${detail}` : ''}`);
75
+ }
76
+ return result;
77
+ }
78
+
79
+ function readInstallMode(env) {
80
+ const configDir = env.GAIA_CONFIG_DIR || path.join(os.homedir(), '.gaia');
81
+ const configPath = path.join(configDir, 'config.json');
82
+ let config;
83
+ try {
84
+ config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
85
+ } catch (error) {
86
+ throw new Error(`cannot read Gaia CLI configuration at ${configPath}: ${error.message}`);
87
+ }
88
+ if (config.install_mode !== 'internal' && config.install_mode !== 'external') {
89
+ throw new Error(`invalid install_mode in ${configPath}`);
90
+ }
91
+ return config.install_mode;
92
+ }
93
+
94
+ function updateCli(args, options = {}) {
95
+ const checkOnly = args.length === 1 && args[0] === '--check';
96
+ const requestedVersion = args.length === 2 && args[0] === '--version' ? args[1] : '';
97
+ if (args.length > 0 && !checkOnly && !requestedVersion) {
98
+ throw new Error(`unsupported update arguments: ${args.join(' ')}`);
99
+ }
100
+ if (requestedVersion) {
101
+ try {
102
+ parseVersion(requestedVersion);
103
+ } catch {
104
+ throw new Error(`explicit version must be a semantic version: ${requestedVersion}`);
105
+ }
106
+ }
107
+
108
+ const env = options.env || process.env;
109
+ const platform = options.platform || process.platform;
110
+ const packageInfo = options.packageInfo || JSON.parse(
111
+ fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
112
+ );
113
+ const execute = options.execute || defaultExecute;
114
+ const write = options.write || ((message) => console.error(message));
115
+ const mode = readInstallMode(env);
116
+
117
+ const requestedSpec = requestedVersion || 'latest';
118
+ const view = npmInvocation([
119
+ 'view', `${packageInfo.name}@${requestedSpec}`, 'version', '--registry', registry,
120
+ ], platform, env);
121
+ const viewResult = executeChecked(execute, view.command, view.args, 'check latest version');
122
+ const targetVersion = viewResult.stdout.trim();
123
+ if (!targetVersion) throw new Error('npm returned an empty latest version');
124
+ if (compareVersions(targetVersion, packageInfo.version) < 0) {
125
+ throw new Error(`refusing to downgrade Gaia CLI from v${packageInfo.version} to v${targetVersion}`);
126
+ }
127
+
128
+ if (checkOnly) {
129
+ const updateAvailable = targetVersion !== packageInfo.version;
130
+ write(updateAvailable
131
+ ? `Update available: v${packageInfo.version} -> v${targetVersion}`
132
+ : `Gaia CLI is already up to date: v${packageInfo.version}`);
133
+ return {
134
+ currentVersion: packageInfo.version,
135
+ targetVersion,
136
+ updated: false,
137
+ updateAvailable,
138
+ };
139
+ }
140
+
141
+ if (targetVersion === packageInfo.version) {
142
+ write(`Gaia CLI is already up to date: v${packageInfo.version}`);
143
+ return { currentVersion: packageInfo.version, targetVersion, updated: false };
144
+ }
145
+
146
+ write(`Updating Gaia CLI from v${packageInfo.version} to v${targetVersion}...`);
147
+ const prefix = npmInvocation(['prefix', '-g'], platform, env);
148
+ const prefixResult = executeChecked(execute, prefix.command, prefix.args, 'resolve npm global prefix');
149
+ const globalPrefix = prefixResult.stdout.trim();
150
+ if (!globalPrefix) throw new Error('npm returned an empty global prefix');
151
+
152
+ const install = npmInvocation([
153
+ 'install', '-g', `${packageInfo.name}@${targetVersion}`, '--registry', registry,
154
+ ], platform, env);
155
+ executeChecked(execute, install.command, install.args, 'install Gaia CLI update');
156
+
157
+ let setupCommand;
158
+ let setupArgs;
159
+ if (platform === 'win32') {
160
+ setupCommand = process.execPath;
161
+ setupArgs = [
162
+ path.win32.join(globalPrefix, 'node_modules', ...packageInfo.name.split('/'), 'scripts', 'run.js'),
163
+ 'setup',
164
+ '--mode',
165
+ mode,
166
+ ];
167
+ } else {
168
+ setupCommand = path.posix.join(globalPrefix, 'bin', 'gaia');
169
+ setupArgs = ['setup', '--mode', mode];
170
+ }
171
+ executeChecked(execute, setupCommand, setupArgs, 'restore Gaia CLI mode');
172
+
173
+ write(`Update completed: v${targetVersion}`);
174
+ write(`Install mode retained: ${mode}`);
175
+ return { currentVersion: packageInfo.version, targetVersion, updated: true };
176
+ }
177
+
178
+ if (require.main === module) {
179
+ try {
180
+ updateCli(process.argv.slice(2));
181
+ } catch (error) {
182
+ console.error(`[gaia-cli] Update failed: ${error.message}`);
183
+ process.exit(1);
184
+ }
185
+ }
186
+
187
+ module.exports = { updateCli };