cmdzero 0.7.1 → 0.8.0
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/overlay/overlay.js +39 -7
- package/package.json +1 -1
- package/src/router.js +41 -0
- package/src/server.js +28 -2
- package/src/telemetry.js +1 -1
package/overlay/overlay.js
CHANGED
|
@@ -81,6 +81,9 @@
|
|
|
81
81
|
.cz-banner .cz-stat b{color:#f1f5f9;font-weight:600;margin-left:5px}
|
|
82
82
|
.cz-banner .cz-stat.cz-accent b{color:#6ee7b7}
|
|
83
83
|
.cz-banner a{color:#6ee7b7;text-decoration:none;flex:none}
|
|
84
|
+
.cz-banner .cz-deploy{flex:none;background:#10b981;color:#052e1b;border:none;border-radius:7px;padding:5px 12px;font-size:11.5px;font-weight:700;cursor:pointer;pointer-events:auto}
|
|
85
|
+
.cz-banner .cz-deploy:hover{background:#34d399}
|
|
86
|
+
.cz-banner .cz-deploy:disabled{opacity:.6;cursor:default}
|
|
84
87
|
.cz-banner a:hover{text-decoration:underline}
|
|
85
88
|
.cz-banner .cz-idle{color:#64748b}
|
|
86
89
|
.cz-wrap{display:flex;flex-direction:column;align-items:flex-end;gap:6px}
|
|
@@ -1083,11 +1086,26 @@
|
|
|
1083
1086
|
const brand = el('div', 'cz-brand');
|
|
1084
1087
|
brand.innerHTML = '<kbd>⌘0</kbd> CmdZero';
|
|
1085
1088
|
const bannerStats = el('div', 'cz-stats');
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1089
|
+
// Build & Deploy — commits the current tweaks and pushes them to the live site
|
|
1090
|
+
// (the daemon runs it through headless claude).
|
|
1091
|
+
const deployBtn = el('button', 'cz-deploy', 'Build & Deploy ↗');
|
|
1092
|
+
let deployId = null;
|
|
1093
|
+
deployBtn.onclick = async () => {
|
|
1094
|
+
if (deployBtn.disabled) return;
|
|
1095
|
+
if (!confirm('Build & deploy the current tweaks to your live site?')) return;
|
|
1096
|
+
deployBtn.disabled = true;
|
|
1097
|
+
deployBtn.textContent = 'Deploying…';
|
|
1098
|
+
try {
|
|
1099
|
+
const r = await api('deploy', {});
|
|
1100
|
+
deployId = String(r.id);
|
|
1101
|
+
addTweak({ id: r.id, status: 'queued', label: 'Build & Deploy — committing & deploying…' });
|
|
1102
|
+
} catch (e) {
|
|
1103
|
+
resetDeployBtn();
|
|
1104
|
+
addTweak({ id: 'x' + Date.now(), status: 'error', label: `deploy: ${e.message}` });
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
function resetDeployBtn() { deployBtn.disabled = false; deployBtn.textContent = 'Build & Deploy ↗'; }
|
|
1108
|
+
banner.append(brand, bannerStats, deployBtn);
|
|
1091
1109
|
root.appendChild(banner);
|
|
1092
1110
|
// Push the page down so the banner sits ABOVE the site rather than over it.
|
|
1093
1111
|
// Inject a stylesheet instead of mutating body's style attribute: frameworks
|
|
@@ -1110,7 +1128,13 @@
|
|
|
1110
1128
|
const historyChip = el('div', 'cz-history');
|
|
1111
1129
|
historyChip.style.display = 'none';
|
|
1112
1130
|
const tweaksWrap = el('div', 'cz-wrap');
|
|
1113
|
-
|
|
1131
|
+
const pinNewestToBottom = () => { if (tweaksWrap.classList.contains('cz-expanded')) tweaksWrap.scrollTop = tweaksWrap.scrollHeight; };
|
|
1132
|
+
historyChip.onclick = () => {
|
|
1133
|
+
tweaksWrap.classList.toggle('cz-expanded');
|
|
1134
|
+
updateHistoryUI();
|
|
1135
|
+
// when expanded the list scrolls; keep the newest (bottom) in view
|
|
1136
|
+
requestAnimationFrame(pinNewestToBottom);
|
|
1137
|
+
};
|
|
1114
1138
|
tray.appendChild(historyChip);
|
|
1115
1139
|
const fade = el('div', 'cz-fade');
|
|
1116
1140
|
fade.style.display = 'none';
|
|
@@ -1219,8 +1243,10 @@
|
|
|
1219
1243
|
row._dot.className = 'cz-dot ' + t.status;
|
|
1220
1244
|
const inFlight = t.status === 'queued' || t.status === 'running';
|
|
1221
1245
|
row._cancel.style.display = inFlight ? '' : 'none';
|
|
1222
|
-
const undoable = t.status === 'done' && !key.startsWith('x');
|
|
1246
|
+
const undoable = t.status === 'done' && !key.startsWith('x') && t.kind !== 'deploy';
|
|
1223
1247
|
row._undo.style.display = undoable ? '' : 'none';
|
|
1248
|
+
// re-enable the Build & Deploy button once its task settles
|
|
1249
|
+
if (typeof deployId !== 'undefined' && deployId && key === deployId && ['done', 'error', 'cancelled'].includes(t.status)) resetDeployBtn();
|
|
1224
1250
|
if (undoable && !undoStack.includes(key)) undoStack.push(key);
|
|
1225
1251
|
if (t.status === 'reverted') {
|
|
1226
1252
|
const i = undoStack.indexOf(key);
|
|
@@ -1248,6 +1274,7 @@
|
|
|
1248
1274
|
tweakData.set(key, { ...(tweakData.get(key) || {}), ...t, id: key });
|
|
1249
1275
|
if (!hydrate) persistTweaks();
|
|
1250
1276
|
updateHistoryUI();
|
|
1277
|
+
if (!hydrate) pinNewestToBottom(); // keep the newest alert in view when expanded
|
|
1251
1278
|
}
|
|
1252
1279
|
|
|
1253
1280
|
// Replay saved alerts after a page load so history survives reloads.
|
|
@@ -1270,6 +1297,11 @@
|
|
|
1270
1297
|
.then((h) => {
|
|
1271
1298
|
showTotals(h.totals);
|
|
1272
1299
|
if (h.tailwind === false) state.tailwind = false;
|
|
1300
|
+
// Show the running overlay version so a stale/cached overlay is obvious.
|
|
1301
|
+
if (h.version) {
|
|
1302
|
+
brand.innerHTML = `<kbd>⌘0</kbd> CmdZero <span style="opacity:.45;font-weight:400">v${h.version}</span>`;
|
|
1303
|
+
console.log(`[cmdzero] overlay v${h.version} — newest alerts at the bottom of the tray`);
|
|
1304
|
+
}
|
|
1273
1305
|
})
|
|
1274
1306
|
.catch(() => {});
|
|
1275
1307
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cmdzero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
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",
|
package/src/router.js
CHANGED
|
@@ -274,3 +274,44 @@ export function runClaude({ prompt, model, effort, cwd, onEvent, onSpawn }) {
|
|
|
274
274
|
onEvent?.({ status: 'running', model });
|
|
275
275
|
});
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
// "Build & Deploy": run the deploy through headless claude (Bash allowed) so it
|
|
279
|
+
// commits the current tweaks and pushes them to the live site.
|
|
280
|
+
const DEPLOY_PROMPT = `You are deploying this project to production. Using the Bash tool, do exactly this, in order:
|
|
281
|
+
1. Run \`git add -A\`, then commit with a short message describing recent UI tweaks. Do NOT add any AI attribution/co-author trailer. If there is nothing to commit, continue.
|
|
282
|
+
2. If a git remote named "origin" exists, run \`git push\`.
|
|
283
|
+
3. Deploy to production. This is a Vercel project — run: \`vercel --prod --yes\`
|
|
284
|
+
On the FINAL line of your reply, output the production URL exactly as: DEPLOY_URL=<url>
|
|
285
|
+
If any step fails, stop and report the exact error.`;
|
|
286
|
+
|
|
287
|
+
export function runDeploy({ cwd, model = 'claude-sonnet-5', onEvent, onSpawn }) {
|
|
288
|
+
return new Promise((resolve) => {
|
|
289
|
+
const started = Date.now();
|
|
290
|
+
const args = [
|
|
291
|
+
'-p', DEPLOY_PROMPT,
|
|
292
|
+
'--model', model,
|
|
293
|
+
'--allowedTools', 'Read,Edit,Bash',
|
|
294
|
+
'--dangerously-skip-permissions', // headless: commit/push/deploy without prompts
|
|
295
|
+
'--output-format', 'json',
|
|
296
|
+
];
|
|
297
|
+
const child = spawn('claude', args, { cwd, env: { ...process.env, CLAUDE_CODE_ENTRYPOINT: undefined }, detached: true });
|
|
298
|
+
let cancelled = false;
|
|
299
|
+
const signalGroup = (sig) => { try { process.kill(-child.pid, sig); } catch { try { child.kill(sig); } catch {} } };
|
|
300
|
+
child.__cancel = () => { cancelled = true; signalGroup('SIGTERM'); setTimeout(() => { if (!child.killed) signalGroup('SIGKILL'); }, 1500).unref?.(); };
|
|
301
|
+
onSpawn?.(child);
|
|
302
|
+
let out = '', err = '';
|
|
303
|
+
child.stdout.on('data', (d) => (out += d));
|
|
304
|
+
child.stderr.on('data', (d) => (err += d));
|
|
305
|
+
child.on('close', (code) => {
|
|
306
|
+
const durationMs = Date.now() - started;
|
|
307
|
+
if (cancelled) return resolve({ ok: false, cancelled: true, durationMs });
|
|
308
|
+
if (code !== 0) return resolve({ ok: false, error: err.trim() || `claude exited ${code}`, durationMs });
|
|
309
|
+
let result = out;
|
|
310
|
+
try { result = JSON.parse(out).result || out; } catch { /* raw text */ }
|
|
311
|
+
const m = /DEPLOY_URL=(\S+)/.exec(result);
|
|
312
|
+
resolve({ ok: true, durationMs, url: m ? m[1] : null, result: String(result).slice(0, 500) });
|
|
313
|
+
});
|
|
314
|
+
child.on('error', (e) => resolve({ ok: false, error: `failed to spawn claude: ${e.message}` }));
|
|
315
|
+
onEvent?.({ status: 'running' });
|
|
316
|
+
});
|
|
317
|
+
}
|
package/src/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { describeTarget, applyTextEdit, applyClassEdit, applyStyleEdit, applyDeleteElement, applyMove, parseLoc, checkSyntax } from './resolver.js';
|
|
6
|
-
import { classify, buildPrompt, runClaude } from './router.js';
|
|
6
|
+
import { classify, buildPrompt, runClaude, runDeploy } from './router.js';
|
|
7
7
|
import { initTelemetry, DISCLOSURE } from './telemetry.js';
|
|
8
8
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -77,7 +77,7 @@ 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, root, totals, tailwind, telemetry: !telemetry.disabled });
|
|
80
|
+
return json(res, { ok: true, version: VERSION, root, totals, tailwind, telemetry: !telemetry.disabled });
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
if (req.method === 'GET' && url.pathname === '/api/events') {
|
|
@@ -184,6 +184,32 @@ export function startServer({ root, port = 4100 }) {
|
|
|
184
184
|
return json(res, { ok: true, ...route });
|
|
185
185
|
}
|
|
186
186
|
|
|
187
|
+
if (url.pathname === '/api/deploy') {
|
|
188
|
+
const id = nextId++;
|
|
189
|
+
telemetry.record('deploy');
|
|
190
|
+
broadcast({ type: 'tweak', id, kind: 'deploy', status: 'queued', label: 'Build & Deploy — committing & deploying…' });
|
|
191
|
+
json(res, { ok: true, id });
|
|
192
|
+
const result = await runDeploy({
|
|
193
|
+
cwd: root,
|
|
194
|
+
onEvent: (e) => broadcast({ type: 'tweak', id, ...e }),
|
|
195
|
+
onSpawn: (child) => running.set(String(id), child),
|
|
196
|
+
});
|
|
197
|
+
running.delete(String(id));
|
|
198
|
+
if (result.cancelled) {
|
|
199
|
+
broadcast({ type: 'tweak', id, status: 'cancelled', label: 'deploy cancelled' });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
broadcast({
|
|
203
|
+
type: 'tweak', id,
|
|
204
|
+
kind: 'deploy',
|
|
205
|
+
status: result.ok ? 'done' : 'error',
|
|
206
|
+
durationMs: result.durationMs,
|
|
207
|
+
label: result.ok ? (result.url ? `deployed → ${result.url}` : 'deployed to production') : 'deploy failed',
|
|
208
|
+
error: result.ok ? undefined : (result.error || '').slice(0, 200),
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
187
213
|
if (url.pathname === '/api/nl') {
|
|
188
214
|
const id = nextId++;
|
|
189
215
|
const { file } = parseLoc(body.loc);
|
package/src/telemetry.js
CHANGED
|
@@ -53,7 +53,7 @@ export function initTelemetry({ version, tailwind }) {
|
|
|
53
53
|
saveConfig(config);
|
|
54
54
|
}
|
|
55
55
|
const disabled = telemetryDisabled();
|
|
56
|
-
const counts = { copy: 0, style: 0, delete: 0, nl: 0, move: 0 };
|
|
56
|
+
const counts = { copy: 0, style: 0, delete: 0, nl: 0, move: 0, deploy: 0 };
|
|
57
57
|
let flushTimer = null;
|
|
58
58
|
|
|
59
59
|
function send(event, extra = {}) {
|