@nyte-ai/host 0.0.0-stage → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -2
- package/THIRD-PARTY-NOTICES.md +800 -0
- package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
- package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
- package/dist/chunks/canvas-preview-0nrnbt76.js +497 -0
- package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
- package/dist/chunks/canvas-preview-1ta3avah.js +56 -0
- package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
- package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
- package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
- package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
- package/dist/chunks/canvas-preview-518dh8by.js +169 -0
- package/dist/chunks/canvas-preview-5hxat7hh.js +239 -0
- package/dist/chunks/canvas-preview-5mex725c.js +14 -0
- package/dist/chunks/canvas-preview-5z63am63.js +295 -0
- package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
- package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
- package/dist/chunks/canvas-preview-75db60xy.js +457 -0
- package/dist/chunks/canvas-preview-76fwvd6e.js +272 -0
- package/dist/chunks/canvas-preview-8xgt67ka.js +598 -0
- package/dist/chunks/canvas-preview-98aj5p34.js +176 -0
- package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
- package/dist/chunks/canvas-preview-a3yw0j70.js +72 -0
- package/dist/chunks/canvas-preview-azg56359.js +68 -0
- package/dist/chunks/canvas-preview-d9wwhbtz.js +240 -0
- package/dist/chunks/canvas-preview-ehf3vp31.js +411 -0
- package/dist/chunks/canvas-preview-evh92ywd.js +369 -0
- package/dist/chunks/canvas-preview-ewgspmsd.js +81 -0
- package/dist/chunks/canvas-preview-g3qj3y3k.js +156 -0
- package/dist/chunks/canvas-preview-h8hshnff.js +63 -0
- package/dist/chunks/canvas-preview-j1tnf51w.js +36 -0
- package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
- package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
- package/dist/chunks/canvas-preview-m02v4yy6.js +386 -0
- package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
- package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
- package/dist/chunks/canvas-preview-rp6fz0w5.js +130 -0
- package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
- package/dist/chunks/canvas-preview-senpjwep.js +518 -0
- package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
- package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
- package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
- package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
- package/dist/chunks/canvas-preview-vax9ne4e.js +341 -0
- package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
- package/dist/chunks/canvas-preview-xn10d4f8.js +608 -0
- package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
- package/dist/chunks/canvas-preview-ymgz0937.js +587 -0
- package/dist/chunks/canvas-preview-yrfz2ak6.js +134 -0
- package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
- package/dist/chunks/canvas-preview-zn2d0k9d.js +133 -0
- package/dist/src/canvas-preview.d.ts +29 -0
- package/dist/src/canvas-preview.js +267 -0
- package/dist/src/credentials.d.ts +44 -0
- package/dist/src/credentials.js +10 -0
- package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
- package/dist/src/deploy/cloudflare/worker.js +196 -0
- package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
- package/dist/src/deploy/vercel/nitro.config.js +30 -0
- package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
- package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
- package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
- package/dist/src/deploy/vercel/server/utils/host.js +7 -0
- package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
- package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
- package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
- package/dist/src/deploy/vercel/server/utils/step.js +6 -0
- package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
- package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
- package/dist/src/durable-object.d.ts +14 -0
- package/dist/src/durable-object.js +231 -0
- package/dist/src/hash.d.ts +1 -0
- package/dist/src/hash.js +6 -0
- package/dist/src/identity-key.d.ts +11 -0
- package/dist/src/identity-key.js +9 -0
- package/dist/src/identity.d.ts +12 -0
- package/dist/src/identity.js +6 -0
- package/dist/src/index.d.ts +21 -0
- package/dist/src/index.js +120 -0
- package/dist/src/key-file.d.ts +1 -0
- package/dist/src/key-file.js +7 -0
- package/dist/src/listen-fetch.d.ts +8 -0
- package/dist/src/listen-fetch.js +9 -0
- package/dist/src/listen.d.ts +4 -0
- package/dist/src/listen.js +10 -0
- package/dist/src/local/command.d.ts +15 -0
- package/dist/src/local/command.js +8 -0
- package/dist/src/local/feed.d.ts +7 -0
- package/dist/src/local/feed.js +6 -0
- package/dist/src/local/files.d.ts +18 -0
- package/dist/src/local/files.js +13 -0
- package/dist/src/local/git-watch.d.ts +15 -0
- package/dist/src/local/git-watch.js +7 -0
- package/dist/src/local/git.d.ts +5 -0
- package/dist/src/local/git.js +13 -0
- package/dist/src/local/github.d.ts +30 -0
- package/dist/src/local/github.js +9 -0
- package/dist/src/local/identity.d.ts +3 -0
- package/dist/src/local/identity.js +7 -0
- package/dist/src/local/jobs.d.ts +7 -0
- package/dist/src/local/jobs.js +6 -0
- package/dist/src/local/legacy-trust.d.ts +10 -0
- package/dist/src/local/legacy-trust.js +6 -0
- package/dist/src/local/patch.d.ts +16 -0
- package/dist/src/local/patch.js +17 -0
- package/dist/src/local/provider.d.ts +11 -0
- package/dist/src/local/provider.js +20 -0
- package/dist/src/local/snapshots.d.ts +7 -0
- package/dist/src/local/snapshots.js +11 -0
- package/dist/src/local/text.d.ts +1 -0
- package/dist/src/local/text.js +6 -0
- package/dist/src/local/tree.d.ts +50 -0
- package/dist/src/local/tree.js +16 -0
- package/dist/src/local/workspace-migration.d.ts +2 -0
- package/dist/src/local/workspace-migration.js +7 -0
- package/dist/src/local/worktrees.d.ts +46 -0
- package/dist/src/local/worktrees.js +14 -0
- package/dist/src/mcp/config.d.ts +75 -0
- package/dist/src/mcp/config.js +12 -0
- package/dist/src/mcp/connect.d.ts +28 -0
- package/dist/src/mcp/connect.js +9 -0
- package/dist/src/mcp/hub.d.ts +19 -0
- package/dist/src/mcp/hub.js +11 -0
- package/dist/src/mcp/oauth.d.ts +46 -0
- package/dist/src/mcp/oauth.js +12 -0
- package/dist/src/mcp/tools.d.ts +29 -0
- package/dist/src/mcp/tools.js +12 -0
- package/dist/src/open.d.ts +42 -0
- package/dist/src/open.js +44 -0
- package/dist/src/plugins/loader.d.ts +21 -0
- package/dist/src/plugins/loader.js +11 -0
- package/dist/src/postgres.d.ts +19 -0
- package/dist/src/postgres.js +8 -0
- package/dist/src/providers.d.ts +16 -0
- package/dist/src/providers.js +112 -0
- package/dist/src/secrets.d.ts +12 -0
- package/dist/src/secrets.js +8 -0
- package/dist/src/serve/binding.d.ts +39 -0
- package/dist/src/serve/binding.js +6 -0
- package/dist/src/serve/handler.d.ts +15 -0
- package/dist/src/serve/handler.js +8 -0
- package/dist/src/serve/stream-server.d.ts +6 -0
- package/dist/src/serve/stream-server.js +6 -0
- package/dist/src/session-index.d.ts +11 -0
- package/dist/src/session-index.js +11 -0
- package/dist/src/shell-env.d.ts +14 -0
- package/dist/src/shell-env.js +108 -0
- package/dist/src/sqlite-workers.d.ts +6 -0
- package/dist/src/sqlite-workers.js +6 -0
- package/dist/src/sqlite.d.ts +7 -0
- package/dist/src/sqlite.js +10 -0
- package/dist/src/store-owner.d.ts +15 -0
- package/dist/src/store-owner.js +9 -0
- package/dist/src/store-rows.d.ts +13 -0
- package/dist/src/store-rows.js +26 -0
- package/dist/src/store-usage.d.ts +25 -0
- package/dist/src/store-usage.js +16 -0
- package/dist/src/usage-scan.d.ts +24 -0
- package/dist/src/usage-scan.js +65 -0
- package/dist/src/usage-service.d.ts +22 -0
- package/dist/src/usage-service.js +12 -0
- package/dist/src/usage-summary.d.ts +9 -0
- package/dist/src/usage-summary.js +16 -0
- package/dist/src/usage-types.d.ts +86 -0
- package/dist/src/usage-types.js +0 -0
- package/dist/src/usage-worker.d.ts +1 -0
- package/dist/src/usage-worker.js +25 -0
- package/dist/src/usage.d.ts +72 -0
- package/dist/src/usage.js +13 -0
- package/package.json +132 -3
- package/src/canvas-preview.ts +402 -0
- package/src/credentials.ts +294 -0
- package/src/deploy/cloudflare/README.md +3 -0
- package/src/deploy/cloudflare/worker.ts +306 -0
- package/src/deploy/cloudflare/wrangler.jsonc +12 -0
- package/src/deploy/vercel/README.md +3 -0
- package/src/deploy/vercel/nitro.config.ts +29 -0
- package/src/deploy/vercel/server/routes/[...].ts +10 -0
- package/src/deploy/vercel/server/utils/host.ts +104 -0
- package/src/deploy/vercel/server/utils/repair.ts +35 -0
- package/src/deploy/vercel/server/utils/step.ts +25 -0
- package/src/deploy/vercel/server/workflows/drive.ts +46 -0
- package/src/deploy/vercel/tsconfig.json +9 -0
- package/src/deploy/vercel/types/bun.d.ts +2 -0
- package/src/durable-object.ts +386 -0
- package/src/hash.ts +5 -0
- package/src/identity-key.ts +20 -0
- package/src/identity.ts +66 -0
- package/src/index.ts +21 -0
- package/src/key-file.ts +53 -0
- package/src/listen-fetch.ts +409 -0
- package/src/listen.ts +228 -0
- package/src/local/command.ts +109 -0
- package/src/local/feed.ts +70 -0
- package/src/local/files.ts +812 -0
- package/src/local/git-watch.ts +189 -0
- package/src/local/git.ts +914 -0
- package/src/local/github.ts +692 -0
- package/src/local/identity.ts +21 -0
- package/src/local/jobs.ts +587 -0
- package/src/local/legacy-trust.ts +105 -0
- package/src/local/patch.ts +334 -0
- package/src/local/provider.ts +184 -0
- package/src/local/snapshots.ts +776 -0
- package/src/local/text.ts +9 -0
- package/src/local/tree.ts +398 -0
- package/src/local/workspace-migration.ts +95 -0
- package/src/local/worktrees.ts +239 -0
- package/src/mcp/config.ts +330 -0
- package/src/mcp/connect.ts +105 -0
- package/src/mcp/hub.ts +836 -0
- package/src/mcp/oauth.ts +436 -0
- package/src/mcp/tools.ts +333 -0
- package/src/open.ts +469 -0
- package/src/plugins/loader.ts +186 -0
- package/src/postgres.ts +492 -0
- package/src/providers.ts +135 -0
- package/src/secrets.ts +31 -0
- package/src/serve/binding.ts +628 -0
- package/src/serve/handler.ts +362 -0
- package/src/serve/stream-server.ts +176 -0
- package/src/session-index.ts +99 -0
- package/src/shell-env.ts +153 -0
- package/src/sqlite-workers.ts +283 -0
- package/src/sqlite.ts +528 -0
- package/src/store-owner.ts +98 -0
- package/src/store-rows.ts +110 -0
- package/src/store-usage.ts +344 -0
- package/src/usage-scan.ts +81 -0
- package/src/usage-service.ts +73 -0
- package/src/usage-summary.ts +79 -0
- package/src/usage-types.ts +87 -0
- package/src/usage-worker.ts +24 -0
- package/src/usage.ts +701 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
import { watch } from "node:fs";
|
|
2
|
+
import type { FSWatcher } from "node:fs";
|
|
3
|
+
import { lstat, readFile, readdir } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
5
|
+
import type { Disposer } from "@nyte-ai/schema";
|
|
6
|
+
import ignore from "ignore";
|
|
7
|
+
|
|
8
|
+
type TreeEntry = {
|
|
9
|
+
readonly path: string;
|
|
10
|
+
readonly kind: "file" | "directory" | "symlink";
|
|
11
|
+
readonly size: number | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type WalkFrame =
|
|
15
|
+
| { readonly kind: "entries"; readonly entries: readonly TreeEntry[] }
|
|
16
|
+
| { readonly kind: "complete" }
|
|
17
|
+
| { readonly kind: "incomplete"; readonly reason: "limit" | "permission" };
|
|
18
|
+
|
|
19
|
+
export type Ignores = {
|
|
20
|
+
matches(path: string, directory: boolean): Promise<boolean>;
|
|
21
|
+
ignored(path: string, directory: boolean): Promise<boolean>;
|
|
22
|
+
forget(directory: string): void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const WALK_LIMIT = 50_000;
|
|
26
|
+
|
|
27
|
+
const WALK_PAGE = 500;
|
|
28
|
+
|
|
29
|
+
type Matcher = ReturnType<typeof ignore>;
|
|
30
|
+
|
|
31
|
+
export function createIgnores(root: string): Ignores {
|
|
32
|
+
const rules = new Map<string, Promise<Matcher | null>>();
|
|
33
|
+
|
|
34
|
+
const loadRules = (directory: string): Promise<Matcher | null> => {
|
|
35
|
+
const known = rules.get(directory);
|
|
36
|
+
|
|
37
|
+
if (known !== undefined) return known;
|
|
38
|
+
const sources = [join(root, directory, ".gitignore")];
|
|
39
|
+
|
|
40
|
+
if (directory === "") sources.push(join(root, ".git", "info", "exclude"));
|
|
41
|
+
|
|
42
|
+
const loaded = Promise.all(
|
|
43
|
+
sources.map((source) => readFile(source, "utf8").catch(() => null)),
|
|
44
|
+
).then((texts) => {
|
|
45
|
+
const found = texts.filter((text) => text !== null);
|
|
46
|
+
|
|
47
|
+
return found.length === 0
|
|
48
|
+
? null
|
|
49
|
+
: found.reduce((matcher: Matcher, text) => matcher.add(text), ignore());
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
rules.set(directory, loaded);
|
|
53
|
+
|
|
54
|
+
return loaded;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const matches = async (path: string, directory: boolean): Promise<boolean> => {
|
|
58
|
+
const segments = path.split("/");
|
|
59
|
+
|
|
60
|
+
if (segments.includes(".git")) return true;
|
|
61
|
+
let result = false;
|
|
62
|
+
|
|
63
|
+
for (let depth = 0; depth < segments.length; depth += 1) {
|
|
64
|
+
const matcher = await loadRules(segments.slice(0, depth).join("/"));
|
|
65
|
+
|
|
66
|
+
if (matcher === null) continue;
|
|
67
|
+
const relative = segments.slice(depth).join("/") + (directory ? "/" : "");
|
|
68
|
+
|
|
69
|
+
if (!ignore.isPathValid(relative.replace(/\/$/, ""))) continue;
|
|
70
|
+
const test = matcher.test(relative);
|
|
71
|
+
|
|
72
|
+
if (test.ignored) result = true;
|
|
73
|
+
else if (test.unignored) result = false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
matches,
|
|
81
|
+
async ignored(path, directory) {
|
|
82
|
+
const segments = path.split("/");
|
|
83
|
+
|
|
84
|
+
for (let depth = 1; depth < segments.length; depth += 1)
|
|
85
|
+
if (await matches(segments.slice(0, depth).join("/"), true)) return true;
|
|
86
|
+
|
|
87
|
+
return matches(path, directory);
|
|
88
|
+
},
|
|
89
|
+
forget(directory) {
|
|
90
|
+
rules.delete(directory);
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function* walkTree(
|
|
96
|
+
root: string,
|
|
97
|
+
options: {
|
|
98
|
+
readonly directory?: string;
|
|
99
|
+
readonly ignored: "exclude" | "include";
|
|
100
|
+
readonly ignores?: Ignores;
|
|
101
|
+
readonly limit?: number;
|
|
102
|
+
readonly page?: number;
|
|
103
|
+
},
|
|
104
|
+
): AsyncGenerator<WalkFrame> {
|
|
105
|
+
const ignores = options.ignores ?? createIgnores(root);
|
|
106
|
+
const limit = options.limit ?? WALK_LIMIT;
|
|
107
|
+
const size = options.page ?? WALK_PAGE;
|
|
108
|
+
const queue = [trimSlashes(options.directory ?? "")];
|
|
109
|
+
let page: TreeEntry[] = [];
|
|
110
|
+
let count = 0;
|
|
111
|
+
let denied = false;
|
|
112
|
+
|
|
113
|
+
for (let next = queue.shift(); next !== undefined; next = queue.shift()) {
|
|
114
|
+
const directory = next;
|
|
115
|
+
|
|
116
|
+
const listed = await readdir(join(root, directory), { withFileTypes: true }).catch(
|
|
117
|
+
(error: unknown) => {
|
|
118
|
+
if (readErrorCode(error) === "EACCES" || readErrorCode(error) === "EPERM") denied = true;
|
|
119
|
+
|
|
120
|
+
return null;
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (listed === null) continue;
|
|
125
|
+
listed.sort((left, right) => (left.name < right.name ? -1 : left.name > right.name ? 1 : 0));
|
|
126
|
+
|
|
127
|
+
const entries = await Promise.all(
|
|
128
|
+
listed.map(async (dirent): Promise<TreeEntry | null> => {
|
|
129
|
+
const path = directory === "" ? dirent.name : `${directory}/${dirent.name}`;
|
|
130
|
+
|
|
131
|
+
const kind = dirent.isSymbolicLink()
|
|
132
|
+
? "symlink"
|
|
133
|
+
: dirent.isDirectory()
|
|
134
|
+
? "directory"
|
|
135
|
+
: dirent.isFile()
|
|
136
|
+
? "file"
|
|
137
|
+
: null;
|
|
138
|
+
|
|
139
|
+
if (kind === null || dirent.name === ".git") return null;
|
|
140
|
+
|
|
141
|
+
if (options.ignored === "exclude" && (await ignores.matches(path, kind === "directory")))
|
|
142
|
+
return null;
|
|
143
|
+
|
|
144
|
+
if (kind === "directory") return { path, kind, size: null };
|
|
145
|
+
const stats = await lstat(join(root, path)).catch(() => null);
|
|
146
|
+
|
|
147
|
+
return stats === null ? null : { path, kind, size: stats.size };
|
|
148
|
+
}),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (entry === null) continue;
|
|
153
|
+
page.push(entry);
|
|
154
|
+
count += 1;
|
|
155
|
+
|
|
156
|
+
if (entry.kind === "directory") queue.push(entry.path);
|
|
157
|
+
|
|
158
|
+
if (count >= limit) {
|
|
159
|
+
yield { kind: "entries", entries: page };
|
|
160
|
+
yield { kind: "incomplete", reason: "limit" };
|
|
161
|
+
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (page.length >= size) {
|
|
166
|
+
yield { kind: "entries", entries: page };
|
|
167
|
+
page = [];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (page.length > 0) yield { kind: "entries", entries: page };
|
|
173
|
+
yield denied ? { kind: "incomplete", reason: "permission" } : { kind: "complete" };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Calls `changed` with the paths below `root` that moved, coalesced over `delay`, from a recursive `fs.watch`; where that is unavailable it walks the tree every `poll` ms when one is given, and calls `unwatched` once either way. */
|
|
177
|
+
export function watchTree(
|
|
178
|
+
root: string,
|
|
179
|
+
options: {
|
|
180
|
+
readonly delay: number;
|
|
181
|
+
readonly poll: number | null;
|
|
182
|
+
readonly unwatched?: () => void;
|
|
183
|
+
},
|
|
184
|
+
changed: (paths: ReadonlySet<string>) => void,
|
|
185
|
+
): Disposer {
|
|
186
|
+
let pending = new Set<string>();
|
|
187
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
188
|
+
let polling: ReturnType<typeof setTimeout> | null = null;
|
|
189
|
+
let watcher: FSWatcher | null = null;
|
|
190
|
+
let fellBack = false;
|
|
191
|
+
let stopped = false;
|
|
192
|
+
|
|
193
|
+
const flush = (): void => {
|
|
194
|
+
timer = null;
|
|
195
|
+
const paths = pending;
|
|
196
|
+
pending = new Set();
|
|
197
|
+
|
|
198
|
+
if (!stopped && paths.size > 0) changed(paths);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const note = (path: string): void => {
|
|
202
|
+
pending.add(path);
|
|
203
|
+
timer ??= setTimeout(flush, options.delay);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const poll = (every: number): void => {
|
|
207
|
+
let known: Map<string, string> | null = null;
|
|
208
|
+
|
|
209
|
+
const tick = async (): Promise<void> => {
|
|
210
|
+
const seen = new Map<string, string>();
|
|
211
|
+
|
|
212
|
+
for await (const frame of walkTree(root, { ignored: "include" }))
|
|
213
|
+
if (frame.kind === "entries")
|
|
214
|
+
for (const entry of frame.entries) {
|
|
215
|
+
const stats = await lstat(join(root, entry.path)).catch(() => null);
|
|
216
|
+
seen.set(entry.path, stats === null ? "" : `${stats.size}:${stats.mtimeMs}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (known !== null) {
|
|
220
|
+
for (const [path, stamp] of seen) if (known.get(path) !== stamp) note(path);
|
|
221
|
+
|
|
222
|
+
for (const path of known.keys()) if (!seen.has(path)) note(path);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
known = seen;
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const again = (): void => {
|
|
229
|
+
if (stopped) return;
|
|
230
|
+
polling = setTimeout(() => {
|
|
231
|
+
tick().then(again, again);
|
|
232
|
+
}, every);
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
tick().then(again, again);
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const fallback = (): void => {
|
|
239
|
+
watcher?.close();
|
|
240
|
+
watcher = null;
|
|
241
|
+
|
|
242
|
+
if (stopped || fellBack) return;
|
|
243
|
+
fellBack = true;
|
|
244
|
+
options.unwatched?.();
|
|
245
|
+
|
|
246
|
+
if (options.poll !== null) poll(options.poll);
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
watcher = watch(root, { recursive: true }, (_event, filename) =>
|
|
251
|
+
note(filename === null ? "" : filename.split(sep).join("/")),
|
|
252
|
+
);
|
|
253
|
+
watcher.on("error", fallback);
|
|
254
|
+
} catch {
|
|
255
|
+
fallback();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return () => {
|
|
259
|
+
stopped = true;
|
|
260
|
+
watcher?.close();
|
|
261
|
+
|
|
262
|
+
if (timer !== null) clearTimeout(timer);
|
|
263
|
+
|
|
264
|
+
if (polling !== null) clearTimeout(polling);
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Calls `changed` when what is at the absolute `target` moves, coalesced over
|
|
270
|
+
* `delay`: with `""` for the target itself (created, removed, replaced or, for
|
|
271
|
+
* a file, written) and the path below it for a change inside a directory,
|
|
272
|
+
* which is watched recursively. A file is watched itself and re-watched when
|
|
273
|
+
* it is replaced, a directory beside a watch on its parent for its own name;
|
|
274
|
+
* while the target is missing, the nearest directory above it that exists is
|
|
275
|
+
* watched for the next missing name, and the watch moves down as they appear.
|
|
276
|
+
* Nothing polls.
|
|
277
|
+
*/
|
|
278
|
+
export function watchPath(
|
|
279
|
+
target: string,
|
|
280
|
+
options: { readonly delay: number },
|
|
281
|
+
changed: (paths: ReadonlySet<string>) => void,
|
|
282
|
+
): Disposer {
|
|
283
|
+
let pending = new Set<string>();
|
|
284
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
285
|
+
let watchers: FSWatcher[] = [];
|
|
286
|
+
let generation = 0;
|
|
287
|
+
let moved = false;
|
|
288
|
+
let stopped = false;
|
|
289
|
+
|
|
290
|
+
const flush = (): void => {
|
|
291
|
+
timer = null;
|
|
292
|
+
const paths = pending;
|
|
293
|
+
pending = new Set();
|
|
294
|
+
|
|
295
|
+
if (stopped) return;
|
|
296
|
+
|
|
297
|
+
if (moved) {
|
|
298
|
+
moved = false;
|
|
299
|
+
void arm();
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (paths.size > 0) changed(paths);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const note = (path: string, replaced: boolean): void => {
|
|
306
|
+
pending.add(path);
|
|
307
|
+
moved ||= replaced;
|
|
308
|
+
timer ??= setTimeout(flush, options.delay);
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
const listen = (
|
|
312
|
+
at: string,
|
|
313
|
+
recursive: boolean,
|
|
314
|
+
heard: (event: string, name: string | null) => void,
|
|
315
|
+
): void => {
|
|
316
|
+
try {
|
|
317
|
+
const watcher = watch(at, { recursive, persistent: false }, (event, name) =>
|
|
318
|
+
heard(event, name === null ? null : name.split(sep).join("/")),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
watcher.on("error", () => note("", true));
|
|
322
|
+
watchers.push(watcher);
|
|
323
|
+
} catch {}
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const arm = async (): Promise<void> => {
|
|
327
|
+
generation += 1;
|
|
328
|
+
const armed = generation;
|
|
329
|
+
|
|
330
|
+
for (const watcher of watchers) watcher.close();
|
|
331
|
+
watchers = [];
|
|
332
|
+
let existing = target;
|
|
333
|
+
let missing: string | null = null;
|
|
334
|
+
let stats = await lstat(existing).catch(() => null);
|
|
335
|
+
|
|
336
|
+
while (stats === null) {
|
|
337
|
+
const parent = dirname(existing);
|
|
338
|
+
|
|
339
|
+
if (parent === existing) return;
|
|
340
|
+
missing = basename(existing);
|
|
341
|
+
existing = parent;
|
|
342
|
+
stats = await lstat(existing).catch(() => null);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (stopped || armed !== generation) return;
|
|
346
|
+
|
|
347
|
+
if (missing !== null) {
|
|
348
|
+
const awaited = missing;
|
|
349
|
+
|
|
350
|
+
if (stats.isDirectory())
|
|
351
|
+
listen(existing, false, (_event, name) => {
|
|
352
|
+
if (name === null || name === awaited) note("", true);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
if (!stats.isDirectory()) {
|
|
359
|
+
listen(target, false, (event) => note("", event === "rename"));
|
|
360
|
+
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const own = basename(target);
|
|
365
|
+
listen(target, true, (_event, name) => note(name ?? "", false));
|
|
366
|
+
listen(dirname(target), false, (event, name) => {
|
|
367
|
+
if (name === null || name === own) note("", event === "rename");
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
void arm();
|
|
372
|
+
|
|
373
|
+
return () => {
|
|
374
|
+
stopped = true;
|
|
375
|
+
generation += 1;
|
|
376
|
+
|
|
377
|
+
for (const watcher of watchers) watcher.close();
|
|
378
|
+
watchers = [];
|
|
379
|
+
|
|
380
|
+
if (timer !== null) clearTimeout(timer);
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function readErrorCode(error: unknown): string | null {
|
|
385
|
+
return typeof error === "object" &&
|
|
386
|
+
error !== null &&
|
|
387
|
+
"code" in error &&
|
|
388
|
+
typeof error.code === "string"
|
|
389
|
+
? error.code
|
|
390
|
+
: null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function trimSlashes(path: string): string {
|
|
394
|
+
return path
|
|
395
|
+
.replace(/\\/g, "/")
|
|
396
|
+
.replace(/^\.?\/+|\/+$/g, "")
|
|
397
|
+
.replace(/^\.$/, "");
|
|
398
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { realpath, stat } from "node:fs/promises";
|
|
2
|
+
import { decodeWorkspaceRecord, envelope } from "@nyte-ai/core/store";
|
|
3
|
+
import type { StoreConnection } from "@nyte-ai/core";
|
|
4
|
+
import { asId, SdkError } from "@nyte-ai/schema";
|
|
5
|
+
import type { RefUpdate } from "@nyte-ai/schema";
|
|
6
|
+
import { Type } from "typebox";
|
|
7
|
+
import { Value } from "typebox/value";
|
|
8
|
+
import { sha256 } from "../hash.ts";
|
|
9
|
+
|
|
10
|
+
const MIGRATION = "host/migrations/local-workspace-identity-v2";
|
|
11
|
+
|
|
12
|
+
const MigrationState = Type.Object({ pending: Type.Array(Type.String()) });
|
|
13
|
+
|
|
14
|
+
export async function migrateLocalWorkspaceIdentities(connection: StoreConnection): Promise<void> {
|
|
15
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
16
|
+
const marker = await connection.refs.get(MIGRATION);
|
|
17
|
+
const state = marker?.target == null ? null : await connection.objects.get(marker.target);
|
|
18
|
+
const pending = Value.Check(MigrationState, state) ? state.pending : null;
|
|
19
|
+
|
|
20
|
+
if (marker !== null && pending === null)
|
|
21
|
+
throw new SdkError("internal", "The workspace identity migration state is malformed.");
|
|
22
|
+
|
|
23
|
+
if (pending?.length === 0) return;
|
|
24
|
+
|
|
25
|
+
const refs =
|
|
26
|
+
pending === null
|
|
27
|
+
? await connection.refs.list("workspaces/")
|
|
28
|
+
: await Promise.all(pending.map((name) => connection.refs.get(name)));
|
|
29
|
+
|
|
30
|
+
const updates: RefUpdate[] = [];
|
|
31
|
+
const remaining: string[] = [];
|
|
32
|
+
|
|
33
|
+
for (const ref of refs) {
|
|
34
|
+
if (ref === null) continue;
|
|
35
|
+
|
|
36
|
+
const record = decodeWorkspaceRecord(
|
|
37
|
+
ref.target === null ? null : await connection.objects.get(ref.target),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (record?.target.kind !== "local" || !("path" in record.target)) continue;
|
|
41
|
+
const real = await realpath(record.target.path).catch(() => null);
|
|
42
|
+
|
|
43
|
+
if (real === null) {
|
|
44
|
+
remaining.push(ref.name);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const metadata = await stat(real, { bigint: true }).catch(() => null);
|
|
49
|
+
|
|
50
|
+
if (metadata === null) {
|
|
51
|
+
remaining.push(ref.name);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
!metadata.isDirectory() ||
|
|
57
|
+
metadata.ino === 0n ||
|
|
58
|
+
metadata.birthtimeNs === 0n ||
|
|
59
|
+
metadata.birthtimeNs / 1_000_000n > BigInt(Math.floor(record.createdAt)) ||
|
|
60
|
+
record.identity !== sha256(`${metadata.dev}:${metadata.ino}`)
|
|
61
|
+
)
|
|
62
|
+
continue;
|
|
63
|
+
|
|
64
|
+
const identity = asId<"environment-identity">(
|
|
65
|
+
sha256(`${metadata.birthtimeNs}:${metadata.ino}`),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
updates.push({
|
|
69
|
+
name: ref.name,
|
|
70
|
+
expect: ref.revision,
|
|
71
|
+
target: await connection.objects.put(
|
|
72
|
+
envelope("workspace", { ...record, identity }, Date.now()),
|
|
73
|
+
),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
updates.length === 0 &&
|
|
79
|
+
pending !== null &&
|
|
80
|
+
pending.length === remaining.length &&
|
|
81
|
+
remaining.every((name, index) => pending[index] === name)
|
|
82
|
+
)
|
|
83
|
+
return;
|
|
84
|
+
|
|
85
|
+
updates.push({
|
|
86
|
+
name: MIGRATION,
|
|
87
|
+
expect: marker?.revision ?? "absent",
|
|
88
|
+
target: await connection.objects.put({ pending: remaining }),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if ((await connection.refs.set(updates)).kind === "set") return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
throw new SdkError("unavailable", "Workspace identities changed while Nyte was updating them.");
|
|
95
|
+
}
|