@foxden-app/foxclaw 0.5.2 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.3 - 2026-06-04
6
+
7
+ ### 中文
8
+ - 修复 0.5.2 中 `/update` 完成回报丢失的问题:Linux 自升级子进程现在通过独立的 user systemd transient service 运行,不再留在 `foxclaw.service` control group 里,因此 `KillMode=control-group` 重启服务时不会提前杀掉 updater。
9
+ - 保留 `KillMode=control-group` 的 app-server 清理能力,同时让升级进程能在服务重启后继续写入完成状态,由新服务启动后的轮询发送 Telegram 成功/失败回报。
10
+
11
+ ### English
12
+ - Fixed missing `/update` completion reports in 0.5.2: on Linux the self-update worker now runs in a separate user systemd transient service instead of the `foxclaw.service` control group, so `KillMode=control-group` no longer kills the updater during service restart.
13
+ - Kept `KillMode=control-group` app-server cleanup while allowing the updater to write its final status after restart; the newly started service polls that status and sends the Telegram success/failure report.
14
+
5
15
  ## 0.5.2 - 2026-06-04
6
16
 
7
17
  ### 中文
package/dist/update.d.ts CHANGED
@@ -40,6 +40,12 @@ interface PerformSelfUpdateOptions {
40
40
  codexCliBin?: string;
41
41
  env?: NodeJS.ProcessEnv;
42
42
  }
43
+ export interface SelfUpdateLaunchCommand {
44
+ command: string;
45
+ args: string[];
46
+ env: NodeJS.ProcessEnv;
47
+ viaSystemdRun: boolean;
48
+ }
43
49
  export interface SelfUpdateOutcome {
44
50
  ok: boolean;
45
51
  fromVersion: string;
@@ -53,5 +59,16 @@ export declare function resolveCodexUpdateInstaller(codexCliBin: string, nodePat
53
59
  export declare function readSelfUpdateStatus(statusFile: string): SelfUpdateStatus | null;
54
60
  export declare function writeSelfUpdateStatus(statusFile: string, status: SelfUpdateStatus): void;
55
61
  export declare function createSelfUpdateRuntime(options: CreateSelfUpdateRuntimeOptions): SelfUpdateRuntime;
62
+ export declare function buildSelfUpdateLaunchCommand(options: {
63
+ entryPoint: string;
64
+ nodePath: string;
65
+ statusFile: string;
66
+ logPath: string;
67
+ codexCliBin?: string;
68
+ env?: NodeJS.ProcessEnv;
69
+ platform?: NodeJS.Platform;
70
+ systemdRunPath?: string | null;
71
+ unitName?: string;
72
+ }): SelfUpdateLaunchCommand;
56
73
  export declare function performSelfUpdate(options: PerformSelfUpdateOptions): SelfUpdateOutcome;
57
74
  export {};
package/dist/update.js CHANGED
@@ -171,12 +171,33 @@ export function createSelfUpdateRuntime(options) {
171
171
  fs.mkdirSync(path.dirname(options.logPath), { recursive: true });
172
172
  const logFd = fs.openSync(options.logPath, 'a', 0o600);
173
173
  try {
174
- const child = spawn(options.nodePath, [options.entryPoint, 'update', '--notification-file', statusFile], {
175
- detached: true,
176
- stdio: ['ignore', logFd, logFd],
177
- env: options.codexCliBin ? { ...process.env, CODEX_CLI_BIN: options.codexCliBin } : process.env,
174
+ const launch = buildSelfUpdateLaunchCommand({
175
+ entryPoint: options.entryPoint,
176
+ nodePath: options.nodePath,
177
+ statusFile,
178
+ logPath: options.logPath,
179
+ ...(options.codexCliBin ? { codexCliBin: options.codexCliBin } : {}),
178
180
  });
179
- child.unref();
181
+ if (launch.viaSystemdRun) {
182
+ const result = spawnSync(launch.command, launch.args, {
183
+ stdio: ['ignore', logFd, logFd],
184
+ env: launch.env,
185
+ });
186
+ if (result.error) {
187
+ throw result.error;
188
+ }
189
+ if (result.status !== 0) {
190
+ throw new Error(`${launch.command} ${launch.args.join(' ')} exited with status ${result.status ?? 'unknown'}.`);
191
+ }
192
+ }
193
+ else {
194
+ const child = spawn(launch.command, launch.args, {
195
+ detached: true,
196
+ stdio: ['ignore', logFd, logFd],
197
+ env: launch.env,
198
+ });
199
+ child.unref();
200
+ }
180
201
  }
181
202
  catch (error) {
182
203
  writeSelfUpdateStatus(statusFile, {
@@ -205,6 +226,40 @@ export function createSelfUpdateRuntime(options) {
205
226
  },
206
227
  };
207
228
  }
229
+ export function buildSelfUpdateLaunchCommand(options) {
230
+ const env = options.codexCliBin
231
+ ? { ...(options.env ?? process.env), CODEX_CLI_BIN: options.codexCliBin }
232
+ : { ...(options.env ?? process.env) };
233
+ const updateArgs = [options.entryPoint, 'update', '--notification-file', options.statusFile];
234
+ const platform = options.platform ?? process.platform;
235
+ const systemdRunPath = options.systemdRunPath === undefined
236
+ ? resolveCommand('systemd-run', env)
237
+ : options.systemdRunPath;
238
+ if (platform === 'linux' && systemdRunPath) {
239
+ const unitName = options.unitName ?? `foxclaw-update-${process.pid}-${Date.now()}`;
240
+ return {
241
+ command: systemdRunPath,
242
+ args: [
243
+ '--user',
244
+ '--collect',
245
+ `--unit=${unitName}`,
246
+ `--property=StandardOutput=append:${options.logPath}`,
247
+ `--property=StandardError=append:${options.logPath}`,
248
+ ...systemdSetEnvArgs(env),
249
+ options.nodePath,
250
+ ...updateArgs,
251
+ ],
252
+ env,
253
+ viaSystemdRun: true,
254
+ };
255
+ }
256
+ return {
257
+ command: options.nodePath,
258
+ args: updateArgs,
259
+ env,
260
+ viaSystemdRun: false,
261
+ };
262
+ }
208
263
  export function performSelfUpdate(options) {
209
264
  const env = options.env ?? process.env;
210
265
  let toVersion = null;
@@ -295,6 +350,31 @@ function executableCandidates(commandName, nodePath, env, preferred = []) {
295
350
  ...(env.PATH || '').split(path.delimiter).filter(Boolean).map((dir) => path.join(dir, commandName)),
296
351
  ].filter((candidate, index, all) => candidate && all.indexOf(candidate) === index);
297
352
  }
353
+ function resolveCommand(commandName, env) {
354
+ for (const directory of (env.PATH || '').split(path.delimiter).filter(Boolean)) {
355
+ const candidate = path.join(directory, commandName);
356
+ if (fs.existsSync(candidate)) {
357
+ return candidate;
358
+ }
359
+ }
360
+ for (const fallback of ['/usr/bin', '/bin', '/usr/local/bin']) {
361
+ const candidate = path.join(fallback, commandName);
362
+ if (fs.existsSync(candidate)) {
363
+ return candidate;
364
+ }
365
+ }
366
+ return null;
367
+ }
368
+ function systemdSetEnvArgs(env) {
369
+ return Object.entries(env)
370
+ .filter((entry) => {
371
+ const [key, value] = entry;
372
+ return /^[A-Za-z_][A-Za-z0-9_]*$/.test(key)
373
+ && typeof value === 'string'
374
+ && !value.includes('\0');
375
+ })
376
+ .map(([key, value]) => `--setenv=${key}=${value}`);
377
+ }
298
378
  function buildInstallerEnv(entryPoint, installer, env) {
299
379
  const pnpmHome = installer.manager === 'pnpm'
300
380
  ? installer.pnpmHome ?? inferPnpmHomeFromEntryPoint(entryPoint)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",