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.
@@ -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
- const reportLink = el('a', null, 'report →');
1087
- reportLink.href = 'https://cmdzero.dev/report';
1088
- reportLink.target = '_blank';
1089
- reportLink.rel = 'noopener';
1090
- banner.append(brand, bannerStats, reportLink);
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
- historyChip.onclick = () => { tweaksWrap.classList.toggle('cz-expanded'); updateHistoryUI(); };
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.7.1",
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 = {}) {