@agent-webui/ai-desk-daemon 1.0.30 → 1.0.32

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
@@ -8,7 +8,7 @@
8
8
 
9
9
  ## ✨ 特性
10
10
 
11
- - 🎯 **自动启动** - Web 访问时自动检测并启动守护进程
11
+ - 🎯 **智能启动** - Web 访问时自动检测并尝试启动守护进程
12
12
  - 🖥️ **图形化管理** - 系统托盘 + 控制面板,便捷管理
13
13
  - 🔄 **实时监控** - 查看守护进程状态、日志和统计信息
14
14
  - 🚀 **智能启动器** - 多种方式自动唤醒守护进程
@@ -24,11 +24,17 @@
24
24
 
25
25
  ```bash
26
26
  # 全局安装
27
- npm install -g agent-webui/ai-desk-daemon
27
+ npm install -g @agent-webui/ai-desk
28
28
 
29
- # 启动 daemon
29
+ # 启动 daemon(默认使用已保存模式)
30
30
  aidesk start
31
31
 
32
+ # 切到 Native mode
33
+ aidesk start --mode native
34
+
35
+ # 切到 Bundled CLI-Anything runtime mode
36
+ aidesk start --mode cli-anything
37
+
32
38
  # 查看状态
33
39
  aidesk status
34
40
  ```
@@ -36,6 +42,9 @@ aidesk status
36
42
  **包含内容**:
37
43
  - ✅ AI Desk Daemon 后台服务
38
44
  - ✅ CLI 命令行管理工具
45
+ - ✅ 默认 harness 运行时与安装脚本
46
+ - ✅ bundled CLI-Anything runtime(随包安装到 AI Desk runtime)
47
+ - ✅ 按平台分发的内置 Python runtime
39
48
  - ✅ HTTP API (http://localhost:9527)
40
49
 
41
50
  **不包含**:
@@ -43,14 +52,21 @@ aidesk status
43
52
  - ❌ 桌面 GUI 应用
44
53
 
45
54
  **可用命令**:
46
- - `aidesk start` - 启动守护进程(后台运行)
55
+ - `aidesk start` - 启动守护进程(后台运行,沿用当前已保存 mode)
56
+ - `aidesk start --mode native` - 切到 Native mode 并启动
57
+ - `aidesk start --mode cli-anything` - 切到 Bundled CLI-Anything runtime mode 并启动
47
58
  - `aidesk start --log` - 启动守护进程(前台运行,跟随日志)
48
59
  - `aidesk stop` - 停止守护进程
49
60
  - `aidesk restart` - 重启守护进程
61
+ - `aidesk restart --mode cli-anything` - 切到 Bundled CLI-Anything runtime mode 并重启
50
62
  - `aidesk status` - 查看状态
51
63
  - `aidesk logs` - 查看日志
52
64
  - `aidesk logs -f` - 实时查看日志(不会停止守护进程)
53
65
 
66
+ **Mode 说明**:
67
+ - `Native mode`:检测、模型查询、命令执行走 daemon 原生实现。
68
+ - `Bundled CLI-Anything runtime mode`:使用 npm 安装时随包准备好的 Python runtime 和 `cli_anything` 入口;如果某个功能当前 runtime 不支持,daemon 会按配置回退到 native。
69
+
54
70
  📖 详细使用说明:[NPM_CLI.md](NPM_CLI.md)
55
71
 
56
72
  ---
@@ -69,6 +85,8 @@ curl -fsSL https://github.com/your-repo/ai-desk-desktop/releases/latest/download
69
85
  ./scripts/install-macos.sh
70
86
  ```
71
87
 
88
+ 安装脚本会为当前会话尝试启动 daemon,但不会再通过 LaunchAgent/Plist 保活。
89
+
72
90
  #### Linux
73
91
 
74
92
  ```bash
package/bin/cli.js CHANGED
@@ -7,12 +7,67 @@
7
7
  const { program } = require('commander');
8
8
  const chalk = require('chalk');
9
9
  const fs = require('fs');
10
+ const path = require('path');
10
11
  const { start, stop, restart, status } = require('../lib/daemon-manager');
11
12
  const { getLogPath } = require('../lib/platform');
12
13
  const { VERSION } = require('../lib/platform');
13
14
 
15
+ function resolveRequestedMode(options) {
16
+ const hasExplicitMode = typeof options.mode === 'string' && options.mode.trim() !== '';
17
+
18
+ if (options.cliAnything && options.native) {
19
+ throw new Error('Use either --cli-anything or --native, not both');
20
+ }
21
+
22
+ if (hasExplicitMode && (options.cliAnything || options.native)) {
23
+ throw new Error('Use either --mode or the mode shortcut flags, not both');
24
+ }
25
+
26
+ if (options.cliAnything) {
27
+ return 'cli-anything';
28
+ }
29
+
30
+ if (options.native) {
31
+ return 'native';
32
+ }
33
+
34
+ if (!hasExplicitMode) {
35
+ return '';
36
+ }
37
+
38
+ const normalizedMode = options.mode.trim().toLowerCase();
39
+ if (normalizedMode !== 'native' && normalizedMode !== 'cli-anything') {
40
+ throw new Error(`Unsupported mode: ${options.mode}. Use "native" or "cli-anything".`);
41
+ }
42
+
43
+ return normalizedMode;
44
+ }
45
+
46
+ function configureRequestedMode(mode) {
47
+ const packageRoot = path.join(__dirname, '..');
48
+ const { loadConfig } = require('../lib/config');
49
+ const { syncCLIAnythingConfig } = require('../lib/cli-anything-manager');
50
+
51
+ if (!mode) {
52
+ const config = loadConfig();
53
+ if (config.cli_anything?.enabled) {
54
+ return syncCLIAnythingConfig(packageRoot, { enabled: true });
55
+ }
56
+ return null;
57
+ }
58
+
59
+ if (mode === 'cli-anything') {
60
+ return syncCLIAnythingConfig(packageRoot, { enabled: true });
61
+ }
62
+
63
+ return syncCLIAnythingConfig(packageRoot, {
64
+ enabled: false,
65
+ ensureInstalled: false,
66
+ });
67
+ }
68
+
14
69
  program
15
- .name('ai-desk-daemon')
70
+ .name('aidesk')
16
71
  .description('AI Desk Daemon - CLI tool for managing the AI Desk daemon service')
17
72
  .version(VERSION);
18
73
 
@@ -20,11 +75,20 @@ program
20
75
  program
21
76
  .command('start')
22
77
  .description('Start the daemon')
78
+ .option('-m, --mode <mode>', 'Implementation mode: native or cli-anything')
79
+ .option('--cli-anything', 'Enable CLI-Anything mode before starting')
80
+ .option('--native', 'Force native mode before starting')
23
81
  .option('--log', 'Follow daemon logs in foreground (Ctrl+C to stop daemon)')
24
82
  .action(async (options) => {
25
83
  try {
26
- const { getPidPath } = require('../lib/platform');
27
- const daemonPid = start();
84
+ const mode = resolveRequestedMode(options) || 'native';
85
+ const modeResult = configureRequestedMode(mode);
86
+ start();
87
+ if (mode === 'cli-anything' && modeResult?.runtimeInfo?.cliAnythingPath) {
88
+ console.log(chalk.cyan(`CLI-Anything mode configured: ${modeResult.runtimeInfo.cliAnythingPath}`));
89
+ } else if (mode === 'native') {
90
+ console.log(chalk.cyan('Native mode configured'));
91
+ }
28
92
  console.log(chalk.green('✓ Daemon started successfully'));
29
93
 
30
94
  // Only follow logs if --log is specified
@@ -138,9 +202,19 @@ program
138
202
  program
139
203
  .command('restart')
140
204
  .description('Restart the daemon')
141
- .action(async () => {
205
+ .option('-m, --mode <mode>', 'Implementation mode: native or cli-anything')
206
+ .option('--cli-anything', 'Enable CLI-Anything mode before restarting')
207
+ .option('--native', 'Force native mode before restarting')
208
+ .action(async (options) => {
142
209
  try {
210
+ const mode = resolveRequestedMode(options);
211
+ const modeResult = configureRequestedMode(mode);
143
212
  restart();
213
+ if (mode === 'cli-anything' && modeResult?.runtimeInfo?.cliAnythingPath) {
214
+ console.log(chalk.cyan(`CLI-Anything mode configured: ${modeResult.runtimeInfo.cliAnythingPath}`));
215
+ } else if (mode === 'native') {
216
+ console.log(chalk.cyan('Native mode configured'));
217
+ }
144
218
  console.log(chalk.green('✓ Daemon restarted successfully'));
145
219
  } catch (error) {
146
220
  console.error(chalk.red('✗ Failed to restart daemon:'), error.message);
@@ -228,4 +302,3 @@ program.parse(process.argv);
228
302
  if (!process.argv.slice(2).length) {
229
303
  program.outputHelp();
230
304
  }
231
-
@@ -0,0 +1,218 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ensureRuntime, readJSON, run, writeJSON } = require('./runtime-manager');
4
+ const { getConfigPath } = require('./platform');
5
+
6
+ const CLI_ANYTHING_MODULE = 'cli_anything';
7
+
8
+ function resolveCommandPath(binDir, command) {
9
+ const candidates = process.platform === 'win32'
10
+ ? [
11
+ path.join(binDir, `${command}.exe`),
12
+ path.join(binDir, `${command}.cmd`),
13
+ path.join(binDir, `${command}.bat`),
14
+ path.join(binDir, command),
15
+ ]
16
+ : [path.join(binDir, command)];
17
+
18
+ for (const candidate of candidates) {
19
+ if (fs.existsSync(candidate)) {
20
+ return candidate;
21
+ }
22
+ }
23
+
24
+ return candidates[0];
25
+ }
26
+
27
+ function resolveCLIAnythingRuntimeSource(packageRoot = '') {
28
+ const candidates = [];
29
+
30
+ if (process.env.AI_DESK_CLI_ANYTHING_RUNTIME_SOURCE) {
31
+ candidates.push(process.env.AI_DESK_CLI_ANYTHING_RUNTIME_SOURCE);
32
+ }
33
+
34
+ if (packageRoot) {
35
+ candidates.push(path.join(packageRoot, 'python-runtime'));
36
+ }
37
+
38
+ candidates.push(path.join(__dirname, '..', 'python-runtime'));
39
+
40
+ for (const candidate of candidates) {
41
+ if (!candidate) {
42
+ continue;
43
+ }
44
+
45
+ const setupPyPath = path.join(candidate, 'setup.py');
46
+ if (fs.existsSync(setupPyPath)) {
47
+ return candidate;
48
+ }
49
+ }
50
+
51
+ return '';
52
+ }
53
+
54
+ function cliAnythingRuntimeLooksHealthy(runtimeInfo, pythonModule = CLI_ANYTHING_MODULE) {
55
+ let importable = false;
56
+
57
+ try {
58
+ const importResult = run(runtimeInfo.pythonPath, ['-c', `import ${pythonModule}`]);
59
+ importable = importResult.status === 0;
60
+ } catch (error) {
61
+ importable = false;
62
+ }
63
+
64
+ const cliAnythingPath = resolveCommandPath(runtimeInfo.binDir, 'cli-anything');
65
+
66
+ return {
67
+ importable,
68
+ cliAnythingPath,
69
+ healthy: importable && fs.existsSync(cliAnythingPath),
70
+ };
71
+ }
72
+
73
+ function installCLIAnythingRuntime(runtimeInfo, packageRoot = '', options = {}) {
74
+ const pythonModule = options.pythonModule || CLI_ANYTHING_MODULE;
75
+ const runtimeSource = resolveCLIAnythingRuntimeSource(packageRoot);
76
+
77
+ if (!runtimeSource) {
78
+ throw new Error('CLI-Anything runtime source not found in the installed AI Desk package');
79
+ }
80
+
81
+ const args = ['-m', 'pip', 'install', '--upgrade'];
82
+ if (options.forceReinstall) {
83
+ args.push('--force-reinstall');
84
+ }
85
+ args.push(runtimeSource);
86
+
87
+ const installResult = run(runtimeInfo.pythonPath, args);
88
+ if (installResult.status !== 0) {
89
+ throw new Error(
90
+ installResult.stderr ||
91
+ installResult.stdout ||
92
+ `Failed to install CLI-Anything runtime from ${runtimeSource}`
93
+ );
94
+ }
95
+
96
+ const health = cliAnythingRuntimeLooksHealthy(runtimeInfo, pythonModule);
97
+ if (!health.importable) {
98
+ throw new Error(`CLI-Anything Python module "${pythonModule}" is not importable after installation`);
99
+ }
100
+
101
+ if (!fs.existsSync(health.cliAnythingPath)) {
102
+ throw new Error(`CLI-Anything executable not found after installation: ${health.cliAnythingPath}`);
103
+ }
104
+
105
+ return {
106
+ ...runtimeInfo,
107
+ cliAnythingPath: health.cliAnythingPath,
108
+ pythonModule,
109
+ runtimeSource,
110
+ };
111
+ }
112
+
113
+ function ensureCLIAnythingRuntime(packageRoot = '', options = {}) {
114
+ const pythonModule = options.pythonModule || CLI_ANYTHING_MODULE;
115
+ const runtimeInfo = ensureRuntime();
116
+ const health = cliAnythingRuntimeLooksHealthy(runtimeInfo, pythonModule);
117
+
118
+ if (health.healthy && !options.forceReinstall) {
119
+ return {
120
+ ...runtimeInfo,
121
+ cliAnythingPath: health.cliAnythingPath,
122
+ pythonModule,
123
+ runtimeSource: resolveCLIAnythingRuntimeSource(packageRoot),
124
+ };
125
+ }
126
+
127
+ return installCLIAnythingRuntime(runtimeInfo, packageRoot, options);
128
+ }
129
+
130
+ function syncCLIAnythingConfig(packageRoot = '', options = {}) {
131
+ const configPath = options.configPath || getConfigPath();
132
+ const config = readJSON(configPath);
133
+ const cliAnything = config.cli_anything || {};
134
+ const runtime = cliAnything.runtime || {};
135
+ const features = cliAnything.features || {};
136
+
137
+ let runtimeInfo = null;
138
+ if (options.ensureInstalled !== false) {
139
+ runtimeInfo = ensureCLIAnythingRuntime(packageRoot, options);
140
+ }
141
+
142
+ if (typeof options.enabled === 'boolean') {
143
+ cliAnything.enabled = options.enabled;
144
+ } else if (typeof cliAnything.enabled !== 'boolean') {
145
+ cliAnything.enabled = false;
146
+ }
147
+
148
+ if (typeof options.fallbackToNative === 'boolean') {
149
+ cliAnything.fallback_to_native = options.fallbackToNative;
150
+ } else if (typeof cliAnything.fallback_to_native !== 'boolean') {
151
+ cliAnything.fallback_to_native = true;
152
+ }
153
+
154
+ if (runtimeInfo) {
155
+ cliAnything.cli_anything_path = runtimeInfo.cliAnythingPath;
156
+ runtime.python_path = runtimeInfo.pythonPath;
157
+ runtime.python_module = runtimeInfo.pythonModule;
158
+ } else {
159
+ cliAnything.cli_anything_path = cliAnything.cli_anything_path || '';
160
+ runtime.python_path = runtime.python_path || '';
161
+ runtime.python_module = runtime.python_module || CLI_ANYTHING_MODULE;
162
+ }
163
+
164
+ if (typeof options.timeout === 'number') {
165
+ cliAnything.timeout = options.timeout;
166
+ } else if (typeof cliAnything.timeout !== 'number') {
167
+ cliAnything.timeout = 30;
168
+ }
169
+
170
+ if (typeof options.maxRetries === 'number') {
171
+ cliAnything.max_retries = options.maxRetries;
172
+ } else if (typeof cliAnything.max_retries !== 'number') {
173
+ cliAnything.max_retries = 3;
174
+ }
175
+
176
+ if (typeof options.generationTimeout === 'number') {
177
+ runtime.generation_timeout = options.generationTimeout;
178
+ } else if (typeof runtime.generation_timeout !== 'number') {
179
+ runtime.generation_timeout = 300;
180
+ }
181
+
182
+ const booleanFeatureDefaults = {
183
+ ai_cli_detection: true,
184
+ model_listing: true,
185
+ command_execution: true,
186
+ task_generation: true,
187
+ };
188
+
189
+ for (const [featureKey, defaultValue] of Object.entries(booleanFeatureDefaults)) {
190
+ if (featureKey === 'task_generation' && typeof options.taskGeneration === 'boolean') {
191
+ features[featureKey] = options.taskGeneration;
192
+ continue;
193
+ }
194
+
195
+ if (typeof features[featureKey] !== 'boolean') {
196
+ features[featureKey] = defaultValue;
197
+ }
198
+ }
199
+
200
+ cliAnything.runtime = runtime;
201
+ cliAnything.features = features;
202
+ config.cli_anything = cliAnything;
203
+ writeJSON(configPath, config);
204
+
205
+ return {
206
+ configPath,
207
+ config,
208
+ runtimeInfo,
209
+ };
210
+ }
211
+
212
+ module.exports = {
213
+ CLI_ANYTHING_MODULE,
214
+ ensureCLIAnythingRuntime,
215
+ resolveCLIAnythingRuntimeSource,
216
+ resolveCommandPath,
217
+ syncCLIAnythingConfig,
218
+ };
package/lib/config.js CHANGED
@@ -7,6 +7,65 @@ const path = require('path');
7
7
  const { getConfigDir, getConfigPath } = require('./platform');
8
8
 
9
9
  const DEFAULT_PORT = 9527;
10
+ const DEFAULT_CONFIG = {
11
+ port: DEFAULT_PORT,
12
+ logLevel: 'info',
13
+ autoStart: true,
14
+ cli_anything: {
15
+ enabled: false,
16
+ fallback_to_native: true,
17
+ cli_anything_path: '',
18
+ timeout: 30,
19
+ max_retries: 3,
20
+ features: {
21
+ ai_cli_detection: true,
22
+ model_listing: true,
23
+ command_execution: true,
24
+ task_generation: true,
25
+ },
26
+ runtime: {
27
+ python_path: '',
28
+ python_module: 'cli_anything',
29
+ generation_timeout: 300,
30
+ },
31
+ },
32
+ harness_runtime: {
33
+ venv_path: '',
34
+ python_path: '',
35
+ registry_path: '',
36
+ source: '',
37
+ runtime_home: '',
38
+ package_name: '',
39
+ package_version: '',
40
+ archive_sha256: '',
41
+ },
42
+ harnesses: {},
43
+ };
44
+
45
+ function cloneDefaultConfig() {
46
+ return JSON.parse(JSON.stringify(DEFAULT_CONFIG));
47
+ }
48
+
49
+ function mergeConfig(base, override) {
50
+ if (!override || typeof override !== 'object' || Array.isArray(override)) {
51
+ return base;
52
+ }
53
+
54
+ const merged = Array.isArray(base) ? [...base] : { ...base };
55
+ for (const [key, value] of Object.entries(override)) {
56
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
57
+ const nestedBase = merged[key] && typeof merged[key] === 'object' && !Array.isArray(merged[key])
58
+ ? merged[key]
59
+ : {};
60
+ merged[key] = mergeConfig(nestedBase, value);
61
+ continue;
62
+ }
63
+
64
+ merged[key] = value;
65
+ }
66
+
67
+ return merged;
68
+ }
10
69
 
11
70
  /**
12
71
  * Ensure config directory exists
@@ -33,22 +92,17 @@ function loadConfig() {
33
92
  const configPath = getConfigPath();
34
93
 
35
94
  if (!fs.existsSync(configPath)) {
36
- // Create default config
37
- const defaultConfig = {
38
- port: DEFAULT_PORT,
39
- logLevel: 'info',
40
- autoStart: true
41
- };
95
+ const defaultConfig = cloneDefaultConfig();
42
96
  saveConfig(defaultConfig);
43
97
  return defaultConfig;
44
98
  }
45
99
 
46
100
  try {
47
101
  const content = fs.readFileSync(configPath, 'utf8');
48
- return JSON.parse(content);
102
+ return mergeConfig(cloneDefaultConfig(), JSON.parse(content));
49
103
  } catch (error) {
50
104
  console.error('Failed to load config:', error.message);
51
- return { port: DEFAULT_PORT };
105
+ return cloneDefaultConfig();
52
106
  }
53
107
  }
54
108
 
@@ -59,7 +113,8 @@ function saveConfig(config) {
59
113
  ensureConfigDir();
60
114
 
61
115
  const configPath = getConfigPath();
62
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
116
+ const mergedConfig = mergeConfig(cloneDefaultConfig(), config);
117
+ fs.writeFileSync(configPath, JSON.stringify(mergedConfig, null, 2), 'utf8');
63
118
  }
64
119
 
65
120
  /**
@@ -97,7 +152,8 @@ module.exports = {
97
152
  saveConfig,
98
153
  getPort,
99
154
  setPort,
155
+ cloneDefaultConfig,
156
+ mergeConfig,
100
157
  getConfigPath, // Re-export from platform.js
101
158
  DEFAULT_PORT
102
159
  };
103
-
@@ -80,7 +80,7 @@ function start() {
80
80
  throw new Error(
81
81
  `Daemon binary not found at: ${binaryPath}\n` +
82
82
  `This might be a corrupted installation. Try reinstalling:\n` +
83
- ` npm install -g @ringcentral/ai-desk-daemon --force`
83
+ ` npm install -g @agent-webui/ai-desk --force`
84
84
  );
85
85
  }
86
86
 
@@ -186,4 +186,3 @@ module.exports = {
186
186
  restart,
187
187
  status
188
188
  };
189
-
@@ -0,0 +1,178 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ensureRuntime, readJSON, run, syncHarnessConfig, writeJSON } = require('./runtime-manager');
4
+
5
+ function listDependencyNames(packageRoot) {
6
+ const packageJsonPath = path.join(packageRoot, 'package.json');
7
+ const packageJson = readJSON(packageJsonPath);
8
+
9
+ const allDeps = {
10
+ ...(packageJson.dependencies || {}),
11
+ ...(packageJson.optionalDependencies || {}),
12
+ };
13
+
14
+ return Object.keys(allDeps).filter((name) => name.startsWith('@agent-webui/ai-desk-harness-'));
15
+ }
16
+
17
+ function findWorkspaceSiblingPackage(packageRoot, packageName) {
18
+ const packagesRoot = path.resolve(packageRoot, '..');
19
+ if (!fs.existsSync(packagesRoot)) {
20
+ return null;
21
+ }
22
+
23
+ const children = fs.readdirSync(packagesRoot, { withFileTypes: true });
24
+ for (const child of children) {
25
+ if (!child.isDirectory()) {
26
+ continue;
27
+ }
28
+
29
+ const packageJsonPath = path.join(packagesRoot, child.name, 'package.json');
30
+ if (!fs.existsSync(packageJsonPath)) {
31
+ continue;
32
+ }
33
+
34
+ const pkg = readJSON(packageJsonPath);
35
+ if (pkg.name === packageName) {
36
+ return path.dirname(packageJsonPath);
37
+ }
38
+ }
39
+
40
+ return null;
41
+ }
42
+
43
+ function resolveHarnessPackageDir(packageRoot, packageName) {
44
+ try {
45
+ const packageJsonPath = require.resolve(`${packageName}/package.json`, {
46
+ paths: [packageRoot],
47
+ });
48
+ return path.dirname(packageJsonPath);
49
+ } catch (error) {
50
+ const workspaceMatch = findWorkspaceSiblingPackage(packageRoot, packageName);
51
+ if (workspaceMatch) {
52
+ return workspaceMatch;
53
+ }
54
+ throw new Error(`Failed to resolve harness package ${packageName} from ${packageRoot}: ${error.message}`);
55
+ }
56
+ }
57
+
58
+ function resolveHarnesses(packageRoot) {
59
+ const harnessNames = listDependencyNames(packageRoot);
60
+
61
+ return harnessNames.map((packageName) => {
62
+ const packageDir = resolveHarnessPackageDir(packageRoot, packageName);
63
+ const packageJson = readJSON(path.join(packageDir, 'package.json'));
64
+ const manifestPath = path.join(packageDir, 'manifest.json');
65
+ const manifest = readJSON(manifestPath);
66
+
67
+ return {
68
+ packageDir,
69
+ packageJson,
70
+ packageName,
71
+ manifest,
72
+ manifestPath,
73
+ sourcePath: path.resolve(packageDir, manifest.source_path),
74
+ };
75
+ });
76
+ }
77
+
78
+ function resolveCommandPath(binDir, command) {
79
+ const candidates = process.platform === 'win32'
80
+ ? [
81
+ path.join(binDir, `${command}.exe`),
82
+ path.join(binDir, `${command}.cmd`),
83
+ path.join(binDir, `${command}.bat`),
84
+ path.join(binDir, command),
85
+ ]
86
+ : [path.join(binDir, command)];
87
+
88
+ for (const candidate of candidates) {
89
+ if (fs.existsSync(candidate)) {
90
+ return candidate;
91
+ }
92
+ }
93
+
94
+ return candidates[0];
95
+ }
96
+
97
+ function installHarnessPackage(runtimeInfo, harness) {
98
+ if (!fs.existsSync(harness.sourcePath)) {
99
+ throw new Error(`Harness source not found: ${harness.sourcePath}`);
100
+ }
101
+
102
+ const installResult = run(runtimeInfo.pythonPath, ['-m', 'pip', 'install', '--upgrade', harness.sourcePath]);
103
+ if (installResult.status !== 0) {
104
+ throw new Error(
105
+ installResult.stderr ||
106
+ installResult.stdout ||
107
+ `Failed to install harness ${harness.packageName} from ${harness.sourcePath}`
108
+ );
109
+ }
110
+
111
+ for (const requirement of harness.manifest.python_requirements || []) {
112
+ const requirementResult = run(runtimeInfo.pythonPath, ['-m', 'pip', 'install', '--upgrade', requirement]);
113
+ if (requirementResult.status !== 0) {
114
+ throw new Error(
115
+ requirementResult.stderr ||
116
+ requirementResult.stdout ||
117
+ `Failed to install Python requirement ${requirement} for ${harness.packageName}`
118
+ );
119
+ }
120
+ }
121
+
122
+ return {
123
+ name: harness.manifest.name,
124
+ displayName: harness.manifest.display_name || harness.manifest.name,
125
+ module: harness.manifest.module,
126
+ commands: harness.manifest.commands || [],
127
+ capabilities: harness.manifest.capabilities || [],
128
+ commandPath: resolveCommandPath(runtimeInfo.binDir, (harness.manifest.commands || [])[0] || ''),
129
+ commandPaths: Object.fromEntries(
130
+ (harness.manifest.commands || []).map((command) => [command, resolveCommandPath(runtimeInfo.binDir, command)])
131
+ ),
132
+ packageName: harness.packageName,
133
+ packageVersion: harness.packageJson.version || '',
134
+ version: harness.manifest.version || harness.packageJson.version || '',
135
+ sourcePath: harness.sourcePath,
136
+ };
137
+ }
138
+
139
+ function writeHarnessRegistry(runtimeInfo, harnesses) {
140
+ const registry = {
141
+ version: 1,
142
+ generated_at: new Date().toISOString(),
143
+ runtime: {
144
+ python_path: runtimeInfo.pythonPath,
145
+ venv_path: runtimeInfo.venvDir,
146
+ registry_path: runtimeInfo.registryPath,
147
+ source: runtimeInfo.source || '',
148
+ runtime_home: runtimeInfo.runtimeHomeDir || '',
149
+ package_name: runtimeInfo.packageName || '',
150
+ package_version: runtimeInfo.packageVersion || '',
151
+ archive_sha256: runtimeInfo.archiveSHA256 || '',
152
+ },
153
+ harnesses,
154
+ };
155
+
156
+ writeJSON(runtimeInfo.registryPath, registry);
157
+ syncHarnessConfig(runtimeInfo, harnesses);
158
+ return registry;
159
+ }
160
+
161
+ function installHarnessesForPackageRoot(packageRoot) {
162
+ const runtimeInfo = ensureRuntime();
163
+ const harnessPackages = resolveHarnesses(packageRoot);
164
+ const installedHarnesses = harnessPackages.map((harness) => installHarnessPackage(runtimeInfo, harness));
165
+ const registry = writeHarnessRegistry(runtimeInfo, installedHarnesses);
166
+
167
+ return {
168
+ harnessCount: installedHarnesses.length,
169
+ harnesses: installedHarnesses,
170
+ registry,
171
+ runtimeInfo,
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ installHarnessesForPackageRoot,
177
+ resolveHarnesses,
178
+ };