@codeyam-editor/codeyam-editor 0.1.3 → 0.1.4

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/npm/utils.js CHANGED
@@ -620,6 +620,114 @@ function tryKillPid(pid) {
620
620
  }
621
621
  }
622
622
 
623
+ /**
624
+ * True when the launcher is attached to an interactive terminal — i.e. a human
625
+ * could have typed Ctrl+C. Headless / agent-driven launchers (cloud containers,
626
+ * `npm run editor` under a supervisor) have no tty here. Used to decide whether a
627
+ * received SIGINT is a deliberate "stop the editor" (forward it) or a stray
628
+ * group / spurious signal that must NOT reap the now-detached server.
629
+ */
630
+ function isInteractiveLauncher(stream = process.stdout) {
631
+ return Boolean(stream && stream.isTTY);
632
+ }
633
+
634
+ /**
635
+ * Spawn options for the editor server child. The server is spawned `detached:
636
+ * true` so it leads its OWN session + process group (setsid), decoupling its
637
+ * lifetime from the launcher's controlling terminal and process group — a stray
638
+ * tty Ctrl+C or a group-wide `kill(-pgid, SIGINT)` can no longer reach it. stdio
639
+ * is detached from the terminal for the same reason (an inherited tty can still
640
+ * deliver tty signals to a detached child): stdout/stderr go to `logFd` when one
641
+ * is supplied, else are ignored — the Rust server writes its own
642
+ * `.codeyam/logs/editor-server.log` regardless.
643
+ */
644
+ function buildServerSpawnOptions({ cwd, env, logFd = null }) {
645
+ return {
646
+ stdio: logFd === null ? "ignore" : ["ignore", logFd, logFd],
647
+ cwd,
648
+ detached: true,
649
+ env,
650
+ };
651
+ }
652
+
653
+ /**
654
+ * Open (creating dirs as needed) the launcher-managed server stdout/stderr log
655
+ * in append mode and return its fd, or null if it can't be opened. Kept separate
656
+ * from the Rust server's own structured `editor-server.log` so the two streams
657
+ * never interleave. Best-effort: a failure here just means the detached server's
658
+ * stdio is ignored, never that the launch itself fails.
659
+ */
660
+ function openServerOutputLog(cwd) {
661
+ try {
662
+ const dir = path.join(cwd, ".codeyam", "logs");
663
+ fs.mkdirSync(dir, { recursive: true });
664
+ return fs.openSync(path.join(dir, "editor-server.out.log"), "a");
665
+ } catch {
666
+ return null;
667
+ }
668
+ }
669
+
670
+ /**
671
+ * Format one launcher-teardown-signal record for
672
+ * `.codeyam/logs/launcher-signals.log`. Pure: every input is passed in, so the
673
+ * formatting is unit-testable without a real signal or `ps`. Captures the
674
+ * launcher's own pid/ppid/pgid, whether it was interactive (a tty Ctrl+C) vs
675
+ * headless (a stray/spurious signal), the decision taken (forwarded teardown vs
676
+ * kept the server alive), and a best-effort `ps` group snapshot so the SENDER of
677
+ * a spurious `kill()` can be identified on the next repro.
678
+ */
679
+ function formatLauncherSignalRecord({ signal, pid, ppid, pgid, interactive, forwarded, ps, timestamp }) {
680
+ const lines = [
681
+ `[${timestamp}] launcher-signal signal=${signal} pid=${pid} ppid=${ppid} pgid=${pgid} ` +
682
+ `interactive=${interactive} action=${forwarded ? "forwarded-teardown" : "ignored-kept-server-alive"}`,
683
+ ];
684
+ if (ps && ps.trim() !== "") {
685
+ lines.push(ps.trimEnd());
686
+ }
687
+ return lines.join("\n") + "\n";
688
+ }
689
+
690
+ /**
691
+ * Append a launcher-teardown-signal forensic record. Called from each launcher's
692
+ * SIGINT handler BEFORE it decides whether to forward, so a server reaped by a
693
+ * stray group SIGINT (or a spurious `kill()` to a headless launcher) always
694
+ * leaves a trail naming the launcher's process group. Best-effort: never throws,
695
+ * so a logging failure can never block the teardown decision.
696
+ */
697
+ function logLauncherTeardownSignal(signal, projectDir, { interactive, forwarded }) {
698
+ try {
699
+ const pid = process.pid;
700
+ const ppid = typeof process.ppid === "number" ? process.ppid : -1;
701
+ let pgid = -1;
702
+ let ps = "";
703
+ if (process.platform !== "win32") {
704
+ const pgidOut = spawnSync("ps", ["-o", "pgid=", "-p", String(pid)], { encoding: "utf8" });
705
+ if (!pgidOut.error) {
706
+ pgid = parseInt(pgidOut.stdout.trim(), 10);
707
+ }
708
+ if (Number.isInteger(pgid) && pgid > 0) {
709
+ const grp = spawnSync("ps", ["-o", "pid,ppid,pgid,tpgid,comm", "-g", String(pgid)], { encoding: "utf8" });
710
+ if (!grp.error) ps = grp.stdout;
711
+ }
712
+ }
713
+ const dir = path.join(projectDir, ".codeyam", "logs");
714
+ fs.mkdirSync(dir, { recursive: true });
715
+ const record = formatLauncherSignalRecord({
716
+ signal,
717
+ pid,
718
+ ppid,
719
+ pgid,
720
+ interactive: Boolean(interactive),
721
+ forwarded: Boolean(forwarded),
722
+ ps,
723
+ timestamp: new Date().toISOString(),
724
+ });
725
+ fs.appendFileSync(path.join(dir, "launcher-signals.log"), record);
726
+ } catch {
727
+ /* best-effort forensics — never block teardown on a logging failure */
728
+ }
729
+ }
730
+
623
731
  /**
624
732
  * Send the platform-correct termination signal to a spawned child process.
625
733
  * Used by editor.js / editor-dev.js / container.js when the user hits Ctrl+C —
@@ -726,12 +834,29 @@ async function ensureServer(port = 14199) {
726
834
  const binary = ensureBinary();
727
835
  // UI bundle is embedded in the Rust binary; CODEYAM_EDITOR_UI_DIR is
728
836
  // now an explicit dev-override only.
729
- const child = spawn(binary, ["start", "--no-open", "--port", String(port)], {
730
- stdio: "inherit",
731
- cwd: process.cwd(),
732
- detached: false,
733
- env: process.env,
734
- });
837
+ //
838
+ // The server is spawned into its OWN session/process group (detached: true)
839
+ // with stdio detached from the launcher's controlling terminal, so a stray
840
+ // tty Ctrl+C or a group-wide SIGINT can no longer reap it — its lifetime is
841
+ // governed only by the launcher's deliberate teardown forward and the
842
+ // pidfile / API-shutdown paths. stdout/stderr are appended to
843
+ // `.codeyam/logs/editor-server.out.log` (the Rust server also writes its own
844
+ // editor-server.log). `unref()` keeps the detached child from pinning the
845
+ // launcher's event loop alive on its own.
846
+ const logFd = openServerOutputLog(process.cwd());
847
+ const child = spawn(
848
+ binary,
849
+ ["start", "--no-open", "--port", String(port)],
850
+ buildServerSpawnOptions({ cwd: process.cwd(), env: process.env, logFd }),
851
+ );
852
+ if (logFd !== null) {
853
+ try {
854
+ fs.closeSync(logFd);
855
+ } catch {
856
+ /* child holds its own dup of the fd; the launcher's copy is no longer needed */
857
+ }
858
+ }
859
+ child.unref();
735
860
 
736
861
  const { outcome, exitedBeforeBind } = await awaitServerReadyOrChildExit(child, port);
737
862
 
@@ -740,7 +865,8 @@ async function ensureServer(port = 14199) {
740
865
  const detail = signal != null ? `signal ${signal}` : `code ${code}`;
741
866
  console.error(
742
867
  `codeyam-editor server exited (${detail}) before binding port ${port}. ` +
743
- "Likely a preflight bail; see output above.",
868
+ "Likely a preflight bail; see .codeyam/logs/editor-server.out.log " +
869
+ "and .codeyam/logs/editor-server.log.",
744
870
  );
745
871
  // Always propagate as a real failure — even on code 0 the caller
746
872
  // needs to know the server isn't running.
@@ -814,6 +940,11 @@ module.exports = {
814
940
  restartServer,
815
941
  ensureServer,
816
942
  awaitServerReadyOrChildExit,
943
+ isInteractiveLauncher,
944
+ buildServerSpawnOptions,
945
+ openServerOutputLog,
946
+ formatLauncherSignalRecord,
947
+ logLauncherTeardownSignal,
817
948
  killChildProcess,
818
949
  deepMergeJson,
819
950
  stripUnderscoreTopLevelKeys,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeyam-editor/codeyam-editor",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Language-agnostic managed execution sandbox for scenario-driven development",
5
5
  "bin": {
6
6
  "codeyam-editor": "./npm/cli.js"
@@ -9,10 +9,10 @@
9
9
  "playwright": "^1.58.2"
10
10
  },
11
11
  "optionalDependencies": {
12
- "@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.3",
13
- "@codeyam-editor/codeyam-editor-darwin-x64": "0.1.3",
14
- "@codeyam-editor/codeyam-editor-linux-x64": "0.1.3",
15
- "@codeyam-editor/codeyam-editor-win32-x64": "0.1.3"
12
+ "@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.4",
13
+ "@codeyam-editor/codeyam-editor-darwin-x64": "0.1.4",
14
+ "@codeyam-editor/codeyam-editor-linux-x64": "0.1.4",
15
+ "@codeyam-editor/codeyam-editor-win32-x64": "0.1.4"
16
16
  },
17
17
  "keywords": [
18
18
  "codeyam",