transitions-refine 0.3.18 → 0.3.19

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.
Files changed (2) hide show
  1. package/bin/cli.mjs +140 -11
  2. package/package.json +1 -1
package/bin/cli.mjs CHANGED
@@ -19,10 +19,11 @@
19
19
  // 4. starts the local refine relay (serves the panel at /inject.js).
20
20
 
21
21
  import { spawn, spawnSync } from "node:child_process";
22
- import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, realpathSync } from "node:fs";
22
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, realpathSync, openSync, rmSync } from "node:fs";
23
23
  import { dirname, join, resolve } from "node:path";
24
24
  import { fileURLToPath } from "node:url";
25
25
  import { homedir } from "node:os";
26
+ import { get as httpGet } from "node:http";
26
27
 
27
28
  const PKG_ROOT = fileURLToPath(new URL("..", import.meta.url));
28
29
  const CWD = process.cwd();
@@ -264,7 +265,108 @@ function log(msg) {
264
265
  process.stdout.write(`${msg}\n`);
265
266
  }
266
267
 
267
- function cmdLive(args) {
268
+ function rel(p) {
269
+ return p && p.startsWith(CWD + "/") ? p.slice(CWD.length + 1) : p;
270
+ }
271
+
272
+ // ── background daemon (for one-shot agent shells) ────────────────────────────
273
+ // Claude Code / Codex / Cursor run a `live` command in a NON-interactive shell
274
+ // that expects the command to EXIT. A foreground relay never exits, so the agent
275
+ // shell buffers (no output) and eventually kills the process group — which runs
276
+ // cleanup and strips the injected tag, so "nothing works". In those shells we
277
+ // instead fork the relay as a detached daemon (its own session via detached:true,
278
+ // so a process-group kill on the agent shell can't reach it), record a PID file,
279
+ // print status, and exit 0. `stop` reads the PID file to tear it down.
280
+ function daemonFile() {
281
+ return join(CWD, ".refine-relay.json");
282
+ }
283
+
284
+ // One GET /health probe. Resolves true if the relay answers at all.
285
+ function httpOk(port, path = "/health", timeoutMs = 800) {
286
+ return new Promise((resolveOk) => {
287
+ let done = false;
288
+ const finish = (v) => { if (!done) { done = true; resolveOk(v); } };
289
+ const req = httpGet({ host: "127.0.0.1", port: Number(port), path, timeout: timeoutMs }, (res) => {
290
+ res.resume(); // drain so the socket can close
291
+ finish(res.statusCode >= 200 && res.statusCode < 500);
292
+ });
293
+ req.on("timeout", () => { req.destroy(); finish(false); });
294
+ req.on("error", () => finish(false));
295
+ });
296
+ }
297
+
298
+ async function waitForHealth(port, totalMs = 6000) {
299
+ const deadline = Date.now() + totalMs;
300
+ while (Date.now() < deadline) {
301
+ if (await httpOk(port)) return true;
302
+ await new Promise((r) => setTimeout(r, 250));
303
+ }
304
+ return false;
305
+ }
306
+
307
+ // Kill a previously-detached relay for this project (best effort).
308
+ function stopDaemon({ silent = false } = {}) {
309
+ let rec = null;
310
+ try { rec = JSON.parse(readFileSync(daemonFile(), "utf8")); } catch {}
311
+ if (!rec || !rec.pid) return false;
312
+ let killed = false;
313
+ try { process.kill(rec.pid, "SIGTERM"); killed = true; } catch {}
314
+ try { rmSync(daemonFile()); } catch {}
315
+ if (killed && !silent) log(`✓ stopped background relay (pid ${rec.pid})`);
316
+ return killed;
317
+ }
318
+
319
+ // Fork the relay as a detached background process and return once it's healthy
320
+ // (or we give up waiting). The CLI then exits, so the agent's shell command
321
+ // completes and surfaces this output while the relay keeps running.
322
+ async function startDetached({ port, page, env, resolved }) {
323
+ stopDaemon({ silent: true }); // replace any prior daemon (port clash / dupes)
324
+
325
+ const logPath = join(CWD, ".refine-relay.log");
326
+ let out = "ignore";
327
+ try { out = openSync(logPath, "a"); } catch {}
328
+ const child = spawn(process.execPath, [join(PKG_ROOT, "server/relay.mjs")], {
329
+ detached: true,
330
+ stdio: ["ignore", out, out],
331
+ env,
332
+ });
333
+ child.unref();
334
+
335
+ const llmWired = Boolean(env.REFINE_AGENT_CMD);
336
+ const record = {
337
+ pid: child.pid,
338
+ port: Number(port),
339
+ page: page || null,
340
+ log: logPath,
341
+ version: PKG_VERSION,
342
+ agentCmd: env.REFINE_AGENT_CMD || null,
343
+ startedAt: new Date().toISOString(),
344
+ };
345
+ try { writeFileSync(daemonFile(), JSON.stringify(record, null, 2) + "\n"); } catch {}
346
+
347
+ const healthy = await waitForHealth(port, 6000);
348
+ log("");
349
+ if (healthy) {
350
+ log(`✓ relay running in background (pid ${child.pid}) — http://localhost:${port}`);
351
+ } else {
352
+ log(`! relay started (pid ${child.pid}) but wasn't healthy within 6s — check ${rel(logPath)}`);
353
+ }
354
+ if (llmWired) {
355
+ log("✓ agent wired — Refine answers jobs itself (no /refine live loop needed).");
356
+ } else {
357
+ log(`• agent NOT wired${resolved && resolved.reason ? ` — ${resolved.reason}` : ""}.`);
358
+ log(" LLM jobs fall back to a /refine live poller. To wire a persistent agent,");
359
+ log(" re-run with --agent <cursor|claude|codex> (that CLI must be on PATH).");
360
+ }
361
+ log("");
362
+ log("Next:");
363
+ log(" 1. Open your app and hard-refresh — the timeline panel is on the page.");
364
+ log(" 2. Click Refine.");
365
+ log(` Stop it (and remove the injected tag): npx transitions-refine stop`);
366
+ log(` Logs: ${rel(logPath)}`);
367
+ }
368
+
369
+ async function cmdLive(args) {
268
370
  const port = String(args.port || process.env.REFINE_RELAY_PORT || 7331);
269
371
  const page = findPage(args.page);
270
372
 
@@ -312,6 +414,18 @@ function cmdLive(args) {
312
414
  log(`• LLM not wired — ${resolved.reason}.`);
313
415
  }
314
416
 
417
+ // 2.75) In a one-shot agent shell (Claude Code / Codex / Cursor tool calls),
418
+ // stdout isn't a TTY and the command is expected to exit. Run the relay
419
+ // as a detached daemon and return, so the agent gets output + a persistent
420
+ // relay. A real interactive terminal (TTY) keeps the foreground Ctrl-C UX.
421
+ // Override either way with --detach / --foreground.
422
+ const inAgentShell = !process.stdout.isTTY;
423
+ const detach = Boolean(args.detach) || (inAgentShell && !args.foreground);
424
+ if (detach) {
425
+ await startDetached({ port, page, env, resolved });
426
+ return;
427
+ }
428
+
315
429
  // 3) start the relay (foreground; Ctrl-C stops it + reverts the injection)
316
430
  const relay = spawn(process.execPath, [join(PKG_ROOT, "server/relay.mjs")], {
317
431
  stdio: "inherit",
@@ -354,21 +468,28 @@ function cmdLive(args) {
354
468
  }
355
469
 
356
470
  function cmdStop(args) {
357
- const page = findPage(args.page);
471
+ // 1) kill a background daemon if one is recorded for this project. Prefer the
472
+ // page the daemon injected (so we clean the right file even without --page).
473
+ let rec = null;
474
+ try { rec = JSON.parse(readFileSync(daemonFile(), "utf8")); } catch {}
475
+ const killed = stopDaemon();
476
+
477
+ // 2) strip the injected tag.
478
+ const page = findPage(args.page) || (rec && rec.page) || null;
358
479
  if (!page || !existsSync(page)) {
359
- log("! no HTML entry found to clean. Pass --page <path> if needed.");
480
+ if (!killed) log("! no HTML entry found to clean. Pass --page <path> if needed.");
360
481
  return;
361
482
  }
362
483
  const html = readFileSync(page, "utf8");
363
484
  if (!html.includes(MARK_START)) {
364
- log(`nothing to remove in ${page.replace(CWD + "/", "")}`);
485
+ if (!killed) log(`nothing to remove in ${rel(page)}`);
365
486
  return;
366
487
  }
367
488
  writeFileSync(page, stripTag(html));
368
- log(`✓ removed injected tag from ${page.replace(CWD + "/", "")}`);
489
+ log(`✓ removed injected tag from ${rel(page)}`);
369
490
  }
370
491
 
371
- function main() {
492
+ async function main() {
372
493
  const args = parseArgs(process.argv.slice(2));
373
494
  const cmd = args._[0] || "live";
374
495
  if (cmd === "live") return cmdLive(args);
@@ -377,11 +498,16 @@ function main() {
377
498
  log(" npx transitions-refine live # inject panel + relay; auto-wire your agent CLI");
378
499
  log(" npx transitions-refine live --agent claude # force an agent: cursor | claude | codex");
379
500
  log(" npx transitions-refine live --llm # install the Cursor CLI if no agent is found");
380
- log(" npx transitions-refine stop # remove the injected tag");
501
+ log(" npx transitions-refine stop # stop the relay + remove the injected tag");
381
502
  log("");
382
503
  log("Options: --page <html> --port <n> --agent <cursor|claude|codex> --llm");
383
- log("It prefers the agent hosting this run (Cursor/Claude Code/Codex) so Refine uses");
384
- log("the subscription you already have. Or set REFINE_AGENT_CMD to wire any CLI.");
504
+ log(" --detach run the relay in the background and exit (default in agent shells)");
505
+ log(" --foreground keep the relay in the foreground (Ctrl-C to stop)");
506
+ log("");
507
+ log("In a coding agent (Claude Code / Codex / Cursor) `live` auto-detaches: it starts");
508
+ log("a background relay, wires that agent's CLI, and exits — so just running the command");
509
+ log("works. It prefers the agent hosting this run so Refine uses the subscription you");
510
+ log("already have. Or set REFINE_AGENT_CMD to wire any CLI.");
385
511
  process.exit(cmd ? 1 : 0);
386
512
  }
387
513
 
@@ -400,7 +526,10 @@ function isCliEntry() {
400
526
  return resolve(process.argv[1]) === resolve(self);
401
527
  }
402
528
  if (isCliEntry()) {
403
- main();
529
+ main().catch((e) => {
530
+ log(`! refine failed: ${e && e.message ? e.message : e}`);
531
+ process.exit(1);
532
+ });
404
533
  }
405
534
 
406
535
  export { AGENTS, HOST_PRECEDENCE, detectHostAgent, resolveAgent, findBin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "transitions-refine",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "Live, agent-driven Refine panel for CSS/Motion transitions — injects a timeline + Refine UI and runs transitions.dev suggestions via your coding agent.",
5
5
  "type": "module",
6
6
  "bin": {