gestalt-mobile 0.20.0 → 0.21.0
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/dist/client/assets/{index-DYERmevO.css → index-D9-XA9jj.css} +1 -1
- package/dist/client/assets/index-ufTniVfO.js +29 -0
- package/dist/client/index.html +2 -2
- package/dist/server/server/app.js +2 -0
- package/dist/server/server/composition.contracts.js +36 -0
- package/dist/server/server/composition.js +3 -0
- package/dist/server/server/features/files/application/ports.js +6 -0
- package/dist/server/server/features/files/copy-entry/endpoint.js +48 -0
- package/dist/server/server/features/files/delete-entry/endpoint.js +23 -0
- package/dist/server/server/features/files/domain/relative-directory.js +15 -0
- package/dist/server/server/features/files/domain/workspace-directory.js +6 -0
- package/dist/server/server/features/files/list-directory/endpoint.js +49 -0
- package/dist/server/server/features/files/move-entry/endpoint.js +4 -0
- package/dist/server/server/features/files/register-routes.js +19 -0
- package/dist/server/server/features/files/upload-file/endpoint.js +35 -0
- package/dist/server/server/platform/filesystem/filesystem-workspace-files.js +559 -0
- package/dist/server/shared/contracts/org-plan-attention.js +2 -2
- package/dist/server/shared/contracts/quiz.js +2 -2
- package/package.json +1 -1
- package/dist/client/assets/index-Byfm-_gV.js +0 -29
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 Dyne.org foundation
|
|
3
|
+
* Designed by Denis Roio <jaromil@dyne.org>
|
|
4
|
+
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
*/
|
|
6
|
+
import { copyFile, lstat, mkdir, open, readdir, realpath, rename, rm, stat, writeFile, } from 'node:fs/promises';
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
8
|
+
import { constants } from 'node:fs';
|
|
9
|
+
import { basename, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
10
|
+
/** Lists exactly one directory; symlinks are represented but never followed. */
|
|
11
|
+
export class FilesystemWorkspaceFiles {
|
|
12
|
+
filesystem;
|
|
13
|
+
mutationHooks;
|
|
14
|
+
cursors = new Map();
|
|
15
|
+
constructor(filesystem = { lstat, readdir, realpath, stat }, mutationHooks = {}) {
|
|
16
|
+
this.filesystem = filesystem;
|
|
17
|
+
this.mutationHooks = mutationHooks;
|
|
18
|
+
}
|
|
19
|
+
async list(workspaceRoot, input) {
|
|
20
|
+
const root = await this.canonicalDirectory(workspaceRoot);
|
|
21
|
+
if (!root)
|
|
22
|
+
return { kind: 'unreadable' };
|
|
23
|
+
const target = await this.targetDirectory(root, input.directory);
|
|
24
|
+
if (target.kind !== 'directory')
|
|
25
|
+
return target;
|
|
26
|
+
const cursor = input.cursor === undefined ? undefined : this.cursors.get(input.cursor);
|
|
27
|
+
if (input.cursor !== undefined && !cursor)
|
|
28
|
+
return { kind: 'invalid-cursor' };
|
|
29
|
+
if (cursor &&
|
|
30
|
+
(cursor.d !== input.directory ||
|
|
31
|
+
cursor.i !== Number(target.metadata.ino) ||
|
|
32
|
+
cursor.m !== Number(target.metadata.mtimeMs)))
|
|
33
|
+
return { kind: 'stale-cursor' };
|
|
34
|
+
try {
|
|
35
|
+
const names = await this.filesystem.readdir(target.path, { encoding: 'utf8' });
|
|
36
|
+
const entries = [];
|
|
37
|
+
for (const name of names) {
|
|
38
|
+
if (name === '.git')
|
|
39
|
+
continue;
|
|
40
|
+
const child = join(target.path, name);
|
|
41
|
+
try {
|
|
42
|
+
const metadata = await this.filesystem.lstat(child);
|
|
43
|
+
const kind = metadata.isSymbolicLink()
|
|
44
|
+
? 'symlink'
|
|
45
|
+
: metadata.isDirectory()
|
|
46
|
+
? 'directory'
|
|
47
|
+
: metadata.isFile()
|
|
48
|
+
? 'file'
|
|
49
|
+
: null;
|
|
50
|
+
if (!kind)
|
|
51
|
+
continue;
|
|
52
|
+
const path = input.directory === '' ? name : `${input.directory}/${name}`;
|
|
53
|
+
entries.push({
|
|
54
|
+
name,
|
|
55
|
+
path,
|
|
56
|
+
kind,
|
|
57
|
+
...(kind === 'file'
|
|
58
|
+
? { size: metadata.size, modifiedAt: metadata.mtime.toISOString() }
|
|
59
|
+
: {}),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
if (error.code !== 'ENOENT')
|
|
64
|
+
return { kind: 'unreadable' };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
entries.sort(compareEntries);
|
|
68
|
+
if (cursor && cursor.s !== snapshot(entries))
|
|
69
|
+
return { kind: 'stale-cursor' };
|
|
70
|
+
const start = cursor ? entries.findIndex((entry) => entry.name === cursor.a) + 1 : 0;
|
|
71
|
+
if (cursor && start === 0)
|
|
72
|
+
return { kind: 'stale-cursor' };
|
|
73
|
+
const pageEntries = entries.slice(start, start + input.limit);
|
|
74
|
+
const last = pageEntries.at(-1);
|
|
75
|
+
const nextCursor = start + pageEntries.length < entries.length && last
|
|
76
|
+
? this.rememberCursor({
|
|
77
|
+
d: input.directory,
|
|
78
|
+
i: Number(target.metadata.ino),
|
|
79
|
+
m: Number(target.metadata.mtimeMs),
|
|
80
|
+
a: last.name,
|
|
81
|
+
s: snapshot(entries),
|
|
82
|
+
})
|
|
83
|
+
: undefined;
|
|
84
|
+
const page = {
|
|
85
|
+
directory: input.directory,
|
|
86
|
+
entries: pageEntries,
|
|
87
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
88
|
+
};
|
|
89
|
+
return { kind: 'available', page };
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return error.code === 'ENOENT'
|
|
93
|
+
? { kind: 'missing' }
|
|
94
|
+
: { kind: 'unreadable' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async copy(rootPath, input) {
|
|
98
|
+
return this.transfer(rootPath, input, false);
|
|
99
|
+
}
|
|
100
|
+
async move(rootPath, input) {
|
|
101
|
+
return this.transfer(rootPath, input, true);
|
|
102
|
+
}
|
|
103
|
+
async upload(rootPath, input) {
|
|
104
|
+
if (!validName(input.filename))
|
|
105
|
+
return { kind: 'invalid-destination' };
|
|
106
|
+
const root = await this.canonicalDirectory(rootPath);
|
|
107
|
+
if (!root)
|
|
108
|
+
return { kind: 'unreadable' };
|
|
109
|
+
const parent = await anchoredDirectory(root, input.directory);
|
|
110
|
+
if (!parent)
|
|
111
|
+
return { kind: 'invalid-destination' };
|
|
112
|
+
try {
|
|
113
|
+
await this.mutationHooks.afterParentCheck?.('upload');
|
|
114
|
+
if (!(await stableAnchor(root, input.directory, parent.path)))
|
|
115
|
+
return { kind: 'invalid-destination' };
|
|
116
|
+
const target = await this.targetName(parent.path, input.filename, input.conflict, 'file');
|
|
117
|
+
if (typeof target !== 'string')
|
|
118
|
+
return target;
|
|
119
|
+
const temp = join(parent.path, `.${randomUUID()}.upload`);
|
|
120
|
+
await writeFile(temp, input.content, { flag: 'wx' });
|
|
121
|
+
try {
|
|
122
|
+
await this.mutationHooks.beforePublish?.('upload');
|
|
123
|
+
await this.publishFile(temp, target, input.conflict);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
kind: 'available',
|
|
130
|
+
source: '',
|
|
131
|
+
path: input.directory ? `${input.directory}/${basename(target)}` : basename(target),
|
|
132
|
+
entryKind: 'file',
|
|
133
|
+
conflict: input.conflict,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { kind: 'unreadable' };
|
|
138
|
+
}
|
|
139
|
+
finally {
|
|
140
|
+
await parent.close();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async delete(rootPath, input) {
|
|
144
|
+
if (!input.recursive)
|
|
145
|
+
return { kind: 'invalid-destination' };
|
|
146
|
+
if (input.path === '.git' || input.path.startsWith('.git/'))
|
|
147
|
+
return { kind: 'protected' };
|
|
148
|
+
if (!validRelative(input.path))
|
|
149
|
+
return { kind: 'invalid-destination' };
|
|
150
|
+
const root = await this.canonicalDirectory(rootPath);
|
|
151
|
+
if (!root)
|
|
152
|
+
return { kind: 'unreadable' };
|
|
153
|
+
const parent = await anchoredDirectory(root, dirnameRelative(input.path));
|
|
154
|
+
if (!parent)
|
|
155
|
+
return { kind: 'missing' };
|
|
156
|
+
try {
|
|
157
|
+
await this.mutationHooks.afterParentCheck?.('delete');
|
|
158
|
+
if (!(await stableAnchor(root, dirnameRelative(input.path), parent.path)))
|
|
159
|
+
return { kind: 'missing' };
|
|
160
|
+
const target = join(parent.path, basename(input.path));
|
|
161
|
+
const metadata = await safeLstat(target);
|
|
162
|
+
if (!metadata || metadata.isSymbolicLink())
|
|
163
|
+
return { kind: 'missing' };
|
|
164
|
+
await rm(target, { recursive: metadata.isDirectory(), force: false });
|
|
165
|
+
return {
|
|
166
|
+
kind: 'available',
|
|
167
|
+
source: input.path,
|
|
168
|
+
path: input.path,
|
|
169
|
+
entryKind: metadata.isDirectory() ? 'directory' : 'file',
|
|
170
|
+
conflict: 'reject',
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return { kind: 'unreadable' };
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
await parent.close();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async transfer(rootPath, input, moving) {
|
|
181
|
+
if (!validRelative(input.source) ||
|
|
182
|
+
(input.destinationDirectory !== '' && !validRelative(input.destinationDirectory)))
|
|
183
|
+
return { kind: 'invalid-destination' };
|
|
184
|
+
const root = await this.canonicalDirectory(rootPath);
|
|
185
|
+
if (!root)
|
|
186
|
+
return { kind: 'unreadable' };
|
|
187
|
+
if (input.source === '.git' || input.source.startsWith('.git/'))
|
|
188
|
+
return { kind: 'protected' };
|
|
189
|
+
const sourceParent = await anchoredDirectory(root, dirnameRelative(input.source));
|
|
190
|
+
if (!sourceParent)
|
|
191
|
+
return { kind: 'missing' };
|
|
192
|
+
const parent = await anchoredDirectory(root, input.destinationDirectory);
|
|
193
|
+
if (!parent) {
|
|
194
|
+
await sourceParent.close();
|
|
195
|
+
return { kind: 'invalid-destination' };
|
|
196
|
+
}
|
|
197
|
+
const source = join(sourceParent.path, basename(input.source));
|
|
198
|
+
const sourceMetadata = await safeLstat(source);
|
|
199
|
+
if (!sourceMetadata || sourceMetadata.isSymbolicLink()) {
|
|
200
|
+
await Promise.all([sourceParent.close(), parent.close()]);
|
|
201
|
+
return { kind: 'missing' };
|
|
202
|
+
}
|
|
203
|
+
if (dirnameRelative(input.source) === input.destinationDirectory && moving) {
|
|
204
|
+
await Promise.all([sourceParent.close(), parent.close()]);
|
|
205
|
+
return { kind: 'same-parent' };
|
|
206
|
+
}
|
|
207
|
+
if (sourceMetadata.isDirectory() &&
|
|
208
|
+
(input.destinationDirectory === input.source ||
|
|
209
|
+
input.destinationDirectory.startsWith(`${input.source}/`))) {
|
|
210
|
+
await Promise.all([sourceParent.close(), parent.close()]);
|
|
211
|
+
return { kind: 'source-inside-destination' };
|
|
212
|
+
}
|
|
213
|
+
const target = await this.targetName(parent.path, basename(source), input.conflict, sourceMetadata.isFile() ? 'file' : 'directory');
|
|
214
|
+
if (typeof target !== 'string') {
|
|
215
|
+
await Promise.all([sourceParent.close(), parent.close()]);
|
|
216
|
+
return target;
|
|
217
|
+
}
|
|
218
|
+
try {
|
|
219
|
+
await this.mutationHooks.afterParentCheck?.(moving ? 'move' : 'copy');
|
|
220
|
+
if (!(await stableAnchor(root, dirnameRelative(input.source), sourceParent.path)) ||
|
|
221
|
+
!(await stableAnchor(root, input.destinationDirectory, parent.path)))
|
|
222
|
+
return { kind: 'invalid-destination' };
|
|
223
|
+
await this.mutationHooks.beforePublish?.(moving ? 'move' : 'copy');
|
|
224
|
+
if (moving) {
|
|
225
|
+
await this.publishMove(source, target, input.conflict, sourceMetadata.isDirectory());
|
|
226
|
+
}
|
|
227
|
+
else if (sourceMetadata.isFile()) {
|
|
228
|
+
const temp = join(parent.path, `.${randomUUID()}.copy`);
|
|
229
|
+
try {
|
|
230
|
+
const sourceHandle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
231
|
+
try {
|
|
232
|
+
await copyFile(procPath(sourceHandle.fd), temp, 0);
|
|
233
|
+
}
|
|
234
|
+
finally {
|
|
235
|
+
await sourceHandle.close();
|
|
236
|
+
}
|
|
237
|
+
await this.publishFile(temp, target, input.conflict);
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
await rm(temp, { force: true }).catch(() => undefined);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const temp = join(parent.path, `.${randomUUID()}.copy-directory`);
|
|
245
|
+
try {
|
|
246
|
+
const sourceDirectory = await openDirectory(source);
|
|
247
|
+
if (!sourceDirectory)
|
|
248
|
+
throw new Error('source changed');
|
|
249
|
+
try {
|
|
250
|
+
await copyTree(sourceDirectory.path, temp, true);
|
|
251
|
+
}
|
|
252
|
+
finally {
|
|
253
|
+
await sourceDirectory.close();
|
|
254
|
+
}
|
|
255
|
+
await this.publishDirectory(temp, target, input.conflict);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
await rm(temp, { recursive: true, force: true }).catch(() => undefined);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
kind: 'available',
|
|
263
|
+
source: input.source,
|
|
264
|
+
path: input.destinationDirectory
|
|
265
|
+
? `${input.destinationDirectory}/${basename(target)}`
|
|
266
|
+
: basename(target),
|
|
267
|
+
entryKind: sourceMetadata.isDirectory() ? 'directory' : 'file',
|
|
268
|
+
conflict: input.conflict,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return { kind: 'unreadable' };
|
|
273
|
+
}
|
|
274
|
+
finally {
|
|
275
|
+
await Promise.all([sourceParent.close(), parent.close()]);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async safeDirectory(root, directory) {
|
|
279
|
+
if (directory !== '' && !validRelative(directory))
|
|
280
|
+
return null;
|
|
281
|
+
const entry = await this.safeEntry(root, directory, true);
|
|
282
|
+
return entry?.metadata.isDirectory() ? entry.path : null;
|
|
283
|
+
}
|
|
284
|
+
async targetName(parent, name, conflict, sourceKind) {
|
|
285
|
+
let target = join(parent, name);
|
|
286
|
+
const existing = await safeLstat(target);
|
|
287
|
+
if (!existing)
|
|
288
|
+
return target;
|
|
289
|
+
if (existing.isSymbolicLink())
|
|
290
|
+
return { kind: 'symlink' };
|
|
291
|
+
if (conflict === 'reject')
|
|
292
|
+
return { kind: 'conflict', replaceAllowed: sourceKind === 'file' && existing.isFile() };
|
|
293
|
+
if (conflict === 'replace') {
|
|
294
|
+
if (sourceKind !== 'file' || !existing.isFile())
|
|
295
|
+
return { kind: 'replace-unsupported' };
|
|
296
|
+
return target;
|
|
297
|
+
}
|
|
298
|
+
const extension = extname(name);
|
|
299
|
+
const stem = extension ? name.slice(0, -extension.length) : name;
|
|
300
|
+
for (let index = 1; index < 10000; index += 1) {
|
|
301
|
+
target = join(parent, `${stem} (${index === 1 ? 'copy' : index})${extension}`);
|
|
302
|
+
if (!(await safeLstat(target)))
|
|
303
|
+
return target;
|
|
304
|
+
}
|
|
305
|
+
return { kind: 'unreadable' };
|
|
306
|
+
}
|
|
307
|
+
async safeEntry(root, value, allowRoot = false) {
|
|
308
|
+
if (value === '' && !allowRoot)
|
|
309
|
+
return null;
|
|
310
|
+
let path = root;
|
|
311
|
+
for (const segment of value === '' ? [] : value.split('/')) {
|
|
312
|
+
if (!validName(segment))
|
|
313
|
+
return null;
|
|
314
|
+
path = join(path, segment);
|
|
315
|
+
const metadata = await safeLstat(path);
|
|
316
|
+
if (!metadata || metadata.isSymbolicLink())
|
|
317
|
+
return null;
|
|
318
|
+
try {
|
|
319
|
+
if (!within(root, await realpath(path)))
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const metadata = await safeLstat(path);
|
|
327
|
+
return metadata && !metadata.isSymbolicLink() ? { path, metadata } : null;
|
|
328
|
+
}
|
|
329
|
+
async publishFile(temp, target, conflict) {
|
|
330
|
+
const reservation = conflict === 'replace' ? null : await reserveFile(target);
|
|
331
|
+
try {
|
|
332
|
+
await rename(temp, target);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
if (reservation)
|
|
336
|
+
await removeReservation(target, reservation, false);
|
|
337
|
+
throw error;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
async publishDirectory(temp, target, conflict) {
|
|
341
|
+
const reservation = conflict === 'replace' ? null : await reserveDirectory(target);
|
|
342
|
+
try {
|
|
343
|
+
await rename(temp, target);
|
|
344
|
+
}
|
|
345
|
+
catch (error) {
|
|
346
|
+
if (reservation)
|
|
347
|
+
await removeReservation(target, reservation, true);
|
|
348
|
+
throw error;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async publishMove(source, target, conflict, directory) {
|
|
352
|
+
const reservation = conflict === 'replace'
|
|
353
|
+
? null
|
|
354
|
+
: directory
|
|
355
|
+
? await reserveDirectory(target)
|
|
356
|
+
: await reserveFile(target);
|
|
357
|
+
try {
|
|
358
|
+
await rename(source, target);
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
if (reservation)
|
|
362
|
+
await removeReservation(target, reservation, directory);
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
rememberCursor(cursor) {
|
|
367
|
+
const token = randomUUID();
|
|
368
|
+
this.cursors.set(token, cursor);
|
|
369
|
+
if (this.cursors.size > 2_000)
|
|
370
|
+
this.cursors.delete(this.cursors.keys().next().value);
|
|
371
|
+
return token;
|
|
372
|
+
}
|
|
373
|
+
async canonicalDirectory(path) {
|
|
374
|
+
if (!isAbsolute(path))
|
|
375
|
+
return null;
|
|
376
|
+
try {
|
|
377
|
+
const canonical = await this.filesystem.realpath(resolve(path));
|
|
378
|
+
return (await this.filesystem.stat(canonical)).isDirectory() ? canonical : null;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async targetDirectory(root, directory) {
|
|
385
|
+
let path = root;
|
|
386
|
+
for (const segment of directory === '' ? [] : directory.split('/')) {
|
|
387
|
+
path = join(path, segment);
|
|
388
|
+
try {
|
|
389
|
+
const metadata = await this.filesystem.lstat(path);
|
|
390
|
+
if (metadata.isSymbolicLink())
|
|
391
|
+
return { kind: 'missing' };
|
|
392
|
+
const canonical = await this.filesystem.realpath(path);
|
|
393
|
+
if (!within(root, canonical))
|
|
394
|
+
return { kind: 'missing' };
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
return error.code === 'ENOENT'
|
|
398
|
+
? { kind: 'missing' }
|
|
399
|
+
: { kind: 'unreadable' };
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
try {
|
|
403
|
+
const metadata = await this.filesystem.lstat(path);
|
|
404
|
+
if (metadata.isSymbolicLink())
|
|
405
|
+
return { kind: 'missing' };
|
|
406
|
+
if (!metadata.isDirectory())
|
|
407
|
+
return { kind: 'not-directory' };
|
|
408
|
+
const canonical = await this.filesystem.realpath(path);
|
|
409
|
+
if (!within(root, canonical) || !(await this.filesystem.stat(canonical)).isDirectory())
|
|
410
|
+
return { kind: 'missing' };
|
|
411
|
+
return { kind: 'directory', path: canonical, metadata };
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
return error.code === 'ENOENT'
|
|
415
|
+
? { kind: 'missing' }
|
|
416
|
+
: { kind: 'unreadable' };
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function within(root, candidate) {
|
|
421
|
+
const path = relative(root, candidate);
|
|
422
|
+
return path === '' || (!path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path));
|
|
423
|
+
}
|
|
424
|
+
function compareEntries(left, right) {
|
|
425
|
+
const rank = { directory: 0, file: 1, symlink: 2 };
|
|
426
|
+
return rank[left.kind] - rank[right.kind] || compareCodePoints(left.name, right.name);
|
|
427
|
+
}
|
|
428
|
+
/** Unicode scalar-value ordering, independent of host locale and UTF-16 code units. */
|
|
429
|
+
function compareCodePoints(left, right) {
|
|
430
|
+
let leftIndex = 0;
|
|
431
|
+
let rightIndex = 0;
|
|
432
|
+
while (leftIndex < left.length && rightIndex < right.length) {
|
|
433
|
+
const leftPoint = left.codePointAt(leftIndex);
|
|
434
|
+
const rightPoint = right.codePointAt(rightIndex);
|
|
435
|
+
if (leftPoint !== rightPoint)
|
|
436
|
+
return leftPoint < rightPoint ? -1 : 1;
|
|
437
|
+
leftIndex += leftPoint > 0xffff ? 2 : 1;
|
|
438
|
+
rightIndex += rightPoint > 0xffff ? 2 : 1;
|
|
439
|
+
}
|
|
440
|
+
return leftIndex === left.length ? (rightIndex === right.length ? 0 : -1) : 1;
|
|
441
|
+
}
|
|
442
|
+
function snapshot(entries) {
|
|
443
|
+
return entries.map((entry) => `${entry.kind}\0${entry.name}`).join('\0');
|
|
444
|
+
}
|
|
445
|
+
function validRelative(value) {
|
|
446
|
+
return (value !== '' &&
|
|
447
|
+
!value.includes('\\') &&
|
|
448
|
+
!value.includes('\0') &&
|
|
449
|
+
value.split('/').every(validName));
|
|
450
|
+
}
|
|
451
|
+
function validName(value) {
|
|
452
|
+
return (value !== '' &&
|
|
453
|
+
value !== '.' &&
|
|
454
|
+
value !== '..' &&
|
|
455
|
+
value !== '.git' &&
|
|
456
|
+
!value.includes('/') &&
|
|
457
|
+
!value.includes('\\') &&
|
|
458
|
+
!value.includes('\0'));
|
|
459
|
+
}
|
|
460
|
+
async function safeLstat(path) {
|
|
461
|
+
try {
|
|
462
|
+
return await lstat(path);
|
|
463
|
+
}
|
|
464
|
+
catch {
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function dirnameRelative(path) {
|
|
469
|
+
const slash = path.lastIndexOf('/');
|
|
470
|
+
return slash < 0 ? '' : path.slice(0, slash);
|
|
471
|
+
}
|
|
472
|
+
function procPath(fd) {
|
|
473
|
+
return `/proc/self/fd/${fd}`;
|
|
474
|
+
}
|
|
475
|
+
async function openDirectory(path) {
|
|
476
|
+
try {
|
|
477
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
478
|
+
return { path: procPath(handle.fd), close: () => handle.close() };
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async function anchoredDirectory(root, directory) {
|
|
485
|
+
if (directory !== '' && !validRelative(directory))
|
|
486
|
+
return null;
|
|
487
|
+
let current = await openDirectory(root);
|
|
488
|
+
if (!current)
|
|
489
|
+
return null;
|
|
490
|
+
for (const segment of directory === '' ? [] : directory.split('/')) {
|
|
491
|
+
const next = await openDirectory(join(current.path, segment));
|
|
492
|
+
await current.close();
|
|
493
|
+
if (!next)
|
|
494
|
+
return null;
|
|
495
|
+
current = next;
|
|
496
|
+
}
|
|
497
|
+
return current;
|
|
498
|
+
}
|
|
499
|
+
async function reserveFile(path) {
|
|
500
|
+
const reservation = await open(path, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, 0o600);
|
|
501
|
+
try {
|
|
502
|
+
return await reservation.stat();
|
|
503
|
+
}
|
|
504
|
+
finally {
|
|
505
|
+
await reservation.close();
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
async function reserveDirectory(path) {
|
|
509
|
+
await mkdir(path);
|
|
510
|
+
return lstat(path);
|
|
511
|
+
}
|
|
512
|
+
async function removeReservation(path, reservation, directory) {
|
|
513
|
+
const current = await safeLstat(path);
|
|
514
|
+
if (current?.dev === reservation.dev && current.ino === reservation.ino)
|
|
515
|
+
await rm(path, { recursive: directory, force: true }).catch(() => undefined);
|
|
516
|
+
}
|
|
517
|
+
async function stableAnchor(root, directory, anchor) {
|
|
518
|
+
try {
|
|
519
|
+
const current = await realpath(directory ? join(root, ...directory.split('/')) : root);
|
|
520
|
+
const anchored = await realpath(anchor);
|
|
521
|
+
return current === anchored && within(root, anchored);
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
async function copyTree(source, target, directory) {
|
|
528
|
+
if (!directory) {
|
|
529
|
+
const sourceHandle = await open(source, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
530
|
+
try {
|
|
531
|
+
await copyFile(procPath(sourceHandle.fd), target, 0);
|
|
532
|
+
}
|
|
533
|
+
finally {
|
|
534
|
+
await sourceHandle.close();
|
|
535
|
+
}
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
await mkdir(target);
|
|
539
|
+
for (const name of await readdir(source)) {
|
|
540
|
+
const childSource = join(source, name);
|
|
541
|
+
const metadata = await lstat(childSource);
|
|
542
|
+
if (metadata.isSymbolicLink() || name === '.git')
|
|
543
|
+
throw new Error('unsupported source');
|
|
544
|
+
if (metadata.isDirectory()) {
|
|
545
|
+
const child = await openDirectory(childSource);
|
|
546
|
+
if (!child)
|
|
547
|
+
throw new Error('source changed');
|
|
548
|
+
try {
|
|
549
|
+
await copyTree(child.path, join(target, name), true);
|
|
550
|
+
}
|
|
551
|
+
finally {
|
|
552
|
+
await child.close();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
else {
|
|
556
|
+
await copyTree(childSource, join(target, name), false);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
@@ -83,7 +83,7 @@ export function parseOrgPlanAttentionResponse(value) {
|
|
|
83
83
|
export function toOrgPlanAttentionToolResponse(response) {
|
|
84
84
|
return {
|
|
85
85
|
success: true,
|
|
86
|
-
contentItems: [{ type: '
|
|
86
|
+
contentItems: [{ type: 'inputText', text: JSON.stringify(response) }],
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
export function isOrgPlanAttentionToolResponse(value) {
|
|
@@ -92,7 +92,7 @@ export function isOrgPlanAttentionToolResponse(value) {
|
|
|
92
92
|
const item = value.contentItems[0];
|
|
93
93
|
if (value.contentItems.length !== 1 ||
|
|
94
94
|
!isRecord(item) ||
|
|
95
|
-
item.type !== '
|
|
95
|
+
item.type !== 'inputText' ||
|
|
96
96
|
typeof item.text !== 'string')
|
|
97
97
|
return false;
|
|
98
98
|
try {
|
|
@@ -92,7 +92,7 @@ export function toQuizToolResponse(answers) {
|
|
|
92
92
|
return {
|
|
93
93
|
contentItems: [
|
|
94
94
|
{
|
|
95
|
-
type: '
|
|
95
|
+
type: 'inputText',
|
|
96
96
|
text: JSON.stringify({
|
|
97
97
|
answers: Object.fromEntries(answers
|
|
98
98
|
.filter((answer) => answer.answer.trim())
|
|
@@ -109,7 +109,7 @@ export function isQuizToolResponseForQuiz(quiz, value) {
|
|
|
109
109
|
const item = value.contentItems[0];
|
|
110
110
|
if (value.contentItems.length !== 1 ||
|
|
111
111
|
!isRecord(item) ||
|
|
112
|
-
item.type !== '
|
|
112
|
+
item.type !== 'inputText' ||
|
|
113
113
|
typeof item.text !== 'string')
|
|
114
114
|
return false;
|
|
115
115
|
try {
|