@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,334 @@
1
+ import { lineDiff } from "@nyte-ai/core";
2
+ import { writePatch } from "@nyte-ai/schema";
3
+ import type { FileStatus, LineStat, PatchEntry } from "@nyte-ai/schema";
4
+ import { sha256 } from "../hash.ts";
5
+ import type { Ran } from "./command.ts";
6
+
7
+ /** One file's section of `git diff` output: its paths, whether git called it binary, whether it has a hunk, its line counts and its text. */
8
+ type DiffSection = {
9
+ readonly oldPath: string | null;
10
+ readonly newPath: string | null;
11
+ readonly binary: boolean;
12
+ readonly hunked: boolean;
13
+ readonly added: number;
14
+ readonly removed: number;
15
+ readonly text: string;
16
+ };
17
+
18
+ export const PATCH_LIMIT = 1024 * 1024;
19
+
20
+ const ESCAPES = new Map([
21
+ ["a", 7],
22
+ ["b", 8],
23
+ ["f", 12],
24
+ ["n", 10],
25
+ ["r", 13],
26
+ ["t", 9],
27
+ ["v", 11],
28
+ ['"', 34],
29
+ ["\\", 92],
30
+ ]);
31
+
32
+ export type Changed = {
33
+ readonly path: string;
34
+ readonly status: FileStatus;
35
+ readonly from?: string;
36
+ };
37
+
38
+ function splitDiff(output: string): readonly DiffSection[] {
39
+ const lines = output.split("\n");
40
+
41
+ if (lines.at(-1) === "") lines.pop();
42
+ const sections: DiffSection[] = [];
43
+ let index = 0;
44
+
45
+ while (index < lines.length) {
46
+ const header = lines[index] ?? "";
47
+
48
+ if (!header.startsWith("diff --git ")) {
49
+ index += 1;
50
+ continue;
51
+ }
52
+
53
+ const start = index;
54
+ const named = headerPaths(header.slice("diff --git ".length));
55
+ let oldPath: string | null = named?.old ?? null;
56
+ let newPath: string | null = named?.new ?? null;
57
+ let binary = false;
58
+ let hunked = false;
59
+ let added = 0;
60
+ let removed = 0;
61
+ index += 1;
62
+
63
+ while (index < lines.length && !(lines[index] ?? "").startsWith("diff --git ")) {
64
+ const line = lines[index] ?? "";
65
+ index += 1;
66
+
67
+ if (line.startsWith("@@ ")) hunked = true;
68
+ else if (hunked && line.startsWith("+")) added += 1;
69
+ else if (hunked && line.startsWith("-")) removed += 1;
70
+ else if (hunked) continue;
71
+ else if (line.startsWith("--- ")) oldPath = sidePath(line.slice(4), "a/");
72
+ else if (line.startsWith("+++ ")) newPath = sidePath(line.slice(4), "b/");
73
+ else if (/^(?:rename|copy) from /u.test(line))
74
+ oldPath = unquote(line.replace(/^(?:rename|copy) from /u, ""));
75
+ else if (/^(?:rename|copy) to /u.test(line))
76
+ newPath = unquote(line.replace(/^(?:rename|copy) to /u, ""));
77
+ else if (line.startsWith("new file mode ")) oldPath = null;
78
+ else if (line.startsWith("deleted file mode ")) newPath = null;
79
+ else if (line.startsWith("Binary files ") || line === "GIT binary patch") binary = true;
80
+ }
81
+
82
+ sections.push({
83
+ oldPath,
84
+ newPath,
85
+ binary,
86
+ hunked,
87
+ added,
88
+ removed,
89
+ text: `${lines.slice(start, index).join("\n")}\n`,
90
+ });
91
+ }
92
+
93
+ return sections;
94
+ }
95
+
96
+ function sectionEntry(path: string, status: FileStatus, section: DiffSection): PatchEntry {
97
+ const digest = sha256(section.text);
98
+
99
+ if (section.binary) return { path, status, digest, kind: "binary" };
100
+
101
+ if (section.text.length > PATCH_LIMIT)
102
+ return { path, status, digest, kind: "too-large", limit: PATCH_LIMIT };
103
+
104
+ if (!section.hunked) return { path, status, digest, kind: "empty" };
105
+
106
+ return {
107
+ path,
108
+ status,
109
+ digest,
110
+ kind: "text",
111
+ added: section.added,
112
+ removed: section.removed,
113
+ patch: section.text,
114
+ };
115
+ }
116
+
117
+ /** The entry for two texts diffed on the host: a text entry's digest is its patch's, so it moves with the whitespace mode; an empty one digests both sides and the mode. */
118
+ export function textEntry(
119
+ path: string,
120
+ status: FileStatus,
121
+ before: string | null,
122
+ after: string | null,
123
+ options?: { readonly ignoreWhitespace?: boolean },
124
+ ): PatchEntry {
125
+ const { hunks, added, removed } = lineDiff(before ?? "", after ?? "", options);
126
+
127
+ if (hunks.length === 0) {
128
+ const mode = options?.ignoreWhitespace === true ? "ignore-whitespace" : "";
129
+
130
+ return {
131
+ path,
132
+ status,
133
+ digest: sha256(`${mode}\u0000${before ?? ""}\u0000${after ?? ""}`),
134
+ kind: "empty",
135
+ };
136
+ }
137
+
138
+ const patch = writePatch(path, status, hunks);
139
+
140
+ return { path, status, digest: sha256(patch), kind: "text", added, removed, patch };
141
+ }
142
+
143
+ export async function readPatches(
144
+ files: readonly Changed[],
145
+ read: (files: readonly Changed[], limit: number) => Promise<Ran>,
146
+ emptyDigest: (file: Changed) => string,
147
+ ): Promise<readonly PatchEntry[]> {
148
+ const settle = (batch: readonly Changed[], ran: Ran): readonly PatchEntry[] => {
149
+ const sections = splitDiff(ran.stdout.toString("utf8"));
150
+
151
+ return batch.map((file) => {
152
+ const section = sections.find(
153
+ (candidate) => (candidate.newPath ?? candidate.oldPath) === file.path,
154
+ );
155
+
156
+ return section === undefined
157
+ ? { path: file.path, status: file.status, digest: emptyDigest(file), kind: "empty" }
158
+ : sectionEntry(file.path, file.status, section);
159
+ });
160
+ };
161
+
162
+ const together = await read(files, PATCH_LIMIT * files.length + 4096).catch(() => null);
163
+
164
+ if (together !== null && !together.truncated && (together.code === 0 || together.code === 1))
165
+ return settle(files, together);
166
+ const entries: PatchEntry[] = [];
167
+
168
+ for (const file of files) {
169
+ const failed = (reason: string): PatchEntry => ({
170
+ path: file.path,
171
+ status: file.status,
172
+ digest: "",
173
+ kind: "failed",
174
+ reason,
175
+ });
176
+
177
+ const alone = await read([file], PATCH_LIMIT + 1).catch((error: unknown) =>
178
+ error instanceof Error ? error.message : String(error),
179
+ );
180
+
181
+ if (typeof alone === "string") entries.push(failed(alone));
182
+ else if (alone.truncated)
183
+ entries.push({
184
+ path: file.path,
185
+ status: file.status,
186
+ digest: sha256(alone.stdout),
187
+ kind: "too-large",
188
+ limit: PATCH_LIMIT,
189
+ });
190
+ else if (alone.code !== 0 && alone.code !== 1)
191
+ entries.push(failed(alone.stderr.trim() || "git diff failed"));
192
+ else entries.push(...settle([file], alone));
193
+ }
194
+
195
+ return entries;
196
+ }
197
+
198
+ export function lineStat(before: string, after: string): LineStat {
199
+ const { added, removed } = lineDiff(before, after);
200
+
201
+ return { kind: "text", added, removed };
202
+ }
203
+
204
+ function headerPaths(rest: string): { readonly old: string; readonly new: string } | null {
205
+ if (rest.startsWith('"')) {
206
+ const end = quotedEnd(rest);
207
+ const old = unquote(rest.slice(0, end + 1));
208
+ const next = unquote(rest.slice(end + 2));
209
+
210
+ return { old: old.replace(/^a\//, ""), new: next.replace(/^b\//, "") };
211
+ }
212
+
213
+ const half = (rest.length - 5) / 2;
214
+
215
+ if (Number.isInteger(half) && rest.startsWith("a/") && rest.slice(half + 2, half + 5) === " b/") {
216
+ const path = rest.slice(2, half + 2);
217
+
218
+ if (rest.slice(half + 5) === path) return { old: path, new: path };
219
+ }
220
+
221
+ const split = /^.{2}(.*?) b\/(.*)$/su.exec(rest);
222
+
223
+ return split === null ? null : { old: split[1] ?? "", new: unquote(split[2] ?? "") };
224
+ }
225
+
226
+ function quotedEnd(text: string): number {
227
+ for (let index = 1; index < text.length; index += 1) {
228
+ if (text[index] === "\\") index += 1;
229
+ else if (text[index] === '"') return index;
230
+ }
231
+
232
+ return text.length - 1;
233
+ }
234
+
235
+ function sidePath(text: string, prefix: string): string | null {
236
+ const name = unquote(text.replace(/\t$/, ""));
237
+
238
+ if (name === "/dev/null") return null;
239
+
240
+ return name.startsWith(prefix) ? name.slice(prefix.length) : name;
241
+ }
242
+
243
+ function unquote(text: string): string {
244
+ if (!text.startsWith('"') || !text.endsWith('"') || text.length < 2) return text;
245
+ const bytes: number[] = [];
246
+ const encoder = new TextEncoder();
247
+
248
+ for (let index = 1; index < text.length - 1; index += 1) {
249
+ const char = text[index] ?? "";
250
+
251
+ if (char !== "\\") {
252
+ bytes.push(...encoder.encode(char));
253
+ continue;
254
+ }
255
+
256
+ const next = text[index + 1] ?? "";
257
+ const octal = /^[0-7]{3}$/.exec(text.slice(index + 1, index + 4));
258
+
259
+ if (octal !== null) {
260
+ bytes.push(Number.parseInt(octal[0], 8));
261
+ index += 3;
262
+ continue;
263
+ }
264
+
265
+ bytes.push(ESCAPES.get(next) ?? next.charCodeAt(0));
266
+ index += 1;
267
+ }
268
+
269
+ return new TextDecoder().decode(new Uint8Array(bytes));
270
+ }
271
+
272
+ export function parseNumstat(output: string): ReadonlyMap<string, LineStat> {
273
+ const tokens = output.split("\u0000");
274
+ const stats = new Map<string, LineStat>();
275
+
276
+ for (let index = 0; index < tokens.length; index += 1) {
277
+ const token = tokens[index] ?? "";
278
+
279
+ if (token === "") continue;
280
+ const [added = "", removed = "", path = ""] = token.split("\t");
281
+ let name = path;
282
+
283
+ if (path === "") {
284
+ name = tokens[index + 2] ?? "";
285
+ index += 2;
286
+ }
287
+
288
+ stats.set(
289
+ name,
290
+ added === "-" || removed === "-"
291
+ ? { kind: "binary" }
292
+ : { kind: "text", added: Number(added) || 0, removed: Number(removed) || 0 },
293
+ );
294
+ }
295
+
296
+ return stats;
297
+ }
298
+
299
+ export function parseNameStatus(output: string): readonly Changed[] {
300
+ const tokens = output.split("\u0000");
301
+ const changed: Changed[] = [];
302
+
303
+ for (let index = 0; index < tokens.length; index += 1) {
304
+ const code = tokens[index] ?? "";
305
+
306
+ if (code === "") continue;
307
+ const letter = code[0];
308
+
309
+ if (letter === "R" || letter === "C") {
310
+ const from = tokens[index + 1] ?? "";
311
+ const path = tokens[index + 2] ?? "";
312
+ index += 2;
313
+ changed.push(letter === "R" ? { path, status: "renamed", from } : { path, status: "added" });
314
+ continue;
315
+ }
316
+
317
+ const path = tokens[index + 1] ?? "";
318
+ index += 1;
319
+
320
+ changed.push({
321
+ path,
322
+ status:
323
+ letter === "A"
324
+ ? "added"
325
+ : letter === "D"
326
+ ? "deleted"
327
+ : letter === "U"
328
+ ? "conflicted"
329
+ : "modified",
330
+ });
331
+ }
332
+
333
+ return changed;
334
+ }
@@ -0,0 +1,184 @@
1
+ import { constants, realpathSync } from "node:fs";
2
+ import { access, readdir, realpath, stat } from "node:fs/promises";
3
+ import { homedir } from "node:os";
4
+ import { isAbsolute, join, relative, sep } from "node:path";
5
+ import { asId, SdkError } from "@nyte-ai/schema";
6
+ import type { WorkspaceTarget } from "@nyte-ai/schema";
7
+ import type { EnvironmentProvider, ExecutionEnv } from "@nyte-ai/core";
8
+ import { sha256 } from "../hash.ts";
9
+ import { localFiles } from "./files.ts";
10
+ import { localGit } from "./git.ts";
11
+ import { localGitHub } from "./github.ts";
12
+ import { folderIdentity } from "./identity.ts";
13
+ import { localExec } from "./jobs.ts";
14
+ import { localSnapshots } from "./snapshots.ts";
15
+ import { localWorktrees } from "./worktrees.ts";
16
+
17
+ const PLUGIN_FILE = /\.(?:ts|mts|js|mjs)$/;
18
+
19
+ const ROOT_SOURCES = [".nyte/SYSTEM.md", ".nyte/APPEND_SYSTEM.md", ".nyte/mcp.json"];
20
+
21
+ /** Returns the local environment provider (`kind: "local"`): a folder on this machine. `inspect` answers a `{ kind: "local", path }` target's `folderIdentity` (§41d), `available` when the path is a readable directory (else `unavailable { unreachable }`; a target without a path `unavailable { unsupported }`, identified by the hash of its locator), and the project sources present under it, relative to it, in this order: the plugin files `.nyte/plugins/<name>.{ts,mts,js,mjs}` and `.nyte/plugins/<name>/index.<ext>`, the skills `.nyte/skills/<name>/SKILL.md` and `.agents/skills/<name>/SKILL.md`, the prompts `.nyte/prompts/<name>.md`, then `.nyte/SYSTEM.md`, `.nyte/APPEND_SYSTEM.md` and `.nyte/mcp.json`: the files project trust gates. Context files such as `AGENTS.md` load without trust and are not listed. `attach` resolves the real path (a target that is not a directory throws `SdkError("unavailable")`) and returns an `ExecutionEnv` with that identity, `cwd` the real path, `files` from `localFiles` (with `ripgrep` and `formatter`), `exec` from `localExec`, and a `close` that ends its file watches and the jobs' readers, leaving running jobs alone. `git` is `localGit` (with `git`), `github` is `localGitHub` (with `gh` and `git`; the login is the machine's, `localGitHubAccount`), `snapshots` is `localSnapshots` storing tree walks under `<home>/.nyte/snapshots` (`home` default `os.homedir()`), and absent for a folder that contains `<home>/.nyte` or lies inside it outside `<home>/.nyte/worktrees` (the home folder itself, its ancestors, Nyte's own state): there a snapshot would walk or stage everything under home, the store and secrets included, to report changes in the store it just wrote; `worktrees` is `localWorktrees` (with `home` and `git`); no `browser` (the desktop wraps one in); `destroy` rejects with `SdkError("unavailable")`: a local folder is never torn down. */
22
+ export function localProvider(options?: {
23
+ readonly home?: string;
24
+ readonly git?: string;
25
+ readonly gh?: string;
26
+ readonly ripgrep?: string;
27
+ readonly formatter?: string;
28
+ /** The login shell a pty job without a command runs, read at each start; null or absent is `$SHELL`. */
29
+ readonly shell?: () => string | null;
30
+ }): EnvironmentProvider {
31
+ const home = options?.home ?? homedir();
32
+ const store = join(home, ".nyte", "snapshots");
33
+ let state: string | undefined;
34
+ const github = localGitHub({ gh: options?.gh, git: options?.git });
35
+
36
+ return {
37
+ kind: "local",
38
+ async inspect(target) {
39
+ const path = readTargetPath(target);
40
+
41
+ if (path === null)
42
+ return {
43
+ identity: asId<"environment-identity">(sha256(JSON.stringify(target))),
44
+ availability: { kind: "unavailable", reason: "unsupported" },
45
+ projectSources: [],
46
+ };
47
+
48
+ const identity = await folderIdentity(path);
49
+
50
+ if (!(await readableDirectory(path)))
51
+ return {
52
+ identity,
53
+ availability: { kind: "unavailable", reason: "unreachable" },
54
+ projectSources: [],
55
+ };
56
+
57
+ return {
58
+ identity,
59
+ availability: { kind: "available" },
60
+ projectSources: await projectSources(path),
61
+ };
62
+ },
63
+ async attach(target) {
64
+ const path = readTargetPath(target);
65
+
66
+ if (path === null) throw new SdkError("unavailable", "A local workspace needs a path.");
67
+ const real = await realpath(path).catch(() => null);
68
+
69
+ if (real === null || !(await readableDirectory(real)))
70
+ throw new SdkError("unavailable", `${path} is not a readable directory.`);
71
+ const detached = new AbortController();
72
+
73
+ const env: ExecutionEnv = {
74
+ identity: await folderIdentity(real),
75
+ cwd: real,
76
+ files: localFiles(real, {
77
+ ripgrep: options?.ripgrep,
78
+ formatter: options?.formatter,
79
+ signal: detached.signal,
80
+ }),
81
+ exec: localExec(real, { shell: options?.shell, signal: detached.signal }),
82
+ close: async () => detached.abort(),
83
+ };
84
+
85
+ return env;
86
+ },
87
+ git: (env) => localGit(env, { git: options?.git }),
88
+ github,
89
+ snapshots(env) {
90
+ state ??= join(realpathOrSelf(home), ".nyte");
91
+
92
+ return nests(env.cwd, state) ||
93
+ (nests(state, env.cwd) && !nests(join(state, "worktrees"), env.cwd))
94
+ ? undefined
95
+ : localSnapshots(env, { store, git: options?.git });
96
+ },
97
+ worktrees: localWorktrees({ home, git: options?.git }),
98
+ async destroy() {
99
+ throw new SdkError("unavailable", "A local folder is never torn down.");
100
+ },
101
+ };
102
+ }
103
+
104
+ function realpathOrSelf(path: string): string {
105
+ try {
106
+ return realpathSync(path);
107
+ } catch {
108
+ return path;
109
+ }
110
+ }
111
+
112
+ function nests(outer: string, inner: string): boolean {
113
+ const path = relative(outer, inner);
114
+
115
+ return path === "" || (path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path));
116
+ }
117
+
118
+ function readTargetPath(target: WorkspaceTarget): string | null {
119
+ return "path" in target && typeof target.path === "string" ? target.path : null;
120
+ }
121
+
122
+ async function readableDirectory(path: string): Promise<boolean> {
123
+ const stats = await stat(path).catch(() => null);
124
+
125
+ return (
126
+ stats !== null &&
127
+ stats.isDirectory() &&
128
+ (await access(path, constants.R_OK | constants.X_OK).then(
129
+ () => true,
130
+ () => false,
131
+ ))
132
+ );
133
+ }
134
+
135
+ async function projectSources(root: string): Promise<readonly string[]> {
136
+ const names = async (directory: string): Promise<readonly string[]> =>
137
+ (await readdir(join(root, directory)).catch(() => [])).sort((left, right) =>
138
+ left < right ? -1 : left > right ? 1 : 0,
139
+ );
140
+
141
+ const fileExists = async (path: string): Promise<boolean> =>
142
+ (await stat(join(root, path)).catch(() => null))?.isFile() === true;
143
+
144
+ const plugins = (
145
+ await Promise.all(
146
+ (await names(".nyte/plugins")).map(async (name) => {
147
+ const path = `.nyte/plugins/${name}`;
148
+
149
+ if (PLUGIN_FILE.test(name) && (await fileExists(path))) return [path];
150
+
151
+ for (const index of ["index.ts", "index.mts", "index.js", "index.mjs"])
152
+ if (await fileExists(`${path}/${index}`)) return [`${path}/${index}`];
153
+
154
+ return [];
155
+ }),
156
+ )
157
+ ).flat();
158
+
159
+ const skills = (
160
+ await Promise.all(
161
+ [".nyte/skills", ".agents/skills"].map(async (directory) =>
162
+ (
163
+ await Promise.all(
164
+ (await names(directory)).map(async (name) => {
165
+ const path = `${directory}/${name}/SKILL.md`;
166
+
167
+ return (await fileExists(path)) ? [path] : [];
168
+ }),
169
+ )
170
+ ).flat(),
171
+ ),
172
+ )
173
+ ).flat();
174
+
175
+ const prompts = (await names(".nyte/prompts"))
176
+ .filter((name) => name.endsWith(".md"))
177
+ .map((name) => `.nyte/prompts/${name}`);
178
+
179
+ const present = await Promise.all(
180
+ ROOT_SOURCES.map(async (path) => ((await fileExists(path)) ? [path] : [])),
181
+ );
182
+
183
+ return [...plugins, ...skills, ...prompts, ...present.flat()];
184
+ }