@gmickel/gno 0.29.0 → 0.29.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gmickel/gno",
3
- "version": "0.29.0",
3
+ "version": "0.29.1",
4
4
  "description": "Local semantic search for your documents. Index Markdown, PDF, and Office files with hybrid BM25 + vector search.",
5
5
  "keywords": [
6
6
  "embeddings",
@@ -5,11 +5,11 @@
5
5
  */
6
6
 
7
7
  // node:fs/promises for rename/unlink (no Bun equivalent for structure ops)
8
- import { rename, unlink } from "node:fs/promises";
9
- // node:os platform: no Bun equivalent
10
- import { platform } from "node:os";
11
- // node:path dirname: no Bun equivalent
12
- import { dirname } from "node:path";
8
+ import { mkdir, rename, unlink } from "node:fs/promises";
9
+ // node:os platform/homedir/tmpdir: no Bun equivalent
10
+ import { homedir, platform as getPlatform, tmpdir } from "node:os";
11
+ // node:path dirname/join/parse: no Bun equivalent
12
+ import { dirname, join, parse } from "node:path";
13
13
 
14
14
  export async function atomicWrite(
15
15
  path: string,
@@ -48,12 +48,176 @@ export async function renameFilePath(
48
48
  await rename(currentPath, nextPath);
49
49
  }
50
50
 
51
- export async function trashFilePath(path: string): Promise<void> {
52
- await runCommand(["trash", path]);
51
+ type TrashFileDeps = {
52
+ homeDir?: string;
53
+ platform?: ReturnType<typeof getPlatform>;
54
+ runCommand?: typeof runCommand;
55
+ tempDir?: string;
56
+ };
57
+
58
+ function isCrossDeviceRenameError(
59
+ error: unknown
60
+ ): error is NodeJS.ErrnoException {
61
+ return (
62
+ error instanceof Error &&
63
+ "code" in error &&
64
+ typeof error.code === "string" &&
65
+ error.code === "EXDEV"
66
+ );
67
+ }
68
+
69
+ async function nextAvailableTrashPath(
70
+ trashDir: string,
71
+ sourcePath: string
72
+ ): Promise<string> {
73
+ const { ext, name, base } = parse(sourcePath);
74
+ let candidate = join(trashDir, base);
75
+ let suffix = 2;
76
+ while (await Bun.file(candidate).exists()) {
77
+ candidate = join(trashDir, `${name} ${suffix}${ext}`);
78
+ suffix += 1;
79
+ }
80
+ return candidate;
81
+ }
82
+
83
+ async function moveFilePath(
84
+ sourcePath: string,
85
+ targetPath: string
86
+ ): Promise<void> {
87
+ try {
88
+ await rename(sourcePath, targetPath);
89
+ return;
90
+ } catch (error) {
91
+ if (!isCrossDeviceRenameError(error)) {
92
+ throw error;
93
+ }
94
+ }
95
+
96
+ await Bun.write(targetPath, Bun.file(sourcePath));
97
+ await unlink(sourcePath);
98
+ }
99
+
100
+ async function trashFilePathOnDarwin(
101
+ path: string,
102
+ homeDir: string
103
+ ): Promise<void> {
104
+ const trashDir = join(homeDir, ".Trash");
105
+ await mkdir(trashDir, { recursive: true });
106
+ const targetPath = await nextAvailableTrashPath(trashDir, path);
107
+ await moveFilePath(path, targetPath);
108
+ }
109
+
110
+ function encodeTrashInfoPath(path: string): string {
111
+ return path
112
+ .split("/")
113
+ .map((segment, index) =>
114
+ index === 0 && segment.length === 0 ? "" : encodeURIComponent(segment)
115
+ )
116
+ .join("/");
117
+ }
118
+
119
+ async function trashFilePathOnLinux(
120
+ path: string,
121
+ homeDir: string
122
+ ): Promise<void> {
123
+ const trashRoot = join(homeDir, ".local", "share", "Trash");
124
+ const filesDir = join(trashRoot, "files");
125
+ const infoDir = join(trashRoot, "info");
126
+ await mkdir(filesDir, { recursive: true });
127
+ await mkdir(infoDir, { recursive: true });
128
+
129
+ const targetPath = await nextAvailableTrashPath(filesDir, path);
130
+ const infoPath = join(infoDir, `${parse(targetPath).base}.trashinfo`);
131
+ const infoContent = [
132
+ "[Trash Info]",
133
+ `Path=${encodeTrashInfoPath(path)}`,
134
+ `DeletionDate=${new Date().toISOString().slice(0, 19)}`,
135
+ "",
136
+ ].join("\n");
137
+
138
+ await moveFilePath(path, targetPath);
139
+ try {
140
+ await Bun.write(infoPath, infoContent);
141
+ } catch (error) {
142
+ await moveFilePath(targetPath, path).catch(() => {
143
+ /* ignore rollback errors */
144
+ });
145
+ throw error;
146
+ }
147
+ }
148
+
149
+ async function trashFilePathOnWindows(
150
+ path: string,
151
+ deps: Required<Pick<TrashFileDeps, "runCommand" | "tempDir">>
152
+ ): Promise<void> {
153
+ const scriptPath = join(deps.tempDir, `gno-trash-${crypto.randomUUID()}.ps1`);
154
+ const script = `param([string]$LiteralPath)
155
+ Add-Type -AssemblyName Microsoft.VisualBasic
156
+ if (Test-Path -LiteralPath $LiteralPath -PathType Container) {
157
+ [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteDirectory(
158
+ $LiteralPath,
159
+ [Microsoft.VisualBasic.FileIO.UIOption]::OnlyErrorDialogs,
160
+ [Microsoft.VisualBasic.FileIO.RecycleOption]::SendToRecycleBin
161
+ )
162
+ } else {
163
+ [Microsoft.VisualBasic.FileIO.FileSystem]::DeleteFile(
164
+ $LiteralPath,
165
+ [Microsoft.VisualBasic.FileIO.UIOption]::OnlyErrorDialogs,
166
+ [Microsoft.VisualBasic.FileIO.RecycleOption]::SendToRecycleBin
167
+ )
168
+ }
169
+ `;
170
+
171
+ await Bun.write(scriptPath, script);
172
+ try {
173
+ await deps.runCommand([
174
+ "powershell",
175
+ "-NoProfile",
176
+ "-ExecutionPolicy",
177
+ "Bypass",
178
+ "-File",
179
+ scriptPath,
180
+ path,
181
+ ]);
182
+ } finally {
183
+ await unlink(scriptPath).catch(() => {
184
+ /* ignore cleanup errors */
185
+ });
186
+ }
187
+ }
188
+
189
+ export async function trashFilePath(
190
+ path: string,
191
+ deps: TrashFileDeps = {}
192
+ ): Promise<void> {
193
+ const platform = deps.platform ?? getPlatform();
194
+ const homeDir = deps.homeDir ?? homedir();
195
+ const runner = deps.runCommand ?? runCommand;
196
+ const tempDir = deps.tempDir ?? tmpdir();
197
+
198
+ if (platform === "darwin") {
199
+ await trashFilePathOnDarwin(path, homeDir);
200
+ return;
201
+ }
202
+
203
+ if (platform === "linux") {
204
+ await trashFilePathOnLinux(path, homeDir);
205
+ return;
206
+ }
207
+
208
+ if (platform === "win32") {
209
+ await trashFilePathOnWindows(path, {
210
+ runCommand: runner,
211
+ tempDir,
212
+ });
213
+ return;
214
+ }
215
+
216
+ throw new Error(`Trash is not supported on platform: ${platform}`);
53
217
  }
54
218
 
55
219
  export async function revealFilePath(path: string): Promise<void> {
56
- if (platform() === "darwin") {
220
+ if (getPlatform() === "darwin") {
57
221
  await runCommand(["open", "-R", path]);
58
222
  return;
59
223
  }
@@ -1258,7 +1258,16 @@ export async function handleTrashDoc(
1258
1258
  defaultSyncService.syncCollection(collectionArg, storeArg, optionsArg));
1259
1259
  ctxHolder.watchService?.suppress(fullPath);
1260
1260
  await (deps?.trashFilePath ?? trashFilePath)(fullPath);
1261
- await store.markInactive(doc.collection, [doc.relPath]);
1261
+ const markInactiveResult = await store.markInactive(doc.collection, [
1262
+ doc.relPath,
1263
+ ]);
1264
+ if (!markInactiveResult.ok) {
1265
+ return errorResponse(
1266
+ "RUNTIME",
1267
+ `File moved to Trash, but removing it from the current index failed: ${markInactiveResult.error.message}`,
1268
+ 500
1269
+ );
1270
+ }
1262
1271
  let warning: string | undefined;
1263
1272
  try {
1264
1273
  await syncCollection(collection, store, {