cmdzero 0.8.0 → 0.8.2

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/README.md CHANGED
@@ -1,8 +1,14 @@
1
+ # CmdZero
2
+
1
3
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/adamcwade/cmdzero/main/assets/hero.png" alt="CmdZero — tweak your UI live in the browser; changes land in your source" width="900">
4
+ <img src="https://raw.githubusercontent.com/adamcwade/cmdzero/main/assets/hero.png" alt="CmdZero — live in-browser UI editing with a style panel, inline copy editing, and a token-savings banner" width="900">
3
5
  </p>
4
6
 
5
- # CmdZero
7
+ <p align="center">
8
+ <strong><a href="https://cmdzero.xyz/demo">▶︎ Try the live demo — no install</a></strong>
9
+ </p>
10
+
11
+ Press `⌘0` and edit a real page with the real overlay, running entirely in your browser. Copy, style, reorder, delete and undo all work; natural-language prompts are scripted (no model is called) and nothing is written to disk.
6
12
 
7
13
  Tweak your UI live in the browser — the changes are written straight to your source files.
8
14
 
@@ -59,9 +65,15 @@ Opt out permanently:
59
65
  export CMDZERO_TELEMETRY=0 # or DO_NOT_TRACK=1 — both respected
60
66
  ```
61
67
 
68
+ Telemetry never blocks or breaks the daemon: it's fire-and-forget with a 3s
69
+ timeout and no retries. It's also silent about its own failures by default — set
70
+ `CMDZERO_TELEMETRY_DEBUG=1` to log delivery errors, or read
71
+ `curl -s localhost:4100/api/health | jq` for the last send and last error.
72
+
62
73
  ## Options
63
74
 
64
75
  - `npx cmdzero --port 4101` (+ `<CmdZeroOverlay origin="http://localhost:4101" />`)
65
76
  - `CMDZERO_FAST_MODEL` / `CMDZERO_SMART_MODEL` — model aliases for the router (default `haiku` / `sonnet`)
66
77
  - `CMDZERO_BASELINE_COST` / `CMDZERO_BASELINE_MS` — baseline for the savings counter
78
+ - `CMDZERO_TELEMETRY=0` / `DO_NOT_TRACK=1` — opt out of telemetry; `CMDZERO_TELEMETRY_DEBUG=1` logs delivery failures
67
79
  - Natural-language tweaks require the [Claude Code CLI](https://claude.com/claude-code) (`claude`) on your PATH; copy/style lanes work without it.
@@ -265,6 +265,47 @@
265
265
  try { localStorage.setItem('cz-panel-collapsed', v ? '1' : '0'); } catch { /* no storage */ }
266
266
  applyPanelLayout();
267
267
  }
268
+ // Everything the overlay draws — outline, popover, move bar, delete button,
269
+ // hover badge — is position:fixed at coordinates measured once from
270
+ // getBoundingClientRect(). The page moves under those coordinates constantly:
271
+ // opening the panel animates <body>'s margin for .26s, that push narrows the
272
+ // viewport so text re-wraps and blocks shift down, an edit changes an
273
+ // element's size, fonts and images land, the page runs its own animations.
274
+ // reposition() on scroll/resize/select catches none of that, which is why a
275
+ // box could sit up to PANEL_W px from its element until something unrelated
276
+ // happened to re-measure.
277
+ //
278
+ // Chasing each cause separately is a losing game, so watch the element
279
+ // itself: compare its rect every frame and redraw only when it actually
280
+ // moved. rAF is the right clock — it's gated on painting exactly like the
281
+ // transitions and reflows we're chasing, so we never measure a layout the
282
+ // user hasn't seen yet. Idle cost is one getBoundingClientRect and a string
283
+ // compare per frame, and the loop only runs while the overlay has something
284
+ // on screen to keep glued.
285
+ let watchRaf = 0;
286
+ let lastRect = '';
287
+ function watchLayout() {
288
+ if (watchRaf) return; // already running
289
+ const step = () => {
290
+ if (!state.selectMode && !state.selected) { watchRaf = 0; lastRect = ''; return; }
291
+ const target = state.selected?.el || state.hoverEl;
292
+ if (target && document.contains(target)) {
293
+ const r = target.getBoundingClientRect();
294
+ const key = `${r.left},${r.top},${r.width},${r.height}`;
295
+ if (key !== lastRect) {
296
+ lastRect = key;
297
+ // the same trio the scroll handler runs — a scroll strands these
298
+ // boxes for exactly the same reason any other layout shift does
299
+ reposition();
300
+ positionMulti();
301
+ setHover(state.hoverEl);
302
+ }
303
+ }
304
+ watchRaf = requestAnimationFrame(step);
305
+ };
306
+ watchRaf = requestAnimationFrame(step);
307
+ }
308
+
268
309
  // Slide the panel in/out and push the page so nothing is covered.
269
310
  function applyPanelLayout() {
270
311
  const open = !!state.selected && !state.panelCollapsed;
@@ -450,6 +491,7 @@
450
491
  renderPopover();
451
492
  renderPanel();
452
493
  reposition();
494
+ watchLayout(); // renderPanel() just started the panel's .26s page push
453
495
  loadMeta();
454
496
  }
455
497
 
@@ -1383,7 +1425,8 @@
1383
1425
  function setMode(on) {
1384
1426
  state.selectMode = on;
1385
1427
  hint.textContent = on ? 'select mode — click · shift-click multi · Tab next · ⌘Z undo · Esc exit' : '⌘0 select mode';
1386
- if (!on) { setHover(null); deselect(); clearMulti(); }
1428
+ if (on) watchLayout(); // keep the boxes glued while anything on the page moves
1429
+ else { setHover(null); deselect(); clearMulti(); }
1387
1430
  }
1388
1431
 
1389
1432
  // Visible, stamped elements in document order — the Tab cycle.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cmdzero",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "Tweak your UI live in the browser and write the changes straight to source. Copy and Tailwind edits cost zero tokens; everything else routes to a right-sized model via headless claude.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,8 @@
8
8
  "cmdzero": "bin/cmdzero.js"
9
9
  },
10
10
  "scripts": {
11
- "start": "node bin/cmdzero.js"
11
+ "start": "node bin/cmdzero.js",
12
+ "test": "node --test test/*.test.js"
12
13
  },
13
14
  "files": [
14
15
  "bin",
package/src/server.js CHANGED
@@ -77,7 +77,17 @@ export function startServer({ root, port = 4100 }) {
77
77
  }
78
78
 
79
79
  if (req.method === 'GET' && url.pathname === '/api/health') {
80
- return json(res, { ok: true, version: VERSION, root, totals, tailwind, telemetry: !telemetry.disabled });
80
+ const { lastSentAt, lastError } = telemetry.status();
81
+ return json(res, {
82
+ ok: true,
83
+ version: VERSION,
84
+ root,
85
+ totals,
86
+ tailwind,
87
+ telemetry: !telemetry.disabled,
88
+ telemetryLastSentAt: lastSentAt,
89
+ telemetryLastError: lastError,
90
+ });
81
91
  }
82
92
 
83
93
  if (req.method === 'GET' && url.pathname === '/api/events') {
@@ -317,8 +327,10 @@ export function startServer({ root, port = 4100 }) {
317
327
  });
318
328
  server.listen(port, () => {
319
329
  console.log(`[cmdzero] daemon on http://localhost:${port} (root: ${root})`);
320
- console.log('[cmdzero] docs & updates → https://cmdzero.dev');
330
+ console.log('[cmdzero] docs & updates → https://cmdzero.xyz');
321
331
  if (telemetry.firstRun && !telemetry.disabled) console.log(DISCLOSURE);
332
+ // After the disclosure, never before it.
333
+ telemetry.start();
322
334
  });
323
335
  return server;
324
336
  }
package/src/telemetry.js CHANGED
@@ -9,9 +9,13 @@ import path from 'node:path';
9
9
  import crypto from 'node:crypto';
10
10
 
11
11
  const ENDPOINT =
12
- process.env.CMDZERO_TELEMETRY_URL || 'https://telemetry.cmdzero.dev/v1/events';
12
+ process.env.CMDZERO_TELEMETRY_URL || 'https://telemetry.cmdzero.xyz/v1/events';
13
13
  const CONFIG_PATH = path.join(os.homedir(), '.cmdzero', 'telemetry.json');
14
14
  const FLUSH_MS = 60_000;
15
+ // Off by default: this runs in other people's dev terminals, and telemetry that
16
+ // complains about itself is worse than telemetry that fails. Delivery status is
17
+ // always readable via /api/health regardless of this flag.
18
+ const DEBUG = process.env.CMDZERO_TELEMETRY_DEBUG === '1';
15
19
 
16
20
  export function telemetryDisabled() {
17
21
  const v = String(process.env.CMDZERO_TELEMETRY ?? '').toLowerCase();
@@ -55,6 +59,19 @@ export function initTelemetry({ version, tailwind }) {
55
59
  const disabled = telemetryDisabled();
56
60
  const counts = { copy: 0, style: 0, delete: 0, nl: 0, move: 0, deploy: 0 };
57
61
  let flushTimer = null;
62
+ let lastSentAt = null;
63
+ let lastError = null;
64
+
65
+ // Must never throw: it runs inside the promise handlers below.
66
+ function note(what, err) {
67
+ if (!err) {
68
+ lastSentAt = new Date().toISOString();
69
+ lastError = null;
70
+ return;
71
+ }
72
+ lastError = { at: new Date().toISOString(), message: `${what}: ${err}` };
73
+ if (DEBUG) console.error(`[cmdzero] telemetry ${what} failed: ${err}`);
74
+ }
58
75
 
59
76
  function send(event, extra = {}) {
60
77
  if (disabled) return;
@@ -73,9 +90,15 @@ export function initTelemetry({ version, tailwind }) {
73
90
  ...extra,
74
91
  }),
75
92
  signal: AbortSignal.timeout(3000),
76
- }).catch(() => {});
77
- } catch {
78
- /* fire and forget */
93
+ })
94
+ .then((res) => note(event, res.ok ? null : `HTTP ${res.status}`))
95
+ // Terminal, and it has to stay that way. If .then() were the tail of this
96
+ // chain a network failure would surface as an unhandled rejection, which
97
+ // Node turns into a dead daemon in someone else's terminal — the
98
+ // .catch(() => {}) this replaces was the only thing preventing that.
99
+ .catch((err) => note(event, err?.message || err));
100
+ } catch (err) {
101
+ note(event, err?.message || err);
79
102
  }
80
103
  }
81
104
 
@@ -92,6 +115,12 @@ export function initTelemetry({ version, tailwind }) {
92
115
  }
93
116
  }
94
117
 
95
- send('boot');
96
- return { firstRun, disabled, record };
118
+ // Not fired here. server.js calls start() once the first-run disclosure has
119
+ // actually printed, so a first-time user's first event never precedes the
120
+ // notice that tells them it's coming.
121
+ function start() {
122
+ send('boot');
123
+ }
124
+
125
+ return { firstRun, disabled, record, start, status: () => ({ lastSentAt, lastError }) };
97
126
  }