agents 0.7.7 → 0.7.8

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.
@@ -1,1265 +0,0 @@
1
- import { channel } from "node:diagnostics_channel";
2
- import { Shell, defineCommand } from "@cloudflare/shell";
3
- //#region src/experimental/workspace.ts
4
- let _Symbol$dispose;
5
- const DEFAULT_INLINE_THRESHOLD = 15e5;
6
- const TEXT_ENCODER = new TextEncoder();
7
- const TEXT_DECODER = new TextDecoder();
8
- const MAX_SYMLINK_DEPTH = 40;
9
- const DEFAULT_BASH_LIMITS = {
10
- maxCommandCount: 5e3,
11
- maxLoopIterations: 2e3,
12
- maxCallDepth: 50
13
- };
14
- const VALID_NAMESPACE = /^[a-zA-Z][a-zA-Z0-9_]*$/;
15
- const LIKE_ESCAPE = "\\";
16
- const MAX_STREAM_SIZE = 100 * 1024 * 1024;
17
- const MAX_DIFF_LINES = 1e4;
18
- const MAX_PATH_LENGTH = 4096;
19
- const MAX_SYMLINK_TARGET_LENGTH = 4096;
20
- const MAX_MKDIR_DEPTH = 100;
21
- const SESS_STATE_BEGIN = "__BASHSESSION_STATE_BEGIN__";
22
- const SESS_STATE_END = "__BASHSESSION_STATE_END__";
23
- const SESS_CWD_PREFIX = "__SESS_CWD__=";
24
- const workspaceRegistry = /* @__PURE__ */ new WeakMap();
25
- const wsChannel = channel("agents:workspace");
26
- var Workspace = class {
27
- /**
28
- * @param host - Any object with a `sql` tagged-template method (typically your Agent: `this`).
29
- * @param options - Optional configuration (namespace, R2 bucket, thresholds, etc.).
30
- *
31
- * ```ts
32
- * class MyAgent extends Agent<Env> {
33
- * workspace = new Workspace(this, {
34
- * r2: this.env.WORKSPACE_FILES,
35
- * // r2Prefix defaults to this.name (the Durable Object ID)
36
- * });
37
- * }
38
- * ```
39
- */
40
- constructor(host, options) {
41
- this.initialized = false;
42
- this.sqlCache = /* @__PURE__ */ new Map();
43
- const ns = options?.namespace ?? "default";
44
- if (!VALID_NAMESPACE.test(ns)) throw new Error(`Invalid workspace namespace "${ns}": must start with a letter and contain only alphanumeric characters or underscores`);
45
- const registered = workspaceRegistry.get(host) ?? /* @__PURE__ */ new Set();
46
- if (registered.has(ns)) throw new Error(`Workspace namespace "${ns}" is already registered on this agent`);
47
- registered.add(ns);
48
- workspaceRegistry.set(host, registered);
49
- this.host = host;
50
- this.namespace = ns;
51
- this.tableName = `cf_workspace_${ns}`;
52
- this.indexName = `cf_workspace_${ns}_parent`;
53
- this.r2 = options?.r2 ?? null;
54
- this.r2Prefix = options?.r2Prefix;
55
- this.threshold = options?.inlineThreshold ?? DEFAULT_INLINE_THRESHOLD;
56
- this.bashLimits = {
57
- ...DEFAULT_BASH_LIMITS,
58
- ...options?.bashLimits
59
- };
60
- this.commands = options?.commands ?? [];
61
- this.env = options?.env ?? {};
62
- this.network = options?.network;
63
- this.onChange = options?.onChange;
64
- }
65
- emit(type, path, entryType) {
66
- if (this.onChange) this.onChange({
67
- type,
68
- path,
69
- entryType
70
- });
71
- }
72
- _observe(type, payload) {
73
- wsChannel.publish({
74
- type,
75
- name: this.host.name,
76
- payload: {
77
- ...payload,
78
- namespace: this.namespace
79
- },
80
- timestamp: Date.now()
81
- });
82
- }
83
- sql(strings, ...values) {
84
- let tsa = this.sqlCache.get(strings);
85
- if (!tsa) {
86
- const replaced = strings.map((s) => s.replace(/__TABLE__/g, this.tableName).replace(/__INDEX__/g, this.indexName));
87
- tsa = Object.assign(replaced, { raw: replaced });
88
- this.sqlCache.set(strings, tsa);
89
- }
90
- return this.host.sql(tsa, ...values);
91
- }
92
- ensureInit() {
93
- if (this.initialized) return;
94
- this.initialized = true;
95
- this.sql`
96
- CREATE TABLE IF NOT EXISTS __TABLE__ (
97
- path TEXT PRIMARY KEY,
98
- parent_path TEXT NOT NULL,
99
- name TEXT NOT NULL,
100
- type TEXT NOT NULL CHECK(type IN ('file','directory','symlink')),
101
- mime_type TEXT NOT NULL DEFAULT 'text/plain',
102
- size INTEGER NOT NULL DEFAULT 0,
103
- storage_backend TEXT NOT NULL DEFAULT 'inline' CHECK(storage_backend IN ('inline','r2')),
104
- r2_key TEXT,
105
- target TEXT,
106
- content_encoding TEXT NOT NULL DEFAULT 'utf8',
107
- content TEXT,
108
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
109
- modified_at INTEGER NOT NULL DEFAULT (unixepoch())
110
- )
111
- `;
112
- this.sql`
113
- CREATE INDEX IF NOT EXISTS __INDEX__
114
- ON __TABLE__(parent_path)
115
- `;
116
- if ((this.sql`
117
- SELECT COUNT(*) AS cnt FROM __TABLE__ WHERE path = '/'
118
- `[0]?.cnt ?? 0) === 0) {
119
- const now = Math.floor(Date.now() / 1e3);
120
- this.sql`
121
- INSERT INTO __TABLE__
122
- (path, parent_path, name, type, size, created_at, modified_at)
123
- VALUES ('/', '', '', 'directory', 0, ${now}, ${now})
124
- `;
125
- }
126
- }
127
- getR2() {
128
- return this.r2;
129
- }
130
- resolveR2Prefix() {
131
- if (this.r2Prefix !== void 0) return this.r2Prefix;
132
- const name = this.host.name;
133
- if (!name) throw new Error("[Workspace] R2 is configured but no r2Prefix was provided and host.name is not available. Either pass r2Prefix in WorkspaceOptions or ensure the host exposes a name property.");
134
- return name;
135
- }
136
- r2Key(filePath) {
137
- return `${this.resolveR2Prefix()}/${this.namespace}${filePath}`;
138
- }
139
- resolveSymlink(path, depth = 0) {
140
- if (depth > MAX_SYMLINK_DEPTH) throw new Error(`ELOOP: too many levels of symbolic links: ${path}`);
141
- const r = this.sql`
142
- SELECT type, target FROM __TABLE__ WHERE path = ${path}
143
- `[0];
144
- if (!r || r.type !== "symlink" || !r.target) return path;
145
- const resolved = r.target.startsWith("/") ? normalizePath(r.target) : normalizePath(getParent(path) + "/" + r.target);
146
- return this.resolveSymlink(resolved, depth + 1);
147
- }
148
- symlink(target, linkPath) {
149
- this.ensureInit();
150
- if (!target || target.trim().length === 0) throw new Error("EINVAL: symlink target must not be empty");
151
- if (target.length > MAX_SYMLINK_TARGET_LENGTH) throw new Error(`ENAMETOOLONG: symlink target exceeds ${MAX_SYMLINK_TARGET_LENGTH} characters`);
152
- const normalized = normalizePath(linkPath);
153
- if (normalized === "/") throw new Error("EPERM: cannot create symlink at root");
154
- const parentPath = getParent(normalized);
155
- const name = getBasename(normalized);
156
- const now = Math.floor(Date.now() / 1e3);
157
- this.ensureParentDir(parentPath);
158
- if (this.sql`
159
- SELECT type FROM __TABLE__ WHERE path = ${normalized}
160
- `[0]) throw new Error(`EEXIST: path already exists: ${linkPath}`);
161
- this.sql`
162
- INSERT INTO __TABLE__
163
- (path, parent_path, name, type, target, size, created_at, modified_at)
164
- VALUES
165
- (${normalized}, ${parentPath}, ${name}, 'symlink', ${target}, 0, ${now}, ${now})
166
- `;
167
- this.emit("create", normalized, "symlink");
168
- }
169
- readlink(path) {
170
- this.ensureInit();
171
- const normalized = normalizePath(path);
172
- const r = this.sql`
173
- SELECT type, target FROM __TABLE__ WHERE path = ${normalized}
174
- `[0];
175
- if (!r) throw new Error(`ENOENT: no such file or directory: ${path}`);
176
- if (r.type !== "symlink" || !r.target) throw new Error(`EINVAL: not a symlink: ${path}`);
177
- return r.target;
178
- }
179
- lstat(path) {
180
- this.ensureInit();
181
- const normalized = normalizePath(path);
182
- const r = this.sql`
183
- SELECT path, name, type, mime_type, size, created_at, modified_at, target
184
- FROM __TABLE__ WHERE path = ${normalized}
185
- `[0];
186
- if (!r) return null;
187
- return toFileInfo(r);
188
- }
189
- stat(path) {
190
- this.ensureInit();
191
- const normalized = normalizePath(path);
192
- const resolved = this.resolveSymlink(normalized);
193
- const r = this.sql`
194
- SELECT path, name, type, mime_type, size, created_at, modified_at, target
195
- FROM __TABLE__ WHERE path = ${resolved}
196
- `[0];
197
- if (!r) return null;
198
- return toFileInfo(r);
199
- }
200
- async readFile(path) {
201
- this.ensureInit();
202
- const normalized = normalizePath(path);
203
- const resolved = this.resolveSymlink(normalized);
204
- const r = this.sql`
205
- SELECT type, storage_backend, r2_key, content, content_encoding
206
- FROM __TABLE__ WHERE path = ${resolved}
207
- `[0];
208
- if (!r) return null;
209
- if (r.type !== "file") throw new Error(`EISDIR: ${path} is a directory`);
210
- this._observe("workspace:read", {
211
- path: resolved,
212
- storage: r.storage_backend
213
- });
214
- if (r.storage_backend === "r2" && r.r2_key) {
215
- const r2 = this.getR2();
216
- if (!r2) throw new Error(`File ${path} is stored in R2 but no R2 bucket was provided`);
217
- const obj = await r2.get(r.r2_key);
218
- if (!obj) return "";
219
- return await obj.text();
220
- }
221
- if (r.content_encoding === "base64" && r.content) {
222
- const bytes = base64ToBytes(r.content);
223
- return TEXT_DECODER.decode(bytes);
224
- }
225
- return r.content ?? "";
226
- }
227
- async readFileBytes(path) {
228
- this.ensureInit();
229
- const normalized = normalizePath(path);
230
- const resolved = this.resolveSymlink(normalized);
231
- const r = this.sql`
232
- SELECT type, storage_backend, r2_key, content, content_encoding
233
- FROM __TABLE__ WHERE path = ${resolved}
234
- `[0];
235
- if (!r) return null;
236
- if (r.type !== "file") throw new Error(`EISDIR: ${path} is a directory`);
237
- this._observe("workspace:read", {
238
- path: resolved,
239
- storage: r.storage_backend
240
- });
241
- if (r.storage_backend === "r2" && r.r2_key) {
242
- const r2 = this.getR2();
243
- if (!r2) throw new Error(`File ${path} is stored in R2 but no R2 bucket was provided`);
244
- const obj = await r2.get(r.r2_key);
245
- if (!obj) return new Uint8Array(0);
246
- return new Uint8Array(await obj.arrayBuffer());
247
- }
248
- if (r.content_encoding === "base64" && r.content) return base64ToBytes(r.content);
249
- return TEXT_ENCODER.encode(r.content ?? "");
250
- }
251
- async writeFileBytes(path, data, mimeType = "application/octet-stream") {
252
- this.ensureInit();
253
- const normalized = this.resolveSymlink(normalizePath(path));
254
- if (normalized === "/") throw new Error("EISDIR: cannot write to root directory");
255
- const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
256
- const size = bytes.byteLength;
257
- const parentPath = getParent(normalized);
258
- const name = getBasename(normalized);
259
- const now = Math.floor(Date.now() / 1e3);
260
- this.ensureParentDir(parentPath);
261
- const existing = this.sql`
262
- SELECT storage_backend, r2_key FROM __TABLE__ WHERE path = ${normalized}
263
- `[0];
264
- const r2 = this.getR2();
265
- if (size >= this.threshold && r2) {
266
- const key = this.r2Key(normalized);
267
- if (existing?.storage_backend === "r2" && existing.r2_key !== key) await r2.delete(existing.r2_key);
268
- await r2.put(key, bytes, { httpMetadata: { contentType: mimeType } });
269
- try {
270
- this.sql`
271
- INSERT INTO __TABLE__
272
- (path, parent_path, name, type, mime_type, size,
273
- storage_backend, r2_key, content_encoding, content, created_at, modified_at)
274
- VALUES
275
- (${normalized}, ${parentPath}, ${name}, 'file', ${mimeType}, ${size},
276
- 'r2', ${key}, 'base64', NULL, ${now}, ${now})
277
- ON CONFLICT(path) DO UPDATE SET
278
- mime_type = excluded.mime_type,
279
- size = excluded.size,
280
- storage_backend = 'r2',
281
- r2_key = excluded.r2_key,
282
- content_encoding = 'base64',
283
- content = NULL,
284
- modified_at = excluded.modified_at
285
- `;
286
- } catch (sqlErr) {
287
- try {
288
- await r2.delete(key);
289
- } catch {
290
- console.error(`[Workspace] Failed to clean up orphaned R2 object ${key} after SQL error`);
291
- }
292
- throw sqlErr;
293
- }
294
- this.emit(existing ? "update" : "create", normalized, "file");
295
- this._observe("workspace:write", {
296
- path: normalized,
297
- size,
298
- storage: "r2",
299
- update: !!existing
300
- });
301
- } else {
302
- if (size >= this.threshold && !r2) console.warn(`[Workspace] File ${path} is ${size} bytes but no R2 bucket was provided. Storing inline.`);
303
- if (existing?.storage_backend === "r2" && existing.r2_key && r2) await r2.delete(existing.r2_key);
304
- const b64 = bytesToBase64(bytes);
305
- this.sql`
306
- INSERT INTO __TABLE__
307
- (path, parent_path, name, type, mime_type, size,
308
- storage_backend, r2_key, content_encoding, content, created_at, modified_at)
309
- VALUES
310
- (${normalized}, ${parentPath}, ${name}, 'file', ${mimeType}, ${size},
311
- 'inline', NULL, 'base64', ${b64}, ${now}, ${now})
312
- ON CONFLICT(path) DO UPDATE SET
313
- mime_type = excluded.mime_type,
314
- size = excluded.size,
315
- storage_backend = 'inline',
316
- r2_key = NULL,
317
- content_encoding = 'base64',
318
- content = excluded.content,
319
- modified_at = excluded.modified_at
320
- `;
321
- this.emit(existing ? "update" : "create", normalized, "file");
322
- this._observe("workspace:write", {
323
- path: normalized,
324
- size,
325
- storage: "inline",
326
- update: !!existing
327
- });
328
- }
329
- }
330
- async writeFile(path, content, mimeType = "text/plain") {
331
- this.ensureInit();
332
- const normalized = this.resolveSymlink(normalizePath(path));
333
- if (normalized === "/") throw new Error("EISDIR: cannot write to root directory");
334
- const parentPath = getParent(normalized);
335
- const name = getBasename(normalized);
336
- const bytes = TEXT_ENCODER.encode(content);
337
- const size = bytes.byteLength;
338
- const now = Math.floor(Date.now() / 1e3);
339
- this.ensureParentDir(parentPath);
340
- const existing = this.sql`
341
- SELECT storage_backend, r2_key FROM __TABLE__ WHERE path = ${normalized}
342
- `[0];
343
- const r2 = this.getR2();
344
- if (size >= this.threshold && r2) {
345
- const key = this.r2Key(normalized);
346
- if (existing?.storage_backend === "r2" && existing.r2_key !== key) await r2.delete(existing.r2_key);
347
- await r2.put(key, bytes, { httpMetadata: { contentType: mimeType } });
348
- try {
349
- this.sql`
350
- INSERT INTO __TABLE__
351
- (path, parent_path, name, type, mime_type, size,
352
- storage_backend, r2_key, content_encoding, content, created_at, modified_at)
353
- VALUES
354
- (${normalized}, ${parentPath}, ${name}, 'file', ${mimeType}, ${size},
355
- 'r2', ${key}, 'utf8', NULL, ${now}, ${now})
356
- ON CONFLICT(path) DO UPDATE SET
357
- mime_type = excluded.mime_type,
358
- size = excluded.size,
359
- storage_backend = 'r2',
360
- r2_key = excluded.r2_key,
361
- content_encoding = 'utf8',
362
- content = NULL,
363
- modified_at = excluded.modified_at
364
- `;
365
- } catch (sqlErr) {
366
- try {
367
- await r2.delete(key);
368
- } catch {
369
- console.error(`[Workspace] Failed to clean up orphaned R2 object ${key} after SQL error`);
370
- }
371
- throw sqlErr;
372
- }
373
- this.emit(existing ? "update" : "create", normalized, "file");
374
- this._observe("workspace:write", {
375
- path: normalized,
376
- size,
377
- storage: "r2",
378
- update: !!existing
379
- });
380
- } else {
381
- if (size >= this.threshold && !r2) console.warn(`[Workspace] File ${path} is ${size} bytes but no R2 bucket was provided. Storing inline — this may hit SQLite row limits for very large files.`);
382
- if (existing?.storage_backend === "r2" && existing.r2_key && r2) await r2.delete(existing.r2_key);
383
- this.sql`
384
- INSERT INTO __TABLE__
385
- (path, parent_path, name, type, mime_type, size,
386
- storage_backend, r2_key, content_encoding, content, created_at, modified_at)
387
- VALUES
388
- (${normalized}, ${parentPath}, ${name}, 'file', ${mimeType}, ${size},
389
- 'inline', NULL, 'utf8', ${content}, ${now}, ${now})
390
- ON CONFLICT(path) DO UPDATE SET
391
- mime_type = excluded.mime_type,
392
- size = excluded.size,
393
- storage_backend = 'inline',
394
- r2_key = NULL,
395
- content_encoding = 'utf8',
396
- content = excluded.content,
397
- modified_at = excluded.modified_at
398
- `;
399
- this.emit(existing ? "update" : "create", normalized, "file");
400
- this._observe("workspace:write", {
401
- path: normalized,
402
- size,
403
- storage: "inline",
404
- update: !!existing
405
- });
406
- }
407
- }
408
- async readFileStream(path) {
409
- this.ensureInit();
410
- const normalized = normalizePath(path);
411
- const resolved = this.resolveSymlink(normalized);
412
- const r = this.sql`
413
- SELECT type, storage_backend, r2_key, content, content_encoding
414
- FROM __TABLE__ WHERE path = ${resolved}
415
- `[0];
416
- if (!r) return null;
417
- if (r.type !== "file") throw new Error(`EISDIR: ${path} is a directory`);
418
- this._observe("workspace:read", {
419
- path: resolved,
420
- storage: r.storage_backend
421
- });
422
- if (r.storage_backend === "r2" && r.r2_key) {
423
- const r2 = this.getR2();
424
- if (!r2) throw new Error(`File ${path} is stored in R2 but no R2 bucket was provided`);
425
- const obj = await r2.get(r.r2_key);
426
- if (!obj) return new ReadableStream({ start(c) {
427
- c.close();
428
- } });
429
- return obj.body;
430
- }
431
- const bytes = r.content_encoding === "base64" && r.content ? base64ToBytes(r.content) : TEXT_ENCODER.encode(r.content ?? "");
432
- return new ReadableStream({ start(controller) {
433
- controller.enqueue(bytes);
434
- controller.close();
435
- } });
436
- }
437
- async writeFileStream(path, stream, mimeType = "application/octet-stream") {
438
- const reader = stream.getReader();
439
- const chunks = [];
440
- let totalSize = 0;
441
- for (;;) {
442
- const { done, value } = await reader.read();
443
- if (done) break;
444
- totalSize += value.byteLength;
445
- if (totalSize > MAX_STREAM_SIZE) {
446
- reader.cancel();
447
- throw new Error(`EFBIG: stream exceeds maximum size of ${MAX_STREAM_SIZE} bytes`);
448
- }
449
- chunks.push(value);
450
- }
451
- const buffer = new Uint8Array(totalSize);
452
- let offset = 0;
453
- for (const chunk of chunks) {
454
- buffer.set(chunk, offset);
455
- offset += chunk.byteLength;
456
- }
457
- await this.writeFileBytes(path, buffer, mimeType);
458
- }
459
- async appendFile(path, content, mimeType = "text/plain") {
460
- this.ensureInit();
461
- const normalized = this.resolveSymlink(normalizePath(path));
462
- const row = this.sql`
463
- SELECT type, storage_backend, content_encoding
464
- FROM __TABLE__ WHERE path = ${normalized}
465
- `[0];
466
- if (!row) {
467
- await this.writeFile(path, content, mimeType);
468
- return;
469
- }
470
- if (row.type !== "file") throw new Error(`EISDIR: ${path} is a directory`);
471
- if (row.storage_backend === "inline" && row.content_encoding === "utf8") {
472
- const appendSize = TEXT_ENCODER.encode(content).byteLength;
473
- const now = Math.floor(Date.now() / 1e3);
474
- this.sql`
475
- UPDATE __TABLE__ SET
476
- content = content || ${content},
477
- size = size + ${appendSize},
478
- modified_at = ${now}
479
- WHERE path = ${normalized}
480
- `;
481
- this.emit("update", normalized, "file");
482
- return;
483
- }
484
- const existing = await this.readFile(path);
485
- await this.writeFile(path, (existing ?? "") + content, mimeType);
486
- }
487
- async deleteFile(path) {
488
- this.ensureInit();
489
- const normalized = normalizePath(path);
490
- const rows = this.sql`
491
- SELECT type, storage_backend, r2_key FROM __TABLE__ WHERE path = ${normalized}
492
- `;
493
- if (!rows[0]) return false;
494
- if (rows[0].type === "directory") throw new Error(`EISDIR: ${path} is a directory — use rm() instead`);
495
- if (rows[0].storage_backend === "r2" && rows[0].r2_key) {
496
- const r2 = this.getR2();
497
- if (r2) await r2.delete(rows[0].r2_key);
498
- }
499
- this.sql`DELETE FROM __TABLE__ WHERE path = ${normalized}`;
500
- this.emit("delete", normalized, rows[0].type);
501
- this._observe("workspace:delete", { path: normalized });
502
- return true;
503
- }
504
- fileExists(path) {
505
- this.ensureInit();
506
- const resolved = this.resolveSymlink(normalizePath(path));
507
- const rows = this.sql`
508
- SELECT type FROM __TABLE__ WHERE path = ${resolved}
509
- `;
510
- return rows.length > 0 && rows[0].type === "file";
511
- }
512
- exists(path) {
513
- this.ensureInit();
514
- const normalized = normalizePath(path);
515
- return (this.sql`
516
- SELECT COUNT(*) AS cnt FROM __TABLE__ WHERE path = ${normalized}
517
- `[0]?.cnt ?? 0) > 0;
518
- }
519
- readDir(dir = "/", opts) {
520
- this.ensureInit();
521
- const normalized = normalizePath(dir);
522
- const limit = opts?.limit ?? 1e3;
523
- const offset = opts?.offset ?? 0;
524
- return this.sql`
525
- SELECT path, name, type, mime_type, size, created_at, modified_at
526
- FROM __TABLE__
527
- WHERE parent_path = ${normalized}
528
- ORDER BY type ASC, name ASC
529
- LIMIT ${limit} OFFSET ${offset}
530
- `.map(toFileInfo);
531
- }
532
- glob(pattern) {
533
- this.ensureInit();
534
- const normalized = normalizePath(pattern);
535
- const likePattern = escapeLike(getGlobPrefix(normalized)) + "%";
536
- const regex = globToRegex(normalized);
537
- return this.sql`
538
- SELECT path, name, type, mime_type, size, created_at, modified_at, target
539
- FROM __TABLE__
540
- WHERE path LIKE ${likePattern} ESCAPE ${LIKE_ESCAPE}
541
- ORDER BY path
542
- `.filter((r) => regex.test(r.path)).map(toFileInfo);
543
- }
544
- mkdir(path, opts, _depth = 0) {
545
- this.ensureInit();
546
- if (_depth > MAX_MKDIR_DEPTH) throw new Error(`ELOOP: mkdir recursion too deep (max ${MAX_MKDIR_DEPTH} levels)`);
547
- const normalized = normalizePath(path);
548
- if (normalized === "/") return;
549
- const existing = this.sql`
550
- SELECT type FROM __TABLE__ WHERE path = ${normalized}
551
- `;
552
- if (existing.length > 0) {
553
- if (existing[0].type === "directory" && opts?.recursive) return;
554
- throw new Error(existing[0].type === "directory" ? `EEXIST: directory already exists: ${path}` : `EEXIST: path exists as a file: ${path}`);
555
- }
556
- const parentPath = getParent(normalized);
557
- const parentRows = this.sql`
558
- SELECT type FROM __TABLE__ WHERE path = ${parentPath}
559
- `;
560
- if (!parentRows[0]) if (opts?.recursive) this.mkdir(parentPath, { recursive: true }, _depth + 1);
561
- else throw new Error(`ENOENT: parent directory not found: ${parentPath}`);
562
- else if (parentRows[0].type !== "directory") throw new Error(`ENOTDIR: parent is not a directory: ${parentPath}`);
563
- const name = getBasename(normalized);
564
- const now = Math.floor(Date.now() / 1e3);
565
- this.sql`
566
- INSERT INTO __TABLE__
567
- (path, parent_path, name, type, size, created_at, modified_at)
568
- VALUES (${normalized}, ${parentPath}, ${name}, 'directory', 0, ${now}, ${now})
569
- `;
570
- this.emit("create", normalized, "directory");
571
- this._observe("workspace:mkdir", {
572
- path: normalized,
573
- recursive: !!opts?.recursive
574
- });
575
- }
576
- async rm(path, opts) {
577
- this.ensureInit();
578
- const normalized = normalizePath(path);
579
- if (normalized === "/") throw new Error("EPERM: cannot remove root directory");
580
- const rows = this.sql`
581
- SELECT type FROM __TABLE__ WHERE path = ${normalized}
582
- `;
583
- if (!rows[0]) {
584
- if (opts?.force) return;
585
- throw new Error(`ENOENT: no such file or directory: ${path}`);
586
- }
587
- if (rows[0].type === "directory") {
588
- if ((this.sql`
589
- SELECT COUNT(*) AS cnt FROM __TABLE__ WHERE parent_path = ${normalized}
590
- `[0]?.cnt ?? 0) > 0) {
591
- if (!opts?.recursive) throw new Error(`ENOTEMPTY: directory not empty: ${path}`);
592
- await this.deleteDescendants(normalized);
593
- }
594
- } else {
595
- const fileRow = this.sql`
596
- SELECT storage_backend, r2_key FROM __TABLE__ WHERE path = ${normalized}
597
- `[0];
598
- if (fileRow?.storage_backend === "r2" && fileRow.r2_key) {
599
- const r2 = this.getR2();
600
- if (r2) await r2.delete(fileRow.r2_key);
601
- }
602
- }
603
- this.sql`DELETE FROM __TABLE__ WHERE path = ${normalized}`;
604
- this.emit("delete", normalized, rows[0].type);
605
- this._observe("workspace:rm", {
606
- path: normalized,
607
- recursive: !!opts?.recursive
608
- });
609
- }
610
- async cp(src, dest, opts) {
611
- this.ensureInit();
612
- const srcNorm = normalizePath(src);
613
- const destNorm = normalizePath(dest);
614
- const srcStat = this.lstat(srcNorm);
615
- if (!srcStat) throw new Error(`ENOENT: no such file or directory: ${src}`);
616
- if (srcStat.type === "symlink") {
617
- const target = this.readlink(srcNorm);
618
- this.symlink(target, destNorm);
619
- return;
620
- }
621
- if (srcStat.type === "directory") {
622
- if (!opts?.recursive) throw new Error(`EISDIR: cannot copy directory without recursive: ${src}`);
623
- this.mkdir(destNorm, { recursive: true });
624
- for (const child of this.readDir(srcNorm)) await this.cp(child.path, `${destNorm}/${child.name}`, opts);
625
- return;
626
- }
627
- const bytes = await this.readFileBytes(srcNorm);
628
- if (bytes) await this.writeFileBytes(destNorm, bytes, srcStat.mimeType);
629
- else await this.writeFile(destNorm, "", srcStat.mimeType);
630
- this._observe("workspace:cp", {
631
- src: srcNorm,
632
- dest: destNorm,
633
- recursive: !!opts?.recursive
634
- });
635
- }
636
- async mv(src, dest, opts) {
637
- this.ensureInit();
638
- const srcNorm = normalizePath(src);
639
- const destNorm = normalizePath(dest);
640
- const srcStat = this.lstat(srcNorm);
641
- if (!srcStat) throw new Error(`ENOENT: no such file or directory: ${src}`);
642
- if (srcStat.type === "directory") {
643
- if (!(opts?.recursive ?? true)) throw new Error(`EISDIR: cannot move directory without recursive: ${src}`);
644
- await this.cp(src, dest, { recursive: true });
645
- await this.rm(src, {
646
- recursive: true,
647
- force: true
648
- });
649
- return;
650
- }
651
- const destParent = getParent(destNorm);
652
- const destName = getBasename(destNorm);
653
- this.ensureParentDir(destParent);
654
- const existingDest = this.sql`
655
- SELECT type FROM __TABLE__ WHERE path = ${destNorm}
656
- `[0];
657
- if (existingDest) {
658
- if (existingDest.type === "directory") throw new Error(`EISDIR: cannot overwrite directory: ${dest}`);
659
- await this.deleteFile(destNorm);
660
- }
661
- if (srcStat.type === "file") {
662
- const row = this.sql`
663
- SELECT storage_backend, r2_key FROM __TABLE__ WHERE path = ${srcNorm}
664
- `[0];
665
- if (row?.storage_backend === "r2" && row.r2_key) {
666
- const r2 = this.getR2();
667
- if (r2) {
668
- const newKey = this.r2Key(destNorm);
669
- const obj = await r2.get(row.r2_key);
670
- if (obj) await r2.put(newKey, await obj.arrayBuffer(), { httpMetadata: obj.httpMetadata });
671
- await r2.delete(row.r2_key);
672
- const now = Math.floor(Date.now() / 1e3);
673
- this.sql`
674
- UPDATE __TABLE__ SET
675
- path = ${destNorm},
676
- parent_path = ${destParent},
677
- name = ${destName},
678
- r2_key = ${newKey},
679
- modified_at = ${now}
680
- WHERE path = ${srcNorm}
681
- `;
682
- this.emit("delete", srcNorm, "file");
683
- this.emit("create", destNorm, "file");
684
- this._observe("workspace:mv", {
685
- src: srcNorm,
686
- dest: destNorm
687
- });
688
- return;
689
- }
690
- }
691
- }
692
- const now = Math.floor(Date.now() / 1e3);
693
- this.sql`
694
- UPDATE __TABLE__ SET
695
- path = ${destNorm},
696
- parent_path = ${destParent},
697
- name = ${destName},
698
- modified_at = ${now}
699
- WHERE path = ${srcNorm}
700
- `;
701
- this.emit("delete", srcNorm, srcStat.type);
702
- this.emit("create", destNorm, srcStat.type);
703
- this._observe("workspace:mv", {
704
- src: srcNorm,
705
- dest: destNorm
706
- });
707
- }
708
- async diff(pathA, pathB) {
709
- const contentA = await this.readFile(pathA);
710
- if (contentA === null) throw new Error(`ENOENT: no such file: ${pathA}`);
711
- const contentB = await this.readFile(pathB);
712
- if (contentB === null) throw new Error(`ENOENT: no such file: ${pathB}`);
713
- const linesA = contentA.split("\n").length;
714
- const linesB = contentB.split("\n").length;
715
- if (linesA > MAX_DIFF_LINES || linesB > MAX_DIFF_LINES) throw new Error(`EFBIG: files too large for diff (max ${MAX_DIFF_LINES} lines)`);
716
- return unifiedDiff(contentA, contentB, normalizePath(pathA), normalizePath(pathB));
717
- }
718
- async diffContent(path, newContent) {
719
- const existing = await this.readFile(path);
720
- if (existing === null) throw new Error(`ENOENT: no such file: ${path}`);
721
- const linesA = existing.split("\n").length;
722
- const linesB = newContent.split("\n").length;
723
- if (linesA > MAX_DIFF_LINES || linesB > MAX_DIFF_LINES) throw new Error(`EFBIG: content too large for diff (max ${MAX_DIFF_LINES} lines)`);
724
- const normalized = normalizePath(path);
725
- return unifiedDiff(existing, newContent, normalized, normalized);
726
- }
727
- _resolveBashConfig(options) {
728
- const commands = options?.commands ? [...this.commands, ...options.commands] : this.commands.length > 0 ? this.commands : void 0;
729
- const hasWsEnv = Object.keys(this.env).length > 0;
730
- return {
731
- commands,
732
- env: options?.env && hasWsEnv ? {
733
- ...this.env,
734
- ...options.env
735
- } : options?.env ?? (hasWsEnv ? this.env : void 0),
736
- network: options?.network ?? this.network
737
- };
738
- }
739
- async bash(command, options) {
740
- this.ensureInit();
741
- const { commands, env, network } = this._resolveBashConfig(options);
742
- const bashInstance = new Shell({
743
- fs: new WorkspaceFileSystem(this),
744
- cwd: options?.cwd ?? "/",
745
- executionLimits: this.bashLimits,
746
- customCommands: commands,
747
- env,
748
- network
749
- });
750
- const t0 = Date.now();
751
- const result = await bashInstance.exec(command);
752
- this._observe("workspace:bash", {
753
- command,
754
- exitCode: result.exitCode,
755
- durationMs: Date.now() - t0
756
- });
757
- return {
758
- stdout: result.stdout,
759
- stderr: result.stderr,
760
- exitCode: result.exitCode
761
- };
762
- }
763
- createBashSession(options) {
764
- this.ensureInit();
765
- const { commands, env, network } = this._resolveBashConfig(options);
766
- return new BashSession({
767
- ws: this,
768
- fs: new WorkspaceFileSystem(this),
769
- bashLimits: this.bashLimits,
770
- commands,
771
- env: env ? { ...env } : {},
772
- network,
773
- cwd: options?.cwd ?? "/",
774
- observe: this._observe.bind(this)
775
- });
776
- }
777
- getWorkspaceInfo() {
778
- this.ensureInit();
779
- const rows = this.sql`
780
- SELECT
781
- SUM(CASE WHEN type = 'file' THEN 1 ELSE 0 END) AS files,
782
- SUM(CASE WHEN type = 'directory' THEN 1 ELSE 0 END) AS dirs,
783
- COALESCE(SUM(CASE WHEN type = 'file' THEN size ELSE 0 END), 0) AS total,
784
- SUM(CASE WHEN type = 'file' AND storage_backend = 'r2' THEN 1 ELSE 0 END) AS r2files
785
- FROM __TABLE__
786
- `;
787
- return {
788
- fileCount: rows[0]?.files ?? 0,
789
- directoryCount: rows[0]?.dirs ?? 0,
790
- totalBytes: rows[0]?.total ?? 0,
791
- r2FileCount: rows[0]?.r2files ?? 0
792
- };
793
- }
794
- /** @internal */
795
- _getAllPaths() {
796
- this.ensureInit();
797
- return this.sql`
798
- SELECT path FROM __TABLE__ ORDER BY path
799
- `.map((r) => r.path);
800
- }
801
- /** @internal */
802
- _updateModifiedAt(path, mtime) {
803
- this.ensureInit();
804
- const normalized = normalizePath(path);
805
- const ts = Math.floor(mtime.getTime() / 1e3);
806
- this.sql`
807
- UPDATE __TABLE__ SET modified_at = ${ts} WHERE path = ${normalized}
808
- `;
809
- }
810
- ensureParentDir(dirPath) {
811
- if (!dirPath || dirPath === "/") return;
812
- const rows = this.sql`
813
- SELECT type FROM __TABLE__ WHERE path = ${dirPath}
814
- `;
815
- if (rows[0]) {
816
- if (rows[0].type !== "directory") throw new Error(`ENOTDIR: ${dirPath} is not a directory`);
817
- return;
818
- }
819
- const missing = [dirPath];
820
- let current = getParent(dirPath);
821
- while (current && current !== "/") {
822
- const r = this.sql`
823
- SELECT type FROM __TABLE__ WHERE path = ${current}
824
- `;
825
- if (r[0]) {
826
- if (r[0].type !== "directory") throw new Error(`ENOTDIR: ${current} is not a directory`);
827
- break;
828
- }
829
- missing.push(current);
830
- current = getParent(current);
831
- }
832
- const now = Math.floor(Date.now() / 1e3);
833
- for (let i = missing.length - 1; i >= 0; i--) {
834
- const p = missing[i];
835
- const parentPath = getParent(p);
836
- const name = getBasename(p);
837
- this.sql`
838
- INSERT INTO __TABLE__
839
- (path, parent_path, name, type, size, created_at, modified_at)
840
- VALUES (${p}, ${parentPath}, ${name}, 'directory', 0, ${now}, ${now})
841
- `;
842
- this.emit("create", p, "directory");
843
- }
844
- }
845
- async deleteDescendants(dirPath) {
846
- const pattern = escapeLike(dirPath) + "/%";
847
- const r2Rows = this.sql`
848
- SELECT r2_key FROM __TABLE__
849
- WHERE path LIKE ${pattern} ESCAPE ${LIKE_ESCAPE}
850
- AND storage_backend = 'r2'
851
- AND r2_key IS NOT NULL
852
- `;
853
- if (r2Rows.length > 0) {
854
- const r2 = this.getR2();
855
- if (r2) {
856
- const keys = r2Rows.map((r) => r.r2_key);
857
- await r2.delete(keys);
858
- }
859
- }
860
- this.sql`DELETE FROM __TABLE__ WHERE path LIKE ${pattern} ESCAPE ${LIKE_ESCAPE}`;
861
- }
862
- };
863
- _Symbol$dispose = Symbol.dispose;
864
- var BashSession = class {
865
- /** @internal — use workspace.createBashSession() instead */
866
- constructor(init) {
867
- this._closed = false;
868
- this._ws = init.ws;
869
- this._fs = init.fs;
870
- this._bashLimits = init.bashLimits;
871
- this._customCommands = init.commands;
872
- this._networkConfig = init.network;
873
- this._observe = init.observe;
874
- this._currentCwd = init.cwd;
875
- this._currentEnv = init.env;
876
- }
877
- async exec(command) {
878
- if (this._closed) throw new Error("BashSession is closed");
879
- const bash = new Shell({
880
- fs: this._fs,
881
- cwd: this._currentCwd,
882
- env: Object.keys(this._currentEnv).length > 0 ? this._currentEnv : void 0,
883
- executionLimits: this._bashLimits,
884
- customCommands: this._customCommands,
885
- network: this._networkConfig
886
- });
887
- const wrapped = `${command}\n__sess_rc=$?\necho "${SESS_STATE_BEGIN}"\necho "${SESS_CWD_PREFIX}$(pwd)"\nenv\necho "${SESS_STATE_END}"\nexit $__sess_rc`;
888
- const t0 = Date.now();
889
- const result = await bash.exec(wrapped);
890
- let stdout = result.stdout;
891
- const beginIdx = stdout.lastIndexOf(SESS_STATE_BEGIN);
892
- const endIdx = stdout.lastIndexOf(SESS_STATE_END);
893
- if (beginIdx >= 0 && endIdx > beginIdx) {
894
- const lines = stdout.slice(beginIdx + 27 + 1, endIdx).split("\n");
895
- const newEnv = {};
896
- for (const line of lines) if (line.startsWith(SESS_CWD_PREFIX)) {
897
- const cwd = line.slice(13).trim();
898
- if (cwd) this._currentCwd = cwd;
899
- } else if (line.includes("=")) {
900
- const eqIdx = line.indexOf("=");
901
- const key = line.slice(0, eqIdx);
902
- const value = line.slice(eqIdx + 1);
903
- if (key && key !== "__sess_rc") newEnv[key] = value;
904
- }
905
- if (Object.keys(newEnv).length > 0) this._currentEnv = newEnv;
906
- let cutStart = beginIdx;
907
- if (cutStart > 0 && stdout[cutStart - 1] === "\n") cutStart--;
908
- stdout = stdout.slice(0, cutStart);
909
- }
910
- this._observe("workspace:bash", {
911
- command,
912
- exitCode: result.exitCode,
913
- durationMs: Date.now() - t0,
914
- session: true
915
- });
916
- return {
917
- stdout,
918
- stderr: result.stderr,
919
- exitCode: result.exitCode
920
- };
921
- }
922
- get cwd() {
923
- return this._currentCwd;
924
- }
925
- get env() {
926
- return { ...this._currentEnv };
927
- }
928
- get isClosed() {
929
- return this._closed;
930
- }
931
- close() {
932
- this._closed = true;
933
- }
934
- [_Symbol$dispose]() {
935
- this.close();
936
- }
937
- };
938
- function fileContentToString(content) {
939
- return typeof content === "string" ? content : TEXT_DECODER.decode(content);
940
- }
941
- var WorkspaceFileSystem = class {
942
- constructor(ws) {
943
- this.ws = ws;
944
- }
945
- async readFile(path, _options) {
946
- const content = await this.ws.readFile(path);
947
- if (content === null) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: ${path}`), { code: "ENOENT" });
948
- return content;
949
- }
950
- async readFileBuffer(path) {
951
- const bytes = await this.ws.readFileBytes(path);
952
- if (bytes === null) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: ${path}`), { code: "ENOENT" });
953
- return bytes;
954
- }
955
- async writeFile(path, content, _options) {
956
- if (typeof content === "string") await this.ws.writeFile(path, content);
957
- else await this.ws.writeFileBytes(path, content);
958
- }
959
- async appendFile(path, content, _options) {
960
- await this.ws.appendFile(path, fileContentToString(content));
961
- }
962
- async exists(path) {
963
- return this.ws.stat(path) !== null;
964
- }
965
- async stat(path) {
966
- const s = this.ws.stat(path);
967
- if (!s) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: ${path}`), { code: "ENOENT" });
968
- return {
969
- isFile: s.type === "file",
970
- isDirectory: s.type === "directory",
971
- isSymbolicLink: false,
972
- mode: s.type === "directory" ? 493 : 420,
973
- size: s.size,
974
- mtime: new Date(s.updatedAt)
975
- };
976
- }
977
- async lstat(path) {
978
- const s = this.ws.lstat(path);
979
- if (!s) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: ${path}`), { code: "ENOENT" });
980
- return {
981
- isFile: s.type === "file",
982
- isDirectory: s.type === "directory",
983
- isSymbolicLink: s.type === "symlink",
984
- mode: s.type === "directory" ? 493 : 420,
985
- size: s.size,
986
- mtime: new Date(s.updatedAt)
987
- };
988
- }
989
- async mkdir(path, options) {
990
- this.ws.mkdir(path, options);
991
- }
992
- async readdir(path) {
993
- return this.ws.readDir(path).map((e) => e.name);
994
- }
995
- async readdirWithFileTypes(path) {
996
- return this.ws.readDir(path).map((e) => ({
997
- name: e.name,
998
- isFile: e.type === "file",
999
- isDirectory: e.type === "directory",
1000
- isSymbolicLink: e.type === "symlink"
1001
- }));
1002
- }
1003
- async rm(path, options) {
1004
- await this.ws.rm(path, options);
1005
- }
1006
- async cp(src, dest, options) {
1007
- await this.ws.cp(src, dest, options);
1008
- }
1009
- async mv(src, dest) {
1010
- await this.ws.mv(src, dest);
1011
- }
1012
- resolvePath(base, path) {
1013
- const parts = (path.startsWith("/") ? path : `${base}/${path}`).split("/").filter(Boolean);
1014
- const resolved = [];
1015
- for (const part of parts) if (part === "..") resolved.pop();
1016
- else if (part !== ".") resolved.push(part);
1017
- return "/" + resolved.join("/");
1018
- }
1019
- getAllPaths() {
1020
- return this.ws._getAllPaths();
1021
- }
1022
- async chmod(_path, _mode) {}
1023
- async symlink(target, linkPath) {
1024
- this.ws.symlink(target, linkPath);
1025
- }
1026
- async link(_existingPath, _newPath) {
1027
- throw new Error("ENOSYS: hard links not supported in workspace filesystem");
1028
- }
1029
- async readlink(path) {
1030
- return this.ws.readlink(path);
1031
- }
1032
- async realpath(path) {
1033
- const normalized = normalizePath(path);
1034
- const s = this.ws.lstat(normalized);
1035
- if (!s) throw Object.assign(/* @__PURE__ */ new Error(`ENOENT: ${path}`), { code: "ENOENT" });
1036
- if (s.type === "symlink") {
1037
- const target = this.ws.readlink(normalized);
1038
- const resolved = target.startsWith("/") ? normalizePath(target) : normalizePath(getParent(normalized) + "/" + target);
1039
- return this.realpath(resolved);
1040
- }
1041
- return normalized;
1042
- }
1043
- async utimes(_path, _atime, mtime) {
1044
- this.ws._updateModifiedAt(_path, mtime);
1045
- }
1046
- };
1047
- function bytesToBase64(bytes) {
1048
- const CHUNK = 8192;
1049
- let binary = "";
1050
- for (let i = 0; i < bytes.byteLength; i += CHUNK) binary += String.fromCharCode(...bytes.subarray(i, Math.min(i + CHUNK, bytes.byteLength)));
1051
- return btoa(binary);
1052
- }
1053
- function base64ToBytes(b64) {
1054
- const binary = atob(b64);
1055
- const bytes = new Uint8Array(binary.length);
1056
- for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1057
- return bytes;
1058
- }
1059
- function escapeLike(s) {
1060
- return s.replace(/[\\%_]/g, (ch) => "\\" + ch);
1061
- }
1062
- function normalizePath(path) {
1063
- if (!path.startsWith("/")) path = "/" + path;
1064
- const parts = path.split("/");
1065
- const resolved = [];
1066
- for (const part of parts) {
1067
- if (part === "" || part === ".") continue;
1068
- if (part === "..") resolved.pop();
1069
- else resolved.push(part);
1070
- }
1071
- const result = "/" + resolved.join("/");
1072
- if (result.length > MAX_PATH_LENGTH) throw new Error(`ENAMETOOLONG: path exceeds ${MAX_PATH_LENGTH} characters`);
1073
- return result;
1074
- }
1075
- function getParent(path) {
1076
- const normalized = normalizePath(path);
1077
- if (normalized === "/") return "";
1078
- const lastSlash = normalized.lastIndexOf("/");
1079
- return lastSlash === 0 ? "/" : normalized.slice(0, lastSlash);
1080
- }
1081
- function getBasename(path) {
1082
- const normalized = normalizePath(path);
1083
- if (normalized === "/") return "";
1084
- return normalized.slice(normalized.lastIndexOf("/") + 1);
1085
- }
1086
- function toFileInfo(r) {
1087
- const info = {
1088
- path: r.path,
1089
- name: r.name,
1090
- type: r.type,
1091
- mimeType: r.mime_type,
1092
- size: r.size,
1093
- createdAt: r.created_at * 1e3,
1094
- updatedAt: r.modified_at * 1e3
1095
- };
1096
- if (r.target) info.target = r.target;
1097
- return info;
1098
- }
1099
- function getGlobPrefix(pattern) {
1100
- const first = pattern.search(/[*?[{]/);
1101
- if (first === -1) return pattern;
1102
- const before = pattern.slice(0, first);
1103
- const lastSlash = before.lastIndexOf("/");
1104
- return lastSlash >= 0 ? before.slice(0, lastSlash + 1) : "/";
1105
- }
1106
- function globToRegex(pattern) {
1107
- let i = 0;
1108
- let re = "^";
1109
- while (i < pattern.length) {
1110
- const ch = pattern[i];
1111
- if (ch === "*") if (pattern[i + 1] === "*") {
1112
- i += 2;
1113
- if (pattern[i] === "/") {
1114
- re += "(?:.+/)?";
1115
- i++;
1116
- } else re += ".*";
1117
- } else {
1118
- re += "[^/]*";
1119
- i++;
1120
- }
1121
- else if (ch === "?") {
1122
- re += "[^/]";
1123
- i++;
1124
- } else if (ch === "[") {
1125
- const close = pattern.indexOf("]", i + 1);
1126
- if (close === -1) {
1127
- re += "\\[";
1128
- i++;
1129
- } else {
1130
- re += pattern.slice(i, close + 1);
1131
- i = close + 1;
1132
- }
1133
- } else if (ch === "{") {
1134
- const close = pattern.indexOf("}", i + 1);
1135
- if (close === -1) {
1136
- re += "\\{";
1137
- i++;
1138
- } else {
1139
- const inner = pattern.slice(i + 1, close).split(",").join("|");
1140
- re += `(?:${inner})`;
1141
- i = close + 1;
1142
- }
1143
- } else {
1144
- re += ch.replace(/[.+^$|\\()]/g, "\\$&");
1145
- i++;
1146
- }
1147
- }
1148
- re += "$";
1149
- return new RegExp(re);
1150
- }
1151
- function unifiedDiff(a, b, labelA, labelB, contextLines = 3) {
1152
- if (a === b) return "";
1153
- const linesA = a.split("\n");
1154
- const linesB = b.split("\n");
1155
- return formatUnified(myersDiff(linesA, linesB), linesA, linesB, labelA, labelB, contextLines);
1156
- }
1157
- function myersDiff(a, b) {
1158
- const n = a.length;
1159
- const m = b.length;
1160
- const max = n + m;
1161
- const vSize = 2 * max + 1;
1162
- const v = new Int32Array(vSize);
1163
- v.fill(-1);
1164
- const offset = max;
1165
- v[offset + 1] = 0;
1166
- const trace = [];
1167
- outer: for (let d = 0; d <= max; d++) {
1168
- trace.push(v.slice());
1169
- for (let k = -d; k <= d; k += 2) {
1170
- let x;
1171
- if (k === -d || k !== d && v[offset + k - 1] < v[offset + k + 1]) x = v[offset + k + 1];
1172
- else x = v[offset + k - 1] + 1;
1173
- let y = x - k;
1174
- while (x < n && y < m && a[x] === b[y]) {
1175
- x++;
1176
- y++;
1177
- }
1178
- v[offset + k] = x;
1179
- if (x >= n && y >= m) break outer;
1180
- }
1181
- }
1182
- const edits = [];
1183
- let x = n;
1184
- let y = m;
1185
- for (let d = trace.length - 1; d >= 0; d--) {
1186
- const vPrev = trace[d];
1187
- const k = x - y;
1188
- let prevK;
1189
- if (k === -d || k !== d && vPrev[offset + k - 1] < vPrev[offset + k + 1]) prevK = k + 1;
1190
- else prevK = k - 1;
1191
- const prevX = vPrev[offset + prevK];
1192
- const prevY = prevX - prevK;
1193
- while (x > prevX && y > prevY) {
1194
- x--;
1195
- y--;
1196
- edits.push({
1197
- type: "keep",
1198
- lineA: x,
1199
- lineB: y
1200
- });
1201
- }
1202
- if (d > 0) if (x === prevX) {
1203
- edits.push({
1204
- type: "insert",
1205
- lineA: x,
1206
- lineB: y - 1
1207
- });
1208
- y--;
1209
- } else {
1210
- edits.push({
1211
- type: "delete",
1212
- lineA: x - 1,
1213
- lineB: y
1214
- });
1215
- x--;
1216
- }
1217
- }
1218
- edits.reverse();
1219
- return edits;
1220
- }
1221
- function formatUnified(edits, linesA, linesB, labelA, labelB, ctx) {
1222
- const out = [];
1223
- out.push(`--- ${labelA}`);
1224
- out.push(`+++ ${labelB}`);
1225
- const changes = [];
1226
- for (let i = 0; i < edits.length; i++) if (edits[i].type !== "keep") changes.push(i);
1227
- if (changes.length === 0) return "";
1228
- let i = 0;
1229
- while (i < changes.length) {
1230
- let start = Math.max(0, changes[i] - ctx);
1231
- let end = Math.min(edits.length - 1, changes[i] + ctx);
1232
- let j = i + 1;
1233
- while (j < changes.length && changes[j] - ctx <= end + 1) {
1234
- end = Math.min(edits.length - 1, changes[j] + ctx);
1235
- j++;
1236
- }
1237
- let startA = edits[start].lineA;
1238
- let startB = edits[start].lineB;
1239
- let countA = 0;
1240
- let countB = 0;
1241
- const hunkLines = [];
1242
- for (let idx = start; idx <= end; idx++) {
1243
- const e = edits[idx];
1244
- if (e.type === "keep") {
1245
- hunkLines.push(` ${linesA[e.lineA]}`);
1246
- countA++;
1247
- countB++;
1248
- } else if (e.type === "delete") {
1249
- hunkLines.push(`-${linesA[e.lineA]}`);
1250
- countA++;
1251
- } else {
1252
- hunkLines.push(`+${linesB[e.lineB]}`);
1253
- countB++;
1254
- }
1255
- }
1256
- out.push(`@@ -${startA + 1},${countA} +${startB + 1},${countB} @@`);
1257
- out.push(...hunkLines);
1258
- i = j;
1259
- }
1260
- return out.join("\n");
1261
- }
1262
- //#endregion
1263
- export { BashSession, Workspace, defineCommand };
1264
-
1265
- //# sourceMappingURL=workspace.js.map