@ariobarin/glossa 0.1.1 → 0.2.0-beta.2

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 (2) hide show
  1. package/dist/app.js +938 -365
  2. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -39921,6 +39921,9 @@ var MAX_TEXT_BYTES = 1024 * 1024;
39921
39921
  var MAX_EDIT_DIFF_BYTES = 128 * 1024;
39922
39922
  var MAX_EDIT_OPERATIONS = 100;
39923
39923
  var MAX_COMMAND_OUTPUT_BYTES = 12 * 1024;
39924
+ var MAX_COMMAND_RETAINED_STREAM_BYTES = 1024 * 1024;
39925
+ var DEFAULT_COMMAND_OUTPUT_RANGE_BYTES = 32 * 1024;
39926
+ var MAX_COMMAND_OUTPUT_RANGE_BYTES = 64 * 1024;
39924
39927
  var DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60 * 1e3;
39925
39928
  var MAX_COMMAND_TIMEOUT_MS = 60 * 60 * 1e3;
39926
39929
  var DEFAULT_COMMAND_FAST_WAIT_MS = 750;
@@ -39976,12 +39979,16 @@ var listFilesJobSchema = listFilesRequestSchema.extend({
39976
39979
  requestId: external_exports.string().uuid(),
39977
39980
  timeoutMs: structuredReadTimeoutSchema
39978
39981
  });
39982
+ var searchGlobSchema = external_exports.string().min(1).max(256).refine((value) => !/[\r\n\u0000]/.test(value), "Search glob must fit on one line").describe("Root-relative glob pattern using forward slashes, for example src/**/*.ts.");
39979
39983
  var searchTextRequestSchema = external_exports.object({
39980
- query: external_exports.string().min(1).max(256).refine((value) => !/[\r\n\u0000]/.test(value), "Search text must fit on one line").describe("Literal single-line UTF-8 text to search for."),
39984
+ query: external_exports.string().min(1).max(256).refine((value) => !/[\r\n\u0000]/.test(value), "Search text must fit on one line").describe("Single-line UTF-8 search expression. Interpreted literally by default or as a JavaScript regular expression when matchMode is regex."),
39981
39985
  path: relativePathSchema.optional().describe("File or directory relative to the exposed root. Defaults to the root."),
39986
+ matchMode: external_exports.enum(["literal", "regex"]).optional().describe("How to interpret query. Defaults to literal; regex uses JavaScript regular-expression syntax."),
39982
39987
  caseSensitive: external_exports.boolean().optional().describe("Whether matching is case-sensitive. Defaults to false."),
39983
39988
  maxResults: external_exports.number().int().min(1).max(MAX_SEARCH_TEXT_RESULTS).optional().describe("Maximum matching lines to return, from 1 through 100. Defaults to 50."),
39984
- extensions: external_exports.array(external_exports.string().regex(/^\.[A-Za-z0-9][A-Za-z0-9._-]{0,19}$/).describe("Filename suffix including the leading dot, such as .ts or .d.ts.")).min(1).max(20).optional().describe("Optional filename extensions to search.")
39989
+ extensions: external_exports.array(external_exports.string().regex(/^\.[A-Za-z0-9][A-Za-z0-9._-]{0,19}$/).describe("Filename suffix including the leading dot, such as .ts or .d.ts.")).min(1).max(20).optional().describe("Optional filename extensions to search."),
39990
+ includeGlobs: external_exports.array(searchGlobSchema).min(1).max(20).optional().describe("Optional root-relative glob patterns. A file must match at least one include pattern before its contents are scanned."),
39991
+ excludeGlobs: external_exports.array(searchGlobSchema).min(1).max(20).optional().describe("Optional root-relative glob patterns. Matching files are skipped before their contents are scanned.")
39985
39992
  }).strict();
39986
39993
  var searchTextJobSchema = searchTextRequestSchema.extend({
39987
39994
  type: external_exports.literal("search_text"),
@@ -40000,8 +40007,8 @@ var readFileRangeJobSchema = readFileRangeRequestSchema.extend({
40000
40007
  });
40001
40008
  var writeFileRequestSchema = external_exports.object({
40002
40009
  path: relativePathSchema,
40003
- content: boundedTextSchema.describe("Complete UTF-8 text content that will replace the file."),
40004
- expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional().describe("Full-file SHA-256 returned by read_file or read_file_range. When provided, the write fails if the file changed.")
40010
+ content: boundedTextSchema.describe("Complete UTF-8 text content for the new file or replacement revision."),
40011
+ expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional().describe("Full-file SHA-256 returned by read_file or read_file_range. Omit only when creating a new path; when provided, write_file replaces exactly that existing revision and fails if it is missing or changed.")
40005
40012
  }).strict();
40006
40013
  var writeFileJobSchema = writeFileRequestSchema.extend({
40007
40014
  type: external_exports.literal("write_file"),
@@ -40029,6 +40036,30 @@ var editFileJobSchema = editFileRequestSchema.safeExtend({
40029
40036
  type: external_exports.literal("edit_file"),
40030
40037
  requestId: external_exports.string().uuid()
40031
40038
  });
40039
+ var makeDirectoryRequestSchema = external_exports.object({
40040
+ path: relativePathSchema,
40041
+ recursive: external_exports.boolean().optional().describe("Whether to create missing parent directories. Defaults to false.")
40042
+ }).strict();
40043
+ var makeDirectoryJobSchema = makeDirectoryRequestSchema.extend({
40044
+ type: external_exports.literal("make_directory"),
40045
+ requestId: external_exports.string().uuid()
40046
+ });
40047
+ var deletePathRequestSchema = external_exports.object({
40048
+ path: relativePathSchema,
40049
+ recursive: external_exports.boolean().optional().describe("Whether to delete a non-empty directory tree. Defaults to false.")
40050
+ }).strict();
40051
+ var deletePathJobSchema = deletePathRequestSchema.extend({
40052
+ type: external_exports.literal("delete_path"),
40053
+ requestId: external_exports.string().uuid()
40054
+ });
40055
+ var movePathRequestSchema = external_exports.object({
40056
+ source: relativePathSchema.describe("Existing file or directory to move."),
40057
+ destination: relativePathSchema.describe("New path inside the exposed root. The destination must not already exist.")
40058
+ }).strict();
40059
+ var movePathJobSchema = movePathRequestSchema.extend({
40060
+ type: external_exports.literal("move_path"),
40061
+ requestId: external_exports.string().uuid()
40062
+ });
40032
40063
  function requireOneCommand(value, context) {
40033
40064
  if ((value.argv ? 1 : 0) + (value.shellCommand ? 1 : 0) !== 1) {
40034
40065
  context.addIssue({
@@ -40058,6 +40089,16 @@ var getCommandJobSchema = getCommandRequestSchema.extend({
40058
40089
  type: external_exports.literal("get_command"),
40059
40090
  requestId: external_exports.string().uuid()
40060
40091
  });
40092
+ var readCommandOutputRequestSchema = external_exports.object({
40093
+ commandId: external_exports.string().uuid().describe("Command identifier returned by run_command."),
40094
+ stream: external_exports.enum(["stdout", "stderr"]).describe("Command output stream to read independently."),
40095
+ offset: external_exports.number().int().min(0).optional().describe("Zero-based retained byte offset. Defaults to 0."),
40096
+ maxBytes: external_exports.number().int().min(4).max(MAX_COMMAND_OUTPUT_RANGE_BYTES).optional().describe("Maximum retained source bytes to inspect, from 4 through 65536. Defaults to 32768.")
40097
+ }).strict();
40098
+ var readCommandOutputJobSchema = readCommandOutputRequestSchema.extend({
40099
+ type: external_exports.literal("read_command_output"),
40100
+ requestId: external_exports.string().uuid()
40101
+ });
40061
40102
  var cancelCommandRequestSchema = external_exports.object({
40062
40103
  commandId: external_exports.string().uuid().describe("Command identifier returned by run_command.")
40063
40104
  }).strict();
@@ -40072,8 +40113,12 @@ var workerJobSchema = external_exports.discriminatedUnion("type", [
40072
40113
  readFileRangeJobSchema,
40073
40114
  writeFileJobSchema,
40074
40115
  editFileJobSchema,
40116
+ makeDirectoryJobSchema,
40117
+ deletePathJobSchema,
40118
+ movePathJobSchema,
40075
40119
  runCommandJobSchema,
40076
40120
  getCommandJobSchema,
40121
+ readCommandOutputJobSchema,
40077
40122
  cancelCommandJobSchema
40078
40123
  ]);
40079
40124
  var workerResultSchema = external_exports.object({
@@ -49820,6 +49865,8 @@ var WorkerError = class extends Error {
49820
49865
  var STREAM_HEAD_BYTES = Math.floor(MAX_COMMAND_OUTPUT_BYTES / 3);
49821
49866
  var STREAM_TAIL_BYTES = MAX_COMMAND_OUTPUT_BYTES - STREAM_HEAD_BYTES;
49822
49867
  var RESTRICTED_SCAN_TAIL_BYTES = 1024;
49868
+ var COMMAND_RECORD_RETENTION_MS = 5 * 60 * 1e3;
49869
+ var MAX_RETAINED_COMMAND_RECORDS = 8;
49823
49870
  function restrictedDataError() {
49824
49871
  return new WorkerError(
49825
49872
  RESTRICTED_DATA_ERROR_CODE,
@@ -49836,20 +49883,26 @@ function scanOutputChunk(previousTail, chunk) {
49836
49883
  tail
49837
49884
  };
49838
49885
  }
49886
+ function markRestrictedData(record2) {
49887
+ if (record2.restrictedDataDetected) return;
49888
+ record2.restrictedDataDetected = true;
49889
+ record2.stdout = emptyCapture();
49890
+ record2.stderr = emptyCapture();
49891
+ record2.stdoutScanTail = Buffer.alloc(0);
49892
+ record2.stderrScanTail = Buffer.alloc(0);
49893
+ if (record2.status === "running") {
49894
+ record2.requestedTerminal = "canceled";
49895
+ void terminateProcessTree(record2.child).catch(() => void 0);
49896
+ }
49897
+ markChanged(record2);
49898
+ }
49839
49899
  function recordCommandOutput(record2, streamName, chunk) {
49840
49900
  if (record2.restrictedDataDetected || chunk.byteLength === 0) return;
49841
49901
  const tailName = streamName === "stdout" ? "stdoutScanTail" : "stderrScanTail";
49842
49902
  const scan = scanOutputChunk(record2[tailName], chunk);
49843
49903
  record2[tailName] = scan.tail;
49844
49904
  if (scan.detected) {
49845
- record2.restrictedDataDetected = true;
49846
- record2.stdout = emptyCapture();
49847
- record2.stderr = emptyCapture();
49848
- if (record2.status === "running") {
49849
- record2.requestedTerminal = "canceled";
49850
- void terminateProcessTree(record2.child).catch(() => void 0);
49851
- }
49852
- markChanged(record2);
49905
+ markRestrictedData(record2);
49853
49906
  return;
49854
49907
  }
49855
49908
  if (capture(record2, record2[streamName], chunk)) markChanged(record2);
@@ -49864,6 +49917,17 @@ function appendTail(existing, chunk) {
49864
49917
  function capture(_record2, stream, chunk) {
49865
49918
  if (chunk.byteLength === 0) return false;
49866
49919
  stream.totalBytes += chunk.byteLength;
49920
+ const retentionBudget = MAX_COMMAND_RETAINED_STREAM_BYTES - stream.retainedBytes;
49921
+ if (retentionBudget > 0) {
49922
+ const retained = chunk.subarray(0, Math.min(chunk.byteLength, retentionBudget));
49923
+ if (retained.byteLength > 0) {
49924
+ stream.retained.push(Buffer.from(retained));
49925
+ stream.retainedBytes += retained.byteLength;
49926
+ }
49927
+ }
49928
+ if (chunk.byteLength > Math.max(0, retentionBudget)) {
49929
+ stream.retentionTruncated = true;
49930
+ }
49867
49931
  let offset = 0;
49868
49932
  if (stream.headBytes < STREAM_HEAD_BYTES) {
49869
49933
  const accepted = chunk.subarray(
@@ -49912,7 +49976,10 @@ function emptyCapture() {
49912
49976
  head: [],
49913
49977
  headBytes: 0,
49914
49978
  tail: Buffer.alloc(0),
49915
- totalBytes: 0
49979
+ retained: [],
49980
+ retainedBytes: 0,
49981
+ totalBytes: 0,
49982
+ retentionTruncated: false
49916
49983
  };
49917
49984
  }
49918
49985
  function retainedBytes(stream, complete) {
@@ -49954,6 +50021,44 @@ function utf8SuffixWithinBudget(value, budget) {
49954
50021
  }
49955
50022
  return characters.slice(start).join("");
49956
50023
  }
50024
+ function isUtf8Continuation(byte) {
50025
+ return byte !== void 0 && (byte & 192) === 128;
50026
+ }
50027
+ function utf8SequenceBytes(byte) {
50028
+ if ((byte & 128) === 0) return 1;
50029
+ if ((byte & 224) === 192) return 2;
50030
+ if ((byte & 240) === 224) return 3;
50031
+ if ((byte & 248) === 240) return 4;
50032
+ return 1;
50033
+ }
50034
+ function retainedRange(stream, requestedOffset, maxBytes) {
50035
+ const retained = Buffer.concat(stream.retained, stream.retainedBytes);
50036
+ let offset = requestedOffset;
50037
+ while (offset < retained.byteLength && isUtf8Continuation(retained[offset])) {
50038
+ offset += 1;
50039
+ }
50040
+ if (offset >= retained.byteLength) {
50041
+ return { offset, content: "" };
50042
+ }
50043
+ let end = Math.min(retained.byteLength, offset + maxBytes);
50044
+ if (end < retained.byteLength) {
50045
+ while (end > offset && isUtf8Continuation(retained[end])) end -= 1;
50046
+ }
50047
+ if (end > offset) {
50048
+ let lead = end - 1;
50049
+ while (lead > offset && isUtf8Continuation(retained[lead])) lead -= 1;
50050
+ const expected = utf8SequenceBytes(retained[lead]);
50051
+ if (expected > 1 && end - lead < expected) end = lead;
50052
+ }
50053
+ if (end <= offset) {
50054
+ end = Math.min(retained.byteLength, offset + maxBytes);
50055
+ }
50056
+ return {
50057
+ offset,
50058
+ content: retained.subarray(offset, end).toString("utf8"),
50059
+ ...end < retained.byteLength ? { nextOffset: end } : {}
50060
+ };
50061
+ }
49957
50062
  function renderStream(stream, budget, complete) {
49958
50063
  if (budget <= 0 || stream.totalBytes === 0) {
49959
50064
  return { content: "", truncated: stream.totalBytes > 0 };
@@ -50046,6 +50151,21 @@ var CommandService = class {
50046
50151
  policy;
50047
50152
  #commands = /* @__PURE__ */ new Map();
50048
50153
  #activeCommandId = null;
50154
+ #pruneRetainedCommands() {
50155
+ while (this.#commands.size >= MAX_RETAINED_COMMAND_RECORDS) {
50156
+ const oldestTerminal = [...this.#commands].find(
50157
+ ([, record2]) => record2.status !== "running"
50158
+ );
50159
+ if (!oldestTerminal) return;
50160
+ this.#commands.delete(oldestTerminal[0]);
50161
+ }
50162
+ }
50163
+ #scheduleDeletion(commandId) {
50164
+ setTimeout(
50165
+ () => this.#commands.delete(commandId),
50166
+ COMMAND_RECORD_RETENTION_MS
50167
+ ).unref();
50168
+ }
50049
50169
  async start(options) {
50050
50170
  if (this.#activeCommandId) {
50051
50171
  const active = this.#commands.get(this.#activeCommandId);
@@ -50130,6 +50250,7 @@ var CommandService = class {
50130
50250
  void terminateProcessTree(child);
50131
50251
  }, timeoutMs);
50132
50252
  record2.timeout.unref();
50253
+ this.#pruneRetainedCommands();
50133
50254
  this.#commands.set(id, record2);
50134
50255
  this.#activeCommandId = id;
50135
50256
  child.stdout.on("data", (chunk) => {
@@ -50147,6 +50268,7 @@ var CommandService = class {
50147
50268
  this.#activeCommandId = null;
50148
50269
  markChanged(record2);
50149
50270
  record2.complete();
50271
+ this.#scheduleDeletion(id);
50150
50272
  });
50151
50273
  child.once("close", (exitCode, signal) => {
50152
50274
  if (record2.status !== "running") return;
@@ -50158,7 +50280,7 @@ var CommandService = class {
50158
50280
  this.#activeCommandId = null;
50159
50281
  markChanged(record2);
50160
50282
  record2.complete();
50161
- setTimeout(() => this.#commands.delete(id), 5 * 60 * 1e3).unref();
50283
+ this.#scheduleDeletion(id);
50162
50284
  });
50163
50285
  if (options.stdin !== void 0) child.stdin.end(options.stdin);
50164
50286
  else child.stdin.end();
@@ -50215,6 +50337,53 @@ var CommandService = class {
50215
50337
  }
50216
50338
  return this.snapshot(record2);
50217
50339
  }
50340
+ async readOutput(commandId, streamName, offset = 0, maxBytes = DEFAULT_COMMAND_OUTPUT_RANGE_BYTES) {
50341
+ const record2 = this.#commands.get(commandId);
50342
+ if (!record2) throw new WorkerError("command_not_found", "The command was not found.");
50343
+ if (streamName !== "stdout" && streamName !== "stderr") {
50344
+ throw new WorkerError(
50345
+ "invalid_output_stream",
50346
+ "Command output stream must be stdout or stderr."
50347
+ );
50348
+ }
50349
+ if (!Number.isInteger(offset) || offset < 0) {
50350
+ throw new WorkerError(
50351
+ "invalid_output_offset",
50352
+ "Command output offset must be a non-negative integer."
50353
+ );
50354
+ }
50355
+ if (!Number.isInteger(maxBytes) || maxBytes < 4 || maxBytes > MAX_COMMAND_OUTPUT_RANGE_BYTES) {
50356
+ throw new WorkerError(
50357
+ "invalid_output_range",
50358
+ "Command output range must be between 4 and 65536 source bytes."
50359
+ );
50360
+ }
50361
+ if (record2.restrictedDataDetected) throw restrictedDataError();
50362
+ const stream = record2[streamName];
50363
+ if (offset > stream.retainedBytes) {
50364
+ throw new WorkerError(
50365
+ "output_offset_out_of_range",
50366
+ "The command output offset exceeds the retained stream length."
50367
+ );
50368
+ }
50369
+ const range = retainedRange(stream, offset, maxBytes);
50370
+ if (containsRestrictedAuthenticationData(range.content)) {
50371
+ markRestrictedData(record2);
50372
+ throw restrictedDataError();
50373
+ }
50374
+ return {
50375
+ commandId,
50376
+ stream: streamName,
50377
+ status: record2.status,
50378
+ offset: range.offset,
50379
+ content: range.content,
50380
+ ...range.nextOffset === void 0 ? {} : { nextOffset: range.nextOffset },
50381
+ retainedBytes: stream.retainedBytes,
50382
+ totalBytes: stream.totalBytes,
50383
+ retentionTruncated: stream.retentionTruncated,
50384
+ complete: record2.status !== "running"
50385
+ };
50386
+ }
50218
50387
  async cancel(commandId) {
50219
50388
  const record2 = this.#commands.get(commandId);
50220
50389
  if (!record2) throw new WorkerError("command_not_found", "The command was not found.");
@@ -50262,124 +50431,414 @@ var CommandService = class {
50262
50431
  import { createHash, randomUUID as randomUUID2 } from "node:crypto";
50263
50432
  import {
50264
50433
  chmod as chmod2,
50265
- lstat,
50434
+ link as link2,
50435
+ lstat as lstat2,
50436
+ mkdir as mkdir3,
50266
50437
  opendir,
50267
50438
  open,
50268
50439
  readFile as readFile3,
50269
50440
  rename,
50270
50441
  rm as rm2,
50271
- stat,
50442
+ rmdir,
50443
+ stat as stat2,
50272
50444
  writeFile as writeFile3
50273
50445
  } from "node:fs/promises";
50274
- import path5 from "node:path";
50446
+ import path6 from "node:path";
50275
50447
  import { performance as performance2 } from "node:perf_hooks";
50276
50448
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
50277
- function sha256(content) {
50278
- return createHash("sha256").update(content).digest("hex");
50449
+
50450
+ // src/worker/path-policy.ts
50451
+ import { lstat, realpath, stat } from "node:fs/promises";
50452
+ import os5 from "node:os";
50453
+ import path5 from "node:path";
50454
+ function samePath(left, right) {
50455
+ return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
50279
50456
  }
50280
- var fileWriteTails = /* @__PURE__ */ new Map();
50281
- async function withFileWriteLock(target, operation) {
50282
- const normalized = path5.normalize(target);
50283
- const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
50284
- const predecessor = fileWriteTails.get(key);
50285
- let release;
50286
- const tail = new Promise((resolve) => {
50287
- release = resolve;
50288
- });
50289
- fileWriteTails.set(key, tail);
50290
- if (predecessor) await predecessor;
50457
+ function accountHomeDirectory() {
50291
50458
  try {
50292
- return await operation();
50293
- } finally {
50294
- release();
50295
- if (fileWriteTails.get(key) === tail) fileWriteTails.delete(key);
50459
+ return os5.userInfo().homedir;
50460
+ } catch {
50461
+ return os5.homedir();
50296
50462
  }
50297
50463
  }
50298
- async function requireRevision(target, expectedSha256) {
50299
- let actual = null;
50300
- try {
50301
- actual = sha256(await readFile3(target));
50302
- } catch (error46) {
50303
- if (error46.code !== "ENOENT") throw error46;
50464
+ function isWithin(root, candidate) {
50465
+ const relative = path5.relative(root, candidate);
50466
+ return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
50467
+ }
50468
+ function validateRelativePath(value) {
50469
+ if (value.includes("\0")) {
50470
+ throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
50304
50471
  }
50305
- if (actual !== expectedSha256) {
50306
- throw new WorkerError("stale_revision", "The file revision has changed.");
50472
+ const explicitNativePosixPath = process.platform !== "win32" && value.startsWith("./");
50473
+ if (path5.isAbsolute(value) || path5.posix.isAbsolute(value) || !explicitNativePosixPath && path5.win32.isAbsolute(value)) {
50474
+ throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
50307
50475
  }
50476
+ const segments = explicitNativePosixPath ? value.split(/\/+/).filter(Boolean) : value.split(/[\\/]+/);
50477
+ if (segments.includes("..")) {
50478
+ throw new WorkerError("path_traversal", "Parent path traversal is not allowed.");
50479
+ }
50480
+ return value === "" ? "." : value;
50308
50481
  }
50309
- var DEFAULT_LIST_FILES_LIMIT = 100;
50310
- var DEFAULT_SEARCH_TEXT_RESULTS = 50;
50311
- var DEFAULT_READ_RANGE_LINES = 200;
50312
- var MAX_REPOSITORY_SCAN_ENTRIES = 2e4;
50313
- var MAX_SEARCH_FILES = 5e3;
50314
- var MAX_SEARCH_BYTES = 32 * 1024 * 1024;
50315
- var SKIPPED_RECURSIVE_DIRECTORIES = /* @__PURE__ */ new Set([
50316
- ".git",
50317
- ".hg",
50318
- ".svn",
50319
- "node_modules"
50320
- ]);
50321
- function compareNames(left, right) {
50322
- return left < right ? -1 : left > right ? 1 : 0;
50323
- }
50324
- function relativeSortKey(root, target) {
50325
- const relative = path5.relative(root, target);
50326
- if (!relative) return ".";
50327
- return process.platform === "win32" ? relative.replaceAll("\\", "/") : relative;
50328
- }
50329
- function displayPath(root, target) {
50330
- const relative = relativeSortKey(root, target);
50331
- if (process.platform === "win32") return relative;
50332
- return relative.includes("\\") ? "./" + relative : relative;
50333
- }
50334
- function isUnavailableFileError(error46) {
50335
- const code = error46?.code;
50336
- return code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "ENOTDIR";
50337
- }
50338
- function isUnavailableDiscoveredPathError(error46) {
50339
- return isUnavailableFileError(error46) || error46 instanceof WorkerError && error46.code === "path_not_found";
50340
- }
50341
- function isLinkedPathError(error46) {
50342
- return error46 instanceof WorkerError && error46.code === "linked_path";
50482
+ async function canonicalizeRoot(candidate) {
50483
+ const root = await realpath(path5.resolve(candidate)).catch((error46) => {
50484
+ if (error46.code === "ENOENT") {
50485
+ throw new WorkerError("root_not_found", "The workspace directory does not exist.");
50486
+ }
50487
+ throw error46;
50488
+ });
50489
+ const rootStat = await stat(root);
50490
+ if (!rootStat.isDirectory()) {
50491
+ throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
50492
+ }
50493
+ const filesystemRoot = path5.parse(root).root;
50494
+ const homes = await Promise.all(
50495
+ [os5.homedir(), accountHomeDirectory()].map(
50496
+ async (home) => await realpath(home).catch(() => path5.resolve(home))
50497
+ )
50498
+ );
50499
+ const isHomeDirectory = homes.some((home) => samePath(root, home));
50500
+ if (samePath(root, filesystemRoot) || isHomeDirectory) {
50501
+ const kind = isHomeDirectory ? "your home directory" : "a filesystem root";
50502
+ throw new WorkerError(
50503
+ "broad_root_refused",
50504
+ `The selected root is ${kind}, which Glossa will not expose. Choose a project directory instead.`
50505
+ );
50506
+ }
50507
+ return root;
50343
50508
  }
50344
- async function readBoundedFile(target, maximumBytes, expectedBytes) {
50345
- const handle = await open(target, "r");
50346
- try {
50347
- const openedStat = await handle.stat();
50348
- if (!openedStat.isFile()) {
50349
- throw new WorkerError("not_file", "The requested path is not a file.");
50509
+ var PathPolicy = class _PathPolicy {
50510
+ constructor(root) {
50511
+ this.root = root;
50512
+ }
50513
+ root;
50514
+ static async create(candidate) {
50515
+ return new _PathPolicy(await canonicalizeRoot(candidate));
50516
+ }
50517
+ async resolveExisting(relativePath) {
50518
+ const lexical = this.resolveLexical(relativePath);
50519
+ await this.rejectLinkedComponents(lexical);
50520
+ const canonical = await realpath(lexical).catch((error46) => {
50521
+ if (error46.code === "ENOENT") {
50522
+ throw new WorkerError("path_not_found", "The requested path does not exist.");
50523
+ }
50524
+ throw error46;
50525
+ });
50526
+ if (!isWithin(this.root, canonical)) {
50527
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50350
50528
  }
50351
- if (openedStat.size !== expectedBytes) {
50352
- throw new WorkerError("file_changed", "The file changed while it was being read.");
50529
+ return canonical;
50530
+ }
50531
+ async resolveDiscoveredExisting(candidate) {
50532
+ const lexical = path5.resolve(candidate);
50533
+ if (!isWithin(this.root, lexical)) {
50534
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50353
50535
  }
50354
- if (openedStat.size > maximumBytes) {
50355
- throw new WorkerError("search_byte_limit", "The search byte limit was reached.");
50536
+ await this.rejectLinkedComponents(lexical);
50537
+ const canonical = await realpath(lexical).catch((error46) => {
50538
+ if (error46.code === "ENOENT") {
50539
+ throw new WorkerError("path_not_found", "The requested path does not exist.");
50540
+ }
50541
+ throw error46;
50542
+ });
50543
+ if (!isWithin(this.root, canonical)) {
50544
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50356
50545
  }
50357
- const buffer = Buffer.allocUnsafe(openedStat.size);
50358
- let offset = 0;
50359
- while (offset < buffer.byteLength) {
50360
- const { bytesRead } = await handle.read(
50361
- buffer,
50362
- offset,
50363
- buffer.byteLength - offset,
50364
- offset
50365
- );
50366
- if (bytesRead === 0) break;
50367
- offset += bytesRead;
50546
+ return canonical;
50547
+ }
50548
+ async resolveWritableFile(relativePath) {
50549
+ const lexical = this.resolveLexical(relativePath);
50550
+ const parent = path5.dirname(lexical);
50551
+ await this.rejectLinkedComponents(parent);
50552
+ const canonicalParent = await realpath(parent).catch((error46) => {
50553
+ if (error46.code === "ENOENT") {
50554
+ throw new WorkerError("parent_not_found", "The destination directory does not exist.");
50555
+ }
50556
+ throw error46;
50557
+ });
50558
+ if (!isWithin(this.root, canonicalParent)) {
50559
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50368
50560
  }
50369
- const completedStat = await handle.stat();
50370
- if (completedStat.size !== openedStat.size || offset !== openedStat.size) {
50371
- throw new WorkerError("file_changed", "The file changed while it was being read.");
50561
+ if (!(await stat(canonicalParent)).isDirectory()) {
50562
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50372
50563
  }
50373
- return Buffer.from(buffer);
50374
- } finally {
50375
- await handle.close();
50564
+ try {
50565
+ const targetStat = await lstat(lexical);
50566
+ if (targetStat.isSymbolicLink()) {
50567
+ throw new WorkerError("linked_path", "Writes through links are not allowed.");
50568
+ }
50569
+ if (targetStat.isDirectory()) {
50570
+ throw new WorkerError("not_file", "The destination is a directory.");
50571
+ }
50572
+ const canonicalTarget = await realpath(lexical);
50573
+ if (!isWithin(this.root, canonicalTarget)) {
50574
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50575
+ }
50576
+ return canonicalTarget;
50577
+ } catch (error46) {
50578
+ if (error46 instanceof WorkerError) throw error46;
50579
+ if (error46.code !== "ENOENT") throw error46;
50580
+ }
50581
+ return path5.join(canonicalParent, path5.basename(lexical));
50376
50582
  }
50377
- }
50378
- function normalizedLines(content) {
50379
- if (content.length === 0) return [];
50380
- const normalized = content.replace(/\r\n?/g, "\n");
50381
- const lines = normalized.split("\n");
50382
- if (normalized.endsWith("\n")) lines.pop();
50583
+ async resolveWritableDirectory(relativePath, recursive) {
50584
+ const lexical = this.resolveLexical(relativePath);
50585
+ await this.rejectLinkedComponents(lexical);
50586
+ try {
50587
+ const targetStat = await lstat(lexical);
50588
+ if (targetStat.isSymbolicLink()) {
50589
+ throw new WorkerError("linked_path", "Directory creation through links is not allowed.");
50590
+ }
50591
+ if (!targetStat.isDirectory()) {
50592
+ throw new WorkerError("not_directory", "The destination is not a directory.");
50593
+ }
50594
+ const canonicalTarget = await realpath(lexical);
50595
+ if (!isWithin(this.root, canonicalTarget)) {
50596
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50597
+ }
50598
+ return { target: canonicalTarget, exists: true };
50599
+ } catch (error46) {
50600
+ if (error46 instanceof WorkerError) throw error46;
50601
+ if (error46.code !== "ENOENT") throw error46;
50602
+ }
50603
+ if (!recursive) {
50604
+ const parent = path5.dirname(lexical);
50605
+ await this.rejectLinkedComponents(parent);
50606
+ const canonicalParent = await realpath(parent).catch(
50607
+ (error46) => {
50608
+ if (error46.code === "ENOENT") {
50609
+ throw new WorkerError(
50610
+ "parent_not_found",
50611
+ "The destination directory does not exist."
50612
+ );
50613
+ }
50614
+ throw error46;
50615
+ }
50616
+ );
50617
+ if (!isWithin(this.root, canonicalParent)) {
50618
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50619
+ }
50620
+ if (!(await stat(canonicalParent)).isDirectory()) {
50621
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50622
+ }
50623
+ return {
50624
+ target: path5.join(canonicalParent, path5.basename(lexical)),
50625
+ exists: false
50626
+ };
50627
+ }
50628
+ let existingAncestor = path5.dirname(lexical);
50629
+ while (!samePath(existingAncestor, this.root)) {
50630
+ try {
50631
+ await this.rejectLinkedComponents(existingAncestor);
50632
+ const canonicalAncestor = await realpath(existingAncestor);
50633
+ if (!isWithin(this.root, canonicalAncestor)) {
50634
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50635
+ }
50636
+ if (!(await stat(canonicalAncestor)).isDirectory()) {
50637
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50638
+ }
50639
+ return {
50640
+ target: path5.join(
50641
+ canonicalAncestor,
50642
+ path5.relative(existingAncestor, lexical)
50643
+ ),
50644
+ exists: false
50645
+ };
50646
+ } catch (error46) {
50647
+ if (error46 instanceof WorkerError) throw error46;
50648
+ if (error46.code !== "ENOENT") throw error46;
50649
+ }
50650
+ existingAncestor = path5.dirname(existingAncestor);
50651
+ }
50652
+ return { target: lexical, exists: false };
50653
+ }
50654
+ async resolveVacantPath(relativePath) {
50655
+ const lexical = this.resolveLexical(relativePath);
50656
+ const parent = path5.dirname(lexical);
50657
+ await this.rejectLinkedComponents(parent);
50658
+ const canonicalParent = await realpath(parent).catch(
50659
+ (error46) => {
50660
+ if (error46.code === "ENOENT") {
50661
+ throw new WorkerError(
50662
+ "parent_not_found",
50663
+ "The destination directory does not exist."
50664
+ );
50665
+ }
50666
+ throw error46;
50667
+ }
50668
+ );
50669
+ if (!isWithin(this.root, canonicalParent)) {
50670
+ throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50671
+ }
50672
+ if (!(await stat(canonicalParent)).isDirectory()) {
50673
+ throw new WorkerError("not_directory", "The destination parent is not a directory.");
50674
+ }
50675
+ try {
50676
+ await lstat(lexical);
50677
+ throw new WorkerError(
50678
+ "destination_exists",
50679
+ "The destination already exists."
50680
+ );
50681
+ } catch (error46) {
50682
+ if (error46 instanceof WorkerError) throw error46;
50683
+ if (error46.code !== "ENOENT") throw error46;
50684
+ }
50685
+ return path5.join(canonicalParent, path5.basename(lexical));
50686
+ }
50687
+ resolveLexical(relativePath) {
50688
+ const validated = validateRelativePath(relativePath);
50689
+ const candidate = path5.resolve(this.root, validated);
50690
+ if (!isWithin(this.root, candidate)) {
50691
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50692
+ }
50693
+ return candidate;
50694
+ }
50695
+ async rejectLinkedComponents(candidate) {
50696
+ if (!isWithin(this.root, candidate)) {
50697
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50698
+ }
50699
+ const relative = path5.relative(this.root, candidate);
50700
+ if (!relative) return;
50701
+ let current = this.root;
50702
+ for (const segment of relative.split(path5.sep)) {
50703
+ current = path5.join(current, segment);
50704
+ try {
50705
+ const currentStat = await lstat(current);
50706
+ if (currentStat.isSymbolicLink()) {
50707
+ throw new WorkerError(
50708
+ "linked_path",
50709
+ "Symlink and junction paths are not allowed."
50710
+ );
50711
+ }
50712
+ } catch (error46) {
50713
+ if (error46 instanceof WorkerError) throw error46;
50714
+ if (error46.code === "ENOENT") return;
50715
+ throw error46;
50716
+ }
50717
+ }
50718
+ }
50719
+ };
50720
+
50721
+ // src/worker/file-service.ts
50722
+ function sha256(content) {
50723
+ return createHash("sha256").update(content).digest("hex");
50724
+ }
50725
+ function isPathWithin(root, candidate) {
50726
+ const relative = path6.relative(root, candidate);
50727
+ return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
50728
+ }
50729
+ var fileWriteTails = /* @__PURE__ */ new Map();
50730
+ async function withFileWriteLock(target, operation) {
50731
+ const normalized = path6.normalize(target);
50732
+ const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
50733
+ const predecessor = fileWriteTails.get(key);
50734
+ let release;
50735
+ const tail = new Promise((resolve) => {
50736
+ release = resolve;
50737
+ });
50738
+ fileWriteTails.set(key, tail);
50739
+ if (predecessor) await predecessor;
50740
+ try {
50741
+ return await operation();
50742
+ } finally {
50743
+ release();
50744
+ if (fileWriteTails.get(key) === tail) fileWriteTails.delete(key);
50745
+ }
50746
+ }
50747
+ async function withFileWriteLocks(targets, operation) {
50748
+ const unique = [...new Set(targets.map((target) => path6.normalize(target)))].sort(
50749
+ (left, right) => left.localeCompare(right)
50750
+ );
50751
+ const run = async (index) => {
50752
+ const target = unique[index];
50753
+ return target === void 0 ? await operation() : await withFileWriteLock(target, async () => await run(index + 1));
50754
+ };
50755
+ return await run(0);
50756
+ }
50757
+ async function requireRevision(target, expectedSha256) {
50758
+ let actual = null;
50759
+ try {
50760
+ actual = sha256(await readFile3(target));
50761
+ } catch (error46) {
50762
+ if (error46.code !== "ENOENT") throw error46;
50763
+ }
50764
+ if (actual !== expectedSha256) {
50765
+ throw new WorkerError("stale_revision", "The file revision has changed.");
50766
+ }
50767
+ }
50768
+ var DEFAULT_LIST_FILES_LIMIT = 100;
50769
+ var DEFAULT_SEARCH_TEXT_RESULTS = 50;
50770
+ var DEFAULT_READ_RANGE_LINES = 200;
50771
+ var MAX_REPOSITORY_SCAN_ENTRIES = 2e4;
50772
+ var MAX_SEARCH_FILES = 5e3;
50773
+ var MAX_SEARCH_BYTES = 32 * 1024 * 1024;
50774
+ var SKIPPED_RECURSIVE_DIRECTORIES = /* @__PURE__ */ new Set([
50775
+ ".git",
50776
+ ".hg",
50777
+ ".svn",
50778
+ "node_modules"
50779
+ ]);
50780
+ function compareNames(left, right) {
50781
+ return left < right ? -1 : left > right ? 1 : 0;
50782
+ }
50783
+ function relativeSortKey(root, target) {
50784
+ const relative = path6.relative(root, target);
50785
+ if (!relative) return ".";
50786
+ return process.platform === "win32" ? relative.replaceAll("\\", "/") : relative;
50787
+ }
50788
+ function displayPath(root, target) {
50789
+ const relative = relativeSortKey(root, target);
50790
+ if (process.platform === "win32") return relative;
50791
+ return relative.includes("\\") ? "./" + relative : relative;
50792
+ }
50793
+ function isUnavailableFileError(error46) {
50794
+ const code = error46?.code;
50795
+ return code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "ENOTDIR";
50796
+ }
50797
+ function isUnavailableDiscoveredPathError(error46) {
50798
+ return isUnavailableFileError(error46) || error46 instanceof WorkerError && error46.code === "path_not_found";
50799
+ }
50800
+ function isLinkedPathError(error46) {
50801
+ return error46 instanceof WorkerError && error46.code === "linked_path";
50802
+ }
50803
+ async function readBoundedFile(target, maximumBytes, expectedBytes) {
50804
+ const handle = await open(target, "r");
50805
+ try {
50806
+ const openedStat = await handle.stat();
50807
+ if (!openedStat.isFile()) {
50808
+ throw new WorkerError("not_file", "The requested path is not a file.");
50809
+ }
50810
+ if (openedStat.size !== expectedBytes) {
50811
+ throw new WorkerError("file_changed", "The file changed while it was being read.");
50812
+ }
50813
+ if (openedStat.size > maximumBytes) {
50814
+ throw new WorkerError("search_byte_limit", "The search byte limit was reached.");
50815
+ }
50816
+ const buffer = Buffer.allocUnsafe(openedStat.size);
50817
+ let offset = 0;
50818
+ while (offset < buffer.byteLength) {
50819
+ const { bytesRead } = await handle.read(
50820
+ buffer,
50821
+ offset,
50822
+ buffer.byteLength - offset,
50823
+ offset
50824
+ );
50825
+ if (bytesRead === 0) break;
50826
+ offset += bytesRead;
50827
+ }
50828
+ const completedStat = await handle.stat();
50829
+ if (completedStat.size !== openedStat.size || offset !== openedStat.size) {
50830
+ throw new WorkerError("file_changed", "The file changed while it was being read.");
50831
+ }
50832
+ return Buffer.from(buffer);
50833
+ } finally {
50834
+ await handle.close();
50835
+ }
50836
+ }
50837
+ function normalizedLines(content) {
50838
+ if (content.length === 0) return [];
50839
+ const normalized = content.replace(/\r\n?/g, "\n");
50840
+ const lines = normalized.split("\n");
50841
+ if (normalized.endsWith("\n")) lines.pop();
50383
50842
  return lines;
50384
50843
  }
50385
50844
  function escapeRegExp(value) {
@@ -50563,7 +51022,7 @@ var FileService = class {
50563
51022
  this.#beforeDeadline = dependencies.beforeDeadline ?? defaultBeforeDeadline;
50564
51023
  this.#openDirectory = dependencies.openDirectory ?? ((directory) => opendir(directory));
50565
51024
  this.#readFileBytes = dependencies.readFileBytes ?? readBoundedFile;
50566
- this.#lstatPath = dependencies.lstatPath ?? ((target) => lstat(target));
51025
+ this.#lstatPath = dependencies.lstatPath ?? ((target) => lstat2(target));
50567
51026
  this.#trustDirectoryEntryTypes = dependencies.trustDirectoryEntryTypes ?? dependencies.lstatPath === void 0;
50568
51027
  this.#maxRepositoryScanEntries = dependencies.maxRepositoryScanEntries ?? MAX_REPOSITORY_SCAN_ENTRIES;
50569
51028
  this.#maxSearchBytes = dependencies.maxSearchBytes ?? MAX_SEARCH_BYTES;
@@ -50634,7 +51093,7 @@ var FileService = class {
50634
51093
  }
50635
51094
  async #readResolvedText(target, maximumBytes = MAX_TEXT_BYTES) {
50636
51095
  const boundedMaximum = Math.min(maximumBytes, MAX_TEXT_BYTES);
50637
- const targetStat = await stat(target);
51096
+ const targetStat = await stat2(target);
50638
51097
  if (!targetStat.isFile()) {
50639
51098
  throw new WorkerError("not_file", "The requested path is not a file.");
50640
51099
  }
@@ -50677,7 +51136,7 @@ var FileService = class {
50677
51136
  this.policy.resolveExisting(startPath),
50678
51137
  deadlineAt
50679
51138
  );
50680
- const startStat = await this.#withinDeadline(stat(start), deadlineAt);
51139
+ const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
50681
51140
  if (!startStat.isDirectory()) {
50682
51141
  throw new WorkerError("not_directory", "The requested path is not a directory.");
50683
51142
  }
@@ -50706,7 +51165,7 @@ var FileService = class {
50706
51165
  if (batch.overflow) throw scanLimitError();
50707
51166
  scannedEntries += batch.children.length;
50708
51167
  for (const child of batch.children) {
50709
- const target = path5.join(directory, child.name);
51168
+ const target = path6.join(directory, child.name);
50710
51169
  heapPush(frontier, {
50711
51170
  target,
50712
51171
  sortKey: relativeSortKey(this.policy.root, target),
@@ -50730,7 +51189,7 @@ var FileService = class {
50730
51189
  throw scanLimitError();
50731
51190
  }
50732
51191
  scannedEntries += 1;
50733
- const target = path5.join(directory, child.name);
51192
+ const target = path6.join(directory, child.name);
50734
51193
  let childStat;
50735
51194
  try {
50736
51195
  childStat = await this.#withinDeadline(
@@ -50876,15 +51335,31 @@ var FileService = class {
50876
51335
  );
50877
51336
  const matchLimit = maxResults + 1;
50878
51337
  const extensions = options.extensions?.map((extension) => extension.toLowerCase());
50879
- const matcher = new RegExp(
50880
- escapeRegExp(options.query),
50881
- options.caseSensitive === true ? "u" : "iu"
50882
- );
51338
+ const includeGlobs = options.includeGlobs;
51339
+ const excludeGlobs = options.excludeGlobs;
51340
+ const globMatches = (relativePath, patterns) => {
51341
+ if (!patterns) return false;
51342
+ const normalized = relativePath.replaceAll("\\", "/");
51343
+ try {
51344
+ return patterns.some((pattern) => path6.posix.matchesGlob(normalized, pattern));
51345
+ } catch {
51346
+ throw new WorkerError("invalid_search", "Search glob pattern is invalid.");
51347
+ }
51348
+ };
51349
+ let matcher;
51350
+ try {
51351
+ matcher = new RegExp(
51352
+ options.matchMode === "regex" ? options.query : escapeRegExp(options.query),
51353
+ options.caseSensitive === true ? "u" : "iu"
51354
+ );
51355
+ } catch {
51356
+ throw new WorkerError("invalid_search", "Search regular expression is invalid.");
51357
+ }
50883
51358
  const start = await this.#withinDeadline(
50884
51359
  this.policy.resolveExisting(options.path ?? "."),
50885
51360
  deadlineAt
50886
51361
  );
50887
- const startStat = await this.#withinDeadline(stat(start), deadlineAt);
51362
+ const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
50888
51363
  const matches = [];
50889
51364
  let scannedEntries = 0;
50890
51365
  let scannedFiles = 0;
@@ -50895,10 +51370,12 @@ var FileService = class {
50895
51370
  const searchFile = async (target) => {
50896
51371
  const relative = displayPath(this.policy.root, target);
50897
51372
  if (extensions && !extensions.some(
50898
- (extension) => path5.basename(target).toLowerCase().endsWith(extension)
51373
+ (extension) => path6.basename(target).toLowerCase().endsWith(extension)
50899
51374
  )) {
50900
51375
  return false;
50901
51376
  }
51377
+ if (includeGlobs && !globMatches(relative, includeGlobs)) return false;
51378
+ if (globMatches(relative, excludeGlobs)) return false;
50902
51379
  if (scannedFiles >= MAX_SEARCH_FILES) {
50903
51380
  scanTruncated = true;
50904
51381
  return true;
@@ -50988,7 +51465,7 @@ var FileService = class {
50988
51465
  scannedEntries += batch.children.length;
50989
51466
  for (const child of batch.children) {
50990
51467
  this.#assertBeforeDeadline(deadlineAt);
50991
- const target = path5.join(directory, child.name);
51468
+ const target = path6.join(directory, child.name);
50992
51469
  let targetType;
50993
51470
  if (this.#trustDirectoryEntryTypes) {
50994
51471
  if (child.isSymbolicLink()) {
@@ -51123,6 +51600,118 @@ var FileService = class {
51123
51600
  ...endLine < lines.length ? { nextLine: endLine + 1 } : {}
51124
51601
  };
51125
51602
  }
51603
+ async makeDirectory(relativePath, recursive = false) {
51604
+ const initial = await this.policy.resolveWritableDirectory(
51605
+ relativePath,
51606
+ recursive
51607
+ );
51608
+ if (initial.exists) return { created: false };
51609
+ return await withFileWriteLock(initial.target, async () => {
51610
+ const current = await this.policy.resolveWritableDirectory(
51611
+ relativePath,
51612
+ recursive
51613
+ );
51614
+ if (current.exists) return { created: false };
51615
+ try {
51616
+ await mkdir3(current.target, { recursive });
51617
+ } catch (error46) {
51618
+ if (error46.code !== "EEXIST") throw error46;
51619
+ }
51620
+ const resolved = await this.policy.resolveExisting(relativePath);
51621
+ if (!(await stat2(resolved)).isDirectory()) {
51622
+ throw new WorkerError("not_directory", "The destination is not a directory.");
51623
+ }
51624
+ return { created: true };
51625
+ });
51626
+ }
51627
+ async deletePath(relativePath, recursive = false) {
51628
+ const initial = await this.policy.resolveExisting(relativePath);
51629
+ if (samePath(initial, this.policy.root)) {
51630
+ throw new WorkerError(
51631
+ "root_operation_refused",
51632
+ "The exposed workspace root cannot be deleted."
51633
+ );
51634
+ }
51635
+ return await withFileWriteLock(initial, async () => {
51636
+ const target = await this.policy.resolveExisting(relativePath);
51637
+ if (samePath(target, this.policy.root)) {
51638
+ throw new WorkerError(
51639
+ "root_operation_refused",
51640
+ "The exposed workspace root cannot be deleted."
51641
+ );
51642
+ }
51643
+ const targetStat = await lstat2(target);
51644
+ if (!targetStat.isFile() && !targetStat.isDirectory()) {
51645
+ throw new WorkerError(
51646
+ "unsupported_path_type",
51647
+ "Only regular files and directories can be deleted."
51648
+ );
51649
+ }
51650
+ try {
51651
+ if (targetStat.isDirectory()) {
51652
+ if (recursive) {
51653
+ await rm2(target, { recursive: true, force: false });
51654
+ } else {
51655
+ await rmdir(target);
51656
+ }
51657
+ } else {
51658
+ await rm2(target, { force: false });
51659
+ }
51660
+ } catch (error46) {
51661
+ const code = error46.code;
51662
+ if (targetStat.isDirectory() && !recursive && (code === "ENOTEMPTY" || code === "EEXIST" || code === "EPERM")) {
51663
+ throw new WorkerError(
51664
+ "directory_not_empty",
51665
+ "The directory is not empty. Set recursive to true to delete its contents."
51666
+ );
51667
+ }
51668
+ throw error46;
51669
+ }
51670
+ return {
51671
+ deletedType: targetStat.isDirectory() ? "directory" : "file"
51672
+ };
51673
+ });
51674
+ }
51675
+ async movePath(sourcePath, destinationPath) {
51676
+ const initialSource = await this.policy.resolveExisting(sourcePath);
51677
+ if (samePath(initialSource, this.policy.root)) {
51678
+ throw new WorkerError(
51679
+ "root_operation_refused",
51680
+ "The exposed workspace root cannot be moved."
51681
+ );
51682
+ }
51683
+ const initialDestination = await this.policy.resolveVacantPath(destinationPath);
51684
+ return await withFileWriteLocks(
51685
+ [initialSource, initialDestination],
51686
+ async () => {
51687
+ const source = await this.policy.resolveExisting(sourcePath);
51688
+ if (samePath(source, this.policy.root)) {
51689
+ throw new WorkerError(
51690
+ "root_operation_refused",
51691
+ "The exposed workspace root cannot be moved."
51692
+ );
51693
+ }
51694
+ const destination = await this.policy.resolveVacantPath(destinationPath);
51695
+ const sourceStat = await lstat2(source);
51696
+ if (!sourceStat.isFile() && !sourceStat.isDirectory()) {
51697
+ throw new WorkerError(
51698
+ "unsupported_path_type",
51699
+ "Only regular files and directories can be moved."
51700
+ );
51701
+ }
51702
+ if (sourceStat.isDirectory() && isPathWithin(source, destination)) {
51703
+ throw new WorkerError(
51704
+ "invalid_destination",
51705
+ "A directory cannot be moved inside itself."
51706
+ );
51707
+ }
51708
+ await rename(source, destination);
51709
+ return {
51710
+ movedType: sourceStat.isDirectory() ? "directory" : "file"
51711
+ };
51712
+ }
51713
+ );
51714
+ }
51126
51715
  async editText(relativePath, edits, expectedSha256) {
51127
51716
  const original = await this.readText(relativePath);
51128
51717
  if (expectedSha256 && original.sha256 !== expectedSha256) {
@@ -51174,25 +51763,45 @@ var FileService = class {
51174
51763
  return await withFileWriteLock(target, async () => {
51175
51764
  let existingMode;
51176
51765
  try {
51177
- const existingStat = await stat(target);
51766
+ const existingStat = await stat2(target);
51178
51767
  if (existingStat.isFile()) existingMode = existingStat.mode & 4095;
51179
51768
  } catch (error46) {
51180
51769
  if (error46.code !== "ENOENT") throw error46;
51181
51770
  }
51771
+ if (existingMode !== void 0 && expectedSha256 === void 0) {
51772
+ throw new WorkerError(
51773
+ "path_exists",
51774
+ "The file already exists. Read it first and pass expectedSha256 to replace that revision."
51775
+ );
51776
+ }
51182
51777
  if (expectedSha256) await requireRevision(target, expectedSha256);
51183
- const temporary = path5.join(path5.dirname(target), `.glossa-${randomUUID2()}.tmp`);
51778
+ const temporary = path6.join(path6.dirname(target), `.glossa-${randomUUID2()}.tmp`);
51184
51779
  try {
51185
51780
  await writeFile3(temporary, bytes, { flag: "wx", mode: 384 });
51186
51781
  target = await this.policy.resolveWritableFile(relativePath);
51187
- const tempStat = await lstat(temporary);
51782
+ const tempStat = await lstat2(temporary);
51188
51783
  if (!tempStat.isFile() || tempStat.isSymbolicLink()) {
51189
51784
  throw new WorkerError("unsafe_temporary_file", "The atomic write temporary file changed.");
51190
51785
  }
51191
51786
  if (existingMode !== void 0 && process.platform !== "win32") {
51192
51787
  await chmod2(temporary, existingMode);
51193
51788
  }
51194
- if (expectedSha256) await requireRevision(target, expectedSha256);
51195
- await rename(temporary, target);
51789
+ if (expectedSha256) {
51790
+ await requireRevision(target, expectedSha256);
51791
+ await rename(temporary, target);
51792
+ } else {
51793
+ try {
51794
+ await link2(temporary, target);
51795
+ } catch (error46) {
51796
+ if (error46.code === "EEXIST") {
51797
+ throw new WorkerError(
51798
+ "path_exists",
51799
+ "The file already exists. Read it first and pass expectedSha256 to replace that revision."
51800
+ );
51801
+ }
51802
+ throw error46;
51803
+ }
51804
+ }
51196
51805
  } finally {
51197
51806
  await rm2(temporary, { force: true });
51198
51807
  }
@@ -51201,173 +51810,6 @@ var FileService = class {
51201
51810
  }
51202
51811
  };
51203
51812
 
51204
- // src/worker/path-policy.ts
51205
- import { lstat as lstat2, realpath, stat as stat2 } from "node:fs/promises";
51206
- import os5 from "node:os";
51207
- import path6 from "node:path";
51208
- function samePath(left, right) {
51209
- return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
51210
- }
51211
- function accountHomeDirectory() {
51212
- try {
51213
- return os5.userInfo().homedir;
51214
- } catch {
51215
- return os5.homedir();
51216
- }
51217
- }
51218
- function isWithin(root, candidate) {
51219
- const relative = path6.relative(root, candidate);
51220
- return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
51221
- }
51222
- function validateRelativePath(value) {
51223
- if (value.includes("\0")) {
51224
- throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
51225
- }
51226
- const explicitNativePosixPath = process.platform !== "win32" && value.startsWith("./");
51227
- if (path6.isAbsolute(value) || path6.posix.isAbsolute(value) || !explicitNativePosixPath && path6.win32.isAbsolute(value)) {
51228
- throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
51229
- }
51230
- const segments = explicitNativePosixPath ? value.split(/\/+/).filter(Boolean) : value.split(/[\\/]+/);
51231
- if (segments.includes("..")) {
51232
- throw new WorkerError("path_traversal", "Parent path traversal is not allowed.");
51233
- }
51234
- return value === "" ? "." : value;
51235
- }
51236
- async function canonicalizeRoot(candidate) {
51237
- const root = await realpath(path6.resolve(candidate)).catch((error46) => {
51238
- if (error46.code === "ENOENT") {
51239
- throw new WorkerError("root_not_found", "The workspace directory does not exist.");
51240
- }
51241
- throw error46;
51242
- });
51243
- const rootStat = await stat2(root);
51244
- if (!rootStat.isDirectory()) {
51245
- throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
51246
- }
51247
- const filesystemRoot = path6.parse(root).root;
51248
- const homes = await Promise.all(
51249
- [os5.homedir(), accountHomeDirectory()].map(
51250
- async (home) => await realpath(home).catch(() => path6.resolve(home))
51251
- )
51252
- );
51253
- const isHomeDirectory = homes.some((home) => samePath(root, home));
51254
- if (samePath(root, filesystemRoot) || isHomeDirectory) {
51255
- const kind = isHomeDirectory ? "your home directory" : "a filesystem root";
51256
- throw new WorkerError(
51257
- "broad_root_refused",
51258
- `The selected root is ${kind}, which Glossa will not expose. Choose a project directory instead.`
51259
- );
51260
- }
51261
- return root;
51262
- }
51263
- var PathPolicy = class _PathPolicy {
51264
- constructor(root) {
51265
- this.root = root;
51266
- }
51267
- root;
51268
- static async create(candidate) {
51269
- return new _PathPolicy(await canonicalizeRoot(candidate));
51270
- }
51271
- async resolveExisting(relativePath) {
51272
- const lexical = this.resolveLexical(relativePath);
51273
- await this.rejectLinkedComponents(lexical);
51274
- const canonical = await realpath(lexical).catch((error46) => {
51275
- if (error46.code === "ENOENT") {
51276
- throw new WorkerError("path_not_found", "The requested path does not exist.");
51277
- }
51278
- throw error46;
51279
- });
51280
- if (!isWithin(this.root, canonical)) {
51281
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51282
- }
51283
- return canonical;
51284
- }
51285
- async resolveDiscoveredExisting(candidate) {
51286
- const lexical = path6.resolve(candidate);
51287
- if (!isWithin(this.root, lexical)) {
51288
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51289
- }
51290
- await this.rejectLinkedComponents(lexical);
51291
- const canonical = await realpath(lexical).catch((error46) => {
51292
- if (error46.code === "ENOENT") {
51293
- throw new WorkerError("path_not_found", "The requested path does not exist.");
51294
- }
51295
- throw error46;
51296
- });
51297
- if (!isWithin(this.root, canonical)) {
51298
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51299
- }
51300
- return canonical;
51301
- }
51302
- async resolveWritableFile(relativePath) {
51303
- const lexical = this.resolveLexical(relativePath);
51304
- const parent = path6.dirname(lexical);
51305
- await this.rejectLinkedComponents(parent);
51306
- const canonicalParent = await realpath(parent).catch((error46) => {
51307
- if (error46.code === "ENOENT") {
51308
- throw new WorkerError("parent_not_found", "The destination directory does not exist.");
51309
- }
51310
- throw error46;
51311
- });
51312
- if (!isWithin(this.root, canonicalParent)) {
51313
- throw new WorkerError("path_escape", "The destination escapes the exposed root.");
51314
- }
51315
- if (!(await stat2(canonicalParent)).isDirectory()) {
51316
- throw new WorkerError("not_directory", "The destination parent is not a directory.");
51317
- }
51318
- try {
51319
- const targetStat = await lstat2(lexical);
51320
- if (targetStat.isSymbolicLink()) {
51321
- throw new WorkerError("linked_path", "Writes through links are not allowed.");
51322
- }
51323
- if (targetStat.isDirectory()) {
51324
- throw new WorkerError("not_file", "The destination is a directory.");
51325
- }
51326
- const canonicalTarget = await realpath(lexical);
51327
- if (!isWithin(this.root, canonicalTarget)) {
51328
- throw new WorkerError("path_escape", "The destination escapes the exposed root.");
51329
- }
51330
- return canonicalTarget;
51331
- } catch (error46) {
51332
- if (error46 instanceof WorkerError) throw error46;
51333
- if (error46.code !== "ENOENT") throw error46;
51334
- }
51335
- return path6.join(canonicalParent, path6.basename(lexical));
51336
- }
51337
- resolveLexical(relativePath) {
51338
- const validated = validateRelativePath(relativePath);
51339
- const candidate = path6.resolve(this.root, validated);
51340
- if (!isWithin(this.root, candidate)) {
51341
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51342
- }
51343
- return candidate;
51344
- }
51345
- async rejectLinkedComponents(candidate) {
51346
- if (!isWithin(this.root, candidate)) {
51347
- throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
51348
- }
51349
- const relative = path6.relative(this.root, candidate);
51350
- if (!relative) return;
51351
- let current = this.root;
51352
- for (const segment of relative.split(path6.sep)) {
51353
- current = path6.join(current, segment);
51354
- try {
51355
- const currentStat = await lstat2(current);
51356
- if (currentStat.isSymbolicLink()) {
51357
- throw new WorkerError(
51358
- "linked_path",
51359
- "Symlink and junction paths are not allowed."
51360
- );
51361
- }
51362
- } catch (error46) {
51363
- if (error46 instanceof WorkerError) throw error46;
51364
- if (error46.code === "ENOENT") return;
51365
- throw error46;
51366
- }
51367
- }
51368
- }
51369
- };
51370
-
51371
51813
  // src/worker/local-worker.ts
51372
51814
  function restrictedDataError2() {
51373
51815
  return new WorkerError(
@@ -51402,6 +51844,14 @@ function jobInputContainsRestrictedData(job) {
51402
51844
  path: job.path,
51403
51845
  edits: job.edits
51404
51846
  });
51847
+ case "make_directory":
51848
+ case "delete_path":
51849
+ return containsRestrictedAuthenticationData(job.path);
51850
+ case "move_path":
51851
+ return containsRestrictedAuthenticationData({
51852
+ source: job.source,
51853
+ destination: job.destination
51854
+ });
51405
51855
  case "run_command":
51406
51856
  return containsRestrictedAuthenticationData({
51407
51857
  argv: job.argv,
@@ -51409,12 +51859,13 @@ function jobInputContainsRestrictedData(job) {
51409
51859
  stdin: job.stdin
51410
51860
  });
51411
51861
  case "get_command":
51862
+ case "read_command_output":
51412
51863
  case "cancel_command":
51413
51864
  return false;
51414
51865
  }
51415
51866
  }
51416
51867
  function resultMayContainRestrictedData(job) {
51417
- return job.type === "read_file" || job.type === "list_files" || job.type === "search_text" || job.type === "read_file_range" || job.type === "edit_file" || job.type === "run_command" || job.type === "get_command" || job.type === "cancel_command";
51868
+ return job.type === "read_file" || job.type === "list_files" || job.type === "search_text" || job.type === "read_file_range" || job.type === "edit_file" || job.type === "run_command" || job.type === "get_command" || job.type === "read_command_output" || job.type === "cancel_command";
51418
51869
  }
51419
51870
  var LocalWorker = class _LocalWorker {
51420
51871
  constructor(accessProfile, policy, files, commands) {
@@ -51439,13 +51890,13 @@ var LocalWorker = class _LocalWorker {
51439
51890
  async handle(job) {
51440
51891
  try {
51441
51892
  const permissions = workerPermissions(this.accessProfile);
51442
- if ((job.type === "write_file" || job.type === "edit_file") && !permissions.writeFiles) {
51893
+ if ((job.type === "write_file" || job.type === "edit_file" || job.type === "make_directory" || job.type === "delete_path" || job.type === "move_path") && !permissions.writeFiles) {
51443
51894
  throw new WorkerError(
51444
51895
  "write_access_disabled",
51445
51896
  "This worker was started without file-write access."
51446
51897
  );
51447
51898
  }
51448
- if ((job.type === "run_command" || job.type === "get_command" || job.type === "cancel_command") && !permissions.runCommands) {
51899
+ if ((job.type === "run_command" || job.type === "get_command" || job.type === "read_command_output" || job.type === "cancel_command") && !permissions.runCommands) {
51449
51900
  throw new WorkerError(
51450
51901
  "command_access_disabled",
51451
51902
  "This worker was started without system-command access."
@@ -51470,9 +51921,12 @@ var LocalWorker = class _LocalWorker {
51470
51921
  value = await this.files.searchText({
51471
51922
  query: job.query,
51472
51923
  ...job.path ? { path: job.path } : {},
51924
+ ...job.matchMode === void 0 ? {} : { matchMode: job.matchMode },
51473
51925
  ...job.caseSensitive === void 0 ? {} : { caseSensitive: job.caseSensitive },
51474
51926
  ...job.maxResults === void 0 ? {} : { maxResults: job.maxResults },
51475
51927
  ...job.extensions ? { extensions: job.extensions } : {},
51928
+ ...job.includeGlobs ? { includeGlobs: job.includeGlobs } : {},
51929
+ ...job.excludeGlobs ? { excludeGlobs: job.excludeGlobs } : {},
51476
51930
  timeoutMs: job.timeoutMs
51477
51931
  });
51478
51932
  break;
@@ -51503,6 +51957,15 @@ var LocalWorker = class _LocalWorker {
51503
51957
  );
51504
51958
  break;
51505
51959
  }
51960
+ case "make_directory":
51961
+ value = await this.files.makeDirectory(job.path, job.recursive);
51962
+ break;
51963
+ case "delete_path":
51964
+ value = await this.files.deletePath(job.path, job.recursive);
51965
+ break;
51966
+ case "move_path":
51967
+ value = await this.files.movePath(job.source, job.destination);
51968
+ break;
51506
51969
  case "run_command":
51507
51970
  value = await this.commands.start({
51508
51971
  ...job.argv ? { argv: job.argv } : {},
@@ -51519,6 +51982,14 @@ var LocalWorker = class _LocalWorker {
51519
51982
  job.afterSequence
51520
51983
  );
51521
51984
  break;
51985
+ case "read_command_output":
51986
+ value = await this.commands.readOutput(
51987
+ job.commandId,
51988
+ job.stream,
51989
+ job.offset,
51990
+ job.maxBytes
51991
+ );
51992
+ break;
51522
51993
  case "cancel_command":
51523
51994
  value = await this.commands.cancel(job.commandId);
51524
51995
  break;
@@ -51556,6 +52027,14 @@ var DeviceRejectedError = class extends Error {
51556
52027
  this.name = "DeviceRejectedError";
51557
52028
  }
51558
52029
  };
52030
+ var RelayAccessProfileUnsupportedError = class extends Error {
52031
+ constructor(profile) {
52032
+ super(
52033
+ `The relay needs an update before it can represent ${profile} access. Update the relay before reconnecting this workspace.`
52034
+ );
52035
+ this.name = "RelayAccessProfileUnsupportedError";
52036
+ }
52037
+ };
51559
52038
  var RelayResponseError = class extends Error {
51560
52039
  constructor(status) {
51561
52040
  super(`The relay returned HTTP ${status}.`);
@@ -51581,6 +52060,7 @@ function supportsCapability(value, capability) {
51581
52060
  function jobLane(job) {
51582
52061
  switch (job.type) {
51583
52062
  case "get_command":
52063
+ case "read_command_output":
51584
52064
  return "status";
51585
52065
  case "cancel_command":
51586
52066
  return "cancel";
@@ -51591,14 +52071,20 @@ function jobLane(job) {
51591
52071
  return "read";
51592
52072
  case "write_file":
51593
52073
  case "edit_file":
52074
+ case "make_directory":
52075
+ case "delete_path":
52076
+ case "move_path":
51594
52077
  case "run_command":
51595
52078
  return "mutation";
51596
52079
  }
51597
52080
  }
51598
- function acceptedJobTypes(counts, total, structuredReads) {
52081
+ function acceptedJobTypes(counts, total, structuredReads, structuredMutations, commandOutputRanges) {
51599
52082
  if (total >= MAX_CONCURRENT_JOBS) return [];
51600
52083
  const accepted = [];
51601
- if (counts.status < 1) accepted.push("get_command");
52084
+ if (counts.status < 1) {
52085
+ accepted.push("get_command");
52086
+ if (commandOutputRanges) accepted.push("read_command_output");
52087
+ }
51602
52088
  if (counts.cancel < 1) accepted.push("cancel_command");
51603
52089
  if (counts.read < 2) {
51604
52090
  accepted.push("read_file");
@@ -51608,6 +52094,9 @@ function acceptedJobTypes(counts, total, structuredReads) {
51608
52094
  }
51609
52095
  if (counts.mutation < 1) {
51610
52096
  accepted.push("write_file", "edit_file", "run_command");
52097
+ if (structuredMutations) {
52098
+ accepted.push("make_directory", "delete_path", "move_path");
52099
+ }
51611
52100
  }
51612
52101
  return accepted;
51613
52102
  }
@@ -51685,7 +52174,9 @@ var RemoteWorker = class {
51685
52174
  await this.#pollGeneration(session);
51686
52175
  } catch (error46) {
51687
52176
  if (this.#signal.aborted) return;
51688
- if (error46 instanceof DeviceRejectedError) throw error46;
52177
+ if (error46 instanceof DeviceRejectedError || error46 instanceof RelayAccessProfileUnsupportedError) {
52178
+ throw error46;
52179
+ }
51689
52180
  const delay4 = reconnectDelayMs(
51690
52181
  failures,
51691
52182
  this.#random,
@@ -51712,6 +52203,25 @@ var RemoteWorker = class {
51712
52203
  }
51713
52204
  }
51714
52205
  async #register() {
52206
+ const currentBody = {
52207
+ workerId: this.#workerId,
52208
+ capabilities: {
52209
+ commandProgress: true,
52210
+ concurrentJobs: true,
52211
+ structuredReads: true,
52212
+ structuredMutations: true,
52213
+ commandOutputRanges: true
52214
+ }
52215
+ };
52216
+ const mutationBody = {
52217
+ workerId: this.#workerId,
52218
+ capabilities: {
52219
+ commandProgress: true,
52220
+ concurrentJobs: true,
52221
+ structuredReads: true,
52222
+ structuredMutations: true
52223
+ }
52224
+ };
51715
52225
  const structuredBody = {
51716
52226
  workerId: this.#workerId,
51717
52227
  capabilities: {
@@ -51724,60 +52234,74 @@ var RemoteWorker = class {
51724
52234
  workerId: this.#workerId,
51725
52235
  capabilities: { commandProgress: true, concurrentJobs: true }
51726
52236
  };
51727
- const versionedStructuredBody = this.#workerVersion ? { ...structuredBody, workerVersion: this.#workerVersion } : void 0;
51728
- const preferredProfileBody = this.#accessProfile ? {
51729
- ...versionedStructuredBody ?? structuredBody,
51730
- accessProfile: this.#accessProfile,
51731
- ...this.#workspaceLabel ? { workspaceLabel: this.#workspaceLabel } : {}
51732
- } : void 0;
51733
- const attempts = [
51734
- ...preferredProfileBody ? [{ body: preferredProfileBody, legacyRelay: false }] : [],
51735
- ...versionedStructuredBody && this.#workspaceLabel ? [{
51736
- body: {
51737
- ...versionedStructuredBody,
51738
- workspaceLabel: this.#workspaceLabel
51739
- },
51740
- legacyRelay: false
51741
- }] : [],
51742
- ...this.#workspaceLabel ? [{
51743
- body: {
51744
- ...structuredBody,
51745
- workspaceLabel: this.#workspaceLabel
51746
- },
51747
- legacyRelay: false
51748
- }] : [],
51749
- ...versionedStructuredBody ? [{ body: versionedStructuredBody, legacyRelay: false }] : [],
51750
- {
51751
- body: structuredBody,
51752
- legacyRelay: false
51753
- },
51754
- ...this.#workspaceLabel ? [{
51755
- body: {
51756
- ...concurrentBody,
51757
- workspaceLabel: this.#workspaceLabel
51758
- },
51759
- legacyRelay: false
51760
- }] : [],
52237
+ const capabilityBodies = [
52238
+ currentBody,
52239
+ mutationBody,
52240
+ structuredBody,
52241
+ concurrentBody,
51761
52242
  {
51762
- body: concurrentBody,
51763
- legacyRelay: false
52243
+ workerId: this.#workerId,
52244
+ capabilities: { commandProgress: true }
51764
52245
  },
52246
+ { workerId: this.#workerId }
52247
+ ];
52248
+ const metadataPhases = [
51765
52249
  {
51766
- body: {
51767
- workerId: this.#workerId,
51768
- capabilities: { commandProgress: true }
51769
- },
51770
- legacyRelay: false
52250
+ includeVersion: Boolean(this.#workerVersion),
52251
+ includeLabel: Boolean(this.#workspaceLabel)
51771
52252
  },
51772
- { body: { workerId: this.#workerId }, legacyRelay: false },
51773
- { body: {}, legacyRelay: true }
52253
+ ...this.#workspaceLabel ? [{
52254
+ includeVersion: Boolean(this.#workerVersion),
52255
+ includeLabel: false
52256
+ }] : [],
52257
+ ...this.#workerVersion && this.#workspaceLabel ? [{ includeVersion: false, includeLabel: true }] : [],
52258
+ { includeVersion: false, includeLabel: false }
51774
52259
  ];
51775
- for (const [index, attempt] of attempts.entries()) {
52260
+ const attempts = [];
52261
+ const seenAttempts = /* @__PURE__ */ new Set();
52262
+ const pushAttempt = (base, includeProfile, includeVersion, includeLabel, legacyRelay = false) => {
52263
+ const body = {
52264
+ ...base,
52265
+ ...includeProfile && this.#accessProfile ? { accessProfile: this.#accessProfile } : {},
52266
+ ...includeVersion && this.#workerVersion ? { workerVersion: this.#workerVersion } : {},
52267
+ ...includeLabel && this.#workspaceLabel ? { workspaceLabel: this.#workspaceLabel } : {}
52268
+ };
52269
+ const key = JSON.stringify(body);
52270
+ if (seenAttempts.has(key)) return;
52271
+ seenAttempts.add(key);
52272
+ attempts.push({ body, legacyRelay, profileIncluded: includeProfile });
52273
+ };
52274
+ if (this.#accessProfile) {
52275
+ for (const metadata of metadataPhases) {
52276
+ for (const body of capabilityBodies) {
52277
+ pushAttempt(
52278
+ body,
52279
+ true,
52280
+ metadata.includeVersion,
52281
+ metadata.includeLabel
52282
+ );
52283
+ }
52284
+ }
52285
+ }
52286
+ if (this.#accessProfile === void 0 || this.#accessProfile === "system") {
52287
+ for (const metadata of metadataPhases) {
52288
+ for (const body of capabilityBodies) {
52289
+ pushAttempt(
52290
+ body,
52291
+ false,
52292
+ metadata.includeVersion,
52293
+ metadata.includeLabel
52294
+ );
52295
+ }
52296
+ }
52297
+ attempts.push({ body: {}, legacyRelay: true, profileIncluded: false });
52298
+ }
52299
+ for (const attempt of attempts) {
51776
52300
  let response;
51777
52301
  try {
51778
52302
  response = await this.#post("/device/register", attempt.body);
51779
52303
  } catch (error46) {
51780
- if (error46 instanceof RelayResponseError && error46.status === 400 && index < attempts.length - 1) {
52304
+ if (error46 instanceof RelayResponseError && error46.status === 400) {
51781
52305
  continue;
51782
52306
  }
51783
52307
  throw error46;
@@ -51795,11 +52319,16 @@ var RemoteWorker = class {
51795
52319
  legacyRelay: attempt.legacyRelay,
51796
52320
  concurrentJobs: !attempt.legacyRelay && supportsCapability(value, "concurrentJobs"),
51797
52321
  structuredReads: !attempt.legacyRelay && supportsCapability(value, "structuredReads"),
51798
- accessProfileAccepted: this.#accessProfile === void 0 || "accessProfile" in value && value.accessProfile === this.#accessProfile,
52322
+ structuredMutations: !attempt.legacyRelay && supportsCapability(value, "structuredMutations"),
52323
+ commandOutputRanges: !attempt.legacyRelay && supportsCapability(value, "commandOutputRanges"),
52324
+ accessProfileAccepted: this.#accessProfile === void 0 || "accessProfile" in value && value.accessProfile === this.#accessProfile || this.#accessProfile === "system" && !attempt.profileIncluded,
51799
52325
  workspaceLabelAccepted: this.#workspaceLabel === void 0 || "workspaceLabel" in value && value.workspaceLabel === this.#workspaceLabel,
51800
52326
  ...workerToken ? { workerToken } : {}
51801
52327
  };
51802
52328
  }
52329
+ if (this.#accessProfile && this.#accessProfile !== "system") {
52330
+ throw new RelayAccessProfileUnsupportedError(this.#accessProfile);
52331
+ }
51803
52332
  throw new Error("The relay rejected every supported registration shape.");
51804
52333
  }
51805
52334
  async #pollGeneration(session) {
@@ -51889,7 +52418,9 @@ var RemoteWorker = class {
51889
52418
  const acceptedTypes = acceptedJobTypes(
51890
52419
  counts,
51891
52420
  inFlight.size,
51892
- session.structuredReads
52421
+ session.structuredReads,
52422
+ session.structuredMutations,
52423
+ session.commandOutputRanges
51893
52424
  );
51894
52425
  if (acceptedTypes.length === 0) {
51895
52426
  await Promise.race(inFlight);
@@ -51919,7 +52450,9 @@ var RemoteWorker = class {
51919
52450
  const refreshedTypes = acceptedJobTypes(
51920
52451
  counts,
51921
52452
  inFlight.size,
51922
- session.structuredReads
52453
+ session.structuredReads,
52454
+ session.structuredMutations,
52455
+ session.commandOutputRanges
51923
52456
  );
51924
52457
  const newlyAcceptedTypes = refreshedTypes.filter(
51925
52458
  (type) => !acceptedTypes.includes(type)
@@ -52099,6 +52632,15 @@ function activitySafeJob(job) {
52099
52632
  newText: ""
52100
52633
  }]
52101
52634
  };
52635
+ case "make_directory":
52636
+ case "delete_path":
52637
+ return { ...job, path: "[restricted input blocked]" };
52638
+ case "move_path":
52639
+ return {
52640
+ ...job,
52641
+ source: "[restricted input blocked]",
52642
+ destination: "[restricted input blocked]"
52643
+ };
52102
52644
  case "run_command":
52103
52645
  return {
52104
52646
  type: "run_command",
@@ -52108,6 +52650,7 @@ function activitySafeJob(job) {
52108
52650
  ...job.waitMs === void 0 ? {} : { waitMs: job.waitMs }
52109
52651
  };
52110
52652
  case "get_command":
52653
+ case "read_command_output":
52111
52654
  case "cancel_command":
52112
52655
  return job;
52113
52656
  }
@@ -52541,6 +53084,16 @@ function summarizeJob(job) {
52541
53084
  `${job.edits.length} ${job.edits.length === 1 ? "edit" : "edits"}`,
52542
53085
  ...job.expectedSha256 ? ["guarded"] : []
52543
53086
  ]);
53087
+ case "make_directory":
53088
+ return pathSummary(job.path, job.recursive ? ["recursive"] : []);
53089
+ case "delete_path":
53090
+ return pathSummary(job.path, job.recursive ? ["recursive"] : []);
53091
+ case "move_path":
53092
+ return {
53093
+ target: `${quoteActivityInput(job.source)} \u2192 ${quoteActivityInput(job.destination)}`,
53094
+ details: [],
53095
+ truncation: "middle"
53096
+ };
52544
53097
  case "run_command":
52545
53098
  return {
52546
53099
  target: job.argv ? `argv [${job.argv.map(quoteActivityInput).join(", ")}]` : `shell ${quoteActivityInput(job.shellCommand ?? "")}`,
@@ -52560,6 +53113,15 @@ function summarizeJob(job) {
52560
53113
  ],
52561
53114
  truncation: "middle"
52562
53115
  };
53116
+ case "read_command_output":
53117
+ return {
53118
+ target: `command ${job.commandId} ${job.stream}`,
53119
+ details: [
53120
+ ...job.offset === void 0 ? [] : [`offset ${job.offset}`],
53121
+ ...job.maxBytes === void 0 ? [] : [`max ${job.maxBytes} bytes`]
53122
+ ],
53123
+ truncation: "middle"
53124
+ };
52563
53125
  case "cancel_command":
52564
53126
  return {
52565
53127
  target: `command ${job.commandId}`,
@@ -53504,7 +54066,7 @@ import path8 from "node:path";
53504
54066
 
53505
54067
  // src/update-lock.ts
53506
54068
  import { randomUUID as randomUUID4 } from "node:crypto";
53507
- import { mkdir as mkdir3, readdir, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
54069
+ import { mkdir as mkdir4, readdir, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
53508
54070
  import path7 from "node:path";
53509
54071
  var UPDATE_SUFFIX = ".update";
53510
54072
  var SESSION_SUFFIX = ".session";
@@ -53552,7 +54114,7 @@ async function activeSessionFiles(directory) {
53552
54114
  return await activeLeaseFiles(directory, SESSION_SUFFIX);
53553
54115
  }
53554
54116
  async function createUpdateLock(directory) {
53555
- await mkdir3(directory, { recursive: true, mode: 448 });
54117
+ await mkdir4(directory, { recursive: true, mode: 448 });
53556
54118
  const lockFile = path7.join(
53557
54119
  directory,
53558
54120
  `${process.pid}-${randomUUID4()}${UPDATE_SUFFIX}`
@@ -53587,7 +54149,7 @@ async function withUpdateLease(action, directory = updateRuntimeDirectory()) {
53587
54149
  }
53588
54150
  }
53589
54151
  async function withWorkspaceLease(action, directory = updateRuntimeDirectory()) {
53590
- await mkdir3(directory, { recursive: true, mode: 448 });
54152
+ await mkdir4(directory, { recursive: true, mode: 448 });
53591
54153
  if (await updateLockIsActive(directory)) {
53592
54154
  throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
53593
54155
  }
@@ -53725,14 +54287,25 @@ async function fetchText(url2, fetchImpl, version2) {
53725
54287
  if (!response.ok) throw new Error(`Glossa could not download ${url2} (HTTP ${response.status}).`);
53726
54288
  return await response.text();
53727
54289
  }
54290
+ function npmInstallInvocation(version2, platform2 = process.platform, commandShell = process.env.ComSpec ?? "cmd.exe") {
54291
+ const normalizedVersion = import_semver.default.valid(version2);
54292
+ if (!normalizedVersion) throw new Error(`Glossa version ${version2} is invalid.`);
54293
+ const packageSpec = `${PACKAGE_NAME}@${normalizedVersion}`;
54294
+ if (platform2 === "win32") {
54295
+ return {
54296
+ command: commandShell,
54297
+ args: ["/d", "/s", "/c", `npm.cmd install --global ${packageSpec}`]
54298
+ };
54299
+ }
54300
+ return {
54301
+ command: "npm",
54302
+ args: ["install", "--global", packageSpec]
54303
+ };
54304
+ }
53728
54305
  async function runNpmInstall(version2) {
53729
- const command = process.platform === "win32" ? "npm.cmd" : "npm";
54306
+ const { command, args } = npmInstallInvocation(version2);
53730
54307
  await new Promise((resolve, reject) => {
53731
- const child = spawn3(
53732
- command,
53733
- ["install", "--global", `${PACKAGE_NAME}@${version2}`],
53734
- { stdio: "inherit" }
53735
- );
54308
+ const child = spawn3(command, args, { stdio: "inherit" });
53736
54309
  child.once("error", reject);
53737
54310
  child.once("close", (status) => {
53738
54311
  if (status === 0) resolve();
@@ -53795,7 +54368,7 @@ async function installUpdate(info, distribution, options = {}) {
53795
54368
  }
53796
54369
 
53797
54370
  // src/update-state.ts
53798
- import { chmod as chmod4, mkdir as mkdir4, readFile as readFile4, rename as rename3, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
54371
+ import { chmod as chmod4, mkdir as mkdir5, readFile as readFile4, rename as rename3, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
53799
54372
  import path9 from "node:path";
53800
54373
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
53801
54374
  function defaultUpdateChannel(version2) {
@@ -53838,7 +54411,7 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
53838
54411
  async function saveUpdateState(state, file2 = updateStateFile()) {
53839
54412
  const directory = path9.dirname(file2);
53840
54413
  const temporary = `${file2}.${process.pid}.tmp`;
53841
- await mkdir4(directory, { recursive: true, mode: 448 });
54414
+ await mkdir5(directory, { recursive: true, mode: 448 });
53842
54415
  try {
53843
54416
  await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
53844
54417
  `, {
@@ -53876,14 +54449,14 @@ function isUpdateCheckDue(lastCheckedAt, now = Date.now()) {
53876
54449
  }
53877
54450
 
53878
54451
  // src/usage-store.ts
53879
- import { appendFile, chmod as chmod5, mkdir as mkdir5, readFile as readFile5 } from "node:fs/promises";
54452
+ import { appendFile, chmod as chmod5, mkdir as mkdir6, readFile as readFile5 } from "node:fs/promises";
53880
54453
  import path10 from "node:path";
53881
54454
  function usageFile() {
53882
54455
  return path10.join(configDirectory(), "usage.jsonl");
53883
54456
  }
53884
54457
  async function recordUsageEvent(event, file2 = usageFile()) {
53885
54458
  const directory = path10.dirname(file2);
53886
- await mkdir5(directory, { recursive: true, mode: 448 });
54459
+ await mkdir6(directory, { recursive: true, mode: 448 });
53887
54460
  await appendFile(file2, `${JSON.stringify(event)}
53888
54461
  `, {
53889
54462
  encoding: "utf8",
@@ -53927,7 +54500,7 @@ async function selectExposureRoot(explicitPath, cwd2 = process.cwd()) {
53927
54500
 
53928
54501
  // src/worker/workspace-lease.ts
53929
54502
  import { createHash as createHash3 } from "node:crypto";
53930
- import { lstat as lstat3, mkdir as mkdir6, rm as rm6, stat as stat3 } from "node:fs/promises";
54503
+ import { lstat as lstat3, mkdir as mkdir7, rm as rm6, stat as stat3 } from "node:fs/promises";
53931
54504
  import net from "node:net";
53932
54505
  import path12 from "node:path";
53933
54506
  import { setTimeout as delay3 } from "node:timers/promises";
@@ -53958,7 +54531,7 @@ function defaultLeaseDirectory() {
53958
54531
  return path12.join(accountHomeDirectory(), ".glossa-workspace-leases");
53959
54532
  }
53960
54533
  async function ensureLeaseDirectory(directory) {
53961
- await mkdir6(directory, { recursive: true, mode: 448 });
54534
+ await mkdir7(directory, { recursive: true, mode: 448 });
53962
54535
  const directoryStat = await lstat3(directory);
53963
54536
  if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
53964
54537
  throw new Error("The Glossa workspace lease location is not a private directory.");
@@ -53982,7 +54555,7 @@ async function acquireGuard(directory, identity) {
53982
54555
  const deadline = Date.now() + GUARD_TIMEOUT_MS;
53983
54556
  while (true) {
53984
54557
  try {
53985
- await mkdir6(guard, { mode: 448 });
54558
+ await mkdir7(guard, { mode: 448 });
53986
54559
  return async () => await rm6(guard, { recursive: true, force: true });
53987
54560
  } catch (error46) {
53988
54561
  if (error46.code !== "EEXIST") throw error46;
@@ -54077,10 +54650,10 @@ async function acquireWorkspaceLease(root, options = {}) {
54077
54650
  };
54078
54651
  } catch (error46) {
54079
54652
  await closeServer(server);
54080
- if (error46.code !== "EADDRINUSE") throw error46;
54081
54653
  if (await endpointIsActive(endpoint3)) {
54082
54654
  throw new WorkspaceAlreadyActiveError();
54083
54655
  }
54656
+ if (error46.code !== "EADDRINUSE") throw error46;
54084
54657
  if (process.platform !== "win32") await rm6(endpoint3, { force: true });
54085
54658
  }
54086
54659
  }
@@ -54091,7 +54664,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54091
54664
  }
54092
54665
 
54093
54666
  // src/main.ts
54094
- var VERSION = "0.1.1";
54667
+ var VERSION = "0.2.0-beta.2";
54095
54668
  var DISTRIBUTION = "npm";
54096
54669
  var HELP = `Glossa ${VERSION}
54097
54670