ccdeck 3.29.1 → 3.29.3

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.
@@ -62,7 +62,7 @@
62
62
  stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
63
63
  })();
64
64
  </script>
65
- <script type="module" crossorigin src="/assets/index-7mOqoJ3Y.js"></script>
65
+ <script type="module" crossorigin src="/assets/index-C7djiYSc.js"></script>
66
66
  <link rel="stylesheet" crossorigin href="/assets/index-FeNTTJ0x.css">
67
67
  </head>
68
68
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccdeck",
3
- "version": "3.29.1",
3
+ "version": "3.29.3",
4
4
  "description": "Live dashboard for Claude Code and Codex — which agent is waiting on you, tool calls, token spend and every Claude Code subagent on one calm canvas. Run it with npx ccdeck.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -52,7 +52,8 @@
52
52
  "3.27.6",
53
53
  "3.27.7",
54
54
  "3.28.0",
55
- "3.28.1"
55
+ "3.28.1",
56
+ "3.29.3"
56
57
  ],
57
58
  "3.29.1": [
58
59
  {
@@ -285,6 +285,24 @@ function unreadablePrefs(path, why) {
285
285
  return err;
286
286
  }
287
287
 
288
+ /** The errors a filesystem raises for a place this user may not write — most
289
+ * often a settings folder a `sudo` run left owned by root (#1335). */
290
+ const NOT_WRITABLE = new Set(["EACCES", "EPERM", "EROFS"]);
291
+
292
+ /**
293
+ * Why a settings write failed, as a reason the panel can name — or null when
294
+ * the failure is not one of the two a person can fix from outside the deck.
295
+ *
296
+ * A code, never the message: the message carries the absolute path, and a
297
+ * route's body is readable by a DNS-rebound page (see sendInternalError). The
298
+ * deck's log keeps the path for the person who has to go and look.
299
+ */
300
+ export function prefsWriteRefusal(err) {
301
+ if (err?.code === "PREFS_UNREADABLE") return "prefs_unreadable";
302
+ if (NOT_WRITABLE.has(err?.code)) return "prefs_not_writable";
303
+ return null;
304
+ }
305
+
288
306
  /**
289
307
  * Read prefs.json, and say WHICH of four things happened — because three of
290
308
  * them hand back the same object and only one of them means it.
@@ -37,7 +37,7 @@ import { createOutputWatch } from "./output-watch.mjs";
37
37
  import { RECAP_MARK, foldRecapLine } from "./session-recap.mjs";
38
38
  import { AWAY_BOOT_GRACE_MS, AWAY_RECHECK_MS, AWAY_TICK_MS, awayGate, awayUpdateStep } from "./auto-update.mjs";
39
39
  import { createPresence } from "./presence.mjs";
40
- import { DEFAULTS as PREF_DEFAULTS, cleanAlias, isAliasKey, lanEnabled, notificationsOn, notificationsVetoed, publicPrefs, readPrefs, updatePrefs, withAlias, withManualEntry, withShared, writePrefs } from "./deck-prefs.mjs";
40
+ import { DEFAULTS as PREF_DEFAULTS, cleanAlias, isAliasKey, lanEnabled, notificationsOn, notificationsVetoed, prefsWriteRefusal, publicPrefs, readPrefs, updatePrefs, withAlias, withManualEntry, withShared, writePrefs } from "./deck-prefs.mjs";
41
41
  import { createEngine, defaultName } from "./lan-engine.mjs";
42
42
  import { createTailnet, IDLE_MS as TAILNET_IDLE_MS } from "./tailscale.mjs";
43
43
  import { portHolder } from "./port-holder.mjs";
@@ -3821,7 +3821,17 @@ async function handlePrefsWrite(req, res) {
3821
3821
  let body = null;
3822
3822
  try { body = JSON.parse(raw ?? ""); } catch { /* handled below */ }
3823
3823
  if (!body || typeof body !== "object") return send(res, 400, { ok: false, reason: "bad_request" });
3824
- _prefs = await writePrefs(body);
3824
+ try {
3825
+ _prefs = await writePrefs(body);
3826
+ } catch (err) {
3827
+ // A settings file or folder this user cannot reach is the machine's to fix,
3828
+ // not the deck's, and `guard`'s bare 500 left the panel with nothing to say
3829
+ // about it (#1335). Anything else is still a bug and still goes to `guard`.
3830
+ const reason = prefsWriteRefusal(err);
3831
+ if (!reason) throw err;
3832
+ console.error(`${PRODUCT}: settings were not saved:`, err?.message ?? err);
3833
+ return send(res, 500, { ok: false, reason });
3834
+ }
3825
3835
  // The engine reads its settings from here rather than holding its own copy,
3826
3836
  // so turning the switch off in the panel really does stop the sockets rather
3827
3837
  // than only changing what the panel says.