@narumitw/pi-caffeinate 0.13.0 → 0.13.1
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 +5 -3
- package/package.json +1 -1
- package/src/caffeinate.ts +43 -391
- package/src/inhibitor-process.ts +43 -0
- package/src/inhibitors.ts +142 -0
- package/src/settings.ts +152 -0
package/README.md
CHANGED
|
@@ -115,8 +115,7 @@ Missing, invalid, or deleted settings default back to `display` mode on every su
|
|
|
115
115
|
Compatibility: older versions used `pi-caffeinate-settings.json`. During the migration window, a
|
|
116
116
|
legacy-only file is automatically migrated to `pi-caffeinate.json` with a warning. If both files
|
|
117
117
|
exist, `pi-caffeinate.json` wins and the legacy file is ignored. The legacy filename is deprecated
|
|
118
|
-
and
|
|
119
|
-
next major release.
|
|
118
|
+
and will be removed in a future major release.
|
|
120
119
|
|
|
121
120
|
### Environment variables
|
|
122
121
|
|
|
@@ -157,13 +156,16 @@ The default display-awake mode prioritizes uninterrupted long-running Pi work ac
|
|
|
157
156
|
```txt
|
|
158
157
|
extensions/pi-caffeinate/
|
|
159
158
|
├── src/
|
|
160
|
-
│
|
|
159
|
+
│ ├── caffeinate.ts # Pi entrypoint and lifecycle orchestration
|
|
160
|
+
│ └── *.ts # Package-local inhibitor and settings modules
|
|
161
161
|
├── README.md
|
|
162
162
|
├── LICENSE
|
|
163
163
|
├── tsconfig.json
|
|
164
164
|
└── package.json
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
Only `caffeinate.ts` is a Pi entrypoint; the other source modules are internal.
|
|
168
|
+
|
|
167
169
|
## 🔎 Keywords
|
|
168
170
|
|
|
169
171
|
Pi extension, Pi coding agent, caffeinate, prevent sleep, keep awake, sleep inhibitor, AI agent automation, long-running coding task, TypeScript Pi package.
|
package/package.json
CHANGED
package/src/caffeinate.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { constants, existsSync } from "node:fs";
|
|
3
|
-
import { access, link, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
-
import { homedir } from "node:os";
|
|
5
|
-
import { dirname, join } from "node:path";
|
|
6
|
-
import pathModule from "node:path";
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
7
2
|
import process from "node:process";
|
|
3
|
+
import { startInhibitorProcess, stopInhibitorProcess } from "./inhibitor-process.js";
|
|
4
|
+
import {
|
|
5
|
+
formatMode,
|
|
6
|
+
getInhibitorCommand,
|
|
7
|
+
type InhibitorCommand,
|
|
8
|
+
splitCommand,
|
|
9
|
+
windowsInhibitorScript,
|
|
10
|
+
} from "./inhibitors.js";
|
|
11
|
+
import {
|
|
12
|
+
type CaffeinateMode,
|
|
13
|
+
loadSettings,
|
|
14
|
+
normalizeCaffeinateSettings,
|
|
15
|
+
saveSettings,
|
|
16
|
+
settingsFilePath,
|
|
17
|
+
} from "./settings.js";
|
|
8
18
|
import type {
|
|
9
19
|
ExtensionAPI,
|
|
10
20
|
ExtensionCommandContext,
|
|
@@ -14,8 +24,6 @@ import type {
|
|
|
14
24
|
const STATUS_KEY = "caffeinate";
|
|
15
25
|
const DISABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
16
26
|
const DEFAULT_MODE = "display" satisfies CaffeinateMode;
|
|
17
|
-
const NEW_SETTINGS_FILE = "pi-caffeinate.json";
|
|
18
|
-
const LEGACY_SETTINGS_FILE = "pi-caffeinate-settings.json";
|
|
19
27
|
const COMMAND_COMPLETIONS = [
|
|
20
28
|
{ value: "display", label: "display", description: "Keep system and display awake" },
|
|
21
29
|
{ value: "sleep", label: "sleep", description: "Keep system awake; allow display sleep" },
|
|
@@ -36,18 +44,9 @@ const MODE_OPTIONS = {
|
|
|
36
44
|
sleep: "Keep system awake; allow display sleep",
|
|
37
45
|
} as const;
|
|
38
46
|
|
|
39
|
-
type CaffeinateMode = "sleep" | "display";
|
|
40
47
|
type CommandAction = "menu" | "help" | "status" | "mode" | "sleep" | "display" | "stop";
|
|
41
48
|
type CommandContext = ExtensionCommandContext;
|
|
42
49
|
|
|
43
|
-
interface InhibitorCommand {
|
|
44
|
-
command: string;
|
|
45
|
-
args: string[];
|
|
46
|
-
description: string;
|
|
47
|
-
releaseOnStdinClose?: boolean;
|
|
48
|
-
custom?: boolean;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
50
|
interface CaffeinateState {
|
|
52
51
|
process?: ChildProcess;
|
|
53
52
|
startedAt?: number;
|
|
@@ -63,11 +62,6 @@ interface CaffeinateState {
|
|
|
63
62
|
iconWarningShown: boolean;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
interface CaffeinateSettings {
|
|
67
|
-
mode: CaffeinateMode;
|
|
68
|
-
updatedAt: number;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
65
|
const state: CaffeinateState = {
|
|
72
66
|
activeTurns: 0,
|
|
73
67
|
available: true,
|
|
@@ -268,12 +262,7 @@ function buildCommandGuide() {
|
|
|
268
262
|
}
|
|
269
263
|
|
|
270
264
|
function startInhibitor(ctx: ExtensionContext) {
|
|
271
|
-
if (state.disabled) {
|
|
272
|
-
updateStatus(ctx);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
if (state.process) {
|
|
265
|
+
if (state.disabled || state.process) {
|
|
277
266
|
updateStatus(ctx);
|
|
278
267
|
return;
|
|
279
268
|
}
|
|
@@ -288,37 +277,32 @@ function startInhibitor(ctx: ExtensionContext) {
|
|
|
288
277
|
}
|
|
289
278
|
|
|
290
279
|
try {
|
|
291
|
-
const child =
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
280
|
+
const child = startInhibitorProcess(
|
|
281
|
+
command,
|
|
282
|
+
(error) => {
|
|
283
|
+
if (state.process === child) {
|
|
284
|
+
state.process = undefined;
|
|
285
|
+
state.startedAt = undefined;
|
|
286
|
+
}
|
|
287
|
+
state.available = false;
|
|
288
|
+
state.lastError = `${command.description} failed: ${error.message}`;
|
|
289
|
+
ctx.ui.notify(state.lastError, "warning");
|
|
290
|
+
updateStatus(ctx);
|
|
291
|
+
},
|
|
292
|
+
(exit) => {
|
|
293
|
+
if (state.process !== child) return;
|
|
294
|
+
state.process = undefined;
|
|
295
|
+
state.startedAt = undefined;
|
|
296
|
+
state.lastError = `${command.description} exited unexpectedly (${exit}).`;
|
|
297
|
+
ctx.ui.notify(state.lastError, "warning");
|
|
298
|
+
updateStatus(ctx);
|
|
299
|
+
},
|
|
300
|
+
);
|
|
296
301
|
state.process = child;
|
|
297
302
|
state.startedAt = Date.now();
|
|
298
303
|
state.command = command;
|
|
299
304
|
state.available = true;
|
|
300
305
|
state.lastError = undefined;
|
|
301
|
-
|
|
302
|
-
child.once("error", (error) => {
|
|
303
|
-
if (state.process === child) {
|
|
304
|
-
state.process = undefined;
|
|
305
|
-
state.startedAt = undefined;
|
|
306
|
-
}
|
|
307
|
-
state.available = false;
|
|
308
|
-
state.lastError = `${command.description} failed: ${error.message}`;
|
|
309
|
-
ctx.ui.notify(state.lastError, "warning");
|
|
310
|
-
updateStatus(ctx);
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
child.once("exit", (code, signal) => {
|
|
314
|
-
if (state.process !== child) return;
|
|
315
|
-
state.process = undefined;
|
|
316
|
-
state.startedAt = undefined;
|
|
317
|
-
state.lastError = `${command.description} exited unexpectedly (${formatExit(code, signal)}).`;
|
|
318
|
-
ctx.ui.notify(state.lastError, "warning");
|
|
319
|
-
updateStatus(ctx);
|
|
320
|
-
});
|
|
321
|
-
|
|
322
306
|
ctx.ui.notify(`Keeping computer awake (${statusModeLabel()}).`, "info");
|
|
323
307
|
updateStatus(ctx);
|
|
324
308
|
} catch (error) {
|
|
@@ -334,194 +318,11 @@ function startInhibitor(ctx: ExtensionContext) {
|
|
|
334
318
|
function stopInhibitor(ctx: ExtensionContext, reason: string, options: { notify?: boolean } = {}) {
|
|
335
319
|
const child = state.process;
|
|
336
320
|
if (!child) return;
|
|
337
|
-
|
|
338
321
|
state.process = undefined;
|
|
339
322
|
state.startedAt = undefined;
|
|
340
|
-
|
|
341
|
-
child.removeAllListeners("exit");
|
|
342
|
-
child.removeAllListeners("error");
|
|
343
|
-
|
|
344
|
-
if (!child.killed) {
|
|
345
|
-
if (state.command?.releaseOnStdinClose && child.stdin && !child.stdin.destroyed) {
|
|
346
|
-
child.stdin.end();
|
|
347
|
-
if (child.exitCode === null && child.signalCode === null) {
|
|
348
|
-
const killTimer = setTimeout(() => {
|
|
349
|
-
if (child.exitCode === null && child.signalCode === null && !child.killed) child.kill();
|
|
350
|
-
}, 2000);
|
|
351
|
-
killTimer.unref();
|
|
352
|
-
child.once("exit", () => clearTimeout(killTimer));
|
|
353
|
-
}
|
|
354
|
-
} else if (process.platform === "win32") {
|
|
355
|
-
child.kill();
|
|
356
|
-
} else {
|
|
357
|
-
child.kill("SIGTERM");
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
323
|
+
stopInhibitorProcess(child, state.command);
|
|
361
324
|
state.command = undefined;
|
|
362
|
-
|
|
363
|
-
if (options.notify !== false) {
|
|
364
|
-
ctx.ui.notify(`Released pi-caffeinate (${reason}).`, "info");
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function getInhibitorCommand(mode: CaffeinateMode): InhibitorCommand | undefined {
|
|
369
|
-
const customCommand = process.env.PI_CAFFEINATE_COMMAND?.trim();
|
|
370
|
-
if (customCommand) {
|
|
371
|
-
const [command, ...args] = splitCommand(customCommand);
|
|
372
|
-
if (command) return { command, args, description: `custom command (${command})`, custom: true };
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
if (process.platform === "darwin") {
|
|
376
|
-
return parentBoundUnixCommand("caffeinate", macCaffeinateArgs(mode), caffeinateDescription(mode));
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (process.platform === "linux") {
|
|
380
|
-
if (isWsl() && commandExists("powershell.exe")) {
|
|
381
|
-
return windowsPowerInhibitorCommand("powershell.exe", mode);
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
if (commandExists("systemd-inhibit")) {
|
|
385
|
-
const what = mode === "sleep" ? "sleep" : "idle:sleep";
|
|
386
|
-
return parentBoundUnixCommand(
|
|
387
|
-
"systemd-inhibit",
|
|
388
|
-
[
|
|
389
|
-
`--what=${what}`,
|
|
390
|
-
"--who=pi-caffeinate",
|
|
391
|
-
"--why=Pi agent is running",
|
|
392
|
-
"--mode=block",
|
|
393
|
-
"sleep",
|
|
394
|
-
"infinity",
|
|
395
|
-
],
|
|
396
|
-
`systemd-inhibit (${formatMode(mode)})`,
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (commandExists("caffeinate")) {
|
|
401
|
-
return parentBoundUnixCommand(
|
|
402
|
-
"caffeinate",
|
|
403
|
-
macCaffeinateArgs(mode),
|
|
404
|
-
caffeinateDescription(mode),
|
|
405
|
-
);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
if (process.platform === "win32") {
|
|
410
|
-
return windowsPowerInhibitorCommand("powershell.exe", mode);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
return undefined;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function macCaffeinateArgs(mode: CaffeinateMode) {
|
|
417
|
-
return mode === "sleep" ? ["-ims"] : ["-dimsu"];
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function caffeinateDescription(mode: CaffeinateMode) {
|
|
421
|
-
return `caffeinate (${formatMode(mode)})`;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
function parentBoundUnixCommand(
|
|
425
|
-
command: string,
|
|
426
|
-
args: string[],
|
|
427
|
-
description: string,
|
|
428
|
-
): InhibitorCommand {
|
|
429
|
-
return {
|
|
430
|
-
command: "sh",
|
|
431
|
-
args: [
|
|
432
|
-
"-c",
|
|
433
|
-
unixParentBoundScript(),
|
|
434
|
-
"pi-caffeinate-watch",
|
|
435
|
-
String(process.pid),
|
|
436
|
-
command,
|
|
437
|
-
...args,
|
|
438
|
-
],
|
|
439
|
-
description,
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
function unixParentBoundScript() {
|
|
444
|
-
return `parent=$1; shift; "$@" & child=$!; ( while kill -0 "$parent" 2>/dev/null; do sleep 5; done; kill "$child" 2>/dev/null ) & watcher=$!; cleanup() { kill "$watcher" 2>/dev/null; kill "$child" 2>/dev/null; wait "$child" 2>/dev/null; }; trap 'cleanup; exit 0' INT TERM HUP EXIT; wait "$child"; status=$?; kill "$watcher" 2>/dev/null; trap - EXIT; exit "$status"`;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
function commandExists(command: string) {
|
|
448
|
-
const path = process.env.PATH ?? "";
|
|
449
|
-
const extensions = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
450
|
-
|
|
451
|
-
for (const directory of path.split(process.platform === "win32" ? ";" : ":")) {
|
|
452
|
-
if (!directory) continue;
|
|
453
|
-
for (const extension of extensions) {
|
|
454
|
-
const candidate = pathModule.join(directory, `${command}${extension}`);
|
|
455
|
-
if (existsSync(candidate)) return true;
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return false;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export function splitCommand(input: string) {
|
|
463
|
-
const parts: string[] = [];
|
|
464
|
-
let current = "";
|
|
465
|
-
let quote: '"' | "'" | undefined;
|
|
466
|
-
let escaping = false;
|
|
467
|
-
|
|
468
|
-
for (const char of input) {
|
|
469
|
-
if (escaping) {
|
|
470
|
-
current += char;
|
|
471
|
-
escaping = false;
|
|
472
|
-
continue;
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
if (char === "\\") {
|
|
476
|
-
escaping = true;
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if ((char === '"' || char === "'") && !quote) {
|
|
481
|
-
quote = char;
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
if (char === quote) {
|
|
486
|
-
quote = undefined;
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
if (/\s/.test(char) && !quote) {
|
|
491
|
-
if (current) {
|
|
492
|
-
parts.push(current);
|
|
493
|
-
current = "";
|
|
494
|
-
}
|
|
495
|
-
continue;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
current += char;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
if (current) parts.push(current);
|
|
502
|
-
return parts;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function windowsPowerInhibitorCommand(command: string, mode: CaffeinateMode): InhibitorCommand {
|
|
506
|
-
return {
|
|
507
|
-
command,
|
|
508
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsInhibitorScript(mode)],
|
|
509
|
-
description: `PowerShell SetThreadExecutionState (${formatMode(mode)})`,
|
|
510
|
-
releaseOnStdinClose: true,
|
|
511
|
-
};
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export function windowsInhibitorScript(mode: CaffeinateMode) {
|
|
515
|
-
const flags = mode === "sleep" ? "0x80000001" : "0x80000003";
|
|
516
|
-
return `$ErrorActionPreference = 'Stop'; Add-Type -Namespace Native -Name Power -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);'; $flags = [uint32]'${flags}'; $release = [uint32]'0x80000000'; $stdin = [Console]::OpenStandardInput(); $buffer = New-Object byte[] 1; $readTask = $stdin.ReadAsync($buffer, 0, 1); try { while ($true) { [Native.Power]::SetThreadExecutionState($flags) | Out-Null; if ($readTask.Wait(30000)) { break } } } finally { [Native.Power]::SetThreadExecutionState($release) | Out-Null }`;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
function isWsl() {
|
|
520
|
-
try {
|
|
521
|
-
return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
522
|
-
} catch {
|
|
523
|
-
return false;
|
|
524
|
-
}
|
|
325
|
+
if (options.notify !== false) ctx.ui.notify(`Released pi-caffeinate (${reason}).`, "info");
|
|
525
326
|
}
|
|
526
327
|
|
|
527
328
|
function updateStatus(ctx: ExtensionContext) {
|
|
@@ -584,10 +385,6 @@ function statusModeLabel() {
|
|
|
584
385
|
return formatMode(state.mode);
|
|
585
386
|
}
|
|
586
387
|
|
|
587
|
-
export function formatMode(mode: CaffeinateMode) {
|
|
588
|
-
return mode === "sleep" ? "system-awake" : "display-awake";
|
|
589
|
-
}
|
|
590
|
-
|
|
591
388
|
async function ensureSettingsLoaded(ctx: ExtensionContext) {
|
|
592
389
|
if (state.disabled || state.settingsLoaded) return;
|
|
593
390
|
await loadSettingsIntoState(ctx);
|
|
@@ -623,158 +420,10 @@ async function loadSettingsIntoState(ctx: ExtensionContext) {
|
|
|
623
420
|
}
|
|
624
421
|
}
|
|
625
422
|
|
|
626
|
-
type SettingsLoadResult =
|
|
627
|
-
| { kind: "missing"; notice?: string }
|
|
628
|
-
| { kind: "invalid"; reason: string; notice?: string }
|
|
629
|
-
| { kind: "loaded"; settings: CaffeinateSettings; notice?: string };
|
|
630
|
-
|
|
631
|
-
type SettingsMigrationResult = {
|
|
632
|
-
kind: "migrated" | "failed";
|
|
633
|
-
notice: string;
|
|
634
|
-
};
|
|
635
|
-
|
|
636
|
-
async function loadSettings(): Promise<SettingsLoadResult> {
|
|
637
|
-
const newPath = settingsFilePath();
|
|
638
|
-
const newSettings = await readSettingsFile(newPath);
|
|
639
|
-
if (newSettings.kind !== "missing") {
|
|
640
|
-
return withLegacyIgnoredNotice(newSettings);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const legacyPath = legacySettingsFilePath();
|
|
644
|
-
const legacySettings = await readSettingsFile(legacyPath);
|
|
645
|
-
const concurrentlyCreatedSettings = await readSettingsFile(newPath);
|
|
646
|
-
if (concurrentlyCreatedSettings.kind !== "missing") {
|
|
647
|
-
return withLegacyIgnoredNotice(concurrentlyCreatedSettings);
|
|
648
|
-
}
|
|
649
|
-
if (legacySettings.kind === "missing") return { kind: "missing" };
|
|
650
|
-
if (legacySettings.kind === "invalid") return legacySettings;
|
|
651
|
-
|
|
652
|
-
const migration = await migrateLegacySettings(legacyPath);
|
|
653
|
-
if (migration.kind === "failed") {
|
|
654
|
-
const settingsCreatedDuringMigration = await readSettingsFile(newPath);
|
|
655
|
-
if (settingsCreatedDuringMigration.kind !== "missing") {
|
|
656
|
-
return withLegacyIgnoredNotice(settingsCreatedDuringMigration);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
return { ...legacySettings, notice: migration.notice };
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async function readSettingsFile(filePath: string): Promise<SettingsLoadResult> {
|
|
664
|
-
let text: string;
|
|
665
|
-
try {
|
|
666
|
-
text = await readFile(filePath, "utf8");
|
|
667
|
-
} catch (error) {
|
|
668
|
-
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
669
|
-
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
try {
|
|
673
|
-
const parsed = JSON.parse(text) as unknown;
|
|
674
|
-
const settings = normalizeCaffeinateSettings(parsed);
|
|
675
|
-
if (settings) return { kind: "loaded", settings };
|
|
676
|
-
return {
|
|
677
|
-
kind: "invalid",
|
|
678
|
-
reason: `${filePath}: expected { "mode": "sleep" | "display" }`,
|
|
679
|
-
};
|
|
680
|
-
} catch (error) {
|
|
681
|
-
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
async function withLegacyIgnoredNotice(settings: SettingsLoadResult): Promise<SettingsLoadResult> {
|
|
686
|
-
if (!(await fileExists(legacySettingsFilePath()))) return settings;
|
|
687
|
-
return {
|
|
688
|
-
...settings,
|
|
689
|
-
notice: `pi-caffeinate legacy settings ignored: ${legacySettingsFilePath()} exists, but ${settingsFilePath()} takes precedence. Delete ${LEGACY_SETTINGS_FILE} after confirming your settings.`,
|
|
690
|
-
};
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
async function migrateLegacySettings(legacyPath: string): Promise<SettingsMigrationResult> {
|
|
694
|
-
const newPath = settingsFilePath();
|
|
695
|
-
try {
|
|
696
|
-
await link(legacyPath, newPath);
|
|
697
|
-
} catch (error) {
|
|
698
|
-
return {
|
|
699
|
-
kind: "failed",
|
|
700
|
-
notice: `pi-caffeinate legacy settings migration failed: could not migrate ${legacyPath} to ${newPath}: ${formatError(error)}. The legacy file was used for this session; future saves will write ${NEW_SETTINGS_FILE}.`,
|
|
701
|
-
};
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
try {
|
|
705
|
-
await rm(legacyPath, { force: true });
|
|
706
|
-
} catch (error) {
|
|
707
|
-
return {
|
|
708
|
-
kind: "migrated",
|
|
709
|
-
notice: `pi-caffeinate settings migrated from ${legacyPath} to ${newPath}, but the legacy file could not be removed: ${formatError(error)}. Delete ${LEGACY_SETTINGS_FILE} after confirming your settings.`,
|
|
710
|
-
};
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
return {
|
|
714
|
-
kind: "migrated",
|
|
715
|
-
notice: `pi-caffeinate settings migrated from ${legacyPath} to ${newPath}. ${LEGACY_SETTINGS_FILE} is deprecated and will be removed in a future major release.`,
|
|
716
|
-
};
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
async function fileExists(filePath: string) {
|
|
720
|
-
try {
|
|
721
|
-
await access(filePath, constants.F_OK);
|
|
722
|
-
return true;
|
|
723
|
-
} catch {
|
|
724
|
-
return false;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
|
|
728
|
-
export function normalizeCaffeinateSettings(value: unknown): CaffeinateSettings | undefined {
|
|
729
|
-
if (!value || typeof value !== "object") return undefined;
|
|
730
|
-
const settings = value as { mode?: unknown; updatedAt?: unknown };
|
|
731
|
-
if (!isCaffeinateMode(settings.mode)) return undefined;
|
|
732
|
-
if (settings.updatedAt !== undefined && typeof settings.updatedAt !== "number") return undefined;
|
|
733
|
-
return { mode: settings.mode, updatedAt: settings.updatedAt ?? 0 };
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
function isCaffeinateMode(value: unknown): value is CaffeinateMode {
|
|
737
|
-
return value === "sleep" || value === "display";
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
async function saveSettings(settings: CaffeinateSettings) {
|
|
741
|
-
const filePath = settingsFilePath();
|
|
742
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
743
|
-
const tempFile = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
744
|
-
try {
|
|
745
|
-
await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
746
|
-
await rename(tempFile, filePath);
|
|
747
|
-
} catch (error) {
|
|
748
|
-
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
749
|
-
throw error;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function settingsFilePath() {
|
|
754
|
-
return join(agentDir(), NEW_SETTINGS_FILE);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
function legacySettingsFilePath() {
|
|
758
|
-
return join(agentDir(), LEGACY_SETTINGS_FILE);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
function agentDir() {
|
|
762
|
-
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
766
|
-
return error instanceof Error && "code" in error;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
423
|
function formatError(error: unknown) {
|
|
770
424
|
return error instanceof Error ? error.message : String(error);
|
|
771
425
|
}
|
|
772
426
|
|
|
773
|
-
function formatExit(code: number | null, signal: NodeJS.Signals | null) {
|
|
774
|
-
if (signal) return `signal ${signal}`;
|
|
775
|
-
return `code ${code ?? "unknown"}`;
|
|
776
|
-
}
|
|
777
|
-
|
|
778
427
|
function isDisabled() {
|
|
779
428
|
const value = process.env.PI_CAFFEINATE_DISABLED?.trim().toLowerCase();
|
|
780
429
|
return value ? DISABLED_VALUES.has(value) : false;
|
|
@@ -797,3 +446,6 @@ function warnDeprecatedIcon(ctx: ExtensionContext) {
|
|
|
797
446
|
"warning",
|
|
798
447
|
);
|
|
799
448
|
}
|
|
449
|
+
|
|
450
|
+
export { formatMode, splitCommand, windowsInhibitorScript } from "./inhibitors.js";
|
|
451
|
+
export { normalizeCaffeinateSettings } from "./settings.js";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import type { InhibitorCommand } from "./inhibitors.js";
|
|
4
|
+
|
|
5
|
+
export function startInhibitorProcess(
|
|
6
|
+
command: InhibitorCommand,
|
|
7
|
+
onError: (error: Error) => void,
|
|
8
|
+
onUnexpectedExit: (description: string) => void,
|
|
9
|
+
): ChildProcess {
|
|
10
|
+
const child = spawn(command.command, command.args, {
|
|
11
|
+
detached: false,
|
|
12
|
+
stdio: [command.releaseOnStdinClose ? "pipe" : "ignore", "pipe", "pipe"],
|
|
13
|
+
});
|
|
14
|
+
child.once("error", onError);
|
|
15
|
+
child.once("exit", (code, signal) => onUnexpectedExit(formatExit(code, signal)));
|
|
16
|
+
return child;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function stopInhibitorProcess(child: ChildProcess, command: InhibitorCommand | undefined) {
|
|
20
|
+
child.removeAllListeners("exit");
|
|
21
|
+
child.removeAllListeners("error");
|
|
22
|
+
if (child.killed) return;
|
|
23
|
+
|
|
24
|
+
if (command?.releaseOnStdinClose && child.stdin && !child.stdin.destroyed) {
|
|
25
|
+
child.stdin.end();
|
|
26
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
27
|
+
const killTimer = setTimeout(() => {
|
|
28
|
+
if (child.exitCode === null && child.signalCode === null && !child.killed) child.kill();
|
|
29
|
+
}, 2000);
|
|
30
|
+
killTimer.unref();
|
|
31
|
+
child.once("exit", () => clearTimeout(killTimer));
|
|
32
|
+
}
|
|
33
|
+
} else if (process.platform === "win32") {
|
|
34
|
+
child.kill();
|
|
35
|
+
} else {
|
|
36
|
+
child.kill("SIGTERM");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatExit(code: number | null, signal: NodeJS.Signals | null) {
|
|
41
|
+
if (signal) return `signal ${signal}`;
|
|
42
|
+
return `code ${code ?? "unknown"}`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
4
|
+
import type { CaffeinateMode } from "./settings.js";
|
|
5
|
+
|
|
6
|
+
export interface InhibitorCommand {
|
|
7
|
+
command: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
description: string;
|
|
10
|
+
releaseOnStdinClose?: boolean;
|
|
11
|
+
custom?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getInhibitorCommand(mode: CaffeinateMode): InhibitorCommand | undefined {
|
|
15
|
+
const customCommand = process.env.PI_CAFFEINATE_COMMAND?.trim();
|
|
16
|
+
if (customCommand) {
|
|
17
|
+
const [command, ...args] = splitCommand(customCommand);
|
|
18
|
+
if (command) return { command, args, description: `custom command (${command})`, custom: true };
|
|
19
|
+
}
|
|
20
|
+
if (process.platform === "darwin") {
|
|
21
|
+
return parentBoundUnixCommand("caffeinate", macCaffeinateArgs(mode), caffeinateDescription(mode));
|
|
22
|
+
}
|
|
23
|
+
if (process.platform === "linux") {
|
|
24
|
+
if (isWsl() && commandExists("powershell.exe")) {
|
|
25
|
+
return windowsPowerInhibitorCommand("powershell.exe", mode);
|
|
26
|
+
}
|
|
27
|
+
if (commandExists("systemd-inhibit")) {
|
|
28
|
+
const what = mode === "sleep" ? "sleep" : "idle:sleep";
|
|
29
|
+
return parentBoundUnixCommand(
|
|
30
|
+
"systemd-inhibit",
|
|
31
|
+
[
|
|
32
|
+
`--what=${what}`,
|
|
33
|
+
"--who=pi-caffeinate",
|
|
34
|
+
"--why=Pi agent is running",
|
|
35
|
+
"--mode=block",
|
|
36
|
+
"sleep",
|
|
37
|
+
"infinity",
|
|
38
|
+
],
|
|
39
|
+
`systemd-inhibit (${formatMode(mode)})`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (commandExists("caffeinate")) {
|
|
43
|
+
return parentBoundUnixCommand("caffeinate", macCaffeinateArgs(mode), caffeinateDescription(mode));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (process.platform === "win32") return windowsPowerInhibitorCommand("powershell.exe", mode);
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function macCaffeinateArgs(mode: CaffeinateMode) {
|
|
51
|
+
return mode === "sleep" ? ["-ims"] : ["-dimsu"];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function caffeinateDescription(mode: CaffeinateMode) {
|
|
55
|
+
return `caffeinate (${formatMode(mode)})`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parentBoundUnixCommand(command: string, args: string[], description: string): InhibitorCommand {
|
|
59
|
+
return {
|
|
60
|
+
command: "sh",
|
|
61
|
+
args: ["-c", unixParentBoundScript(), "pi-caffeinate-watch", String(process.pid), command, ...args],
|
|
62
|
+
description,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function unixParentBoundScript() {
|
|
67
|
+
return `parent=$1; shift; "$@" & child=$!; ( while kill -0 "$parent" 2>/dev/null; do sleep 5; done; kill "$child" 2>/dev/null ) & watcher=$!; cleanup() { kill "$watcher" 2>/dev/null; kill "$child" 2>/dev/null; wait "$child" 2>/dev/null; }; trap 'cleanup; exit 0' INT TERM HUP EXIT; wait "$child"; status=$?; kill "$watcher" 2>/dev/null; trap - EXIT; exit "$status"`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function commandExists(command: string) {
|
|
71
|
+
const searchPath = process.env.PATH ?? "";
|
|
72
|
+
const extensions = process.platform === "win32" ? ["", ".exe", ".cmd", ".bat"] : [""];
|
|
73
|
+
for (const directory of searchPath.split(process.platform === "win32" ? ";" : ":")) {
|
|
74
|
+
if (!directory) continue;
|
|
75
|
+
for (const extension of extensions) {
|
|
76
|
+
if (existsSync(path.join(directory, `${command}${extension}`))) return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function splitCommand(input: string) {
|
|
83
|
+
const parts: string[] = [];
|
|
84
|
+
let current = "";
|
|
85
|
+
let quote: '"' | "'" | undefined;
|
|
86
|
+
let escaping = false;
|
|
87
|
+
for (const char of input) {
|
|
88
|
+
if (escaping) {
|
|
89
|
+
current += char;
|
|
90
|
+
escaping = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (char === "\\") {
|
|
94
|
+
escaping = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if ((char === '"' || char === "'") && !quote) {
|
|
98
|
+
quote = char;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
if (char === quote) {
|
|
102
|
+
quote = undefined;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (/\s/.test(char) && !quote) {
|
|
106
|
+
if (current) {
|
|
107
|
+
parts.push(current);
|
|
108
|
+
current = "";
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
current += char;
|
|
113
|
+
}
|
|
114
|
+
if (current) parts.push(current);
|
|
115
|
+
return parts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function windowsPowerInhibitorCommand(command: string, mode: CaffeinateMode): InhibitorCommand {
|
|
119
|
+
return {
|
|
120
|
+
command,
|
|
121
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsInhibitorScript(mode)],
|
|
122
|
+
description: `PowerShell SetThreadExecutionState (${formatMode(mode)})`,
|
|
123
|
+
releaseOnStdinClose: true,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function windowsInhibitorScript(mode: CaffeinateMode) {
|
|
128
|
+
const flags = mode === "sleep" ? "0x80000001" : "0x80000003";
|
|
129
|
+
return `$ErrorActionPreference = 'Stop'; Add-Type -Namespace Native -Name Power -MemberDefinition '[DllImport("kernel32.dll")] public static extern uint SetThreadExecutionState(uint esFlags);'; $flags = [uint32]'${flags}'; $release = [uint32]'0x80000000'; $stdin = [Console]::OpenStandardInput(); $buffer = New-Object byte[] 1; $readTask = $stdin.ReadAsync($buffer, 0, 1); try { while ($true) { [Native.Power]::SetThreadExecutionState($flags) | Out-Null; if ($readTask.Wait(30000)) { break } } } finally { [Native.Power]::SetThreadExecutionState($release) | Out-Null }`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isWsl() {
|
|
133
|
+
try {
|
|
134
|
+
return existsSync("/proc/sys/fs/binfmt_misc/WSLInterop");
|
|
135
|
+
} catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function formatMode(mode: CaffeinateMode) {
|
|
141
|
+
return mode === "sleep" ? "system-awake" : "display-awake";
|
|
142
|
+
}
|
package/src/settings.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, link, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import process from "node:process";
|
|
6
|
+
|
|
7
|
+
const NEW_SETTINGS_FILE = "pi-caffeinate.json";
|
|
8
|
+
const LEGACY_SETTINGS_FILE = "pi-caffeinate-settings.json";
|
|
9
|
+
|
|
10
|
+
export type CaffeinateMode = "sleep" | "display";
|
|
11
|
+
|
|
12
|
+
export interface CaffeinateSettings {
|
|
13
|
+
mode: CaffeinateMode;
|
|
14
|
+
updatedAt: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type SettingsLoadResult =
|
|
18
|
+
| { kind: "missing"; notice?: string }
|
|
19
|
+
| { kind: "invalid"; reason: string; notice?: string }
|
|
20
|
+
| { kind: "loaded"; settings: CaffeinateSettings; notice?: string };
|
|
21
|
+
|
|
22
|
+
type SettingsMigrationResult = {
|
|
23
|
+
kind: "migrated" | "failed";
|
|
24
|
+
notice: string;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export async function loadSettings(): Promise<SettingsLoadResult> {
|
|
28
|
+
const newPath = settingsFilePath();
|
|
29
|
+
const newSettings = await readSettingsFile(newPath);
|
|
30
|
+
if (newSettings.kind !== "missing") return withLegacyIgnoredNotice(newSettings);
|
|
31
|
+
|
|
32
|
+
const legacyPath = legacySettingsFilePath();
|
|
33
|
+
const legacySettings = await readSettingsFile(legacyPath);
|
|
34
|
+
const concurrentlyCreatedSettings = await readSettingsFile(newPath);
|
|
35
|
+
if (concurrentlyCreatedSettings.kind !== "missing") {
|
|
36
|
+
return withLegacyIgnoredNotice(concurrentlyCreatedSettings);
|
|
37
|
+
}
|
|
38
|
+
if (legacySettings.kind === "missing") return { kind: "missing" };
|
|
39
|
+
if (legacySettings.kind === "invalid") return legacySettings;
|
|
40
|
+
|
|
41
|
+
const migration = await migrateLegacySettings(legacyPath);
|
|
42
|
+
if (migration.kind === "failed") {
|
|
43
|
+
const settingsCreatedDuringMigration = await readSettingsFile(newPath);
|
|
44
|
+
if (settingsCreatedDuringMigration.kind !== "missing") {
|
|
45
|
+
return withLegacyIgnoredNotice(settingsCreatedDuringMigration);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { ...legacySettings, notice: migration.notice };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function readSettingsFile(filePath: string): Promise<SettingsLoadResult> {
|
|
52
|
+
let text: string;
|
|
53
|
+
try {
|
|
54
|
+
text = await readFile(filePath, "utf8");
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
57
|
+
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const settings = normalizeCaffeinateSettings(JSON.parse(text) as unknown);
|
|
61
|
+
if (settings) return { kind: "loaded", settings };
|
|
62
|
+
return { kind: "invalid", reason: `${filePath}: expected { "mode": "sleep" | "display" }` };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function withLegacyIgnoredNotice(settings: SettingsLoadResult): Promise<SettingsLoadResult> {
|
|
69
|
+
if (!(await fileExists(legacySettingsFilePath()))) return settings;
|
|
70
|
+
return {
|
|
71
|
+
...settings,
|
|
72
|
+
notice: `pi-caffeinate legacy settings ignored: ${legacySettingsFilePath()} exists, but ${settingsFilePath()} takes precedence. Delete ${LEGACY_SETTINGS_FILE} after confirming your settings.`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function migrateLegacySettings(legacyPath: string): Promise<SettingsMigrationResult> {
|
|
77
|
+
const newPath = settingsFilePath();
|
|
78
|
+
try {
|
|
79
|
+
await link(legacyPath, newPath);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return {
|
|
82
|
+
kind: "failed",
|
|
83
|
+
notice: `pi-caffeinate legacy settings migration failed: could not migrate ${legacyPath} to ${newPath}: ${formatError(error)}. The legacy file was used for this session; future saves will write ${NEW_SETTINGS_FILE}.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
await rm(legacyPath, { force: true });
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return {
|
|
90
|
+
kind: "migrated",
|
|
91
|
+
notice: `pi-caffeinate settings migrated from ${legacyPath} to ${newPath}, but the legacy file could not be removed: ${formatError(error)}. Delete ${LEGACY_SETTINGS_FILE} after confirming your settings.`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
kind: "migrated",
|
|
96
|
+
notice: `pi-caffeinate settings migrated from ${legacyPath} to ${newPath}. ${LEGACY_SETTINGS_FILE} is deprecated and will be removed in a future major release.`,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function fileExists(filePath: string) {
|
|
101
|
+
try {
|
|
102
|
+
await access(filePath, constants.F_OK);
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function normalizeCaffeinateSettings(value: unknown): CaffeinateSettings | undefined {
|
|
110
|
+
if (!value || typeof value !== "object") return undefined;
|
|
111
|
+
const settings = value as { mode?: unknown; updatedAt?: unknown };
|
|
112
|
+
if (!isCaffeinateMode(settings.mode)) return undefined;
|
|
113
|
+
if (settings.updatedAt !== undefined && typeof settings.updatedAt !== "number") return undefined;
|
|
114
|
+
return { mode: settings.mode, updatedAt: settings.updatedAt ?? 0 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function isCaffeinateMode(value: unknown): value is CaffeinateMode {
|
|
118
|
+
return value === "sleep" || value === "display";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function saveSettings(settings: CaffeinateSettings) {
|
|
122
|
+
const filePath = settingsFilePath();
|
|
123
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
124
|
+
const tempFile = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
125
|
+
try {
|
|
126
|
+
await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
127
|
+
await rename(tempFile, filePath);
|
|
128
|
+
} catch (error) {
|
|
129
|
+
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function settingsFilePath() {
|
|
135
|
+
return join(agentDir(), NEW_SETTINGS_FILE);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function legacySettingsFilePath() {
|
|
139
|
+
return join(agentDir(), LEGACY_SETTINGS_FILE);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function agentDir() {
|
|
143
|
+
return process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent");
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
147
|
+
return error instanceof Error && "code" in error;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function formatError(error: unknown) {
|
|
151
|
+
return error instanceof Error ? error.message : String(error);
|
|
152
|
+
}
|