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.
- package/bin/cli.mjs +140 -11
- 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
|
|
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
|
-
|
|
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
|
|
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
|
|
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("
|
|
384
|
-
log("the
|
|
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.
|
|
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": {
|