@devvoid/voidshift-awake 0.2.0 → 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 +38 -15
- package/dist/config.d.ts +8 -0
- package/dist/config.js +32 -0
- package/dist/decide.d.ts +2 -0
- package/dist/decide.js +3 -3
- package/dist/index.js +201 -25
- package/dist/plist.d.ts +1 -0
- package/dist/plist.js +9 -0
- package/dist/time.d.ts +2 -0
- package/dist/time.js +9 -0
- package/dist/types.d.ts +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
# VoidShift
|
|
2
2
|
|
|
3
|
-
One CLI for the Void dev tools. Install it once, run `
|
|
4
|
-
tools you want, and it sets each one up with good
|
|
3
|
+
One CLI for the Void dev tools. Install it once, run `vsf` (short for
|
|
4
|
+
`voidshift`), pick the tools you want, and it sets each one up with good
|
|
5
|
+
defaults you can change. Everything setup does can be seen and changed later
|
|
6
|
+
from the command line.
|
|
5
7
|
|
|
6
8
|
> Early release. macOS only for now; Windows and Linux are next.
|
|
7
9
|
|
|
8
10
|
```sh
|
|
9
11
|
npm i -g @devvoid/voidshift
|
|
10
|
-
|
|
11
|
-
|
|
12
|
+
vsf setup # pick tools and set them up
|
|
13
|
+
vsf setup --defaults # take every default, ask only what has none
|
|
14
|
+
vsf # what every tool is doing (first run: setup)
|
|
15
|
+
vsf help # every command; vsf <tool> help for a tool's
|
|
12
16
|
```
|
|
13
17
|
|
|
14
18
|
## Tools
|
|
@@ -22,9 +26,10 @@ voidshift --defaults # take every default, ask only what has none
|
|
|
22
26
|
### awake
|
|
23
27
|
|
|
24
28
|
```sh
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
vsf awake on [--until 7am | --for 6h] [--battery 20]
|
|
30
|
+
vsf awake status
|
|
31
|
+
vsf awake logs [-f] # what the watchdog decided
|
|
32
|
+
vsf awake off
|
|
28
33
|
```
|
|
29
34
|
|
|
30
35
|
Setup asks for your Mac password once to add a rule that allows only
|
|
@@ -58,9 +63,18 @@ your checkout is never touched, and labels the card as it goes (`vs-dev-v1`,
|
|
|
58
63
|
`vs-qa-v1`, then `vs-qa-verified` or `vs-parked` with a comment saying why).
|
|
59
64
|
|
|
60
65
|
```sh
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
66
|
+
vsf agents status # runner, the card in progress, parked cards
|
|
67
|
+
vsf agents run ENG-123 [--from qa] # run one card now, in this terminal
|
|
68
|
+
vsf agents start | stop | restart # the background runner (stop cancels the card)
|
|
69
|
+
vsf agents pause | resume # pick up no new cards for a while
|
|
70
|
+
vsf agents cancel | retry ENG-123 # stop a card, or queue a parked one again
|
|
71
|
+
vsf agents runs | show ENG-123 # what happened, step by step
|
|
72
|
+
vsf agents logs [ENG-123] [-f] # the runner log, or a card's step logs
|
|
73
|
+
vsf agents workflows # workflows in the order they match
|
|
74
|
+
vsf agents workflow add|edit|show|rename|move|rm
|
|
75
|
+
vsf agents projects | project add|edit|show|rename|rm
|
|
76
|
+
vsf agents homes | link DIR # where the agents are installed
|
|
77
|
+
vsf agents clean # old worktrees and run logs
|
|
64
78
|
```
|
|
65
79
|
|
|
66
80
|
### memory
|
|
@@ -73,8 +87,11 @@ decisions and their why, dead ends, things that are not obvious from the code.
|
|
|
73
87
|
Background agents read it for each card and save what they learn.
|
|
74
88
|
|
|
75
89
|
```sh
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
vsf memory status # the vault and where recall is on
|
|
91
|
+
vsf memory search WORDS # notes recall would bring up
|
|
92
|
+
vsf memory try "a prompt" # exactly what recall would add to that prompt
|
|
93
|
+
vsf memory check # notes missing from INDEX.md, broken links
|
|
94
|
+
vsf memory on | off # turn recall on or off
|
|
78
95
|
```
|
|
79
96
|
|
|
80
97
|
Setup creates a vault at `~/memory` or uses one you already have. Codex asks you
|
|
@@ -91,9 +108,15 @@ to trust the recall hook once: run `/hooks` in Codex.
|
|
|
91
108
|
- **Your config** lives in `~/.config/voidshift`.
|
|
92
109
|
|
|
93
110
|
```sh
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
111
|
+
vsf doctor # check everything is set up, and whether an update is out
|
|
112
|
+
vsf config [tool] # every saved setting
|
|
113
|
+
vsf config set KEY VALUE # e.g. vsf config set agents.models.dev opus
|
|
114
|
+
vsf config edit [tool] # edit settings in $EDITOR, checked before saving
|
|
115
|
+
vsf update # update VoidShift and every tool you added
|
|
116
|
+
vsf tools # list tools
|
|
117
|
+
|
|
118
|
+
Read commands take `--json`. Workflow and project names can be shortened to
|
|
119
|
+
any unique start, e.g. `tether` for `tether-relay-build`.
|
|
97
120
|
```
|
|
98
121
|
|
|
99
122
|
## Development
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type AwakeConfig = {
|
|
2
|
+
battery: number;
|
|
3
|
+
heat: boolean;
|
|
4
|
+
revertOnReboot: boolean;
|
|
5
|
+
};
|
|
6
|
+
export declare function validateConfig(value: Record<string, unknown>): string[];
|
|
7
|
+
export declare function readConfig(configDir: string): AwakeConfig;
|
|
8
|
+
export declare function writeConfig(configDir: string, value: Record<string, unknown>): string[];
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { DEFAULT_BATTERY_FLOOR, readState, writeState } from "./state.js";
|
|
2
|
+
const KEYS = ['battery', 'heat', 'revertOnReboot'];
|
|
3
|
+
// Returns the problems with a proposed config change; empty means it is fine.
|
|
4
|
+
export function validateConfig(value) {
|
|
5
|
+
const problems = [];
|
|
6
|
+
for (const [key, v] of Object.entries(value)) {
|
|
7
|
+
if (!KEYS.includes(key))
|
|
8
|
+
problems.push(`Unknown setting "${key}". Known: ${KEYS.join(', ')}.`);
|
|
9
|
+
else if (key === 'battery' && !(typeof v === 'number' && Number.isInteger(v) && v >= 5 && v <= 95)) {
|
|
10
|
+
problems.push('battery must be a whole number from 5 to 95.');
|
|
11
|
+
}
|
|
12
|
+
else if (key !== 'battery' && typeof v !== 'boolean')
|
|
13
|
+
problems.push(`${key} must be true or false.`);
|
|
14
|
+
}
|
|
15
|
+
return problems;
|
|
16
|
+
}
|
|
17
|
+
export function readConfig(configDir) {
|
|
18
|
+
const s = readState(configDir);
|
|
19
|
+
return { battery: s.batteryFloor, heat: s.heat !== false, revertOnReboot: s.revertOnReboot !== false };
|
|
20
|
+
}
|
|
21
|
+
export function writeConfig(configDir, value) {
|
|
22
|
+
const problems = validateConfig(value);
|
|
23
|
+
if (problems.length)
|
|
24
|
+
return problems;
|
|
25
|
+
const state = readState(configDir);
|
|
26
|
+
// The written value is the whole config: a missing key goes back to its default.
|
|
27
|
+
state.batteryFloor = value.battery ?? DEFAULT_BATTERY_FLOOR;
|
|
28
|
+
state.heat = value.heat;
|
|
29
|
+
state.revertOnReboot = value.revertOnReboot;
|
|
30
|
+
writeState(configDir, state);
|
|
31
|
+
return [];
|
|
32
|
+
}
|
package/dist/decide.d.ts
CHANGED
package/dist/decide.js
CHANGED
|
@@ -4,9 +4,9 @@ export function decide(state, sys, now) {
|
|
|
4
4
|
if (!state.managed)
|
|
5
5
|
return { action: 'keep' };
|
|
6
6
|
if (!sys.sleepDisabled)
|
|
7
|
-
return { action: 'forget', reason: 'sleep was turned back on outside
|
|
7
|
+
return { action: 'forget', reason: 'sleep was turned back on outside VoidShift' };
|
|
8
8
|
const since = state.since ? new Date(state.since) : null;
|
|
9
|
-
if (since && sys.bootTime && sys.bootTime > since)
|
|
9
|
+
if (state.revertOnReboot !== false && since && sys.bootTime && sys.bootTime > since)
|
|
10
10
|
return { action: 'revert', reason: 'the Mac restarted' };
|
|
11
11
|
if (state.until && now >= new Date(state.until))
|
|
12
12
|
return { action: 'revert', reason: 'the timer ended' };
|
|
@@ -14,7 +14,7 @@ export function decide(state, sys, now) {
|
|
|
14
14
|
if (onBattery && percent !== null && percent <= state.batteryFloor) {
|
|
15
15
|
return { action: 'revert', reason: `battery reached ${percent}%` };
|
|
16
16
|
}
|
|
17
|
-
if (sys.thermalSerious)
|
|
17
|
+
if (state.heat !== false && sys.thermalSerious)
|
|
18
18
|
return { action: 'revert', reason: 'the Mac is running too hot' };
|
|
19
19
|
return { action: 'keep' };
|
|
20
20
|
}
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,63 @@
|
|
|
1
|
-
import { rmSync } from 'node:fs';
|
|
1
|
+
import { closeSync, existsSync, openSync, readFileSync, readSync, rmSync, statSync } from 'node:fs';
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
|
+
import { readConfig, writeConfig } from "./config.js";
|
|
3
4
|
import { decide } from "./decide.js";
|
|
4
5
|
import { logPath, readState, stateDir, writeState } from "./state.js";
|
|
5
|
-
import { SUDOERS_PATH, installSudoers, loadWatchdog, readSystem, removeSudoers, ruleInstalled, ruleWorks, setSleepDisabled, unloadWatchdog, watchdogLoaded, } from "./system.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SUDOERS_PATH, installSudoers, loadWatchdog, plistPath, readSystem, removeSudoers, ruleInstalled, ruleWorks, setSleepDisabled, unloadWatchdog, watchdogLoaded, } from "./system.js";
|
|
7
|
+
import { programPaths } from "./plist.js";
|
|
8
|
+
import { formatLeft, minutesLeft, parseFor, parseUntil } from "./time.js";
|
|
7
9
|
const clock = (d) => d.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' });
|
|
8
10
|
function ensureWatchdog(ctx) {
|
|
9
11
|
return watchdogLoaded() || loadWatchdog(ctx.home, [...ctx.selfCommand, 'awake', 'tick'], logPath(ctx.configDir));
|
|
10
12
|
}
|
|
13
|
+
const USAGE = {
|
|
14
|
+
on: 'Usage: vsf awake on [--until TIME | --for DURATION] [--battery PERCENT]\n Keep the Mac running with the lid closed. TIME is like 7am or 19:30, DURATION like 6h.',
|
|
15
|
+
off: 'Usage: vsf awake off\n Turn sleep back on.',
|
|
16
|
+
status: 'Usage: vsf awake status [--json]\n Show whether the Mac is being kept awake, and the guardrails.',
|
|
17
|
+
logs: 'Usage: vsf awake logs [-f | --follow] [--lines N]\n Show the last N (default 50) watchdog log lines. -f keeps printing new ones.',
|
|
18
|
+
help: 'Usage: vsf awake help [COMMAND]\n Show usage for one command, or the list.',
|
|
19
|
+
};
|
|
20
|
+
const summary = () => Object.values(USAGE).map((u) => u.split('\n')[0]).join('\n');
|
|
21
|
+
const bad = (ctx, command, e) => {
|
|
22
|
+
ctx.log(e instanceof Error ? e.message.split(/\. To specify|\n/)[0] : String(e));
|
|
23
|
+
ctx.log(USAGE[command]);
|
|
24
|
+
return 2;
|
|
25
|
+
};
|
|
26
|
+
async function help(args, ctx) {
|
|
27
|
+
const name = args.find((a) => !a.startsWith('-'));
|
|
28
|
+
if (!name)
|
|
29
|
+
ctx.log(summary());
|
|
30
|
+
else if (USAGE[name])
|
|
31
|
+
ctx.log(USAGE[name]);
|
|
32
|
+
else {
|
|
33
|
+
ctx.log(`No command called "${name}".`);
|
|
34
|
+
ctx.log(summary());
|
|
35
|
+
return 2;
|
|
36
|
+
}
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
11
39
|
async function on(args, ctx) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
40
|
+
let values;
|
|
41
|
+
try {
|
|
42
|
+
({ values } = parseArgs({
|
|
43
|
+
args,
|
|
44
|
+
options: { until: { type: 'string' }, for: { type: 'string' }, battery: { type: 'string' }, help: { type: 'boolean', short: 'h' } },
|
|
45
|
+
allowPositionals: true,
|
|
46
|
+
strict: true,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
return bad(ctx, 'on', e);
|
|
51
|
+
}
|
|
52
|
+
if (values.help) {
|
|
53
|
+
ctx.log(USAGE.on);
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
16
56
|
const now = new Date();
|
|
17
57
|
let until = null;
|
|
18
58
|
if (values.until && values.for) {
|
|
19
59
|
ctx.log('Use --until or --for, not both.');
|
|
60
|
+
ctx.log(USAGE.on);
|
|
20
61
|
return 2;
|
|
21
62
|
}
|
|
22
63
|
if (values.until)
|
|
@@ -25,54 +66,181 @@ async function on(args, ctx) {
|
|
|
25
66
|
until = parseFor(values.for, now);
|
|
26
67
|
if ((values.until || values.for) && !until) {
|
|
27
68
|
ctx.log('Could not read that time. Try --until 7am, --until 19:30 or --for 6h.');
|
|
69
|
+
ctx.log(USAGE.on);
|
|
28
70
|
return 2;
|
|
29
71
|
}
|
|
30
|
-
const
|
|
72
|
+
const prior = readState(ctx.configDir);
|
|
73
|
+
const floor = values.battery ? Number(values.battery) : prior.batteryFloor;
|
|
31
74
|
if (!Number.isInteger(floor) || floor < 5 || floor > 95) {
|
|
32
75
|
ctx.log('--battery takes a whole percentage between 5 and 95.');
|
|
76
|
+
ctx.log(USAGE.on);
|
|
33
77
|
return 2;
|
|
34
78
|
}
|
|
79
|
+
const already = prior.managed && readSystem().sleepDisabled;
|
|
35
80
|
if (!setSleepDisabled(true)) {
|
|
36
|
-
ctx.log('Could not turn sleep off. Run
|
|
81
|
+
ctx.log('Could not turn sleep off. Run vsf add awake first.');
|
|
37
82
|
return 1;
|
|
38
83
|
}
|
|
39
84
|
writeState(ctx.configDir, {
|
|
85
|
+
...prior,
|
|
40
86
|
managed: true,
|
|
41
|
-
since: now.toISOString(),
|
|
42
|
-
|
|
87
|
+
since: already && prior.since ? prior.since : now.toISOString(),
|
|
88
|
+
// Changing only --battery on a running session keeps its end time.
|
|
89
|
+
until: until ? until.toISOString() : already ? prior.until : undefined,
|
|
43
90
|
batteryFloor: floor,
|
|
44
91
|
});
|
|
45
92
|
if (!ensureWatchdog(ctx))
|
|
46
|
-
ctx.log('Warning: the watchdog is not running, so guardrails will not act. Run
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
ctx.log(
|
|
93
|
+
ctx.log('Warning: the watchdog is not running, so guardrails will not act. Run vsf doctor.');
|
|
94
|
+
const endsAt = until ?? (already && prior.until ? new Date(prior.until) : null);
|
|
95
|
+
const stop = endsAt ? `at ${clock(endsAt)} or at ${floor}% battery` : `at ${floor}% battery`;
|
|
96
|
+
ctx.log(already ? `Updated. Stops ${stop}, or with vsf awake off.` : `Awake. The lid can close. Stops ${stop}, or with vsf awake off.`);
|
|
97
|
+
if (!already)
|
|
98
|
+
ctx.log('Keep it out of a bag: a closed Mac with no airflow runs hot.');
|
|
50
99
|
return 0;
|
|
51
100
|
}
|
|
52
|
-
async function off(
|
|
101
|
+
async function off(args, ctx) {
|
|
102
|
+
try {
|
|
103
|
+
const { values } = parseArgs({ args, options: { help: { type: 'boolean', short: 'h' } }, allowPositionals: true, strict: true });
|
|
104
|
+
if (values.help) {
|
|
105
|
+
ctx.log(USAGE.off);
|
|
106
|
+
return 0;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
return bad(ctx, 'off', e);
|
|
111
|
+
}
|
|
53
112
|
const ok = setSleepDisabled(false);
|
|
54
113
|
writeState(ctx.configDir, { ...readState(ctx.configDir), managed: false, lastReason: 'you turned it off' });
|
|
55
114
|
ctx.log(ok ? 'Sleep restored.' : 'Could not restore sleep. Run: sudo pmset -a disablesleep 0');
|
|
56
115
|
return ok ? 0 : 1;
|
|
57
116
|
}
|
|
58
|
-
async function status(
|
|
117
|
+
async function status(args, ctx) {
|
|
118
|
+
let values;
|
|
119
|
+
try {
|
|
120
|
+
({ values } = parseArgs({
|
|
121
|
+
args,
|
|
122
|
+
options: { json: { type: 'boolean' }, help: { type: 'boolean', short: 'h' } },
|
|
123
|
+
allowPositionals: true,
|
|
124
|
+
strict: true,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
catch (e) {
|
|
128
|
+
return bad(ctx, 'status', e);
|
|
129
|
+
}
|
|
130
|
+
if (values.help) {
|
|
131
|
+
ctx.log(USAGE.status);
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
59
134
|
const state = readState(ctx.configDir);
|
|
60
135
|
const sys = readSystem();
|
|
136
|
+
const on = state.managed && sys.sleepDisabled;
|
|
137
|
+
const left = on && state.until ? minutesLeft(new Date(state.until), new Date()) : null;
|
|
138
|
+
const loaded = watchdogLoaded();
|
|
139
|
+
if (values.json) {
|
|
140
|
+
ctx.log(JSON.stringify({
|
|
141
|
+
on,
|
|
142
|
+
sleepDisabledByOther: sys.sleepDisabled && !state.managed,
|
|
143
|
+
since: on ? (state.since ?? null) : null,
|
|
144
|
+
until: on ? (state.until ?? null) : null,
|
|
145
|
+
minutesLeft: left,
|
|
146
|
+
batteryFloor: state.batteryFloor,
|
|
147
|
+
floorApplies: sys.battery.onBattery,
|
|
148
|
+
power: { onBattery: sys.battery.onBattery, percent: sys.battery.percent },
|
|
149
|
+
watchdog: loaded,
|
|
150
|
+
lastReason: state.lastReason ?? null,
|
|
151
|
+
heat: state.heat !== false,
|
|
152
|
+
revertOnReboot: state.revertOnReboot !== false,
|
|
153
|
+
}, null, 2));
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
61
156
|
const power = sys.battery.onBattery ? `on battery, ${sys.battery.percent ?? '?'}%` : 'on power adapter';
|
|
62
|
-
if (
|
|
157
|
+
if (on) {
|
|
63
158
|
const since = state.since ? clock(new Date(state.since)) : '?';
|
|
64
159
|
const stop = state.until ? `at ${clock(new Date(state.until))} or at ${state.batteryFloor}% battery` : `at ${state.batteryFloor}% battery`;
|
|
65
160
|
ctx.log(`Awake since ${since}. Stops ${stop}. Mac is ${power}.`);
|
|
66
161
|
}
|
|
67
162
|
else if (sys.sleepDisabled) {
|
|
68
|
-
ctx.log(`Sleep is turned off, but not by
|
|
163
|
+
ctx.log(`Sleep is turned off, but not by VoidShift. Mac is ${power}.`);
|
|
69
164
|
}
|
|
70
165
|
else {
|
|
71
166
|
const last = state.lastReason ? ` Last session ended because ${state.lastReason}.` : '';
|
|
72
167
|
ctx.log(`Off. The Mac sleeps normally.${last}`);
|
|
73
168
|
}
|
|
169
|
+
if (left !== null)
|
|
170
|
+
ctx.log(`Time left: ${formatLeft(left)}.`);
|
|
171
|
+
if (!sys.battery.onBattery)
|
|
172
|
+
ctx.log(`Battery floor ${state.batteryFloor}% applies on battery only; the Mac is on power now.`);
|
|
173
|
+
if (!loaded)
|
|
174
|
+
ctx.log('Watchdog: not loaded, guardrails will not act. vsf setup awake fixes it.');
|
|
74
175
|
return 0;
|
|
75
176
|
}
|
|
177
|
+
async function logs(args, ctx) {
|
|
178
|
+
let values;
|
|
179
|
+
try {
|
|
180
|
+
({ values } = parseArgs({
|
|
181
|
+
args,
|
|
182
|
+
options: { follow: { type: 'boolean', short: 'f' }, lines: { type: 'string' }, help: { type: 'boolean', short: 'h' } },
|
|
183
|
+
allowPositionals: true,
|
|
184
|
+
strict: true,
|
|
185
|
+
}));
|
|
186
|
+
}
|
|
187
|
+
catch (e) {
|
|
188
|
+
return bad(ctx, 'logs', e);
|
|
189
|
+
}
|
|
190
|
+
if (values.help) {
|
|
191
|
+
ctx.log(USAGE.logs);
|
|
192
|
+
return 0;
|
|
193
|
+
}
|
|
194
|
+
const n = values.lines === undefined ? 50 : Number(values.lines);
|
|
195
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
196
|
+
ctx.log('--lines takes a whole number, 1 or more.');
|
|
197
|
+
ctx.log(USAGE.logs);
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
const path = logPath(ctx.configDir);
|
|
201
|
+
if (!existsSync(path)) {
|
|
202
|
+
ctx.log('No watchdog log yet.');
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
const text = readFileSync(path, 'utf8');
|
|
206
|
+
const lines = text.split('\n');
|
|
207
|
+
if (lines[lines.length - 1] === '')
|
|
208
|
+
lines.pop();
|
|
209
|
+
for (const l of lines.slice(-n))
|
|
210
|
+
ctx.log(l);
|
|
211
|
+
if (!values.follow)
|
|
212
|
+
return 0;
|
|
213
|
+
// Poll for appended bytes until Ctrl+C. A shrunk file means it was rotated.
|
|
214
|
+
let offset = Buffer.byteLength(text);
|
|
215
|
+
return new Promise((resolve) => {
|
|
216
|
+
const timer = setInterval(() => {
|
|
217
|
+
try {
|
|
218
|
+
const size = statSync(path).size;
|
|
219
|
+
if (size < offset)
|
|
220
|
+
offset = 0;
|
|
221
|
+
if (size === offset)
|
|
222
|
+
return;
|
|
223
|
+
const fd = openSync(path, 'r');
|
|
224
|
+
const buf = Buffer.alloc(size - offset);
|
|
225
|
+
readSync(fd, buf, 0, buf.length, offset);
|
|
226
|
+
closeSync(fd);
|
|
227
|
+
offset = size;
|
|
228
|
+
const chunk = buf.toString('utf8').split('\n');
|
|
229
|
+
if (chunk[chunk.length - 1] === '')
|
|
230
|
+
chunk.pop();
|
|
231
|
+
for (const l of chunk)
|
|
232
|
+
ctx.log(l);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
// The log may be briefly missing; try again next second.
|
|
236
|
+
}
|
|
237
|
+
}, 1000);
|
|
238
|
+
process.once('SIGINT', () => {
|
|
239
|
+
clearInterval(timer);
|
|
240
|
+
resolve(0);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
}
|
|
76
244
|
// Run by launchd every minute.
|
|
77
245
|
async function tick(_args, ctx) {
|
|
78
246
|
const state = readState(ctx.configDir);
|
|
@@ -108,7 +276,7 @@ const awake = {
|
|
|
108
276
|
}
|
|
109
277
|
if (!ensureWatchdog(ctx))
|
|
110
278
|
throw new Error('could not start the watchdog');
|
|
111
|
-
ctx.log(`Ready.
|
|
279
|
+
ctx.log(`Ready. vsf awake on keeps the Mac running until ${floor}% battery or vsf awake off.`);
|
|
112
280
|
},
|
|
113
281
|
async uninstall(ctx) {
|
|
114
282
|
const state = readState(ctx.configDir);
|
|
@@ -125,21 +293,29 @@ const awake = {
|
|
|
125
293
|
checks.push(ruleInstalled() && works
|
|
126
294
|
? { name: 'awake rule', status: 'ok', detail: SUDOERS_PATH }
|
|
127
295
|
: works
|
|
128
|
-
? { name: 'awake rule', status: 'warn', detail: "works only through another tool's sudoers rule", fix: '
|
|
129
|
-
: { name: 'awake rule', status: 'fail', detail: 'cannot turn sleep off without a password', fix: '
|
|
296
|
+
? { name: 'awake rule', status: 'warn', detail: "works only through another tool's sudoers rule", fix: 'vsf setup awake' }
|
|
297
|
+
: { name: 'awake rule', status: 'fail', detail: 'cannot turn sleep off without a password', fix: 'vsf setup awake' });
|
|
130
298
|
checks.push(watchdogLoaded()
|
|
131
299
|
? { name: 'awake watchdog', status: 'ok', detail: 'running every minute' }
|
|
132
|
-
: { name: 'awake watchdog', status: 'fail', detail: 'not loaded, guardrails will not act', fix: '
|
|
300
|
+
: { name: 'awake watchdog', status: 'fail', detail: 'not loaded, guardrails will not act', fix: 'vsf setup awake' });
|
|
301
|
+
const plist = plistPath(ctx.home);
|
|
302
|
+
if (existsSync(plist)) {
|
|
303
|
+
const missing = programPaths(readFileSync(plist, 'utf8')).find((p) => !existsSync(p));
|
|
304
|
+
checks.push(missing
|
|
305
|
+
? { name: 'awake program', status: 'fail', detail: `watchdog points at a missing ${missing}`, fix: 'vsf setup awake' }
|
|
306
|
+
: { name: 'awake program', status: 'ok', detail: 'watchdog program exists' });
|
|
307
|
+
}
|
|
133
308
|
const state = readState(ctx.configDir);
|
|
134
309
|
const sys = readSystem();
|
|
135
310
|
if (sys.sleepDisabled && !state.managed) {
|
|
136
|
-
checks.push({ name: 'awake state', status: 'warn', detail: 'sleep is off, but not by
|
|
311
|
+
checks.push({ name: 'awake state', status: 'warn', detail: 'sleep is off, but not by VoidShift', fix: 'Another tool may be holding it. sudo pmset -a disablesleep 0 resets it' });
|
|
137
312
|
}
|
|
138
313
|
else {
|
|
139
314
|
checks.push({ name: 'awake state', status: 'ok', detail: state.managed ? 'on' : 'off' });
|
|
140
315
|
}
|
|
141
316
|
return checks;
|
|
142
317
|
},
|
|
143
|
-
|
|
318
|
+
config: { read: readConfig, write: writeConfig },
|
|
319
|
+
commands: { on, off, status, logs, help, tick },
|
|
144
320
|
};
|
|
145
321
|
export default awake;
|
package/dist/plist.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function programPaths(plist: string): string[];
|
package/dist/plist.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Absolute paths in the watchdog's ProgramArguments, so doctor can spot a
|
|
2
|
+
// plist that points at something that was moved or removed.
|
|
3
|
+
export function programPaths(plist) {
|
|
4
|
+
const array = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(plist);
|
|
5
|
+
if (!array)
|
|
6
|
+
return [];
|
|
7
|
+
const unxml = (s) => s.replace(/</g, '<').replace(/>/g, '>').replace(/&/g, '&');
|
|
8
|
+
return [...array[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => unxml(m[1])).filter((v) => v.startsWith('/'));
|
|
9
|
+
}
|
package/dist/time.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export declare function parseUntil(input: string, now: Date): Date | null;
|
|
2
2
|
export declare function parseFor(input: string, now: Date): Date | null;
|
|
3
|
+
export declare function minutesLeft(until: Date, now: Date): number;
|
|
4
|
+
export declare function formatLeft(minutes: number): string;
|
package/dist/time.js
CHANGED
|
@@ -35,3 +35,12 @@ export function parseFor(input, now) {
|
|
|
35
35
|
return null;
|
|
36
36
|
return new Date(now.getTime() + minutes * 60_000);
|
|
37
37
|
}
|
|
38
|
+
// Whole minutes until a time, rounded up, never negative.
|
|
39
|
+
export function minutesLeft(until, now) {
|
|
40
|
+
return Math.max(0, Math.ceil((until.getTime() - now.getTime()) / 60_000));
|
|
41
|
+
}
|
|
42
|
+
export function formatLeft(minutes) {
|
|
43
|
+
const h = Math.floor(minutes / 60);
|
|
44
|
+
const m = minutes % 60;
|
|
45
|
+
return h && m ? `${h}h ${m}m` : h ? `${h}h` : `${m}m`;
|
|
46
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -44,4 +44,8 @@ export interface Tool {
|
|
|
44
44
|
uninstall(ctx: ToolContext): Promise<void>;
|
|
45
45
|
checks(ctx: ToolContext): Promise<Check[]>;
|
|
46
46
|
commands?: Record<string, ToolCommand>;
|
|
47
|
+
config?: {
|
|
48
|
+
read(configDir: string): Record<string, unknown>;
|
|
49
|
+
write(configDir: string, value: Record<string, unknown>): string[];
|
|
50
|
+
};
|
|
47
51
|
}
|