@agent-webui/ai-desk-daemon 1.0.60 → 1.0.61-beta4

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/bin/cli.js CHANGED
@@ -11,6 +11,9 @@ const path = require('path');
11
11
  const { start, stop, restart, status } = require('../lib/daemon-manager');
12
12
  const { getLogPath } = require('../lib/platform');
13
13
  const { VERSION } = require('../lib/platform');
14
+ const { getPort } = require('../lib/config');
15
+ const { reportLifecycle } = require('../lib/daemon-registry');
16
+ const { upgradePackage } = require('../lib/self-upgrade');
14
17
 
15
18
  function wait(ms) {
16
19
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -127,6 +130,17 @@ function configureRequestedMode(mode) {
127
130
  });
128
131
  }
129
132
 
133
+ async function reportRegistryLifecycle(event) {
134
+ try {
135
+ await reportLifecycle(event, {
136
+ port: Number(getPort()),
137
+ version: VERSION,
138
+ });
139
+ } catch (error) {
140
+ console.warn(chalk.yellow(`[registry] Failed to report daemon ${event}: ${error.message || error}`));
141
+ }
142
+ }
143
+
130
144
  program
131
145
  .name('aidesk')
132
146
  .description('AI Desk Daemon - CLI tool for managing the AI Desk daemon service')
@@ -145,6 +159,7 @@ program
145
159
  const mode = resolveRequestedMode(options) || 'native';
146
160
  const modeResult = configureRequestedMode(mode);
147
161
  start();
162
+ await reportRegistryLifecycle('start');
148
163
  if (mode === 'cli-anything' && modeResult?.runtimeInfo?.cliAnythingPath) {
149
164
  console.log(chalk.cyan(`CLI-Anything mode configured: ${modeResult.runtimeInfo.cliAnythingPath}`));
150
165
  } else if (mode === 'native') {
@@ -170,7 +185,7 @@ program
170
185
  const tail = startUnixLogFollow(logPath);
171
186
 
172
187
  // Handle Ctrl+C - stop the daemon
173
- process.on('SIGINT', () => {
188
+ process.on('SIGINT', async () => {
174
189
  console.log(chalk.yellow('\n\n⏹ Stopping daemon...'));
175
190
  tail.kill();
176
191
 
@@ -178,6 +193,7 @@ program
178
193
  // Stop the daemon
179
194
  const { stop } = require('../lib/daemon-manager');
180
195
  stop();
196
+ await reportRegistryLifecycle('stop');
181
197
  console.log(chalk.green('✓ Daemon stopped successfully'));
182
198
  } catch (error) {
183
199
  console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
@@ -191,7 +207,7 @@ program
191
207
 
192
208
  const pollInterval = startWindowsLogFollow(logPath);
193
209
 
194
- process.on('SIGINT', () => {
210
+ process.on('SIGINT', async () => {
195
211
  clearInterval(pollInterval);
196
212
  console.log(chalk.yellow('\n\n⏹ Stopping daemon...'));
197
213
 
@@ -199,6 +215,7 @@ program
199
215
  // Stop the daemon
200
216
  const { stop } = require('../lib/daemon-manager');
201
217
  stop();
218
+ await reportRegistryLifecycle('stop');
202
219
  console.log(chalk.green('✓ Daemon stopped successfully'));
203
220
  } catch (error) {
204
221
  console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
@@ -221,6 +238,7 @@ program
221
238
  .action(async () => {
222
239
  try {
223
240
  stop();
241
+ await reportRegistryLifecycle('stop');
224
242
  console.log(chalk.green('✓ Daemon stopped successfully'));
225
243
  } catch (error) {
226
244
  console.error(chalk.red('✗ Failed to stop daemon:'), error.message);
@@ -240,6 +258,7 @@ program
240
258
  const mode = resolveRequestedMode(options);
241
259
  const modeResult = configureRequestedMode(mode);
242
260
  restart();
261
+ await reportRegistryLifecycle('restart');
243
262
  if (mode === 'cli-anything' && modeResult?.runtimeInfo?.cliAnythingPath) {
244
263
  console.log(chalk.cyan(`CLI-Anything mode configured: ${modeResult.runtimeInfo.cliAnythingPath}`));
245
264
  } else if (mode === 'native') {
@@ -286,6 +305,24 @@ program
286
305
  }
287
306
  });
288
307
 
308
+ // Upgrade command
309
+ program
310
+ .command('upgrade')
311
+ .description('Upgrade AI Desk CLI to the latest published version')
312
+ .option('--force', 'Reinstall the latest version even when the current version is already latest')
313
+ .action(async (options) => {
314
+ try {
315
+ upgradePackage({
316
+ packageName: '@agent-webui/ai-desk-daemon',
317
+ packageRoot: path.join(__dirname, '..'),
318
+ force: Boolean(options.force),
319
+ });
320
+ } catch (error) {
321
+ console.error(chalk.red('✗ Failed to upgrade AI Desk:'), error.message);
322
+ process.exit(error.exitCode || 1);
323
+ }
324
+ });
325
+
289
326
  // Logs command
290
327
  program
291
328
  .command('logs')
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  const fs = require('fs');
6
- const { spawn, execSync } = require('child_process');
6
+ const { spawn, spawnSync, execSync } = require('child_process');
7
7
  const http = require('http');
8
8
  const { getDaemonBinaryPath, getPidPath, getConfiguredLogPath, getLogPath, VERSION } = require('./platform');
9
9
  const { getPort, getConfigPath } = require('./config');
@@ -143,26 +143,166 @@ function start() {
143
143
  return child.pid;
144
144
  }
145
145
 
146
+ function parsePid(value) {
147
+ const pid = Number.parseInt(String(value || '').trim(), 10);
148
+ if (!Number.isInteger(pid) || pid <= 0) {
149
+ return null;
150
+ }
151
+ return pid;
152
+ }
153
+
154
+ function parsePidList(output) {
155
+ return String(output || '')
156
+ .split(/\r?\n/)
157
+ .map((line) => parsePid(line))
158
+ .filter((pid) => pid && pid !== process.pid);
159
+ }
160
+
161
+ function listPidsByNameUnix(processName) {
162
+ const pgrep = spawnSync('pgrep', ['-x', processName], { encoding: 'utf8' });
163
+ if (pgrep.status === 0) {
164
+ return parsePidList(pgrep.stdout);
165
+ }
166
+
167
+ if (pgrep.error && pgrep.error.code !== 'ENOENT') {
168
+ throw pgrep.error;
169
+ }
170
+
171
+ const ps = spawnSync('ps', ['-A', '-o', 'pid=', '-o', 'comm='], { encoding: 'utf8' });
172
+ if (ps.status !== 0) {
173
+ return [];
174
+ }
175
+
176
+ const pids = [];
177
+ for (const line of String(ps.stdout || '').split(/\r?\n/)) {
178
+ const trimmed = line.trim();
179
+ if (!trimmed) {
180
+ continue;
181
+ }
182
+
183
+ const match = trimmed.match(/^(\d+)\s+(.+)$/);
184
+ if (!match) {
185
+ continue;
186
+ }
187
+
188
+ if (require('path').basename(match[2]) === processName) {
189
+ const pid = parsePid(match[1]);
190
+ if (pid && pid !== process.pid) {
191
+ pids.push(pid);
192
+ }
193
+ }
194
+ }
195
+ return pids;
196
+ }
197
+
198
+ function isIgnorableKillError(error) {
199
+ return error && (error.code === 'ESRCH' || error.message === 'kill ESRCH');
200
+ }
201
+
202
+ function terminatePid(pid, signal = 'SIGTERM') {
203
+ process.kill(pid, signal);
204
+ }
205
+
206
+ function terminateAllByNameUnix(processName) {
207
+ const killedPids = [];
208
+ for (const pid of listPidsByNameUnix(processName)) {
209
+ try {
210
+ terminatePid(pid, 'SIGTERM');
211
+ killedPids.push(pid);
212
+ } catch (error) {
213
+ if (!isIgnorableKillError(error)) {
214
+ throw error;
215
+ }
216
+ }
217
+ }
218
+ return killedPids;
219
+ }
220
+
221
+ function terminateAllByNameWindows(processName) {
222
+ const imageName = processName.endsWith('.exe') ? processName : `${processName}.exe`;
223
+ const result = spawnSync('taskkill', ['/F', '/T', '/IM', imageName], { encoding: 'utf8' });
224
+ if (result.status === 0) {
225
+ return [];
226
+ }
227
+
228
+ const output = `${result.stderr || ''}${result.stdout || ''}`;
229
+ if (/not found|no running instance|not running/i.test(output)) {
230
+ return [];
231
+ }
232
+
233
+ if (result.error) {
234
+ throw result.error;
235
+ }
236
+
237
+ throw new Error(output.trim() || `taskkill failed for ${imageName}`);
238
+ }
239
+
240
+ function terminateAllByName(processName) {
241
+ if (process.platform === 'win32') {
242
+ return terminateAllByNameWindows(processName);
243
+ }
244
+ return terminateAllByNameUnix(processName);
245
+ }
246
+
247
+ function addKilledPid(killedPids, pid) {
248
+ if (pid && !killedPids.includes(pid)) {
249
+ killedPids.push(pid);
250
+ }
251
+ }
252
+
146
253
  /**
147
254
  * Stop daemon
148
255
  */
149
- function stop() {
150
- const pidPath = getPidPath();
151
-
152
- if (!fs.existsSync(pidPath)) {
153
- throw new Error('Daemon is not running');
256
+ function stop(deps = {}) {
257
+ const fsApi = deps.fs || fs;
258
+ const platformApi = deps.platform || { getPidPath };
259
+ const processManager = deps.processManager || {
260
+ terminatePid,
261
+ terminateAllByName,
262
+ };
263
+ const logger = deps.logger || console;
264
+ const pidPath = platformApi.getPidPath();
265
+ let pidFilePid = null;
266
+ const killedPids = [];
267
+ const errors = [];
268
+
269
+ if (fsApi.existsSync(pidPath)) {
270
+ pidFilePid = parsePid(fsApi.readFileSync(pidPath, 'utf8'));
271
+ if (pidFilePid) {
272
+ try {
273
+ processManager.terminatePid(pidFilePid, 'SIGTERM');
274
+ addKilledPid(killedPids, pidFilePid);
275
+ } catch (error) {
276
+ if (!isIgnorableKillError(error)) {
277
+ errors.push(error);
278
+ }
279
+ }
280
+ }
281
+
282
+ try {
283
+ fsApi.unlinkSync(pidPath);
284
+ } catch (error) {
285
+ errors.push(error);
286
+ }
154
287
  }
155
-
156
- const pid = parseInt(fs.readFileSync(pidPath, 'utf8').trim());
157
-
288
+
158
289
  try {
159
- process.kill(pid, 'SIGTERM');
160
- fs.unlinkSync(pidPath);
161
- console.log('Daemon stopped');
290
+ for (const pid of processManager.terminateAllByName('ai-desk-daemon') || []) {
291
+ addKilledPid(killedPids, pid);
292
+ }
162
293
  } catch (error) {
163
- fs.unlinkSync(pidPath);
164
- throw new Error(`Failed to stop daemon: ${error.message}`);
294
+ errors.push(error);
295
+ }
296
+
297
+ if (errors.length > 0) {
298
+ throw new Error(`Failed to stop daemon: ${errors.map((error) => error.message).join('; ')}`);
165
299
  }
300
+
301
+ logger.log('Daemon stopped');
302
+ return {
303
+ pidFilePid,
304
+ killedPids,
305
+ };
166
306
  }
167
307
 
168
308
  /**
@@ -0,0 +1,254 @@
1
+ const fs = require('fs');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const http = require('http');
5
+ const https = require('https');
6
+ const crypto = require('crypto');
7
+
8
+ const DEFAULT_REGISTRY_API_BASE_URL = 'https://desk.int.rclabenv.com/';
9
+
10
+ function trim(value) {
11
+ if (typeof value === 'string') return value.trim();
12
+ if (typeof value === 'number' && Number.isFinite(value)) return String(value);
13
+ return '';
14
+ }
15
+
16
+ function normalizeApiBaseUrl(value) {
17
+ const raw = trim(value).replace(/\/+$/, '');
18
+ if (!raw) return '';
19
+ return raw.endsWith('/api/v1') ? raw : `${raw}/api/v1`;
20
+ }
21
+
22
+ function parseTruthyEnv(value) {
23
+ const raw = trim(value).toLowerCase();
24
+ if (!raw) return null;
25
+ return raw === '1' || raw === 'true' || raw === 'yes' || raw === 'on';
26
+ }
27
+
28
+ function readRegistrySessionIdentity(homeDir = os.homedir()) {
29
+ const sessionPath = path.join(homeDir, '.aidesktop', 'session.json');
30
+ try {
31
+ if (!fs.existsSync(sessionPath)) return null;
32
+ const session = JSON.parse(fs.readFileSync(sessionPath, 'utf8'));
33
+ if (!session || typeof session !== 'object') return null;
34
+ const rcAccountId = trim(session.account_id || session.accountId);
35
+ const rcExtensionId = trim(session.extension_id || session.extensionId);
36
+ if (!rcAccountId || !rcExtensionId) return null;
37
+ return { rcAccountId, rcExtensionId };
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ function readRegistryConfigFromDaemonConfig(homeDir = os.homedir()) {
44
+ const configPath = path.join(homeDir, '.aidesktop', 'daemon-config.json');
45
+ try {
46
+ if (!fs.existsSync(configPath)) return null;
47
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
48
+ const registry = config && typeof config === 'object' ? config.registry : null;
49
+ if (!registry || typeof registry !== 'object') return null;
50
+ const sessionIdentity = readRegistrySessionIdentity(homeDir);
51
+ const apiBaseUrl = normalizeApiBaseUrl(
52
+ registry.api_base_url || registry.apiBaseUrl || DEFAULT_REGISTRY_API_BASE_URL,
53
+ );
54
+ const rcAccountId = trim(registry.rc_account_id || registry.rcAccountId) ||
55
+ sessionIdentity?.rcAccountId || '';
56
+ const rcExtensionId = trim(registry.rc_extension_id || registry.rcExtensionId) ||
57
+ sessionIdentity?.rcExtensionId || '';
58
+ if (!apiBaseUrl || !rcAccountId || !rcExtensionId) return null;
59
+ return {
60
+ apiBaseUrl,
61
+ rcAccountId,
62
+ rcExtensionId,
63
+ rcUsername: trim(registry.rc_username || registry.rcUsername),
64
+ tlsInsecureSkipVerify: Boolean(registry.tls_insecure_skip_verify || registry.tlsInsecureSkipVerify),
65
+ };
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ function getRegistryConfig(env = process.env, homeDir = os.homedir()) {
72
+ const daemonConfig = readRegistryConfigFromDaemonConfig(homeDir);
73
+ const sessionIdentity = readRegistrySessionIdentity(homeDir);
74
+ const apiBaseUrl = normalizeApiBaseUrl(
75
+ env.AI_DESK_API_BASE_URL || env.AI_DESK_BACKEND_API_URL || env.AI_DESK_BACKEND_URL,
76
+ ) || daemonConfig?.apiBaseUrl || normalizeApiBaseUrl(DEFAULT_REGISTRY_API_BASE_URL);
77
+ const rcAccountId = trim(env.AI_DESK_RC_ACCOUNT_ID || env.rcAccountId) ||
78
+ daemonConfig?.rcAccountId ||
79
+ sessionIdentity?.rcAccountId ||
80
+ '';
81
+ const rcExtensionId = trim(env.AI_DESK_RC_EXTENSION_ID || env.rcExtensionId) ||
82
+ daemonConfig?.rcExtensionId ||
83
+ sessionIdentity?.rcExtensionId ||
84
+ '';
85
+ if (apiBaseUrl && rcAccountId && rcExtensionId) {
86
+ const tlsEnvValue = parseTruthyEnv(env.AI_DESK_TLS_INSECURE_SKIP_VERIFY);
87
+ return {
88
+ apiBaseUrl,
89
+ rcAccountId,
90
+ rcExtensionId,
91
+ rcUsername: trim(env.AI_DESK_RC_USERNAME || env.rcUsername) || daemonConfig?.rcUsername || '',
92
+ tlsInsecureSkipVerify: tlsEnvValue === null
93
+ ? Boolean(daemonConfig?.tlsInsecureSkipVerify)
94
+ : tlsEnvValue,
95
+ };
96
+ }
97
+ return null;
98
+ }
99
+
100
+ function registryStatePaths(homeDir = os.homedir()) {
101
+ const root = path.join(homeDir, '.aidesktop');
102
+ return {
103
+ root,
104
+ instanceIdPath: path.join(root, 'daemon-instance-id'),
105
+ runStatePath: path.join(root, 'daemon-run-state.json'),
106
+ };
107
+ }
108
+
109
+ function ensureDaemonInstanceId(paths = registryStatePaths()) {
110
+ try {
111
+ fs.mkdirSync(paths.root, { recursive: true });
112
+ if (fs.existsSync(paths.instanceIdPath)) {
113
+ const existing = fs.readFileSync(paths.instanceIdPath, 'utf8').trim();
114
+ if (existing) return existing;
115
+ }
116
+ const next = `daemon-${crypto.randomUUID()}`;
117
+ fs.writeFileSync(paths.instanceIdPath, `${next}\n`, 'utf8');
118
+ return next;
119
+ } catch {
120
+ return `daemon-${crypto.randomUUID()}`;
121
+ }
122
+ }
123
+
124
+ function readRunState(paths = registryStatePaths()) {
125
+ try {
126
+ if (!fs.existsSync(paths.runStatePath)) return null;
127
+ const data = JSON.parse(fs.readFileSync(paths.runStatePath, 'utf8'));
128
+ return data && typeof data === 'object' ? data : null;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ function writeRunState(state, paths = registryStatePaths()) {
135
+ try {
136
+ fs.mkdirSync(paths.root, { recursive: true });
137
+ fs.writeFileSync(paths.runStatePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
138
+ } catch {
139
+ // best effort only
140
+ }
141
+ }
142
+
143
+ function listIpAddresses() {
144
+ const out = [];
145
+ const interfaces = os.networkInterfaces();
146
+ for (const items of Object.values(interfaces)) {
147
+ for (const item of items || []) {
148
+ if (!item.internal && item.address) {
149
+ out.push(item.address);
150
+ }
151
+ }
152
+ }
153
+ return [...new Set(out)];
154
+ }
155
+
156
+ function buildLifecyclePayload({
157
+ event,
158
+ runId,
159
+ seq = 1,
160
+ port,
161
+ version,
162
+ daemonInstanceId,
163
+ }) {
164
+ return {
165
+ daemonInstanceId,
166
+ runId,
167
+ event,
168
+ seq,
169
+ hostname: os.hostname(),
170
+ osUsername: os.userInfo().username,
171
+ daemonVersion: version,
172
+ port,
173
+ ipAddresses: listIpAddresses(),
174
+ };
175
+ }
176
+
177
+ function postJSON(urlString, payload, config) {
178
+ return new Promise((resolve, reject) => {
179
+ const url = new URL(urlString);
180
+ const body = JSON.stringify(payload);
181
+ const transport = url.protocol === 'https:' ? https : http;
182
+ const req = transport.request(
183
+ url,
184
+ {
185
+ method: 'POST',
186
+ ...(url.protocol === 'https:' && config.tlsInsecureSkipVerify
187
+ ? { agent: new https.Agent({ rejectUnauthorized: false }) }
188
+ : {}),
189
+ headers: {
190
+ 'Content-Type': 'application/json',
191
+ 'Content-Length': Buffer.byteLength(body),
192
+ rcAccountId: config.rcAccountId,
193
+ rcExtensionId: config.rcExtensionId,
194
+ ...(config.rcUsername ? { rcUsername: config.rcUsername } : {}),
195
+ },
196
+ timeout: 3000,
197
+ },
198
+ (res) => {
199
+ res.resume();
200
+ res.on('end', () => {
201
+ if (res.statusCode >= 200 && res.statusCode < 300) {
202
+ resolve(true);
203
+ } else {
204
+ reject(new Error(`registry returned ${res.statusCode}`));
205
+ }
206
+ });
207
+ },
208
+ );
209
+ req.on('timeout', () => {
210
+ req.destroy(new Error('registry request timed out'));
211
+ });
212
+ req.on('error', reject);
213
+ req.write(body);
214
+ req.end();
215
+ });
216
+ }
217
+
218
+ async function reportLifecycle(event, { port, version, seq = 1 } = {}) {
219
+ const config = getRegistryConfig();
220
+ if (!config) return false;
221
+
222
+ const paths = registryStatePaths();
223
+ const daemonInstanceId = ensureDaemonInstanceId(paths);
224
+ let runState = readRunState(paths);
225
+ if (event === 'start' || event === 'restart' || !runState?.runId) {
226
+ runState = { runId: crypto.randomUUID(), seq: 0 };
227
+ }
228
+ runState.seq = Math.max(Number(runState.seq || 0) + 1, seq);
229
+ writeRunState(runState, paths);
230
+
231
+ const payload = buildLifecyclePayload({
232
+ event,
233
+ runId: runState.runId,
234
+ seq: runState.seq,
235
+ port,
236
+ version,
237
+ daemonInstanceId,
238
+ });
239
+ await postJSON(`${config.apiBaseUrl}/daemon-instances/lifecycle`, payload, config);
240
+ return true;
241
+ }
242
+
243
+ module.exports = {
244
+ buildLifecyclePayload,
245
+ DEFAULT_REGISTRY_API_BASE_URL,
246
+ getRegistryConfig,
247
+ readRegistrySessionIdentity,
248
+ readRegistryConfigFromDaemonConfig,
249
+ registryStatePaths,
250
+ ensureDaemonInstanceId,
251
+ readRunState,
252
+ writeRunState,
253
+ reportLifecycle,
254
+ };
@@ -0,0 +1,313 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawnSync } = require('child_process');
4
+
5
+ const OFFICIAL_NPM_REGISTRY = 'https://registry.npmjs.org/';
6
+ const DEFAULT_PACKAGE_NAME = '@agent-webui/ai-desk';
7
+ const SKIP_UPDATE_ENV = 'AI_DESK_SKIP_SELF_UPDATE';
8
+ const DEFAULT_UPDATE_COMMANDS = new Set(['start', 'restart', 'upgrade']);
9
+
10
+ function readJSON(filePath) {
11
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
12
+ }
13
+
14
+ function currentVersion(packageRoot) {
15
+ return readJSON(path.join(packageRoot, 'package.json')).version;
16
+ }
17
+
18
+ function isPackagedInstall(packageRoot) {
19
+ return path.resolve(packageRoot).split(path.sep).includes('node_modules');
20
+ }
21
+
22
+ function parseVersion(version) {
23
+ return String(version || '')
24
+ .trim()
25
+ .replace(/^v/, '')
26
+ .split('-')[0]
27
+ .split('.')
28
+ .map((part) => Number.parseInt(part, 10) || 0);
29
+ }
30
+
31
+ function compareVersions(left, right) {
32
+ const leftParts = parseVersion(left);
33
+ const rightParts = parseVersion(right);
34
+ const length = Math.max(leftParts.length, rightParts.length);
35
+ for (let index = 0; index < length; index += 1) {
36
+ const delta = (leftParts[index] || 0) - (rightParts[index] || 0);
37
+ if (delta !== 0) {
38
+ return delta;
39
+ }
40
+ }
41
+ return 0;
42
+ }
43
+
44
+ function npmCommand(env = process.env) {
45
+ if (env.npm_execpath && fs.existsSync(env.npm_execpath)) {
46
+ return {
47
+ command: process.execPath,
48
+ args: [env.npm_execpath],
49
+ };
50
+ }
51
+
52
+ return {
53
+ command: process.platform === 'win32' ? 'npm.cmd' : 'npm',
54
+ args: [],
55
+ };
56
+ }
57
+
58
+ function runNpm(args, options = {}) {
59
+ const npm = npmCommand(options.env || process.env);
60
+ return spawnSync(npm.command, [...npm.args, ...args], {
61
+ encoding: 'utf8',
62
+ ...options,
63
+ });
64
+ }
65
+
66
+ function npmOutput(result) {
67
+ return `${result.stderr || ''}${result.stdout || ''}`.trim();
68
+ }
69
+
70
+ function withOfficialRegistry(args) {
71
+ return [...args, `--registry=${OFFICIAL_NPM_REGISTRY}`];
72
+ }
73
+
74
+ function runNpmWithRegistryFallback(args, options = {}) {
75
+ const primaryResult = runNpm(withOfficialRegistry(args), options);
76
+ if (primaryResult.status === 0) {
77
+ return primaryResult;
78
+ }
79
+
80
+ const fallbackResult = runNpm(args, options);
81
+ if (fallbackResult.status === 0) {
82
+ return fallbackResult;
83
+ }
84
+
85
+ fallbackResult.primaryError = npmOutput(primaryResult);
86
+ return fallbackResult;
87
+ }
88
+
89
+ function latestPublishedVersion(packageName = DEFAULT_PACKAGE_NAME) {
90
+ const result = runNpmWithRegistryFallback(['view', packageName, 'version'], {
91
+ stdio: ['ignore', 'pipe', 'pipe'],
92
+ });
93
+
94
+ if (result.status !== 0) {
95
+ const output = npmOutput(result);
96
+ const primaryError = result.primaryError ? `Official registry error: ${result.primaryError}\n` : '';
97
+ const fallbackError = output ? `Fallback registry error: ${output}` : '';
98
+ const errorMessage = `${primaryError}${fallbackError}`.trim();
99
+ throw new Error(errorMessage || 'Unable to check npm registry');
100
+ }
101
+
102
+ return String(result.stdout || '').trim();
103
+ }
104
+
105
+ function realpathIfExists(targetPath) {
106
+ try {
107
+ return fs.realpathSync(targetPath);
108
+ } catch {
109
+ return '';
110
+ }
111
+ }
112
+
113
+ function isSubPath(childPath, parentPath) {
114
+ const relativePath = path.relative(parentPath, childPath);
115
+ return relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath));
116
+ }
117
+
118
+ function globalNpmRoot() {
119
+ const result = runNpm(['root', '-g'], {
120
+ stdio: ['ignore', 'pipe', 'ignore'],
121
+ });
122
+ if (result.status !== 0) {
123
+ return '';
124
+ }
125
+ return realpathIfExists(String(result.stdout || '').trim());
126
+ }
127
+
128
+ function packagePathFromProjectRoot(projectRoot, packageName = DEFAULT_PACKAGE_NAME) {
129
+ return path.join(projectRoot, 'node_modules', ...packageName.split('/'));
130
+ }
131
+
132
+ function findLocalProjectRoot(packageRoot, packageName = DEFAULT_PACKAGE_NAME) {
133
+ const realPackageRoot = realpathIfExists(packageRoot);
134
+ const realGlobalRoot = globalNpmRoot();
135
+ if (realGlobalRoot && isSubPath(realPackageRoot, realGlobalRoot)) {
136
+ return '';
137
+ }
138
+
139
+ let currentDir = packageRoot;
140
+ while (currentDir && currentDir !== path.dirname(currentDir)) {
141
+ const candidate = realpathIfExists(packagePathFromProjectRoot(currentDir, packageName));
142
+ if (candidate && candidate === realPackageRoot) {
143
+ return currentDir;
144
+ }
145
+ currentDir = path.dirname(currentDir);
146
+ }
147
+
148
+ return '';
149
+ }
150
+
151
+ function installLatestVersion({
152
+ packageName = DEFAULT_PACKAGE_NAME,
153
+ packageRoot,
154
+ latestVersion,
155
+ }) {
156
+ const projectRoot = findLocalProjectRoot(packageRoot, packageName);
157
+ const installTarget = `${packageName}@${latestVersion}`;
158
+
159
+ if (projectRoot) {
160
+ return runNpmWithRegistryFallback(['install', installTarget, '--include=optional'], {
161
+ cwd: projectRoot,
162
+ stdio: 'inherit',
163
+ });
164
+ }
165
+
166
+ return runNpmWithRegistryFallback(['install', '-g', installTarget, '--include=optional'], {
167
+ stdio: 'inherit',
168
+ });
169
+ }
170
+
171
+ function assertInstallSucceeded(result) {
172
+ if (result.error) {
173
+ throw result.error;
174
+ }
175
+
176
+ if (result.status !== 0) {
177
+ const error = new Error(`npm install failed with exit code ${result.status}`);
178
+ error.exitCode = typeof result.status === 'number' ? result.status : 1;
179
+ throw error;
180
+ }
181
+ }
182
+
183
+ function upgradePackage({
184
+ packageName = DEFAULT_PACKAGE_NAME,
185
+ packageRoot,
186
+ installedVersion,
187
+ latestVersion,
188
+ force = false,
189
+ logger = console,
190
+ latestPublishedVersion: latestPublishedVersionFn = latestPublishedVersion,
191
+ installLatestVersion: installLatestVersionFn = (version) => installLatestVersion({
192
+ packageName,
193
+ packageRoot,
194
+ latestVersion: version,
195
+ }),
196
+ } = {}) {
197
+ const current = installedVersion || currentVersion(packageRoot);
198
+ const latest = latestVersion || latestPublishedVersionFn(packageName);
199
+
200
+ if (!force && compareVersions(latest, current) <= 0) {
201
+ logger.log(`AI Desk is already up to date (${current}).`);
202
+ return {
203
+ upgraded: false,
204
+ installedVersion: current,
205
+ latestVersion: latest,
206
+ };
207
+ }
208
+
209
+ const verb = force && compareVersions(latest, current) <= 0 ? 'Reinstalling' : 'Updating';
210
+ logger.log(`${verb} AI Desk from ${current} to ${latest}...`);
211
+ const installResult = installLatestVersionFn(latest);
212
+ assertInstallSucceeded(installResult);
213
+ logger.log(`AI Desk updated to ${latest}.`);
214
+
215
+ return {
216
+ upgraded: true,
217
+ installedVersion: current,
218
+ latestVersion: latest,
219
+ };
220
+ }
221
+
222
+ function requestedCommand(args) {
223
+ for (const arg of args) {
224
+ if (arg === '--') {
225
+ return '';
226
+ }
227
+ if (!arg.startsWith('-')) {
228
+ return arg;
229
+ }
230
+ }
231
+ return '';
232
+ }
233
+
234
+ function shouldCheckForUpdate({
235
+ args,
236
+ isPackagedInstall: packagedInstall,
237
+ skipUpdate,
238
+ updateCommands = DEFAULT_UPDATE_COMMANDS,
239
+ }) {
240
+ if (skipUpdate) {
241
+ return false;
242
+ }
243
+
244
+ if (!packagedInstall || args.includes('--help') || args.includes('-h')) {
245
+ return false;
246
+ }
247
+
248
+ return updateCommands.has(requestedCommand(args));
249
+ }
250
+
251
+ function updateBeforeCommand({
252
+ args,
253
+ packageName = DEFAULT_PACKAGE_NAME,
254
+ packageRoot,
255
+ env = process.env,
256
+ logger = console,
257
+ restartWithUpdatedCli,
258
+ updateCommands = DEFAULT_UPDATE_COMMANDS,
259
+ } = {}) {
260
+ const command = requestedCommand(args);
261
+ if (!shouldCheckForUpdate({
262
+ args,
263
+ isPackagedInstall: isPackagedInstall(packageRoot),
264
+ skipUpdate: Boolean(env[SKIP_UPDATE_ENV]),
265
+ updateCommands,
266
+ })) {
267
+ return { checked: false };
268
+ }
269
+
270
+ logger.log('Checking for AI Desk updates...');
271
+
272
+ let result;
273
+ try {
274
+ result = upgradePackage({
275
+ packageName,
276
+ packageRoot,
277
+ logger,
278
+ });
279
+ } catch (error) {
280
+ if (command === 'upgrade') {
281
+ throw error;
282
+ }
283
+ logger.warn(`Unable to check for AI Desk updates: ${error.message}`);
284
+ return { checked: true, upgraded: false, error };
285
+ }
286
+
287
+ if (result.upgraded && command !== 'upgrade' && typeof restartWithUpdatedCli === 'function') {
288
+ logger.log(`Restarting aidesk ${command} with the updated CLI...`);
289
+ restartWithUpdatedCli(args);
290
+ }
291
+
292
+ return {
293
+ checked: true,
294
+ command,
295
+ ...result,
296
+ };
297
+ }
298
+
299
+ module.exports = {
300
+ DEFAULT_PACKAGE_NAME,
301
+ DEFAULT_UPDATE_COMMANDS,
302
+ SKIP_UPDATE_ENV,
303
+ compareVersions,
304
+ currentVersion,
305
+ findLocalProjectRoot,
306
+ installLatestVersion,
307
+ isPackagedInstall,
308
+ latestPublishedVersion,
309
+ requestedCommand,
310
+ shouldCheckForUpdate,
311
+ updateBeforeCommand,
312
+ upgradePackage,
313
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-webui/ai-desk-daemon",
3
- "version": "1.0.60",
3
+ "version": "1.0.61-beta4",
4
4
  "description": "AI Desk Daemon - CLI tool for managing the AI Desk daemon service",
5
5
  "workspaces": [
6
6
  "packages/*"
@@ -39,16 +39,16 @@
39
39
  "chalk": "^4.1.2"
40
40
  },
41
41
  "optionalDependencies": {
42
- "@agent-webui/ai-desk-daemon-darwin-arm64": "1.0.60",
43
- "@agent-webui/ai-desk-daemon-darwin-x64": "1.0.60",
44
- "@agent-webui/ai-desk-daemon-linux-arm64": "1.0.60",
45
- "@agent-webui/ai-desk-daemon-linux-x64": "1.0.60",
46
- "@agent-webui/ai-desk-daemon-win32-x64": "1.0.60",
47
- "@agent-webui/ai-desk-python-darwin-arm64": "1.0.60",
48
- "@agent-webui/ai-desk-python-darwin-x64": "1.0.60",
49
- "@agent-webui/ai-desk-python-linux-arm64": "1.0.60",
50
- "@agent-webui/ai-desk-python-linux-x64": "1.0.60",
51
- "@agent-webui/ai-desk-python-win32-x64": "1.0.60"
42
+ "@agent-webui/ai-desk-daemon-darwin-arm64": "1.0.61-beta4",
43
+ "@agent-webui/ai-desk-daemon-darwin-x64": "1.0.61-beta4",
44
+ "@agent-webui/ai-desk-daemon-linux-arm64": "1.0.61-beta4",
45
+ "@agent-webui/ai-desk-daemon-linux-x64": "1.0.61-beta4",
46
+ "@agent-webui/ai-desk-daemon-win32-x64": "1.0.61-beta4",
47
+ "@agent-webui/ai-desk-python-darwin-arm64": "1.0.61-beta4",
48
+ "@agent-webui/ai-desk-python-darwin-x64": "1.0.61-beta4",
49
+ "@agent-webui/ai-desk-python-linux-arm64": "1.0.61-beta4",
50
+ "@agent-webui/ai-desk-python-linux-x64": "1.0.61-beta4",
51
+ "@agent-webui/ai-desk-python-win32-x64": "1.0.61-beta4"
52
52
  },
53
53
  "repository": {
54
54
  "type": "git",