@particle-academy/fancy-term-host 0.2.2 → 0.3.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/README.md +17 -0
- package/dist/{chunk-7NXYRMNA.js → chunk-2A4TFTNQ.js} +20 -3
- package/dist/chunk-2A4TFTNQ.js.map +1 -0
- package/dist/{chunk-DB3SIE53.js → chunk-Q4WC2TIE.js} +3 -3
- package/dist/{chunk-DB3SIE53.js.map → chunk-Q4WC2TIE.js.map} +1 -1
- package/dist/electron.cjs +153 -0
- package/dist/electron.cjs.map +1 -0
- package/dist/electron.d.cts +79 -0
- package/dist/electron.d.ts +79 -0
- package/dist/electron.js +144 -0
- package/dist/electron.js.map +1 -0
- package/dist/index.cjs +89 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -3
- package/dist/index.d.ts +4 -3
- package/dist/index.js +75 -30
- package/dist/index.js.map +1 -1
- package/dist/pty-host.cjs +9 -1
- package/dist/pty-host.cjs.map +1 -1
- package/dist/pty-host.js +10 -2
- package/dist/pty-host.js.map +1 -1
- package/dist/service.cjs.map +1 -1
- package/dist/service.js +2 -2
- package/docs/packaging.md +96 -0
- package/package.json +11 -1
- package/dist/chunk-7NXYRMNA.js.map +0 -1
package/README.md
CHANGED
|
@@ -25,6 +25,23 @@ npm install @particle-academy/fancy-term-host node-pty
|
|
|
25
25
|
`node-pty` is a **peer dependency**: you own its native build (and, under
|
|
26
26
|
Electron, the `asar-unpack` so its `.node` binary loads outside the archive).
|
|
27
27
|
|
|
28
|
+
### Packaging into an Electron app
|
|
29
|
+
|
|
30
|
+
A packaged `node-pty` won't spawn a terminal on **any** OS until three per-OS
|
|
31
|
+
rebuild gaps are patched (Windows `conpty.dll` subdir, unsigned macOS
|
|
32
|
+
`spawn-helper`, Linux `+x`). `fancy-term-host` ships the fix as an
|
|
33
|
+
`electron-builder` `afterPack` hook — wire it once:
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
const { fancyTermAfterPack } = require('@particle-academy/fancy-term-host/electron');
|
|
37
|
+
module.exports = {
|
|
38
|
+
asarUnpack: ['**/node_modules/node-pty/**'],
|
|
39
|
+
afterPack: (context) => fancyTermAfterPack(context),
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
See [`docs/packaging.md`](./docs/packaging.md) for the full breakdown.
|
|
44
|
+
|
|
28
45
|
## Wire it up
|
|
29
46
|
|
|
30
47
|
You provide four ports (see [`docs/ports.md`](./docs/ports.md)); the core does
|
|
@@ -94,6 +94,23 @@ function isPidAlive(pid) {
|
|
|
94
94
|
return err.code === "EPERM";
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
function terminateHost(pid, signal = "SIGTERM") {
|
|
98
|
+
if (!pid || pid <= 0 || pid === process.pid) return false;
|
|
99
|
+
try {
|
|
100
|
+
process.kill(pid, signal);
|
|
101
|
+
return true;
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async function awaitPidGone(pid, timeoutMs = 2e3) {
|
|
107
|
+
const deadline = Date.now() + timeoutMs;
|
|
108
|
+
while (Date.now() < deadline) {
|
|
109
|
+
if (!isPidAlive(pid)) return true;
|
|
110
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
111
|
+
}
|
|
112
|
+
return !isPidAlive(pid);
|
|
113
|
+
}
|
|
97
114
|
function pidfileUsable(pf) {
|
|
98
115
|
if (!pf) return false;
|
|
99
116
|
if (pf.protocolVersion !== PROTOCOL_VERSION) return false;
|
|
@@ -122,6 +139,6 @@ function resolveHostScript(dirname) {
|
|
|
122
139
|
return null;
|
|
123
140
|
}
|
|
124
141
|
|
|
125
|
-
export { FrameDecoder, PROTOCOL_VERSION, deletePidfile, encodeFrame, isPidAlive, pidfilePath, pidfileUsable, readPidfile, resolveHostScript, socketPathFor, userHash, writePidfile };
|
|
126
|
-
//# sourceMappingURL=chunk-
|
|
127
|
-
//# sourceMappingURL=chunk-
|
|
142
|
+
export { FrameDecoder, PROTOCOL_VERSION, awaitPidGone, deletePidfile, encodeFrame, isPidAlive, pidfilePath, pidfileUsable, readPidfile, resolveHostScript, socketPathFor, terminateHost, userHash, writePidfile };
|
|
143
|
+
//# sourceMappingURL=chunk-2A4TFTNQ.js.map
|
|
144
|
+
//# sourceMappingURL=chunk-2A4TFTNQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/host-protocol.ts","../src/host-locate.ts"],"names":[],"mappings":"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","file":"chunk-2A4TFTNQ.js","sourcesContent":["/**\n * Pty-host wire protocol (Tier 3).\n *\n * The detached pty-host (main/terminal/pty-host.ts) and the in-app HostClient\n * (main/terminal/host-client.ts) talk over a local IPC transport — a named pipe\n * on Windows, a unix domain socket on POSIX — using a tiny length-prefixed JSON\n * framing so there's no heavy dependency. This module is PURE (no electron, no\n * node-pty, no net): just the message shapes + the encode/decode for the framing,\n * so it can be imported by both ends AND unit-tested in isolation.\n *\n * Framing: each message is `[4-byte big-endian uint32 length][utf8 JSON body]`.\n * The length prefix is the byte length of the JSON body. A FrameDecoder buffers\n * partial reads and yields whole messages as they complete — TCP/pipe streams\n * don't preserve message boundaries, so we can't assume one `data` event == one\n * message.\n */\n\n/**\n * Protocol version. Bumped whenever the message shapes change in a way that\n * makes an old host incompatible with a new client (or vice-versa). The client\n * refuses to attach to a host whose pidfile reports a different version and\n * spawns a fresh host instead — see host-client.ts connect-or-spawn.\n */\nexport const PROTOCOL_VERSION = 2;\n\n/** Requests the client sends to the host. `seq` correlates a reply. */\nexport type ClientMessage =\n | { kind: 'hello'; seq: number; protocolVersion: number }\n | {\n kind: 'create';\n seq: number;\n opts: {\n id: string;\n cwd: string;\n shell?: string;\n args?: string[];\n cols?: number;\n rows?: number;\n env?: Record<string, string>;\n };\n }\n | { kind: 'write'; id: string; data: string }\n | { kind: 'resize'; id: string; cols: number; rows: number }\n | { kind: 'kill'; id: string }\n | { kind: 'list'; seq: number }\n | { kind: 'set-retained'; id: string; retained: boolean }\n | { kind: 'get-scrollback'; seq: number; id: string }\n | { kind: 'ping'; seq: number }\n | { kind: 'shutdown'; seq: number };\n\n/** Pushes + replies the host sends to the client. */\nexport type HostMessage =\n | { kind: 'hello-ok'; seq: number; protocolVersion: number; pid: number }\n | {\n kind: 'created';\n seq: number;\n result: {\n id: string;\n pid: number;\n shell: string;\n existing: boolean;\n scrollback: string;\n };\n }\n | {\n kind: 'list-result';\n seq: number;\n terminals: Array<{ id: string; pid: number; shell: string }>;\n }\n | { kind: 'scrollback-result'; seq: number; scrollback: string | null }\n | { kind: 'pong'; seq: number }\n | { kind: 'shutdown-ok'; seq: number }\n | { kind: 'data'; id: string; data: string }\n | { kind: 'exit'; id: string; exitCode: number; signal?: number };\n\nexport type Frame = ClientMessage | HostMessage;\n\nconst LENGTH_BYTES = 4;\n\n/** Encode a message as a length-prefixed JSON frame ready for the socket. */\nexport function encodeFrame(msg: Frame): Buffer {\n const body = Buffer.from(JSON.stringify(msg), 'utf8');\n const header = Buffer.allocUnsafe(LENGTH_BYTES);\n header.writeUInt32BE(body.length, 0);\n return Buffer.concat([header, body]);\n}\n\n/**\n * Streaming frame decoder. Feed it raw socket chunks via `push`; it returns the\n * complete messages that became available (zero or more), buffering any partial\n * tail until the rest arrives. One decoder per socket.\n *\n * Resilient by design: a malformed JSON body is skipped (the frame is consumed\n * but yields nothing) rather than throwing — a corrupt frame must not wedge the\n * whole stream. An absurd length prefix (> MAX_FRAME) is treated as a desync and\n * the buffer is reset; the caller can decide whether to drop the connection.\n */\nexport class FrameDecoder {\n private buffer: Buffer = Buffer.alloc(0);\n\n /** Hard cap on a single frame (16 MB). Guards against a runaway/garbage\n * length prefix allocating unbounded memory. node-pty data chunks are tiny;\n * a serialized scrollback is bounded well under this. */\n static readonly MAX_FRAME = 16 * 1024 * 1024;\n\n /** True when the last push hit an oversized/desynced frame. The caller\n * should drop the connection — the stream can't be trusted to realign. */\n desynced = false;\n\n push(chunk: Buffer): Frame[] {\n this.buffer = this.buffer.length ? Buffer.concat([this.buffer, chunk]) : chunk;\n const out: Frame[] = [];\n for (;;) {\n if (this.buffer.length < LENGTH_BYTES) break;\n const len = this.buffer.readUInt32BE(0);\n if (len > FrameDecoder.MAX_FRAME) {\n // Desync / garbage. Reset and flag — realigning a length-prefixed\n // stream after a bad prefix isn't possible without a sentinel.\n this.desynced = true;\n this.buffer = Buffer.alloc(0);\n break;\n }\n if (this.buffer.length < LENGTH_BYTES + len) break; // wait for more\n const body = this.buffer.subarray(LENGTH_BYTES, LENGTH_BYTES + len);\n this.buffer = this.buffer.subarray(LENGTH_BYTES + len);\n try {\n out.push(JSON.parse(body.toString('utf8')) as Frame);\n } catch {\n /* skip a corrupt frame; the framing itself is still aligned */\n }\n }\n return out;\n }\n}\n","import path from 'node:path';\nimport os from 'node:os';\nimport fs from 'node:fs';\nimport crypto from 'node:crypto';\nimport { PROTOCOL_VERSION } from './host-protocol';\n\n/**\n * Path + pidfile resolution for the detached pty-host (Tier 3).\n *\n * Kept ELECTRON-FREE on the resolution side that the host itself uses (the host\n * is a plain node process — no `app`), so the userData path is passed IN. The\n * in-app side (host-client lifecycle) imports `app` separately and feeds it here.\n */\n\nexport interface Pidfile {\n pid: number;\n socketPath: string;\n protocolVersion: number;\n startedAt: number;\n}\n\n/** Short, stable per-user hash so two OS users don't collide on the Windows\n * pipe name (the pipe namespace is machine-global). */\nexport function userHash(): string {\n const seed = `${os.userInfo().username}|${os.hostname()}`;\n return crypto.createHash('sha1').update(seed).digest('hex').slice(0, 12);\n}\n\n/**\n * The local IPC transport address.\n * • Windows: a named pipe `\\\\.\\pipe\\genie-ptyhost-<userhash>`. The default\n * Windows pipe ACL is per-logon-session, so another user on the same machine\n * can't open it — that's our ACL. (Documented; we don't tighten further.)\n * • POSIX: a unix domain socket under userData (preferred — survives /tmp\n * cleaners and is per-user by directory perms) named `ptyhost.sock`.\n */\nexport function socketPathFor(userDataDir: string): string {\n if (process.platform === 'win32') {\n return `\\\\\\\\.\\\\pipe\\\\genie-ptyhost-${userHash()}`;\n }\n // Keep the path short — unix socket paths have a ~104-char limit. userData is\n // typically well under that; fall back to os.tmpdir() if it's pathologically\n // long.\n const candidate = path.join(userDataDir, 'ptyhost.sock');\n if (candidate.length < 100) return candidate;\n return path.join(os.tmpdir(), `genie-ptyhost-${userHash()}.sock`);\n}\n\nexport function pidfilePath(userDataDir: string): string {\n return path.join(userDataDir, 'ptyhost.json');\n}\n\nexport function writePidfile(userDataDir: string, pf: Pidfile): void {\n const target = pidfilePath(userDataDir);\n const tmp = `${target}.tmp`;\n fs.writeFileSync(tmp, JSON.stringify(pf));\n fs.renameSync(tmp, target);\n}\n\nexport function readPidfile(userDataDir: string): Pidfile | null {\n try {\n const raw = fs.readFileSync(pidfilePath(userDataDir), 'utf8');\n const pf = JSON.parse(raw) as Pidfile;\n if (\n typeof pf.pid !== 'number' ||\n typeof pf.socketPath !== 'string' ||\n typeof pf.protocolVersion !== 'number'\n ) {\n return null;\n }\n return pf;\n } catch {\n return null;\n }\n}\n\nexport function deletePidfile(userDataDir: string): void {\n try {\n fs.rmSync(pidfilePath(userDataDir), { force: true });\n } catch {\n /* ignore */\n }\n}\n\n/** True when a process with `pid` is alive (signal 0 probes without killing). */\nexport function isPidAlive(pid: number): boolean {\n if (!pid || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = exists but not ours (still \"alive\"); ESRCH = gone.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Terminate a stale/wedged host process so its SINGLE-INSTANCE transport (the\n * Windows named pipe / POSIX socket) frees up for a fresh host to bind. A wedged\n * incumbent that still owns the pipe would otherwise EADDRINUSE every respawn and\n * silently swallow every client `hello` — see the connect-or-spawn recovery in\n * host-lifecycle (#8).\n *\n * Best-effort — never throws. Refuses `pid <= 0` and our OWN pid (so a caller\n * that reads its own pidfile can't self-destruct). On Windows `process.kill`\n * maps to TerminateProcess (immediate); on POSIX the default SIGTERM disposition\n * exits the host promptly (it installs no handler). Returns true when a signal\n * was actually delivered.\n */\nexport function terminateHost(\n pid: number,\n signal: NodeJS.Signals = 'SIGTERM',\n): boolean {\n if (!pid || pid <= 0 || pid === process.pid) return false;\n try {\n process.kill(pid, signal);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve once `pid` is gone (polling up to `timeoutMs`). Called after\n * {@link terminateHost} so a freshly-spawned host doesn't race the reaped one\n * for the single-instance transport. Resolves true when the pid is no longer\n * alive, false if it outlives the timeout.\n */\nexport async function awaitPidGone(pid: number, timeoutMs = 2000): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!isPidAlive(pid)) return true;\n await new Promise((r) => setTimeout(r, 50));\n }\n return !isPidAlive(pid);\n}\n\n/**\n * Decide whether an existing pidfile points at a usable host.\n * Usable = pid alive AND protocol versions match. A stale/dead/mismatched\n * pidfile means we must spawn a fresh host.\n */\nexport function pidfileUsable(pf: Pidfile | null): boolean {\n if (!pf) return false;\n if (pf.protocolVersion !== PROTOCOL_VERSION) return false;\n if (!isPidAlive(pf.pid)) return false;\n return true;\n}\n\n/**\n * Resolve the compiled pty-host script on disk, trying multiple candidate paths\n * so it works in BOTH `npm run dev` (script at app/pty-host.js next to\n * background.js) AND a packaged asar build. node-pty's native binding can't load\n * from inside an asar, so the host (which requires node-pty) must run UNPACKED —\n * `app.asar.unpacked/...`. We try the unpacked path first, then the in-asar path,\n * then a dev-relative path. Returns the first that exists, or null.\n *\n * `dirname` is main/background's __dirname (the directory the compiled main\n * bundle lives in). The host script is emitted alongside it as `pty-host.js`.\n */\nexport function resolveHostScript(dirname: string): string | null {\n const candidates = [\n // Packaged: node-pty must be unpacked, so run the host from the unpacked\n // tree too (its require('node-pty') resolves to the unpacked .node).\n dirname.includes(`app.asar${path.sep}`) || dirname.includes('app.asar/')\n ? dirname.replace(\n /app\\.asar([\\\\/])/,\n `app.asar.unpacked$1`,\n ) + path.sep + 'pty-host.js'\n : '',\n // Same dir as the compiled main bundle (dev: app/pty-host.js).\n path.join(dirname, 'pty-host.js'),\n // Defensive: a sibling unpacked dir computed from the asar path.\n path.join(dirname.replace('app.asar', 'app.asar.unpacked'), 'pty-host.js'),\n ].filter(Boolean);\n\n for (const c of candidates) {\n try {\n if (fs.existsSync(c)) return c;\n } catch {\n /* keep trying */\n }\n }\n return null;\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveHostScript } from './chunk-
|
|
1
|
+
import { resolveHostScript } from './chunk-2A4TFTNQ.js';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
@@ -8,5 +8,5 @@ function ptyHostScriptPath() {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export { ptyHostScriptPath };
|
|
11
|
-
//# sourceMappingURL=chunk-
|
|
12
|
-
//# sourceMappingURL=chunk-
|
|
11
|
+
//# sourceMappingURL=chunk-Q4WC2TIE.js.map
|
|
12
|
+
//# sourceMappingURL=chunk-Q4WC2TIE.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/host-script.ts"],"names":[],"mappings":";;;;AAuBO,SAAS,iBAAA,GAA4B;AACxC,EAAA,MAAM,IAAA,GACF,OAAO,SAAA,KAAc,WAAA,GACf,SAAA,GACA,KAAK,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AAGrD,EAAA,OAAO,kBAAkB,IAAI,CAAA,IAAK,IAAA,CAAK,IAAA,CAAK,MAAM,aAAa,CAAA;AACnE","file":"chunk-
|
|
1
|
+
{"version":3,"sources":["../src/host-script.ts"],"names":[],"mappings":";;;;AAuBO,SAAS,iBAAA,GAA4B;AACxC,EAAA,MAAM,IAAA,GACF,OAAO,SAAA,KAAc,WAAA,GACf,SAAA,GACA,KAAK,OAAA,CAAQ,aAAA,CAAc,MAAA,CAAA,IAAA,CAAY,GAAG,CAAC,CAAA;AAGrD,EAAA,OAAO,kBAAkB,IAAI,CAAA,IAAK,IAAA,CAAK,IAAA,CAAK,MAAM,aAAa,CAAA;AACnE","file":"chunk-Q4WC2TIE.js","sourcesContent":["import path from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { resolveHostScript } from './host-locate';\n\n/**\n * Absolute path to the bundled detached pty-host script (Tier 3), so a\n * `HostSpawner` can launch it as a detached child without knowing this\n * package's dist layout:\n *\n * ```ts\n * import { spawn } from 'node:child_process';\n * import { ptyHostScriptPath } from '@particle-academy/fancy-term-host';\n *\n * const child = spawn(process.execPath, [ptyHostScriptPath(), userDataDir], {\n * detached: true, stdio: 'ignore',\n * });\n * child.unref();\n * ```\n *\n * The host script is emitted alongside this module in `dist/` as\n * `pty-host.js`. Works in both the ESM and CJS builds (esbuild fills in\n * `import.meta.url` for the CJS output; `__dirname` is used when present).\n */\nexport function ptyHostScriptPath(): string {\n const here =\n typeof __dirname !== 'undefined'\n ? __dirname\n : path.dirname(fileURLToPath(import.meta.url));\n // Reuse host-locate's candidate logic (handles dev/packaged layouts); fall\n // back to the expected emitted location if the existence probe misses.\n return resolveHostScript(here) ?? path.join(here, 'pty-host.js');\n}\n"]}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fs = require('fs');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var child_process = require('child_process');
|
|
6
|
+
|
|
7
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
8
|
+
|
|
9
|
+
var fs__default = /*#__PURE__*/_interopDefault(fs);
|
|
10
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
11
|
+
|
|
12
|
+
// src/electron/after-pack.ts
|
|
13
|
+
function nodeAfterPackIo() {
|
|
14
|
+
return {
|
|
15
|
+
exists: (p) => {
|
|
16
|
+
try {
|
|
17
|
+
return fs__default.default.existsSync(p);
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
readdir: (d) => {
|
|
23
|
+
try {
|
|
24
|
+
return fs__default.default.readdirSync(d);
|
|
25
|
+
} catch {
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
mkdirp: (d) => {
|
|
30
|
+
fs__default.default.mkdirSync(d, { recursive: true });
|
|
31
|
+
},
|
|
32
|
+
copyFile: (s, d) => {
|
|
33
|
+
fs__default.default.copyFileSync(s, d);
|
|
34
|
+
},
|
|
35
|
+
chmodExec: (p) => {
|
|
36
|
+
fs__default.default.chmodSync(p, 493);
|
|
37
|
+
},
|
|
38
|
+
run: (cmd, args) => {
|
|
39
|
+
const r = child_process.spawnSync(cmd, args, { encoding: "utf8" });
|
|
40
|
+
return {
|
|
41
|
+
code: r.status ?? -1,
|
|
42
|
+
stderr: (r.stderr ?? "") + (r.error ? String(r.error) : "")
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function resourcesDirs(appOutDir, platform, io) {
|
|
48
|
+
if (platform === "darwin") {
|
|
49
|
+
return io.readdir(appOutDir).filter((n) => n.endsWith(".app")).map((a) => path__default.default.join(appOutDir, a, "Contents", "Resources"));
|
|
50
|
+
}
|
|
51
|
+
return [path__default.default.join(appOutDir, "resources")];
|
|
52
|
+
}
|
|
53
|
+
function nodePtyCandidates(resources) {
|
|
54
|
+
return [
|
|
55
|
+
path__default.default.join(resources, "app.asar.unpacked", "node_modules", "node-pty"),
|
|
56
|
+
path__default.default.join(resources, "app", "node_modules", "node-pty")
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
function resolveNodePtyDir(appOutDir, platform, io) {
|
|
60
|
+
for (const res of resourcesDirs(appOutDir, platform, io)) {
|
|
61
|
+
for (const dir of nodePtyCandidates(res)) {
|
|
62
|
+
if (io.exists(dir)) return dir;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function findConptySource(nodePty, io) {
|
|
68
|
+
const prebuilds = path__default.default.join(nodePty, "prebuilds");
|
|
69
|
+
for (const name of io.readdir(prebuilds)) {
|
|
70
|
+
const c = path__default.default.join(prebuilds, name, "conpty");
|
|
71
|
+
if (io.exists(path__default.default.join(c, "conpty.dll"))) return c;
|
|
72
|
+
}
|
|
73
|
+
const tp = path__default.default.join(nodePty, "third_party", "conpty");
|
|
74
|
+
if (io.exists(path__default.default.join(tp, "conpty.dll"))) return tp;
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
var CONPTY_ASSETS = ["conpty.dll", "OpenConsole.exe"];
|
|
78
|
+
function winFixConpty(nodePty, release, io) {
|
|
79
|
+
const destDir = path__default.default.join(release, "conpty");
|
|
80
|
+
if (CONPTY_ASSETS.every((f) => io.exists(path__default.default.join(destDir, f)))) {
|
|
81
|
+
return { platform: "win32", action: "already-present", ok: true };
|
|
82
|
+
}
|
|
83
|
+
const src = findConptySource(nodePty, io);
|
|
84
|
+
if (!src) {
|
|
85
|
+
return {
|
|
86
|
+
platform: "win32",
|
|
87
|
+
action: "failed",
|
|
88
|
+
ok: false,
|
|
89
|
+
detail: "conpty source not found (prebuilds/*/conpty or third_party/conpty)"
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
io.mkdirp(destDir);
|
|
93
|
+
const copied = [];
|
|
94
|
+
for (const f of CONPTY_ASSETS) {
|
|
95
|
+
const s = path__default.default.join(src, f);
|
|
96
|
+
if (io.exists(s)) {
|
|
97
|
+
io.copyFile(s, path__default.default.join(destDir, f));
|
|
98
|
+
copied.push(f);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const ok = CONPTY_ASSETS.every((f) => io.exists(path__default.default.join(destDir, f)));
|
|
102
|
+
return {
|
|
103
|
+
platform: "win32",
|
|
104
|
+
action: ok ? "fixed" : "failed",
|
|
105
|
+
ok,
|
|
106
|
+
detail: `copied [${copied.join(", ")}] into build/Release/conpty from ${src}`
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function macSignSpawnHelper(release, io) {
|
|
110
|
+
const helper = path__default.default.join(release, "spawn-helper");
|
|
111
|
+
if (!io.exists(helper)) {
|
|
112
|
+
return { platform: "darwin", action: "skipped", ok: true, detail: "spawn-helper not present" };
|
|
113
|
+
}
|
|
114
|
+
const { code, stderr } = io.run("codesign", ["--force", "--sign", "-", helper]);
|
|
115
|
+
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()}` };
|
|
116
|
+
}
|
|
117
|
+
function linuxChmodSpawnHelper(release, io) {
|
|
118
|
+
const helper = path__default.default.join(release, "spawn-helper");
|
|
119
|
+
if (!io.exists(helper)) {
|
|
120
|
+
return { platform: "linux", action: "skipped", ok: true, detail: "spawn-helper not present" };
|
|
121
|
+
}
|
|
122
|
+
io.chmodExec(helper);
|
|
123
|
+
return { platform: "linux", action: "fixed", ok: true, detail: "chmod +x spawn-helper" };
|
|
124
|
+
}
|
|
125
|
+
async function fancyTermAfterPack(context, opts = {}, io = nodeAfterPackIo()) {
|
|
126
|
+
const platform = opts.platform ?? context.electronPlatformName;
|
|
127
|
+
const nodePty = opts.nodePtyDir ?? resolveNodePtyDir(context.appOutDir, platform, io);
|
|
128
|
+
if (!nodePty) {
|
|
129
|
+
return {
|
|
130
|
+
platform,
|
|
131
|
+
action: "skipped",
|
|
132
|
+
ok: false,
|
|
133
|
+
detail: "node-pty not found under appOutDir (adjust asarUnpack or pass nodePtyDir)"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
const release = path__default.default.join(nodePty, "build", "Release");
|
|
137
|
+
switch (platform) {
|
|
138
|
+
case "win32":
|
|
139
|
+
return winFixConpty(nodePty, release, io);
|
|
140
|
+
case "darwin":
|
|
141
|
+
return macSignSpawnHelper(release, io);
|
|
142
|
+
case "linux":
|
|
143
|
+
return linuxChmodSpawnHelper(release, io);
|
|
144
|
+
default:
|
|
145
|
+
return { platform, action: "skipped", ok: true, detail: "no packaging fix needed" };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
exports.fancyTermAfterPack = fancyTermAfterPack;
|
|
150
|
+
exports.nodeAfterPackIo = nodeAfterPackIo;
|
|
151
|
+
exports.resolveNodePtyDir = resolveNodePtyDir;
|
|
152
|
+
//# sourceMappingURL=electron.cjs.map
|
|
153
|
+
//# sourceMappingURL=electron.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/electron/after-pack.ts"],"names":["fs","spawnSync","path"],"mappings":"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","file":"electron.cjs","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\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\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/** Find the conpty asset source dir (`prebuilds/<plat>/conpty` or `third_party/conpty`). */\nfunction findConptySource(nodePty: string, io: AfterPackIo): string | null {\n const prebuilds = path.join(nodePty, 'prebuilds');\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(nodePty: string, release: string, io: AfterPackIo): 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 = findConptySource(nodePty, io);\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 return winFixConpty(nodePty, release, io);\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"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* electron-builder `afterPack` helper (Tier 0 — packaging).
|
|
3
|
+
*
|
|
4
|
+
* fancy-term-host wraps `node-pty`, a NATIVE addon. `electron-builder`
|
|
5
|
+
* (via `install-app-deps`) rebuilds it, but the rebuild output is left
|
|
6
|
+
* unusable per-OS, so a standard packaged app spawns NO terminal until each
|
|
7
|
+
* consumer discovers + patches it. This helper performs the three known fixes
|
|
8
|
+
* in one call so consumers don't have to hand-roll them (#7):
|
|
9
|
+
*
|
|
10
|
+
* • **Windows** — node-pty's `conpty.node` `LoadLibrary`s a `conpty.dll` from a
|
|
11
|
+
* `build/Release/conpty/` SUBDIR that the rebuild doesn't create; the dll +
|
|
12
|
+
* `OpenConsole.exe` ship only under `prebuilds/<plat>/conpty/` (or
|
|
13
|
+
* `third_party/conpty/`). Runtime error otherwise:
|
|
14
|
+
* `Cannot find conpty.dll at .../build/Release/conpty/conpty.dll`. We copy
|
|
15
|
+
* them into place.
|
|
16
|
+
* • **macOS** — node-pty's `spawn-helper` ships UNSIGNED, so Apple Silicon
|
|
17
|
+
* SIGKILLs it on exec (all arm64 code must be at least ad-hoc signed) and the
|
|
18
|
+
* shell child never starts. We ad-hoc sign it (`codesign --force --sign -`).
|
|
19
|
+
* • **Linux** — `spawn-helper` must stay executable after packaging. We
|
|
20
|
+
* `chmod +x` it.
|
|
21
|
+
*
|
|
22
|
+
* Usage (electron-builder config):
|
|
23
|
+
* ```js
|
|
24
|
+
* const { fancyTermAfterPack } = require('@particle-academy/fancy-term-host/electron');
|
|
25
|
+
* module.exports = { afterPack: (context) => fancyTermAfterPack(context) };
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* The IO is injected (see {@link AfterPackIo}) so the decision logic is unit
|
|
29
|
+
* testable without touching the filesystem or spawning `codesign`.
|
|
30
|
+
*/
|
|
31
|
+
/** The subset of electron-builder's AfterPackContext this helper reads. */
|
|
32
|
+
interface AfterPackContext {
|
|
33
|
+
/** The per-OS output dir being packed (e.g. `dist/mac-arm64`, `dist/win-unpacked`). */
|
|
34
|
+
appOutDir: string;
|
|
35
|
+
/** `'darwin' | 'win32' | 'linux'` — the platform being packed. */
|
|
36
|
+
electronPlatformName: string;
|
|
37
|
+
}
|
|
38
|
+
interface AfterPackOptions {
|
|
39
|
+
/** Override the platform (defaults to `context.electronPlatformName`). */
|
|
40
|
+
platform?: string;
|
|
41
|
+
/** Explicit node-pty package dir; skips the search under `appOutDir`. */
|
|
42
|
+
nodePtyDir?: string;
|
|
43
|
+
}
|
|
44
|
+
type AfterPackAction = 'fixed' | 'already-present' | 'skipped' | 'failed';
|
|
45
|
+
interface AfterPackResult {
|
|
46
|
+
platform: string;
|
|
47
|
+
action: AfterPackAction;
|
|
48
|
+
/** True when the terminal should now work (fixed / already-ok / nothing-to-do). */
|
|
49
|
+
ok: boolean;
|
|
50
|
+
detail?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Injected IO so the orchestration is unit-testable. `nodeAfterPackIo()` is prod. */
|
|
53
|
+
interface AfterPackIo {
|
|
54
|
+
exists(p: string): boolean;
|
|
55
|
+
/** Directory entries, or [] on any failure. */
|
|
56
|
+
readdir(dir: string): string[];
|
|
57
|
+
mkdirp(dir: string): void;
|
|
58
|
+
copyFile(src: string, dest: string): void;
|
|
59
|
+
/** Make a file executable (0o755). */
|
|
60
|
+
chmodExec(p: string): void;
|
|
61
|
+
/** Run a command; resolves the exit code + captured stderr. */
|
|
62
|
+
run(cmd: string, args: string[]): {
|
|
63
|
+
code: number;
|
|
64
|
+
stderr: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Production {@link AfterPackIo}: real fs + child_process. */
|
|
68
|
+
declare function nodeAfterPackIo(): AfterPackIo;
|
|
69
|
+
/** Locate the packaged node-pty dir under `appOutDir`, or null. */
|
|
70
|
+
declare function resolveNodePtyDir(appOutDir: string, platform: string, io: AfterPackIo): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Make a packaged node-pty spawn a working terminal on the platform being packed.
|
|
73
|
+
* Call from electron-builder's `afterPack`. NEVER throws for the expected
|
|
74
|
+
* outcomes; returns a result describing what it did (inspect `ok`). Idempotent —
|
|
75
|
+
* safe to run on every build.
|
|
76
|
+
*/
|
|
77
|
+
declare function fancyTermAfterPack(context: AfterPackContext, opts?: AfterPackOptions, io?: AfterPackIo): Promise<AfterPackResult>;
|
|
78
|
+
|
|
79
|
+
export { type AfterPackAction, type AfterPackContext, type AfterPackIo, type AfterPackOptions, type AfterPackResult, fancyTermAfterPack, nodeAfterPackIo, resolveNodePtyDir };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* electron-builder `afterPack` helper (Tier 0 — packaging).
|
|
3
|
+
*
|
|
4
|
+
* fancy-term-host wraps `node-pty`, a NATIVE addon. `electron-builder`
|
|
5
|
+
* (via `install-app-deps`) rebuilds it, but the rebuild output is left
|
|
6
|
+
* unusable per-OS, so a standard packaged app spawns NO terminal until each
|
|
7
|
+
* consumer discovers + patches it. This helper performs the three known fixes
|
|
8
|
+
* in one call so consumers don't have to hand-roll them (#7):
|
|
9
|
+
*
|
|
10
|
+
* • **Windows** — node-pty's `conpty.node` `LoadLibrary`s a `conpty.dll` from a
|
|
11
|
+
* `build/Release/conpty/` SUBDIR that the rebuild doesn't create; the dll +
|
|
12
|
+
* `OpenConsole.exe` ship only under `prebuilds/<plat>/conpty/` (or
|
|
13
|
+
* `third_party/conpty/`). Runtime error otherwise:
|
|
14
|
+
* `Cannot find conpty.dll at .../build/Release/conpty/conpty.dll`. We copy
|
|
15
|
+
* them into place.
|
|
16
|
+
* • **macOS** — node-pty's `spawn-helper` ships UNSIGNED, so Apple Silicon
|
|
17
|
+
* SIGKILLs it on exec (all arm64 code must be at least ad-hoc signed) and the
|
|
18
|
+
* shell child never starts. We ad-hoc sign it (`codesign --force --sign -`).
|
|
19
|
+
* • **Linux** — `spawn-helper` must stay executable after packaging. We
|
|
20
|
+
* `chmod +x` it.
|
|
21
|
+
*
|
|
22
|
+
* Usage (electron-builder config):
|
|
23
|
+
* ```js
|
|
24
|
+
* const { fancyTermAfterPack } = require('@particle-academy/fancy-term-host/electron');
|
|
25
|
+
* module.exports = { afterPack: (context) => fancyTermAfterPack(context) };
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* The IO is injected (see {@link AfterPackIo}) so the decision logic is unit
|
|
29
|
+
* testable without touching the filesystem or spawning `codesign`.
|
|
30
|
+
*/
|
|
31
|
+
/** The subset of electron-builder's AfterPackContext this helper reads. */
|
|
32
|
+
interface AfterPackContext {
|
|
33
|
+
/** The per-OS output dir being packed (e.g. `dist/mac-arm64`, `dist/win-unpacked`). */
|
|
34
|
+
appOutDir: string;
|
|
35
|
+
/** `'darwin' | 'win32' | 'linux'` — the platform being packed. */
|
|
36
|
+
electronPlatformName: string;
|
|
37
|
+
}
|
|
38
|
+
interface AfterPackOptions {
|
|
39
|
+
/** Override the platform (defaults to `context.electronPlatformName`). */
|
|
40
|
+
platform?: string;
|
|
41
|
+
/** Explicit node-pty package dir; skips the search under `appOutDir`. */
|
|
42
|
+
nodePtyDir?: string;
|
|
43
|
+
}
|
|
44
|
+
type AfterPackAction = 'fixed' | 'already-present' | 'skipped' | 'failed';
|
|
45
|
+
interface AfterPackResult {
|
|
46
|
+
platform: string;
|
|
47
|
+
action: AfterPackAction;
|
|
48
|
+
/** True when the terminal should now work (fixed / already-ok / nothing-to-do). */
|
|
49
|
+
ok: boolean;
|
|
50
|
+
detail?: string;
|
|
51
|
+
}
|
|
52
|
+
/** Injected IO so the orchestration is unit-testable. `nodeAfterPackIo()` is prod. */
|
|
53
|
+
interface AfterPackIo {
|
|
54
|
+
exists(p: string): boolean;
|
|
55
|
+
/** Directory entries, or [] on any failure. */
|
|
56
|
+
readdir(dir: string): string[];
|
|
57
|
+
mkdirp(dir: string): void;
|
|
58
|
+
copyFile(src: string, dest: string): void;
|
|
59
|
+
/** Make a file executable (0o755). */
|
|
60
|
+
chmodExec(p: string): void;
|
|
61
|
+
/** Run a command; resolves the exit code + captured stderr. */
|
|
62
|
+
run(cmd: string, args: string[]): {
|
|
63
|
+
code: number;
|
|
64
|
+
stderr: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** Production {@link AfterPackIo}: real fs + child_process. */
|
|
68
|
+
declare function nodeAfterPackIo(): AfterPackIo;
|
|
69
|
+
/** Locate the packaged node-pty dir under `appOutDir`, or null. */
|
|
70
|
+
declare function resolveNodePtyDir(appOutDir: string, platform: string, io: AfterPackIo): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Make a packaged node-pty spawn a working terminal on the platform being packed.
|
|
73
|
+
* Call from electron-builder's `afterPack`. NEVER throws for the expected
|
|
74
|
+
* outcomes; returns a result describing what it did (inspect `ok`). Idempotent —
|
|
75
|
+
* safe to run on every build.
|
|
76
|
+
*/
|
|
77
|
+
declare function fancyTermAfterPack(context: AfterPackContext, opts?: AfterPackOptions, io?: AfterPackIo): Promise<AfterPackResult>;
|
|
78
|
+
|
|
79
|
+
export { type AfterPackAction, type AfterPackContext, type AfterPackIo, type AfterPackOptions, type AfterPackResult, fancyTermAfterPack, nodeAfterPackIo, resolveNodePtyDir };
|
package/dist/electron.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
function findConptySource(nodePty, io) {
|
|
61
|
+
const prebuilds = path.join(nodePty, "prebuilds");
|
|
62
|
+
for (const name of io.readdir(prebuilds)) {
|
|
63
|
+
const c = path.join(prebuilds, name, "conpty");
|
|
64
|
+
if (io.exists(path.join(c, "conpty.dll"))) return c;
|
|
65
|
+
}
|
|
66
|
+
const tp = path.join(nodePty, "third_party", "conpty");
|
|
67
|
+
if (io.exists(path.join(tp, "conpty.dll"))) return tp;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
var CONPTY_ASSETS = ["conpty.dll", "OpenConsole.exe"];
|
|
71
|
+
function winFixConpty(nodePty, release, io) {
|
|
72
|
+
const destDir = path.join(release, "conpty");
|
|
73
|
+
if (CONPTY_ASSETS.every((f) => io.exists(path.join(destDir, f)))) {
|
|
74
|
+
return { platform: "win32", action: "already-present", ok: true };
|
|
75
|
+
}
|
|
76
|
+
const src = findConptySource(nodePty, io);
|
|
77
|
+
if (!src) {
|
|
78
|
+
return {
|
|
79
|
+
platform: "win32",
|
|
80
|
+
action: "failed",
|
|
81
|
+
ok: false,
|
|
82
|
+
detail: "conpty source not found (prebuilds/*/conpty or third_party/conpty)"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
io.mkdirp(destDir);
|
|
86
|
+
const copied = [];
|
|
87
|
+
for (const f of CONPTY_ASSETS) {
|
|
88
|
+
const s = path.join(src, f);
|
|
89
|
+
if (io.exists(s)) {
|
|
90
|
+
io.copyFile(s, path.join(destDir, f));
|
|
91
|
+
copied.push(f);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const ok = CONPTY_ASSETS.every((f) => io.exists(path.join(destDir, f)));
|
|
95
|
+
return {
|
|
96
|
+
platform: "win32",
|
|
97
|
+
action: ok ? "fixed" : "failed",
|
|
98
|
+
ok,
|
|
99
|
+
detail: `copied [${copied.join(", ")}] into build/Release/conpty from ${src}`
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function macSignSpawnHelper(release, io) {
|
|
103
|
+
const helper = path.join(release, "spawn-helper");
|
|
104
|
+
if (!io.exists(helper)) {
|
|
105
|
+
return { platform: "darwin", action: "skipped", ok: true, detail: "spawn-helper not present" };
|
|
106
|
+
}
|
|
107
|
+
const { code, stderr } = io.run("codesign", ["--force", "--sign", "-", helper]);
|
|
108
|
+
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()}` };
|
|
109
|
+
}
|
|
110
|
+
function linuxChmodSpawnHelper(release, io) {
|
|
111
|
+
const helper = path.join(release, "spawn-helper");
|
|
112
|
+
if (!io.exists(helper)) {
|
|
113
|
+
return { platform: "linux", action: "skipped", ok: true, detail: "spawn-helper not present" };
|
|
114
|
+
}
|
|
115
|
+
io.chmodExec(helper);
|
|
116
|
+
return { platform: "linux", action: "fixed", ok: true, detail: "chmod +x spawn-helper" };
|
|
117
|
+
}
|
|
118
|
+
async function fancyTermAfterPack(context, opts = {}, io = nodeAfterPackIo()) {
|
|
119
|
+
const platform = opts.platform ?? context.electronPlatformName;
|
|
120
|
+
const nodePty = opts.nodePtyDir ?? resolveNodePtyDir(context.appOutDir, platform, io);
|
|
121
|
+
if (!nodePty) {
|
|
122
|
+
return {
|
|
123
|
+
platform,
|
|
124
|
+
action: "skipped",
|
|
125
|
+
ok: false,
|
|
126
|
+
detail: "node-pty not found under appOutDir (adjust asarUnpack or pass nodePtyDir)"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const release = path.join(nodePty, "build", "Release");
|
|
130
|
+
switch (platform) {
|
|
131
|
+
case "win32":
|
|
132
|
+
return winFixConpty(nodePty, release, io);
|
|
133
|
+
case "darwin":
|
|
134
|
+
return macSignSpawnHelper(release, io);
|
|
135
|
+
case "linux":
|
|
136
|
+
return linuxChmodSpawnHelper(release, io);
|
|
137
|
+
default:
|
|
138
|
+
return { platform, action: "skipped", ok: true, detail: "no packaging fix needed" };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { fancyTermAfterPack, nodeAfterPackIo, resolveNodePtyDir };
|
|
143
|
+
//# sourceMappingURL=electron.js.map
|
|
144
|
+
//# sourceMappingURL=electron.js.map
|