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 +14 -2
- package/overlay/overlay.js +44 -1
- package/package.json +3 -2
- package/src/server.js +14 -2
- package/src/telemetry.js +35 -6
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 —
|
|
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
|
-
|
|
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.
|
package/overlay/overlay.js
CHANGED
|
@@ -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 (
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
})
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
96
|
-
|
|
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
|
}
|