@narumitw/pi-caffeinate 0.11.0 → 0.13.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 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-settings.json
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@narumitw/pi-caffeinate",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "Pi extension that keeps the computer awake while the agent is running.",
5
5
  "type": "module",
6
6
  "license": "MIT",
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 SETTINGS_FILE = join(
18
- process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"),
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: ${SETTINGS_FILE}`,
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
- async function loadSettings(): Promise<
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(SETTINGS_FILE, "utf8");
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 { kind: "invalid", reason: 'expected { "mode": "sleep" | "display" }' };
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
- await mkdir(dirname(SETTINGS_FILE), { recursive: true });
658
- const tempFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
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, SETTINGS_FILE);
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
  }