@foxden-app/foxclaw 0.5.12 → 0.5.14
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 +22 -0
- package/dist/auth/mirror.d.ts +1 -0
- package/dist/auth/mirror.js +66 -8
- package/dist/controller/controller.js +14 -6
- package/dist/main.js +37 -4
- package/dist/systemd.d.ts +7 -0
- package/dist/systemd.js +17 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.14 - 2026-06-08
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- Linux `foxclaw start` / `foxclaw restart` / `install-systemd` 现在如果检测到自己正运行在 `foxclaw.service` cgroup 内,会通过一次性的 `systemd-run --user` helper 在服务外执行重启,避免命令执行者被自己重启时杀掉导致半截输出或不确定状态。
|
|
9
|
+
- 继续保留 systemd 作为稳定守护层:主 service 仍由 `Restart=always`、`KillMode=control-group` 和 user linger 保活;重启编排则交给短生命周期 helper,避免再引入一个更脆弱的常驻 Node 守护进程。
|
|
10
|
+
|
|
11
|
+
### English
|
|
12
|
+
- On Linux, `foxclaw start`, `foxclaw restart`, and `install-systemd` now detect when they are running inside the `foxclaw.service` cgroup and delegate the actual restart to a one-shot `systemd-run --user` helper outside that cgroup, avoiding half-written output or uncertain state when the caller would otherwise kill itself.
|
|
13
|
+
- systemd remains the stable supervisor with `Restart=always`, `KillMode=control-group`, and user linger; restart orchestration moves to a short-lived helper instead of adding another long-running Node watchdog.
|
|
14
|
+
|
|
15
|
+
## 0.5.13 - 2026-06-08
|
|
16
|
+
|
|
17
|
+
### 中文
|
|
18
|
+
- `auth.json_team_<localpart>` 候选现在会校验文件内 ChatGPT email localpart 是否匹配候选名;不匹配时 `/auth` 标为无效,避免继续显示另一位 seat 的额度。
|
|
19
|
+
- 本机 auth mirror 不再传播 team 候选名与文件身份不一致的 auth,并会在启动 reconcile 时用仍然匹配候选名的 runtime 副本修复错误副本。
|
|
20
|
+
- `/auth refresh all` 和当前候选额度刷新会跳过身份与 `team_` 候选名不匹配的文件,避免脏额度快照再次写入。
|
|
21
|
+
|
|
22
|
+
### English
|
|
23
|
+
- `auth.json_team_<localpart>` candidates now verify that the ChatGPT email local part inside the auth file matches the candidate name; mismatches are marked invalid in `/auth` instead of displaying another seat's quota.
|
|
24
|
+
- The local auth mirror no longer propagates team candidates whose filename identity and auth payload disagree, and startup reconciliation can repair bad copies from a runtime copy that still matches the candidate name.
|
|
25
|
+
- `/auth refresh all` and current-candidate quota refresh now skip files whose identity does not match the `team_` candidate name, preventing dirty quota snapshots from being recorded again.
|
|
26
|
+
|
|
5
27
|
## 0.5.12 - 2026-06-08
|
|
6
28
|
|
|
7
29
|
### 中文
|
package/dist/auth/mirror.d.ts
CHANGED
|
@@ -109,3 +109,4 @@ export declare function isAuthCandidateName(name: string): boolean;
|
|
|
109
109
|
export declare function readChatGptAuthRecord(filePath: string): Promise<ChatGptAuthRecord | null>;
|
|
110
110
|
export declare function readChatGptAuthMetadata(filePath: string): Promise<ChatGptAuthMetadata | null>;
|
|
111
111
|
export declare function parseChatGptAuthMetadata(raw: string): ChatGptAuthMetadata | null;
|
|
112
|
+
export declare function chatGptAuthMetadataMatchesCandidateName(candidateName: string, metadata: Pick<ChatGptAuthMetadata, 'email'>): boolean;
|
package/dist/auth/mirror.js
CHANGED
|
@@ -159,9 +159,18 @@ export class AuthCandidateMirror {
|
|
|
159
159
|
const destination = await readChatGptAuthRecord(destinationPath);
|
|
160
160
|
if (!destination)
|
|
161
161
|
return null;
|
|
162
|
+
const destinationMatchesName = chatGptAuthMetadataMatchesCandidateName(candidateName, destination);
|
|
162
163
|
const sources = await this.collectAuthRecords();
|
|
163
164
|
const newest = sources
|
|
164
|
-
.filter(entry =>
|
|
165
|
+
.filter((entry) => {
|
|
166
|
+
if (!chatGptAuthMetadataMatchesCandidateName(candidateName, entry.record)) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (!destinationMatchesName) {
|
|
170
|
+
return entry.candidateName === candidateName;
|
|
171
|
+
}
|
|
172
|
+
return authRecordsCompatible(entry.record, destination);
|
|
173
|
+
})
|
|
165
174
|
.reduce((current, entry) => (!current || entry.record.lastRefreshMs > current.record.lastRefreshMs ? entry : current), null);
|
|
166
175
|
if (!newest || newest.record.lastRefreshMs <= destination.lastRefreshMs) {
|
|
167
176
|
return null;
|
|
@@ -200,10 +209,17 @@ export class AuthCandidateMirror {
|
|
|
200
209
|
skipped += this.runtimes.length;
|
|
201
210
|
continue;
|
|
202
211
|
}
|
|
212
|
+
if (!chatGptAuthMetadataMatchesCandidateName(name, canonical)) {
|
|
213
|
+
skipped += this.runtimes.length;
|
|
214
|
+
this.logger.warn('auth.mirror.distribution_identity_mismatch', { name });
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
203
217
|
for (const runtime of this.runtimes) {
|
|
204
218
|
const destinationPath = path.join(runtime.authDir, name);
|
|
205
219
|
const destination = await readChatGptAuthRecord(destinationPath);
|
|
206
|
-
if (destination
|
|
220
|
+
if (destination
|
|
221
|
+
&& chatGptAuthMetadataMatchesCandidateName(name, destination)
|
|
222
|
+
&& !authRecordsCompatible(destination, canonical)) {
|
|
207
223
|
skipped += 1;
|
|
208
224
|
this.logger.warn('auth.mirror.distribution_conflict', { runtimeId: runtime.id, name });
|
|
209
225
|
continue;
|
|
@@ -232,8 +248,11 @@ export class AuthCandidateMirror {
|
|
|
232
248
|
if (!metadata) {
|
|
233
249
|
return { ok: false, imported: false, reason: 'invalid ChatGPT auth payload' };
|
|
234
250
|
}
|
|
251
|
+
if (!chatGptAuthMetadataMatchesCandidateName(candidateName, metadata)) {
|
|
252
|
+
return { ok: false, imported: false, reason: 'candidate auth identity does not match candidate name' };
|
|
253
|
+
}
|
|
235
254
|
const existing = (await this.collectAuthRecords())
|
|
236
|
-
.filter(entry => entry.candidateName === candidateName);
|
|
255
|
+
.filter(entry => entry.candidateName === candidateName && chatGptAuthMetadataMatchesCandidateName(candidateName, entry.record));
|
|
237
256
|
const conflicting = existing.find(entry => !authRecordsCompatible(entry.record, metadata));
|
|
238
257
|
if (conflicting) {
|
|
239
258
|
return { ok: false, imported: false, reason: 'same candidate belongs to a different account or ChatGPT user' };
|
|
@@ -286,9 +305,15 @@ export class AuthCandidateMirror {
|
|
|
286
305
|
const record = await readChatGptAuthRecord(sourcePath);
|
|
287
306
|
if (!record)
|
|
288
307
|
return false;
|
|
308
|
+
if (!chatGptAuthMetadataMatchesCandidateName(name, record)) {
|
|
309
|
+
this.logger.warn('auth.mirror.identity_mismatch', { runtimeId: runtime.id, name });
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
289
312
|
const canonicalPath = path.join(this.canonicalDir, name);
|
|
290
313
|
const canonical = await readChatGptAuthRecord(canonicalPath);
|
|
291
|
-
if (canonical
|
|
314
|
+
if (canonical
|
|
315
|
+
&& chatGptAuthMetadataMatchesCandidateName(name, canonical)
|
|
316
|
+
&& !authRecordsCompatible(canonical, record)) {
|
|
292
317
|
this.logger.warn('auth.mirror.account_conflict', { runtimeId: runtime.id, name });
|
|
293
318
|
return false;
|
|
294
319
|
}
|
|
@@ -315,7 +340,15 @@ export class AuthCandidateMirror {
|
|
|
315
340
|
await atomicWrite(canonicalPath, record.raw);
|
|
316
341
|
for (const target of this.runtimes) {
|
|
317
342
|
if (target.id !== runtime.id) {
|
|
318
|
-
|
|
343
|
+
const targetPath = path.join(target.authDir, name);
|
|
344
|
+
const targetRecord = await readChatGptAuthRecord(targetPath);
|
|
345
|
+
if (targetRecord
|
|
346
|
+
&& chatGptAuthMetadataMatchesCandidateName(name, targetRecord)
|
|
347
|
+
&& !authRecordsCompatible(targetRecord, record)) {
|
|
348
|
+
this.logger.warn('auth.mirror.target_conflict', { sourceRuntimeId: runtime.id, targetRuntimeId: target.id, name });
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
await atomicWrite(targetPath, record.raw);
|
|
319
352
|
}
|
|
320
353
|
}
|
|
321
354
|
this.lastSyncedRefresh.set(name, record.lastRefreshMs);
|
|
@@ -422,12 +455,17 @@ export class AuthCandidateMirror {
|
|
|
422
455
|
})))).filter((entry) => entry.record !== null);
|
|
423
456
|
if (records.length === 0)
|
|
424
457
|
return;
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
458
|
+
const trustedRecords = records.filter(entry => chatGptAuthMetadataMatchesCandidateName(name, entry.record));
|
|
459
|
+
if (trustedRecords.length === 0) {
|
|
460
|
+
this.logger.warn('auth.mirror.startup_identity_mismatch', { name });
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const reference = trustedRecords[0].record;
|
|
464
|
+
if (trustedRecords.some(entry => !authRecordsCompatible(reference, entry.record))) {
|
|
427
465
|
this.logger.warn('auth.mirror.startup_conflict', { name });
|
|
428
466
|
return;
|
|
429
467
|
}
|
|
430
|
-
const newest =
|
|
468
|
+
const newest = trustedRecords.reduce((current, entry) => (entry.record.lastRefreshMs > current.record.lastRefreshMs ? entry : current));
|
|
431
469
|
this.lastSyncedRefresh.set(name, newest.record.lastRefreshMs);
|
|
432
470
|
for (const destination of paths) {
|
|
433
471
|
await atomicWrite(destination, newest.record.raw);
|
|
@@ -560,6 +598,26 @@ export function parseChatGptAuthMetadata(raw) {
|
|
|
560
598
|
return null;
|
|
561
599
|
}
|
|
562
600
|
}
|
|
601
|
+
export function chatGptAuthMetadataMatchesCandidateName(candidateName, metadata) {
|
|
602
|
+
const expectedLocalPart = expectedTeamCandidateEmailLocalPart(candidateName);
|
|
603
|
+
if (!expectedLocalPart || !metadata.email) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
const actualLocalPart = metadata.email.split('@', 1)[0]?.trim().toLowerCase() ?? '';
|
|
607
|
+
return actualLocalPart === expectedLocalPart;
|
|
608
|
+
}
|
|
609
|
+
function expectedTeamCandidateEmailLocalPart(candidateName) {
|
|
610
|
+
const prefix = 'auth.json_team_';
|
|
611
|
+
if (!candidateName.startsWith(prefix)) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
const raw = candidateName.slice(prefix.length).trim().toLowerCase();
|
|
615
|
+
if (!raw || raw.includes('/') || raw.includes('\\')) {
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
const localPart = raw.includes('@') ? raw.split('@', 1)[0] : raw;
|
|
619
|
+
return /^[a-z0-9][a-z0-9._+-]*$/.test(localPart) ? localPart : null;
|
|
620
|
+
}
|
|
563
621
|
function chatGptQuotaIdentityId(accountId, userId, email) {
|
|
564
622
|
if (userId) {
|
|
565
623
|
return `${accountId}:user:${userId}`;
|
|
@@ -3,7 +3,7 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { normalizeLocale, t } from '../i18n.js';
|
|
6
|
-
import { parseChatGptAuthMetadata, readChatGptAuthMetadata } from '../auth/mirror.js';
|
|
6
|
+
import { chatGptAuthMetadataMatchesCandidateName, parseChatGptAuthMetadata, readChatGptAuthMetadata, } from '../auth/mirror.js';
|
|
7
7
|
import { parseCommand } from './commands.js';
|
|
8
8
|
import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPanelKeyboard, buildThreadListKeyboard, buildThreadsKeyboard, clampEffortToModel, formatAccessPresetLabel, formatActiveTurnMessageModeLabel, formatAccessSettingsMessage, formatApprovalPolicyLabel, formatCollaborationModeLabel, formatModelSettingsMessage, formatSandboxModeLabel, formatServiceTierStatusLabel, formatSetupPanelMessage, formatThreadContextSummary, formatThreadsMessage, formatWeixinAccessCopyPaste, formatWeixinModelCopyPaste, formatWeixinThreadsCopyPaste, formatWeixinWhereNavCopyPaste, formatWhereMessage, normalizeRequestedEffort, resolveCurrentModel, resolveActiveTurnMessageMode, resolveRequestedModel, } from './presentation.js';
|
|
9
9
|
import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
|
|
@@ -5410,7 +5410,7 @@ export class BridgeSessionCore {
|
|
|
5410
5410
|
try {
|
|
5411
5411
|
for (const candidate of candidates) {
|
|
5412
5412
|
const before = await readChatGptAuthMetadata(candidate.path);
|
|
5413
|
-
if (!before) {
|
|
5413
|
+
if (!before || !chatGptAuthMetadataMatchesCandidateName(candidate.name, before)) {
|
|
5414
5414
|
result.skipped.push(candidate.name);
|
|
5415
5415
|
continue;
|
|
5416
5416
|
}
|
|
@@ -5427,7 +5427,9 @@ export class BridgeSessionCore {
|
|
|
5427
5427
|
throw new Error('Codex did not return ChatGPT rate limits after refresh');
|
|
5428
5428
|
}
|
|
5429
5429
|
const after = await readChatGptAuthMetadata(candidate.path);
|
|
5430
|
-
if (!after
|
|
5430
|
+
if (!after
|
|
5431
|
+
|| !chatGptAuthMetadataMatchesCandidateName(candidate.name, after)
|
|
5432
|
+
|| !chatGptAuthMetadataCompatible(before, after)) {
|
|
5431
5433
|
throw new Error('refreshed auth identity did not match the original candidate');
|
|
5432
5434
|
}
|
|
5433
5435
|
if (after.lastRefreshMs <= before.lastRefreshMs) {
|
|
@@ -5655,6 +5657,9 @@ export class BridgeSessionCore {
|
|
|
5655
5657
|
}
|
|
5656
5658
|
try {
|
|
5657
5659
|
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
5660
|
+
if (!metadata || !chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)) {
|
|
5661
|
+
return;
|
|
5662
|
+
}
|
|
5658
5663
|
const snapshot = selectCodexRateLimitSnapshot(await this.app.readAccountRateLimits());
|
|
5659
5664
|
if (!snapshot) {
|
|
5660
5665
|
return;
|
|
@@ -5701,13 +5706,16 @@ export class BridgeSessionCore {
|
|
|
5701
5706
|
async readCodexAuthCandidateQuotaIdentities(candidates) {
|
|
5702
5707
|
const entries = await Promise.all(candidates.map(async (candidate) => {
|
|
5703
5708
|
const metadata = await readChatGptAuthMetadata(candidate.path);
|
|
5704
|
-
|
|
5709
|
+
const metadataMatchesName = metadata
|
|
5710
|
+
? chatGptAuthMetadataMatchesCandidateName(candidate.name, metadata)
|
|
5711
|
+
: false;
|
|
5712
|
+
candidate.credentialKind = metadata && metadataMatchesName
|
|
5705
5713
|
? 'chatgpt'
|
|
5706
5714
|
: await isCodexApiKeyAuthCandidate(candidate.path)
|
|
5707
5715
|
? 'api-key'
|
|
5708
5716
|
: 'invalid';
|
|
5709
|
-
candidate.credentialLastRefreshMs = metadata
|
|
5710
|
-
return [candidate.name, metadata ? {
|
|
5717
|
+
candidate.credentialLastRefreshMs = metadata && metadataMatchesName ? metadata.lastRefreshMs : null;
|
|
5718
|
+
return [candidate.name, metadata && metadataMatchesName ? {
|
|
5711
5719
|
accountId: metadata.accountId,
|
|
5712
5720
|
quotaIdentityId: metadata.quotaIdentityId,
|
|
5713
5721
|
} : null];
|
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
10
10
|
import { APP_HOME, DEFAULT_CODEX_TELEGRAM_HOME, DEFAULT_ENV_PATH, DEFAULT_LOG_PATH, DEFAULT_STATUS_PATH, getLoadedEnvPath, loadConfig, loadEnv, } from './config.js';
|
|
11
11
|
import { acquireProcessLock, LockHeldError } from './lock.js';
|
|
12
12
|
import { readRuntimeStatus, writeRuntimeStatus } from './runtime.js';
|
|
13
|
-
import { buildFoxclawSystemdUnitText, refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns } from './systemd.js';
|
|
13
|
+
import { buildFoxclawSystemdUnitText, buildSystemdRestartHelperArgs, cgroupContainsSystemdUnit, refreshFoxclawExecStartDropIns, removeFoxclawExecStartDropIns, } from './systemd.js';
|
|
14
14
|
import { createSelfUpdateRuntime, inferPnpmHomeFromEntryPoint, performSelfUpdate, readSelfUpdateStatus, writeSelfUpdateStatus, } from './update.js';
|
|
15
15
|
const rawCommand = process.argv[2];
|
|
16
16
|
const command = rawCommand || 'serve';
|
|
@@ -1305,13 +1305,46 @@ function installSystemd() {
|
|
|
1305
1305
|
ensureSystemdUserLingerEnabled();
|
|
1306
1306
|
spawnChecked('systemctl', ['--user', 'daemon-reload']);
|
|
1307
1307
|
spawnChecked('systemctl', ['--user', 'enable', unitName]);
|
|
1308
|
+
restartSystemdUnit(unitName);
|
|
1309
|
+
console.log(`Installed ${unitPath}`);
|
|
1310
|
+
console.log(`Status: systemctl --user status ${unitName}`);
|
|
1311
|
+
console.log(`Logs: journalctl --user -u ${unitName} -f`);
|
|
1312
|
+
}
|
|
1313
|
+
function restartSystemdUnit(unitName) {
|
|
1314
|
+
if (isCurrentProcessInSystemdUnit(unitName)) {
|
|
1315
|
+
const systemdRun = resolveCommand('systemd-run');
|
|
1316
|
+
if (systemdRun) {
|
|
1317
|
+
const helperUnitName = `foxclaw-restart-${process.pid}-${Date.now()}`;
|
|
1318
|
+
const result = spawnSync(systemdRun, buildSystemdRestartHelperArgs({
|
|
1319
|
+
unitName,
|
|
1320
|
+
helperUnitName,
|
|
1321
|
+
delaySeconds: 1,
|
|
1322
|
+
}), { stdio: 'inherit' });
|
|
1323
|
+
if (result.status === 0) {
|
|
1324
|
+
console.log(`[OK] scheduled ${unitName} restart via transient systemd helper: ${helperUnitName}`);
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
console.log('[WARN] transient systemd restart helper failed; falling back to direct restart.');
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
console.log('[WARN] systemd-run not found; falling back to direct restart.');
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1308
1333
|
const restarted = spawnSync('systemctl', ['--user', 'restart', unitName], { stdio: 'inherit' });
|
|
1309
1334
|
if (restarted.status !== 0) {
|
|
1310
1335
|
spawnChecked('systemctl', ['--user', 'start', unitName]);
|
|
1311
1336
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1337
|
+
}
|
|
1338
|
+
function isCurrentProcessInSystemdUnit(unitName) {
|
|
1339
|
+
if (process.platform !== 'linux') {
|
|
1340
|
+
return false;
|
|
1341
|
+
}
|
|
1342
|
+
try {
|
|
1343
|
+
return cgroupContainsSystemdUnit(fs.readFileSync('/proc/self/cgroup', 'utf8'), unitName);
|
|
1344
|
+
}
|
|
1345
|
+
catch {
|
|
1346
|
+
return false;
|
|
1347
|
+
}
|
|
1315
1348
|
}
|
|
1316
1349
|
function ensureSystemdUserLingerEnabled() {
|
|
1317
1350
|
if (process.platform !== 'linux') {
|
package/dist/systemd.d.ts
CHANGED
|
@@ -11,7 +11,14 @@ export interface FoxclawSystemdUnitTextOptions {
|
|
|
11
11
|
pathValue: string;
|
|
12
12
|
execStart: string;
|
|
13
13
|
}
|
|
14
|
+
export interface SystemdRestartHelperOptions {
|
|
15
|
+
unitName: string;
|
|
16
|
+
helperUnitName: string;
|
|
17
|
+
delaySeconds: number;
|
|
18
|
+
}
|
|
14
19
|
export declare function buildFoxclawSystemdUnitText(options: FoxclawSystemdUnitTextOptions): string;
|
|
20
|
+
export declare function cgroupContainsSystemdUnit(cgroupText: string, unitName: string): boolean;
|
|
21
|
+
export declare function buildSystemdRestartHelperArgs(options: SystemdRestartHelperOptions): string[];
|
|
15
22
|
export declare function refreshFoxclawExecStartDropIns(userSystemdDir: string, unitName: string, escapedEntryPoint: string): SystemdDropInUpdate[];
|
|
16
23
|
export declare function removeFoxclawExecStartDropIns(userSystemdDir: string, unitName: string): SystemdDropInUpdate[];
|
|
17
24
|
export declare function refreshFoxclawExecStartText(text: string, escapedEntryPoint: string): {
|
package/dist/systemd.js
CHANGED
|
@@ -29,6 +29,23 @@ KillMode=control-group
|
|
|
29
29
|
WantedBy=default.target
|
|
30
30
|
`;
|
|
31
31
|
}
|
|
32
|
+
export function cgroupContainsSystemdUnit(cgroupText, unitName) {
|
|
33
|
+
const escapedUnitName = unitName.replace(/-/g, '\\x2d');
|
|
34
|
+
return cgroupText
|
|
35
|
+
.split(/\r?\n/)
|
|
36
|
+
.some((line) => line.includes(`/${unitName}`) || line.includes(`/${escapedUnitName}`));
|
|
37
|
+
}
|
|
38
|
+
export function buildSystemdRestartHelperArgs(options) {
|
|
39
|
+
return [
|
|
40
|
+
'--user',
|
|
41
|
+
'--collect',
|
|
42
|
+
`--unit=${options.helperUnitName}`,
|
|
43
|
+
'--description=Restart FoxClaw service outside foxclaw.service cgroup',
|
|
44
|
+
'/bin/sh',
|
|
45
|
+
'-lc',
|
|
46
|
+
`sleep ${Math.max(0, options.delaySeconds)}; exec systemctl --user restart ${options.unitName}`,
|
|
47
|
+
];
|
|
48
|
+
}
|
|
32
49
|
export function refreshFoxclawExecStartDropIns(userSystemdDir, unitName, escapedEntryPoint) {
|
|
33
50
|
const dropInDir = path.join(userSystemdDir, `${unitName}.d`);
|
|
34
51
|
let names;
|