ccdeck 3.29.4 → 3.29.6
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/dist/web/assets/{BrowserWatchModal-BH5mTAKD.js → BrowserWatchModal-UR0ISjic.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-sQsuFhNj.js → UsageHistoryModal-B8dnL7pR.js} +1 -1
- package/dist/web/assets/{index-GYpcFZ76.js → index-6Mi0L2Ks.js} +7 -7
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/release-notes.json +2 -1
- package/src/server/deck-prefs.mjs +60 -16
- package/src/server/index.mjs +5 -2
package/dist/web/index.html
CHANGED
|
@@ -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-
|
|
65
|
+
<script type="module" crossorigin src="/assets/index-6Mi0L2Ks.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.
|
|
3
|
+
"version": "3.29.6",
|
|
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": {
|
package/release-notes.json
CHANGED
|
@@ -273,7 +273,7 @@ export const quarantinePath = (home = deckDataDir(), at = Date.now()) =>
|
|
|
273
273
|
* not the user's. Shaped like installer.mjs's SETTINGS_UNREADABLE, which is
|
|
274
274
|
* the same policy on the other file this deck rewrites: a file we cannot
|
|
275
275
|
* reproduce is never treated as an empty one. */
|
|
276
|
-
function unreadablePrefs(path, why) {
|
|
276
|
+
function unreadablePrefs(path, why, blocked) {
|
|
277
277
|
const err = new Error(
|
|
278
278
|
`${path} could not be read (${why}). Refusing to overwrite it — this deck's ` +
|
|
279
279
|
`LAN key and its pairings are in there and cannot be re-derived. Fix the ` +
|
|
@@ -282,6 +282,7 @@ function unreadablePrefs(path, why) {
|
|
|
282
282
|
err.code = "PREFS_UNREADABLE";
|
|
283
283
|
err.prefsPath = path;
|
|
284
284
|
err.why = why;
|
|
285
|
+
err.blocked = blocked;
|
|
285
286
|
return err;
|
|
286
287
|
}
|
|
287
288
|
|
|
@@ -303,6 +304,40 @@ export function prefsWriteRefusal(err) {
|
|
|
303
304
|
return null;
|
|
304
305
|
}
|
|
305
306
|
|
|
307
|
+
/** What blocked a prefs.json that is not JSON and could not be moved aside —
|
|
308
|
+
* no errno for it, so a name of the deck's own in the same shape. */
|
|
309
|
+
const NOT_JSON = Object.freeze({ code: "BADJSON", owner: "unknown", on: "file" });
|
|
310
|
+
|
|
311
|
+
const OWNERS = new Set(["you", "other", "unknown"]);
|
|
312
|
+
/** An errno as Node spells one, or the deck's BADJSON. Anything else — which
|
|
313
|
+
* nothing here produces — is dropped rather than echoed to the page. */
|
|
314
|
+
const ERRNO = /^E?[A-Z][A-Z0-9]{1,15}$/;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The same refusal, told precisely enough to act on from a screenshot (#1335).
|
|
318
|
+
*
|
|
319
|
+
* 3.29.3's panel could only guess — "the file may belong to another user" — and
|
|
320
|
+
* the one Mac it was written for turned out not to be the case 3.29.4 repairs,
|
|
321
|
+
* with nothing on the screen to say which case it was. So the page now gets the
|
|
322
|
+
* errno, who owns what blocked the read (`you`, `other`, `unknown`), and whether
|
|
323
|
+
* that is the `file` or its `folder`.
|
|
324
|
+
*
|
|
325
|
+
* Every field is picked from a closed set. Still no path and no uid: the route's
|
|
326
|
+
* body is readable by a DNS-rebound page (see prefsWriteRefusal), and a path
|
|
327
|
+
* names the user. Null when there is nothing precise to add.
|
|
328
|
+
*/
|
|
329
|
+
export function prefsRefusalDetail(err) {
|
|
330
|
+
const blocked = err?.code === "PREFS_UNREADABLE" ? err.blocked
|
|
331
|
+
: NOT_WRITABLE.has(err?.code) ? { code: err.code, owner: "unknown", on: "folder" }
|
|
332
|
+
: null;
|
|
333
|
+
if (!blocked || typeof blocked !== "object") return null;
|
|
334
|
+
return {
|
|
335
|
+
code: ERRNO.test(blocked.code ?? "") ? blocked.code : "",
|
|
336
|
+
owner: OWNERS.has(blocked.owner) ? blocked.owner : "unknown",
|
|
337
|
+
on: blocked.on === "folder" ? "folder" : "file",
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
|
|
306
341
|
/** The read errors that can mean "not yours" rather than "broken". Only these
|
|
307
342
|
* send a read to setAsideForeign; EISDIR, EIO and EMFILE never do. */
|
|
308
343
|
const DENIED = new Set(["EACCES", "EPERM"]);
|
|
@@ -335,12 +370,15 @@ export const foreignPath = (home = deckDataDir(), at = Date.now()) =>
|
|
|
335
370
|
* is, and so is anything on Windows, where there is no uid to compare and
|
|
336
371
|
* EPERM is usually another program holding the file.
|
|
337
372
|
*
|
|
338
|
-
* Answers `{ moved,
|
|
339
|
-
* `{ why }
|
|
373
|
+
* Answers `{ moved, uid }`, `{ gone }` when the file vanished under it, or,
|
|
374
|
+
* when it stayed, `{ why, owner, on }`: a sentence for the log, possibly empty,
|
|
375
|
+
* and who owns what blocked the read — "you", "other" or "unknown" — and
|
|
376
|
+
* whether that is the "file" or its "folder". The last two are what the panel
|
|
377
|
+
* is told (see prefsRefusalDetail); the sentence, which has the path, is not.
|
|
340
378
|
*/
|
|
341
379
|
async function setAsideForeign(path, home, deps) {
|
|
342
380
|
const uid = deps.getuid ? deps.getuid() : process.getuid?.();
|
|
343
|
-
if (uid === undefined) return { why: "" };
|
|
381
|
+
if (uid === undefined) return { why: "", owner: "unknown", on: "file" };
|
|
344
382
|
const look = deps.stat ?? stat;
|
|
345
383
|
let st;
|
|
346
384
|
try {
|
|
@@ -351,19 +389,23 @@ async function setAsideForeign(path, home, deps) {
|
|
|
351
389
|
// alone moved. Name the folder and its owner, which is what a person needs.
|
|
352
390
|
const folder = prefsDir(home);
|
|
353
391
|
const owner = await look(folder).then(s => s.uid, () => undefined);
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
392
|
+
if (owner === undefined) return { why: "", owner: "unknown", on: "folder" };
|
|
393
|
+
if (owner === uid) return { why: "", owner: "you", on: "folder" };
|
|
394
|
+
return {
|
|
395
|
+
why: ` The folder ${folder} belongs to another user (uid ${owner}), most likely from a run with sudo; sudo chown -R "$(id -un)" "${folder}" gives it back.`,
|
|
396
|
+
owner: "other",
|
|
397
|
+
on: "folder",
|
|
398
|
+
};
|
|
357
399
|
}
|
|
358
|
-
if (st.uid === uid) return { why: "" };
|
|
400
|
+
if (st.uid === uid) return { why: "", owner: "you", on: "file" };
|
|
359
401
|
const to = foreignPath(home);
|
|
360
402
|
try {
|
|
361
403
|
await (deps.rename ?? renameWithRetry)(path, to);
|
|
362
404
|
} catch (err) {
|
|
363
405
|
if (err?.code === "ENOENT") return { gone: true };
|
|
364
|
-
return { why: ` It belongs to another user (uid ${st.uid}) and could not be moved aside either: ${err?.message ?? err}
|
|
406
|
+
return { why: ` It belongs to another user (uid ${st.uid}) and could not be moved aside either: ${err?.message ?? err}.`, owner: "other", on: "file" };
|
|
365
407
|
}
|
|
366
|
-
return { moved: to,
|
|
408
|
+
return { moved: to, uid: st.uid };
|
|
367
409
|
}
|
|
368
410
|
|
|
369
411
|
/**
|
|
@@ -391,7 +433,8 @@ async function setAsideForeign(path, home, deps) {
|
|
|
391
433
|
* "unreadable" the read itself failed for some reason other than absence — a
|
|
392
434
|
* permission on a file this user owns, a directory in the way,
|
|
393
435
|
* a folder another user owns. The file is still there and still
|
|
394
|
-
* unread, which is exactly when a write must not land.
|
|
436
|
+
* unread, which is exactly when a write must not land. `blocked`
|
|
437
|
+
* says what stopped it; see prefsRefusalDetail.
|
|
395
438
|
*
|
|
396
439
|
* A BYTE-ORDER MARK IS NOT DAMAGE. Notepad and `Set-Content` write one, and
|
|
397
440
|
* `JSON.parse` throws on it — so stripping it here is what keeps a perfectly
|
|
@@ -414,11 +457,12 @@ export async function loadPrefs(home = deckDataDir(), deps = {}) {
|
|
|
414
457
|
// this machine doing this same thing. What is at `path` now is nothing.
|
|
415
458
|
if (aside.gone) return { prefs: defaults(), source: "missing", quarantined: "" };
|
|
416
459
|
if (aside.moved) {
|
|
417
|
-
warn(`${PRODUCT}: ${path} belongs to another user (uid ${aside.
|
|
460
|
+
warn(`${PRODUCT}: ${path} belongs to another user (uid ${aside.uid}) — most likely left by a run with sudo — so this deck could not read it. It has been kept as ${aside.moved}, unread and still theirs, and this deck is starting with fresh settings: paired decks will need to accept it again. To have the old key back instead, stop the deck and run: sudo chown "$(id -un)" "${aside.moved}" && mv "${aside.moved}" "${path}"`);
|
|
418
461
|
return { prefs: defaults(), source: "foreign", quarantined: aside.moved };
|
|
419
462
|
}
|
|
420
463
|
warn(`${PRODUCT}: could not read ${path}: ${err?.message ?? err}.${aside.why ?? ""} Leaving it alone — settings will not be saved until it can be read.`);
|
|
421
|
-
|
|
464
|
+
const blocked = { code: err?.code ?? "", owner: aside.owner ?? "unknown", on: aside.on ?? "file" };
|
|
465
|
+
return { prefs: defaults(), source: "unreadable", quarantined: "", blocked };
|
|
422
466
|
}
|
|
423
467
|
|
|
424
468
|
try {
|
|
@@ -582,10 +626,10 @@ async function save(mutate, home, deps) {
|
|
|
582
626
|
// stick. POSIX rename(2) has no such rule, which is why this shipped green.
|
|
583
627
|
const mv = deps.rename ?? renameWithRetry;
|
|
584
628
|
const target = prefsPath(home);
|
|
585
|
-
const { prefs: prev, source, quarantined } = await loadPrefs(home, deps);
|
|
586
|
-
if (source === "unreadable") throw unreadablePrefs(target, "the read failed");
|
|
629
|
+
const { prefs: prev, source, quarantined, blocked } = await loadPrefs(home, deps);
|
|
630
|
+
if (source === "unreadable") throw unreadablePrefs(target, "the read failed", blocked);
|
|
587
631
|
if (source === "corrupt" && !quarantined) {
|
|
588
|
-
throw unreadablePrefs(target, "it is not JSON and could not be moved aside");
|
|
632
|
+
throw unreadablePrefs(target, "it is not JSON and could not be moved aside", NOT_JSON);
|
|
589
633
|
}
|
|
590
634
|
// INSIDE THE JOB, after the read above and after the two guards that
|
|
591
635
|
// decide whether this file may be written at all — see `updatePrefs`.
|
package/src/server/index.mjs
CHANGED
|
@@ -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, prefsWriteRefusal, publicPrefs, readPrefs, updatePrefs, withAlias, withManualEntry, withShared, writePrefs } from "./deck-prefs.mjs";
|
|
40
|
+
import { DEFAULTS as PREF_DEFAULTS, cleanAlias, isAliasKey, lanEnabled, notificationsOn, notificationsVetoed, prefsRefusalDetail, 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";
|
|
@@ -3830,7 +3830,10 @@ async function handlePrefsWrite(req, res) {
|
|
|
3830
3830
|
const reason = prefsWriteRefusal(err);
|
|
3831
3831
|
if (!reason) throw err;
|
|
3832
3832
|
console.error(`${PRODUCT}: settings were not saved:`, err?.message ?? err);
|
|
3833
|
-
|
|
3833
|
+
// What blocked it, from closed sets and without the path, so a screenshot
|
|
3834
|
+
// of the panel is enough to tell the cases apart (#1335).
|
|
3835
|
+
const detail = prefsRefusalDetail(err);
|
|
3836
|
+
return send(res, 500, detail ? { ok: false, reason, detail } : { ok: false, reason });
|
|
3834
3837
|
}
|
|
3835
3838
|
// The engine reads its settings from here rather than holding its own copy,
|
|
3836
3839
|
// so turning the switch off in the panel really does stop the sockets rather
|