ccdeck 3.26.0 → 3.26.1

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.
@@ -44,7 +44,7 @@
44
44
  stored === "light" || (stored !== "dark" && light) ? "light" : "dark");
45
45
  })();
46
46
  </script>
47
- <script type="module" crossorigin src="/assets/index-DfvLgdJz.js"></script>
47
+ <script type="module" crossorigin src="/assets/index-CO8B_qBe.js"></script>
48
48
  <link rel="stylesheet" crossorigin href="/assets/index-DlaHgzye.css">
49
49
  </head>
50
50
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ccdeck",
3
- "version": "3.26.0",
3
+ "version": "3.26.1",
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": {
@@ -44,7 +44,8 @@
44
44
  "3.23.2",
45
45
  "3.23.3",
46
46
  "3.23.4",
47
- "3.26.0"
47
+ "3.26.0",
48
+ "3.26.1"
48
49
  ],
49
50
  "3.25.0": [
50
51
  {
@@ -11,6 +11,7 @@
11
11
  // the current call always serves from the already-installed copy. If the
12
12
  // managed install is missing/broken we fall back to the old npx path so the
13
13
  // feature still works on a fresh machine.
14
+ import { createRequire } from "node:module";
14
15
  import { spawn } from "node:child_process";
15
16
  import { existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
16
17
  import path from "node:path";
@@ -738,12 +739,43 @@ function discardDamagedInstall(runner, err) {
738
739
  export const userSpec = (file, args = [], platform = process.platform) =>
739
740
  spawnSpec(file, args, platform);
740
741
 
742
+ /**
743
+ * The platform binary ccusage's own cli.js would run, resolved the way cli.js
744
+ * resolves it (`@ccusage/ccusage-<platform>-<arch>/bin/ccusage[.exe]`, from
745
+ * the package's own directory), or null when there is none.
746
+ *
747
+ * WINDOWS ONLY, and for one reason: cli.js starts that binary WITHOUT hiding
748
+ * its console. The deck runs with no console of its own (detached, or inside
749
+ * the desktop app), so the binary is given a new one — and with Windows
750
+ * Terminal as the default terminal that is a window flashing up on every
751
+ * usage read, measured on a Windows 10 box as a CASCADIA_HOSTING_WINDOW_CLASS
752
+ * hosting ccusage. Running the binary directly, with windowsHide, is what
753
+ * cli.js does minus the window. Elsewhere nothing flashes, and the wrapper
754
+ * stays in the path it has always been in.
755
+ *
756
+ * Exported for tests; `resolve` is injectable.
757
+ */
758
+ export function nativeCcusage(entry, platform = process.platform, arch = process.arch, resolve = null) {
759
+ if (platform !== "win32" || typeof entry !== "string" || !entry) return null;
760
+ const pkg = arch === "arm64" ? "@ccusage/ccusage-win32-arm64" : arch === "x64" ? "@ccusage/ccusage-win32-x64" : null;
761
+ if (!pkg) return null;
762
+ try {
763
+ const req = resolve ?? createRequire(entry).resolve;
764
+ return req(`${pkg}/bin/ccusage.exe`);
765
+ } catch {
766
+ return null;
767
+ }
768
+ }
769
+
741
770
  // One attempt with one runner. No branch gets a shell. The managed install is
742
771
  // `node <entry> …`, which never needed one; the user's own copy and the npx
743
772
  // fallback are routed through spawnSpec instead — see npxSpec and userSpec, and
744
773
  // note that `args` here ends in whatever /api/ccusage was asked for.
745
774
  function runOnce(runner, args) {
746
- const { file, args: full, opts } = runner.kind === "node"
775
+ const native = runner.kind === "node" ? nativeCcusage(runner.entry) : null;
776
+ const { file, args: full, opts } = native
777
+ ? { file: native, args, opts: {} }
778
+ : runner.kind === "node"
747
779
  ? { file: process.execPath, args: [runner.entry, ...args], opts: {} }
748
780
  : runner.kind === "path"
749
781
  ? userSpec(runner.file, args)
@@ -171,15 +171,31 @@ export function ruleCovers(rules, profileName) {
171
171
  * wrong one for the router at home, and this module cannot tell which one
172
172
  * somebody is sitting in. The panel says so beside it.
173
173
  */
174
- export function fixSteps({ category, alias, exePath }) {
174
+ export function fixSteps({ category, alias, exePath, rules = [] }) {
175
175
  const steps = [];
176
- if (profileFor(category) === "Public" && alias) {
176
+ const name = profileFor(category);
177
+ if (name === "Public" && alias) {
177
178
  steps.push(`Set-NetConnectionProfile -InterfaceAlias "${alias}" -NetworkCategory Private`);
178
179
  }
179
- steps.push(
180
- "New-NetFirewallRule -DisplayName \"ccdeck (local network)\" -Direction Inbound"
181
- + ` -Program "${exePath}" -Action Allow -Profile Private`,
182
- );
180
+ // THE PROFILE OF THE NETWORK THE DECK IS ON, NOT ALWAYS PRIVATE. A company
181
+ // LAN joined to a domain is `DomainAuthenticated`, which the firewall answers
182
+ // with its Domain profile, and a rule scoped to Private never applies there.
183
+ // Measured on a Windows box whose Ethernet is DomainAuthenticated: the rule
184
+ // was pasted, the panel went on saying there was none — correctly — and a
185
+ // second paste made a second rule that did nothing either. Domain and
186
+ // Private together, so a laptop that goes home keeps working there too.
187
+ // Public is never added: a Public network gets the category line above.
188
+ const profile = name === "Domain" ? "Domain,Private" : "Private";
189
+ // AND A RULE THAT IS ALREADY THERE IS WIDENED, NOT JOINED BY ANOTHER. The
190
+ // probe only returns rules for this program, so an inbound allow rule in
191
+ // `rules` is one of ours on the wrong profile, and a new rule beside it is
192
+ // the duplicate this was reported with.
193
+ const ours = (Array.isArray(rules) ? rules : [])
194
+ .some(r => r?.enabled && String(r.direction).toLowerCase() === "inbound" && String(r.action).toLowerCase() === "allow");
195
+ steps.push(ours
196
+ ? `Get-NetFirewallApplicationFilter -Program "${exePath}" | Get-NetFirewallRule | Set-NetFirewallRule -Profile ${profile}`
197
+ : "New-NetFirewallRule -DisplayName \"ccdeck (local network)\" -Direction Inbound"
198
+ + ` -Program "${exePath}" -Action Allow -Profile ${profile}`);
183
199
  return steps;
184
200
  }
185
201
 
@@ -477,8 +493,10 @@ export function reachability({ platform, probe, aliases = [], exePath = "", inbo
477
493
  // workaround below obvious rather than magic.
478
494
  text: name === "Public"
479
495
  ? "This network is set to Public, and Windows drops what other decks send. They can still hear this deck — that is why one of them may already show it."
480
- : "Windows has no inbound rule for this deck, so it drops what other decks send. They can still hear it — that is why one of them may already show this machine.",
481
- steps: fixSteps({ category, alias: net?.alias ?? "", exePath }),
496
+ : name === "Domain" && probe.rules.length
497
+ ? "Windows lets this deck in on Private networks only, and this one is a company (Domain) network, so it drops what other decks send. They can still hear it — that is why one of them may already show this machine."
498
+ : "Windows has no inbound rule for this deck, so it drops what other decks send. They can still hear it — that is why one of them may already show this machine.",
499
+ steps: fixSteps({ category, alias: net?.alias ?? "", exePath, rules: probe.rules }),
482
500
  };
483
501
  }
484
502