@nyte-ai/host 0.0.0-stage → 0.0.19

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 (250) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +6 -2
  3. package/THIRD-PARTY-NOTICES.md +866 -0
  4. package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
  5. package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
  6. package/dist/chunks/canvas-preview-11nadadc.js +241 -0
  7. package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
  8. package/dist/chunks/canvas-preview-2jwbe14k.js +130 -0
  9. package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
  10. package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
  11. package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
  12. package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
  13. package/dist/chunks/canvas-preview-518dh8by.js +169 -0
  14. package/dist/chunks/canvas-preview-55s0y68g.js +387 -0
  15. package/dist/chunks/canvas-preview-5ekjx6sw.js +613 -0
  16. package/dist/chunks/canvas-preview-5mex725c.js +14 -0
  17. package/dist/chunks/canvas-preview-5z63am63.js +295 -0
  18. package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
  19. package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
  20. package/dist/chunks/canvas-preview-8xc0vn7v.js +458 -0
  21. package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
  22. package/dist/chunks/canvas-preview-af86jz8t.js +141 -0
  23. package/dist/chunks/canvas-preview-aq5j4p25.js +240 -0
  24. package/dist/chunks/canvas-preview-b2v8wfnz.js +36 -0
  25. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  26. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  27. package/dist/chunks/canvas-preview-cawy6vbz.js +352 -0
  28. package/dist/chunks/canvas-preview-crmzxxt8.js +518 -0
  29. package/dist/chunks/canvas-preview-drctegz4.js +591 -0
  30. package/dist/chunks/canvas-preview-e3vnkkc5.js +134 -0
  31. package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
  32. package/dist/chunks/canvas-preview-fxa0rfb0.js +61 -0
  33. package/dist/chunks/canvas-preview-g5fkjs34.js +598 -0
  34. package/dist/chunks/canvas-preview-jeddjex8.js +166 -0
  35. package/dist/chunks/canvas-preview-k1yg4fgp.js +571 -0
  36. package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
  37. package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
  38. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  39. package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
  40. package/dist/chunks/canvas-preview-n2a35zv5.js +81 -0
  41. package/dist/chunks/canvas-preview-pkzqg96v.js +63 -0
  42. package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
  43. package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
  44. package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
  45. package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
  46. package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
  47. package/dist/chunks/canvas-preview-tnqe7k0b.js +374 -0
  48. package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
  49. package/dist/chunks/canvas-preview-vm9zdp2t.js +398 -0
  50. package/dist/chunks/canvas-preview-w54ymtb3.js +272 -0
  51. package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
  52. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  53. package/dist/chunks/canvas-preview-ycepb05s.js +176 -0
  54. package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
  55. package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
  56. package/dist/src/canvas-preview.d.ts +29 -0
  57. package/dist/src/canvas-preview.js +271 -0
  58. package/dist/src/credentials.d.ts +44 -0
  59. package/dist/src/credentials.js +10 -0
  60. package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
  61. package/dist/src/deploy/cloudflare/worker.js +196 -0
  62. package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
  63. package/dist/src/deploy/vercel/nitro.config.js +30 -0
  64. package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
  65. package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
  66. package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
  67. package/dist/src/deploy/vercel/server/utils/host.js +7 -0
  68. package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
  69. package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
  70. package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
  71. package/dist/src/deploy/vercel/server/utils/step.js +6 -0
  72. package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
  73. package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
  74. package/dist/src/durable-object.d.ts +14 -0
  75. package/dist/src/durable-object.js +231 -0
  76. package/dist/src/hash.d.ts +1 -0
  77. package/dist/src/hash.js +6 -0
  78. package/dist/src/identity-key.d.ts +11 -0
  79. package/dist/src/identity-key.js +9 -0
  80. package/dist/src/identity.d.ts +12 -0
  81. package/dist/src/identity.js +6 -0
  82. package/dist/src/images/exif.d.ts +1 -0
  83. package/dist/src/images/exif.js +6 -0
  84. package/dist/src/images/node.d.ts +37 -0
  85. package/dist/src/images/node.js +12 -0
  86. package/dist/src/images/photon.d.ts +3 -0
  87. package/dist/src/images/photon.js +7 -0
  88. package/dist/src/images/worker.d.ts +1 -0
  89. package/dist/src/images/worker.js +23 -0
  90. package/dist/src/index.d.ts +20 -0
  91. package/dist/src/index.js +121 -0
  92. package/dist/src/key-file.d.ts +1 -0
  93. package/dist/src/key-file.js +7 -0
  94. package/dist/src/listen-fetch.d.ts +8 -0
  95. package/dist/src/listen-fetch.js +9 -0
  96. package/dist/src/listen.d.ts +4 -0
  97. package/dist/src/listen.js +10 -0
  98. package/dist/src/local/command.d.ts +20 -0
  99. package/dist/src/local/command.js +12 -0
  100. package/dist/src/local/feed.d.ts +7 -0
  101. package/dist/src/local/feed.js +6 -0
  102. package/dist/src/local/files.d.ts +18 -0
  103. package/dist/src/local/files.js +13 -0
  104. package/dist/src/local/git-watch.d.ts +15 -0
  105. package/dist/src/local/git-watch.js +7 -0
  106. package/dist/src/local/git.d.ts +5 -0
  107. package/dist/src/local/git.js +13 -0
  108. package/dist/src/local/github.d.ts +30 -0
  109. package/dist/src/local/github.js +9 -0
  110. package/dist/src/local/identity.d.ts +3 -0
  111. package/dist/src/local/identity.js +7 -0
  112. package/dist/src/local/jobs.d.ts +7 -0
  113. package/dist/src/local/jobs.js +7 -0
  114. package/dist/src/local/legacy-trust.d.ts +10 -0
  115. package/dist/src/local/legacy-trust.js +6 -0
  116. package/dist/src/local/patch.d.ts +16 -0
  117. package/dist/src/local/patch.js +17 -0
  118. package/dist/src/local/provider.d.ts +11 -0
  119. package/dist/src/local/provider.js +20 -0
  120. package/dist/src/local/snapshots.d.ts +7 -0
  121. package/dist/src/local/snapshots.js +11 -0
  122. package/dist/src/local/text.d.ts +1 -0
  123. package/dist/src/local/text.js +6 -0
  124. package/dist/src/local/tree.d.ts +50 -0
  125. package/dist/src/local/tree.js +16 -0
  126. package/dist/src/local/workspace-migration.d.ts +2 -0
  127. package/dist/src/local/workspace-migration.js +7 -0
  128. package/dist/src/local/worktrees.d.ts +46 -0
  129. package/dist/src/local/worktrees.js +14 -0
  130. package/dist/src/mcp/config.d.ts +75 -0
  131. package/dist/src/mcp/config.js +12 -0
  132. package/dist/src/mcp/connect.d.ts +15 -0
  133. package/dist/src/mcp/connect.js +9 -0
  134. package/dist/src/mcp/hub.d.ts +19 -0
  135. package/dist/src/mcp/hub.js +11 -0
  136. package/dist/src/mcp/oauth.d.ts +46 -0
  137. package/dist/src/mcp/oauth.js +12 -0
  138. package/dist/src/mcp/tools.d.ts +29 -0
  139. package/dist/src/mcp/tools.js +12 -0
  140. package/dist/src/open.d.ts +14 -0
  141. package/dist/src/open.js +46 -0
  142. package/dist/src/plugins/loader.d.ts +21 -0
  143. package/dist/src/plugins/loader.js +11 -0
  144. package/dist/src/postgres.d.ts +19 -0
  145. package/dist/src/postgres.js +8 -0
  146. package/dist/src/providers.d.ts +16 -0
  147. package/dist/src/providers.js +112 -0
  148. package/dist/src/secrets.d.ts +12 -0
  149. package/dist/src/secrets.js +8 -0
  150. package/dist/src/serve/binding.d.ts +39 -0
  151. package/dist/src/serve/binding.js +6 -0
  152. package/dist/src/serve/handler.d.ts +15 -0
  153. package/dist/src/serve/handler.js +8 -0
  154. package/dist/src/serve/stream-server.d.ts +6 -0
  155. package/dist/src/serve/stream-server.js +6 -0
  156. package/dist/src/session-index.d.ts +11 -0
  157. package/dist/src/session-index.js +11 -0
  158. package/dist/src/shell-env.d.ts +14 -0
  159. package/dist/src/shell-env.js +108 -0
  160. package/dist/src/sqlite-workers.d.ts +6 -0
  161. package/dist/src/sqlite-workers.js +6 -0
  162. package/dist/src/sqlite.d.ts +8 -0
  163. package/dist/src/sqlite.js +11 -0
  164. package/dist/src/store-rows.d.ts +13 -0
  165. package/dist/src/store-rows.js +26 -0
  166. package/dist/src/store-usage.d.ts +25 -0
  167. package/dist/src/store-usage.js +19 -0
  168. package/dist/src/usage-history.d.ts +54 -0
  169. package/dist/src/usage-history.js +14 -0
  170. package/dist/src/usage-scan.d.ts +24 -0
  171. package/dist/src/usage-scan.js +65 -0
  172. package/dist/src/usage-service.d.ts +23 -0
  173. package/dist/src/usage-service.js +16 -0
  174. package/dist/src/usage-summary.d.ts +9 -0
  175. package/dist/src/usage-summary.js +16 -0
  176. package/dist/src/usage-types.d.ts +86 -0
  177. package/dist/src/usage-types.js +0 -0
  178. package/dist/src/usage-worker.d.ts +1 -0
  179. package/dist/src/usage-worker.js +29 -0
  180. package/dist/src/usage.d.ts +195 -0
  181. package/dist/src/usage.js +23 -0
  182. package/package.json +140 -3
  183. package/src/canvas-preview.ts +403 -0
  184. package/src/credentials.ts +294 -0
  185. package/src/deploy/cloudflare/README.md +3 -0
  186. package/src/deploy/cloudflare/worker.ts +306 -0
  187. package/src/deploy/cloudflare/wrangler.jsonc +12 -0
  188. package/src/deploy/vercel/README.md +3 -0
  189. package/src/deploy/vercel/nitro.config.ts +29 -0
  190. package/src/deploy/vercel/server/routes/[...].ts +10 -0
  191. package/src/deploy/vercel/server/utils/host.ts +104 -0
  192. package/src/deploy/vercel/server/utils/repair.ts +35 -0
  193. package/src/deploy/vercel/server/utils/step.ts +25 -0
  194. package/src/deploy/vercel/server/workflows/drive.ts +46 -0
  195. package/src/deploy/vercel/tsconfig.json +9 -0
  196. package/src/deploy/vercel/types/bun.d.ts +2 -0
  197. package/src/durable-object.ts +386 -0
  198. package/src/hash.ts +5 -0
  199. package/src/identity-key.ts +20 -0
  200. package/src/identity.ts +66 -0
  201. package/src/images/exif.ts +69 -0
  202. package/src/images/node.ts +163 -0
  203. package/src/images/photon.ts +141 -0
  204. package/src/images/worker.ts +29 -0
  205. package/src/index.ts +39 -0
  206. package/src/key-file.ts +53 -0
  207. package/src/listen-fetch.ts +409 -0
  208. package/src/listen.ts +228 -0
  209. package/src/local/command.ts +161 -0
  210. package/src/local/feed.ts +70 -0
  211. package/src/local/files.ts +817 -0
  212. package/src/local/git-watch.ts +199 -0
  213. package/src/local/git.ts +919 -0
  214. package/src/local/github.ts +691 -0
  215. package/src/local/identity.ts +21 -0
  216. package/src/local/jobs.ts +573 -0
  217. package/src/local/legacy-trust.ts +105 -0
  218. package/src/local/patch.ts +334 -0
  219. package/src/local/provider.ts +184 -0
  220. package/src/local/snapshots.ts +778 -0
  221. package/src/local/text.ts +9 -0
  222. package/src/local/tree.ts +398 -0
  223. package/src/local/workspace-migration.ts +95 -0
  224. package/src/local/worktrees.ts +239 -0
  225. package/src/mcp/config.ts +330 -0
  226. package/src/mcp/connect.ts +92 -0
  227. package/src/mcp/hub.ts +836 -0
  228. package/src/mcp/oauth.ts +436 -0
  229. package/src/mcp/tools.ts +333 -0
  230. package/src/open.ts +449 -0
  231. package/src/plugins/loader.ts +186 -0
  232. package/src/postgres.ts +492 -0
  233. package/src/providers.ts +135 -0
  234. package/src/secrets.ts +31 -0
  235. package/src/serve/binding.ts +629 -0
  236. package/src/serve/handler.ts +362 -0
  237. package/src/serve/stream-server.ts +176 -0
  238. package/src/session-index.ts +99 -0
  239. package/src/shell-env.ts +153 -0
  240. package/src/sqlite-workers.ts +283 -0
  241. package/src/sqlite.ts +553 -0
  242. package/src/store-rows.ts +110 -0
  243. package/src/store-usage.ts +347 -0
  244. package/src/usage-history.ts +219 -0
  245. package/src/usage-scan.ts +81 -0
  246. package/src/usage-service.ts +82 -0
  247. package/src/usage-summary.ts +79 -0
  248. package/src/usage-types.ts +87 -0
  249. package/src/usage-worker.ts +24 -0
  250. package/src/usage.ts +776 -0
@@ -0,0 +1,573 @@
1
+ import { spawn } from "node:child_process";
2
+ import type { ChildProcess, SpawnOptions } from "node:child_process";
3
+ import { createRequire } from "node:module";
4
+ import { basename, resolve } from "node:path";
5
+ import type { IPty, IPtyForkOptions } from "@lydell/node-pty";
6
+ import { asId, SdkError } from "@nyte-ai/schema";
7
+ import type { JobEnd, OutputFrame, RequestOptions } from "@nyte-ai/schema";
8
+ import {
9
+ processEnvironment,
10
+ closable,
11
+ dev,
12
+ readTraceparent,
13
+ startSpan,
14
+ tracing,
15
+ } from "@nyte-ai/core";
16
+ import type { EndSpan, ExecutionEnv, Job } from "@nyte-ai/core";
17
+ import { findExecutable, killTree, run, spawnCommand } from "./command.ts";
18
+
19
+ type PtyModule = {
20
+ spawn(file: string, args: string[] | string, options: IPtyForkOptions): IPty;
21
+ };
22
+
23
+ type Driver = {
24
+ write(bytes: Uint8Array): Promise<void>;
25
+ resize(size: { readonly columns: number; readonly rows: number }): boolean;
26
+ idle(): boolean | Promise<boolean>;
27
+ kill(signal: NodeJS.Signals): void;
28
+ readonly pty: boolean;
29
+ };
30
+
31
+ const OUTPUT_LIMIT = 1024 * 1024;
32
+
33
+ /** How far the slowest reader may fall behind a pty before it pauses, and how close it must come again before the pty resumes. */
34
+ const PAUSE_BYTES = 256 * 1024;
35
+
36
+ const RESUME_BYTES = 64 * 1024;
37
+
38
+ const STOP_GRACE_MS = 2_000;
39
+
40
+ const CLOSE_GRACE_MS = 2_000;
41
+
42
+ const DONE = { done: true, value: undefined } as const;
43
+
44
+ let loaded: PtyModule | null | undefined;
45
+
46
+ /** Returns `exec` for a folder: each call starts one process as a `Job` (a fresh id; `key` null, core keys pty jobs) in `cwd` resolved against `root` (default `root`), with `env` laid over `process.env`. A string command runs through the platform shell (`/bin/sh -c`, `cmd.exe /d /s /c` on Windows) and an argv directly; `[]` with `pty` runs the login shell (`shell()` read at each start, else `$SHELL`, else `/bin/sh`, as a login shell; §37h), a pty's environment carrying `TERM=xterm-256color`, `COLORTERM=truecolor` and `TERM_PROGRAM=Nyte` without Electron's `ELECTRON_RUN_AS_NODE` and `NODE_OPTIONS`, and `[]` without one throws `SdkError("invalid")`. A program that cannot start throws `SdkError("unavailable")`. Without `pty` the process is piped through `node:child_process` in its own process group, stdout and stderr interleaved in `output` (with `stdio: "protocol"`, stderr's frames tagged `stream: "stderr"` and the pipe paced like a pty, so a reader open from the start misses nothing) and stdin fed by `write`; with `pty` it runs under `@lydell/node-pty`, loaded on first use through `createRequire` (§41a; `loadPty`, given, loads it instead, on every pty start), at the given size, and a platform where it does not load throws `SdkError("unavailable")`. `output()` opens a reader over the last 1 MiB of output that yields `bytes`, a `gap` where the reader fell behind or opened after output was dropped, and `exit { code }` last; a merged pipe never stalls, and a pty or a protocol pipe pauses while its slowest open reader is 256 KiB behind, resuming once it is within 64 KiB. `resize` resizes a pty (`not-pty` for a pipe, `missing` once it ended); `idle` checks the OS process table for a login shell with no live children (false for a pipe or explicit command, true once it ended); `stop` sends SIGTERM to a pipe's process group (SIGHUP to a pty), SIGKILL after 2 s (on Windows, which has no signals, it ends a pipe's process tree at once with `taskkill /T /F`), and answers `settled { outcome }` (also once it ended); `settled` resolves `exited { code, reason }` (`code` null after a signal), `reason` `exited` when the process ended on its own and `terminated` after `stop` or an aborted `signal`, and an aborted `settled` signal rejects with `SdkError("unavailable")`; `close` detaches this handle's readers. An aborted `signal` stops the process. `signal` on the options ends every reader of every job started here, leaving the processes running (what the provider's `close` does). */
47
+ export function localExec(
48
+ root: string,
49
+ options?: {
50
+ readonly shell?: () => string | null;
51
+ readonly signal?: AbortSignal;
52
+ readonly loadPty?: () => unknown;
53
+ },
54
+ ): ExecutionEnv["exec"] {
55
+ const readers = new Set<() => void>();
56
+ const custom = options?.loadPty;
57
+ const load = custom === undefined ? loadPty : () => ptyFrom(custom);
58
+
59
+ options?.signal?.addEventListener(
60
+ "abort",
61
+ () => {
62
+ for (const close of readers) close();
63
+ },
64
+ { once: true },
65
+ );
66
+
67
+ return async (command, input) => {
68
+ if (input?.signal?.aborted === true) throw new SdkError("unavailable", "aborted");
69
+
70
+ if (typeof command !== "string" && command.length === 0 && input?.pty === undefined)
71
+ throw new SdkError("invalid", "An empty command runs only in a pty, as the login shell.");
72
+ const cwd = input?.cwd === undefined ? root : resolve(root, input.cwd);
73
+ await processEnvironment();
74
+ const env = { ...process.env, ...input?.env };
75
+ const trace = readTraceparent(input?.env?.TRACEPARENT);
76
+ const spawned = startSpan("pty.spawn", trace, { pty: input?.pty !== undefined });
77
+ const log = tracedOutput(createOutputLog(OUTPUT_LIMIT), trace);
78
+ let settle: (end: JobEnd) => void = () => undefined;
79
+
80
+ const ended = new Promise<JobEnd>((done) => {
81
+ settle = done;
82
+ });
83
+
84
+ let stopping = false;
85
+ let finished: JobEnd | null = null;
86
+
87
+ const finish = (code: number | null): void => {
88
+ if (finished !== null) return;
89
+ finished = { kind: "exited", code, reason: stopping ? "terminated" : "exited" };
90
+ dev("jobs", "exit", { code, reason: finished.reason });
91
+ log.end(code);
92
+ settle(finished);
93
+ };
94
+
95
+ const driver =
96
+ input?.pty === undefined
97
+ ? await pipe(command, { cwd, env, protocol: input?.stdio === "protocol" }, log, finish)
98
+ : await terminal(
99
+ command,
100
+ { cwd, env, size: input.pty, shell: options?.shell?.() ?? undefined, load },
101
+ log,
102
+ finish,
103
+ );
104
+
105
+ spawned();
106
+
107
+ dev("jobs", "start", {
108
+ program: typeof command === "string" ? command.split(/\s/, 1)[0] : (command[0] ?? null),
109
+ args: typeof command === "string" ? null : command.length - 1,
110
+ pty: driver.pty,
111
+ });
112
+
113
+ let stopped: Promise<JobEnd> | null = null;
114
+
115
+ const stop = (): Promise<JobEnd> => {
116
+ stopped ??= (async () => {
117
+ if (finished !== null) return finished;
118
+ stopping = true;
119
+ driver.kill(driver.pty ? "SIGHUP" : "SIGTERM");
120
+ const timer = setTimeout(() => driver.kill("SIGKILL"), STOP_GRACE_MS);
121
+
122
+ try {
123
+ return await ended;
124
+ } finally {
125
+ clearTimeout(timer);
126
+ }
127
+ })();
128
+
129
+ return stopped;
130
+ };
131
+
132
+ input?.signal?.addEventListener("abort", () => void stop(), { once: true });
133
+ const own = new Set<() => void>();
134
+
135
+ const close = async (): Promise<void> => {
136
+ for (const detach of own) detach();
137
+ };
138
+
139
+ const job: Job = {
140
+ id: asId<"job">(crypto.randomUUID()),
141
+ key: null,
142
+ write: (bytes) => driver.write(bytes),
143
+ output: () =>
144
+ closable<OutputFrame>(() => {
145
+ const reader = log.reader();
146
+
147
+ const detach = (): void => {
148
+ own.delete(detach);
149
+ readers.delete(detach);
150
+ reader.close();
151
+ };
152
+
153
+ own.add(detach);
154
+ readers.add(detach);
155
+
156
+ return { pull: () => reader.next(), close: detach };
157
+ }),
158
+ async resize(size) {
159
+ if (!driver.pty) return { kind: "not-pty" };
160
+
161
+ return finished === null && driver.resize(size) ? { kind: "resized" } : { kind: "missing" };
162
+ },
163
+ idle: async () => driver.pty && (finished !== null || driver.idle()),
164
+ stop: async () => ({ kind: "settled", outcome: await stop() }),
165
+ settled: (request) => waitEnd(ended, request),
166
+ close,
167
+ [Symbol.asyncDispose]: close,
168
+ };
169
+
170
+ return job;
171
+ };
172
+ }
173
+
174
+ async function pipe(
175
+ command: string | readonly string[],
176
+ options: { readonly cwd: string; readonly env: NodeJS.ProcessEnv; readonly protocol: boolean },
177
+ log: OutputLog,
178
+ finish: (code: number | null) => void,
179
+ ): Promise<Driver> {
180
+ const windows = process.platform === "win32";
181
+ const [file, ...args] = typeof command === "string" ? [command] : command;
182
+
183
+ const settings: SpawnOptions = {
184
+ cwd: options.cwd,
185
+ env: options.env,
186
+ detached: !windows,
187
+ stdio: "pipe",
188
+ windowsHide: true,
189
+ };
190
+
191
+ const child: ChildProcess =
192
+ typeof command === "string"
193
+ ? spawn(command, { ...settings, shell: true })
194
+ : spawnCommand(
195
+ windows ? ((await findExecutable(file ?? "")) ?? file ?? "") : (file ?? ""),
196
+ args,
197
+ settings,
198
+ );
199
+
200
+ await new Promise<void>((started, failed) => {
201
+ child.once("spawn", started);
202
+ child.once("error", (error) =>
203
+ failed(new SdkError("unavailable", `Cannot start ${file ?? ""}: ${error.message}`)),
204
+ );
205
+ });
206
+
207
+ child.stdout?.on("data", (chunk: Buffer) => log.push(chunk));
208
+ child.stderr?.on("data", (chunk: Buffer) =>
209
+ log.push(chunk, options.protocol ? "stderr" : undefined),
210
+ );
211
+
212
+ if (options.protocol)
213
+ log.throttle({
214
+ pause: () => {
215
+ child.stdout?.pause();
216
+ child.stderr?.pause();
217
+ },
218
+ resume: () => {
219
+ child.stdout?.resume();
220
+ child.stderr?.resume();
221
+ },
222
+ });
223
+ child.stdin?.on("error", () => undefined);
224
+ let killed = false;
225
+ child.on("close", (code) => finish(killed ? null : code));
226
+ child.on("exit", (code) => {
227
+ setTimeout(() => {
228
+ child.stdout?.destroy();
229
+ child.stderr?.destroy();
230
+ finish(killed ? null : code);
231
+ }, CLOSE_GRACE_MS).unref();
232
+ });
233
+
234
+ return {
235
+ pty: false,
236
+ async write(bytes) {
237
+ const stdin = child.stdin;
238
+
239
+ if (stdin === null || !stdin.writable)
240
+ throw new SdkError("unavailable", "The process no longer reads its input.");
241
+ await new Promise<void>((written) => stdin.write(bytes, () => written()));
242
+ },
243
+ resize: () => false,
244
+ idle: () => false,
245
+ kill(signal) {
246
+ if (windows) {
247
+ killed = true;
248
+ killTree(child, signal);
249
+
250
+ return;
251
+ }
252
+
253
+ try {
254
+ if (child.pid !== undefined) process.kill(-child.pid, signal);
255
+ else child.kill(signal);
256
+ } catch {
257
+ child.kill(signal);
258
+ }
259
+ },
260
+ };
261
+ }
262
+
263
+ /** The environment a pty starts with: the process's, as a terminal emulator announces itself, without the variables that would make a child Node or Electron run as the host does. */
264
+ function terminalEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
265
+ const { ELECTRON_RUN_AS_NODE: _electron, NODE_OPTIONS: _node, ...rest } = env;
266
+
267
+ return { ...rest, TERM: "xterm-256color", COLORTERM: "truecolor", TERM_PROGRAM: "Nyte" };
268
+ }
269
+
270
+ async function terminal(
271
+ command: string | readonly string[],
272
+ options: {
273
+ readonly cwd: string;
274
+ readonly env: NodeJS.ProcessEnv;
275
+ readonly size: { readonly columns: number; readonly rows: number };
276
+ readonly shell: string | undefined;
277
+ readonly load: () => PtyModule | null;
278
+ },
279
+ log: OutputLog,
280
+ finish: (code: number | null) => void,
281
+ ): Promise<Driver> {
282
+ const pty = options.load();
283
+
284
+ if (pty === null)
285
+ throw new SdkError(
286
+ "unavailable",
287
+ "Terminals need @lydell/node-pty, which does not load on this platform.",
288
+ );
289
+ const windows = process.platform === "win32";
290
+
291
+ const shell =
292
+ options.shell ??
293
+ (windows ? (process.env.COMSPEC ?? "cmd.exe") : (process.env.SHELL ?? "/bin/sh"));
294
+
295
+ const [file, ...args] =
296
+ typeof command === "string"
297
+ ? windows
298
+ ? [shell, "/d", "/s", "/c", command]
299
+ : ["/bin/sh", "-c", command]
300
+ : command.length === 0
301
+ ? windows
302
+ ? [shell]
303
+ : [shell, "-l"]
304
+ : command;
305
+
306
+ let term: IPty;
307
+
308
+ try {
309
+ term = pty.spawn(file ?? shell, args, {
310
+ name: "xterm-256color",
311
+ cols: options.size.columns,
312
+ rows: options.size.rows,
313
+ cwd: options.cwd,
314
+ env: terminalEnv(options.env),
315
+ });
316
+ } catch (error) {
317
+ throw new SdkError(
318
+ "unavailable",
319
+ `Cannot start ${file ?? shell} in a pty: ${error instanceof Error ? error.message : String(error)}`,
320
+ );
321
+ }
322
+
323
+ const encoder = new TextEncoder();
324
+
325
+ term.onData((text) => log.push(encoder.encode(text)));
326
+ term.onExit(({ exitCode, signal }) => finish(signal ? null : exitCode));
327
+ log.throttle({ pause: () => term.pause(), resume: () => term.resume() });
328
+
329
+ return {
330
+ pty: true,
331
+ async write(bytes) {
332
+ term.write(Buffer.from(bytes));
333
+ },
334
+ resize(size) {
335
+ term.resize(size.columns, size.rows);
336
+
337
+ return true;
338
+ },
339
+ async idle() {
340
+ if (typeof command === "string" || command.length !== 0) return false;
341
+
342
+ const result = await run(
343
+ windows ? "powershell.exe" : "ps",
344
+ windows
345
+ ? [
346
+ "-NoProfile",
347
+ "-NonInteractive",
348
+ "-Command",
349
+ `Get-CimInstance Win32_Process -Filter "ParentProcessId = ${term.pid} AND Name != 'conhost.exe' AND Name != 'OpenConsole.exe'" | Select-Object -ExpandProperty ProcessId`,
350
+ ]
351
+ : ["-A", "-o", "pid=,ppid=,stat=,comm="],
352
+ { cwd: options.cwd, timeoutMs: 1_500 },
353
+ );
354
+
355
+ if (result.code !== 0) throw new SdkError("unavailable", "Cannot inspect terminal processes");
356
+ const processes = result.stdout.toString("utf8").trim();
357
+
358
+ if (windows) return processes === "";
359
+
360
+ return !processes.split("\n").some((line) => {
361
+ const [pid, parent, state, ...name] = line.trim().split(/\s+/);
362
+
363
+ if (state?.startsWith("Z")) return false;
364
+
365
+ if (Number(parent) === term.pid) return true;
366
+
367
+ return (
368
+ Number(pid) === term.pid && basename(name.join(" ")).replace(/^-/, "") !== basename(shell)
369
+ );
370
+ });
371
+ },
372
+ kill(signal) {
373
+ try {
374
+ term.kill(windows ? undefined : signal);
375
+ } catch {}
376
+ },
377
+ };
378
+ }
379
+
380
+ function loadPty(): PtyModule | null {
381
+ if (loaded === undefined)
382
+ loaded = ptyFrom(() => createRequire(import.meta.url)("@lydell/node-pty"));
383
+
384
+ return loaded;
385
+ }
386
+
387
+ function ptyFrom(load: () => unknown): PtyModule | null {
388
+ try {
389
+ const required = load();
390
+
391
+ return fitsPtyModule(required) ? required : null;
392
+ } catch {
393
+ return null;
394
+ }
395
+ }
396
+
397
+ /** Whether a required module exports `spawn`, as `@lydell/node-pty` does. */
398
+ // oxlint-disable-next-line nyte-code/no-one-off-boolean-helpers -- a guard: inlined, the check narrows `spawn` to Function, not to the module's typed signature
399
+ function fitsPtyModule(value: unknown): value is PtyModule {
400
+ return (
401
+ typeof value === "object" &&
402
+ value !== null &&
403
+ "spawn" in value &&
404
+ typeof value.spawn === "function"
405
+ );
406
+ }
407
+
408
+ function waitEnd(ended: Promise<JobEnd>, request: RequestOptions | undefined): Promise<JobEnd> {
409
+ const signal = request?.signal;
410
+
411
+ if (signal === undefined) return ended;
412
+
413
+ if (signal.aborted) return Promise.reject(new SdkError("unavailable", "aborted"));
414
+
415
+ return new Promise((settled, failed) => {
416
+ const abort = (): void => failed(new SdkError("unavailable", "aborted"));
417
+ signal.addEventListener("abort", abort, { once: true });
418
+ ended.then((end) => {
419
+ signal.removeEventListener("abort", abort);
420
+ settled(end);
421
+ }, failed);
422
+ });
423
+ }
424
+
425
+ type OutputLog = {
426
+ push(bytes: Uint8Array, stream?: "stderr"): void;
427
+ end(code: number | null): void;
428
+ reader(): { next(): Promise<IteratorResult<OutputFrame>>; close(): void };
429
+ /** Pauses the source while the slowest reader is `PAUSE_BYTES` behind, until it is back within `RESUME_BYTES`. */
430
+ throttle(source: { pause(): void; resume(): void }): void;
431
+ };
432
+
433
+ /** `log`, timing its output as the `pty.output` span of `trace` from the first byte to the exit while tracing is on. */
434
+ function tracedOutput(log: OutputLog, trace: string | null): OutputLog {
435
+ if (!tracing.enabled) return log;
436
+ let ended: EndSpan | null = null;
437
+ let bytes = 0;
438
+
439
+ return {
440
+ ...log,
441
+ push(chunk, stream) {
442
+ ended ??= startSpan("pty.output", trace);
443
+ bytes += chunk.byteLength;
444
+ log.push(chunk, stream);
445
+ },
446
+ end(code) {
447
+ ended?.({ bytes, code });
448
+ log.end(code);
449
+ },
450
+ };
451
+ }
452
+
453
+ /** A process's output kept for its readers: the last `limit` bytes, a reader that falls further behind skipping to a gap. */
454
+ function createOutputLog(limit: number): OutputLog {
455
+ const state: {
456
+ readonly chunks: Extract<OutputFrame, { readonly kind: "bytes" }>[];
457
+ first: number;
458
+ size: number;
459
+ pushed: number;
460
+ exit: { readonly code: number | null } | null;
461
+ } = { chunks: [], first: 0, size: 0, pushed: 0, exit: null };
462
+
463
+ const waiters = new Set<() => void>();
464
+ const positions = new Set<{ read: number }>();
465
+ let source: { pause(): void; resume(): void } | null = null;
466
+ let paused = false;
467
+
468
+ const behind = (): number =>
469
+ Math.max(0, ...[...positions].map((position) => state.pushed - position.read));
470
+
471
+ const pace = (): void => {
472
+ if (source === null) return;
473
+ const lag = behind();
474
+
475
+ if (!paused && lag > PAUSE_BYTES) {
476
+ paused = true;
477
+ source.pause();
478
+ } else if (paused && lag < RESUME_BYTES) {
479
+ paused = false;
480
+ source.resume();
481
+ }
482
+ };
483
+
484
+ const wake = (): void => {
485
+ const pending = [...waiters];
486
+ waiters.clear();
487
+
488
+ for (const resolve of pending) resolve();
489
+ };
490
+
491
+ return {
492
+ throttle(next) {
493
+ source = next;
494
+ },
495
+ push(bytes, stream) {
496
+ state.chunks.push(
497
+ stream === undefined
498
+ ? { kind: "bytes", data: bytes }
499
+ : { kind: "bytes", data: bytes, stream },
500
+ );
501
+ state.size += bytes.byteLength;
502
+ state.pushed += bytes.byteLength;
503
+
504
+ while (state.size > limit && state.chunks.length > 1) {
505
+ state.size -= state.chunks.shift()?.data.byteLength ?? 0;
506
+ state.first += 1;
507
+ }
508
+
509
+ pace();
510
+ wake();
511
+ },
512
+ end(code) {
513
+ state.exit ??= { code };
514
+ wake();
515
+ },
516
+ reader() {
517
+ let cursor = 0;
518
+ let done = false;
519
+ let waiting: (() => void) | null = null;
520
+ const position = { read: 0 };
521
+ positions.add(position);
522
+
523
+ return {
524
+ async next() {
525
+ for (;;) {
526
+ if (done) return DONE;
527
+
528
+ if (cursor < state.first) {
529
+ cursor = state.first;
530
+ position.read = state.pushed - state.size;
531
+
532
+ return { done: false, value: { kind: "gap" } };
533
+ }
534
+
535
+ const chunk = state.chunks[cursor - state.first];
536
+
537
+ if (chunk !== undefined) {
538
+ cursor += 1;
539
+ position.read += chunk.data.byteLength;
540
+ pace();
541
+
542
+ return { done: false, value: chunk };
543
+ }
544
+
545
+ if (state.exit !== null) {
546
+ done = true;
547
+
548
+ return { done: false, value: { kind: "exit", code: state.exit.code } };
549
+ }
550
+
551
+ await new Promise<void>((woken) => {
552
+ waiting = woken;
553
+ waiters.add(woken);
554
+ });
555
+ waiting = null;
556
+ }
557
+ },
558
+ close() {
559
+ done = true;
560
+ positions.delete(position);
561
+ pace();
562
+ const resolve = waiting;
563
+ waiting = null;
564
+
565
+ if (resolve !== null) {
566
+ waiters.delete(resolve);
567
+ resolve();
568
+ }
569
+ },
570
+ };
571
+ },
572
+ };
573
+ }
@@ -0,0 +1,105 @@
1
+ import { readFile, realpath, stat } from "node:fs/promises";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { envelope, trustRef } from "@nyte-ai/core/store";
4
+ import type { StoreConnection } from "@nyte-ai/core";
5
+ import { SdkError } from "@nyte-ai/schema";
6
+ import type { RefUpdate } from "@nyte-ai/schema";
7
+ import { Type } from "typebox";
8
+ import { Value } from "typebox/value";
9
+
10
+ const MIGRATION = "host/migrations/legacy-folder-trust-v1";
11
+
12
+ /** `~/.nyte/workspaces.json` from before the store: a folder the user trusted has `trusted: true`; `false` only means it was opened. */
13
+ const WorkspacesFile = Type.Record(Type.String(), Type.Object({ trusted: Type.Boolean() }));
14
+
15
+ /** `trust.json` from uji and early Nyte: every folder in it was trusted. */
16
+ const TrustFile = Type.Record(Type.String(), Type.Literal(true));
17
+
18
+ /**
19
+ * Carries the folders an older Nyte, or uji before it, was told to trust into
20
+ * the store, once per store: the grants in `~/.nyte/workspaces.json`,
21
+ * `~/.nyte/trust.json` and `~/.uji/trust.json` become folder trust decisions
22
+ * where the store saved none. A folder that is gone, the home folder and the
23
+ * folders above it are skipped: home holds the user's own files, and a grant
24
+ * there trusted every project under it. The files are left as they are.
25
+ */
26
+ export async function importLegacyTrust(connection: StoreConnection, home: string): Promise<void> {
27
+ for (let attempt = 0; attempt < 3; attempt += 1) {
28
+ if ((await connection.refs.get(MIGRATION)) !== null) return;
29
+ const folders = await legacyFolders(home);
30
+ const updates: RefUpdate[] = [];
31
+
32
+ for (const path of folders) {
33
+ const target = { kind: "local", path } as const;
34
+ const name = await trustRef(target);
35
+
36
+ if ((await connection.refs.get(name)) !== null) continue;
37
+ updates.push({
38
+ name,
39
+ expect: "absent",
40
+ target: await connection.objects.put(
41
+ envelope("trust", { target, trusted: true }, Date.now()),
42
+ ),
43
+ });
44
+ }
45
+
46
+ updates.push({
47
+ name: MIGRATION,
48
+ expect: "absent",
49
+ target: await connection.objects.put({ imported: folders }),
50
+ });
51
+
52
+ if ((await connection.refs.set(updates)).kind === "set") return;
53
+ }
54
+
55
+ throw new SdkError("unavailable", "Folder trust changed while Nyte was importing it.");
56
+ }
57
+
58
+ async function legacyFolders(home: string): Promise<readonly string[]> {
59
+ const [workspaces, nyte, uji] = await Promise.all([
60
+ readJson(join(home, ".nyte", "workspaces.json")),
61
+ readJson(join(home, ".nyte", "trust.json")),
62
+ readJson(join(home, ".uji", "trust.json")),
63
+ ]);
64
+
65
+ const granted = [
66
+ ...(Value.Check(WorkspacesFile, workspaces)
67
+ ? Object.entries(workspaces).flatMap(([path, row]) => (row.trusted ? [path] : []))
68
+ : []),
69
+ ...[nyte, uji].flatMap((file) => (Value.Check(TrustFile, file) ? Object.keys(file) : [])),
70
+ ];
71
+
72
+ const ownHome = await realpath(home).catch(() => home);
73
+ const found = await Promise.all(granted.map(canonicalFolder));
74
+
75
+ return [
76
+ ...new Set(
77
+ found.flatMap((path) =>
78
+ path === null || path === ownHome || ownHome.startsWith(`${path}/`) || path === "/"
79
+ ? []
80
+ : [path],
81
+ ),
82
+ ),
83
+ ];
84
+ }
85
+
86
+ async function canonicalFolder(path: string): Promise<string | null> {
87
+ if (!isAbsolute(path)) return null;
88
+ const real = await realpath(path).catch(() => null);
89
+
90
+ if (real === null || (await stat(real).catch(() => null))?.isDirectory() !== true) return null;
91
+
92
+ return real.replace(/\/+$/u, "");
93
+ }
94
+
95
+ async function readJson(path: string): Promise<unknown> {
96
+ const text = await readFile(path, "utf8").catch(() => null);
97
+
98
+ if (text === null) return null;
99
+
100
+ try {
101
+ return JSON.parse(text);
102
+ } catch {
103
+ return null;
104
+ }
105
+ }