@particle-academy/fancy-term-host 0.2.3 → 0.3.1

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.
@@ -0,0 +1,167 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawnSync } from 'child_process';
4
+
5
+ // src/electron/after-pack.ts
6
+ function nodeAfterPackIo() {
7
+ return {
8
+ exists: (p) => {
9
+ try {
10
+ return fs.existsSync(p);
11
+ } catch {
12
+ return false;
13
+ }
14
+ },
15
+ readdir: (d) => {
16
+ try {
17
+ return fs.readdirSync(d);
18
+ } catch {
19
+ return [];
20
+ }
21
+ },
22
+ mkdirp: (d) => {
23
+ fs.mkdirSync(d, { recursive: true });
24
+ },
25
+ copyFile: (s, d) => {
26
+ fs.copyFileSync(s, d);
27
+ },
28
+ chmodExec: (p) => {
29
+ fs.chmodSync(p, 493);
30
+ },
31
+ run: (cmd, args) => {
32
+ const r = spawnSync(cmd, args, { encoding: "utf8" });
33
+ return {
34
+ code: r.status ?? -1,
35
+ stderr: (r.stderr ?? "") + (r.error ? String(r.error) : "")
36
+ };
37
+ }
38
+ };
39
+ }
40
+ function resourcesDirs(appOutDir, platform, io) {
41
+ if (platform === "darwin") {
42
+ return io.readdir(appOutDir).filter((n) => n.endsWith(".app")).map((a) => path.join(appOutDir, a, "Contents", "Resources"));
43
+ }
44
+ return [path.join(appOutDir, "resources")];
45
+ }
46
+ function nodePtyCandidates(resources) {
47
+ return [
48
+ path.join(resources, "app.asar.unpacked", "node_modules", "node-pty"),
49
+ path.join(resources, "app", "node_modules", "node-pty")
50
+ ];
51
+ }
52
+ function resolveNodePtyDir(appOutDir, platform, io) {
53
+ for (const res of resourcesDirs(appOutDir, platform, io)) {
54
+ for (const dir of nodePtyCandidates(res)) {
55
+ if (io.exists(dir)) return dir;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ var ARCH_ENUM_NAMES = {
61
+ 0: "ia32",
62
+ 1: "x64",
63
+ 2: "armv7l",
64
+ 3: "arm64",
65
+ 4: "universal"
66
+ };
67
+ function targetArchName(context, opts) {
68
+ if (opts.arch) return opts.arch;
69
+ const a = context.arch;
70
+ if (typeof a === "string") return a;
71
+ if (typeof a === "number") return ARCH_ENUM_NAMES[a];
72
+ return void 0;
73
+ }
74
+ function findConptySource(nodePty, io, preferDir) {
75
+ const prebuilds = path.join(nodePty, "prebuilds");
76
+ if (preferDir) {
77
+ const c = path.join(prebuilds, preferDir, "conpty");
78
+ if (io.exists(path.join(c, "conpty.dll"))) return c;
79
+ }
80
+ for (const name of io.readdir(prebuilds)) {
81
+ const c = path.join(prebuilds, name, "conpty");
82
+ if (io.exists(path.join(c, "conpty.dll"))) return c;
83
+ }
84
+ const tp = path.join(nodePty, "third_party", "conpty");
85
+ if (io.exists(path.join(tp, "conpty.dll"))) return tp;
86
+ return null;
87
+ }
88
+ var CONPTY_ASSETS = ["conpty.dll", "OpenConsole.exe"];
89
+ function winFixConpty(nodePty, release, io, select = {}) {
90
+ const destDir = path.join(release, "conpty");
91
+ if (CONPTY_ASSETS.every((f) => io.exists(path.join(destDir, f)))) {
92
+ return { platform: "win32", action: "already-present", ok: true };
93
+ }
94
+ const src = select.conptySource ?? findConptySource(nodePty, io, select.archDir);
95
+ if (!src) {
96
+ return {
97
+ platform: "win32",
98
+ action: "failed",
99
+ ok: false,
100
+ detail: "conpty source not found (prebuilds/*/conpty or third_party/conpty)"
101
+ };
102
+ }
103
+ io.mkdirp(destDir);
104
+ const copied = [];
105
+ for (const f of CONPTY_ASSETS) {
106
+ const s = path.join(src, f);
107
+ if (io.exists(s)) {
108
+ io.copyFile(s, path.join(destDir, f));
109
+ copied.push(f);
110
+ }
111
+ }
112
+ const ok = CONPTY_ASSETS.every((f) => io.exists(path.join(destDir, f)));
113
+ return {
114
+ platform: "win32",
115
+ action: ok ? "fixed" : "failed",
116
+ ok,
117
+ detail: `copied [${copied.join(", ")}] into build/Release/conpty from ${src}`
118
+ };
119
+ }
120
+ function macSignSpawnHelper(release, io) {
121
+ const helper = path.join(release, "spawn-helper");
122
+ if (!io.exists(helper)) {
123
+ return { platform: "darwin", action: "skipped", ok: true, detail: "spawn-helper not present" };
124
+ }
125
+ const { code, stderr } = io.run("codesign", ["--force", "--sign", "-", helper]);
126
+ return code === 0 ? { platform: "darwin", action: "fixed", ok: true, detail: "ad-hoc signed spawn-helper" } : { platform: "darwin", action: "failed", ok: false, detail: `codesign failed: ${stderr.trim()}` };
127
+ }
128
+ function linuxChmodSpawnHelper(release, io) {
129
+ const helper = path.join(release, "spawn-helper");
130
+ if (!io.exists(helper)) {
131
+ return { platform: "linux", action: "skipped", ok: true, detail: "spawn-helper not present" };
132
+ }
133
+ io.chmodExec(helper);
134
+ return { platform: "linux", action: "fixed", ok: true, detail: "chmod +x spawn-helper" };
135
+ }
136
+ async function fancyTermAfterPack(context, opts = {}, io = nodeAfterPackIo()) {
137
+ const platform = opts.platform ?? context.electronPlatformName;
138
+ const nodePty = opts.nodePtyDir ?? resolveNodePtyDir(context.appOutDir, platform, io);
139
+ if (!nodePty) {
140
+ return {
141
+ platform,
142
+ action: "skipped",
143
+ ok: false,
144
+ detail: "node-pty not found under appOutDir (adjust asarUnpack or pass nodePtyDir)"
145
+ };
146
+ }
147
+ const release = path.join(nodePty, "build", "Release");
148
+ switch (platform) {
149
+ case "win32": {
150
+ const archName = targetArchName(context, opts);
151
+ return winFixConpty(nodePty, release, io, {
152
+ archDir: archName ? `${platform}-${archName}` : null,
153
+ conptySource: opts.conptySource
154
+ });
155
+ }
156
+ case "darwin":
157
+ return macSignSpawnHelper(release, io);
158
+ case "linux":
159
+ return linuxChmodSpawnHelper(release, io);
160
+ default:
161
+ return { platform, action: "skipped", ok: true, detail: "no packaging fix needed" };
162
+ }
163
+ }
164
+
165
+ export { fancyTermAfterPack, nodeAfterPackIo, resolveNodePtyDir };
166
+ //# sourceMappingURL=electron.js.map
167
+ //# sourceMappingURL=electron.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/electron/after-pack.ts"],"names":[],"mappings":"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","file":"electron.js","sourcesContent":["/**\n * electron-builder `afterPack` helper (Tier 0 — packaging).\n *\n * fancy-term-host wraps `node-pty`, a NATIVE addon. `electron-builder`\n * (via `install-app-deps`) rebuilds it, but the rebuild output is left\n * unusable per-OS, so a standard packaged app spawns NO terminal until each\n * consumer discovers + patches it. This helper performs the three known fixes\n * in one call so consumers don't have to hand-roll them (#7):\n *\n * • **Windows** — node-pty's `conpty.node` `LoadLibrary`s a `conpty.dll` from a\n * `build/Release/conpty/` SUBDIR that the rebuild doesn't create; the dll +\n * `OpenConsole.exe` ship only under `prebuilds/<plat>/conpty/` (or\n * `third_party/conpty/`). Runtime error otherwise:\n * `Cannot find conpty.dll at .../build/Release/conpty/conpty.dll`. We copy\n * them into place.\n * • **macOS** — node-pty's `spawn-helper` ships UNSIGNED, so Apple Silicon\n * SIGKILLs it on exec (all arm64 code must be at least ad-hoc signed) and the\n * shell child never starts. We ad-hoc sign it (`codesign --force --sign -`).\n * • **Linux** — `spawn-helper` must stay executable after packaging. We\n * `chmod +x` it.\n *\n * Usage (electron-builder config):\n * ```js\n * const { fancyTermAfterPack } = require('@particle-academy/fancy-term-host/electron');\n * module.exports = { afterPack: (context) => fancyTermAfterPack(context) };\n * ```\n *\n * The IO is injected (see {@link AfterPackIo}) so the decision logic is unit\n * testable without touching the filesystem or spawning `codesign`.\n */\n\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport { spawnSync } from 'node:child_process';\n\n/** The subset of electron-builder's AfterPackContext this helper reads. */\nexport interface AfterPackContext {\n /** The per-OS output dir being packed (e.g. `dist/mac-arm64`, `dist/win-unpacked`). */\n appOutDir: string;\n /** `'darwin' | 'win32' | 'linux'` — the platform being packed. */\n electronPlatformName: string;\n /**\n * The target architecture being packed — electron-builder's `Arch` enum\n * (`ia32=0, x64=1, armv7l=2, arm64=3, universal=4`). Used to pick the\n * matching `conpty.dll` prebuild on Windows (#9). A string arch name\n * (`'x64'`, `'arm64'`, …) is also accepted.\n */\n arch?: number | string;\n}\n\nexport interface AfterPackOptions {\n /** Override the platform (defaults to `context.electronPlatformName`). */\n platform?: string;\n /** Explicit node-pty package dir; skips the search under `appOutDir`. */\n nodePtyDir?: string;\n /**\n * Override the target arch name (`'x64'`, `'arm64'`, `'ia32'`, …) used to\n * select the Windows `conpty` prebuild. Defaults to `context.arch`.\n */\n arch?: string;\n /**\n * Pin the conpty source dir outright, skipping prebuild discovery. Use when\n * your `conpty.dll` + `OpenConsole.exe` live somewhere non-standard.\n */\n conptySource?: string;\n}\n\nexport type AfterPackAction = 'fixed' | 'already-present' | 'skipped' | 'failed';\n\nexport interface AfterPackResult {\n platform: string;\n action: AfterPackAction;\n /** True when the terminal should now work (fixed / already-ok / nothing-to-do). */\n ok: boolean;\n detail?: string;\n}\n\n/** Injected IO so the orchestration is unit-testable. `nodeAfterPackIo()` is prod. */\nexport interface AfterPackIo {\n exists(p: string): boolean;\n /** Directory entries, or [] on any failure. */\n readdir(dir: string): string[];\n mkdirp(dir: string): void;\n copyFile(src: string, dest: string): void;\n /** Make a file executable (0o755). */\n chmodExec(p: string): void;\n /** Run a command; resolves the exit code + captured stderr. */\n run(cmd: string, args: string[]): { code: number; stderr: string };\n}\n\n/** Production {@link AfterPackIo}: real fs + child_process. */\nexport function nodeAfterPackIo(): AfterPackIo {\n return {\n exists: (p) => {\n try {\n return fs.existsSync(p);\n } catch {\n return false;\n }\n },\n readdir: (d) => {\n try {\n return fs.readdirSync(d);\n } catch {\n return [];\n }\n },\n mkdirp: (d) => {\n fs.mkdirSync(d, { recursive: true });\n },\n copyFile: (s, d) => {\n fs.copyFileSync(s, d);\n },\n chmodExec: (p) => {\n fs.chmodSync(p, 0o755);\n },\n run: (cmd, args) => {\n const r = spawnSync(cmd, args, { encoding: 'utf8' });\n return {\n code: r.status ?? -1,\n stderr: (r.stderr ?? '') + (r.error ? String(r.error) : ''),\n };\n },\n };\n}\n\n/** Resources dir(s) inside a packed app where `app.asar.unpacked` lives. */\nfunction resourcesDirs(appOutDir: string, platform: string, io: AfterPackIo): string[] {\n if (platform === 'darwin') {\n // <appOutDir>/<Product>.app/Contents/Resources — the .app name varies.\n return io\n .readdir(appOutDir)\n .filter((n) => n.endsWith('.app'))\n .map((a) => path.join(appOutDir, a, 'Contents', 'Resources'));\n }\n // Windows / Linux: <appOutDir>/resources\n return [path.join(appOutDir, 'resources')];\n}\n\n/** Candidate node-pty package dirs under a resources dir (asar-unpacked or plain). */\nfunction nodePtyCandidates(resources: string): string[] {\n return [\n path.join(resources, 'app.asar.unpacked', 'node_modules', 'node-pty'),\n path.join(resources, 'app', 'node_modules', 'node-pty'),\n ];\n}\n\n/** Locate the packaged node-pty dir under `appOutDir`, or null. */\nexport function resolveNodePtyDir(\n appOutDir: string,\n platform: string,\n io: AfterPackIo,\n): string | null {\n for (const res of resourcesDirs(appOutDir, platform, io)) {\n for (const dir of nodePtyCandidates(res)) {\n if (io.exists(dir)) return dir;\n }\n }\n return null;\n}\n\n/** electron-builder `Arch` enum → Node/node-pty arch name (used in prebuild dirs). */\nconst ARCH_ENUM_NAMES: Record<number, string> = {\n 0: 'ia32',\n 1: 'x64',\n 2: 'armv7l',\n 3: 'arm64',\n 4: 'universal',\n};\n\n/** The target arch name to match a prebuild against: opts override → context.arch. */\nfunction targetArchName(context: AfterPackContext, opts: AfterPackOptions): string | undefined {\n if (opts.arch) return opts.arch;\n const a = context.arch;\n if (typeof a === 'string') return a;\n if (typeof a === 'number') return ARCH_ENUM_NAMES[a];\n return undefined;\n}\n\n/**\n * Find the conpty asset source dir (`prebuilds/<plat>-<arch>/conpty` or\n * `third_party/conpty`).\n *\n * When `preferDir` is given (e.g. `win32-x64`), that arch-specific prebuild wins\n * — node-pty ships a `conpty.dll` per arch (distinct machine code, NOT\n * interchangeable), and an x64 app given the arm64 dll can't `LoadLibrary` it,\n * so no terminal spawns (#9). We fall back to the first prebuild carrying a\n * `conpty.dll` only for single-arch installs or when the target arch's prebuild\n * is genuinely absent.\n */\nfunction findConptySource(nodePty: string, io: AfterPackIo, preferDir?: string | null): string | null {\n const prebuilds = path.join(nodePty, 'prebuilds');\n if (preferDir) {\n const c = path.join(prebuilds, preferDir, 'conpty');\n if (io.exists(path.join(c, 'conpty.dll'))) return c;\n }\n for (const name of io.readdir(prebuilds)) {\n const c = path.join(prebuilds, name, 'conpty');\n if (io.exists(path.join(c, 'conpty.dll'))) return c;\n }\n const tp = path.join(nodePty, 'third_party', 'conpty');\n if (io.exists(path.join(tp, 'conpty.dll'))) return tp;\n return null;\n}\n\nconst CONPTY_ASSETS = ['conpty.dll', 'OpenConsole.exe'];\n\nfunction winFixConpty(\n nodePty: string,\n release: string,\n io: AfterPackIo,\n select: { archDir?: string | null; conptySource?: string | null } = {},\n): AfterPackResult {\n const destDir = path.join(release, 'conpty');\n if (CONPTY_ASSETS.every((f) => io.exists(path.join(destDir, f)))) {\n return { platform: 'win32', action: 'already-present', ok: true };\n }\n const src = select.conptySource ?? findConptySource(nodePty, io, select.archDir);\n if (!src) {\n return {\n platform: 'win32',\n action: 'failed',\n ok: false,\n detail: 'conpty source not found (prebuilds/*/conpty or third_party/conpty)',\n };\n }\n io.mkdirp(destDir);\n const copied: string[] = [];\n for (const f of CONPTY_ASSETS) {\n const s = path.join(src, f);\n if (io.exists(s)) {\n io.copyFile(s, path.join(destDir, f));\n copied.push(f);\n }\n }\n const ok = CONPTY_ASSETS.every((f) => io.exists(path.join(destDir, f)));\n return {\n platform: 'win32',\n action: ok ? 'fixed' : 'failed',\n ok,\n detail: `copied [${copied.join(', ')}] into build/Release/conpty from ${src}`,\n };\n}\n\nfunction macSignSpawnHelper(release: string, io: AfterPackIo): AfterPackResult {\n const helper = path.join(release, 'spawn-helper');\n if (!io.exists(helper)) {\n return { platform: 'darwin', action: 'skipped', ok: true, detail: 'spawn-helper not present' };\n }\n const { code, stderr } = io.run('codesign', ['--force', '--sign', '-', helper]);\n return code === 0\n ? { platform: 'darwin', action: 'fixed', ok: true, detail: 'ad-hoc signed spawn-helper' }\n : { platform: 'darwin', action: 'failed', ok: false, detail: `codesign failed: ${stderr.trim()}` };\n}\n\nfunction linuxChmodSpawnHelper(release: string, io: AfterPackIo): AfterPackResult {\n const helper = path.join(release, 'spawn-helper');\n if (!io.exists(helper)) {\n return { platform: 'linux', action: 'skipped', ok: true, detail: 'spawn-helper not present' };\n }\n io.chmodExec(helper);\n return { platform: 'linux', action: 'fixed', ok: true, detail: 'chmod +x spawn-helper' };\n}\n\n/**\n * Make a packaged node-pty spawn a working terminal on the platform being packed.\n * Call from electron-builder's `afterPack`. NEVER throws for the expected\n * outcomes; returns a result describing what it did (inspect `ok`). Idempotent —\n * safe to run on every build.\n */\nexport async function fancyTermAfterPack(\n context: AfterPackContext,\n opts: AfterPackOptions = {},\n io: AfterPackIo = nodeAfterPackIo(),\n): Promise<AfterPackResult> {\n const platform = opts.platform ?? context.electronPlatformName;\n const nodePty = opts.nodePtyDir ?? resolveNodePtyDir(context.appOutDir, platform, io);\n if (!nodePty) {\n return {\n platform,\n action: 'skipped',\n ok: false,\n detail: 'node-pty not found under appOutDir (adjust asarUnpack or pass nodePtyDir)',\n };\n }\n const release = path.join(nodePty, 'build', 'Release');\n switch (platform) {\n case 'win32': {\n const archName = targetArchName(context, opts);\n return winFixConpty(nodePty, release, io, {\n archDir: archName ? `${platform}-${archName}` : null,\n conptySource: opts.conptySource,\n });\n }\n case 'darwin':\n return macSignSpawnHelper(release, io);\n case 'linux':\n return linuxChmodSpawnHelper(release, io);\n default:\n return { platform, action: 'skipped', ok: true, detail: 'no packaging fix needed' };\n }\n}\n"]}
package/dist/index.cjs CHANGED
@@ -6,6 +6,7 @@ var fs2 = require('fs');
6
6
  var os2 = require('os');
7
7
  var path = require('path');
8
8
  var crypto = require('crypto');
9
+ var child_process = require('child_process');
9
10
  var zlib = require('zlib');
10
11
  var net = require('net');
11
12
  var url = require('url');
@@ -155,6 +156,27 @@ function unixCandidates() {
155
156
  }
156
157
  ];
157
158
  }
159
+ function parseDsclUserShell(output) {
160
+ const m = /UserShell:\s*(\S+)/.exec(output);
161
+ return m ? m[1] : null;
162
+ }
163
+ function macUserShell() {
164
+ try {
165
+ const user = os2__default.default.userInfo().username;
166
+ const out = child_process.execFileSync("dscl", [".", "-read", `/Users/${user}`, "UserShell"], {
167
+ encoding: "utf8",
168
+ timeout: 2e3
169
+ });
170
+ return parseDsclUserShell(out);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+ function resolveLoginShell(env = process.env, platform = process.platform, macLookup = macUserShell) {
176
+ if (env.SHELL) return env.SHELL;
177
+ if (platform === "darwin") return macLookup();
178
+ return null;
179
+ }
158
180
  function detectShells() {
159
181
  const candidates = process.platform === "win32" ? windowsCandidates() : unixCandidates();
160
182
  const found = [];
@@ -163,14 +185,18 @@ function detectShells() {
163
185
  if (command) found.push({ id: c.id, label: c.label, command, args: c.args });
164
186
  }
165
187
  if (process.platform !== "win32") {
166
- const login = process.env.SHELL;
167
- if (login && !found.some((s) => s.command === login) && fs2__default.default.existsSync(login)) {
168
- found.unshift({
188
+ const login = resolveLoginShell();
189
+ if (login && fs2__default.default.existsSync(login)) {
190
+ const existing = found.find((s) => s.command === login);
191
+ const rest = found.filter((s) => s.command !== login);
192
+ const head = existing ?? {
169
193
  id: path__default.default.basename(login),
170
194
  label: path__default.default.basename(login),
171
195
  command: login,
172
196
  args: ["-l"]
173
- });
197
+ };
198
+ found.length = 0;
199
+ found.push(head, ...rest);
174
200
  }
175
201
  }
176
202
  return found;
@@ -232,6 +258,12 @@ function cwdHookSpawn(command, settings) {
232
258
  path__default.default.join(dir, ".zshenv"),
233
259
  `# fancy-term-host (generated)
234
260
  [ -f "${orig}/.zshenv" ] && source "${orig}/.zshenv"
261
+ `
262
+ );
263
+ const okProfile = writeShim(
264
+ path__default.default.join(dir, ".zprofile"),
265
+ `# fancy-term-host (generated)
266
+ [ -f "${orig}/.zprofile" ] && source "${orig}/.zprofile"
235
267
  `
236
268
  );
237
269
  const okRc = writeShim(
@@ -244,7 +276,7 @@ typeset -ga precmd_functions
244
276
  precmd_functions+=(__fth_osc7)
245
277
  `
246
278
  );
247
- return okEnv && okRc ? { env: { ZDOTDIR: dir }, args: [] } : empty;
279
+ return okEnv && okProfile && okRc ? { env: { ZDOTDIR: dir }, args: [] } : empty;
248
280
  }
249
281
  if (kind === "fish") {
250
282
  const dir = hookDir();
@@ -1081,6 +1113,23 @@ function isPidAlive(pid) {
1081
1113
  return err.code === "EPERM";
1082
1114
  }
1083
1115
  }
1116
+ function terminateHost(pid, signal = "SIGTERM") {
1117
+ if (!pid || pid <= 0 || pid === process.pid) return false;
1118
+ try {
1119
+ process.kill(pid, signal);
1120
+ return true;
1121
+ } catch {
1122
+ return false;
1123
+ }
1124
+ }
1125
+ async function awaitPidGone(pid, timeoutMs = 2e3) {
1126
+ const deadline = Date.now() + timeoutMs;
1127
+ while (Date.now() < deadline) {
1128
+ if (!isPidAlive(pid)) return true;
1129
+ await new Promise((r) => setTimeout(r, 50));
1130
+ }
1131
+ return !isPidAlive(pid);
1132
+ }
1084
1133
  function pidfileUsable(pf) {
1085
1134
  if (!pf) return false;
1086
1135
  if (pf.protocolVersion !== PROTOCOL_VERSION) return false;
@@ -1141,6 +1190,35 @@ async function awaitUsableHost(userData, timeoutMs = 4e3) {
1141
1190
  }
1142
1191
  return false;
1143
1192
  }
1193
+ async function reapHost(userData, pf) {
1194
+ if (pf && isPidAlive(pf.pid) && terminateHost(pf.pid)) {
1195
+ await awaitPidGone(pf.pid, 2e3);
1196
+ }
1197
+ deletePidfile(userData);
1198
+ }
1199
+ async function connectOrSpawnHost(userData, spawner, snapshots) {
1200
+ let pf = readPidfile(userData);
1201
+ if (pidfileUsable(pf)) {
1202
+ try {
1203
+ return await HostClient.connect(pf.socketPath, snapshots);
1204
+ } catch {
1205
+ await reapHost(userData, pf);
1206
+ pf = null;
1207
+ }
1208
+ } else if (pf) {
1209
+ await reapHost(userData, pf);
1210
+ pf = null;
1211
+ }
1212
+ deletePidfile(userData);
1213
+ const hostScript = spawner.resolveHostScript();
1214
+ if (!hostScript) return null;
1215
+ spawner.spawnDetached(hostScript, { GENIE_USERDATA: userData });
1216
+ const up = await awaitUsableHost(userData);
1217
+ if (!up) return null;
1218
+ pf = readPidfile(userData);
1219
+ if (!pf) return null;
1220
+ return await HostClient.connect(pf.socketPath, snapshots);
1221
+ }
1144
1222
  async function initTerminalBackend() {
1145
1223
  setActiveBackend(inProcessBackend());
1146
1224
  if (!deps || !detachedEnabled()) {
@@ -1149,33 +1227,17 @@ async function initTerminalBackend() {
1149
1227
  const { spawner, snapshots } = deps;
1150
1228
  const userData = spawner.userDataDir();
1151
1229
  try {
1152
- const hostScript = spawner.resolveHostScript();
1153
- let pf = readPidfile(userData);
1154
- if (!pidfileUsable(pf)) {
1155
- deletePidfile(userData);
1156
- if (!hostScript) {
1157
- status(
1158
- "Detached terminals unavailable (host not found) \u2014 using in-process. Sessions won't survive a full quit."
1159
- );
1160
- return { host: false, reattachIds: [] };
1161
- }
1162
- spawner.spawnDetached(hostScript, { GENIE_USERDATA: userData });
1163
- const up = await awaitUsableHost(userData);
1164
- if (!up) {
1165
- status(
1166
- "Detached terminals unavailable (host didn't start) \u2014 using in-process. Sessions won't survive a full quit."
1167
- );
1168
- return { host: false, reattachIds: [] };
1169
- }
1170
- pf = readPidfile(userData);
1171
- }
1172
- if (!pf) {
1230
+ const c = await connectOrSpawnHost(userData, spawner, snapshots);
1231
+ if (!c) {
1232
+ client = null;
1233
+ usingHost = false;
1234
+ setActiveBackend(inProcessBackend());
1173
1235
  status(
1174
1236
  "Detached terminals unavailable \u2014 using in-process. Sessions won't survive a full quit."
1175
1237
  );
1176
1238
  return { host: false, reattachIds: [] };
1177
1239
  }
1178
- client = await HostClient.connect(pf.socketPath, snapshots);
1240
+ client = c;
1179
1241
  client.on("error", onHostError);
1180
1242
  setActiveBackend(client);
1181
1243
  usingHost = true;