@edgehero/pi-dispatch 5.0.0 → 5.2.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.
@@ -83,8 +83,13 @@ export function pauseWindowsVersionRefusal(doc, { supported = PAUSE_WINDOWS_VERS
83
83
  export function heldPauseWindows(text, path) {
84
84
  if ((text ?? "").trim() === "") return { doc: null, windows: [] };
85
85
  const { value, problem } = heldDocument(text, "windows");
86
- const refused = problem ?? (findDuplicateKey(text) ? "has a key written twice" : null);
87
- if (refused) return { doc: value ?? null, problem: `the pause-windows file ${path} ${refused}, so its windows cannot be kept` };
86
+ // Whenever the text parsed, a problem with its shape included (`"windows": {}`), as the loader judges the duplicate first.
87
+ const doubled = value !== undefined && findDuplicateKey(text) !== null;
88
+ const refused = doubled ? "has a key written twice" : problem ?? null;
89
+ // A key written twice hands back no document (issue #846): the callers ask the document whether a newer release wrote
90
+ // it before they read `problem`, and `{"version":1,"version":2}` would send the operator to upgrade to a release that
91
+ // refuses the file too. Broken, never newer, as the loader reads it and as the triggers writer does.
92
+ if (refused) return { doc: doubled ? null : value ?? null, problem: `the pause-windows file ${path} ${refused}, so its windows cannot be kept` };
88
93
  return { doc: value, windows: Array.isArray(value.windows) ? value.windows : [] };
89
94
  }
90
95
 
@@ -158,7 +163,7 @@ function validateTz(tz, at, path) {
158
163
  * (unknown fields dropped; `fromMin`/`toMin` precomputed). Throws `configError` (fail-loud) on any malformed
159
164
  * entry. `path` is for messages only.
160
165
  *
161
- * The file's `version` is judged FIRST, before the windows (issue #761): a newer file's windows may use a shape this
166
+ * The file's `version` is judged right after the duplicate-key check (issue #846) and before the windows (issue #761): a newer file's windows may use a shape this
162
167
  * build would call malformed, and the true message is "upgrade". A newer file throws a configError with
163
168
  * `.newer === true` (the worker defers jobs rather than misread a pause); a broken `version`, or a window using a field
164
169
  * or a value that needs a higher version than the file declares, throws the ordinary configError. `supported`, `table`
@@ -171,6 +176,17 @@ export function parsePauseWindows(text, path, { supported = PAUSE_WINDOWS_VERSIO
171
176
  } catch (error) {
172
177
  throw configError(`pause-windows file is not valid JSON: ${path} (${error.message})`);
173
178
  }
179
+ // Issue #846: a key written twice is REFUSED, after the parse and before anything else, as the triggers loader refuses
180
+ // one (`triggers.mjs`, issue #313). `JSON.parse` keeps the LAST value, so `{"windows":[...],"windows":[]}` loaded no
181
+ // window while a person reading the file sees two, and a reload swapped a live list down to none with nothing said:
182
+ // quiet hours stopped applying, which costs money in exactly the hours someone believes are paused. Refused, the boot
183
+ // does not start and a reload keeps the last good windows and logs why (`reloadPauseWindows`). The writers have
184
+ // refused such a file since #841 (`heldPauseWindows`), so only a hand edit makes one. BEFORE the version, the triggers
185
+ // loader's order: a doubled `version` is broken, never newer, and a newer release refuses the duplicate as well.
186
+ const duplicate = findDuplicateKey(text);
187
+ if (duplicate) {
188
+ throw configError(`pause-windows file has a duplicate key ${JSON.stringify(duplicate.key)} at ${duplicate.at}: JSON keeps the LAST value, so the file a person reads and the file that applies are not the same: ${path}`);
189
+ }
174
190
  const problem = pauseWindowsVersionRefusal(parsed, { supported });
175
191
  if (problem) {
176
192
  const error = configError(`${problem.message}: ${path}`);
@@ -4,7 +4,10 @@ import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  export function setupCachePath(path) {
7
- return /(?:^|[/\\])(?:_npx|_cacache|npm-cache|\.cache)(?:[/\\]|$)/.test(String(path));
7
+ // pnpm's, yarn's and bun's temporary stores are as disposable as npm's own. Matched by their whole shape, because a folder someone named
8
+ // `dlx` is an ordinary folder.
9
+ const text = String(path);
10
+ return /(?:^|[/\\])(?:_npx|_cacache|npm-cache|\.cache)(?:[/\\]|$)/.test(text) || /(?:^|[/\\])pnpm[/\\]dlx(?:[/\\]|$)/.test(text) || /(?:^|[/\\])xfs-[^/\\]+[/\\]dlx-\d+(?:[/\\]|$)/.test(text) || /(?:^|[/\\])bunx-\d+-[^/\\]+(?:[/\\]|$)/.test(text) || /(?:^|[/\\])\.yarn[/\\]berry[/\\]cache(?:[/\\]|$)/.test(text);
8
11
  }
9
12
  export function trustedSetupFile(entry, { uid = process.geteuid?.(), realpath = realpathSync, stat = statSync } = {}) {
10
13
  try {
package/src/up.mjs CHANGED
@@ -1155,7 +1155,9 @@ export async function runUp(argv = [], deps = {}) {
1155
1155
  for (const [name, note] of summary) {
1156
1156
  out(` ${name.padEnd(21)} ${note}\n`);
1157
1157
  }
1158
- out(`
1158
+ // `up --console` goes on to /setup, whose checklist is the next step, and these bare commands are not on a local install's PATH.
1159
+ const nextOut = deps.skipNextSteps ? () => {} : out;
1160
+ nextOut(`
1159
1161
  Next:
1160
1162
  edit ${envPath}
1161
1163
  set ANTHROPIC_API_KEY (or your provider's key) — already logged into pi? leave it blank
@@ -1167,7 +1169,7 @@ Next:
1167
1169
  // A worker run BY HAND reads only its shell, so on the podman venue it needs the list exported; the service reads
1168
1170
  // `.env` itself. Printed only there, so every other deployment's closing text is unchanged.
1169
1171
  if (venues.podmanUsed) {
1170
- out(" (podman venue: a worker started by hand reads only its shell, so export the same PI_BACKENDS there first, or run it as a service with `pi-dispatch service install`, which reads .env)\n");
1172
+ nextOut(" (podman venue: a worker started by hand reads only its shell, so export the same PI_BACKENDS there first, or run it as a service with `pi-dispatch service install`, which reads .env)\n");
1171
1173
  }
1172
1174
  // Issue #708: the console as a service, offered only where it could start (an operator token exists): without one,
1173
1175
  // `service install --console` refuses, and a "Next:" line that leads to a refusal is not a next step.
@@ -1175,10 +1177,10 @@ Next:
1175
1177
  try {
1176
1178
  consoleReady = consoleOperators(cwd) > 0 && !consoleUnitInstalled(cwd);
1177
1179
  } catch {}
1178
- if (consoleReady) out(" pi-dispatch service install --console\n keep the operator console running as a service (open it with /dispatch console)\n");
1180
+ if (consoleReady) nextOut(" pi-dispatch service install --console\n keep the operator console running as a service (open it with /dispatch console)\n");
1179
1181
  // Issue #707: the openshell venues' own next step, said only where one is blessed.
1180
1182
  if (openshell.used) {
1181
- out(` pi-dispatch service install\n the worker as a ${platform === "darwin" ? "LaunchAgent (the gateway runs in this login session: `brew services start openshell`, the service of OpenShell's release formula, keeps it there)" : `user service that pulls in the gateway's own unit (${OPENSHELL_GATEWAY_UNIT})`}; then \`pi-dispatch doctor\` reads the gateway, its engine and the image back. A worker started by hand reads only its shell, so export the same PI_BACKENDS (and PI_OPENSHELL_ENGINE) there first\n`);
1183
+ nextOut(` pi-dispatch service install\n the worker as a ${platform === "darwin" ? "LaunchAgent (the gateway runs in this login session: `brew services start openshell`, the service of OpenShell's release formula, keeps it there)" : `user service that pulls in the gateway's own unit (${OPENSHELL_GATEWAY_UNIT})`}; then \`pi-dispatch doctor\` reads the gateway, its engine and the image back. A worker started by hand reads only its shell, so export the same PI_BACKENDS (and PI_OPENSHELL_ENGINE) there first\n`);
1182
1184
  }
1183
1185
  // Issue #464: a Valkey this pass refused (another account's, or one no one could name) is a failed `up` whatever doctor
1184
1186
  // says: a worker started now would take that queue's jobs, and an exit 0 read by a script or the wizard says ready.