@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,691 @@
1
+ import { homedir } from "node:os";
2
+ import type { ChildProcess } from "node:child_process";
3
+ import { Type } from "typebox";
4
+ import type { Static } from "typebox";
5
+ import { Compile } from "typebox/compile";
6
+ import type {
7
+ GitHubAccount,
8
+ GitHubAccountState,
9
+ GitHubCheck,
10
+ GitHubDeviceCode,
11
+ GitHubPullRequest,
12
+ GitHubPullRequestContext,
13
+ GitHubPullRequestOutcome,
14
+ GitHubRepository,
15
+ GitHubState,
16
+ } from "@nyte-ai/schema";
17
+ import { dev } from "@nyte-ai/core";
18
+ import type { ExecutionEnv, GitHubAccountApi, GitHubApi } from "@nyte-ai/core";
19
+ import { findExecutable, killTree, run, spawnCommand } from "./command.ts";
20
+
21
+ const ENV = {
22
+ GH_PROMPT_DISABLED: "1",
23
+ GH_NO_UPDATE_NOTIFIER: "1",
24
+ GIT_TERMINAL_PROMPT: "0",
25
+ NO_COLOR: "1",
26
+ LC_ALL: "C",
27
+ };
28
+
29
+ const OUTPUT_LIMIT = 1_000_000;
30
+
31
+ const QUERY_TIMEOUT_MS = 10_000;
32
+
33
+ /** `gh pr create` writes through the API, so it outlives a read. */
34
+ const CREATE_TIMEOUT_MS = 60_000;
35
+
36
+ /** How long `gh auth login` may take to print its one-time code. */
37
+ const SIGN_IN_CODE_TIMEOUT_MS = 30_000;
38
+
39
+ /** GitHub device codes expire after 15 minutes; the sign-in ends with them. */
40
+ const SIGN_IN_LIFETIME_MS = 15 * 60_000;
41
+
42
+ const NO_PULL_REQUEST: GitHubPullRequestContext = { kind: "none" };
43
+
44
+ const PULL_REQUEST_FIELDS =
45
+ "number,title,url,state,isDraft,headRefName,baseRefName,statusCheckRollup";
46
+
47
+ /** gh orders remotes this way to choose a base repository; the state names the one it would use. */
48
+ const REMOTE_PRIORITY = ["upstream", "github", "origin"];
49
+
50
+ /** What the person reading the message can do next. `gh`'s own output never reaches a state. */
51
+ const RECOVERY = {
52
+ update: "Update the GitHub CLI (gh), then try again.",
53
+ signIn: "GitHub rejected the CLI's login. Run gh auth login in a terminal, then try again.",
54
+ signOut: "Couldn't sign out of GitHub. Run gh auth logout in a terminal, then try again.",
55
+ account: "Couldn't reach GitHub. Try again.",
56
+ pullRequest: "Couldn't read this branch's pull request. Try again.",
57
+ createPullRequest: "Couldn't open a pull request. Run gh pr create in a terminal to see why.",
58
+ } as const;
59
+
60
+ type Command =
61
+ | { readonly kind: "missing" }
62
+ | { readonly kind: "failed" }
63
+ | {
64
+ readonly kind: "completed";
65
+ readonly code: number;
66
+ readonly stdout: string;
67
+ readonly stderr: string;
68
+ };
69
+
70
+ type Auth =
71
+ | { readonly kind: "missing" }
72
+ | { readonly kind: "signed_out" }
73
+ | { readonly kind: "error"; readonly message: string }
74
+ | { readonly kind: "ready"; readonly login: string };
75
+
76
+ const authSchema = Compile(
77
+ Type.Array(Type.Object({ active: Type.Boolean(), state: Type.String(), login: Type.String() })),
78
+ );
79
+
80
+ const accountSchema = Compile(
81
+ Type.Object({
82
+ login: Type.String({ minLength: 1 }),
83
+ name: Type.Union([Type.String(), Type.Null()]),
84
+ avatar_url: Type.Union([Type.String(), Type.Null()]),
85
+ }),
86
+ );
87
+
88
+ const optionalText = Type.Optional(Type.Union([Type.String(), Type.Null()]));
89
+
90
+ const PullRequestJson = Type.Object({
91
+ number: Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER }),
92
+ title: Type.String(),
93
+ url: Type.String(),
94
+ state: Type.Enum(["OPEN", "CLOSED", "MERGED"]),
95
+ isDraft: Type.Boolean(),
96
+ headRefName: Type.String({ minLength: 1 }),
97
+ baseRefName: Type.String({ minLength: 1 }),
98
+ statusCheckRollup: Type.Union([
99
+ Type.Null(),
100
+ Type.Array(
101
+ Type.Object({
102
+ __typename: Type.String(),
103
+ name: optionalText,
104
+ context: optionalText,
105
+ status: optionalText,
106
+ conclusion: optionalText,
107
+ state: optionalText,
108
+ detailsUrl: optionalText,
109
+ targetUrl: optionalText,
110
+ }),
111
+ ),
112
+ ]),
113
+ });
114
+
115
+ const pullRequestSchema = Compile(PullRequestJson);
116
+
117
+ type RollupEntry = NonNullable<Static<typeof PullRequestJson>["statusCheckRollup"]>[number];
118
+
119
+ function parseJson<T>(validator: { Parse(value: unknown): T }, text: string): T | undefined {
120
+ try {
121
+ return validator.Parse(JSON.parse(text));
122
+ } catch {
123
+ return undefined;
124
+ }
125
+ }
126
+
127
+ /** An https URL on one of `hostnames` (any host when absent), without credentials or a port; anything else is dropped. */
128
+ function safeUrl(
129
+ value: string | null | undefined,
130
+ hostnames?: readonly string[],
131
+ ): string | undefined {
132
+ const url = URL.parse(value ?? "");
133
+
134
+ if (
135
+ url === null ||
136
+ url.protocol !== "https:" ||
137
+ url.username !== "" ||
138
+ url.password !== "" ||
139
+ url.port !== "" ||
140
+ (hostnames !== undefined && !hostnames.includes(url.hostname))
141
+ )
142
+ return undefined;
143
+
144
+ return url.href;
145
+ }
146
+
147
+ const REPOSITORY_PART = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/u;
148
+
149
+ /** Only the repository's identity is read from a remote URL: userinfo and the raw URL are discarded. */
150
+ function parseRemote(remoteName: string, remote: string): GitHubRepository | undefined {
151
+ const scp = /^(?:[^@\s]+@)?github\.com:([^/\s]+)\/([^/\s]+?)\/?$/iu.exec(remote);
152
+ const url = scp === null ? URL.parse(remote) : null;
153
+
154
+ const parts =
155
+ scp === null
156
+ ? url !== null && url.hostname.toLowerCase() === "github.com"
157
+ ? url.pathname.split("/").filter((segment) => segment !== "")
158
+ : []
159
+ : [scp[1] ?? "", scp[2] ?? ""];
160
+
161
+ if (parts.length !== 2) return undefined;
162
+ const owner = parts[0] ?? "";
163
+ const name = (parts[1] ?? "").replace(/\.git$/iu, "");
164
+
165
+ if (!REPOSITORY_PART.test(owner) || !REPOSITORY_PART.test(name)) return undefined;
166
+
167
+ return { owner, name, remoteName, url: `https://github.com/${owner}/${name}` };
168
+ }
169
+
170
+ /** Folds a check run's status and conclusion, or a commit status's state, to the state a reader acts on. */
171
+ function checkState(entry: RollupEntry): GitHubCheck["state"] {
172
+ if (entry.__typename === "StatusContext") {
173
+ if (entry.state === "SUCCESS") return "passed";
174
+
175
+ return entry.state === "ERROR" || entry.state === "FAILURE" ? "failed" : "pending";
176
+ }
177
+
178
+ if (entry.status !== "COMPLETED") return "pending";
179
+
180
+ switch (entry.conclusion) {
181
+ case "SUCCESS":
182
+ case "NEUTRAL":
183
+ return "passed";
184
+ case "SKIPPED":
185
+ case "STALE":
186
+ return "skipped";
187
+ default:
188
+ return "failed";
189
+ }
190
+ }
191
+
192
+ function readCheck(entry: RollupEntry): GitHubCheck {
193
+ const url = safeUrl(entry.detailsUrl ?? entry.targetUrl);
194
+ const check = { name: entry.name ?? entry.context ?? "Check", state: checkState(entry) };
195
+
196
+ return url === undefined ? check : { ...check, url };
197
+ }
198
+
199
+ function readPullRequestJson(text: string): GitHubPullRequest | undefined {
200
+ const pull = parseJson(pullRequestSchema, text);
201
+ const url = safeUrl(pull?.url, ["github.com"]);
202
+
203
+ if (pull === undefined || url === undefined) return undefined;
204
+
205
+ return {
206
+ number: pull.number,
207
+ title: pull.title,
208
+ url,
209
+ state: pull.state,
210
+ draft: pull.isDraft,
211
+ headRefName: pull.headRefName,
212
+ baseRefName: pull.baseRefName,
213
+ checks: (pull.statusCheckRollup ?? []).map(readCheck),
214
+ };
215
+ }
216
+
217
+ function readAccountJson(text: string): GitHubAccount | undefined {
218
+ const account = parseJson(accountSchema, text);
219
+
220
+ if (account === undefined) return undefined;
221
+ const avatarUrl = safeUrl(account.avatar_url, ["avatars.githubusercontent.com"]);
222
+
223
+ return {
224
+ login: account.login,
225
+ ...(account.name === null || account.name === "" ? {} : { name: account.name }),
226
+ ...(avatarUrl === undefined ? {} : { avatarUrl }),
227
+ };
228
+ }
229
+
230
+ const USER_CODE = /one-time code\W*([A-Z0-9]{4}-[A-Z0-9]{4})\b/iu;
231
+
232
+ const PRINTED_URL = /https:\/\/\S+/u;
233
+
234
+ /** Sign-in output is scanned line by line; a longer line cannot hold the code. */
235
+ const SIGN_IN_LINE_LIMIT = 4_096;
236
+
237
+ type SignInStart =
238
+ | { readonly kind: "code"; readonly deviceCode: GitHubDeviceCode }
239
+ | { readonly kind: "ended" }
240
+ | { readonly kind: "failed" };
241
+
242
+ interface SignIn {
243
+ /** Settles once `gh` printed its code and page, or when it ended before. */
244
+ readonly started: Promise<SignInStart>;
245
+ readonly expiresAt: number;
246
+ cancel(): void;
247
+ }
248
+
249
+ /** Runs `gh auth login --web` on github.com, reading the one-time code and the page it prints; `ended` runs once the process is gone. */
250
+ function startSignIn(file: string, cwd: string, ended: () => void): SignIn {
251
+ const started = Promise.withResolvers<SignInStart>();
252
+ const expiresAt = Date.now() + SIGN_IN_LIFETIME_MS;
253
+ let userCode: string | undefined;
254
+ let verificationUri: string | undefined;
255
+ let done = false;
256
+
257
+ const child: ChildProcess = spawnCommand(
258
+ file,
259
+ [
260
+ "auth",
261
+ "login",
262
+ "--hostname",
263
+ "github.com",
264
+ "--web",
265
+ "--git-protocol",
266
+ "https",
267
+ "--skip-ssh-key",
268
+ ],
269
+ { cwd, env: { ...process.env, ...ENV }, stdio: ["ignore", "pipe", "pipe"] },
270
+ );
271
+
272
+ const end = (start: SignInStart): void => {
273
+ if (done) return;
274
+ done = true;
275
+ clearTimeout(codeTimer);
276
+ clearTimeout(lifetime);
277
+ child.stdout?.destroy();
278
+ child.stderr?.destroy();
279
+
280
+ if (child.exitCode === null && child.signalCode === null) killTree(child, "SIGKILL");
281
+ started.resolve(start);
282
+ ended();
283
+ };
284
+
285
+ const readLine = (line: string): void => {
286
+ userCode ??= USER_CODE.exec(line)?.[1]?.toUpperCase();
287
+ const printed = PRINTED_URL.exec(line)?.[0];
288
+
289
+ if (verificationUri === undefined && printed !== undefined) {
290
+ verificationUri = safeUrl(printed, ["github.com"]);
291
+
292
+ if (verificationUri === undefined) end({ kind: "failed" });
293
+ }
294
+
295
+ if (userCode === undefined || verificationUri === undefined) return;
296
+ clearTimeout(codeTimer);
297
+ started.resolve({ kind: "code", deviceCode: { userCode, verificationUri } });
298
+ };
299
+
300
+ for (const stream of [child.stdout, child.stderr]) {
301
+ let pending = "";
302
+ stream?.setEncoding("utf8");
303
+ stream?.on("data", (chunk: string) => {
304
+ const lines = (pending + chunk).split(/\r?\n/u);
305
+ pending = (lines.pop() ?? "").slice(-SIGN_IN_LINE_LIMIT);
306
+
307
+ for (const line of lines) if (!done) readLine(line);
308
+ });
309
+ }
310
+
311
+ child.on("error", () => end({ kind: "failed" }));
312
+ child.on("close", (code) => end(code === 0 ? { kind: "ended" } : { kind: "failed" }));
313
+ const codeTimer = setTimeout(() => end({ kind: "failed" }), SIGN_IN_CODE_TIMEOUT_MS);
314
+ const lifetime = setTimeout(() => end({ kind: "failed" }), SIGN_IN_LIFETIME_MS);
315
+
316
+ return { started: started.promise, expiresAt, cancel: () => end({ kind: "ended" }) };
317
+ }
318
+
319
+ type Gh = (
320
+ cwd: string,
321
+ args: readonly string[],
322
+ extra?: { readonly input?: string; readonly timeoutMs?: number },
323
+ ) => Promise<Command>;
324
+
325
+ /** Runs `binary` (found on the PATH at each call) with gh's prompts and colors off. */
326
+ function ghCommand(binary: string): Gh {
327
+ return async (cwd, args, extra) => {
328
+ const file = await findExecutable(binary);
329
+
330
+ if (file === null) return { kind: "missing" };
331
+ const started = Date.now();
332
+
333
+ const ran = await run(file, args, {
334
+ cwd,
335
+ env: ENV,
336
+ input: extra?.input,
337
+ limit: OUTPUT_LIMIT,
338
+ timeoutMs: extra?.timeoutMs ?? QUERY_TIMEOUT_MS,
339
+ }).catch(() => null);
340
+
341
+ dev("github", "run", {
342
+ command: args.slice(0, 2).join(" "),
343
+ code: ran?.code ?? null,
344
+ ms: Date.now() - started,
345
+ });
346
+
347
+ if (ran === null || ran.code === null || ran.truncated) return { kind: "failed" };
348
+
349
+ return {
350
+ kind: "completed",
351
+ code: ran.code,
352
+ stdout: ran.stdout.toString("utf8"),
353
+ stderr: ran.stderr,
354
+ };
355
+ };
356
+ }
357
+
358
+ /** gh's github.com login: `gh auth status`, the active account only. */
359
+ async function readAuth(gh: Gh, cwd: string): Promise<Auth> {
360
+ const result = await gh(cwd, [
361
+ "auth",
362
+ "status",
363
+ "--hostname",
364
+ "github.com",
365
+ "--json",
366
+ "hosts",
367
+ "--jq",
368
+ '[(.hosts["github.com"] // [])[] | {active, state, login}]',
369
+ ]);
370
+
371
+ if (result.kind === "missing") return result;
372
+
373
+ if (result.kind === "failed") return { kind: "error", message: RECOVERY.account };
374
+
375
+ if (result.code !== 0)
376
+ return {
377
+ kind: "error",
378
+ message: /unknown flag: --(?:json|jq)\b/u.test(result.stderr)
379
+ ? RECOVERY.update
380
+ : RECOVERY.account,
381
+ };
382
+
383
+ const entries = parseJson(authSchema, result.stdout);
384
+
385
+ if (entries === undefined) return { kind: "error", message: RECOVERY.update };
386
+ const active = entries.find((entry) => entry.active);
387
+
388
+ if (active === undefined) return { kind: "signed_out" };
389
+
390
+ if (active.state !== "success")
391
+ return {
392
+ kind: "error",
393
+ message: active.state === "error" ? RECOVERY.signIn : RECOVERY.account,
394
+ };
395
+
396
+ return { kind: "ready", login: active.login };
397
+ }
398
+
399
+ /** The account gh is signed in as: its login, then its profile (`gh api user`). */
400
+ async function readAccountState(gh: Gh, cwd: string): Promise<GitHubAccountState> {
401
+ const auth = await readAuth(gh, cwd);
402
+
403
+ if (auth.kind === "missing") return { kind: "cli_missing" };
404
+
405
+ if (auth.kind !== "ready") return auth;
406
+ const result = await gh(cwd, ["api", "--hostname", "github.com", "user"]);
407
+
408
+ const account =
409
+ result.kind === "completed" && result.code === 0 ? readAccountJson(result.stdout) : undefined;
410
+
411
+ return account === undefined
412
+ ? { kind: "error", message: RECOVERY.account }
413
+ : { kind: "ready", account };
414
+ }
415
+
416
+ /**
417
+ * Returns the machine's GitHub CLI login through `gh` (`gh`, default `gh` on
418
+ * the PATH), run in `home` (default `os.homedir()`). Nothing runs until it is
419
+ * asked for. `account` reads the github.com account (`gh auth status`, then
420
+ * `gh api user`); while a sign-in runs, it answers its code until it ends,
421
+ * then what the login became. `signIn` starts at most one `gh auth login`
422
+ * for the whole machine and `signOut` cancels it or runs `gh auth logout`.
423
+ */
424
+ export function localGitHubAccount(options?: {
425
+ readonly gh?: string;
426
+ readonly home?: string;
427
+ }): GitHubAccountApi {
428
+ const binary = options?.gh ?? "gh";
429
+ const gh = ghCommand(binary);
430
+ const home = options?.home ?? homedir();
431
+ let signing: SignIn | undefined;
432
+
433
+ const signingState = async (attempt: SignIn): Promise<GitHubAccountState> => {
434
+ const start = await attempt.started;
435
+
436
+ if (start.kind !== "code") {
437
+ const state = await readAccountState(gh, home);
438
+
439
+ return start.kind === "failed" && state.kind === "signed_out"
440
+ ? { kind: "error", message: RECOVERY.signIn }
441
+ : state;
442
+ }
443
+
444
+ return {
445
+ kind: "signing_in",
446
+ deviceCode: {
447
+ ...start.deviceCode,
448
+ expiresInSeconds: Math.max(0, Math.ceil((attempt.expiresAt - Date.now()) / 1000)),
449
+ },
450
+ };
451
+ };
452
+
453
+ return {
454
+ account: () => (signing === undefined ? readAccountState(gh, home) : signingState(signing)),
455
+ async signIn() {
456
+ if (signing !== undefined) return signingState(signing);
457
+ const state = await readAccountState(gh, home);
458
+
459
+ if (state.kind !== "signed_out" && state.kind !== "error") return state;
460
+
461
+ if (signing !== undefined) return signingState(signing);
462
+ const file = await findExecutable(binary);
463
+
464
+ if (file === null) return { kind: "cli_missing" };
465
+
466
+ const attempt = startSignIn(file, home, () => {
467
+ if (signing === attempt) signing = undefined;
468
+ });
469
+
470
+ signing = attempt;
471
+
472
+ return signingState(attempt);
473
+ },
474
+ async signOut() {
475
+ if (signing !== undefined) {
476
+ signing.cancel();
477
+ signing = undefined;
478
+
479
+ return readAccountState(gh, home);
480
+ }
481
+
482
+ const auth = await readAuth(gh, home);
483
+
484
+ if (auth.kind !== "ready") return readAccountState(gh, home);
485
+
486
+ const result = await gh(home, [
487
+ "auth",
488
+ "logout",
489
+ "--hostname",
490
+ "github.com",
491
+ "--user",
492
+ auth.login,
493
+ ]);
494
+
495
+ const state = await readAccountState(gh, home);
496
+
497
+ if ((result.kind === "completed" && result.code === 0) || state.kind !== "ready")
498
+ return state;
499
+
500
+ return { kind: "error", message: RECOVERY.signOut };
501
+ },
502
+ };
503
+ }
504
+
505
+ /**
506
+ * Returns GitHub for each local workspace through the machine's `gh` (`gh`,
507
+ * default `gh` on the PATH) and `git`. Nothing runs until a state is asked
508
+ * for. `state` reads the repository from the remotes in gh's
509
+ * base-repository order (`upstream`, `github`, `origin`, then the rest; only
510
+ * github.com remotes) and, while gh has a login, the checked-out branch's
511
+ * pull request with its check rollup (`gh pr view <branch> --json`, the
512
+ * branch as `<owner>:<branch>` when it tracks a fork); without one the pull
513
+ * request is `unavailable`. `createPullRequest` answers each precondition, an
514
+ * open pull request as `exists`, and otherwise runs
515
+ * `gh pr create --head <branch>` with the title as an argument, the body on
516
+ * stdin, and `--fill` for whichever of the two is missing.
517
+ */
518
+ export function localGitHub(options?: {
519
+ readonly gh?: string;
520
+ readonly git?: string;
521
+ }): (env: ExecutionEnv) => GitHubApi {
522
+ const gh = ghCommand(options?.gh ?? "gh");
523
+ const gitBinary = options?.git ?? "git";
524
+
525
+ const git = async (cwd: string, args: readonly string[]): Promise<string | null> => {
526
+ const ran = await run(gitBinary, args, {
527
+ cwd,
528
+ env: ENV,
529
+ timeoutMs: QUERY_TIMEOUT_MS,
530
+ }).catch(() => null);
531
+
532
+ return ran?.code === 0 ? ran.stdout.toString("utf8").trim() : null;
533
+ };
534
+
535
+ const remotes = async (cwd: string): Promise<readonly GitHubRepository[]> => {
536
+ const listed = (await git(cwd, ["remote", "-v"])) ?? "";
537
+ const found = new Map<string, GitHubRepository>();
538
+
539
+ for (const line of listed.split("\n")) {
540
+ const [name = "", url = ""] = line.split(/\s+/u);
541
+ const repository = found.has(name) ? undefined : parseRemote(name, url);
542
+
543
+ if (repository !== undefined) found.set(name, repository);
544
+ }
545
+
546
+ const rank = (name: string): number => {
547
+ const position = REMOTE_PRIORITY.findIndex((candidate) => candidate === name);
548
+
549
+ return position < 0 ? REMOTE_PRIORITY.length : position;
550
+ };
551
+
552
+ return [...found.values()].sort(
553
+ (left, right) => rank(left.remoteName) - rank(right.remoteName),
554
+ );
555
+ };
556
+
557
+ const readHead = async (
558
+ cwd: string,
559
+ repository: GitHubRepository,
560
+ found: readonly GitHubRepository[],
561
+ ): Promise<string | undefined> => {
562
+ const branch = await git(cwd, ["symbolic-ref", "--quiet", "--short", "HEAD"]);
563
+
564
+ if (branch === null || branch === "") return undefined;
565
+ const tracked = await git(cwd, ["config", "--get", `branch.${branch}.remote`]);
566
+ const owner = found.find((candidate) => candidate.remoteName === tracked)?.owner;
567
+
568
+ return owner === undefined || owner === repository.owner ? branch : `${owner}:${branch}`;
569
+ };
570
+
571
+ const readPullRequest = async (
572
+ cwd: string,
573
+ repository: GitHubRepository,
574
+ head: string | undefined,
575
+ ): Promise<GitHubPullRequestContext> => {
576
+ if (head === undefined) return { kind: "none" };
577
+
578
+ const result = await gh(cwd, [
579
+ "pr",
580
+ "view",
581
+ head,
582
+ "--repo",
583
+ `${repository.owner}/${repository.name}`,
584
+ "--json",
585
+ PULL_REQUEST_FIELDS,
586
+ ]);
587
+
588
+ if (result.kind === "completed" && result.code === 0) {
589
+ const pullRequest = readPullRequestJson(result.stdout);
590
+
591
+ return pullRequest === undefined
592
+ ? { kind: "error", message: RECOVERY.pullRequest }
593
+ : { kind: "ready", pullRequest };
594
+ }
595
+
596
+ if (result.kind === "completed" && /no (?:open )?pull requests? found/iu.test(result.stderr))
597
+ return { kind: "none" };
598
+
599
+ return { kind: "error", message: RECOVERY.pullRequest };
600
+ };
601
+
602
+ const readState = async (
603
+ cwd: string,
604
+ ): Promise<{
605
+ readonly state: GitHubState;
606
+ readonly auth: Auth;
607
+ readonly head: string | undefined;
608
+ }> => {
609
+ const [found, auth] = await Promise.all([remotes(cwd), readAuth(gh, cwd)]);
610
+ const repository = found[0];
611
+ const located = repository === undefined ? {} : { repository };
612
+
613
+ if (auth.kind !== "ready")
614
+ return { state: { ...located, pullRequest: { kind: "unavailable" } }, auth, head: undefined };
615
+
616
+ if (repository === undefined)
617
+ return { state: { pullRequest: NO_PULL_REQUEST }, auth, head: undefined };
618
+ const head = await readHead(cwd, repository, found);
619
+
620
+ return {
621
+ state: { repository, pullRequest: await readPullRequest(cwd, repository, head) },
622
+ auth,
623
+ head,
624
+ };
625
+ };
626
+
627
+ const createPullRequest = async (
628
+ cwd: string,
629
+ input: { readonly title?: string; readonly body?: string; readonly draft?: boolean },
630
+ ): Promise<GitHubPullRequestOutcome> => {
631
+ const { state, auth, head } = await readState(cwd);
632
+
633
+ if (auth.kind === "missing") return { kind: "cli_missing" };
634
+
635
+ if (auth.kind === "signed_out") return { kind: "signed_out" };
636
+
637
+ if (auth.kind === "error") return { kind: "failed", message: auth.message };
638
+ const { repository, pullRequest } = state;
639
+
640
+ if (repository === undefined) return { kind: "no_remote" };
641
+
642
+ if (head === undefined) return { kind: "detached" };
643
+
644
+ if (pullRequest.kind === "ready" && pullRequest.pullRequest.state === "OPEN")
645
+ return { kind: "exists", pullRequest: pullRequest.pullRequest };
646
+
647
+ const title = input.title?.trim() ?? "";
648
+ const body = input.body ?? "";
649
+
650
+ const result = await gh(
651
+ cwd,
652
+ [
653
+ "pr",
654
+ "create",
655
+ "--repo",
656
+ `${repository.owner}/${repository.name}`,
657
+ "--head",
658
+ head,
659
+ ...(title === "" ? [] : ["--title", title]),
660
+ ...(body === "" ? [] : ["--body-file", "-"]),
661
+ ...(title === "" || body === "" ? ["--fill"] : []),
662
+ ...(input.draft === true ? ["--draft"] : []),
663
+ ],
664
+ { input: body, timeoutMs: CREATE_TIMEOUT_MS },
665
+ );
666
+
667
+ if (result.kind === "missing") return { kind: "cli_missing" };
668
+
669
+ if (result.kind === "completed" && result.code === 0) {
670
+ const url = result.stdout
671
+ .split(/\r?\n/u)
672
+ .map((line) => safeUrl(line.trim(), ["github.com"]))
673
+ .find((candidate) => candidate !== undefined);
674
+
675
+ if (url !== undefined) return { kind: "created", url };
676
+ }
677
+
678
+ if (result.kind === "completed" && /already exists/iu.test(result.stderr)) {
679
+ const current = await readPullRequest(cwd, repository, head);
680
+
681
+ if (current.kind === "ready") return { kind: "exists", pullRequest: current.pullRequest };
682
+ }
683
+
684
+ return { kind: "failed", message: RECOVERY.createPullRequest };
685
+ };
686
+
687
+ return (env) => ({
688
+ state: async () => (await readState(env.cwd)).state,
689
+ createPullRequest: (input) => createPullRequest(env.cwd, input),
690
+ });
691
+ }
@@ -0,0 +1,21 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { asId } from "@nyte-ai/schema";
4
+ import type { EnvironmentIdentity } from "@nyte-ai/schema";
5
+ import { sha256 } from "../hash.ts";
6
+
7
+ /** Returns a folder's identity from its inode and nanosecond creation time, independent of boot-assigned device numbers and its path. A rename keeps it; a replacement folder gets another identity. Without an inode or creation time, hashes the real path. An unresolved path hashes its absolute form so inspection can report it unavailable. */
8
+ export async function folderIdentity(path: string): Promise<EnvironmentIdentity> {
9
+ const real = await realpath(path).catch(() => null);
10
+
11
+ if (real === null) return asId<"environment-identity">(sha256(resolve(path)));
12
+ const stats = await stat(real, { bigint: true }).catch(() => null);
13
+
14
+ return asId<"environment-identity">(
15
+ sha256(
16
+ stats === null || stats.ino === 0n || stats.birthtimeNs === 0n
17
+ ? real
18
+ : `${stats.birthtimeNs}:${stats.ino}`,
19
+ ),
20
+ );
21
+ }