@supanadit/morse-web 0.12.0 → 0.13.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/dist/server.mjs CHANGED
@@ -79,7 +79,7 @@ function hasKnownType(value, allowed) {
79
79
  }
80
80
 
81
81
  // ../protocol/dist/version.js
82
- var PROTOCOL_VERSION = 20;
82
+ var PROTOCOL_VERSION = 21;
83
83
  var FRONTEND_MANIFEST_FILE = "webview.manifest.json";
84
84
  var DEFAULT_WS_PATH = "/ws";
85
85
 
@@ -2145,6 +2145,17 @@ function createPiRpcAdapter(config, logger) {
2145
2145
  };
2146
2146
  }
2147
2147
 
2148
+ // ../host-runtime/dist/terminal-buffer.js
2149
+ var TERMINAL_BUFFER_CHARS = 256 * 1024;
2150
+ function trimTail(text, maxChars = TERMINAL_BUFFER_CHARS) {
2151
+ if (text.length <= maxChars) {
2152
+ return text;
2153
+ }
2154
+ const tail = text.slice(-maxChars);
2155
+ const newline = tail.indexOf("\n");
2156
+ return newline === -1 ? tail : tail.slice(newline + 1);
2157
+ }
2158
+
2148
2159
  // ../host-runtime/dist/transcript-projector.js
2149
2160
  var TranscriptProjector = class {
2150
2161
  options;
@@ -2671,8 +2682,15 @@ var HostSessionController = class {
2671
2682
  loadingOlderHistory = false;
2672
2683
  /** Discriminates history item ids across pages (see `historyItems`). */
2673
2684
  historyPageSeq = 0;
2674
- /** The shells this client has open, keyed by the terminal id it named. */
2685
+ /** This client's live attachments to shells, keyed by the terminal id it named. */
2675
2686
  terminals = /* @__PURE__ */ new Map();
2687
+ /**
2688
+ * The ids this connection has already opened. It separates a *first* open — a
2689
+ * page reattaching to a shell the host still runs — from a repeat open (the
2690
+ * Restart button), which must replace the shell and clear its scrollback
2691
+ * instead of replaying it into the emulator that already shows it.
2692
+ */
2693
+ openedTerminals = /* @__PURE__ */ new Set();
2676
2694
  /**
2677
2695
  * Bumped whenever a terminal id is opened or closed. A shell's late `exit`
2678
2696
  * (a replaced one, a killed one) compares its generation and stays silent, so
@@ -2721,11 +2739,12 @@ var HostSessionController = class {
2721
2739
  return;
2722
2740
  }
2723
2741
  this.disposed = true;
2724
- for (const process2 of this.terminals.values()) {
2725
- process2.kill();
2742
+ for (const session of this.terminals.values()) {
2743
+ session.detach();
2726
2744
  }
2727
2745
  this.terminals.clear();
2728
2746
  this.terminalGenerations.clear();
2747
+ this.openedTerminals.clear();
2729
2748
  this.detachRegistry?.();
2730
2749
  this.detachRegistry = void 0;
2731
2750
  this.detachTranscripts?.();
@@ -2830,23 +2849,38 @@ var HostSessionController = class {
2830
2849
  }
2831
2850
  }
2832
2851
  /**
2833
- * Starts a shell for the bottom panel. The directory is the viewing session's
2834
- * (or the draft's), never an arbitrary path the client names — the backend
2835
- * still re-checks it against the host policy before spawning.
2852
+ * Attaches this client to a shell for the bottom panel. The directory is the
2853
+ * viewing session's (or the draft's), never an arbitrary path the client
2854
+ * names — the backend still re-checks it against the host policy before
2855
+ * spawning.
2856
+ *
2857
+ * A *first* open on this connection may land on a shell that is already running
2858
+ * (a reloaded page reattaching); a repeat open (the Restart button) replaces
2859
+ * it, scrollback and all, so the fresh shell does not replay the old one.
2836
2860
  */
2837
2861
  async openTerminal(payload) {
2838
- if (this.options.terminal === void 0) {
2862
+ const backend = this.options.terminal;
2863
+ if (backend === void 0) {
2839
2864
  this.emitTerminalExit(payload.terminalId, void 0, "This host has no terminal.");
2840
2865
  return;
2841
2866
  }
2842
- this.closeTerminal(payload.terminalId);
2843
- const generation = this.terminalGenerations.get(payload.terminalId) ?? 0;
2867
+ const previous = this.terminals.get(payload.terminalId);
2868
+ if (previous !== void 0) {
2869
+ previous.detach();
2870
+ this.terminals.delete(payload.terminalId);
2871
+ backend.close(payload.terminalId);
2872
+ } else if (this.openedTerminals.has(payload.terminalId)) {
2873
+ backend.close(payload.terminalId);
2874
+ }
2875
+ this.openedTerminals.add(payload.terminalId);
2876
+ const generation = (this.terminalGenerations.get(payload.terminalId) ?? 0) + 1;
2877
+ this.terminalGenerations.set(payload.terminalId, generation);
2844
2878
  const cwd = payload.cwd ?? this.currentState().workspace.cwd;
2845
2879
  const cols = clampGeometry(payload.cols, 80);
2846
2880
  const rows = clampGeometry(payload.rows, 24);
2847
- let process2;
2881
+ let session;
2848
2882
  try {
2849
- process2 = await this.options.terminal.open({ cwd, cols, rows }, {
2883
+ session = await backend.attach(payload.terminalId, { cwd, cols, rows }, {
2850
2884
  output: (data) => {
2851
2885
  if (this.terminalGenerations.get(payload.terminalId) === generation) {
2852
2886
  this.emitTerminalOutput(payload.terminalId, data);
@@ -2866,19 +2900,25 @@ var HostSessionController = class {
2866
2900
  return;
2867
2901
  }
2868
2902
  if (!this.disposed && this.terminalGenerations.get(payload.terminalId) === generation) {
2869
- this.terminals.set(payload.terminalId, process2);
2903
+ this.terminals.set(payload.terminalId, session);
2904
+ if (session.replay.length > 0) {
2905
+ this.emitTerminalOutput(payload.terminalId, session.replay);
2906
+ }
2870
2907
  } else {
2871
- process2.kill();
2908
+ session.detach();
2872
2909
  }
2873
2910
  }
2911
+ /**
2912
+ * Ends a shell for good: the reader closed the pane. Bumps the generation first,
2913
+ * so a late `exit` for the shell being replaced is not reported as news.
2914
+ */
2874
2915
  closeTerminal(terminalId) {
2875
2916
  this.terminalGenerations.set(terminalId, (this.terminalGenerations.get(terminalId) ?? 0) + 1);
2876
- const process2 = this.terminals.get(terminalId);
2877
- if (process2 === void 0) {
2878
- return;
2879
- }
2917
+ const session = this.terminals.get(terminalId);
2880
2918
  this.terminals.delete(terminalId);
2881
- process2.kill();
2919
+ this.openedTerminals.delete(terminalId);
2920
+ session?.detach();
2921
+ this.options.terminal?.close(terminalId);
2882
2922
  }
2883
2923
  emitTerminalOutput(terminalId, data) {
2884
2924
  if (data.length > 0 && !this.disposed) {
@@ -3769,7 +3809,50 @@ var ServerProjectPolicy = class {
3769
3809
  // ../server/src/internal/terminal/terminal.service.ts
3770
3810
  import { spawn as spawnPty } from "node-pty";
3771
3811
  import { Inject as Inject2, Injectable } from "@nestjs/common";
3812
+
3813
+ // ../server/src/internal/terminal/terminal-log.ts
3814
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3815
+ import { join as join5 } from "node:path";
3816
+ var LOG_DIR = "terminals";
3817
+ var LOG_MAX_CHARS = TERMINAL_BUFFER_CHARS * 4;
3818
+ function readTerminalLog(dataDir, terminalId) {
3819
+ try {
3820
+ const raw = readFileSync(logPath(dataDir, terminalId), "utf8");
3821
+ return trimTail(raw, LOG_MAX_CHARS);
3822
+ } catch {
3823
+ return "";
3824
+ }
3825
+ }
3826
+ function writeTerminalLog(dataDir, terminalId, text) {
3827
+ const trimmed = trimTail(text, LOG_MAX_CHARS);
3828
+ const target = logPath(dataDir, terminalId);
3829
+ const temp = `${target}.${process.pid}.tmp`;
3830
+ try {
3831
+ mkdirSync(join5(dataDir, LOG_DIR), { recursive: true });
3832
+ writeFileSync(temp, trimmed);
3833
+ renameSync(temp, target);
3834
+ } catch {
3835
+ try {
3836
+ writeFileSync(target, trimmed);
3837
+ } catch {
3838
+ rmSync(temp, { force: true });
3839
+ }
3840
+ }
3841
+ }
3842
+ function removeTerminalLog(dataDir, terminalId) {
3843
+ rmSync(logPath(dataDir, terminalId), { force: true });
3844
+ }
3845
+ function safeTerminalName(terminalId) {
3846
+ const safe = terminalId.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 100);
3847
+ return safe.length > 0 ? safe : "terminal";
3848
+ }
3849
+ function logPath(dataDir, terminalId) {
3850
+ return join5(dataDir, LOG_DIR, `${safeTerminalName(terminalId)}.log`);
3851
+ }
3852
+
3853
+ // ../server/src/internal/terminal/terminal.service.ts
3772
3854
  var SIGKILL_AFTER_MS = 1500;
3855
+ var FLUSH_DEBOUNCE_MS = 2e3;
3773
3856
  function defaultShell() {
3774
3857
  const configured = process.env.SHELL?.trim();
3775
3858
  if (configured !== void 0 && configured.length > 0) {
@@ -3778,17 +3861,95 @@ function defaultShell() {
3778
3861
  return process.platform === "win32" ? "powershell.exe" : "/bin/bash";
3779
3862
  }
3780
3863
  var ServerTerminalBackend = class {
3781
- constructor(policy, logger) {
3864
+ constructor(policy, logger, config) {
3782
3865
  this.policy = policy;
3783
3866
  this.logger = logger;
3867
+ this.config = config;
3868
+ this.entries = /* @__PURE__ */ new Map();
3869
+ }
3870
+ attach(terminalId, options, sink) {
3871
+ const entry = this.entryFor(terminalId);
3872
+ if (entry.pty === void 0) {
3873
+ this.spawn(terminalId, entry, options);
3874
+ } else {
3875
+ this.resize(entry, options.cols, options.rows);
3876
+ }
3877
+ entry.sinks.add(sink);
3878
+ this.cancelIdle(entry);
3879
+ return {
3880
+ replay: entry.output,
3881
+ write: (data) => entry.pty?.write(data),
3882
+ resize: (cols, rows) => this.resize(entry, cols, rows),
3883
+ detach: () => this.detach(terminalId, entry, sink)
3884
+ };
3784
3885
  }
3785
- open(options, sink) {
3886
+ /** Ends a shell for good, drops its scrollback, and tells any other viewer. */
3887
+ close(terminalId) {
3888
+ const entry = this.entries.get(terminalId);
3889
+ this.entries.delete(terminalId);
3890
+ if (entry !== void 0) {
3891
+ this.stopFlush(entry);
3892
+ this.cancelIdle(entry);
3893
+ entry.generation += 1;
3894
+ const sinks = [...entry.sinks];
3895
+ entry.sinks.clear();
3896
+ this.killPty(entry.pty);
3897
+ entry.pty = void 0;
3898
+ for (const sink of sinks) {
3899
+ sink.exit();
3900
+ }
3901
+ }
3902
+ removeTerminalLog(this.config.dataDir, terminalId);
3903
+ }
3904
+ onModuleDestroy() {
3905
+ for (const [terminalId, entry] of this.entries) {
3906
+ entry.generation += 1;
3907
+ this.persist(terminalId, entry);
3908
+ this.killPty(entry.pty);
3909
+ entry.pty = void 0;
3910
+ this.stopFlush(entry);
3911
+ this.cancelIdle(entry);
3912
+ }
3913
+ this.entries.clear();
3914
+ }
3915
+ /** SIGTERM, then SIGKILL for a shell that ignores it (a hung process group). */
3916
+ killPty(pty) {
3917
+ if (pty === void 0) {
3918
+ return;
3919
+ }
3920
+ try {
3921
+ pty.kill();
3922
+ } catch {
3923
+ return;
3924
+ }
3925
+ const timer = setTimeout(() => {
3926
+ try {
3927
+ pty.kill("SIGKILL");
3928
+ } catch {
3929
+ }
3930
+ }, SIGKILL_AFTER_MS);
3931
+ timer.unref?.();
3932
+ }
3933
+ entryFor(terminalId) {
3934
+ const existing = this.entries.get(terminalId);
3935
+ if (existing !== void 0) {
3936
+ return existing;
3937
+ }
3938
+ const entry = {
3939
+ sinks: /* @__PURE__ */ new Set(),
3940
+ output: readTerminalLog(this.config.dataDir, terminalId),
3941
+ dirty: false,
3942
+ generation: 0
3943
+ };
3944
+ this.entries.set(terminalId, entry);
3945
+ return entry;
3946
+ }
3947
+ spawn(terminalId, entry, options) {
3786
3948
  if (!this.policy.canOpen(options.cwd)) {
3787
- throw new UnsupportedByHostError(
3788
- `Morse is not allowed to run a terminal in ${options.cwd}.`
3789
- );
3949
+ throw new UnsupportedByHostError(`Morse is not allowed to run a terminal in ${options.cwd}.`);
3790
3950
  }
3791
3951
  const shell = defaultShell();
3952
+ const generation = ++entry.generation;
3792
3953
  const pty = spawnPty(shell, [], {
3793
3954
  name: "xterm-256color",
3794
3955
  cols: options.cols,
@@ -3796,41 +3957,105 @@ var ServerTerminalBackend = class {
3796
3957
  cwd: options.cwd,
3797
3958
  env: { ...process.env, TERM: "xterm-256color", MORSE_TERMINAL: "1" }
3798
3959
  });
3960
+ entry.pty = pty;
3799
3961
  this.logger.info(`Terminal opened in ${options.cwd} (${shell}, pid ${pty.pid})`);
3800
- let exited = false;
3801
- const data = pty.onData((chunk) => sink.output(chunk));
3962
+ pty.onData((chunk) => {
3963
+ if (entry.generation !== generation) {
3964
+ return;
3965
+ }
3966
+ entry.output += chunk;
3967
+ if (entry.output.length > TERMINAL_BUFFER_CHARS * 1.5) {
3968
+ entry.output = trimTail(entry.output, TERMINAL_BUFFER_CHARS);
3969
+ }
3970
+ entry.dirty = true;
3971
+ this.scheduleFlush(terminalId, entry);
3972
+ for (const sink of entry.sinks) {
3973
+ sink.output(chunk);
3974
+ }
3975
+ });
3802
3976
  pty.onExit(({ exitCode }) => {
3803
- exited = true;
3804
- data.dispose();
3805
- sink.exit(exitCode);
3977
+ if (entry.generation !== generation) {
3978
+ return;
3979
+ }
3980
+ entry.pty = void 0;
3981
+ this.persist(terminalId, entry);
3982
+ for (const sink of [...entry.sinks]) {
3983
+ sink.exit(exitCode);
3984
+ }
3806
3985
  });
3807
- return {
3808
- write: (input) => {
3809
- if (!exited) {
3810
- pty.write(input);
3811
- }
3812
- },
3813
- resize: (cols, rows) => {
3814
- if (!exited) {
3815
- pty.resize(cols, rows);
3816
- }
3817
- },
3818
- kill: () => {
3819
- if (exited) {
3820
- return;
3821
- }
3822
- exited = true;
3823
- pty.kill();
3824
- const timer = setTimeout(() => pty.kill("SIGKILL"), SIGKILL_AFTER_MS);
3825
- timer.unref?.();
3986
+ }
3987
+ resize(entry, cols, rows) {
3988
+ try {
3989
+ entry.pty?.resize(cols, rows);
3990
+ } catch {
3991
+ }
3992
+ }
3993
+ detach(terminalId, entry, sink) {
3994
+ entry.sinks.delete(sink);
3995
+ if (entry.sinks.size > 0) {
3996
+ return;
3997
+ }
3998
+ this.persist(terminalId, entry);
3999
+ this.scheduleIdle(terminalId, entry);
4000
+ }
4001
+ /**
4002
+ * Reclaims a shell nobody has reattached to. The log stays (the output should
4003
+ * still be there next time); only the process goes.
4004
+ */
4005
+ scheduleIdle(terminalId, entry) {
4006
+ if (this.config.terminalIdleMs <= 0) {
4007
+ return;
4008
+ }
4009
+ this.cancelIdle(entry);
4010
+ entry.idleTimer = setTimeout(() => {
4011
+ entry.idleTimer = void 0;
4012
+ if (entry.sinks.size > 0) {
4013
+ return;
3826
4014
  }
3827
- };
4015
+ entry.generation += 1;
4016
+ this.killPty(entry.pty);
4017
+ entry.pty = void 0;
4018
+ this.persist(terminalId, entry);
4019
+ this.entries.delete(terminalId);
4020
+ }, this.config.terminalIdleMs);
4021
+ entry.idleTimer.unref?.();
4022
+ }
4023
+ cancelIdle(entry) {
4024
+ if (entry.idleTimer !== void 0) {
4025
+ clearTimeout(entry.idleTimer);
4026
+ entry.idleTimer = void 0;
4027
+ }
4028
+ }
4029
+ scheduleFlush(terminalId, entry) {
4030
+ if (entry.flushTimer !== void 0) {
4031
+ return;
4032
+ }
4033
+ entry.flushTimer = setTimeout(() => {
4034
+ entry.flushTimer = void 0;
4035
+ this.persist(terminalId, entry);
4036
+ }, FLUSH_DEBOUNCE_MS);
4037
+ entry.flushTimer.unref?.();
4038
+ }
4039
+ stopFlush(entry) {
4040
+ if (entry.flushTimer !== void 0) {
4041
+ clearTimeout(entry.flushTimer);
4042
+ entry.flushTimer = void 0;
4043
+ }
4044
+ }
4045
+ persist(terminalId, entry) {
4046
+ this.stopFlush(entry);
4047
+ if (!entry.dirty) {
4048
+ return;
4049
+ }
4050
+ entry.dirty = false;
4051
+ writeTerminalLog(this.config.dataDir, terminalId, entry.output);
3828
4052
  }
3829
4053
  };
3830
4054
  ServerTerminalBackend = __decorateClass([
3831
4055
  Injectable(),
3832
4056
  __decorateParam(0, Inject2(MORSE_PROJECT_POLICY)),
3833
- __decorateParam(1, Inject2(MORSE_LOGGER))
4057
+ __decorateParam(1, Inject2(MORSE_LOGGER)),
4058
+ __decorateParam(2, Inject2(MORSE_CONFIG))
3834
4059
  ], ServerTerminalBackend);
3835
4060
 
3836
4061
  // ../server/src/internal/registry.ts
@@ -3872,7 +4097,7 @@ import { Inject as Inject4, Injectable as Injectable3 } from "@nestjs/common";
3872
4097
  // ../server/src/internal/uploads/upload-store.ts
3873
4098
  import { existsSync } from "node:fs";
3874
4099
  import { mkdir, writeFile } from "node:fs/promises";
3875
- import { basename as basename2, isAbsolute as isAbsolute3, join as join5, relative, sep as sep3 } from "node:path";
4100
+ import { basename as basename2, isAbsolute as isAbsolute3, join as join6, relative, sep as sep3 } from "node:path";
3876
4101
  var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
3877
4102
  var DEFAULT_UPLOAD_DIR = ".morse/uploads";
3878
4103
  var UploadError = class extends Error {
@@ -3892,7 +4117,7 @@ async function saveUpload(cwd, request, uploadDir = DEFAULT_UPLOAD_DIR) {
3892
4117
  `That file is ${formatMb(bytes.length)} MB; the limit is ${formatMb(MAX_UPLOAD_BYTES)} MB.`
3893
4118
  );
3894
4119
  }
3895
- const directory = isAbsolute3(uploadDir) ? uploadDir : join5(cwd, uploadDir);
4120
+ const directory = isAbsolute3(uploadDir) ? uploadDir : join6(cwd, uploadDir);
3896
4121
  await mkdir(directory, { recursive: true });
3897
4122
  const target = uniquePath(directory, name);
3898
4123
  await writeFile(target, bytes);
@@ -3911,10 +4136,10 @@ function uniquePath(directory, name) {
3911
4136
  const dot = name.lastIndexOf(".");
3912
4137
  const stem = dot > 0 ? name.slice(0, dot) : name;
3913
4138
  const extension = dot > 0 ? name.slice(dot) : "";
3914
- let candidate = join5(directory, name);
4139
+ let candidate = join6(directory, name);
3915
4140
  let suffix = 1;
3916
4141
  while (existsSync(candidate)) {
3917
- candidate = join5(directory, `${stem}-${suffix}${extension}`);
4142
+ candidate = join6(directory, `${stem}-${suffix}${extension}`);
3918
4143
  suffix += 1;
3919
4144
  }
3920
4145
  return candidate;
@@ -3931,7 +4156,7 @@ function formatMb(bytes) {
3931
4156
  // ../server/src/internal/workspace/directory-browser.ts
3932
4157
  import { readdir as readdir3, stat as stat3 } from "node:fs/promises";
3933
4158
  import { homedir as homedir3 } from "node:os";
3934
- import { basename as basename3, dirname, isAbsolute as isAbsolute4, join as join6, resolve as resolve4 } from "node:path";
4159
+ import { basename as basename3, dirname, isAbsolute as isAbsolute4, join as join7, resolve as resolve4 } from "node:path";
3935
4160
  var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
3936
4161
  "node_modules",
3937
4162
  "dist",
@@ -3955,7 +4180,7 @@ async function browseDirectory(path, options) {
3955
4180
  throw new Error(`A folder path must be absolute: ${requested}`);
3956
4181
  }
3957
4182
  const entries = await readdir3(start, { withFileTypes: true });
3958
- const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path: join6(start, entry.name) })).sort((left, right) => left.name.localeCompare(right.name));
4183
+ const directories = entries.filter((entry) => entry.isDirectory()).filter((entry) => !shouldSkip(entry.name)).map((entry) => ({ name: entry.name, path: join7(start, entry.name) })).sort((left, right) => left.name.localeCompare(right.name));
3959
4184
  const parent = dirname(start);
3960
4185
  return {
3961
4186
  path: start,
@@ -3970,7 +4195,7 @@ function shouldSkip(name) {
3970
4195
  }
3971
4196
  async function isGitRepo(path) {
3972
4197
  try {
3973
- await stat3(join6(path, ".git"));
4198
+ await stat3(join7(path, ".git"));
3974
4199
  return true;
3975
4200
  } catch {
3976
4201
  return false;
@@ -4384,7 +4609,7 @@ function gitExec(args, cwd, timeout = 5e3) {
4384
4609
  // ../server/src/internal/workspace/workspace-index.ts
4385
4610
  import { execFile as execFile2 } from "node:child_process";
4386
4611
  import { readdir as readdir4 } from "node:fs/promises";
4387
- import { join as join7, relative as relative4 } from "node:path";
4612
+ import { join as join8, relative as relative4 } from "node:path";
4388
4613
  var SKIPPED_DIRECTORIES2 = /* @__PURE__ */ new Set([
4389
4614
  "node_modules",
4390
4615
  "dist",
@@ -4495,7 +4720,7 @@ async function walk(root, directory, files) {
4495
4720
  if (files.length >= MAX_INDEXED_FILES) {
4496
4721
  return;
4497
4722
  }
4498
- const full = join7(directory, entry.name);
4723
+ const full = join8(directory, entry.name);
4499
4724
  if (entry.isDirectory()) {
4500
4725
  if (SKIPPED_DIRECTORIES2.has(entry.name) || entry.name.startsWith(".")) {
4501
4726
  continue;
@@ -4513,20 +4738,20 @@ function toPosix3(path) {
4513
4738
  }
4514
4739
 
4515
4740
  // ../server/src/internal/workspace/workbench-store.ts
4516
- import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
4517
- import { join as join8 } from "node:path";
4741
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
4742
+ import { join as join9 } from "node:path";
4518
4743
  var LAYOUT_FILE = "workbench.json";
4519
4744
  var DRAFTS_FILE = "drafts.json";
4520
4745
  var LAYOUT_MAX_BYTES = 256 * 1024;
4521
4746
  var DRAFTS_MAX_BYTES = 32 * 1024 * 1024;
4522
4747
  function readWorkbench(dataDir) {
4523
- return readSlot(join8(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
4748
+ return readSlot(join9(dataDir, LAYOUT_FILE), LAYOUT_MAX_BYTES);
4524
4749
  }
4525
4750
  function saveWorkbench(dataDir, snapshot) {
4526
4751
  return saveSlot(dataDir, LAYOUT_FILE, snapshot, LAYOUT_MAX_BYTES);
4527
4752
  }
4528
4753
  function readDrafts(dataDir) {
4529
- return readSlot(join8(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
4754
+ return readSlot(join9(dataDir, DRAFTS_FILE), DRAFTS_MAX_BYTES);
4530
4755
  }
4531
4756
  function saveDrafts(dataDir, snapshot) {
4532
4757
  return saveSlot(dataDir, DRAFTS_FILE, snapshot, DRAFTS_MAX_BYTES);
@@ -4534,7 +4759,7 @@ function saveDrafts(dataDir, snapshot) {
4534
4759
  function readSlot(path, maxBytes) {
4535
4760
  let raw;
4536
4761
  try {
4537
- raw = readFileSync(path, "utf8");
4762
+ raw = readFileSync2(path, "utf8");
4538
4763
  } catch {
4539
4764
  return void 0;
4540
4765
  }
@@ -4558,19 +4783,19 @@ function saveSlot(dataDir, file, snapshot, maxBytes) {
4558
4783
  if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
4559
4784
  throw new UnsupportedByHostError(`That ${file} snapshot is too large to save.`);
4560
4785
  }
4561
- const target = join8(dataDir, file);
4786
+ const target = join9(dataDir, file);
4562
4787
  const temp = `${target}.${process.pid}.tmp`;
4563
4788
  try {
4564
- mkdirSync(dataDir, { recursive: true });
4565
- writeFileSync(temp, serialized);
4566
- renameSync(temp, target);
4789
+ mkdirSync2(dataDir, { recursive: true });
4790
+ writeFileSync2(temp, serialized);
4791
+ renameSync2(temp, target);
4567
4792
  return true;
4568
4793
  } catch {
4569
4794
  try {
4570
- writeFileSync(target, serialized);
4795
+ writeFileSync2(target, serialized);
4571
4796
  return true;
4572
4797
  } catch {
4573
- rmSync(temp, { force: true });
4798
+ rmSync2(temp, { force: true });
4574
4799
  return false;
4575
4800
  }
4576
4801
  }
@@ -4892,19 +5117,19 @@ MorseGateway = __decorateClass([
4892
5117
  ], MorseGateway);
4893
5118
 
4894
5119
  // ../server/src/app/config.ts
4895
- import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute6, join as join9 } from "node:path";
4896
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
5120
+ import { basename as basename4, dirname as dirname2, isAbsolute as isAbsolute6, join as join10 } from "node:path";
5121
+ import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
4897
5122
  import { homedir as homedir4 } from "node:os";
4898
5123
  import { fileURLToPath } from "node:url";
4899
5124
  function loadConfig(env = process.env) {
4900
5125
  const workspacePath = resolveWorkspacePath(env);
4901
5126
  const configuredUiDir = env.MORSE_UI_DIR;
4902
5127
  const moduleDir = dirname2(fileURLToPath(import.meta.url));
4903
- const bundledUiDir = join9(moduleDir, "ui");
4904
- const monorepoUiDir = join9(moduleDir, "..", "..", "..", "ui-angular", "dist");
4905
- const defaultUiDir = existsSync2(join9(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
5128
+ const bundledUiDir = join10(moduleDir, "ui");
5129
+ const monorepoUiDir = join10(moduleDir, "..", "..", "..", "ui-angular", "dist");
5130
+ const defaultUiDir = existsSync2(join10(bundledUiDir, "index.html")) ? bundledUiDir : monorepoUiDir;
4906
5131
  const defaultHost = "127.0.0.1";
4907
- const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join9(process.cwd(), configuredUiDir) : defaultUiDir;
5132
+ const uiDir = configuredUiDir ? isAbsolute6(configuredUiDir) ? configuredUiDir : join10(process.cwd(), configuredUiDir) : defaultUiDir;
4908
5133
  return {
4909
5134
  host: env.MORSE_HOST ?? defaultHost,
4910
5135
  port: parsePositiveInt(env.MORSE_PORT, 4399),
@@ -4920,6 +5145,7 @@ function loadConfig(env = process.env) {
4920
5145
  projects: parseList(env.MORSE_PROJECTS),
4921
5146
  uploadDir: env.MORSE_UPLOAD_DIR?.trim() || DEFAULT_UPLOAD_DIR,
4922
5147
  dataDir: resolveDataDir(env),
5148
+ terminalIdleMs: parsePositiveInt(env.MORSE_TERMINAL_IDLE_MS, 30 * 6e4),
4923
5149
  updateCheck: !isOff(env.MORSE_UPDATE_CHECK),
4924
5150
  instance: env.MORSE_INSTANCE?.trim() || void 0
4925
5151
  };
@@ -4927,7 +5153,7 @@ function loadConfig(env = process.env) {
4927
5153
  function readFrontendIdentity(uiDir) {
4928
5154
  try {
4929
5155
  return frontendIdentity(
4930
- parseFrontendManifest(readFileSync2(join9(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
5156
+ parseFrontendManifest(readFileSync3(join10(uiDir, FRONTEND_MANIFEST_FILE), "utf8"))
4931
5157
  );
4932
5158
  } catch {
4933
5159
  return void 0;
@@ -4935,7 +5161,7 @@ function readFrontendIdentity(uiDir) {
4935
5161
  }
4936
5162
  function resolveDataDir(env) {
4937
5163
  const explicit = env.MORSE_HOME?.trim();
4938
- return explicit && explicit.length > 0 ? explicit : join9(homedir4(), ".morse");
5164
+ return explicit && explicit.length > 0 ? explicit : join10(homedir4(), ".morse");
4939
5165
  }
4940
5166
  function parsePositiveInt(value, fallback) {
4941
5167
  if (!value) {
@@ -4962,7 +5188,7 @@ function resolveWorkspacePath(env) {
4962
5188
  const start = process.cwd();
4963
5189
  let directory = start;
4964
5190
  for (; ; ) {
4965
- const manifest = join9(directory, "package.json");
5191
+ const manifest = join10(directory, "package.json");
4966
5192
  if (existsSync2(manifest) && declaresWorkspaces(manifest)) {
4967
5193
  return directory;
4968
5194
  }
@@ -4975,7 +5201,7 @@ function resolveWorkspacePath(env) {
4975
5201
  }
4976
5202
  function declaresWorkspaces(manifestPath) {
4977
5203
  try {
4978
- const manifest = JSON.parse(readFileSync2(manifestPath, "utf8"));
5204
+ const manifest = JSON.parse(readFileSync3(manifestPath, "utf8"));
4979
5205
  return manifest.workspaces !== void 0;
4980
5206
  } catch {
4981
5207
  return false;