@nyte-ai/host 0.0.18 → 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 (102) hide show
  1. package/THIRD-PARTY-NOTICES.md +66 -0
  2. package/dist/chunks/{canvas-preview-5hxat7hh.js → canvas-preview-11nadadc.js} +8 -6
  3. package/dist/chunks/{canvas-preview-rp6fz0w5.js → canvas-preview-2jwbe14k.js} +5 -5
  4. package/dist/chunks/{canvas-preview-m02v4yy6.js → canvas-preview-55s0y68g.js} +3 -2
  5. package/dist/chunks/{canvas-preview-xn10d4f8.js → canvas-preview-5ekjx6sw.js} +9 -4
  6. package/dist/chunks/{canvas-preview-75db60xy.js → canvas-preview-8xc0vn7v.js} +5 -4
  7. package/dist/chunks/{canvas-preview-zn2d0k9d.js → canvas-preview-af86jz8t.js} +14 -6
  8. package/dist/chunks/{canvas-preview-d9wwhbtz.js → canvas-preview-aq5j4p25.js} +1 -1
  9. package/dist/chunks/{canvas-preview-j1tnf51w.js → canvas-preview-b2v8wfnz.js} +1 -1
  10. package/dist/chunks/canvas-preview-befkjea2.js +60 -0
  11. package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
  12. package/dist/chunks/{canvas-preview-vax9ne4e.js → canvas-preview-cawy6vbz.js} +22 -11
  13. package/dist/chunks/{canvas-preview-senpjwep.js → canvas-preview-crmzxxt8.js} +3 -3
  14. package/dist/chunks/{canvas-preview-ymgz0937.js → canvas-preview-drctegz4.js} +8 -4
  15. package/dist/chunks/{canvas-preview-yrfz2ak6.js → canvas-preview-e3vnkkc5.js} +6 -6
  16. package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
  17. package/dist/chunks/{canvas-preview-1ta3avah.js → canvas-preview-fxa0rfb0.js} +10 -5
  18. package/dist/chunks/{canvas-preview-8xgt67ka.js → canvas-preview-g5fkjs34.js} +1 -1
  19. package/dist/chunks/{canvas-preview-g3qj3y3k.js → canvas-preview-jeddjex8.js} +13 -3
  20. package/dist/chunks/{canvas-preview-0nrnbt76.js → canvas-preview-k1yg4fgp.js} +199 -125
  21. package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
  22. package/dist/chunks/{canvas-preview-ewgspmsd.js → canvas-preview-n2a35zv5.js} +1 -1
  23. package/dist/chunks/{canvas-preview-h8hshnff.js → canvas-preview-pkzqg96v.js} +1 -1
  24. package/dist/chunks/{canvas-preview-evh92ywd.js → canvas-preview-tnqe7k0b.js} +15 -10
  25. package/dist/chunks/{canvas-preview-ehf3vp31.js → canvas-preview-vm9zdp2t.js} +45 -58
  26. package/dist/chunks/{canvas-preview-76fwvd6e.js → canvas-preview-w54ymtb3.js} +1 -1
  27. package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
  28. package/dist/chunks/{canvas-preview-98aj5p34.js → canvas-preview-ycepb05s.js} +1 -1
  29. package/dist/src/canvas-preview.js +7 -3
  30. package/dist/src/deploy/cloudflare/worker.js +1 -1
  31. package/dist/src/deploy/vercel/server/routes/[...].js +2 -2
  32. package/dist/src/deploy/vercel/server/utils/host.js +1 -1
  33. package/dist/src/deploy/vercel/server/workflows/drive.js +3 -3
  34. package/dist/src/images/exif.d.ts +1 -0
  35. package/dist/src/images/exif.js +6 -0
  36. package/dist/src/images/node.d.ts +37 -0
  37. package/dist/src/images/node.js +12 -0
  38. package/dist/src/images/photon.d.ts +3 -0
  39. package/dist/src/images/photon.js +7 -0
  40. package/dist/src/images/worker.d.ts +1 -0
  41. package/dist/src/images/worker.js +23 -0
  42. package/dist/src/index.d.ts +0 -1
  43. package/dist/src/index.js +23 -22
  44. package/dist/src/listen-fetch.js +3 -3
  45. package/dist/src/listen.js +4 -4
  46. package/dist/src/local/command.d.ts +5 -0
  47. package/dist/src/local/command.js +6 -2
  48. package/dist/src/local/files.js +2 -2
  49. package/dist/src/local/git-watch.d.ts +1 -1
  50. package/dist/src/local/git-watch.js +1 -1
  51. package/dist/src/local/git.js +3 -3
  52. package/dist/src/local/github.js +2 -2
  53. package/dist/src/local/jobs.d.ts +1 -1
  54. package/dist/src/local/jobs.js +2 -1
  55. package/dist/src/local/provider.js +9 -9
  56. package/dist/src/local/snapshots.js +2 -2
  57. package/dist/src/local/worktrees.js +2 -2
  58. package/dist/src/mcp/connect.d.ts +0 -13
  59. package/dist/src/mcp/connect.js +1 -1
  60. package/dist/src/mcp/hub.js +2 -2
  61. package/dist/src/open.d.ts +2 -30
  62. package/dist/src/open.js +20 -18
  63. package/dist/src/serve/binding.js +1 -1
  64. package/dist/src/serve/handler.js +2 -2
  65. package/dist/src/sqlite.d.ts +2 -1
  66. package/dist/src/sqlite.js +4 -3
  67. package/dist/src/store-usage.js +4 -1
  68. package/dist/src/usage-history.d.ts +54 -0
  69. package/dist/src/usage-history.js +14 -0
  70. package/dist/src/usage-service.d.ts +2 -1
  71. package/dist/src/usage-service.js +7 -3
  72. package/dist/src/usage-worker.js +8 -4
  73. package/dist/src/usage.d.ts +150 -27
  74. package/dist/src/usage.js +12 -2
  75. package/package.json +16 -8
  76. package/src/canvas-preview.ts +4 -3
  77. package/src/images/exif.ts +69 -0
  78. package/src/images/node.ts +163 -0
  79. package/src/images/photon.ts +141 -0
  80. package/src/images/worker.ts +29 -0
  81. package/src/index.ts +19 -1
  82. package/src/local/command.ts +64 -12
  83. package/src/local/files.ts +8 -3
  84. package/src/local/git-watch.ts +16 -6
  85. package/src/local/git.ts +7 -2
  86. package/src/local/github.ts +3 -4
  87. package/src/local/jobs.ts +55 -69
  88. package/src/local/snapshots.ts +4 -2
  89. package/src/local/worktrees.ts +4 -4
  90. package/src/mcp/connect.ts +1 -14
  91. package/src/open.ts +16 -36
  92. package/src/serve/binding.ts +4 -3
  93. package/src/sqlite.ts +33 -8
  94. package/src/store-usage.ts +5 -2
  95. package/src/usage-history.ts +219 -0
  96. package/src/usage-service.ts +12 -3
  97. package/src/usage.ts +251 -176
  98. package/dist/chunks/canvas-preview-a3yw0j70.js +0 -72
  99. package/dist/chunks/canvas-preview-azg56359.js +0 -68
  100. package/dist/src/store-owner.d.ts +0 -15
  101. package/dist/src/store-owner.js +0 -9
  102. package/src/store-owner.ts +0 -98
@@ -0,0 +1,141 @@
1
+ import {
2
+ PhotonImage,
3
+ SamplingFilter,
4
+ fliph,
5
+ flipv,
6
+ initSync,
7
+ resize,
8
+ } from "@silvia-odwyer/photon/photon_rs.js";
9
+ import type { SyncInitInput } from "@silvia-odwyer/photon/photon_rs.js";
10
+ import type { ImageLimits, ImageProcessor, PreparedImage } from "@nyte-ai/core";
11
+ import { exifOrientation } from "./exif.ts";
12
+
13
+ const INLINE_IMAGE_TYPES: ReadonlySet<string> = new Set([
14
+ "image/png",
15
+ "image/jpeg",
16
+ "image/gif",
17
+ "image/webp",
18
+ ]);
19
+
20
+ const JPEG_QUALITIES = [80, 70, 55, 40];
21
+
22
+ export function photonImages(wasm: SyncInitInput): ImageProcessor {
23
+ initSync({ module: wasm });
24
+
25
+ return {
26
+ prepare: (bytes, mimeType, limits) => Promise.resolve(prepare(bytes, mimeType, limits)),
27
+ };
28
+ }
29
+
30
+ function prepare(
31
+ bytes: Uint8Array,
32
+ mimeType: string,
33
+ limits: ImageLimits,
34
+ ): PreparedImage | undefined {
35
+ const orientation = exifOrientation(bytes);
36
+ let image: PhotonImage | undefined;
37
+
38
+ try {
39
+ image = PhotonImage.new_from_byteslice(bytes);
40
+
41
+ if (
42
+ INLINE_IMAGE_TYPES.has(mimeType) &&
43
+ orientation === 1 &&
44
+ image.get_width() <= limits.maxWidth &&
45
+ image.get_height() <= limits.maxHeight &&
46
+ base64Length(bytes.byteLength) <= limits.maxBase64Bytes
47
+ )
48
+ return { data: bytes.toBase64(), mimeType };
49
+
50
+ if (orientation >= 5) {
51
+ const upright = transposed(image, orientation);
52
+ image.free();
53
+ image = undefined;
54
+ image = new PhotonImage(upright.pixels, upright.width, upright.height);
55
+ } else if (orientation !== 1) {
56
+ if (orientation !== 4) fliph(image);
57
+
58
+ if (orientation !== 2) flipv(image);
59
+ }
60
+
61
+ const width = image.get_width();
62
+ const height = image.get_height();
63
+ const scale = Math.min(1, limits.maxWidth / width, limits.maxHeight / height);
64
+
65
+ let target = {
66
+ width: Math.max(1, Math.round(width * scale)),
67
+ height: Math.max(1, Math.round(height * scale)),
68
+ };
69
+
70
+ for (;;) {
71
+ const encoded = encodeWithin(image, target, limits.maxBase64Bytes, mimeType === "image/jpeg");
72
+
73
+ if (encoded !== undefined)
74
+ return target.width === width && target.height === height
75
+ ? encoded
76
+ : { ...encoded, resized: { from: { width, height }, to: target } };
77
+
78
+ if (target.width === 1 && target.height === 1) return undefined;
79
+ target = {
80
+ width: Math.max(1, Math.floor(target.width * 0.75)),
81
+ height: Math.max(1, Math.floor(target.height * 0.75)),
82
+ };
83
+ }
84
+ } catch {
85
+ return undefined;
86
+ } finally {
87
+ image?.free();
88
+ }
89
+ }
90
+
91
+ function encodeWithin(
92
+ image: PhotonImage,
93
+ size: { readonly width: number; readonly height: number },
94
+ maxBase64Bytes: number,
95
+ jpegFirst: boolean,
96
+ ): { readonly data: string; readonly mimeType: string } | undefined {
97
+ const same = size.width === image.get_width() && size.height === image.get_height();
98
+ const scaled = same ? image : resize(image, size.width, size.height, SamplingFilter.Lanczos3);
99
+
100
+ try {
101
+ const png = { mimeType: "image/png", encode: () => scaled.get_bytes() };
102
+
103
+ const jpegs = JPEG_QUALITIES.map((quality) => ({
104
+ mimeType: "image/jpeg",
105
+ encode: () => scaled.get_bytes_jpeg(quality),
106
+ }));
107
+
108
+ for (const candidate of jpegFirst ? [...jpegs, png] : [png, ...jpegs]) {
109
+ const encoded = candidate.encode();
110
+
111
+ if (base64Length(encoded.byteLength) <= maxBase64Bytes)
112
+ return { data: encoded.toBase64(), mimeType: candidate.mimeType };
113
+ }
114
+
115
+ return undefined;
116
+ } finally {
117
+ if (!same) scaled.free();
118
+ }
119
+ }
120
+
121
+ function transposed(image: PhotonImage, orientation: number) {
122
+ const width = image.get_width();
123
+ const height = image.get_height();
124
+ const source = image.get_raw_pixels();
125
+ const pixels = new Uint8Array(source.length);
126
+
127
+ for (let y = 0; y < height; y++) {
128
+ for (let x = 0; x < width; x++) {
129
+ const outX = orientation === 5 || orientation === 8 ? y : height - 1 - y;
130
+ const outY = orientation === 5 || orientation === 6 ? x : width - 1 - x;
131
+ const from = (y * width + x) * 4;
132
+ pixels.set(source.subarray(from, from + 4), (outY * height + outX) * 4);
133
+ }
134
+ }
135
+
136
+ return { pixels, width: height, height: width };
137
+ }
138
+
139
+ function base64Length(byteLength: number): number {
140
+ return Math.ceil(byteLength / 3) * 4;
141
+ }
@@ -0,0 +1,29 @@
1
+ import { parentPort, workerData } from "node:worker_threads";
2
+ import type { Static } from "typebox";
3
+ import type { PrepareRequest, prepareReply } from "./node.ts";
4
+ import { photonImages } from "./photon.ts";
5
+
6
+ const port = parentPort;
7
+
8
+ if (port === null) throw new Error("The image worker needs a parent thread");
9
+
10
+ const wasm: unknown = workerData;
11
+
12
+ if (!(wasm instanceof Uint8Array)) throw new Error("The image worker needs Photon's WebAssembly");
13
+
14
+ const images = photonImages(wasm);
15
+
16
+ port.on("message", (request: PrepareRequest) => {
17
+ void images.prepare(request.bytes, request.mimeType, request.limits).then(
18
+ (result) =>
19
+ port.postMessage({
20
+ id: request.id,
21
+ result: result ?? null,
22
+ } satisfies Static<typeof prepareReply>),
23
+ (cause: unknown) =>
24
+ port.postMessage({
25
+ id: request.id,
26
+ error: cause instanceof Error ? cause.message : String(cause),
27
+ } satisfies Static<typeof prepareReply>),
28
+ );
29
+ });
package/src/index.ts CHANGED
@@ -1,21 +1,39 @@
1
1
  export * from "./credentials.ts";
2
+
2
3
  export * from "./identity.ts";
4
+
3
5
  export { identitySigner } from "./identity-key.ts";
6
+
4
7
  export * from "./listen.ts";
8
+
5
9
  export * from "./listen-fetch.ts";
10
+
6
11
  export * from "./local/files.ts";
12
+
7
13
  export * from "./local/git.ts";
14
+
8
15
  export * from "./local/github.ts";
16
+
9
17
  export * from "./local/identity.ts";
18
+
10
19
  export * from "./local/jobs.ts";
20
+
11
21
  export * from "./local/provider.ts";
22
+
12
23
  export { createMcpHub } from "./mcp/hub.ts";
24
+
13
25
  export type { McpConnector } from "./mcp/connect.ts";
26
+
14
27
  export * from "./local/snapshots.ts";
28
+
15
29
  export * from "./local/worktrees.ts";
30
+
16
31
  export * from "./open.ts";
32
+
17
33
  export * from "./plugins/loader.ts";
34
+
18
35
  export * from "./postgres.ts";
36
+
19
37
  export * from "./secrets.ts";
38
+
20
39
  export * from "./sqlite.ts";
21
- export { StoreOwned } from "./store-owner.ts";
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
+ import type { ChildProcess, SpawnOptions } from "node:child_process";
2
3
  import { constants } from "node:fs";
3
4
  import { access, stat } from "node:fs/promises";
4
- import { delimiter, isAbsolute, join } from "node:path";
5
+ import { delimiter, extname, isAbsolute, join } from "node:path";
5
6
  import { SdkError } from "@nyte-ai/schema";
6
7
  import { processEnvironment } from "@nyte-ai/core";
7
8
 
@@ -25,9 +26,10 @@ export async function run(
25
26
  },
26
27
  ): Promise<Ran> {
27
28
  await processEnvironment();
29
+ const resolved = process.platform === "win32" ? ((await findExecutable(file)) ?? file) : file;
28
30
 
29
31
  return new Promise((resolve, reject) => {
30
- const child = spawn(file, args, {
32
+ const child = spawnCommand(resolved, args, {
31
33
  cwd: options.cwd,
32
34
  env: { ...process.env, ...options.env },
33
35
  stdio: ["pipe", "pipe", "pipe"],
@@ -44,7 +46,7 @@ export async function run(
44
46
  child.on("error", (error) =>
45
47
  reject(new SdkError("unavailable", `Cannot run ${file}: ${error.message}`)),
46
48
  );
47
- child.stdout.on("data", (chunk: Buffer) => {
49
+ child.stdout?.on("data", (chunk: Buffer) => {
48
50
  if (truncated) return;
49
51
  size += chunk.byteLength;
50
52
 
@@ -58,8 +60,8 @@ export async function run(
58
60
 
59
61
  stdout.push(chunk);
60
62
  });
61
- child.stderr.on("data", (chunk: Buffer) => stderr.push(chunk));
62
- child.stdin.on("error", () => undefined);
63
+ child.stderr?.on("data", (chunk: Buffer) => stderr.push(chunk));
64
+ child.stdin?.on("error", () => undefined);
63
65
  child.on("close", (code) =>
64
66
  resolve({
65
67
  code,
@@ -68,22 +70,72 @@ export async function run(
68
70
  truncated,
69
71
  }),
70
72
  );
71
- child.stdin.end(options.input);
73
+ child.stdin?.end(options.input);
72
74
  });
73
75
  }
74
76
 
77
+ /** Spawns `file` as `spawn` does, except that on Windows a `.cmd` or `.bat` file runs through `cmd.exe` with its arguments escaped, as Node no longer starts batch files without a shell. */
78
+ export function spawnCommand(
79
+ file: string,
80
+ args: readonly string[],
81
+ options: SpawnOptions,
82
+ ): ChildProcess {
83
+ if (process.platform !== "win32" || !/\.(cmd|bat)$/i.test(file))
84
+ return spawn(file, args, options);
85
+ const shim = /node_modules[\\/]\.bin[\\/][^\\/]+\.cmd$/i.test(file);
86
+ const line = [escapeCommand(file), ...args.map((arg) => escapeArgument(arg, shim))].join(" ");
87
+
88
+ return spawn(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `"${line}"`], {
89
+ ...options,
90
+ windowsVerbatimArguments: true,
91
+ });
92
+ }
93
+
94
+ /** Ends `child` with `signal`; on Windows, which has no signals, it ends the child's whole process tree with `taskkill /T /F`, as `kill` alone would leave what a batch file or a shell started running. */
95
+ export function killTree(child: ChildProcess, signal: NodeJS.Signals): void {
96
+ if (process.platform !== "win32" || child.pid === undefined) {
97
+ child.kill(signal);
98
+
99
+ return;
100
+ }
101
+
102
+ spawn("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
103
+ stdio: "ignore",
104
+ windowsHide: true,
105
+ }).on("error", () => child.kill(signal));
106
+ }
107
+
108
+ const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
109
+
110
+ function escapeCommand(command: string): string {
111
+ return command.replace(CMD_META, "^$1");
112
+ }
113
+
114
+ function escapeArgument(argument: string, twice: boolean): string {
115
+ const quoted = `"${argument.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1")}"`;
116
+ const escaped = quoted.replace(CMD_META, "^$1");
117
+
118
+ return twice ? escaped.replace(CMD_META, "^$1") : escaped;
119
+ }
120
+
75
121
  export async function findExecutable(
76
122
  name: string,
77
123
  directories: readonly string[] = [],
78
124
  ): Promise<string | null> {
79
- if (isAbsolute(name)) return (await executable(name)) ? name : null;
80
- await processEnvironment();
81
- const path = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
125
+ const pathext = (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter((ext) => ext !== "");
82
126
 
83
127
  const extensions =
84
- process.platform === "win32"
85
- ? ["", ...(process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";")]
86
- : [""];
128
+ process.platform !== "win32" ? [""] : extname(name) === "" ? pathext : ["", ...pathext];
129
+
130
+ if (isAbsolute(name)) {
131
+ for (const extension of extensions)
132
+ if (await executable(`${name}${extension}`)) return `${name}${extension}`;
133
+
134
+ return null;
135
+ }
136
+
137
+ await processEnvironment();
138
+ const path = (process.env.PATH ?? "").split(delimiter).filter((entry) => entry !== "");
87
139
 
88
140
  for (const directory of [...directories, ...path])
89
141
  for (const extension of extensions) {
@@ -167,7 +167,7 @@ export function localFiles(
167
167
  if (stats === null) return null;
168
168
 
169
169
  if (stats.isSymbolicLink()) {
170
- const link = await readlink(target);
170
+ const link = await readLink(target);
171
171
 
172
172
  return { kind: "symlink", size: stats.size, version: hashVersion(link), target: link };
173
173
  }
@@ -218,7 +218,7 @@ export function localFiles(
218
218
 
219
219
  const current =
220
220
  (await currentVersion(target)) ??
221
- (link.isSymbolicLink() ? hashVersion(await readlink(target)) : null);
221
+ (link.isSymbolicLink() ? hashVersion(await readLink(target)) : null);
222
222
 
223
223
  if (removeOptions?.expect !== undefined && removeOptions.expect !== current)
224
224
  return { kind: "stale", current };
@@ -326,6 +326,10 @@ async function currentVersion(path: string): Promise<FileVersion | null> {
326
326
  return stats.isDirectory() ? directoryVersion(path) : hashFile(path).catch(missing);
327
327
  }
328
328
 
329
+ async function readLink(path: string): Promise<string> {
330
+ return (await readlink(path)).split(sep).join("/");
331
+ }
332
+
329
333
  async function resolveLink(path: string): Promise<string> {
330
334
  const link = await lstat(path).catch(missing);
331
335
 
@@ -529,7 +533,7 @@ function rejectFailure(code: number | null, stderr: string): void {
529
533
  }
530
534
 
531
535
  function ripgrepPath(path: string): string {
532
- return path.replace(/^\.\//, "").split(sep).join("/");
536
+ return path.split(sep).join("/").replace(/^\.\//, "");
533
537
  }
534
538
 
535
539
  async function* grep(
@@ -586,6 +590,7 @@ async function* grep(
586
590
  } finally {
587
591
  lines.close();
588
592
  child.kill();
593
+ await closed;
589
594
  }
590
595
  }
591
596
 
@@ -1,4 +1,5 @@
1
- import { isAbsolute, relative, sep } from "node:path";
1
+ import { lstat } from "node:fs/promises";
2
+ import { isAbsolute, join, relative, sep } from "node:path";
2
3
  import type { Disposer, GitStatus } from "@nyte-ai/schema";
3
4
  import type { FeedSink } from "./feed.ts";
4
5
  import { createIgnores, watchTree } from "./tree.ts";
@@ -33,7 +34,7 @@ const GIT_STATE = new Set([
33
34
  "rebase-apply",
34
35
  ]);
35
36
 
36
- /** Returns the status watch `localGit` shares among its watchers: the first subscriber starts it and the last one stops it, and each gets the status at once, then again whenever a change could move it. A recursive `fs.watch` on the workspace hears worktree paths that `.gitignore` and `.git/info/exclude` do not ignore, and, in the git directory and the common directory, only HEAD, the index, branch and remote refs, `packed-refs`, `config` and the merge, cherry-pick, revert and rebase state, so object writes, logs and Nyte's snapshot refs read nothing; the `.git` entry itself appearing or going drops the remembered repository and starts over. Only where `fs.watch` is unavailable does it poll, every 2 s. */
37
+ /** Returns the status watch `localGit` shares among its watchers: the first subscriber starts it and the last one stops it, and each gets the status at once, then again whenever a change could move it. A recursive `fs.watch` on the workspace hears worktree paths that `.gitignore` and `.git/info/exclude` do not ignore, and, in the git directory and the common directory, only HEAD, the index, branch and remote refs, `packed-refs`, `config` and the merge, cherry-pick, revert and rebase state, so object writes, logs and Nyte's snapshot refs read nothing; the `.git` entry itself appearing or going (not a change inside it, which Windows also reports on the folder) drops the remembered repository and starts over. Only where `fs.watch` is unavailable does it poll, every 2 s. */
37
38
  export function createWatcher(
38
39
  cwd: string,
39
40
  read: () => Promise<GitStatus>,
@@ -88,7 +89,10 @@ export function createWatcher(
88
89
  );
89
90
  };
90
91
 
91
- void repository().then((repo) => {
92
+ const gitEntry = async (): Promise<boolean> =>
93
+ (await lstat(join(cwd, ".git")).catch(() => null)) !== null;
94
+
95
+ void Promise.all([repository(), gitEntry()]).then(([repo, hadGit]) => {
92
96
  if (stopped) return;
93
97
  const insideGitDir = repo === null ? null : pathInside(cwd, repo.gitDir);
94
98
  const ignores = repo === null ? null : createIgnores(repo.root);
@@ -99,7 +103,11 @@ export function createWatcher(
99
103
  let found: "none" | "status" = "none";
100
104
 
101
105
  for (const path of paths) {
102
- if (path === ".git") return "repository";
106
+ if (path === ".git") {
107
+ if ((await gitEntry()) !== hadGit) return "repository";
108
+
109
+ continue;
110
+ }
103
111
 
104
112
  if (repo === null || ignores === null) continue;
105
113
 
@@ -115,7 +123,10 @@ export function createWatcher(
115
123
  if (path.endsWith(".gitignore"))
116
124
  ignores.forget(`${repo.prefix}${path}`.split("/").slice(0, -1).join("/"));
117
125
 
118
- if (found === "none" && !(await ignores.ignored(`${repo.prefix}${path}`, false)))
126
+ if (found !== "none") continue;
127
+ const directory = (await lstat(join(cwd, path)).catch(() => null))?.isDirectory();
128
+
129
+ if (!(await ignores.ignored(`${repo.prefix}${path}`, directory === true)))
119
130
  found = "status";
120
131
  }
121
132
 
@@ -175,7 +186,6 @@ function movesStatus(path: string): boolean {
175
186
  return (
176
187
  path === "" ||
177
188
  GIT_STATE.has(top) ||
178
- path === "refs" ||
179
189
  path.startsWith("refs/heads/") ||
180
190
  path.startsWith("refs/remotes/")
181
191
  );
package/src/local/git.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { lstat, readFile, stat } from "node:fs/promises";
2
- import { join } from "node:path";
2
+ import { join, resolve } from "node:path";
3
3
  import { asId, SdkError } from "@nyte-ai/schema";
4
4
  import type {
5
5
  BlameView,
@@ -101,7 +101,12 @@ export function localGit(env: ExecutionEnv, options?: { readonly git?: string })
101
101
 
102
102
  return root === undefined || gitDir === undefined
103
103
  ? null
104
- : { root, gitDir, commonDir: commonDir || gitDir, prefix };
104
+ : {
105
+ root: resolve(root),
106
+ gitDir: resolve(gitDir),
107
+ commonDir: resolve(commonDir || gitDir),
108
+ prefix,
109
+ };
105
110
  };
106
111
 
107
112
  const repository = async (): Promise<Repository | null> => {
@@ -1,4 +1,3 @@
1
- import { spawn } from "node:child_process";
2
1
  import { homedir } from "node:os";
3
2
  import type { ChildProcess } from "node:child_process";
4
3
  import { Type } from "typebox";
@@ -17,7 +16,7 @@ import type {
17
16
  } from "@nyte-ai/schema";
18
17
  import { dev } from "@nyte-ai/core";
19
18
  import type { ExecutionEnv, GitHubAccountApi, GitHubApi } from "@nyte-ai/core";
20
- import { findExecutable, run } from "./command.ts";
19
+ import { findExecutable, killTree, run, spawnCommand } from "./command.ts";
21
20
 
22
21
  const ENV = {
23
22
  GH_PROMPT_DISABLED: "1",
@@ -255,7 +254,7 @@ function startSignIn(file: string, cwd: string, ended: () => void): SignIn {
255
254
  let verificationUri: string | undefined;
256
255
  let done = false;
257
256
 
258
- const child: ChildProcess = spawn(
257
+ const child: ChildProcess = spawnCommand(
259
258
  file,
260
259
  [
261
260
  "auth",
@@ -278,7 +277,7 @@ function startSignIn(file: string, cwd: string, ended: () => void): SignIn {
278
277
  child.stdout?.destroy();
279
278
  child.stderr?.destroy();
280
279
 
281
- if (child.exitCode === null && child.signalCode === null) child.kill("SIGKILL");
280
+ if (child.exitCode === null && child.signalCode === null) killTree(child, "SIGKILL");
282
281
  started.resolve(start);
283
282
  ended();
284
283
  };