@narumitw/pi-caffeinate 0.10.0 → 0.12.0
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 +7 -1
- package/package.json +1 -1
- package/src/caffeinate.ts +116 -19
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ Releases any active inhibitor until Pi starts another agent run.
|
|
|
98
98
|
`/caffeinate sleep` and `/caffeinate display` save the selected mode to:
|
|
99
99
|
|
|
100
100
|
```text
|
|
101
|
-
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-caffeinate
|
|
101
|
+
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-caffeinate.json
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
Example:
|
|
@@ -112,6 +112,12 @@ Example:
|
|
|
112
112
|
|
|
113
113
|
Missing, invalid, or deleted settings default back to `display` mode on every supported OS.
|
|
114
114
|
|
|
115
|
+
Compatibility: older versions used `pi-caffeinate-settings.json`. During the migration window, a
|
|
116
|
+
legacy-only file is automatically migrated to `pi-caffeinate.json` with a warning. If both files
|
|
117
|
+
exist, `pi-caffeinate.json` wins and the legacy file is ignored. The legacy filename is deprecated
|
|
118
|
+
and planned for removal after at least one minor release cycle, preferably 4–8 weeks, or at the
|
|
119
|
+
next major release.
|
|
120
|
+
|
|
115
121
|
### Environment variables
|
|
116
122
|
|
|
117
123
|
Disable the extension:
|
package/package.json
CHANGED
package/src/caffeinate.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
|
-
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { constants, existsSync } from "node:fs";
|
|
3
|
+
import { access, link, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
5
|
import { dirname, join } from "node:path";
|
|
6
6
|
import pathModule from "node:path";
|
|
@@ -14,10 +14,8 @@ import type {
|
|
|
14
14
|
const STATUS_KEY = "caffeinate";
|
|
15
15
|
const DISABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
16
16
|
const DEFAULT_MODE = "display" satisfies CaffeinateMode;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
"pi-caffeinate-settings.json",
|
|
20
|
-
);
|
|
17
|
+
const NEW_SETTINGS_FILE = "pi-caffeinate.json";
|
|
18
|
+
const LEGACY_SETTINGS_FILE = "pi-caffeinate-settings.json";
|
|
21
19
|
const COMMAND_COMPLETIONS = [
|
|
22
20
|
{ value: "display", label: "display", description: "Keep system and display awake" },
|
|
23
21
|
{ value: "sleep", label: "sleep", description: "Keep system awake; allow display sleep" },
|
|
@@ -61,6 +59,7 @@ interface CaffeinateState {
|
|
|
61
59
|
mode: CaffeinateMode;
|
|
62
60
|
settingsLoaded: boolean;
|
|
63
61
|
settingsError?: string;
|
|
62
|
+
settingsNotice?: string;
|
|
64
63
|
iconWarningShown: boolean;
|
|
65
64
|
}
|
|
66
65
|
|
|
@@ -81,6 +80,7 @@ const state: CaffeinateState = {
|
|
|
81
80
|
export default function caffeinate(pi: ExtensionAPI) {
|
|
82
81
|
pi.on("session_start", async (_event, ctx) => {
|
|
83
82
|
state.iconWarningShown = false;
|
|
83
|
+
state.settingsNotice = undefined;
|
|
84
84
|
warnDeprecatedIcon(ctx);
|
|
85
85
|
await loadSettingsIntoState(ctx);
|
|
86
86
|
updateStatus(ctx);
|
|
@@ -547,10 +547,11 @@ function describeState() {
|
|
|
547
547
|
const customCommand = hasCustomCommand();
|
|
548
548
|
const lines = [
|
|
549
549
|
`Mode: ${formatMode(state.mode)}${customCommand ? " (overridden by custom command)" : ""}`,
|
|
550
|
-
`Settings: ${
|
|
550
|
+
`Settings: ${settingsFilePath()}`,
|
|
551
551
|
];
|
|
552
552
|
|
|
553
553
|
if (customCommand) lines.push("Custom command: PI_CAFFEINATE_COMMAND overrides the saved mode.");
|
|
554
|
+
if (state.settingsNotice) lines.push(`Settings note: ${state.settingsNotice}`);
|
|
554
555
|
if (state.settingsError) lines.push(`Settings warning: ${state.settingsError}`);
|
|
555
556
|
if (state.disabled) {
|
|
556
557
|
lines.unshift("pi-caffeinate is disabled by PI_CAFFEINATE_DISABLED.");
|
|
@@ -602,6 +603,10 @@ async function loadSettingsIntoState(ctx: ExtensionContext) {
|
|
|
602
603
|
const settings = await loadSettings();
|
|
603
604
|
state.settingsLoaded = true;
|
|
604
605
|
state.settingsError = undefined;
|
|
606
|
+
if (settings.notice) {
|
|
607
|
+
state.settingsNotice = settings.notice;
|
|
608
|
+
ctx.ui.notify(settings.notice, "warning");
|
|
609
|
+
}
|
|
605
610
|
|
|
606
611
|
if (settings.kind === "loaded") {
|
|
607
612
|
state.mode = settings.settings.mode;
|
|
@@ -618,26 +623,105 @@ async function loadSettingsIntoState(ctx: ExtensionContext) {
|
|
|
618
623
|
}
|
|
619
624
|
}
|
|
620
625
|
|
|
621
|
-
|
|
622
|
-
| { kind: "missing" }
|
|
623
|
-
| { kind: "invalid"; reason: string }
|
|
624
|
-
| { kind: "loaded"; settings: CaffeinateSettings }
|
|
625
|
-
|
|
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> {
|
|
626
664
|
let text: string;
|
|
627
665
|
try {
|
|
628
|
-
text = await readFile(
|
|
666
|
+
text = await readFile(filePath, "utf8");
|
|
629
667
|
} catch (error) {
|
|
630
668
|
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
631
|
-
return { kind: "invalid", reason: formatError(error) };
|
|
669
|
+
return { kind: "invalid", reason: `${filePath}: ${formatError(error)}` };
|
|
632
670
|
}
|
|
633
671
|
|
|
634
672
|
try {
|
|
635
673
|
const parsed = JSON.parse(text) as unknown;
|
|
636
674
|
const settings = normalizeCaffeinateSettings(parsed);
|
|
637
675
|
if (settings) return { kind: "loaded", settings };
|
|
638
|
-
return {
|
|
676
|
+
return {
|
|
677
|
+
kind: "invalid",
|
|
678
|
+
reason: `${filePath}: expected { "mode": "sleep" | "display" }`,
|
|
679
|
+
};
|
|
639
680
|
} catch (error) {
|
|
640
|
-
return { kind: "invalid", reason: formatError(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;
|
|
641
725
|
}
|
|
642
726
|
}
|
|
643
727
|
|
|
@@ -654,17 +738,30 @@ function isCaffeinateMode(value: unknown): value is CaffeinateMode {
|
|
|
654
738
|
}
|
|
655
739
|
|
|
656
740
|
async function saveSettings(settings: CaffeinateSettings) {
|
|
657
|
-
|
|
658
|
-
|
|
741
|
+
const filePath = settingsFilePath();
|
|
742
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
743
|
+
const tempFile = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
659
744
|
try {
|
|
660
745
|
await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
661
|
-
await rename(tempFile,
|
|
746
|
+
await rename(tempFile, filePath);
|
|
662
747
|
} catch (error) {
|
|
663
748
|
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
664
749
|
throw error;
|
|
665
750
|
}
|
|
666
751
|
}
|
|
667
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
|
+
|
|
668
765
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
669
766
|
return error instanceof Error && "code" in error;
|
|
670
767
|
}
|