ccdeck 3.29.3 → 3.29.4
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-RXK8WXaU.js → BrowserWatchModal-BH5mTAKD.js} +1 -1
- package/dist/web/assets/{UsageHistoryModal-Dk5zag6g.js → UsageHistoryModal-sQsuFhNj.js} +1 -1
- package/dist/web/assets/{index-C7djiYSc.js → index-GYpcFZ76.js} +2 -2
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/release-notes.json +2 -1
- package/src/server/deck-prefs.mjs +80 -5
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-GYpcFZ76.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.4",
|
|
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
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
// write defaults over it, destroying the LAN key every paired machine had
|
|
34
34
|
// pinned. A file that cannot be parsed is now moved aside and said out loud;
|
|
35
35
|
// only a genuinely ABSENT file starts clean. See loadPrefs.
|
|
36
|
-
import { chmod, mkdir, readFile, unlink } from "node:fs/promises";
|
|
36
|
+
import { chmod, mkdir, readFile, stat, unlink } from "node:fs/promises";
|
|
37
37
|
// The rename, with the Windows retry ladder installer.mjs wrote for exactly
|
|
38
38
|
// this call. See the note over the write below (#786). `stripBom` and
|
|
39
39
|
// `createTemp` come from the same module for the reason its export block gives:
|
|
@@ -303,8 +303,71 @@ export function prefsWriteRefusal(err) {
|
|
|
303
303
|
return null;
|
|
304
304
|
}
|
|
305
305
|
|
|
306
|
+
/** The read errors that can mean "not yours" rather than "broken". Only these
|
|
307
|
+
* send a read to setAsideForeign; EISDIR, EIO and EMFILE never do. */
|
|
308
|
+
const DENIED = new Set(["EACCES", "EPERM"]);
|
|
309
|
+
|
|
310
|
+
/** Where a prefs.json another user owns is put. Beside the file, like
|
|
311
|
+
* quarantinePath, and stamped in milliseconds for the same Windows reason —
|
|
312
|
+
* but a name of its own, because nothing is wrong with these bytes. */
|
|
313
|
+
export const foreignPath = (home = deckDataDir(), at = Date.now()) =>
|
|
314
|
+
`${prefsPath(home)}.foreign-${at}`;
|
|
315
|
+
|
|
306
316
|
/**
|
|
307
|
-
*
|
|
317
|
+
* Move a prefs.json that ANOTHER USER OWNS out of the way, or say why not.
|
|
318
|
+
*
|
|
319
|
+
* #1335. A `sudo ccdeck` that kept the user's HOME writes a root-owned 0600
|
|
320
|
+
* prefs.json into the user's own folder. From then on every
|
|
321
|
+
* deck that user starts is refused the read, refuses every settings write on
|
|
322
|
+
* top of it — rightly — and so can never keep anything: a new LAN identity on
|
|
323
|
+
* every start, a share tick that says "Could not share that account", and no
|
|
324
|
+
* way out from inside the deck.
|
|
325
|
+
*
|
|
326
|
+
* WHY MOVING IT IS SAFE HERE WHEN IT IS NOT FOR "unreadable". The refusal
|
|
327
|
+
* exists so the deck never loses a key it could still have read. A file owned
|
|
328
|
+
* by somebody else is not one this user's deck will ever read — waiting does
|
|
329
|
+
* not help, and the deck is already running on defaults. A rename reads and
|
|
330
|
+
* changes nothing: owner and mode travel with the file, so the key in it stays
|
|
331
|
+
* private and whole, and `sudo chown` plus a move puts it back.
|
|
332
|
+
*
|
|
333
|
+
* Only when the owner is KNOWN to be somebody else. A file this user owns and
|
|
334
|
+
* still cannot read (a mode bit, a lock, a sandbox) is left exactly where it
|
|
335
|
+
* is, and so is anything on Windows, where there is no uid to compare and
|
|
336
|
+
* EPERM is usually another program holding the file.
|
|
337
|
+
*
|
|
338
|
+
* Answers `{ moved, owner }`, `{ gone }` when the file vanished under it, or
|
|
339
|
+
* `{ why }` — a sentence for the log, possibly empty — when it stayed.
|
|
340
|
+
*/
|
|
341
|
+
async function setAsideForeign(path, home, deps) {
|
|
342
|
+
const uid = deps.getuid ? deps.getuid() : process.getuid?.();
|
|
343
|
+
if (uid === undefined) return { why: "" };
|
|
344
|
+
const look = deps.stat ?? stat;
|
|
345
|
+
let st;
|
|
346
|
+
try {
|
|
347
|
+
st = await look(path);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
if (err?.code === "ENOENT") return { gone: true };
|
|
350
|
+
// The folder itself is out of reach, so nothing in it can be looked at, let
|
|
351
|
+
// alone moved. Name the folder and its owner, which is what a person needs.
|
|
352
|
+
const folder = prefsDir(home);
|
|
353
|
+
const owner = await look(folder).then(s => s.uid, () => undefined);
|
|
354
|
+
return owner !== undefined && owner !== uid
|
|
355
|
+
? { 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.` }
|
|
356
|
+
: { why: "" };
|
|
357
|
+
}
|
|
358
|
+
if (st.uid === uid) return { why: "" };
|
|
359
|
+
const to = foreignPath(home);
|
|
360
|
+
try {
|
|
361
|
+
await (deps.rename ?? renameWithRetry)(path, to);
|
|
362
|
+
} catch (err) {
|
|
363
|
+
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}.` };
|
|
365
|
+
}
|
|
366
|
+
return { moved: to, owner: st.uid };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Read prefs.json, and say WHICH of five things happened — because four of
|
|
308
371
|
* them hand back the same object and only one of them means it.
|
|
309
372
|
*
|
|
310
373
|
* WHY THE SOURCE IS PART OF THE ANSWER. The old read collapsed "no file yet"
|
|
@@ -322,9 +385,13 @@ export function prefsWriteRefusal(err) {
|
|
|
322
385
|
* defaults, silently, which is what a first start is.
|
|
323
386
|
* "corrupt" bytes that are not JSON. Moved aside to `quarantined` BEFORE
|
|
324
387
|
* this returns, so nothing can merge over them.
|
|
388
|
+
* "foreign" a file ANOTHER USER owns and this one may not read — what a
|
|
389
|
+
* `sudo` run leaves (#1335). Moved aside to `quarantined`, still
|
|
390
|
+
* theirs and unread, BEFORE this returns. See setAsideForeign.
|
|
325
391
|
* "unreadable" the read itself failed for some reason other than absence — a
|
|
326
|
-
* permission, a directory in the way
|
|
327
|
-
*
|
|
392
|
+
* permission on a file this user owns, a directory in the way,
|
|
393
|
+
* a folder another user owns. The file is still there and still
|
|
394
|
+
* unread, which is exactly when a write must not land.
|
|
328
395
|
*
|
|
329
396
|
* A BYTE-ORDER MARK IS NOT DAMAGE. Notepad and `Set-Content` write one, and
|
|
330
397
|
* `JSON.parse` throws on it — so stripping it here is what keeps a perfectly
|
|
@@ -342,7 +409,15 @@ export async function loadPrefs(home = deckDataDir(), deps = {}) {
|
|
|
342
409
|
raw = await read(path, "utf8");
|
|
343
410
|
} catch (err) {
|
|
344
411
|
if (err?.code === "ENOENT") return { prefs: defaults(), source: "missing", quarantined: "" };
|
|
345
|
-
|
|
412
|
+
const aside = DENIED.has(err?.code) ? await setAsideForeign(path, home, deps) : {};
|
|
413
|
+
// Somebody else moved it between the read and the look — a second deck on
|
|
414
|
+
// this machine doing this same thing. What is at `path` now is nothing.
|
|
415
|
+
if (aside.gone) return { prefs: defaults(), source: "missing", quarantined: "" };
|
|
416
|
+
if (aside.moved) {
|
|
417
|
+
warn(`${PRODUCT}: ${path} belongs to another user (uid ${aside.owner}) — 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
|
+
return { prefs: defaults(), source: "foreign", quarantined: aside.moved };
|
|
419
|
+
}
|
|
420
|
+
warn(`${PRODUCT}: could not read ${path}: ${err?.message ?? err}.${aside.why ?? ""} Leaving it alone — settings will not be saved until it can be read.`);
|
|
346
421
|
return { prefs: defaults(), source: "unreadable", quarantined: "" };
|
|
347
422
|
}
|
|
348
423
|
|