@narumitw/pi-caffeinate 0.1.37 → 0.3.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 +67 -12
- package/package.json +1 -1
- package/src/caffeinate.ts +366 -36
package/README.md
CHANGED
|
@@ -10,9 +10,11 @@ It is designed for long-running coding, refactoring, debugging, web research, an
|
|
|
10
10
|
|
|
11
11
|
- Starts an OS sleep inhibitor when Pi begins processing (`agent_start`).
|
|
12
12
|
- Releases the inhibitor when processing ends (`agent_end`) or the session shuts down.
|
|
13
|
-
- Publishes
|
|
13
|
+
- Publishes the active keep-awake mode as status while an inhibitor is active.
|
|
14
14
|
- Supports macOS, Windows, WSL, and Linux.
|
|
15
|
-
-
|
|
15
|
+
- Defaults to display-awake mode on every supported OS: prevent system sleep and keep the screen/display awake.
|
|
16
|
+
- Provides a single `/caffeinate` command with menu-based controls and direct subcommands.
|
|
17
|
+
- Persists the selected keep-awake mode in a small JSON settings file.
|
|
16
18
|
- Allows a custom inhibitor command through environment configuration.
|
|
17
19
|
- Allows a custom status icon through environment configuration.
|
|
18
20
|
- Fails safely when no supported inhibitor is available.
|
|
@@ -37,30 +39,81 @@ pi -e ./extensions/pi-caffeinate
|
|
|
37
39
|
|
|
38
40
|
## 🖥️ Supported platforms
|
|
39
41
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
The default mode is `display` on every supported OS. That means pi-caffeinate prevents system sleep, suspend, or hibernate and keeps the screen/display awake.
|
|
43
|
+
|
|
44
|
+
Use `/caffeinate sleep` if you want to prevent system sleep while allowing normal display idle behavior such as screen blanking or monitor power-off.
|
|
45
|
+
|
|
46
|
+
| Platform | `sleep` mode | `display` mode, default |
|
|
47
|
+
| --- | --- | --- |
|
|
48
|
+
| macOS | `caffeinate -ims` | `caffeinate -dimsu` |
|
|
49
|
+
| Windows | PowerShell `SetThreadExecutionState(0x80000001)` | PowerShell `SetThreadExecutionState(0x80000003)` |
|
|
50
|
+
| WSL | Windows `powershell.exe` with `SetThreadExecutionState(0x80000001)` | Windows `powershell.exe` with `SetThreadExecutionState(0x80000003)` |
|
|
51
|
+
| Linux with systemd | `systemd-inhibit --what=sleep ... sleep infinity` | `systemd-inhibit --what=idle:sleep ... sleep infinity` |
|
|
52
|
+
| Linux fallback | `caffeinate -ims` when available | `caffeinate -dimsu` when available |
|
|
45
53
|
|
|
46
54
|
If no supported inhibitor is available, the extension stays loaded and reports that caffeinate is unavailable.
|
|
47
55
|
|
|
48
56
|
## 🚀 Commands
|
|
49
57
|
|
|
50
58
|
```text
|
|
51
|
-
/caffeinate
|
|
59
|
+
/caffeinate
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Opens keep-awake controls. In non-interactive sessions, it prints command usage and status.
|
|
63
|
+
|
|
64
|
+
```text
|
|
65
|
+
/caffeinate display
|
|
52
66
|
```
|
|
53
67
|
|
|
54
|
-
|
|
68
|
+
Keeps the system and screen/display awake. If an inhibitor is currently active, it is restarted so the new mode applies immediately.
|
|
55
69
|
|
|
56
70
|
```text
|
|
57
|
-
/caffeinate
|
|
71
|
+
/caffeinate sleep
|
|
58
72
|
```
|
|
59
73
|
|
|
60
|
-
|
|
74
|
+
Keeps the system awake while allowing normal display sleep. If an inhibitor is currently active, it is restarted so the new mode applies immediately.
|
|
75
|
+
|
|
76
|
+
```text
|
|
77
|
+
/caffeinate status
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Shows whether an inhibitor is active, unavailable, disabled, or idle. The status includes the current mode and settings file path.
|
|
81
|
+
|
|
82
|
+
```text
|
|
83
|
+
/caffeinate mode
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Opens an interactive selector for the keep-awake mode.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
/caffeinate stop
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Releases any active inhibitor until Pi starts another agent run.
|
|
61
93
|
|
|
62
94
|
## ⚙️ Configuration
|
|
63
95
|
|
|
96
|
+
### Persisted mode
|
|
97
|
+
|
|
98
|
+
`/caffeinate sleep` and `/caffeinate display` save the selected mode to:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
${PI_CODING_AGENT_DIR:-~/.pi/agent}/pi-caffeinate-settings.json
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Example:
|
|
105
|
+
|
|
106
|
+
```json
|
|
107
|
+
{
|
|
108
|
+
"mode": "display",
|
|
109
|
+
"updatedAt": 1791763200000
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Missing, invalid, or deleted settings default back to `display` mode on every supported OS.
|
|
114
|
+
|
|
115
|
+
### Environment variables
|
|
116
|
+
|
|
64
117
|
Disable the extension:
|
|
65
118
|
|
|
66
119
|
```bash
|
|
@@ -73,7 +126,7 @@ Use a custom inhibitor command:
|
|
|
73
126
|
PI_CAFFEINATE_COMMAND='systemd-inhibit --what=idle:sleep --why="pi running" --mode=block sleep infinity' pi
|
|
74
127
|
```
|
|
75
128
|
|
|
76
|
-
The custom command is parsed with shell-like quoting and is run directly without a shell.
|
|
129
|
+
The custom command is parsed with shell-like quoting and is run directly without a shell. `PI_CAFFEINATE_COMMAND` takes precedence over the saved mode; `/caffeinate status` reports when a custom command is active.
|
|
77
130
|
|
|
78
131
|
Customise the status bar icon (default: `💊`):
|
|
79
132
|
|
|
@@ -85,6 +138,8 @@ PI_CAFFEINATE_ICON='☕️' pi
|
|
|
85
138
|
|
|
86
139
|
AI coding agents often run tool-heavy tasks that take several minutes. `pi-caffeinate` keeps your machine awake during active Pi work, helping browser automation, local builds, test runs, code generation, and long prompts finish reliably.
|
|
87
140
|
|
|
141
|
+
The default display-awake mode prioritizes uninterrupted long-running Pi work across platforms, including Linux desktops that require idle inhibition to prevent automatic suspend. Use sleep-only mode when you prefer normal screen power saving and your system does not need idle inhibition to keep Pi running.
|
|
142
|
+
|
|
88
143
|
## 🗂️ Package layout
|
|
89
144
|
|
|
90
145
|
```txt
|
package/package.json
CHANGED
package/src/caffeinate.ts
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
1
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
+
import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
3
6
|
import pathModule from "node:path";
|
|
4
7
|
import process from "node:process";
|
|
5
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ExtensionAPI,
|
|
10
|
+
ExtensionCommandContext,
|
|
11
|
+
ExtensionContext,
|
|
12
|
+
} from "@mariozechner/pi-coding-agent";
|
|
6
13
|
|
|
7
14
|
const STATUS_KEY = "caffeinate";
|
|
8
15
|
const DISABLED_VALUES = new Set(["1", "true", "yes", "on"]);
|
|
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
|
+
);
|
|
21
|
+
const COMMAND_COMPLETIONS = [
|
|
22
|
+
{ value: "display", label: "Keep system and display awake" },
|
|
23
|
+
{ value: "sleep", label: "Keep system awake; allow display sleep" },
|
|
24
|
+
{ value: "status", label: "Show current status" },
|
|
25
|
+
{ value: "mode", label: "Choose keep-awake mode" },
|
|
26
|
+
{ value: "stop", label: "Release inhibitor for now" },
|
|
27
|
+
{ value: "help", label: "Show command help" },
|
|
28
|
+
];
|
|
29
|
+
const MENU_OPTIONS = {
|
|
30
|
+
display: "Keep system and display awake",
|
|
31
|
+
sleep: "Keep system awake; allow display sleep",
|
|
32
|
+
status: "Show current status",
|
|
33
|
+
stop: "Release inhibitor for now",
|
|
34
|
+
help: "Show command help",
|
|
35
|
+
} as const;
|
|
36
|
+
const MODE_OPTIONS = {
|
|
37
|
+
display: "Keep system and display awake",
|
|
38
|
+
sleep: "Keep system awake; allow display sleep",
|
|
39
|
+
} as const;
|
|
40
|
+
|
|
41
|
+
type CaffeinateMode = "sleep" | "display";
|
|
42
|
+
type CommandAction = "menu" | "help" | "status" | "mode" | "sleep" | "display" | "stop";
|
|
43
|
+
type CommandContext = ExtensionCommandContext;
|
|
9
44
|
|
|
10
45
|
interface InhibitorCommand {
|
|
11
46
|
command: string;
|
|
12
47
|
args: string[];
|
|
13
48
|
description: string;
|
|
14
49
|
releaseOnStdinClose?: boolean;
|
|
50
|
+
custom?: boolean;
|
|
15
51
|
}
|
|
16
52
|
|
|
17
53
|
interface CaffeinateState {
|
|
@@ -22,20 +58,32 @@ interface CaffeinateState {
|
|
|
22
58
|
activeTurns: number;
|
|
23
59
|
available: boolean;
|
|
24
60
|
disabled: boolean;
|
|
61
|
+
mode: CaffeinateMode;
|
|
62
|
+
settingsLoaded: boolean;
|
|
63
|
+
settingsError?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface CaffeinateSettings {
|
|
67
|
+
mode: CaffeinateMode;
|
|
68
|
+
updatedAt: number;
|
|
25
69
|
}
|
|
26
70
|
|
|
27
71
|
const state: CaffeinateState = {
|
|
28
72
|
activeTurns: 0,
|
|
29
73
|
available: true,
|
|
30
74
|
disabled: isDisabled(),
|
|
75
|
+
mode: DEFAULT_MODE,
|
|
76
|
+
settingsLoaded: false,
|
|
31
77
|
};
|
|
32
78
|
|
|
33
79
|
export default function caffeinate(pi: ExtensionAPI) {
|
|
34
|
-
pi.on("session_start", (_event, ctx) => {
|
|
80
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
81
|
+
await loadSettingsIntoState(ctx);
|
|
35
82
|
updateStatus(ctx);
|
|
36
83
|
});
|
|
37
84
|
|
|
38
|
-
pi.on("agent_start", (_event, ctx) => {
|
|
85
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
86
|
+
await ensureSettingsLoaded(ctx);
|
|
39
87
|
state.activeTurns += 1;
|
|
40
88
|
startInhibitor(ctx);
|
|
41
89
|
});
|
|
@@ -52,22 +100,167 @@ export default function caffeinate(pi: ExtensionAPI) {
|
|
|
52
100
|
ctx.ui.setStatus(STATUS_KEY, undefined);
|
|
53
101
|
});
|
|
54
102
|
|
|
55
|
-
pi.registerCommand("caffeinate
|
|
56
|
-
description: "
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
103
|
+
pi.registerCommand("caffeinate", {
|
|
104
|
+
description: "Open pi-caffeinate keep-awake controls",
|
|
105
|
+
getArgumentCompletions: (prefix) => commandCompletions(prefix),
|
|
106
|
+
handler: async (args, ctx) => {
|
|
107
|
+
await ensureSettingsLoaded(ctx);
|
|
108
|
+
await handleCaffeinateCommand(args, ctx);
|
|
60
109
|
},
|
|
61
110
|
});
|
|
62
111
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function handleCaffeinateCommand(args: string, ctx: CommandContext) {
|
|
115
|
+
const command = parseCommand(args);
|
|
116
|
+
switch (command) {
|
|
117
|
+
case "menu":
|
|
118
|
+
await showMenu(ctx);
|
|
119
|
+
return;
|
|
120
|
+
case "help":
|
|
121
|
+
ctx.ui.notify(buildCommandGuide(), "info");
|
|
122
|
+
return;
|
|
123
|
+
case "status":
|
|
124
|
+
showStatus(ctx);
|
|
125
|
+
return;
|
|
126
|
+
case "mode":
|
|
127
|
+
await showModeSelector(ctx);
|
|
128
|
+
return;
|
|
129
|
+
case "sleep":
|
|
130
|
+
await setMode(ctx, "sleep");
|
|
131
|
+
return;
|
|
132
|
+
case "display":
|
|
133
|
+
await setMode(ctx, "display");
|
|
134
|
+
return;
|
|
135
|
+
case "stop":
|
|
136
|
+
stopCaffeinate(ctx, "manual stop");
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ctx.ui.notify(`Unknown /caffeinate command: ${args.trim()}\n\n${buildCommandGuide()}`, "warning");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function showMenu(ctx: CommandContext) {
|
|
144
|
+
if (!ctx.hasUI) {
|
|
145
|
+
ctx.ui.notify(`${buildCommandGuide()}\n\n${describeState()}`, statusLevel());
|
|
146
|
+
updateStatus(ctx);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const choice = await ctx.ui.select("pi-caffeinate controls", Object.values(MENU_OPTIONS));
|
|
151
|
+
switch (choice) {
|
|
152
|
+
case MENU_OPTIONS.status:
|
|
153
|
+
showStatus(ctx);
|
|
154
|
+
return;
|
|
155
|
+
case MENU_OPTIONS.sleep:
|
|
156
|
+
await setMode(ctx, "sleep");
|
|
157
|
+
return;
|
|
158
|
+
case MENU_OPTIONS.display:
|
|
159
|
+
await setMode(ctx, "display");
|
|
160
|
+
return;
|
|
161
|
+
case MENU_OPTIONS.stop:
|
|
162
|
+
stopCaffeinate(ctx, "manual stop");
|
|
163
|
+
return;
|
|
164
|
+
case MENU_OPTIONS.help:
|
|
165
|
+
ctx.ui.notify(buildCommandGuide(), "info");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function showModeSelector(ctx: CommandContext) {
|
|
171
|
+
if (!ctx.hasUI) {
|
|
172
|
+
ctx.ui.notify(
|
|
173
|
+
`Mode selection needs an interactive UI. Run /caffeinate sleep or /caffeinate display.\n\n${describeState()}`,
|
|
174
|
+
statusLevel(),
|
|
175
|
+
);
|
|
176
|
+
updateStatus(ctx);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const choice = await ctx.ui.select(
|
|
181
|
+
`pi-caffeinate mode (current: ${formatMode(state.mode)})`,
|
|
182
|
+
Object.values(MODE_OPTIONS),
|
|
183
|
+
);
|
|
184
|
+
if (choice === MODE_OPTIONS.sleep) {
|
|
185
|
+
await setMode(ctx, "sleep");
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
if (choice === MODE_OPTIONS.display) {
|
|
189
|
+
await setMode(ctx, "display");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function setMode(ctx: ExtensionContext, mode: CaffeinateMode) {
|
|
194
|
+
const previousMode = state.mode;
|
|
195
|
+
state.mode = mode;
|
|
196
|
+
state.settingsError = undefined;
|
|
197
|
+
|
|
198
|
+
let saved = true;
|
|
199
|
+
try {
|
|
200
|
+
await saveSettings({ mode, updatedAt: Date.now() });
|
|
201
|
+
} catch (error) {
|
|
202
|
+
saved = false;
|
|
203
|
+
state.settingsError = `settings save failed: ${formatError(error)}`;
|
|
204
|
+
ctx.ui.notify(`pi-caffeinate settings save failed: ${formatError(error)}`, "warning");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (state.process && previousMode !== mode && !state.command?.custom) {
|
|
208
|
+
stopInhibitor(ctx, "mode changed", { notify: false });
|
|
209
|
+
startInhibitor(ctx);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
ctx.ui.notify(
|
|
213
|
+
saved
|
|
214
|
+
? `pi-caffeinate mode set to ${formatMode(mode)} and saved.`
|
|
215
|
+
: `pi-caffeinate mode set to ${formatMode(mode)} for this session, but settings were not saved.`,
|
|
216
|
+
saved ? "info" : "warning",
|
|
217
|
+
);
|
|
218
|
+
updateStatus(ctx);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function showStatus(ctx: ExtensionContext) {
|
|
222
|
+
ctx.ui.notify(describeState(), statusLevel());
|
|
223
|
+
updateStatus(ctx);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function stopCaffeinate(ctx: ExtensionContext, reason: string) {
|
|
227
|
+
state.activeTurns = 0;
|
|
228
|
+
stopInhibitor(ctx, reason);
|
|
229
|
+
updateStatus(ctx);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function parseCommand(args: string): CommandAction | "unknown" {
|
|
233
|
+
const command = args.trim().toLowerCase();
|
|
234
|
+
if (!command) return "menu";
|
|
235
|
+
if (command === "help") return "help";
|
|
236
|
+
if (command === "status") return "status";
|
|
237
|
+
if (command === "mode" || command === "config" || command === "settings") return "mode";
|
|
238
|
+
if (command === "sleep" || command === "system") return "sleep";
|
|
239
|
+
if (command === "display" || command === "screen") return "display";
|
|
240
|
+
if (command === "stop" || command === "off") return "stop";
|
|
241
|
+
return "unknown";
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function commandCompletions(prefix: string) {
|
|
245
|
+
const normalized = prefix.trim().toLowerCase();
|
|
246
|
+
if (normalized.includes(" ")) return null;
|
|
247
|
+
|
|
248
|
+
const matches = COMMAND_COMPLETIONS.filter((completion) =>
|
|
249
|
+
completion.value.startsWith(normalized),
|
|
250
|
+
);
|
|
251
|
+
return matches.length > 0 ? matches : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function buildCommandGuide() {
|
|
255
|
+
return [
|
|
256
|
+
"pi-caffeinate commands:",
|
|
257
|
+
"/caffeinate — open keep-awake controls",
|
|
258
|
+
"/caffeinate display — keep the system and display awake",
|
|
259
|
+
"/caffeinate sleep — keep the system awake while allowing display sleep",
|
|
260
|
+
"/caffeinate status — show current mode, settings, and inhibitor state",
|
|
261
|
+
"/caffeinate mode — choose a keep-awake mode",
|
|
262
|
+
"/caffeinate stop — release the active inhibitor until the next agent run",
|
|
263
|
+
].join("\n");
|
|
71
264
|
}
|
|
72
265
|
|
|
73
266
|
function startInhibitor(ctx: ExtensionContext) {
|
|
@@ -81,7 +274,7 @@ function startInhibitor(ctx: ExtensionContext) {
|
|
|
81
274
|
return;
|
|
82
275
|
}
|
|
83
276
|
|
|
84
|
-
const command = getInhibitorCommand();
|
|
277
|
+
const command = getInhibitorCommand(state.mode);
|
|
85
278
|
if (!command) {
|
|
86
279
|
state.available = false;
|
|
87
280
|
state.lastError = `No supported sleep inhibitor found for ${process.platform}.`;
|
|
@@ -168,49 +361,62 @@ function stopInhibitor(ctx: ExtensionContext, reason: string, options: { notify?
|
|
|
168
361
|
}
|
|
169
362
|
}
|
|
170
363
|
|
|
171
|
-
function getInhibitorCommand(): InhibitorCommand | undefined {
|
|
364
|
+
function getInhibitorCommand(mode: CaffeinateMode): InhibitorCommand | undefined {
|
|
172
365
|
const customCommand = process.env.PI_CAFFEINATE_COMMAND?.trim();
|
|
173
366
|
if (customCommand) {
|
|
174
367
|
const [command, ...args] = splitCommand(customCommand);
|
|
175
|
-
if (command) return { command, args, description: command };
|
|
368
|
+
if (command) return { command, args, description: `custom command (${command})`, custom: true };
|
|
176
369
|
}
|
|
177
370
|
|
|
178
371
|
if (process.platform === "darwin") {
|
|
179
|
-
return parentBoundUnixCommand("caffeinate",
|
|
372
|
+
return parentBoundUnixCommand("caffeinate", macCaffeinateArgs(mode), caffeinateDescription(mode));
|
|
180
373
|
}
|
|
181
374
|
|
|
182
375
|
if (process.platform === "linux") {
|
|
183
376
|
if (isWsl() && commandExists("powershell.exe")) {
|
|
184
|
-
return windowsPowerInhibitorCommand("powershell.exe");
|
|
377
|
+
return windowsPowerInhibitorCommand("powershell.exe", mode);
|
|
185
378
|
}
|
|
186
379
|
|
|
187
380
|
if (commandExists("systemd-inhibit")) {
|
|
381
|
+
const what = mode === "sleep" ? "sleep" : "idle:sleep";
|
|
188
382
|
return parentBoundUnixCommand(
|
|
189
383
|
"systemd-inhibit",
|
|
190
384
|
[
|
|
191
|
-
|
|
385
|
+
`--what=${what}`,
|
|
192
386
|
"--who=pi-caffeinate",
|
|
193
387
|
"--why=Pi agent is running",
|
|
194
388
|
"--mode=block",
|
|
195
389
|
"sleep",
|
|
196
390
|
"infinity",
|
|
197
391
|
],
|
|
198
|
-
|
|
392
|
+
`systemd-inhibit (${formatMode(mode)})`,
|
|
199
393
|
);
|
|
200
394
|
}
|
|
201
395
|
|
|
202
396
|
if (commandExists("caffeinate")) {
|
|
203
|
-
return parentBoundUnixCommand(
|
|
397
|
+
return parentBoundUnixCommand(
|
|
398
|
+
"caffeinate",
|
|
399
|
+
macCaffeinateArgs(mode),
|
|
400
|
+
caffeinateDescription(mode),
|
|
401
|
+
);
|
|
204
402
|
}
|
|
205
403
|
}
|
|
206
404
|
|
|
207
405
|
if (process.platform === "win32") {
|
|
208
|
-
return windowsPowerInhibitorCommand("powershell.exe");
|
|
406
|
+
return windowsPowerInhibitorCommand("powershell.exe", mode);
|
|
209
407
|
}
|
|
210
408
|
|
|
211
409
|
return undefined;
|
|
212
410
|
}
|
|
213
411
|
|
|
412
|
+
function macCaffeinateArgs(mode: CaffeinateMode) {
|
|
413
|
+
return mode === "sleep" ? ["-ims"] : ["-dimsu"];
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function caffeinateDescription(mode: CaffeinateMode) {
|
|
417
|
+
return `caffeinate (${formatMode(mode)})`;
|
|
418
|
+
}
|
|
419
|
+
|
|
214
420
|
function parentBoundUnixCommand(
|
|
215
421
|
command: string,
|
|
216
422
|
args: string[],
|
|
@@ -292,17 +498,18 @@ function splitCommand(input: string) {
|
|
|
292
498
|
return parts;
|
|
293
499
|
}
|
|
294
500
|
|
|
295
|
-
function windowsPowerInhibitorCommand(command: string): InhibitorCommand {
|
|
501
|
+
function windowsPowerInhibitorCommand(command: string, mode: CaffeinateMode): InhibitorCommand {
|
|
296
502
|
return {
|
|
297
503
|
command,
|
|
298
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsInhibitorScript()],
|
|
299
|
-
description:
|
|
504
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsInhibitorScript(mode)],
|
|
505
|
+
description: `PowerShell SetThreadExecutionState (${formatMode(mode)})`,
|
|
300
506
|
releaseOnStdinClose: true,
|
|
301
507
|
};
|
|
302
508
|
}
|
|
303
509
|
|
|
304
|
-
function windowsInhibitorScript() {
|
|
305
|
-
|
|
510
|
+
function windowsInhibitorScript(mode: CaffeinateMode) {
|
|
511
|
+
const flags = mode === "sleep" ? "0x80000001" : "0x80000003";
|
|
512
|
+
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 }`;
|
|
306
513
|
}
|
|
307
514
|
|
|
308
515
|
function isWsl() {
|
|
@@ -320,7 +527,7 @@ function updateStatus(ctx: ExtensionContext) {
|
|
|
320
527
|
}
|
|
321
528
|
|
|
322
529
|
if (state.process) {
|
|
323
|
-
ctx.ui.setStatus(STATUS_KEY, `${getIcon()}
|
|
530
|
+
ctx.ui.setStatus(STATUS_KEY, `${getIcon()} ${statusModeLabel()}`);
|
|
324
531
|
return;
|
|
325
532
|
}
|
|
326
533
|
|
|
@@ -333,14 +540,133 @@ function updateStatus(ctx: ExtensionContext) {
|
|
|
333
540
|
}
|
|
334
541
|
|
|
335
542
|
function describeState() {
|
|
336
|
-
|
|
543
|
+
const customCommand = hasCustomCommand();
|
|
544
|
+
const lines = [
|
|
545
|
+
`Mode: ${formatMode(state.mode)}${customCommand ? " (overridden by custom command)" : ""}`,
|
|
546
|
+
`Settings: ${SETTINGS_FILE}`,
|
|
547
|
+
];
|
|
548
|
+
|
|
549
|
+
if (customCommand) lines.push("Custom command: PI_CAFFEINATE_COMMAND overrides the saved mode.");
|
|
550
|
+
if (state.settingsError) lines.push(`Settings warning: ${state.settingsError}`);
|
|
551
|
+
if (state.disabled) {
|
|
552
|
+
lines.unshift("pi-caffeinate is disabled by PI_CAFFEINATE_DISABLED.");
|
|
553
|
+
return lines.join("\n");
|
|
554
|
+
}
|
|
555
|
+
|
|
337
556
|
if (state.process) {
|
|
338
557
|
const seconds = state.startedAt ? Math.round((Date.now() - state.startedAt) / 1000) : 0;
|
|
339
|
-
|
|
558
|
+
lines.unshift(
|
|
559
|
+
`pi-caffeinate is active using ${state.command?.description ?? "an inhibitor"} for ${seconds}s.`,
|
|
560
|
+
);
|
|
561
|
+
return lines.join("\n");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (!state.available) {
|
|
565
|
+
lines.unshift(`pi-caffeinate is unavailable: ${state.lastError ?? "unknown reason"}`);
|
|
566
|
+
return lines.join("\n");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
lines.unshift("pi-caffeinate is idle and will keep the computer awake during the next agent run.");
|
|
570
|
+
return lines.join("\n");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function statusLevel() {
|
|
574
|
+
return state.available && !state.settingsError ? "info" : "warning";
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function statusModeLabel() {
|
|
578
|
+
if (state.command?.custom) return "custom";
|
|
579
|
+
return state.mode === "sleep" ? "sleep" : "display";
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function formatMode(mode: CaffeinateMode) {
|
|
583
|
+
return mode === "sleep" ? "sleep-only" : "display-awake";
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function ensureSettingsLoaded(ctx: ExtensionContext) {
|
|
587
|
+
if (state.disabled || state.settingsLoaded) return;
|
|
588
|
+
await loadSettingsIntoState(ctx);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
async function loadSettingsIntoState(ctx: ExtensionContext) {
|
|
592
|
+
if (state.disabled) {
|
|
593
|
+
state.settingsLoaded = true;
|
|
594
|
+
state.settingsError = undefined;
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const settings = await loadSettings();
|
|
599
|
+
state.settingsLoaded = true;
|
|
600
|
+
state.settingsError = undefined;
|
|
601
|
+
|
|
602
|
+
if (settings.kind === "loaded") {
|
|
603
|
+
state.mode = settings.settings.mode;
|
|
604
|
+
return;
|
|
340
605
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
606
|
+
|
|
607
|
+
state.mode = DEFAULT_MODE;
|
|
608
|
+
if (settings.kind === "invalid") {
|
|
609
|
+
state.settingsError = settings.reason;
|
|
610
|
+
ctx.ui.notify(
|
|
611
|
+
`pi-caffeinate settings ignored: ${settings.reason}; using ${formatMode(DEFAULT_MODE)} mode.`,
|
|
612
|
+
"warning",
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
async function loadSettings(): Promise<
|
|
618
|
+
| { kind: "missing" }
|
|
619
|
+
| { kind: "invalid"; reason: string }
|
|
620
|
+
| { kind: "loaded"; settings: CaffeinateSettings }
|
|
621
|
+
> {
|
|
622
|
+
let text: string;
|
|
623
|
+
try {
|
|
624
|
+
text = await readFile(SETTINGS_FILE, "utf8");
|
|
625
|
+
} catch (error) {
|
|
626
|
+
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
627
|
+
return { kind: "invalid", reason: formatError(error) };
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
try {
|
|
631
|
+
const parsed = JSON.parse(text) as unknown;
|
|
632
|
+
const settings = normalizeCaffeinateSettings(parsed);
|
|
633
|
+
if (settings) return { kind: "loaded", settings };
|
|
634
|
+
return { kind: "invalid", reason: 'expected { "mode": "sleep" | "display" }' };
|
|
635
|
+
} catch (error) {
|
|
636
|
+
return { kind: "invalid", reason: formatError(error) };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function normalizeCaffeinateSettings(value: unknown): CaffeinateSettings | undefined {
|
|
641
|
+
if (!value || typeof value !== "object") return undefined;
|
|
642
|
+
const settings = value as { mode?: unknown; updatedAt?: unknown };
|
|
643
|
+
if (!isCaffeinateMode(settings.mode)) return undefined;
|
|
644
|
+
if (settings.updatedAt !== undefined && typeof settings.updatedAt !== "number") return undefined;
|
|
645
|
+
return { mode: settings.mode, updatedAt: settings.updatedAt ?? 0 };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function isCaffeinateMode(value: unknown): value is CaffeinateMode {
|
|
649
|
+
return value === "sleep" || value === "display";
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
async function saveSettings(settings: CaffeinateSettings) {
|
|
653
|
+
await mkdir(dirname(SETTINGS_FILE), { recursive: true });
|
|
654
|
+
const tempFile = `${SETTINGS_FILE}.${process.pid}.${Date.now()}.tmp`;
|
|
655
|
+
try {
|
|
656
|
+
await writeFile(tempFile, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
657
|
+
await rename(tempFile, SETTINGS_FILE);
|
|
658
|
+
} catch (error) {
|
|
659
|
+
await rm(tempFile, { force: true }).catch(() => undefined);
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
665
|
+
return error instanceof Error && "code" in error;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function formatError(error: unknown) {
|
|
669
|
+
return error instanceof Error ? error.message : String(error);
|
|
344
670
|
}
|
|
345
671
|
|
|
346
672
|
function formatExit(code: number | null, signal: NodeJS.Signals | null) {
|
|
@@ -353,6 +679,10 @@ function isDisabled() {
|
|
|
353
679
|
return value ? DISABLED_VALUES.has(value) : false;
|
|
354
680
|
}
|
|
355
681
|
|
|
682
|
+
function hasCustomCommand() {
|
|
683
|
+
return Boolean(process.env.PI_CAFFEINATE_COMMAND?.trim());
|
|
684
|
+
}
|
|
685
|
+
|
|
356
686
|
function getIcon() {
|
|
357
687
|
return process.env.PI_CAFFEINATE_ICON?.trim() ?? "💊";
|
|
358
688
|
}
|