@code-yeongyu/senpi 2026.9.25 → 2026.9.27

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.
Files changed (152) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/bun-runtime.d.ts +18 -2
  3. package/dist/bun-runtime.d.ts.map +1 -1
  4. package/dist/bun-runtime.js +60 -8
  5. package/dist/bun-runtime.js.map +1 -1
  6. package/dist/bundle/chunks/{app-server-command-RB6QUPRI.js → app-server-command-7WEJGW7D.js} +1 -1
  7. package/dist/bundle/chunks/bun-extension-importer-R4HXF3GD.js +14 -0
  8. package/dist/bundle/chunks/{chunk-JJ3NJZBD.js → chunk-3QCAGZFO.js} +19 -18
  9. package/dist/bundle/chunks/chunk-HCXLCGMF.js +8 -0
  10. package/dist/bundle/chunks/{chunk-OZCB6BP3.js → chunk-HH3D5ZYA.js} +3 -3
  11. package/dist/bundle/chunks/chunk-W3D3ZYDS.js +2 -0
  12. package/dist/bundle/chunks/{cli-main-KO53KYDN.js → cli-main-YLZ4MMLV.js} +1 -1
  13. package/dist/bundle/chunks/{interactive-host-runtime-YI75GFD5.js → interactive-host-runtime-6WY6424X.js} +1 -1
  14. package/dist/bundle/chunks/{interactive-mode-ABDNLJSI.js → interactive-mode-6AHR5TEM.js} +1 -1
  15. package/dist/bundle/chunks/{multi-session-host-JV3MNWXZ.js → multi-session-host-5IWJEFDF.js} +1 -1
  16. package/dist/bundle/chunks/{package-manager-cli-ZYYAVCXU.js → package-manager-cli-FZKBCLWV.js} +1 -1
  17. package/dist/bundle/chunks/{rpc-mode-Q2OUJURB.js → rpc-mode-JSCHAE6O.js} +1 -1
  18. package/dist/bundle/chunks/session-worker.js +119 -118
  19. package/dist/bundle/cli.js +1 -1
  20. package/dist/bundle/index.js +1 -1
  21. package/dist/bundle/rpc-entry.js +1 -1
  22. package/dist/core/agent-session.d.ts.map +1 -1
  23. package/dist/core/agent-session.js +10 -5
  24. package/dist/core/agent-session.js.map +1 -1
  25. package/dist/core/dynamic-prompt/handoff.js +1 -1
  26. package/dist/core/dynamic-prompt/handoff.js.map +1 -1
  27. package/dist/core/extensions/builtin/todotools/state.d.ts +1 -1
  28. package/dist/core/extensions/builtin/todotools/state.d.ts.map +1 -1
  29. package/dist/core/extensions/builtin/todotools/state.js +1 -1
  30. package/dist/core/extensions/builtin/todotools/state.js.map +1 -1
  31. package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.d.ts.map +1 -1
  32. package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js +3 -1
  33. package/dist/core/extensions/builtin/ttsr/detectors/repetitive-turns.js.map +1 -1
  34. package/dist/core/extensions/bun-extension-importer.d.ts.map +1 -1
  35. package/dist/core/extensions/bun-extension-importer.js +10 -4
  36. package/dist/core/extensions/bun-extension-importer.js.map +1 -1
  37. package/dist/core/extensions/bun-extension-registry.js +6 -5
  38. package/dist/core/extensions/bun-extension-registry.js.map +1 -1
  39. package/dist/core/messages.d.ts.map +1 -1
  40. package/dist/core/messages.js +4 -2
  41. package/dist/core/messages.js.map +1 -1
  42. package/dist/core/provider-rejected-images.d.ts +18 -0
  43. package/dist/core/provider-rejected-images.d.ts.map +1 -0
  44. package/dist/core/provider-rejected-images.js +107 -0
  45. package/dist/core/provider-rejected-images.js.map +1 -0
  46. package/dist/core/session-title-generator.d.ts.map +1 -1
  47. package/dist/core/session-title-generator.js +21 -3
  48. package/dist/core/session-title-generator.js.map +1 -1
  49. package/dist/core/session-title-reasoning.d.ts +14 -0
  50. package/dist/core/session-title-reasoning.d.ts.map +1 -0
  51. package/dist/core/session-title-reasoning.js +31 -0
  52. package/dist/core/session-title-reasoning.js.map +1 -0
  53. package/dist/core/tools/path-utils.d.ts.map +1 -1
  54. package/dist/core/tools/path-utils.js +13 -0
  55. package/dist/core/tools/path-utils.js.map +1 -1
  56. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  57. package/dist/modes/interactive/interactive-mode.js +2 -0
  58. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  59. package/dist/modes/interactive/runtime-notice-presenter.d.ts +8 -0
  60. package/dist/modes/interactive/runtime-notice-presenter.d.ts.map +1 -0
  61. package/dist/modes/interactive/runtime-notice-presenter.js +69 -0
  62. package/dist/modes/interactive/runtime-notice-presenter.js.map +1 -0
  63. package/dist/modes/interactive/runtime-notice.d.ts +50 -0
  64. package/dist/modes/interactive/runtime-notice.d.ts.map +1 -0
  65. package/dist/modes/interactive/runtime-notice.js +104 -0
  66. package/dist/modes/interactive/runtime-notice.js.map +1 -0
  67. package/docs/environment-variables.md +2 -0
  68. package/docs/quickstart.md +7 -1
  69. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +24 -0
  70. package/node_modules/@code-yeongyu/senpi-codemode/package.json +4 -4
  71. package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
  72. package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -1
  73. package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
  74. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
  75. package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
  76. package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
  77. package/node_modules/@earendil-works/pi-ai/dist/providers/data/vercel-ai-gateway.json +1 -1
  78. package/node_modules/@earendil-works/pi-ai/package.json +2 -2
  79. package/node_modules/@earendil-works/pi-pty/native/prebuilds/darwin-arm64/senpi_pty.darwin-arm64.node +0 -0
  80. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  81. package/node_modules/@earendil-works/pi-telemetry/package.json +1 -1
  82. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  83. package/package.json +7 -22
  84. package/dist/bundle/chunks/bun-extension-importer-H4IJCWWQ.js +0 -14
  85. package/dist/bundle/chunks/chunk-34HMVB33.js +0 -2
  86. package/dist/bundle/chunks/chunk-SGYEOIOJ.js +0 -8
  87. package/node_modules/@code-yeongyu/senpi-desktop-engine/README.md +0 -31
  88. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts +0 -38
  89. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.d.ts.map +0 -1
  90. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js +0 -117
  91. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/handshake.js.map +0 -1
  92. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts +0 -3
  93. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.d.ts.map +0 -1
  94. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js +0 -3
  95. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/index.js.map +0 -1
  96. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts +0 -42
  97. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.d.ts.map +0 -1
  98. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js +0 -79
  99. package/node_modules/@code-yeongyu/senpi-desktop-engine/dist/locator.js.map +0 -1
  100. package/node_modules/@code-yeongyu/senpi-desktop-engine/native/check-prebuild-fresh.mjs +0 -160
  101. package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.d.ts +0 -20
  102. package/node_modules/@code-yeongyu/senpi-desktop-engine/native/index.js +0 -23
  103. package/node_modules/@code-yeongyu/senpi-desktop-engine/native/prebuilds/darwin-arm64/senpi-desktop-engine +0 -0
  104. package/node_modules/@code-yeongyu/senpi-desktop-engine/package.json +0 -53
  105. package/node_modules/@code-yeongyu/senpi-desktop-prelude/README.md +0 -20
  106. package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts +0 -2
  107. package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.d.ts.map +0 -1
  108. package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js +0 -2
  109. package/node_modules/@code-yeongyu/senpi-desktop-prelude/dist/index.js.map +0 -1
  110. package/node_modules/@code-yeongyu/senpi-desktop-prelude/package.json +0 -44
  111. package/node_modules/@code-yeongyu/senpi-desktop-protocol/README.md +0 -20
  112. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts +0 -38
  113. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.d.ts.map +0 -1
  114. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js +0 -2
  115. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/audit.js.map +0 -1
  116. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts +0 -28
  117. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.d.ts.map +0 -1
  118. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js +0 -102
  119. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/call.js.map +0 -1
  120. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts +0 -14
  121. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.d.ts.map +0 -1
  122. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js +0 -71
  123. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/engine-schema.generated.js.map +0 -1
  124. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts +0 -7
  125. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.d.ts.map +0 -1
  126. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js +0 -4
  127. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/index.js.map +0 -1
  128. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts +0 -66
  129. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.d.ts.map +0 -1
  130. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js +0 -8
  131. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/json-rpc.js.map +0 -1
  132. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts +0 -44
  133. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.d.ts.map +0 -1
  134. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js +0 -2
  135. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/session.js.map +0 -1
  136. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts +0 -59
  137. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.d.ts.map +0 -1
  138. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js +0 -2
  139. package/node_modules/@code-yeongyu/senpi-desktop-protocol/dist/wire.js.map +0 -1
  140. package/node_modules/@code-yeongyu/senpi-desktop-protocol/package.json +0 -45
  141. package/node_modules/@code-yeongyu/senpi-desktop-service/README.md +0 -27
  142. package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts +0 -2
  143. package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.d.ts.map +0 -1
  144. package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js +0 -2
  145. package/node_modules/@code-yeongyu/senpi-desktop-service/dist/index.js.map +0 -1
  146. package/node_modules/@code-yeongyu/senpi-desktop-service/package.json +0 -44
  147. package/node_modules/@code-yeongyu/senpi-desktop-tool/README.md +0 -20
  148. package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts +0 -2
  149. package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.d.ts.map +0 -1
  150. package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js +0 -2
  151. package/node_modules/@code-yeongyu/senpi-desktop-tool/dist/index.js.map +0 -1
  152. package/node_modules/@code-yeongyu/senpi-desktop-tool/package.json +0 -44
@@ -1,31 +0,0 @@
1
- # @code-yeongyu/senpi-desktop-engine
2
-
3
- Finds the `senpi-desktop-engine` binary (`crates/senpi-desktop-engine`) for this host and checks it speaks the host's protocol. It does not implement any desktop behavior.
4
-
5
- ## Contract
6
-
7
- - `locateDesktopEngine()` returns `{ path, diagnostic: null }` or `{ path: null, diagnostic }`. It checks these paths in order:
8
- 1. compiled sidecar: `<dirname(process.execPath)>/native/prebuilds/<platform>-<arch>/senpi-desktop-engine[.exe]`
9
- 2. vendored prebuild: `native/prebuilds/<platform>-<arch>/senpi-desktop-engine[.exe]` in this package
10
- 3. dev build: `<repo>/target/release/senpi-desktop-engine[.exe]`
11
-
12
- A candidate that is missing, lacks the executable bit, or carries macOS `com.apple.quarantine` is skipped. The quarantine attribute is detected and never cleared. The diagnostic `code` is `quarantined` when a skipped candidate was quarantined, otherwise `native-unavailable`, and it lists every attempted path.
13
- - `helloDesktopEngine(path)` spawns the binary with `--stdio`, sends `engine.hello`, and resolves only when `abi === ENGINE_ABI` and `protocolVersion === PROTOCOL_VERSION` from `@code-yeongyu/senpi-desktop-protocol`. On any other ABI or protocol version it throws `DesktopEngineAbiMismatchError` (`code: "abi-mismatch"`, naming both versions). When no well-formed reply arrives it throws `DesktopEngineHandshakeError` (`code: "handshake-failed"`). This handshake is the ABI sentinel.
14
- - `./native` resolves only the vendored host prebuild, without spawning it.
15
-
16
- ## Prebuilds
17
-
18
- Only the host prebuild is committed, as `packages/pty` does. `bun run check:prebuild` rebuilds it with `cargo build --release -p senpi-desktop-engine --locked` and `--remap-path-prefix`, then byte-compares the rebuilt binary against the committed one. Pass `-- --update` to re-vendor it. `node scripts/build-desktop-engine-local.mjs` builds the dev candidate into `target/release/`.
19
-
20
- ## Import direction
21
-
22
- `scripts/desktop-package-boundaries.test.mjs` enforces these edges, in both `package.json` and `src/`:
23
-
24
- - `@code-yeongyu/senpi-desktop-protocol` and `@code-yeongyu/senpi-desktop-prelude` import no workspace package.
25
- - `@code-yeongyu/senpi-desktop-engine` may import `-protocol`.
26
- - `@code-yeongyu/senpi-desktop-service` may import `-protocol`, `-engine`, and `-prelude`.
27
- - `@code-yeongyu/senpi-desktop-tool` may import `-protocol`, `-engine`, `-prelude`, and `-service`.
28
- - `@code-yeongyu/senpi` (coding-agent) may import only `-tool` and `-service`. `@code-yeongyu/senpi-codemode` imports none of them.
29
- - No desktop package imports `pi-agent-core`, `pi-ai`, or `pi-tui`. There is no shared utils package: the five desktop packages are the whole set.
30
-
31
- The Rust side runs the other way: `senpi-desktop-core` <- `-safety` <- `-session` <- backends <- `senpi-desktop-engine` (the binary). The TS packages reach it only through the engine's stdio JSON-RPC.
@@ -1,38 +0,0 @@
1
- import { type ChildProcessWithoutNullStreams } from "node:child_process";
2
- import { ENGINE_ABI, PROTOCOL_VERSION } from "@code-yeongyu/senpi-desktop-protocol";
3
- /** The `engine.hello` reply of an engine this package accepts. */
4
- export interface EngineHello {
5
- readonly protocolVersion: typeof PROTOCOL_VERSION;
6
- readonly engineVersion: string;
7
- readonly buildSha: string;
8
- readonly abi: typeof ENGINE_ABI;
9
- }
10
- export interface EngineContract {
11
- readonly abi: string;
12
- readonly protocolVersion: string;
13
- }
14
- /** Starts the engine process; injectable so tests can run a script engine through `node`. */
15
- export type DesktopEngineSpawner = (enginePath: string, args: readonly string[]) => ChildProcessWithoutNullStreams;
16
- export interface HelloOptions {
17
- /** Bounds the whole handshake: spawn, reply, and exit at stdin EOF. */
18
- readonly timeoutMs?: number;
19
- readonly spawnEngine?: DesktopEngineSpawner;
20
- }
21
- export declare const HELLO_TIMEOUT_MS = 10000;
22
- /** The ABI sentinel: the engine answered, but speaks a contract this host does not. */
23
- export declare class DesktopEngineAbiMismatchError extends Error {
24
- readonly code = "abi-mismatch";
25
- readonly enginePath: string;
26
- readonly expected: EngineContract;
27
- readonly actual: EngineContract;
28
- constructor(enginePath: string, actual: EngineContract);
29
- }
30
- /** The engine never produced a well-formed `engine.hello` reply. */
31
- export declare class DesktopEngineHandshakeError extends Error {
32
- readonly code = "handshake-failed";
33
- readonly enginePath: string;
34
- constructor(enginePath: string, reason: string);
35
- }
36
- /** Spawns the engine with `--stdio`, sends `engine.hello`, and accepts only the host's ABI and protocol. */
37
- export declare function helloDesktopEngine(enginePath: string, options?: HelloOptions): Promise<EngineHello>;
38
- //# sourceMappingURL=handshake.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"handshake.d.ts","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,8BAA8B,EAAS,MAAM,oBAAoB,CAAC;AAEhF,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAEpF,kEAAkE;AAClE,MAAM,WAAW,WAAW;IAC3B,QAAQ,CAAC,eAAe,EAAE,OAAO,gBAAgB,CAAC;IAClD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,GAAG,EAAE,OAAO,UAAU,CAAC;CAChC;AAED,MAAM,WAAW,cAAc;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CACjC;AAED,6FAA6F;AAC7F,MAAM,MAAM,oBAAoB,GAAG,CAAC,UAAU,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,8BAA8B,CAAC;AAEnH,MAAM,WAAW,YAAY;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,WAAW,CAAC,EAAE,oBAAoB,CAAC;CAC5C;AAED,eAAO,MAAM,gBAAgB,QAAS,CAAC;AAIvC,uFAAuF;AACvF,qBAAa,6BAA8B,SAAQ,KAAK;IACvD,QAAQ,CAAC,IAAI,kBAAkB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,QAAQ,EAAE,cAAc,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAEhC,YAAY,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,cAAc,EASrD;CACD;AAED,oEAAoE;AACpE,qBAAa,2BAA4B,SAAQ,KAAK;IACrD,QAAQ,CAAC,IAAI,sBAAsB;IACnC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAE5B,YAAY,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAI7C;CACD;AAKD,4GAA4G;AAC5G,wBAAsB,kBAAkB,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAc7G"}
@@ -1,117 +0,0 @@
1
- import { spawn } from "node:child_process";
2
- import { createInterface } from "node:readline";
3
- import { ENGINE_ABI, PROTOCOL_VERSION } from "@code-yeongyu/senpi-desktop-protocol";
4
- export const HELLO_TIMEOUT_MS = 10_000;
5
- const HELLO_REQUEST_ID = 1;
6
- const STDERR_TAIL_BYTES = 4096;
7
- /** The ABI sentinel: the engine answered, but speaks a contract this host does not. */
8
- export class DesktopEngineAbiMismatchError extends Error {
9
- constructor(enginePath, actual) {
10
- const expected = { abi: ENGINE_ABI, protocolVersion: PROTOCOL_VERSION };
11
- super(`desktop engine ABI mismatch at ${enginePath}: expected abi ${expected.abi} protocol ${expected.protocolVersion}, engine reported abi ${actual.abi} protocol ${actual.protocolVersion}`);
12
- this.code = "abi-mismatch";
13
- this.name = "DesktopEngineAbiMismatchError";
14
- this.enginePath = enginePath;
15
- this.expected = expected;
16
- this.actual = actual;
17
- }
18
- }
19
- /** The engine never produced a well-formed `engine.hello` reply. */
20
- export class DesktopEngineHandshakeError extends Error {
21
- constructor(enginePath, reason) {
22
- super(`desktop engine handshake failed at ${enginePath}: ${reason}`);
23
- this.code = "handshake-failed";
24
- this.name = "DesktopEngineHandshakeError";
25
- this.enginePath = enginePath;
26
- }
27
- }
28
- const defaultSpawner = (enginePath, args) => spawn(enginePath, args, { stdio: "pipe", windowsHide: true });
29
- /** Spawns the engine with `--stdio`, sends `engine.hello`, and accepts only the host's ABI and protocol. */
30
- export async function helloDesktopEngine(enginePath, options = {}) {
31
- const result = await requestHello(enginePath, options);
32
- if (!isRecord(result) || !isHelloShape(result)) {
33
- throw new DesktopEngineHandshakeError(enginePath, `malformed engine.hello result ${JSON.stringify(result)}`);
34
- }
35
- if (result.abi !== ENGINE_ABI || result.protocolVersion !== PROTOCOL_VERSION) {
36
- throw new DesktopEngineAbiMismatchError(enginePath, { abi: result.abi, protocolVersion: result.protocolVersion });
37
- }
38
- return {
39
- protocolVersion: PROTOCOL_VERSION,
40
- engineVersion: result.engineVersion,
41
- buildSha: result.buildSha,
42
- abi: ENGINE_ABI,
43
- };
44
- }
45
- function requestHello(enginePath, options) {
46
- const timeoutMs = options.timeoutMs ?? HELLO_TIMEOUT_MS;
47
- const child = (options.spawnEngine ?? defaultSpawner)(enginePath, ["--stdio"]);
48
- return new Promise((resolve, reject) => {
49
- let outcome;
50
- let stderrTail = "";
51
- // The first outcome wins; ending stdin then lets the engine exit at EOF.
52
- const settle = (next) => {
53
- if (outcome !== undefined)
54
- return;
55
- outcome = next;
56
- child.stdin.end();
57
- };
58
- const timer = setTimeout(() => {
59
- settle({ ok: false, reason: `no engine.hello reply and exit within ${timeoutMs} ms` });
60
- child.kill("SIGKILL");
61
- }, timeoutMs);
62
- child.stderr.setEncoding("utf8");
63
- child.stderr.on("data", (chunk) => {
64
- stderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_BYTES);
65
- });
66
- createInterface({ input: child.stdout }).on("line", (line) => settleFromLine(line, settle));
67
- child.stdin.on("error", (error) => settle({ ok: false, reason: `stdin: ${error.message}` }));
68
- child.on("error", (error) => settle({ ok: false, reason: `spawn: ${error.message}` }));
69
- child.on("close", (code, signal) => {
70
- clearTimeout(timer);
71
- const final = outcome ?? {
72
- ok: false,
73
- reason: `engine exited (code ${code ?? "null"}, signal ${signal ?? "null"}) before replying; stderr: ${stderrTail.trim() || "<empty>"}`,
74
- };
75
- if (final.ok)
76
- resolve(final.result);
77
- else
78
- reject(new DesktopEngineHandshakeError(enginePath, final.reason));
79
- });
80
- child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: HELLO_REQUEST_ID, method: "engine.hello", params: {} })}\n`);
81
- });
82
- }
83
- function settleFromLine(line, settle) {
84
- const message = parseJsonLine(line);
85
- if (message === undefined) {
86
- settle({ ok: false, reason: `non-JSON output line ${JSON.stringify(line)}` });
87
- return;
88
- }
89
- // Notifications (e.g. `engine.log`) may precede the reply; only our id settles.
90
- if (!isRecord(message) || message.id !== HELLO_REQUEST_ID)
91
- return;
92
- if ("result" in message) {
93
- settle({ ok: true, result: message.result });
94
- return;
95
- }
96
- settle({ ok: false, reason: `engine.hello returned an error ${JSON.stringify(message.error)}` });
97
- }
98
- function parseJsonLine(line) {
99
- try {
100
- return JSON.parse(line);
101
- }
102
- catch (error) {
103
- if (error instanceof SyntaxError)
104
- return undefined;
105
- throw error;
106
- }
107
- }
108
- function isRecord(value) {
109
- return typeof value === "object" && value !== null;
110
- }
111
- function isHelloShape(value) {
112
- return (typeof value.protocolVersion === "string" &&
113
- typeof value.engineVersion === "string" &&
114
- typeof value.buildSha === "string" &&
115
- typeof value.abi === "string");
116
- }
117
- //# sourceMappingURL=handshake.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"handshake.js","sourceRoot":"","sources":["../src/handshake.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuC,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAwBpF,MAAM,CAAC,MAAM,gBAAgB,GAAG,MAAM,CAAC;AACvC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAC3B,MAAM,iBAAiB,GAAG,IAAI,CAAC;AAE/B,uFAAuF;AACvF,MAAM,OAAO,6BAA8B,SAAQ,KAAK;IAMvD,YAAY,UAAkB,EAAE,MAAsB;QACrD,MAAM,QAAQ,GAAG,EAAE,GAAG,EAAE,UAAU,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC;QACxE,KAAK,CACJ,kCAAkC,UAAU,kBAAkB,QAAQ,CAAC,GAAG,aAAa,QAAQ,CAAC,eAAe,yBAAyB,MAAM,CAAC,GAAG,aAAa,MAAM,CAAC,eAAe,EAAE,CACvL,CAAC;QATM,SAAI,GAAG,cAAc,CAAC;QAU9B,IAAI,CAAC,IAAI,GAAG,+BAA+B,CAAC;QAC5C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD;AAED,oEAAoE;AACpE,MAAM,OAAO,2BAA4B,SAAQ,KAAK;IAIrD,YAAY,UAAkB,EAAE,MAAc;QAC7C,KAAK,CAAC,sCAAsC,UAAU,KAAK,MAAM,EAAE,CAAC,CAAC;QAJ7D,SAAI,GAAG,kBAAkB,CAAC;QAKlC,IAAI,CAAC,IAAI,GAAG,6BAA6B,CAAC;QAC1C,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;IAC9B,CAAC;CACD;AAED,MAAM,cAAc,GAAyB,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,CACjE,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;AAE/D,4GAA4G;AAC5G,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,UAAkB,EAAE,OAAO,GAAiB,EAAE;IACtF,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IACvD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,EAAE,CAAC;QAChD,MAAM,IAAI,2BAA2B,CAAC,UAAU,EAAE,iCAAiC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;IAC9G,CAAC;IACD,IAAI,MAAM,CAAC,GAAG,KAAK,UAAU,IAAI,MAAM,CAAC,eAAe,KAAK,gBAAgB,EAAE,CAAC;QAC9E,MAAM,IAAI,6BAA6B,CAAC,UAAU,EAAE,EAAE,GAAG,EAAE,MAAM,CAAC,GAAG,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE,CAAC,CAAC;IACnH,CAAC;IACD,OAAO;QACN,eAAe,EAAE,gBAAgB;QACjC,aAAa,EAAE,MAAM,CAAC,aAAa;QACnC,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,GAAG,EAAE,UAAU;KACf,CAAC;AACH,CAAC;AAID,SAAS,YAAY,CAAC,UAAkB,EAAE,OAAqB;IAC9D,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,gBAAgB,CAAC;IACxD,MAAM,KAAK,GAAG,CAAC,OAAO,CAAC,WAAW,IAAI,cAAc,CAAC,CAAC,UAAU,EAAE,CAAC,SAAS,CAAC,CAAC,CAAC;IAC/E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACtC,IAAI,OAA4B,CAAC;QACjC,IAAI,UAAU,GAAG,EAAE,CAAC;QACpB,yEAAyE;QACzE,MAAM,MAAM,GAAG,CAAC,IAAa,EAAE,EAAE;YAChC,IAAI,OAAO,KAAK,SAAS;gBAAE,OAAO;YAClC,OAAO,GAAG,IAAI,CAAC;YACf,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC;QACnB,CAAC,CAAC;QACF,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC7B,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,yCAAyC,SAAS,KAAK,EAAE,CAAC,CAAC;YACvF,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACvB,CAAC,EAAE,SAAS,CAAC,CAAC;QAEd,KAAK,CAAC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACjC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACzC,UAAU,GAAG,CAAC,UAAU,GAAG,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,iBAAiB,CAAC,CAAC;QAC7D,CAAC,CAAC,CAAC;QACH,eAAe,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;QAC5F,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;QAC7F,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;QACvF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE;YAClC,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,MAAM,KAAK,GAAG,OAAO,IAAI;gBACxB,EAAE,EAAE,KAAK;gBACT,MAAM,EAAE,uBAAuB,IAAI,IAAI,MAAM,YAAY,MAAM,IAAI,MAAM,8BAA8B,UAAU,CAAC,IAAI,EAAE,IAAI,SAAS,EAAE;aACvI,CAAC;YACF,IAAI,KAAK,CAAC,EAAE;gBAAE,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;;gBAC/B,MAAM,CAAC,IAAI,2BAA2B,CAAC,UAAU,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;QACxE,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,KAAK,CAAC,KAAK,CAChB,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,EAAE,gBAAgB,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,IAAI,CACnG,CAAC;IACH,CAAC,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,cAAc,CAAC,IAAY,EAAE,MAAkC;IACvE,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,CAAC;IACpC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;QAC3B,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,wBAAwB,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QAC9E,OAAO;IACR,CAAC;IACD,gFAAgF;IAChF,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,EAAE,KAAK,gBAAgB;QAAE,OAAO;IAClE,IAAI,QAAQ,IAAI,OAAO,EAAE,CAAC;QACzB,MAAM,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC7C,OAAO;IACR,CAAC;IACD,MAAM,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,kCAAkC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC;AAClG,CAAC;AAED,SAAS,aAAa,CAAC,IAAY;IAClC,IAAI,CAAC;QACJ,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACzB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,IAAI,KAAK,YAAY,WAAW;YAAE,OAAO,SAAS,CAAC;QACnD,MAAM,KAAK,CAAC;IACb,CAAC;AACF,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC/B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,CAAC;AACpD,CAAC;AAED,SAAS,YAAY,CAAC,KAA8B;IACnD,OAAO,CACN,OAAO,KAAK,CAAC,eAAe,KAAK,QAAQ;QACzC,OAAO,KAAK,CAAC,aAAa,KAAK,QAAQ;QACvC,OAAO,KAAK,CAAC,QAAQ,KAAK,QAAQ;QAClC,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,CAC7B,CAAC;AACH,CAAC","sourcesContent":["import { type ChildProcessWithoutNullStreams, spawn } from \"node:child_process\";\nimport { createInterface } from \"node:readline\";\nimport { ENGINE_ABI, PROTOCOL_VERSION } from \"@code-yeongyu/senpi-desktop-protocol\";\n\n/** The `engine.hello` reply of an engine this package accepts. */\nexport interface EngineHello {\n\treadonly protocolVersion: typeof PROTOCOL_VERSION;\n\treadonly engineVersion: string;\n\treadonly buildSha: string;\n\treadonly abi: typeof ENGINE_ABI;\n}\n\nexport interface EngineContract {\n\treadonly abi: string;\n\treadonly protocolVersion: string;\n}\n\n/** Starts the engine process; injectable so tests can run a script engine through `node`. */\nexport type DesktopEngineSpawner = (enginePath: string, args: readonly string[]) => ChildProcessWithoutNullStreams;\n\nexport interface HelloOptions {\n\t/** Bounds the whole handshake: spawn, reply, and exit at stdin EOF. */\n\treadonly timeoutMs?: number;\n\treadonly spawnEngine?: DesktopEngineSpawner;\n}\n\nexport const HELLO_TIMEOUT_MS = 10_000;\nconst HELLO_REQUEST_ID = 1;\nconst STDERR_TAIL_BYTES = 4096;\n\n/** The ABI sentinel: the engine answered, but speaks a contract this host does not. */\nexport class DesktopEngineAbiMismatchError extends Error {\n\treadonly code = \"abi-mismatch\";\n\treadonly enginePath: string;\n\treadonly expected: EngineContract;\n\treadonly actual: EngineContract;\n\n\tconstructor(enginePath: string, actual: EngineContract) {\n\t\tconst expected = { abi: ENGINE_ABI, protocolVersion: PROTOCOL_VERSION };\n\t\tsuper(\n\t\t\t`desktop engine ABI mismatch at ${enginePath}: expected abi ${expected.abi} protocol ${expected.protocolVersion}, engine reported abi ${actual.abi} protocol ${actual.protocolVersion}`,\n\t\t);\n\t\tthis.name = \"DesktopEngineAbiMismatchError\";\n\t\tthis.enginePath = enginePath;\n\t\tthis.expected = expected;\n\t\tthis.actual = actual;\n\t}\n}\n\n/** The engine never produced a well-formed `engine.hello` reply. */\nexport class DesktopEngineHandshakeError extends Error {\n\treadonly code = \"handshake-failed\";\n\treadonly enginePath: string;\n\n\tconstructor(enginePath: string, reason: string) {\n\t\tsuper(`desktop engine handshake failed at ${enginePath}: ${reason}`);\n\t\tthis.name = \"DesktopEngineHandshakeError\";\n\t\tthis.enginePath = enginePath;\n\t}\n}\n\nconst defaultSpawner: DesktopEngineSpawner = (enginePath, args) =>\n\tspawn(enginePath, args, { stdio: \"pipe\", windowsHide: true });\n\n/** Spawns the engine with `--stdio`, sends `engine.hello`, and accepts only the host's ABI and protocol. */\nexport async function helloDesktopEngine(enginePath: string, options: HelloOptions = {}): Promise<EngineHello> {\n\tconst result = await requestHello(enginePath, options);\n\tif (!isRecord(result) || !isHelloShape(result)) {\n\t\tthrow new DesktopEngineHandshakeError(enginePath, `malformed engine.hello result ${JSON.stringify(result)}`);\n\t}\n\tif (result.abi !== ENGINE_ABI || result.protocolVersion !== PROTOCOL_VERSION) {\n\t\tthrow new DesktopEngineAbiMismatchError(enginePath, { abi: result.abi, protocolVersion: result.protocolVersion });\n\t}\n\treturn {\n\t\tprotocolVersion: PROTOCOL_VERSION,\n\t\tengineVersion: result.engineVersion,\n\t\tbuildSha: result.buildSha,\n\t\tabi: ENGINE_ABI,\n\t};\n}\n\ntype Outcome = { readonly ok: true; readonly result: unknown } | { readonly ok: false; readonly reason: string };\n\nfunction requestHello(enginePath: string, options: HelloOptions): Promise<unknown> {\n\tconst timeoutMs = options.timeoutMs ?? HELLO_TIMEOUT_MS;\n\tconst child = (options.spawnEngine ?? defaultSpawner)(enginePath, [\"--stdio\"]);\n\treturn new Promise((resolve, reject) => {\n\t\tlet outcome: Outcome | undefined;\n\t\tlet stderrTail = \"\";\n\t\t// The first outcome wins; ending stdin then lets the engine exit at EOF.\n\t\tconst settle = (next: Outcome) => {\n\t\t\tif (outcome !== undefined) return;\n\t\t\toutcome = next;\n\t\t\tchild.stdin.end();\n\t\t};\n\t\tconst timer = setTimeout(() => {\n\t\t\tsettle({ ok: false, reason: `no engine.hello reply and exit within ${timeoutMs} ms` });\n\t\t\tchild.kill(\"SIGKILL\");\n\t\t}, timeoutMs);\n\n\t\tchild.stderr.setEncoding(\"utf8\");\n\t\tchild.stderr.on(\"data\", (chunk: string) => {\n\t\t\tstderrTail = (stderrTail + chunk).slice(-STDERR_TAIL_BYTES);\n\t\t});\n\t\tcreateInterface({ input: child.stdout }).on(\"line\", (line) => settleFromLine(line, settle));\n\t\tchild.stdin.on(\"error\", (error) => settle({ ok: false, reason: `stdin: ${error.message}` }));\n\t\tchild.on(\"error\", (error) => settle({ ok: false, reason: `spawn: ${error.message}` }));\n\t\tchild.on(\"close\", (code, signal) => {\n\t\t\tclearTimeout(timer);\n\t\t\tconst final = outcome ?? {\n\t\t\t\tok: false,\n\t\t\t\treason: `engine exited (code ${code ?? \"null\"}, signal ${signal ?? \"null\"}) before replying; stderr: ${stderrTail.trim() || \"<empty>\"}`,\n\t\t\t};\n\t\t\tif (final.ok) resolve(final.result);\n\t\t\telse reject(new DesktopEngineHandshakeError(enginePath, final.reason));\n\t\t});\n\n\t\tchild.stdin.write(\n\t\t\t`${JSON.stringify({ jsonrpc: \"2.0\", id: HELLO_REQUEST_ID, method: \"engine.hello\", params: {} })}\\n`,\n\t\t);\n\t});\n}\n\nfunction settleFromLine(line: string, settle: (outcome: Outcome) => void): void {\n\tconst message = parseJsonLine(line);\n\tif (message === undefined) {\n\t\tsettle({ ok: false, reason: `non-JSON output line ${JSON.stringify(line)}` });\n\t\treturn;\n\t}\n\t// Notifications (e.g. `engine.log`) may precede the reply; only our id settles.\n\tif (!isRecord(message) || message.id !== HELLO_REQUEST_ID) return;\n\tif (\"result\" in message) {\n\t\tsettle({ ok: true, result: message.result });\n\t\treturn;\n\t}\n\tsettle({ ok: false, reason: `engine.hello returned an error ${JSON.stringify(message.error)}` });\n}\n\nfunction parseJsonLine(line: string): unknown {\n\ttry {\n\t\treturn JSON.parse(line);\n\t} catch (error) {\n\t\tif (error instanceof SyntaxError) return undefined;\n\t\tthrow error;\n\t}\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction isHelloShape(value: Record<string, unknown>): value is Record<keyof EngineHello, string> {\n\treturn (\n\t\ttypeof value.protocolVersion === \"string\" &&\n\t\ttypeof value.engineVersion === \"string\" &&\n\t\ttypeof value.buildSha === \"string\" &&\n\t\ttypeof value.abi === \"string\"\n\t);\n}\n"]}
@@ -1,3 +0,0 @@
1
- export { DesktopEngineAbiMismatchError, DesktopEngineHandshakeError, type DesktopEngineSpawner, type EngineContract, type EngineHello, HELLO_TIMEOUT_MS, type HelloOptions, helloDesktopEngine, } from "./handshake.ts";
2
- export { DESKTOP_ENGINE_BINARY, type DesktopEngineLocateDiagnostic, type DesktopEngineLocateDiagnosticCode, type DesktopEngineLocation, type DesktopEngineLocatorOptions, type DesktopEngineQuarantineProbe, getDesktopEngineCandidatePaths, getDesktopEngineFileName, getDesktopEngineHost, isQuarantinedFile, locateDesktopEngine, QUARANTINE_ATTRIBUTE, } from "./locator.ts";
3
- //# sourceMappingURL=index.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,2BAA2B,EAC3B,KAAK,oBAAoB,EACzB,KAAK,cAAc,EACnB,KAAK,WAAW,EAChB,gBAAgB,EAChB,KAAK,YAAY,EACjB,kBAAkB,GAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,qBAAqB,EACrB,KAAK,6BAA6B,EAClC,KAAK,iCAAiC,EACtC,KAAK,qBAAqB,EAC1B,KAAK,2BAA2B,EAChC,KAAK,4BAA4B,EACjC,8BAA8B,EAC9B,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,cAAc,CAAC"}
@@ -1,3 +0,0 @@
1
- export { DesktopEngineAbiMismatchError, DesktopEngineHandshakeError, HELLO_TIMEOUT_MS, helloDesktopEngine, } from "./handshake.js";
2
- export { DESKTOP_ENGINE_BINARY, getDesktopEngineCandidatePaths, getDesktopEngineFileName, getDesktopEngineHost, isQuarantinedFile, locateDesktopEngine, QUARANTINE_ATTRIBUTE, } from "./locator.js";
3
- //# sourceMappingURL=index.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,6BAA6B,EAC7B,2BAA2B,EAI3B,gBAAgB,EAEhB,kBAAkB,GAClB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EACN,qBAAqB,EAMrB,8BAA8B,EAC9B,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,mBAAmB,EACnB,oBAAoB,GACpB,MAAM,cAAc,CAAC","sourcesContent":["export {\n\tDesktopEngineAbiMismatchError,\n\tDesktopEngineHandshakeError,\n\ttype DesktopEngineSpawner,\n\ttype EngineContract,\n\ttype EngineHello,\n\tHELLO_TIMEOUT_MS,\n\ttype HelloOptions,\n\thelloDesktopEngine,\n} from \"./handshake.ts\";\nexport {\n\tDESKTOP_ENGINE_BINARY,\n\ttype DesktopEngineLocateDiagnostic,\n\ttype DesktopEngineLocateDiagnosticCode,\n\ttype DesktopEngineLocation,\n\ttype DesktopEngineLocatorOptions,\n\ttype DesktopEngineQuarantineProbe,\n\tgetDesktopEngineCandidatePaths,\n\tgetDesktopEngineFileName,\n\tgetDesktopEngineHost,\n\tisQuarantinedFile,\n\tlocateDesktopEngine,\n\tQUARANTINE_ATTRIBUTE,\n} from \"./locator.ts\";\n"]}
@@ -1,42 +0,0 @@
1
- export declare const DESKTOP_ENGINE_BINARY = "senpi-desktop-engine";
2
- export declare const QUARANTINE_ATTRIBUTE = "com.apple.quarantine";
3
- /** `quarantined` wins over `native-unavailable`: it names the one fix the user can apply. */
4
- export type DesktopEngineLocateDiagnosticCode = "native-unavailable" | "quarantined";
5
- export interface DesktopEngineLocateDiagnostic {
6
- readonly code: DesktopEngineLocateDiagnosticCode;
7
- readonly host: string;
8
- readonly attemptedPaths: readonly string[];
9
- readonly message: string;
10
- readonly cause: string;
11
- }
12
- export type DesktopEngineLocation = {
13
- readonly path: string;
14
- readonly diagnostic: null;
15
- } | {
16
- readonly path: null;
17
- readonly diagnostic: DesktopEngineLocateDiagnostic;
18
- };
19
- /** Reports whether a candidate carries macOS `com.apple.quarantine`; injectable for tests. */
20
- export type DesktopEngineQuarantineProbe = (enginePath: string) => boolean;
21
- export interface DesktopEngineLocatorOptions {
22
- readonly platform?: string;
23
- readonly arch?: string;
24
- /** Directory of the running executable; a compiled senpi ships the engine as a sidecar beside it. */
25
- readonly execDir?: string;
26
- /** Root of this package, which holds `native/prebuilds/<host>/`. */
27
- readonly packageDir?: string;
28
- /** Repository root whose `target/release/` holds a dev build. */
29
- readonly repoRoot?: string;
30
- readonly isQuarantined?: DesktopEngineQuarantineProbe;
31
- }
32
- export declare function getDesktopEngineHost(platform?: string, arch?: string): string;
33
- export declare function getDesktopEngineFileName(platform?: string): string;
34
- /** Candidates in priority order: compiled sidecar, vendored package prebuild, dev `target/release`. */
35
- export declare function getDesktopEngineCandidatePaths(options?: DesktopEngineLocatorOptions): readonly string[];
36
- export declare function locateDesktopEngine(options?: DesktopEngineLocatorOptions): DesktopEngineLocation;
37
- /**
38
- * Node exposes no `getxattr`, so `xattr -p` is the only channel; it exits 0 only when the
39
- * attribute is present. A failed probe returns `false`: an unreadable attribute never rejects.
40
- */
41
- export declare function isQuarantinedFile(enginePath: string, platform?: string): boolean;
42
- //# sourceMappingURL=locator.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"locator.d.ts","sourceRoot":"","sources":["../src/locator.ts"],"names":[],"mappings":"AAKA,eAAO,MAAM,qBAAqB,yBAAyB,CAAC;AAC5D,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAE3D,6FAA6F;AAC7F,MAAM,MAAM,iCAAiC,GAAG,oBAAoB,GAAG,aAAa,CAAC;AAErF,MAAM,WAAW,6BAA6B;IAC7C,QAAQ,CAAC,IAAI,EAAE,iCAAiC,CAAC;IACjD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,qBAAqB,GAC9B;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;CAAE,GACpD;IAAE,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,6BAA6B,CAAA;CAAE,CAAC;AAE/E,8FAA8F;AAC9F,MAAM,MAAM,4BAA4B,GAAG,CAAC,UAAU,EAAE,MAAM,KAAK,OAAO,CAAC;AAE3E,MAAM,WAAW,2BAA2B;IAC3C,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,qGAAqG;IACrG,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,iEAAiE;IACjE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,CAAC,EAAE,4BAA4B,CAAC;CACtD;AAID,wBAAgB,oBAAoB,CAAC,QAAQ,GAAE,MAAyB,EAAE,IAAI,GAAE,MAAqB,GAAG,MAAM,CAE7G;AAED,wBAAgB,wBAAwB,CAAC,QAAQ,GAAE,MAAyB,GAAG,MAAM,CAEpF;AAED,uGAAuG;AACvG,wBAAgB,8BAA8B,CAAC,OAAO,GAAE,2BAAgC,GAAG,SAAS,MAAM,EAAE,CAQ3G;AAED,wBAAgB,mBAAmB,CAAC,OAAO,GAAE,2BAAgC,GAAG,qBAAqB,CAiCpG;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAyB,GAAG,OAAO,CAOlG"}
@@ -1,79 +0,0 @@
1
- import { spawnSync } from "node:child_process";
2
- import { accessSync, constants, existsSync } from "node:fs";
3
- import { dirname, join } from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- export const DESKTOP_ENGINE_BINARY = "senpi-desktop-engine";
6
- export const QUARANTINE_ATTRIBUTE = "com.apple.quarantine";
7
- const defaultPackageDir = join(dirname(fileURLToPath(import.meta.url)), "..");
8
- export function getDesktopEngineHost(platform = process.platform, arch = process.arch) {
9
- return `${platform}-${arch}`;
10
- }
11
- export function getDesktopEngineFileName(platform = process.platform) {
12
- return platform === "win32" ? `${DESKTOP_ENGINE_BINARY}.exe` : DESKTOP_ENGINE_BINARY;
13
- }
14
- /** Candidates in priority order: compiled sidecar, vendored package prebuild, dev `target/release`. */
15
- export function getDesktopEngineCandidatePaths(options = {}) {
16
- const host = getDesktopEngineHost(options.platform, options.arch);
17
- const file = getDesktopEngineFileName(options.platform);
18
- const packageDir = options.packageDir ?? defaultPackageDir;
19
- const execDir = options.execDir ?? dirname(process.execPath);
20
- const repoRoot = options.repoRoot ?? join(packageDir, "..", "..");
21
- const prebuild = join("native", "prebuilds", host, file);
22
- return [join(execDir, prebuild), join(packageDir, prebuild), join(repoRoot, "target", "release", file)];
23
- }
24
- export function locateDesktopEngine(options = {}) {
25
- const platform = options.platform ?? process.platform;
26
- const host = getDesktopEngineHost(platform, options.arch);
27
- const attemptedPaths = getDesktopEngineCandidatePaths(options);
28
- const isQuarantined = options.isQuarantined ?? ((enginePath) => isQuarantinedFile(enginePath, platform));
29
- const causes = [];
30
- const quarantinedPaths = [];
31
- for (const enginePath of attemptedPaths) {
32
- if (!existsSync(enginePath)) {
33
- causes.push(`${enginePath}: missing`);
34
- continue;
35
- }
36
- // Spawning a quarantined, non-notarized binary hands control to Gatekeeper instead of
37
- // returning an error. Report it; never clear the attribute on the user's behalf.
38
- if (isQuarantined(enginePath)) {
39
- quarantinedPaths.push(enginePath);
40
- causes.push(`${enginePath}: blocked because ${QUARANTINE_ATTRIBUTE} is present (macOS Gatekeeper)`);
41
- continue;
42
- }
43
- if (!isExecutable(enginePath)) {
44
- causes.push(`${enginePath}: not executable (chmod +x)`);
45
- continue;
46
- }
47
- return { path: enginePath, diagnostic: null };
48
- }
49
- const code = quarantinedPaths.length > 0 ? "quarantined" : "native-unavailable";
50
- const message = code === "quarantined"
51
- ? `The ${DESKTOP_ENGINE_BINARY} binary for ${host} is quarantined by macOS Gatekeeper: ${quarantinedPaths.join(", ")}.`
52
- : `No ${DESKTOP_ENGINE_BINARY} binary is available for ${host}.`;
53
- return { path: null, diagnostic: { code, host, attemptedPaths, message, cause: causes.join("; ") } };
54
- }
55
- /**
56
- * Node exposes no `getxattr`, so `xattr -p` is the only channel; it exits 0 only when the
57
- * attribute is present. A failed probe returns `false`: an unreadable attribute never rejects.
58
- */
59
- export function isQuarantinedFile(enginePath, platform = process.platform) {
60
- if (platform !== "darwin")
61
- return false;
62
- const probe = spawnSync("/usr/bin/xattr", ["-p", QUARANTINE_ATTRIBUTE, enginePath], {
63
- stdio: ["ignore", "ignore", "ignore"],
64
- timeout: 2000,
65
- });
66
- return probe.error === undefined && probe.status === 0;
67
- }
68
- function isExecutable(enginePath) {
69
- try {
70
- accessSync(enginePath, constants.X_OK);
71
- return true;
72
- }
73
- catch (error) {
74
- if (error instanceof Error && "code" in error)
75
- return false;
76
- throw error;
77
- }
78
- }
79
- //# sourceMappingURL=locator.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"locator.js","sourceRoot":"","sources":["../src/locator.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from \"node:child_process\";\nimport { accessSync, constants, existsSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const DESKTOP_ENGINE_BINARY = \"senpi-desktop-engine\";\nexport const QUARANTINE_ATTRIBUTE = \"com.apple.quarantine\";\n\n/** `quarantined` wins over `native-unavailable`: it names the one fix the user can apply. */\nexport type DesktopEngineLocateDiagnosticCode = \"native-unavailable\" | \"quarantined\";\n\nexport interface DesktopEngineLocateDiagnostic {\n\treadonly code: DesktopEngineLocateDiagnosticCode;\n\treadonly host: string;\n\treadonly attemptedPaths: readonly string[];\n\treadonly message: string;\n\treadonly cause: string;\n}\n\nexport type DesktopEngineLocation =\n\t| { readonly path: string; readonly diagnostic: null }\n\t| { readonly path: null; readonly diagnostic: DesktopEngineLocateDiagnostic };\n\n/** Reports whether a candidate carries macOS `com.apple.quarantine`; injectable for tests. */\nexport type DesktopEngineQuarantineProbe = (enginePath: string) => boolean;\n\nexport interface DesktopEngineLocatorOptions {\n\treadonly platform?: string;\n\treadonly arch?: string;\n\t/** Directory of the running executable; a compiled senpi ships the engine as a sidecar beside it. */\n\treadonly execDir?: string;\n\t/** Root of this package, which holds `native/prebuilds/<host>/`. */\n\treadonly packageDir?: string;\n\t/** Repository root whose `target/release/` holds a dev build. */\n\treadonly repoRoot?: string;\n\treadonly isQuarantined?: DesktopEngineQuarantineProbe;\n}\n\nconst defaultPackageDir = join(dirname(fileURLToPath(import.meta.url)), \"..\");\n\nexport function getDesktopEngineHost(platform: string = process.platform, arch: string = process.arch): string {\n\treturn `${platform}-${arch}`;\n}\n\nexport function getDesktopEngineFileName(platform: string = process.platform): string {\n\treturn platform === \"win32\" ? `${DESKTOP_ENGINE_BINARY}.exe` : DESKTOP_ENGINE_BINARY;\n}\n\n/** Candidates in priority order: compiled sidecar, vendored package prebuild, dev `target/release`. */\nexport function getDesktopEngineCandidatePaths(options: DesktopEngineLocatorOptions = {}): readonly string[] {\n\tconst host = getDesktopEngineHost(options.platform, options.arch);\n\tconst file = getDesktopEngineFileName(options.platform);\n\tconst packageDir = options.packageDir ?? defaultPackageDir;\n\tconst execDir = options.execDir ?? dirname(process.execPath);\n\tconst repoRoot = options.repoRoot ?? join(packageDir, \"..\", \"..\");\n\tconst prebuild = join(\"native\", \"prebuilds\", host, file);\n\treturn [join(execDir, prebuild), join(packageDir, prebuild), join(repoRoot, \"target\", \"release\", file)];\n}\n\nexport function locateDesktopEngine(options: DesktopEngineLocatorOptions = {}): DesktopEngineLocation {\n\tconst platform = options.platform ?? process.platform;\n\tconst host = getDesktopEngineHost(platform, options.arch);\n\tconst attemptedPaths = getDesktopEngineCandidatePaths(options);\n\tconst isQuarantined = options.isQuarantined ?? ((enginePath: string) => isQuarantinedFile(enginePath, platform));\n\tconst causes: string[] = [];\n\tconst quarantinedPaths: string[] = [];\n\n\tfor (const enginePath of attemptedPaths) {\n\t\tif (!existsSync(enginePath)) {\n\t\t\tcauses.push(`${enginePath}: missing`);\n\t\t\tcontinue;\n\t\t}\n\t\t// Spawning a quarantined, non-notarized binary hands control to Gatekeeper instead of\n\t\t// returning an error. Report it; never clear the attribute on the user's behalf.\n\t\tif (isQuarantined(enginePath)) {\n\t\t\tquarantinedPaths.push(enginePath);\n\t\t\tcauses.push(`${enginePath}: blocked because ${QUARANTINE_ATTRIBUTE} is present (macOS Gatekeeper)`);\n\t\t\tcontinue;\n\t\t}\n\t\tif (!isExecutable(enginePath)) {\n\t\t\tcauses.push(`${enginePath}: not executable (chmod +x)`);\n\t\t\tcontinue;\n\t\t}\n\t\treturn { path: enginePath, diagnostic: null };\n\t}\n\n\tconst code = quarantinedPaths.length > 0 ? \"quarantined\" : \"native-unavailable\";\n\tconst message =\n\t\tcode === \"quarantined\"\n\t\t\t? `The ${DESKTOP_ENGINE_BINARY} binary for ${host} is quarantined by macOS Gatekeeper: ${quarantinedPaths.join(\", \")}.`\n\t\t\t: `No ${DESKTOP_ENGINE_BINARY} binary is available for ${host}.`;\n\treturn { path: null, diagnostic: { code, host, attemptedPaths, message, cause: causes.join(\"; \") } };\n}\n\n/**\n * Node exposes no `getxattr`, so `xattr -p` is the only channel; it exits 0 only when the\n * attribute is present. A failed probe returns `false`: an unreadable attribute never rejects.\n */\nexport function isQuarantinedFile(enginePath: string, platform: string = process.platform): boolean {\n\tif (platform !== \"darwin\") return false;\n\tconst probe = spawnSync(\"/usr/bin/xattr\", [\"-p\", QUARANTINE_ATTRIBUTE, enginePath], {\n\t\tstdio: [\"ignore\", \"ignore\", \"ignore\"],\n\t\ttimeout: 2000,\n\t});\n\treturn probe.error === undefined && probe.status === 0;\n}\n\nfunction isExecutable(enginePath: string): boolean {\n\ttry {\n\t\taccessSync(enginePath, constants.X_OK);\n\t\treturn true;\n\t} catch (error) {\n\t\tif (error instanceof Error && \"code\" in error) return false;\n\t\tthrow error;\n\t}\n}\n"]}
@@ -1,160 +0,0 @@
1
- #!/usr/bin/env node
2
- import { spawn } from "node:child_process";
3
- import { constants as fsConstants } from "node:fs";
4
- import { access, chmod, copyFile, mkdir, readFile } from "node:fs/promises";
5
- import { dirname, join } from "node:path";
6
- import { fileURLToPath } from "node:url";
7
-
8
- const scriptDir = dirname(fileURLToPath(import.meta.url));
9
- const defaultRootDir = join(scriptDir, "..", "..", "..");
10
-
11
- /** What this gate vendors. The pty gate is the same shape for a napi `.node`. */
12
- export const PREBUILD = {
13
- crate: "senpi-desktop-engine",
14
- packagePath: ["packages", "desktop-engine"],
15
- targetDirName: "senpi-desktop-engine-prebuild",
16
- /** Compile-time env the binary embeds; unset so the vendored bytes are reproducible. */
17
- volatileBuildEnv: ["SENPI_DESKTOP_BUILD_SHA"],
18
- };
19
-
20
- const rustTargetByHost = new Map([
21
- ["darwin-arm64", "aarch64-apple-darwin"],
22
- ["darwin-x64", "x86_64-apple-darwin"],
23
- ["linux-arm64", "aarch64-unknown-linux-gnu"],
24
- ["linux-x64", "x86_64-unknown-linux-gnu"],
25
- ["win32-arm64", "aarch64-pc-windows-msvc"],
26
- ["win32-x64", "x86_64-pc-windows-msvc"],
27
- ]);
28
-
29
- export function getHost(platform = process.platform, arch = process.arch) {
30
- return `${platform}-${arch}`;
31
- }
32
-
33
- export function getBinaryName(host) {
34
- return host.startsWith("win32-") ? `${PREBUILD.crate}.exe` : PREBUILD.crate;
35
- }
36
-
37
- export function getVendoredPrebuildPath(rootDir, host) {
38
- return join(rootDir, ...PREBUILD.packagePath, "native", "prebuilds", host, getBinaryName(host));
39
- }
40
-
41
- export async function checkPrebuildFreshness(options = {}) {
42
- const rootDir = options.rootDir ?? defaultRootDir;
43
- const host = options.host ?? getHost();
44
- const rustTarget = options.rustTarget ?? rustTargetByHost.get(host);
45
- if (!rustTarget) {
46
- throw new Error(`error: unsupported native prebuild host target ${host}`);
47
- }
48
-
49
- const builtFile =
50
- options.builtFile ??
51
- (await buildHostPrebuild({
52
- host,
53
- rootDir,
54
- rustTarget,
55
- targetDir: join(rootDir, "target", PREBUILD.targetDirName, host),
56
- }));
57
- const vendoredFile = getVendoredPrebuildPath(rootDir, host);
58
-
59
- if (options.update) {
60
- await mkdir(dirname(vendoredFile), { recursive: true });
61
- await copyFile(builtFile, vendoredFile);
62
- await chmod(vendoredFile, 0o755);
63
- return { builtFile, host, rustTarget, status: "updated", vendoredFile };
64
- }
65
-
66
- await assertFileExists(vendoredFile, `error: missing vendored prebuild for ${host}: ${vendoredFile}`);
67
- const [builtBytes, vendoredBytes] = await Promise.all([readFile(builtFile), readFile(vendoredFile)]);
68
- if (!builtBytes.equals(vendoredBytes)) {
69
- throw new Error(
70
- `error: stale vendored prebuild for ${host}: ${vendoredFile} differs from rebuilt ${builtFile} (${rustTarget})`,
71
- );
72
- }
73
-
74
- return { builtFile, host, rustTarget, status: "fresh", vendoredFile };
75
- }
76
-
77
- async function buildHostPrebuild({ rootDir, host, rustTarget, targetDir }) {
78
- await run(
79
- "cargo",
80
- [
81
- "build",
82
- "--release",
83
- "-p",
84
- PREBUILD.crate,
85
- "--locked",
86
- "--target",
87
- rustTarget,
88
- "--target-dir",
89
- targetDir,
90
- ],
91
- rootDir,
92
- );
93
-
94
- const builtFile = join(targetDir, rustTarget, "release", getBinaryName(host));
95
- await assertFileExists(builtFile, `error: cargo build did not produce expected host target ${host}: ${builtFile}`);
96
- return builtFile;
97
- }
98
-
99
- function run(command, args, cwd) {
100
- const env = { ...process.env, RUSTFLAGS: appendRustflags(process.env.RUSTFLAGS, `--remap-path-prefix=${cwd}=.`) };
101
- for (const name of PREBUILD.volatileBuildEnv) delete env[name];
102
- return new Promise((resolve, reject) => {
103
- const child = spawn(command, args, { cwd, env, shell: false, stdio: "inherit" });
104
- child.on("error", reject);
105
- child.on("close", (code) => {
106
- if (code === 0) {
107
- resolve();
108
- } else {
109
- reject(new Error(`error: ${command} ${args.join(" ")} failed with exit ${code ?? 1}`));
110
- }
111
- });
112
- });
113
- }
114
-
115
- function appendRustflags(current, flag) {
116
- return current ? `${current} ${flag}` : flag;
117
- }
118
-
119
- async function assertFileExists(file, message) {
120
- try {
121
- await access(file, fsConstants.R_OK);
122
- } catch {
123
- throw new Error(message);
124
- }
125
- }
126
-
127
- function parseArgs(argv) {
128
- const options = {};
129
- for (let i = 0; i < argv.length; i++) {
130
- const arg = argv[i];
131
- if (arg === "--update") {
132
- options.update = true;
133
- } else if (arg === "--host") {
134
- options.host = argv[++i];
135
- } else if (arg.startsWith("--host=")) {
136
- options.host = arg.slice("--host=".length);
137
- } else if (arg === "--root") {
138
- options.rootDir = argv[++i];
139
- } else if (arg.startsWith("--root=")) {
140
- options.rootDir = arg.slice("--root=".length);
141
- } else if (arg === "--built-file") {
142
- options.builtFile = argv[++i];
143
- } else if (arg.startsWith("--built-file=")) {
144
- options.builtFile = arg.slice("--built-file=".length);
145
- } else {
146
- throw new Error(`error: unknown argument ${arg}`);
147
- }
148
- }
149
- return options;
150
- }
151
-
152
- if (process.argv[1] === fileURLToPath(import.meta.url)) {
153
- try {
154
- const result = await checkPrebuildFreshness(parseArgs(process.argv.slice(2)));
155
- console.log(`${result.status} native prebuild for ${result.host}: ${result.vendoredFile}`);
156
- } catch (error) {
157
- console.error(error instanceof Error ? error.message : String(error));
158
- process.exit(1);
159
- }
160
- }
@@ -1,20 +0,0 @@
1
- export interface NativeDesktopEngineUnavailableDiagnostic {
2
- readonly code: "native-unavailable";
3
- readonly host: string;
4
- readonly attemptedPath: string;
5
- readonly message: string;
6
- }
7
-
8
- export type NativeDesktopEngineResult =
9
- | {
10
- readonly path: string;
11
- readonly diagnostic: null;
12
- }
13
- | {
14
- readonly path: null;
15
- readonly diagnostic: NativeDesktopEngineUnavailableDiagnostic;
16
- };
17
-
18
- export function resolveNativeDesktopEngine(): NativeDesktopEngineResult;
19
-
20
- export const nativeDesktopEngine: NativeDesktopEngineResult;
@@ -1,23 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { dirname, join } from "node:path";
3
- import { fileURLToPath } from "node:url";
4
-
5
- export function resolveNativeDesktopEngine() {
6
- const host = `${process.platform}-${process.arch}`;
7
- const file = process.platform === "win32" ? "senpi-desktop-engine.exe" : "senpi-desktop-engine";
8
- const attemptedPath = join(dirname(fileURLToPath(import.meta.url)), "prebuilds", host, file);
9
- if (existsSync(attemptedPath)) {
10
- return { path: attemptedPath, diagnostic: null };
11
- }
12
- return {
13
- path: null,
14
- diagnostic: {
15
- code: "native-unavailable",
16
- host,
17
- attemptedPath,
18
- message: `No @code-yeongyu/senpi-desktop-engine prebuild is vendored for ${host}.`,
19
- },
20
- };
21
- }
22
-
23
- export const nativeDesktopEngine = resolveNativeDesktopEngine();