cmdzero 0.8.1 → 0.8.3
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 +6 -0
- package/package.json +3 -2
- package/src/resolver.js +12 -7
- package/src/server.js +14 -2
- package/src/telemetry.js +35 -6
package/README.md
CHANGED
|
@@ -65,9 +65,15 @@ Opt out permanently:
|
|
|
65
65
|
export CMDZERO_TELEMETRY=0 # or DO_NOT_TRACK=1 — both respected
|
|
66
66
|
```
|
|
67
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
|
+
|
|
68
73
|
## Options
|
|
69
74
|
|
|
70
75
|
- `npx cmdzero --port 4101` (+ `<CmdZeroOverlay origin="http://localhost:4101" />`)
|
|
71
76
|
- `CMDZERO_FAST_MODEL` / `CMDZERO_SMART_MODEL` — model aliases for the router (default `haiku` / `sonnet`)
|
|
72
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
|
|
73
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cmdzero",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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/resolver.js
CHANGED
|
@@ -28,13 +28,18 @@ export function parseLoc(loc) {
|
|
|
28
28
|
export function loadTarget(root, loc) {
|
|
29
29
|
const parsed = parseLoc(loc);
|
|
30
30
|
const { line, col } = parsed;
|
|
31
|
-
// Stamps come from
|
|
32
|
-
// 0-based column) and the @cmdzero/react dev runtime (
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
// Stamps come from three sources: the babel plugin (root-relative path,
|
|
32
|
+
// 0-based column) and the @cmdzero/react dev runtime (1-based column), which
|
|
33
|
+
// reports whatever the bundler handed jsxDEV — an absolute path under
|
|
34
|
+
// webpack, but "[project]/..." under Turbopack. Normalize the path and match
|
|
35
|
+
// either column convention.
|
|
36
|
+
const raw = parsed.file.replace(/^\[project\]\//, '');
|
|
37
|
+
// Resolve before the escape check: the guard has to see a normalized path,
|
|
38
|
+
// or a "foo/../../.." stamp walks straight past a leading-".." test.
|
|
39
|
+
const file = path.relative(root, path.resolve(root, raw));
|
|
40
|
+
if (file.startsWith('..') || path.isAbsolute(file)) {
|
|
41
|
+
throw new Error('file outside root');
|
|
42
|
+
}
|
|
38
43
|
const abs = path.resolve(root, file);
|
|
39
44
|
const content = fs.readFileSync(abs, 'utf8');
|
|
40
45
|
const ast = parseSource(content, file);
|
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
|
}
|