@ariobarin/glossa 0.1.0-beta.10 → 0.1.0-beta.12

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 (3) hide show
  1. package/README.md +17 -10
  2. package/dist/app.js +2662 -1549
  3. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -5,6 +5,34 @@ var __export = (target, all) => {
5
5
  __defProp(target, name, { get: all[name], enumerable: true });
6
6
  };
7
7
 
8
+ // src/auth-config.ts
9
+ var DEFAULT_AUTH_CONFIG = {
10
+ issuer: "https://dev-fl2h5xhp6umeh74m.us.auth0.com/",
11
+ clientId: "9mwnK9nTAd8q1kxnKIZxC1wodxzfWHg5",
12
+ audience: "https://mcp.glossa.sh/",
13
+ scope: "openid profile offline_access glossa:device"
14
+ };
15
+ function configuredValue(environment, name, fallback) {
16
+ const value = environment[name]?.trim();
17
+ return value || fallback;
18
+ }
19
+ function loadAuthConfig(environment = process.env) {
20
+ return {
21
+ issuer: configuredValue(environment, "GLOSSA_AUTH0_ISSUER", DEFAULT_AUTH_CONFIG.issuer),
22
+ clientId: configuredValue(
23
+ environment,
24
+ "GLOSSA_AUTH0_CLI_CLIENT_ID",
25
+ DEFAULT_AUTH_CONFIG.clientId
26
+ ),
27
+ audience: configuredValue(
28
+ environment,
29
+ "GLOSSA_AUTH0_AUDIENCE",
30
+ DEFAULT_AUTH_CONFIG.audience
31
+ ),
32
+ scope: DEFAULT_AUTH_CONFIG.scope
33
+ };
34
+ }
35
+
8
36
  // src/config-store.ts
9
37
  import path2 from "node:path";
10
38
 
@@ -239,6 +267,21 @@ var SessionExpiredError = class extends Error {
239
267
  this.name = "SessionExpiredError";
240
268
  }
241
269
  };
270
+ function accessTokenSubject(credentials) {
271
+ try {
272
+ const parts = credentials.accessToken.split(".");
273
+ if (parts.length !== 3) throw new Error();
274
+ const payload = JSON.parse(
275
+ Buffer.from(parts[1], "base64url").toString("utf8")
276
+ );
277
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
278
+ throw new Error();
279
+ }
280
+ return payload.sub;
281
+ } catch {
282
+ throw new Error("Glossa could not identify the signed-in account.");
283
+ }
284
+ }
242
285
  function sessionExpiredError() {
243
286
  return new SessionExpiredError();
244
287
  }
@@ -331,34 +374,6 @@ async function loadUserProfile(credentials, dependencies = {}) {
331
374
  return { credentials: current, profile: parseProfile(await response.json()) };
332
375
  }
333
376
 
334
- // src/auth-config.ts
335
- var DEFAULT_AUTH_CONFIG = {
336
- issuer: "https://dev-fl2h5xhp6umeh74m.us.auth0.com/",
337
- clientId: "9mwnK9nTAd8q1kxnKIZxC1wodxzfWHg5",
338
- audience: "https://mcp.glossa.sh/",
339
- scope: "openid profile offline_access glossa:device"
340
- };
341
- function configuredValue(environment, name, fallback) {
342
- const value = environment[name]?.trim();
343
- return value || fallback;
344
- }
345
- function loadAuthConfig(environment = process.env) {
346
- return {
347
- issuer: configuredValue(environment, "GLOSSA_AUTH0_ISSUER", DEFAULT_AUTH_CONFIG.issuer),
348
- clientId: configuredValue(
349
- environment,
350
- "GLOSSA_AUTH0_CLI_CLIENT_ID",
351
- DEFAULT_AUTH_CONFIG.clientId
352
- ),
353
- audience: configuredValue(
354
- environment,
355
- "GLOSSA_AUTH0_AUDIENCE",
356
- DEFAULT_AUTH_CONFIG.audience
357
- ),
358
- scope: DEFAULT_AUTH_CONFIG.scope
359
- };
360
- }
361
-
362
377
  // src/device-flow.ts
363
378
  import { setTimeout as delay } from "node:timers/promises";
364
379
 
@@ -500,30 +515,34 @@ function credentialsMatchLoginOptions(credentials, options) {
500
515
  Boolean(credentials.refreshToken)
501
516
  );
502
517
  }
503
- async function ensureSignedIn(options, dependencies = {}) {
518
+ async function signedInSession(options, dependencies = {}) {
504
519
  const load = dependencies.loadCredentials ?? loadCredentials;
505
520
  const validate = dependencies.validCredentials ?? validCredentials;
506
521
  const login = dependencies.loginWithDeviceFlow ?? loginWithDeviceFlow;
507
522
  const loaded = await load();
508
523
  if (loaded && credentialsMatchLoginOptions(loaded.credentials, options)) {
509
524
  try {
510
- await validate(
525
+ const credentials = await validate(
511
526
  loaded.credentials,
512
527
  options.signal ? { signal: options.signal } : {}
513
528
  );
514
- return false;
529
+ return { credentials, loginPerformed: false };
515
530
  } catch (error46) {
516
531
  if (!(error46 instanceof SessionExpiredError)) throw error46;
517
532
  }
518
533
  }
519
534
  await login(options);
520
- return true;
535
+ const completed = await load();
536
+ if (!completed) throw new Error("Glossa could not load the completed login.");
537
+ return {
538
+ credentials: await validate(
539
+ completed.credentials,
540
+ options.signal ? { signal: options.signal } : {}
541
+ ),
542
+ loginPerformed: true
543
+ };
521
544
  }
522
545
 
523
- // src/cli-options.ts
524
- import { existsSync } from "node:fs";
525
- import path3 from "node:path";
526
-
527
546
  // ../../node_modules/zod/v4/classic/external.js
528
547
  var external_exports = {};
529
548
  __export(external_exports, {
@@ -13070,12 +13089,20 @@ config(en_default());
13070
13089
  var MAX_TEXT_BYTES = 1024 * 1024;
13071
13090
  var MAX_EDIT_DIFF_BYTES = 128 * 1024;
13072
13091
  var MAX_EDIT_OPERATIONS = 100;
13092
+ var MAX_COMMAND_OUTPUT_BYTES = 12 * 1024;
13073
13093
  var DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60 * 1e3;
13074
13094
  var MAX_COMMAND_TIMEOUT_MS = 60 * 60 * 1e3;
13075
13095
  var DEFAULT_COMMAND_FAST_WAIT_MS = 750;
13076
13096
  var MAX_COMMAND_FAST_WAIT_MS = 5e3;
13077
13097
  var MAX_COMMAND_STATUS_WAIT_MS = 15e3;
13098
+ var MAX_LIST_FILES_RESULTS = 200;
13099
+ var MAX_SEARCH_TEXT_RESULTS = 100;
13100
+ var MAX_SEARCH_TEXT_SNIPPET_CHARS = 400;
13101
+ var MAX_READ_FILE_RANGE_LINES = 500;
13102
+ var MAX_READ_FILE_RANGE_BYTES = 64 * 1024;
13103
+ var MAX_STRUCTURED_READ_TIMEOUT_MS = 8e3;
13078
13104
  var deviceNameSchema = external_exports.string().trim().min(1).max(80).regex(/^[^\u0000-\u001f\u007f]+$/, "Device name contains control characters");
13105
+ var workspaceLabelSchema = external_exports.string().trim().min(1).max(80).regex(/^[^\u0000-\u001f\u007f]+$/, "Workspace label contains control characters");
13079
13106
  var relativePathSchema = external_exports.string().max(4096).describe("Path relative to the exposed workspace root. Absolute paths and parent traversal are rejected.");
13080
13107
  var boundedTextSchema = external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_TEXT_BYTES);
13081
13108
  var readFileRequestSchema = external_exports.object({
@@ -13085,6 +13112,41 @@ var readFileJobSchema = readFileRequestSchema.extend({
13085
13112
  type: external_exports.literal("read_file"),
13086
13113
  requestId: external_exports.string().uuid()
13087
13114
  });
13115
+ var structuredReadTimeoutSchema = external_exports.number().int().min(1).max(MAX_STRUCTURED_READ_TIMEOUT_MS);
13116
+ var listFilesCursorSchema = external_exports.string().max(4096).describe("Opaque cursor returned by an earlier list_files result.");
13117
+ var listFilesRequestSchema = external_exports.object({
13118
+ path: relativePathSchema.optional().describe("Directory relative to the exposed root. Defaults to the root."),
13119
+ recursive: external_exports.boolean().optional().describe("Whether to include descendants. Defaults to false."),
13120
+ cursor: listFilesCursorSchema.optional(),
13121
+ limit: external_exports.number().int().min(1).max(MAX_LIST_FILES_RESULTS).optional().describe("Maximum entries to return, from 1 through 200. Defaults to 100.")
13122
+ }).strict();
13123
+ var listFilesJobSchema = listFilesRequestSchema.extend({
13124
+ type: external_exports.literal("list_files"),
13125
+ requestId: external_exports.string().uuid(),
13126
+ timeoutMs: structuredReadTimeoutSchema
13127
+ });
13128
+ var searchTextRequestSchema = external_exports.object({
13129
+ 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."),
13130
+ path: relativePathSchema.optional().describe("File or directory relative to the exposed root. Defaults to the root."),
13131
+ caseSensitive: external_exports.boolean().optional().describe("Whether matching is case-sensitive. Defaults to false."),
13132
+ 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."),
13133
+ 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.")
13134
+ }).strict();
13135
+ var searchTextJobSchema = searchTextRequestSchema.extend({
13136
+ type: external_exports.literal("search_text"),
13137
+ requestId: external_exports.string().uuid(),
13138
+ timeoutMs: structuredReadTimeoutSchema
13139
+ });
13140
+ var readFileRangeRequestSchema = external_exports.object({
13141
+ path: relativePathSchema,
13142
+ startLine: external_exports.number().int().min(1).optional().describe("First one-based line to return. Defaults to 1."),
13143
+ lineCount: external_exports.number().int().min(1).max(MAX_READ_FILE_RANGE_LINES).optional().describe("Maximum complete lines to return, from 1 through 500. Defaults to 200.")
13144
+ }).strict();
13145
+ var readFileRangeJobSchema = readFileRangeRequestSchema.extend({
13146
+ type: external_exports.literal("read_file_range"),
13147
+ requestId: external_exports.string().uuid(),
13148
+ timeoutMs: structuredReadTimeoutSchema
13149
+ });
13088
13150
  var writeFileRequestSchema = external_exports.object({
13089
13151
  path: relativePathSchema,
13090
13152
  content: boundedTextSchema.describe("Complete UTF-8 text content that will replace the file."),
@@ -13138,7 +13200,8 @@ var runCommandJobSchema = runCommandRequestSchema.safeExtend({
13138
13200
  });
13139
13201
  var getCommandRequestSchema = external_exports.object({
13140
13202
  commandId: external_exports.string().uuid().describe("Command identifier returned by run_command."),
13141
- waitMs: external_exports.number().int().min(0).max(MAX_COMMAND_STATUS_WAIT_MS).optional().describe("Optional long-poll duration in milliseconds, from 0 through 15000.")
13203
+ waitMs: external_exports.number().int().min(0).max(MAX_COMMAND_STATUS_WAIT_MS).optional().describe("Optional long-poll duration in milliseconds, from 0 through 15000."),
13204
+ afterSequence: external_exports.number().int().min(0).optional().describe("Sequence returned by an earlier command result. When current, wait for output or status to change.")
13142
13205
  }).strict();
13143
13206
  var getCommandJobSchema = getCommandRequestSchema.extend({
13144
13207
  type: external_exports.literal("get_command"),
@@ -13153,6 +13216,9 @@ var cancelCommandJobSchema = cancelCommandRequestSchema.extend({
13153
13216
  });
13154
13217
  var workerJobSchema = external_exports.discriminatedUnion("type", [
13155
13218
  readFileJobSchema,
13219
+ listFilesJobSchema,
13220
+ searchTextJobSchema,
13221
+ readFileRangeJobSchema,
13156
13222
  writeFileJobSchema,
13157
13223
  editFileJobSchema,
13158
13224
  runCommandJobSchema,
@@ -13170,468 +13236,144 @@ var workerResultSchema = external_exports.object({
13170
13236
  }).optional()
13171
13237
  });
13172
13238
 
13173
- // src/completions.ts
13174
- var SUPPORTED_SHELLS = ["powershell", "bash", "zsh", "fish"];
13175
- var commands = [
13176
- "ui",
13177
- "start",
13178
- "status",
13179
- "doctor",
13180
- "devices",
13181
- "completions",
13182
- "update",
13183
- "upgrade",
13184
- "login",
13185
- "logout"
13186
- ];
13187
- function powershellScript() {
13188
- const list = commands.map((command) => `'${command}'`).join(", ");
13189
- return `# PowerShell completion for Glossa. Source it from your PowerShell profile.
13190
- Register-ArgumentCompleter -Native -CommandName glossa -ScriptBlock {
13191
- param($wordToComplete, $commandAst, $cursorPosition)
13192
- $commands = @(${list})
13193
- $elements = $commandAst.CommandElements
13194
- $last = $elements[$elements.Count - 1]
13195
- # Position of the argument being completed (1 = first argument after glossa).
13196
- # A trailing space starts a new argument; otherwise we complete the last token.
13197
- if ($cursorPosition -gt $last.Extent.EndOffset) {
13198
- $position = $elements.Count
13199
- } else {
13200
- $position = $elements.Count - 1
13201
- }
13202
- if ($position -eq 1) {
13203
- @($commands + '--help' + '--version') |
13204
- Where-Object { $_ -like "$wordToComplete*" } |
13205
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
13206
- return
13207
- }
13208
- $command = $elements[1].Value
13209
- if ($wordToComplete -like '-*') {
13210
- switch ($command) {
13211
- 'ui' {
13212
- @('--allow-broad-root', '--device-name') | Where-Object { $_ -like "$wordToComplete*" } |
13213
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_) }
13214
- }
13215
- 'start' {
13216
- @('--allow-broad-root', '--device-name') | Where-Object { $_ -like "$wordToComplete*" } |
13217
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_) }
13218
- }
13219
- 'status' {
13220
- @('--json') | Where-Object { $_ -like "$wordToComplete*" } |
13221
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_) }
13222
- }
13223
- 'doctor' {
13224
- @('--json') | Where-Object { $_ -like "$wordToComplete*" } |
13225
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_) }
13226
- }
13227
- 'devices' {
13228
- if ($position -eq 3 -and $elements[2].Value -eq 'list') {
13229
- @('--json') | Where-Object { $_ -like "$wordToComplete*" } |
13230
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_) }
13231
- }
13232
- }
13233
- 'logout' {
13234
- @('--browser') | Where-Object { $_ -like "$wordToComplete*" } |
13235
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterName', $_) }
13236
- }
13237
- }
13238
- return
13239
- }
13240
- if ($position -eq 2) {
13241
- switch ($command) {
13242
- 'devices' {
13243
- @('list', 'rename', 'revoke') | Where-Object { $_ -like "$wordToComplete*" } |
13244
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
13245
- }
13246
- 'completions' {
13247
- @('powershell', 'bash', 'zsh', 'fish') | Where-Object { $_ -like "$wordToComplete*" } |
13248
- ForEach-Object { [System.Management.Automation.CompletionResult]::new($_, $_, 'ParameterValue', $_) }
13249
- }
13250
- }
13251
- }
13252
- }
13253
- `;
13254
- }
13255
- function bashScript() {
13256
- return `# Bash completion for Glossa. Source it or install under /etc/bash_completion.d.
13257
- _glossa() {
13258
- local cur="\${COMP_WORDS[COMP_CWORD]}"
13259
- if [ "$COMP_CWORD" -eq 1 ]; then
13260
- COMPREPLY=( $(compgen -W "${commands.join(" ")} --help --version" -- "$cur") )
13261
- return
13262
- fi
13263
- local cmd="\${COMP_WORDS[1]}"
13264
- if [[ "$cur" == -* ]]; then
13265
- case "$cmd" in
13266
- ui|start) COMPREPLY=( $(compgen -W "--allow-broad-root --device-name" -- "$cur") ) ;;
13267
- status|doctor) COMPREPLY=( $(compgen -W "--json" -- "$cur") ) ;;
13268
- devices)
13269
- if [ "$COMP_CWORD" -eq 3 ] && [ "\${COMP_WORDS[2]}" = "list" ]; then
13270
- COMPREPLY=( $(compgen -W "--json" -- "$cur") )
13271
- fi ;;
13272
- logout) COMPREPLY=( $(compgen -W "--browser" -- "$cur") ) ;;
13273
- esac
13274
- return
13275
- fi
13276
- case "$cmd" in
13277
- devices)
13278
- if [ "$COMP_CWORD" -eq 2 ]; then
13279
- COMPREPLY=( $(compgen -W "list rename revoke" -- "$cur") )
13280
- fi ;;
13281
- completions)
13282
- if [ "$COMP_CWORD" -eq 2 ]; then
13283
- COMPREPLY=( $(compgen -W "powershell bash zsh fish" -- "$cur") )
13284
- fi ;;
13285
- ui|start) ;; # workspace directory: fall through to filename completion
13286
- esac
13287
- }
13288
- # -o default lets readline fall back to filename completion for workspace paths
13289
- # when the completion function produces no matches.
13290
- complete -o default -F _glossa glossa
13291
- `;
13292
- }
13293
- function zshScript() {
13294
- return `# Zsh completion for Glossa. Source this after compinit from your profile.
13295
- _glossa() {
13296
- local context state state_descr line
13297
- typeset -A opt_args
13298
- local -a glossa_commands
13299
- glossa_commands=(
13300
- 'ui:open the interactive session HUD'
13301
- 'start:expose a workspace'
13302
- 'status:show account, relay, and active workers'
13303
- 'doctor:check local and relay readiness'
13304
- 'devices:manage enrolled computers'
13305
- 'completions:emit a shell completion script'
13306
- 'update:update the current Glossa installation'
13307
- 'upgrade:alias for update'
13308
- 'login:ensure a Glossa session'
13309
- 'logout:remove local credentials'
13310
- )
13311
- _arguments -C '1:command or workspace:->commands' '*::argument:->args'
13312
- case "$state" in
13313
- commands)
13314
- _describe 'glossa command' glossa_commands
13315
- _files
13316
- ;;
13317
- args)
13318
- case $words[2] in
13319
- devices)
13320
- if (( CURRENT == 3 )); then
13321
- _values 'device action' list rename revoke
13322
- elif [[ $words[3] == list ]]; then
13323
- _arguments '--json[print machine-readable JSON]'
13324
- fi
13325
- ;;
13326
- completions) _arguments '2:shell:(powershell bash zsh fish)' ;;
13327
- ui|start)
13328
- _arguments '--allow-broad-root[allow home or drive roots]' '--device-name[name this computer on first enrollment]:device name:' '2:workspace:_directories'
13329
- ;;
13330
- status|doctor) _arguments '--json[print machine-readable JSON]' ;;
13331
- logout) _arguments '--browser[also sign out of the browser session]' ;;
13332
- esac
13333
- ;;
13334
- esac
13335
- }
13336
- compdef _glossa glossa
13337
- `;
13338
- }
13339
- function fishScript() {
13340
- const lines = [
13341
- "# Fish completion for Glossa. Source it or drop into ~/.config/fish/completions.",
13342
- // No global -f: the first argument may be a workspace directory, so fish
13343
- // should still offer files there.
13344
- ...commands.map(
13345
- (command) => `complete -c glossa -n '__fish_use_subcommand' -a '${command}'`
13346
- ),
13347
- "complete -c glossa -f -n '__fish_seen_subcommand_from devices; and test (count (commandline -opc)) -eq 2' -a 'list rename revoke'",
13348
- "complete -c glossa -f -n '__fish_seen_subcommand_from completions; and test (count (commandline -opc)) -eq 2' -a 'powershell bash zsh fish'",
13349
- "complete -c glossa -n '__fish_seen_subcommand_from ui start' -l allow-broad-root",
13350
- "complete -c glossa -n '__fish_seen_subcommand_from ui start' -l device-name -r",
13351
- "complete -c glossa -n '__fish_seen_subcommand_from status doctor' -l json",
13352
- "complete -c glossa -n '__fish_seen_subcommand_from devices; and contains -- list (commandline -opc)' -l json",
13353
- "complete -c glossa -n '__fish_seen_subcommand_from logout' -l browser",
13354
- ""
13355
- ];
13356
- return lines.join("\n");
13357
- }
13358
- function completionScript(shell) {
13359
- switch (shell) {
13360
- case "powershell":
13361
- return powershellScript();
13362
- case "bash":
13363
- return bashScript();
13364
- case "zsh":
13365
- return zshScript();
13366
- case "fish":
13367
- return fishScript();
13368
- }
13369
- }
13370
-
13371
13239
  // src/cli-options.ts
13372
13240
  var UsageError = class extends Error {
13373
13241
  };
13374
- var helpTopics = /* @__PURE__ */ new Set([
13375
- "ui",
13376
- "start",
13377
- "status",
13378
- "doctor",
13379
- "devices",
13242
+ var retiredCommands = /* @__PURE__ */ new Set([
13380
13243
  "completions",
13381
- "update",
13244
+ "doctor",
13382
13245
  "login",
13383
- "logout"
13246
+ "start",
13247
+ "update"
13384
13248
  ]);
13385
- function parseDeviceName(value) {
13386
- const parsed = deviceNameSchema.safeParse(value);
13387
- if (!parsed.success) {
13388
- throw new UsageError(
13389
- "Device names must be 1 to 80 characters with no control characters."
13390
- );
13391
- }
13392
- return parsed.data;
13393
- }
13394
- function parseWorkspaceCommand(command, args) {
13395
- if (args.includes("--help") || args.includes("-h")) {
13396
- return { command: "help", topic: command };
13397
- }
13249
+ function parseWorkspace(args) {
13398
13250
  let selectedPath;
13399
- let allowBroadRoot = false;
13400
- let deviceName;
13251
+ let label;
13401
13252
  let optionsEnded = false;
13402
13253
  for (let index = 0; index < args.length; index += 1) {
13403
13254
  const argument = args[index];
13404
13255
  if (!optionsEnded && argument === "--") {
13405
13256
  optionsEnded = true;
13406
- } else if (!optionsEnded && argument === "--allow-broad-root") {
13407
- allowBroadRoot = true;
13408
- } else if (!optionsEnded && argument === "--device-name") {
13257
+ } else if (!optionsEnded && argument === "--label") {
13258
+ if (label !== void 0) {
13259
+ throw new UsageError("Glossa accepts at most one workspace label.");
13260
+ }
13409
13261
  const value = args[index + 1];
13410
- if (value === void 0 || value.startsWith("-")) {
13411
- throw new UsageError("--device-name requires a value.");
13262
+ if (value === void 0 || value === "--") {
13263
+ throw new UsageError("Use --label <name>.");
13264
+ }
13265
+ const parsed = workspaceLabelSchema.safeParse(value);
13266
+ if (!parsed.success) {
13267
+ throw new UsageError("Workspace labels must be 1-80 printable characters.");
13412
13268
  }
13413
- deviceName = parseDeviceName(value);
13269
+ label = parsed.data;
13414
13270
  index += 1;
13415
- } else if (!optionsEnded && argument.startsWith("--device-name=")) {
13416
- deviceName = parseDeviceName(argument.slice("--device-name=".length));
13417
13271
  } else if (!optionsEnded && argument.startsWith("-")) {
13418
- throw new UsageError(`Unknown ${command} option: ${argument}`);
13272
+ throw new UsageError(`Unknown option: ${argument}`);
13419
13273
  } else if (selectedPath) {
13420
- throw new UsageError(`${command === "ui" ? "UI" : "Start"} accepts at most one directory.`);
13274
+ throw new UsageError("Glossa accepts at most one directory.");
13275
+ } else if (!optionsEnded && retiredCommands.has(argument)) {
13276
+ throw new UsageError(`The ${argument} command is no longer available.`);
13421
13277
  } else {
13422
13278
  selectedPath = argument;
13423
13279
  }
13424
13280
  }
13425
13281
  return {
13426
- command,
13282
+ command: "workspace",
13427
13283
  ...selectedPath ? { path: selectedPath } : {},
13428
- allowBroadRoot,
13429
- ...deviceName ? { deviceName } : {}
13284
+ ...label ? { label } : {}
13430
13285
  };
13431
13286
  }
13432
- function singleJsonOption(command, args) {
13433
- if (args.length === 0) return false;
13434
- if (args.length === 1 && args[0] === "--json") return true;
13435
- throw new UsageError(`${command} accepts only --json.`);
13436
- }
13437
13287
  function parseDevices(args) {
13438
- const [action, ...options] = args;
13439
- if (!action || action === "--help" || action === "-h") {
13440
- return { command: "help", topic: "devices" };
13441
- }
13442
- if (action === "list") {
13443
- return { command: "devices", action, json: singleJsonOption("Devices list", options) };
13444
- }
13445
- if (action === "rename" && options.length === 2) {
13446
- return { command: "devices", action, deviceId: options[0], name: options[1] };
13447
- }
13448
- if (action === "revoke" && options.length === 1) {
13449
- return { command: "devices", action, deviceId: options[0] };
13288
+ if (args[0] === "revoke" && args.length === 2) {
13289
+ return { command: "devices", action: "revoke", deviceId: args[1] };
13450
13290
  }
13451
- throw new UsageError("Use: glossa devices list, rename <id> <name>, or revoke <id>.");
13291
+ throw new UsageError("Use: glossa devices revoke <id>.");
13452
13292
  }
13453
- function likelyDirectory(value) {
13454
- return value === "." || value === ".." || path3.isAbsolute(value) || value.includes("/") || value.includes("\\") || existsSync(value);
13455
- }
13456
- var KNOWN_COMMANDS = [
13457
- "ui",
13458
- "start",
13459
- "status",
13460
- "doctor",
13461
- "devices",
13462
- "completions",
13463
- "update",
13464
- "upgrade",
13465
- "login",
13466
- "logout"
13467
- ];
13468
- function editDistance(a, b) {
13469
- const m = a.length;
13470
- const n = b.length;
13471
- if (m === 0) return n;
13472
- if (n === 0) return m;
13473
- let previous = Array.from({ length: n + 1 }, (_, index) => index);
13474
- for (let i = 1; i <= m; i += 1) {
13475
- const current = [i];
13476
- for (let j = 1; j <= n; j += 1) {
13477
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
13478
- current[j] = Math.min(
13479
- previous[j] + 1,
13480
- current[j - 1] + 1,
13481
- previous[j - 1] + cost
13482
- );
13483
- }
13484
- previous = current;
13485
- }
13486
- return previous[n];
13487
- }
13488
- function suggestCommand(input) {
13489
- const lower = input.toLowerCase();
13490
- if (lower.length >= 3) {
13491
- const prefixMatches = KNOWN_COMMANDS.filter((command) => command.startsWith(lower));
13492
- if (prefixMatches.length === 1) return prefixMatches[0];
13493
- }
13494
- let best;
13495
- let bestDistance = Infinity;
13496
- for (const command of KNOWN_COMMANDS) {
13497
- const distance = editDistance(lower, command);
13498
- if (distance < bestDistance) {
13499
- bestDistance = distance;
13500
- best = command;
13501
- }
13502
- }
13503
- if (best && bestDistance <= 3 && bestDistance <= Math.ceil(lower.length / 2)) {
13504
- return best;
13505
- }
13506
- return void 0;
13293
+ function noOptions(command, args) {
13294
+ if (args.length > 0) throw new UsageError(`${command} accepts no options.`);
13507
13295
  }
13508
13296
  function parseInvocation(args) {
13509
13297
  const [command, ...options] = args;
13510
- if (!command) return parseWorkspaceCommand("start", []);
13298
+ if (!command) return parseWorkspace([]);
13511
13299
  if (command === "--help" || command === "-h") {
13512
- if (options.length > 0) throw new UsageError("Help accepts one command name.");
13300
+ noOptions("Help", options);
13513
13301
  return { command: "help" };
13514
13302
  }
13515
- if (command === "help") {
13516
- if (options.length > 1) throw new UsageError("Help accepts one command name.");
13517
- const topic = options[0];
13518
- if (!topic) return { command: "help" };
13519
- if (!helpTopics.has(topic)) {
13520
- throw new UsageError(`Unknown help topic: ${topic}`);
13521
- }
13522
- return { command: "help", topic };
13523
- }
13524
13303
  if (command === "--version" || command === "-v") {
13525
- if (options.length > 0) throw new UsageError("Version accepts no arguments.");
13304
+ noOptions("Version", options);
13526
13305
  return { command: "version" };
13527
13306
  }
13528
- if (command === "ui") return parseWorkspaceCommand("ui", options);
13529
- if (command === "start") return parseWorkspaceCommand("start", options);
13307
+ if (command === "--") return parseWorkspace(args);
13530
13308
  if (command === "status") {
13531
- if (options.includes("--help") || options.includes("-h")) {
13532
- return { command: "help", topic: "status" };
13533
- }
13534
- return { command: "status", json: singleJsonOption("Status", options) };
13535
- }
13536
- if (command === "doctor") {
13537
- if (options.includes("--help") || options.includes("-h")) {
13538
- return { command: "help", topic: "doctor" };
13539
- }
13540
- return { command: "doctor", json: singleJsonOption("Doctor", options) };
13309
+ noOptions("Status", options);
13310
+ return { command };
13541
13311
  }
13542
13312
  if (command === "devices") return parseDevices(options);
13543
- if (command === "completions") {
13544
- if (options.includes("--help") || options.includes("-h")) {
13545
- return { command: "help", topic: "completions" };
13546
- }
13547
- if (options.length !== 1) {
13548
- throw new UsageError("Use: glossa completions <shell>.");
13549
- }
13550
- const shell = options[0];
13551
- if (!SUPPORTED_SHELLS.includes(shell)) {
13552
- throw new UsageError(
13553
- `Unsupported shell: ${shell}. Use one of: ${SUPPORTED_SHELLS.join(", ")}.`
13554
- );
13555
- }
13556
- return { command: "completions", shell };
13557
- }
13558
- if (command === "update" || command === "upgrade") {
13559
- if (options.includes("--help") || options.includes("-h")) {
13560
- return { command: "help", topic: "update" };
13561
- }
13562
- if (options.length > 0) {
13563
- throw new UsageError(`${command === "update" ? "Update" : "Upgrade"} accepts no arguments.`);
13564
- }
13565
- return { command: "update" };
13566
- }
13567
- if (command === "login") {
13568
- if (options.includes("--help") || options.includes("-h")) {
13569
- return { command: "help", topic: "login" };
13570
- }
13571
- if (options.length > 0) throw new UsageError("Login accepts no arguments.");
13572
- return { command: "login" };
13573
- }
13574
13313
  if (command === "logout") {
13575
- if (options.includes("--help") || options.includes("-h")) {
13576
- return { command: "help", topic: "logout" };
13577
- }
13578
- if (options.length === 0) return { command: "logout", browser: false };
13579
- if (options.length === 1 && options[0] === "--browser") {
13580
- return { command: "logout", browser: true };
13581
- }
13582
- throw new UsageError("Logout accepts only --browser.");
13314
+ noOptions("Logout", options);
13315
+ return { command };
13583
13316
  }
13584
- if (command === "--") return parseWorkspaceCommand("start", options);
13585
- if (command.startsWith("-")) return parseWorkspaceCommand("start", args);
13586
- if (likelyDirectory(command)) return parseWorkspaceCommand("start", args);
13587
- const suggestion = suggestCommand(command);
13588
- throw new UsageError(
13589
- suggestion ? `Unknown command: ${command}. Did you mean "${suggestion}"?` : `Unknown command: ${command}`
13590
- );
13317
+ return parseWorkspace(args);
13591
13318
  }
13592
13319
 
13593
- // src/device-store.ts
13594
- import path4 from "node:path";
13595
- var FILE_DEVICE_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 device credential file.";
13596
- function parseDeviceCredential(value) {
13597
- let parsed;
13598
- try {
13599
- parsed = JSON.parse(value);
13600
- } catch {
13601
- throw new Error("Stored Glossa device credentials are invalid.");
13602
- }
13603
- let relayOriginValid = false;
13604
- if (typeof parsed.relayOrigin === "string") {
13605
- try {
13606
- relayOriginValid = new URL(parsed.relayOrigin).origin === parsed.relayOrigin;
13607
- } catch {
13608
- relayOriginValid = false;
13609
- }
13610
- }
13611
- if (!relayOriginValid || typeof parsed.deviceId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
13612
- parsed.deviceId
13613
- ) || typeof parsed.deviceName !== "string" || parsed.deviceName.length === 0 || typeof parsed.token !== "string" || !parsed.token.startsWith(`gld_${parsed.deviceId}_`)) {
13614
- throw new Error("Stored Glossa device credentials are invalid.");
13615
- }
13616
- return parsed;
13320
+ // src/device-format.ts
13321
+ function deviceStatus(device) {
13322
+ if (device.revokedAt) return "revoked";
13323
+ if (device.activeWorkers === null) return "worker count unavailable";
13324
+ if (device.activeWorkers === 0) return "offline";
13325
+ return `${device.activeWorkers} active ${device.activeWorkers === 1 ? "worker" : "workers"}`;
13617
13326
  }
13618
- var store2 = new SecureStore({
13619
- account: "device",
13620
- file: path4.join(configDirectory(), "device.json"),
13621
- warning: FILE_DEVICE_WARNING,
13622
- parse: parseDeviceCredential
13623
- });
13624
- async function loadDeviceCredential() {
13625
- return (await store2.load())?.value ?? null;
13327
+ function formatRelativeTime(iso, now = Date.now()) {
13328
+ if (!iso) return "never";
13329
+ const parsed = Date.parse(iso);
13330
+ if (!Number.isFinite(parsed)) return "unknown";
13331
+ const seconds = Math.max(0, Math.round((now - parsed) / 1e3));
13332
+ if (seconds < 60) return "just now";
13333
+ const minutes = Math.round(seconds / 60);
13334
+ if (minutes < 60) return `${minutes}m ago`;
13335
+ const hours = Math.round(minutes / 60);
13336
+ if (hours < 24) return `${hours}h ago`;
13337
+ const days = Math.round(hours / 24);
13338
+ return `${days}d ago`;
13626
13339
  }
13627
- async function peekDeviceCredential() {
13628
- return (await store2.peek())?.value ?? null;
13340
+ function formatDeviceRow(device, now = Date.now()) {
13341
+ const platform = device.platform ?? "unknown platform";
13342
+ return `${device.id} ${device.name} ${platform} last seen ${formatRelativeTime(device.lastSeenAt, now)} ${deviceStatus(device)}`;
13629
13343
  }
13630
- async function saveDeviceCredential(credential) {
13631
- await store2.save(credential);
13344
+
13345
+ // src/logout.ts
13346
+ function browserLogoutUrl(issuer) {
13347
+ return new URL(
13348
+ "v2/logout",
13349
+ issuer.endsWith("/") ? issuer : `${issuer}/`
13350
+ ).toString();
13632
13351
  }
13633
- async function deleteDeviceCredential() {
13634
- await store2.delete();
13352
+ async function logoutFromGlossa(dependencies = {}) {
13353
+ const remove = dependencies.deleteCredentials ?? deleteCredentials;
13354
+ const peek = dependencies.peekCredentials ?? peekCredentials;
13355
+ const browse = dependencies.openBrowser ?? openBrowser;
13356
+ const log = dependencies.log ?? console.log;
13357
+ let stored = null;
13358
+ let present = true;
13359
+ try {
13360
+ stored = await peek();
13361
+ present = stored !== null;
13362
+ } catch {
13363
+ }
13364
+ const issuer = dependencies.issuer ?? stored?.credentials.issuer;
13365
+ await remove();
13366
+ log(
13367
+ present ? "Signed out of Glossa." : "Already signed out of Glossa."
13368
+ );
13369
+ const url2 = browserLogoutUrl(issuer ?? loadAuthConfig().issuer);
13370
+ const opened = await browse(url2);
13371
+ if (opened) {
13372
+ log("Opened Glossa browser sign-out.");
13373
+ } else {
13374
+ log("Open this URL to finish signing out in your browser:");
13375
+ log(url2);
13376
+ }
13635
13377
  }
13636
13378
 
13637
13379
  // src/relay-client.ts
@@ -13661,15 +13403,21 @@ function normalizedOrigin(value, kind) {
13661
13403
  }
13662
13404
  return url2.origin;
13663
13405
  }
13664
- function loadRelayEndpoints(environment = process.env) {
13665
- const relayOrigin = normalizedOrigin(
13406
+ function loadRelayOrigin(environment = process.env) {
13407
+ return normalizedOrigin(
13666
13408
  environment.GLOSSA_RELAY_ORIGIN?.trim() || DEFAULT_RELAY_ORIGIN,
13667
13409
  "relay"
13668
13410
  );
13669
- const workerOrigin = normalizedOrigin(
13411
+ }
13412
+ function loadWorkerOrigin(relayOrigin, environment = process.env) {
13413
+ return normalizedOrigin(
13670
13414
  environment.GLOSSA_WORKER_ORIGIN?.trim() || relayOrigin,
13671
13415
  "worker"
13672
13416
  );
13417
+ }
13418
+ function loadRelayEndpoints(environment = process.env) {
13419
+ const relayOrigin = loadRelayOrigin(environment);
13420
+ const workerOrigin = loadWorkerOrigin(relayOrigin, environment);
13673
13421
  return { relayOrigin, workerOrigin };
13674
13422
  }
13675
13423
  function defaultDeviceName() {
@@ -13686,7 +13434,9 @@ function relayError(status, data) {
13686
13434
  );
13687
13435
  }
13688
13436
  if (status === 409 && data.error === "device_name_conflict") {
13689
- return new Error("A Glossa device already uses this name. Run glossa devices list, then rename or revoke the old device.");
13437
+ return new Error(
13438
+ "Glossa could not choose a unique name for this computer. Try again."
13439
+ );
13690
13440
  }
13691
13441
  if (status === 404 && data.error === "device_not_found") {
13692
13442
  return new Error("The Glossa device was not found.");
@@ -13733,44 +13483,48 @@ async function accountOwnsDevice(endpoints, credentials, deviceId, fetchRequest
13733
13483
  );
13734
13484
  }
13735
13485
  async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = fetch) {
13736
- const name = deviceNameSchema.parse(deviceName);
13737
- const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/enroll`, {
13738
- method: "POST",
13739
- headers: {
13740
- authorization: `${credentials.tokenType} ${credentials.accessToken}`,
13741
- "content-type": "application/json"
13742
- },
13743
- body: JSON.stringify({ name, platform: `${process.platform}-${process.arch}` })
13744
- });
13745
- let data = {};
13746
- try {
13747
- data = await response.json();
13748
- } catch {
13749
- }
13750
- if (!response.ok) throw relayError(response.status, data);
13751
- if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
13752
- throw new Error("The Glossa relay returned an invalid device enrollment response.");
13486
+ const baseName = deviceNameSchema.parse(deviceName);
13487
+ let name = baseName;
13488
+ for (let attempt = 0; attempt < 3; attempt += 1) {
13489
+ const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/enroll`, {
13490
+ method: "POST",
13491
+ headers: {
13492
+ authorization: `${credentials.tokenType} ${credentials.accessToken}`,
13493
+ "content-type": "application/json"
13494
+ },
13495
+ body: JSON.stringify({ name, platform: `${process.platform}-${process.arch}` })
13496
+ });
13497
+ let data = {};
13498
+ try {
13499
+ data = await response.json();
13500
+ } catch {
13501
+ }
13502
+ if (response.status === 409 && data.error === "device_name_conflict" && attempt < 2) {
13503
+ const activeNames = new Set(
13504
+ (await listDevices(endpoints, credentials, fetchRequest)).filter((device) => device.revokedAt === null).map((device) => device.name)
13505
+ );
13506
+ for (let suffix = 2; ; suffix += 1) {
13507
+ const ending = `-${suffix}`;
13508
+ const candidate = `${baseName.slice(0, 80 - ending.length)}${ending}`;
13509
+ if (!activeNames.has(candidate)) {
13510
+ name = deviceNameSchema.parse(candidate);
13511
+ break;
13512
+ }
13513
+ }
13514
+ continue;
13515
+ }
13516
+ if (!response.ok) throw relayError(response.status, data);
13517
+ if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
13518
+ throw new Error("The Glossa relay returned an invalid device enrollment response.");
13519
+ }
13520
+ return {
13521
+ relayOrigin: endpoints.relayOrigin,
13522
+ deviceId: data.device.id,
13523
+ deviceName: data.device.name,
13524
+ token: data.device_token
13525
+ };
13753
13526
  }
13754
- return {
13755
- relayOrigin: endpoints.relayOrigin,
13756
- deviceId: data.device.id,
13757
- deviceName: data.device.name,
13758
- token: data.device_token
13759
- };
13760
- }
13761
- async function renameDevice(endpoints, credentials, deviceId, name, fetchRequest = fetch) {
13762
- const validName = deviceNameSchema.parse(name);
13763
- const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
13764
- method: "PATCH",
13765
- headers: {
13766
- authorization: `${credentials.tokenType} ${credentials.accessToken}`,
13767
- "content-type": "application/json"
13768
- },
13769
- body: JSON.stringify({ name: validName })
13770
- });
13771
- const data = await response.json().catch(() => ({}));
13772
- if (!response.ok) throw relayError(response.status, data);
13773
- return parseDevices2([data.device])[0];
13527
+ throw new Error("Glossa could not enroll this computer.");
13774
13528
  }
13775
13529
  async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
13776
13530
  const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
@@ -13785,671 +13539,331 @@ async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fet
13785
13539
  }
13786
13540
  }
13787
13541
 
13788
- // src/runtime.ts
13789
- function isStandaloneExecutable(value = true ? false : false) {
13790
- if (typeof value === "boolean") return value;
13791
- if (!value || typeof value !== "object") return false;
13792
- return value.isStandaloneExecutable === true;
13793
- }
13794
-
13795
- // src/doctor.ts
13796
- var MIN_NODE_MAJOR = 22;
13797
- var MIN_NODE_MINOR = 9;
13798
- var HEALTHZ_TIMEOUT_MS = 5e3;
13799
- function nodeVersionSatisfies(version2) {
13800
- const match = /^v?(\d+)\.(\d+)/.exec(version2.trim());
13801
- if (!match) return false;
13802
- const major = Number(match[1]);
13803
- const minor = Number(match[2]);
13804
- if (!Number.isInteger(major) || !Number.isInteger(minor)) return false;
13805
- return major > MIN_NODE_MAJOR || major === MIN_NODE_MAJOR && minor >= MIN_NODE_MINOR;
13806
- }
13807
- async function runDoctorChecks(dependencies = {}) {
13808
- const standalone = dependencies.standalone ?? isStandaloneExecutable();
13809
- const checks = [];
13810
- if (standalone) {
13811
- const runtimeReady = await (dependencies.probeStandaloneRuntime ?? defaultProbeStandaloneRuntime)();
13812
- checks.push({
13813
- name: "Runtime",
13814
- status: runtimeReady ? "pass" : "fail",
13815
- detail: runtimeReady ? "Self-contained Glossa executable." : "The executable is missing its native credential module.",
13816
- ...runtimeReady ? {} : { nextStep: "Reinstall Glossa with npm or the direct installer." }
13817
- });
13542
+ // src/status-display.ts
13543
+ function formatStatus(status) {
13544
+ const lines = [
13545
+ `Signed in as ${status.account}.`,
13546
+ `Relay connected: ${status.relay}`
13547
+ ];
13548
+ if (status.activeWorkers === null) {
13549
+ lines.push("Active workspaces: unavailable");
13550
+ } else if (status.activeWorkers === 0) {
13551
+ lines.push("No active workspaces. Run glossa from the project folder you want to expose.");
13818
13552
  } else {
13819
- const nodeVersion = dependencies.nodeVersion ?? process.versions.node;
13820
- const nodeOk = nodeVersionSatisfies(nodeVersion);
13821
- const displayedNodeVersion = nodeVersion.startsWith("v") ? nodeVersion : `v${nodeVersion}`;
13822
- checks.push({
13823
- name: "Node.js",
13824
- status: nodeOk ? "pass" : "fail",
13825
- detail: `Node.js ${displayedNodeVersion}`,
13826
- ...nodeOk ? {} : { nextStep: `Install Node.js ${MIN_NODE_MAJOR}.${MIN_NODE_MINOR} or newer and restart your terminal.` }
13827
- });
13553
+ lines.push(`Active workspaces: ${status.activeWorkers}`);
13828
13554
  }
13829
- let endpoints = dependencies.endpoints;
13830
- if (!endpoints) {
13555
+ if (status.devices.length === 0) lines.push("No active devices.");
13556
+ else lines.push(...status.devices.map((device) => formatDeviceRow(device)));
13557
+ return lines;
13558
+ }
13559
+
13560
+ // src/status-service.ts
13561
+ function accountLabel(profile) {
13562
+ return profile.email ?? profile.name ?? profile.sub;
13563
+ }
13564
+ function activeWorkerCount(devices) {
13565
+ if (devices.some((device) => device.activeWorkers === null)) return null;
13566
+ return devices.reduce((sum, device) => sum + device.activeWorkers, 0);
13567
+ }
13568
+ var WorkspaceStatusService = class {
13569
+ constructor(credentials, endpoints, dependencies = {}) {
13570
+ this.endpoints = endpoints;
13571
+ this.dependencies = dependencies;
13572
+ this.#credentials = credentials;
13573
+ }
13574
+ endpoints;
13575
+ dependencies;
13576
+ #credentials;
13577
+ #inFlight;
13578
+ async refresh(signal) {
13579
+ if (this.#inFlight) return await this.#inFlight;
13580
+ const pending = this.#load(signal);
13581
+ this.#inFlight = pending;
13831
13582
  try {
13832
- endpoints = (dependencies.loadEndpoints ?? loadRelayEndpoints)();
13833
- } catch (error46) {
13834
- const message = error46 instanceof Error ? error46.message : String(error46);
13835
- checks.push({
13836
- name: "Relay",
13837
- status: "fail",
13838
- detail: `Endpoint configuration is invalid: ${message}`,
13839
- nextStep: "Set GLOSSA_RELAY_ORIGIN and GLOSSA_WORKER_ORIGIN to origin URLs only, without paths."
13840
- });
13583
+ return await pending;
13584
+ } finally {
13585
+ if (this.#inFlight === pending) this.#inFlight = void 0;
13841
13586
  }
13842
13587
  }
13843
- if (endpoints) {
13844
- const fetchHealthz = dependencies.fetchHealthz ?? defaultFetchHealthz;
13845
- const relayOk = await fetchHealthz(endpoints.relayOrigin);
13846
- checks.push({
13847
- name: "Relay",
13848
- status: relayOk ? "pass" : "fail",
13849
- detail: relayOk ? `${endpoints.relayOrigin} is reachable.` : `${endpoints.relayOrigin} is not reachable.`,
13850
- ...relayOk ? {} : { nextStep: "Check your internet connection. If you self-host, confirm GLOSSA_RELAY_ORIGIN." }
13588
+ async #load(signal) {
13589
+ const validate = this.dependencies.validCredentials ?? validCredentials;
13590
+ const devicesForAccount = this.dependencies.listDevices ?? listDevices;
13591
+ const profileForAccount = this.dependencies.loadUserProfile ?? loadUserProfile;
13592
+ const baseFetch = this.dependencies.fetch ?? fetch;
13593
+ const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
13594
+ this.#credentials = await validate(
13595
+ this.#credentials,
13596
+ signal ? { signal } : {}
13597
+ );
13598
+ const requestCredentials = this.#credentials;
13599
+ const profileRequest = profileForAccount(
13600
+ requestCredentials,
13601
+ signal ? { signal, fetch: fetchRequest } : { fetch: fetchRequest }
13602
+ ).catch((error46) => {
13603
+ if (signal?.aborted) throw error46;
13604
+ return null;
13851
13605
  });
13852
- if (endpoints.workerOrigin !== endpoints.relayOrigin) {
13853
- const workerOk = await fetchHealthz(endpoints.workerOrigin);
13854
- checks.push({
13855
- name: "Worker",
13856
- status: workerOk ? "pass" : "fail",
13857
- detail: workerOk ? `${endpoints.workerOrigin} is reachable.` : `${endpoints.workerOrigin} is not reachable.`,
13858
- ...workerOk ? {} : { nextStep: "Confirm GLOSSA_WORKER_ORIGIN and the worker endpoint reverse proxy." }
13859
- });
13860
- }
13861
- }
13862
- const probeCredentials = dependencies.probeCredentials ?? defaultProbeCredentials;
13863
- const credentialState = await probeCredentials();
13864
- checks.push(signInCheck(credentialState));
13865
- const probeDeviceCredential = dependencies.probeDeviceCredential ?? defaultProbeDeviceCredential;
13866
- const deviceCredentialState = await probeDeviceCredential();
13867
- checks.push(deviceCredentialCheck(deviceCredentialState));
13868
- return checks;
13869
- }
13870
- async function defaultProbeStandaloneRuntime() {
13871
- try {
13872
- const keyring = await import("@napi-rs/keyring");
13873
- return typeof keyring.AsyncEntry === "function";
13874
- } catch {
13875
- return false;
13876
- }
13877
- }
13878
- function signInCheck(state) {
13879
- if (state === "present") {
13880
- return { name: "Sign-in", status: "pass", detail: "Signed in to Glossa." };
13881
- }
13882
- if (state === "absent") {
13606
+ const devicesRequest = devicesForAccount(
13607
+ this.endpoints,
13608
+ requestCredentials,
13609
+ fetchRequest
13610
+ );
13611
+ const [profile, devices] = await Promise.all([
13612
+ profileRequest,
13613
+ devicesRequest
13614
+ ]);
13615
+ if (profile) this.#credentials = profile.credentials;
13616
+ const activeDevices = devices.filter((device) => device.revokedAt === null);
13883
13617
  return {
13884
- name: "Sign-in",
13885
- status: "warn",
13886
- detail: "Not signed in yet.",
13887
- nextStep: 'Run "glossa" inside a workspace. Sign-in opens automatically.'
13618
+ account: profile ? accountLabel(profile.profile) : "Account unavailable",
13619
+ relay: this.endpoints.relayOrigin,
13620
+ activeWorkers: activeWorkerCount(activeDevices),
13621
+ devices: activeDevices
13888
13622
  };
13889
13623
  }
13890
- return {
13891
- name: "Sign-in",
13892
- status: "fail",
13893
- detail: "Stored credentials are unreadable.",
13894
- nextStep: 'Run "glossa logout" to clear them, then start Glossa again.'
13895
- };
13624
+ };
13625
+
13626
+ // src/ui-hud.ts
13627
+ import { emitKeypressEvents } from "node:readline";
13628
+
13629
+ // src/first-run.ts
13630
+ import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
13631
+ import path3 from "node:path";
13632
+ var CONNECT_HINT_FILE = "connect-hint-shown";
13633
+ var CONNECT_HINT_URL = "https://glossa.sh/docs/quickstart";
13634
+ function shouldShowConnectHint(relayOrigin) {
13635
+ return relayOrigin === DEFAULT_RELAY_ORIGIN;
13896
13636
  }
13897
- function deviceCredentialCheck(state) {
13898
- if (state === "present") {
13899
- return {
13900
- name: "Device",
13901
- status: "pass",
13902
- detail: "Device credentials are readable."
13903
- };
13904
- }
13905
- if (state === "absent") {
13906
- return {
13907
- name: "Device",
13908
- status: "warn",
13909
- detail: "No device is enrolled on this computer yet.",
13910
- nextStep: 'Run "glossa --device-name <name> ." inside a workspace to enroll it.'
13911
- };
13912
- }
13637
+ function connectHintStore(directory = configDirectory()) {
13638
+ const file2 = path3.join(directory, CONNECT_HINT_FILE);
13913
13639
  return {
13914
- name: "Device",
13915
- status: "fail",
13916
- detail: "Stored device credentials are unreadable.",
13917
- nextStep: "Remove the local Glossa device credential from the operating-system credential store (or the device.json fallback), then start Glossa again to re-enroll."
13918
- };
13919
- }
13920
- function formatDoctorResult(checks, json2) {
13921
- if (json2) return JSON.stringify({ checks }, null, 2);
13922
- const nameWidth = Math.max(...checks.map((check2) => check2.name.length));
13923
- const lines = ["Glossa doctor", ""];
13924
- for (const check2 of checks) {
13925
- const name = check2.name.padEnd(nameWidth);
13926
- lines.push(` ${name} ${check2.status.toUpperCase()} ${check2.detail}`);
13927
- if (check2.nextStep) {
13928
- lines.push(` ${" ".repeat(nameWidth)} ${check2.nextStep}`);
13640
+ async exists() {
13641
+ try {
13642
+ await readFile2(file2, "utf8");
13643
+ return true;
13644
+ } catch (error46) {
13645
+ if (error46.code === "ENOENT") return false;
13646
+ throw error46;
13647
+ }
13648
+ },
13649
+ async mark() {
13650
+ await mkdir2(directory, { recursive: true, mode: 448 });
13651
+ await writeFile2(file2, "", { encoding: "utf8", mode: 384 });
13929
13652
  }
13930
- }
13931
- const failed = checks.filter((check2) => check2.status === "fail").length;
13932
- lines.push("");
13933
- lines.push(
13934
- failed === 0 ? "Glossa is ready to start." : `${failed} check${failed === 1 ? "" : "s"} failed. Resolve the items above before starting.`
13935
- );
13936
- return lines.join("\n");
13653
+ };
13937
13654
  }
13938
- async function runDoctor(json2, dependencies = {}, log = console.log) {
13939
- const checks = await runDoctorChecks(dependencies);
13940
- log(formatDoctorResult(checks, json2));
13941
- return checks.every((check2) => check2.status !== "fail");
13655
+ async function announceConnectHint(store3, log) {
13656
+ if (await store3.exists()) return false;
13657
+ log(`Next: add Glossa in ChatGPT. Follow the quickstart at ${CONNECT_HINT_URL}.`);
13658
+ await store3.mark();
13659
+ return true;
13942
13660
  }
13943
- async function defaultFetchHealthz(origin) {
13661
+
13662
+ // src/device-store.ts
13663
+ import path4 from "node:path";
13664
+ var FILE_DEVICE_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 device credential file.";
13665
+ function parseDeviceCredential(value) {
13666
+ let parsed;
13944
13667
  try {
13945
- const response = await fetch(`${origin}/healthz`, {
13946
- signal: AbortSignal.timeout(HEALTHZ_TIMEOUT_MS)
13947
- });
13948
- if (!response.ok) return false;
13949
- const data = await response.json();
13950
- return data.ok === true && data.service === "glossa-relay";
13668
+ parsed = JSON.parse(value);
13951
13669
  } catch {
13952
- return false;
13670
+ throw new Error("Stored Glossa device credentials are invalid.");
13953
13671
  }
13954
- }
13955
- async function defaultProbeCredentials() {
13956
- try {
13957
- return await peekCredentials() !== null ? "present" : "absent";
13958
- } catch {
13959
- return "error";
13672
+ let relayOriginValid = false;
13673
+ if (typeof parsed.relayOrigin === "string") {
13674
+ try {
13675
+ relayOriginValid = new URL(parsed.relayOrigin).origin === parsed.relayOrigin;
13676
+ } catch {
13677
+ relayOriginValid = false;
13678
+ }
13960
13679
  }
13961
- }
13962
- async function defaultProbeDeviceCredential() {
13963
- try {
13964
- return await peekDeviceCredential() !== null ? "present" : "absent";
13965
- } catch {
13966
- return "error";
13680
+ if (!relayOriginValid || typeof parsed.deviceId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
13681
+ parsed.deviceId
13682
+ ) || typeof parsed.deviceName !== "string" || parsed.deviceName.length === 0 || parsed.accountSubject !== void 0 && (typeof parsed.accountSubject !== "string" || parsed.accountSubject.length === 0) || typeof parsed.token !== "string" || !parsed.token.startsWith(`gld_${parsed.deviceId}_`)) {
13683
+ throw new Error("Stored Glossa device credentials are invalid.");
13967
13684
  }
13685
+ return parsed;
13968
13686
  }
13969
-
13970
- // src/device-format.ts
13971
- function deviceStatus(device) {
13972
- if (device.revokedAt) return "revoked";
13973
- if (device.activeWorkers === null) return "worker count unavailable";
13974
- if (device.activeWorkers === 0) return "offline";
13975
- return `${device.activeWorkers} active ${device.activeWorkers === 1 ? "worker" : "workers"}`;
13687
+ var store2 = new SecureStore({
13688
+ account: "device",
13689
+ file: path4.join(configDirectory(), "device.json"),
13690
+ warning: FILE_DEVICE_WARNING,
13691
+ parse: parseDeviceCredential
13692
+ });
13693
+ async function loadDeviceCredential() {
13694
+ return (await store2.load())?.value ?? null;
13976
13695
  }
13977
- function formatRelativeTime(iso, now = Date.now()) {
13978
- if (!iso) return "never";
13979
- const parsed = Date.parse(iso);
13980
- if (!Number.isFinite(parsed)) return "unknown";
13981
- const seconds = Math.max(0, Math.round((now - parsed) / 1e3));
13982
- if (seconds < 60) return "just now";
13983
- const minutes = Math.round(seconds / 60);
13984
- if (minutes < 60) return `${minutes}m ago`;
13985
- const hours = Math.round(minutes / 60);
13986
- if (hours < 24) return `${hours}h ago`;
13987
- const days = Math.round(hours / 24);
13988
- return `${days}d ago`;
13696
+ async function saveDeviceCredential(credential) {
13697
+ await store2.save(credential);
13989
13698
  }
13990
- function formatDeviceRow(device, now = Date.now()) {
13991
- const platform = device.platform ?? "unknown platform";
13992
- return `${device.id} ${device.name} ${platform} last seen ${formatRelativeTime(device.lastSeenAt, now)} ${deviceStatus(device)}`;
13699
+ async function deleteDeviceCredential() {
13700
+ await store2.delete();
13993
13701
  }
13994
13702
 
13995
- // src/logout.ts
13996
- function browserLogoutUrl(issuer) {
13997
- return new URL(
13998
- "v2/logout",
13999
- issuer.endsWith("/") ? issuer : `${issuer}/`
14000
- ).toString();
14001
- }
14002
- async function logoutFromGlossa(options, dependencies = {}) {
14003
- const remove = dependencies.deleteCredentials ?? deleteCredentials;
14004
- const peek = dependencies.peekCredentials ?? peekCredentials;
14005
- const browse = dependencies.openBrowser ?? openBrowser;
14006
- const log = dependencies.log ?? console.log;
14007
- let stored = null;
14008
- let present = true;
14009
- try {
14010
- stored = await peek();
14011
- present = stored !== null;
14012
- } catch {
14013
- }
14014
- const issuer = dependencies.issuer ?? stored?.credentials.issuer;
14015
- await remove();
14016
- log(
14017
- present ? "Signed out of Glossa locally." : "Already signed out of Glossa locally."
14018
- );
14019
- if (!options.browser) return;
14020
- const url2 = browserLogoutUrl(issuer ?? loadAuthConfig().issuer);
14021
- const opened = await browse(url2);
14022
- if (opened) {
14023
- log("Opened Glossa browser sign-out.");
14024
- } else {
14025
- log("Open this URL to finish signing out in your browser:");
14026
- log(url2);
14027
- }
14028
- log(
14029
- "Reconnect Glossa in ChatGPT, then choose the same Google account when the CLI signs in."
14030
- );
14031
- }
13703
+ // src/worker/command-service.ts
13704
+ import { spawn as spawn2 } from "node:child_process";
13705
+ import { randomUUID } from "node:crypto";
13706
+ import { StringDecoder } from "node:string_decoder";
13707
+ import { setTimeout as delay2 } from "node:timers/promises";
14032
13708
 
14033
- // src/status-guidance.ts
14034
- function noActiveWorkerHint(activeWorkers, deviceCount) {
14035
- if (deviceCount === 0) return null;
14036
- if (activeWorkers === 0) {
14037
- return 'No active workers. Run "glossa" inside a workspace so ChatGPT can use it.';
13709
+ // src/worker/errors.ts
13710
+ var WorkerError = class extends Error {
13711
+ constructor(code, message) {
13712
+ super(message);
13713
+ this.code = code;
13714
+ this.name = "WorkerError";
14038
13715
  }
14039
- return null;
14040
- }
13716
+ code;
13717
+ };
14041
13718
 
14042
- // src/update.ts
14043
- import { spawnSync } from "node:child_process";
14044
- import { createHash } from "node:crypto";
14045
- import {
14046
- chmodSync,
14047
- existsSync as existsSync2,
14048
- renameSync,
14049
- rmSync,
14050
- writeFileSync
14051
- } from "node:fs";
14052
- var NPM_PACKAGE = "@ariobarin/glossa@beta";
14053
- var DEFAULT_RELEASES_API = "https://api.github.com/repos/ariobarin/glossa/releases?per_page=20";
14054
- function npmUpdateInvocation(platform = process.platform, environment = process.env) {
14055
- if (platform === "win32") {
14056
- return {
14057
- command: environment.ComSpec ?? environment.COMSPEC ?? "cmd.exe",
14058
- args: ["/d", "/s", "/c", `npm install --global ${NPM_PACKAGE}`]
14059
- };
14060
- }
14061
- return {
14062
- command: "npm",
14063
- args: ["install", "--global", NPM_PACKAGE]
14064
- };
14065
- }
14066
- function standaloneAssetName(platform = process.platform, architecture = process.arch) {
14067
- const operatingSystem = platform === "win32" ? "windows" : platform === "darwin" ? "macos" : platform === "linux" ? "linux" : null;
14068
- if (!operatingSystem || !["x64", "arm64"].includes(architecture)) {
14069
- throw new Error(
14070
- `The direct Glossa installer does not support ${platform}/${architecture}. Use npm instead.`
14071
- );
14072
- }
14073
- return `glossa-${operatingSystem}-${architecture}${platform === "win32" ? ".exe" : ""}`;
14074
- }
14075
- function selectStandaloneRelease(value, assetName) {
14076
- if (!Array.isArray(value)) {
14077
- throw new Error("The Glossa release service returned an invalid response.");
14078
- }
14079
- for (const candidate of value) {
14080
- if (candidate.draft === true || typeof candidate.tag_name !== "string") {
14081
- continue;
14082
- }
14083
- if (!candidate.tag_name.startsWith("cli-v") || !Array.isArray(candidate.assets)) {
14084
- continue;
14085
- }
14086
- const assets = candidate.assets;
14087
- const binary = assets.find((asset) => asset.name === assetName);
14088
- const checksum = assets.find(
14089
- (asset) => asset.name === `${assetName}.sha256`
13719
+ // src/worker/command-service.ts
13720
+ var STREAM_HEAD_BYTES = Math.floor(MAX_COMMAND_OUTPUT_BYTES / 3);
13721
+ var STREAM_TAIL_BYTES = MAX_COMMAND_OUTPUT_BYTES - STREAM_HEAD_BYTES;
13722
+ function appendTail(existing, chunk) {
13723
+ if (chunk.byteLength >= STREAM_TAIL_BYTES) {
13724
+ return Buffer.from(chunk.subarray(chunk.byteLength - STREAM_TAIL_BYTES));
13725
+ }
13726
+ const combined = existing.byteLength === 0 ? chunk : Buffer.concat([existing, chunk]);
13727
+ return combined.byteLength <= STREAM_TAIL_BYTES ? combined : combined.subarray(combined.byteLength - STREAM_TAIL_BYTES);
13728
+ }
13729
+ function capture(_record2, stream, chunk) {
13730
+ if (chunk.byteLength === 0) return false;
13731
+ stream.totalBytes += chunk.byteLength;
13732
+ let offset = 0;
13733
+ if (stream.headBytes < STREAM_HEAD_BYTES) {
13734
+ const accepted = chunk.subarray(
13735
+ 0,
13736
+ Math.min(chunk.byteLength, STREAM_HEAD_BYTES - stream.headBytes)
14090
13737
  );
14091
- if (typeof binary?.browser_download_url === "string" && typeof checksum?.browser_download_url === "string") {
14092
- return {
14093
- version: candidate.tag_name.slice("cli-v".length),
14094
- binaryUrl: binary.browser_download_url,
14095
- checksumUrl: checksum.browser_download_url
14096
- };
13738
+ if (accepted.byteLength > 0) {
13739
+ stream.head.push(Buffer.from(accepted));
13740
+ stream.headBytes += accepted.byteLength;
13741
+ offset = accepted.byteLength;
14097
13742
  }
14098
13743
  }
14099
- throw new Error(
14100
- `No Glossa direct-install release contains ${assetName}. Use npm or try again after the next release.`
14101
- );
14102
- }
14103
- function expectedChecksum(text, assetName) {
14104
- for (const line of text.split(/\r?\n/)) {
14105
- const match = /^([a-fA-F0-9]{64})\s+\*?(.+)$/.exec(line.trim());
14106
- if (match?.[1] && match[2] === assetName) return match[1].toLowerCase();
13744
+ if (offset < chunk.byteLength) {
13745
+ stream.tail = appendTail(stream.tail, chunk.subarray(offset));
14107
13746
  }
14108
- throw new Error(`The checksum file did not contain ${assetName}.`);
13747
+ return true;
14109
13748
  }
14110
- async function checkedFetch(fetcher, url2) {
14111
- const response = await fetcher(url2, {
14112
- headers: { "User-Agent": "glossa-cli" }
14113
- });
14114
- if (!response.ok) {
14115
- throw new Error(`Download failed with HTTP ${response.status}: ${url2}`);
14116
- }
14117
- return response;
14118
- }
14119
- async function updateStandalone(dependencies) {
14120
- const platform = dependencies.platform ?? process.platform;
14121
- const architecture = dependencies.architecture ?? process.arch;
14122
- const environment = dependencies.environment ?? process.env;
14123
- const executablePath = dependencies.executablePath ?? process.execPath;
14124
- const fetcher = dependencies.fetch ?? fetch;
14125
- const assetName = standaloneAssetName(platform, architecture);
14126
- const releasesUrl = environment.GLOSSA_RELEASES_API ?? DEFAULT_RELEASES_API;
14127
- const releasesResponse = await checkedFetch(fetcher, releasesUrl);
14128
- const release = selectStandaloneRelease(
14129
- await releasesResponse.json(),
14130
- assetName
14131
- );
14132
- if (release.version === dependencies.currentVersion) {
14133
- return `Glossa ${release.version} is already current.`;
14134
- }
14135
- const [binaryResponse, checksumResponse] = await Promise.all([
14136
- checkedFetch(fetcher, release.binaryUrl),
14137
- checkedFetch(fetcher, release.checksumUrl)
14138
- ]);
14139
- const binary = Buffer.from(await binaryResponse.arrayBuffer());
14140
- const expected = expectedChecksum(await checksumResponse.text(), assetName);
14141
- const actual = createHash("sha256").update(binary).digest("hex");
14142
- if (actual !== expected) {
14143
- throw new Error(
14144
- `Glossa refused the update because the SHA-256 checksum did not match.`
14145
- );
13749
+ function markChanged(record2) {
13750
+ record2.sequence += 1;
13751
+ const waiters = [...record2.changeWaiters];
13752
+ record2.changeWaiters.clear();
13753
+ for (const waiter of waiters) waiter();
13754
+ }
13755
+ async function waitForChange(record2, afterSequence, waitMs) {
13756
+ if (record2.status !== "running" || record2.sequence > afterSequence || waitMs === 0) {
13757
+ return;
14146
13758
  }
14147
- const downloadPath = `${executablePath}.download-${process.pid}`;
14148
- const backupPath = `${executablePath}.old`;
14149
- writeFileSync(downloadPath, binary, { mode: 493 });
14150
- if (platform !== "win32") chmodSync(downloadPath, 493);
13759
+ let changed;
13760
+ const change = new Promise((resolve) => {
13761
+ changed = resolve;
13762
+ record2.changeWaiters.add(changed);
13763
+ });
13764
+ const waitController = new AbortController();
14151
13765
  try {
14152
- if (platform === "win32") {
14153
- rmSync(backupPath, { force: true });
14154
- renameSync(executablePath, backupPath);
14155
- try {
14156
- renameSync(downloadPath, executablePath);
14157
- } catch (error46) {
14158
- renameSync(backupPath, executablePath);
14159
- throw error46;
14160
- }
14161
- try {
14162
- rmSync(backupPath, { force: true });
14163
- } catch {
14164
- }
14165
- } else {
14166
- renameSync(downloadPath, executablePath);
14167
- }
13766
+ await Promise.race([
13767
+ change,
13768
+ delay2(waitMs, void 0, { signal: waitController.signal })
13769
+ ]);
14168
13770
  } finally {
14169
- if (existsSync2(downloadPath)) rmSync(downloadPath, { force: true });
13771
+ record2.changeWaiters.delete(changed);
13772
+ waitController.abort();
14170
13773
  }
14171
- return `Updated Glossa to ${release.version}.`;
14172
13774
  }
14173
- async function updateGlossa(dependencies = {}) {
14174
- const log = dependencies.log ?? console.log;
14175
- const standalone = dependencies.standalone ?? isStandaloneExecutable();
14176
- if (standalone) {
14177
- log("Updating the standalone Glossa executable...");
14178
- const message = dependencies.updateStandalone ? await dependencies.updateStandalone() : await updateStandalone(dependencies);
14179
- log(message);
14180
- return;
13775
+ function emptyCapture() {
13776
+ return {
13777
+ head: [],
13778
+ headBytes: 0,
13779
+ tail: Buffer.alloc(0),
13780
+ totalBytes: 0
13781
+ };
13782
+ }
13783
+ function retainedBytes(stream, complete) {
13784
+ const head = Buffer.concat(stream.head, stream.headBytes);
13785
+ const retained = Buffer.concat([head, stream.tail]);
13786
+ const content = stream.totalBytes <= MAX_COMMAND_OUTPUT_BYTES ? complete ? retained.toString("utf8") : new StringDecoder("utf8").write(retained) : safePrefix(head) + safeSuffix(stream.tail);
13787
+ return Math.min(Buffer.byteLength(content), MAX_COMMAND_OUTPUT_BYTES);
13788
+ }
13789
+ function safePrefix(buffer) {
13790
+ return new StringDecoder("utf8").write(buffer);
13791
+ }
13792
+ function safeSuffix(buffer) {
13793
+ let start = 0;
13794
+ while (start < buffer.byteLength && (buffer[start] & 192) === 128) {
13795
+ start += 1;
13796
+ }
13797
+ return new StringDecoder("utf8").write(buffer.subarray(start));
13798
+ }
13799
+ function utf8PrefixWithinBudget(value, budget) {
13800
+ let used = 0;
13801
+ let end = 0;
13802
+ for (const character of value) {
13803
+ const bytes = Buffer.byteLength(character);
13804
+ if (used + bytes > budget) break;
13805
+ used += bytes;
13806
+ end += character.length;
13807
+ }
13808
+ return value.slice(0, end);
13809
+ }
13810
+ function utf8SuffixWithinBudget(value, budget) {
13811
+ const characters = Array.from(value);
13812
+ let used = 0;
13813
+ let start = characters.length;
13814
+ while (start > 0) {
13815
+ const bytes = Buffer.byteLength(characters[start - 1]);
13816
+ if (used + bytes > budget) break;
13817
+ used += bytes;
13818
+ start -= 1;
13819
+ }
13820
+ return characters.slice(start).join("");
13821
+ }
13822
+ function renderStream(stream, budget, complete) {
13823
+ if (budget <= 0 || stream.totalBytes === 0) {
13824
+ return { content: "", truncated: stream.totalBytes > 0 };
13825
+ }
13826
+ const head = Buffer.concat(stream.head, stream.headBytes);
13827
+ const retained = Buffer.concat([head, stream.tail]);
13828
+ if (stream.totalBytes <= budget) {
13829
+ const content = complete ? retained.toString("utf8") : new StringDecoder("utf8").write(retained);
13830
+ if (Buffer.byteLength(content) <= budget) {
13831
+ return { content, truncated: false };
13832
+ }
13833
+ const prefixBudget2 = Math.floor(budget / 3);
13834
+ return {
13835
+ content: utf8PrefixWithinBudget(content, prefixBudget2) + utf8SuffixWithinBudget(content, budget - prefixBudget2),
13836
+ truncated: true
13837
+ };
14181
13838
  }
14182
- const run = dependencies.run ?? spawnSync;
14183
- const invocation = npmUpdateInvocation(
14184
- dependencies.platform,
14185
- dependencies.environment
14186
- );
14187
- log("Updating Glossa from npm...");
14188
- const result = run(invocation.command, invocation.args, { stdio: "inherit" });
14189
- if (result.error) {
14190
- throw new Error(`Glossa could not start npm: ${result.error.message}`);
14191
- }
14192
- if (result.status !== 0) {
14193
- throw new Error(
14194
- `npm could not update Glossa (exit ${result.status ?? "unknown"}).`
14195
- );
14196
- }
14197
- log("Glossa updated. Run glossa --version to verify the installed version.");
14198
- }
14199
-
14200
- // src/ui-hud.ts
14201
- import { emitKeypressEvents } from "node:readline";
14202
- function initialHudState(workspace) {
13839
+ const headBudget = Math.min(head.byteLength, Math.floor(budget / 3));
13840
+ const tailBudget = Math.min(stream.tail.byteLength, budget - headBudget);
13841
+ const remaining = budget - headBudget - tailBudget;
13842
+ const extraHead = Math.min(remaining, head.byteLength - headBudget);
13843
+ const prefixBudget = headBudget + extraHead;
13844
+ const prefix = head.subarray(0, headBudget + extraHead);
13845
+ const suffix = stream.tail.subarray(stream.tail.byteLength - tailBudget);
14203
13846
  return {
14204
- workspace,
14205
- connection: "starting",
14206
- message: void 0,
14207
- activities: [],
14208
- showDetails: false,
14209
- showHelp: false
14210
- };
14211
- }
14212
- function activityLabel(event) {
14213
- const noun = event.jobType === "write_file" ? "File write" : event.jobType === "edit_file" ? "File edit" : event.jobType === "run_command" ? "Command" : "Cancellation";
14214
- if (event.phase === "requested") return `${noun} requested`;
14215
- if (event.jobType === "run_command") return `Command ${event.ok ? "started" : "rejected"}`;
14216
- return `${noun} ${event.ok ? "completed" : "rejected"}`;
14217
- }
14218
- function applyHudEvent(state, event) {
14219
- if (event.type === "session") {
14220
- return { ...state, workspace: event.root, deviceName: event.deviceName };
14221
- }
14222
- if (event.type === "status") {
14223
- if (event.status.state === "retrying") {
14224
- return { ...state, connection: "retrying", message: event.status.error.message };
14225
- }
14226
- return { ...state, connection: event.status.state, message: void 0 };
14227
- }
14228
- if (event.type === "notice") return { ...state, message: event.message };
14229
- const activity = event.phase === "finished" ? { label: activityLabel(event), requestId: event.requestId, ok: event.ok } : { label: activityLabel(event), requestId: event.requestId };
14230
- return { ...state, activities: [...state.activities.slice(-7), activity] };
14231
- }
14232
- function style(enabled, code, value) {
14233
- return enabled ? `\x1B[${code}m${value}\x1B[0m` : value;
14234
- }
14235
- function renderTitle(width, color) {
14236
- const title = "Glossa";
14237
- const padding = " ".repeat(Math.max(0, Math.floor((width - title.length) / 2)));
14238
- return `${padding}${style(color, "38;2;120;77;250;1", title)}`;
14239
- }
14240
- function truncate(value, width) {
14241
- if (value.length <= width) return value;
14242
- if (width <= 1) return "\u2026";
14243
- return `${value.slice(0, width - 1)}\u2026`;
14244
- }
14245
- function wrapText(value, width) {
14246
- const words = value.split(/\s+/);
14247
- const lines = [];
14248
- let line = "";
14249
- for (const word of words) {
14250
- if (!line) line = word;
14251
- else if (`${line} ${word}`.length <= width) line += ` ${word}`;
14252
- else {
14253
- lines.push(line);
14254
- line = word;
14255
- }
14256
- }
14257
- if (line) lines.push(line);
14258
- return lines;
14259
- }
14260
- function connectionCopy(state) {
14261
- if (state.connection === "connected") {
14262
- return {
14263
- glyph: "\u25CF",
14264
- label: "Connected",
14265
- detail: state.message ?? "ChatGPT can use this workspace."
14266
- };
14267
- }
14268
- if (state.connection === "connecting" || state.connection === "starting") {
14269
- return { glyph: "\u25CC", label: "Connecting", detail: "Establishing the managed relay session\u2026" };
14270
- }
14271
- if (state.connection === "retrying") {
14272
- return { glyph: "\u25CC", label: "Reconnecting", detail: state.message ?? "Retrying automatically\u2026" };
14273
- }
14274
- if (state.connection === "error") {
14275
- return { glyph: "\xD7", label: "Error", detail: state.message ?? "The session stopped unexpectedly." };
14276
- }
14277
- return { glyph: "\u25CB", label: "Disconnected", detail: "The workspace is no longer exposed." };
14278
- }
14279
- function renderHud(state, width = 80, color = !process.env.NO_COLOR) {
14280
- const copy = connectionCopy(state);
14281
- const usable = Math.max(24, width - 4);
14282
- const lines = [
14283
- renderTitle(width, color),
14284
- "",
14285
- `${style(color, state.connection === "connected" ? "32;1" : "36;1", copy.glyph)} ${style(color, "1", copy.label)}`,
14286
- ` ${truncate(copy.detail, usable)}`,
14287
- "",
14288
- `${style(color, "2", "Workspace")} ${truncate(state.workspace, Math.max(8, usable - 11))}`,
14289
- "",
14290
- style(color, "1", "Authority"),
14291
- ...wrapText(
14292
- "Files may be modified and commands have the full environment and permissions of this account.",
14293
- usable
14294
- ).map((line) => ` ${line}`)
14295
- ];
14296
- if (state.deviceName) lines.push(`${style(color, "2", "Device")} ${truncate(state.deviceName, Math.max(8, usable - 11))}`);
14297
- if (state.showHelp) {
14298
- lines.push("", style(color, "1", "Keys"));
14299
- lines.push(" d toggle recent activity");
14300
- lines.push(" ? hide this help");
14301
- lines.push(" q disconnect and quit");
14302
- } else if (state.showDetails) {
14303
- lines.push("", style(color, "1", "Recent activity"));
14304
- if (state.activities.length === 0) lines.push(style(color, "2", " No tool activity yet."));
14305
- for (const activity of state.activities.slice(-5)) {
14306
- const outcome = activity.ok === false ? style(color, "31", "\xD7") : style(color, "2", "\xB7");
14307
- lines.push(`${outcome} ${truncate(activity.label, Math.max(8, usable - 16))} ${style(color, "2", activity.requestId.slice(0, 8))}`);
14308
- }
14309
- } else {
14310
- const latest = state.activities.at(-1);
14311
- lines.push("", latest ? `${style(color, "2", "Latest")} ${truncate(latest.label, Math.max(8, usable - 11))}` : style(color, "2", "No tool activity yet."));
14312
- }
14313
- lines.push("", style(color, "2", "d details ? help q disconnect"));
14314
- return lines.join("\n");
14315
- }
14316
- async function runSessionHud(actions, input = process.stdin, output = process.stdout) {
14317
- if (!input.isTTY || !output.isTTY) {
14318
- throw new Error("glossa ui requires an interactive terminal. Use glossa start instead.");
14319
- }
14320
- emitKeypressEvents(input);
14321
- const wasRaw = input.isRaw;
14322
- const wasPaused = input.isPaused();
14323
- const controller = new AbortController();
14324
- let state = initialHudState(actions.workspace);
14325
- let stopUi;
14326
- const render = () => {
14327
- const view = renderHud(state, output.columns ?? 80);
14328
- output.write(`\x1B[H\x1B[2J${view}`);
14329
- };
14330
- const session = actions.run(controller.signal, (event) => {
14331
- state = applyHudEvent(state, event);
14332
- render();
14333
- }).then(() => {
14334
- if (!controller.signal.aborted) state = { ...state, connection: "disconnected" };
14335
- render();
14336
- }).catch((error46) => {
14337
- state = {
14338
- ...state,
14339
- connection: "error",
14340
- message: error46 instanceof Error ? error46.message : String(error46)
14341
- };
14342
- render();
14343
- throw error46;
14344
- });
14345
- input.setRawMode(true);
14346
- input.resume();
14347
- output.write("\x1B[?1049h\x1B[?25l");
14348
- render();
14349
- const stop = () => {
14350
- controller.abort();
14351
- stopUi?.();
14352
- };
14353
- process.once("SIGINT", stop);
14354
- process.once("SIGTERM", stop);
14355
- try {
14356
- await new Promise((resolve) => {
14357
- stopUi = resolve;
14358
- const onKeypress = (value, key) => {
14359
- if (key.ctrl && key.name === "c" || key.name === "q") return stop();
14360
- if (key.name === "d") {
14361
- state = { ...state, showDetails: !state.showDetails, showHelp: false };
14362
- render();
14363
- } else if (value === "?" || key.sequence === "?") {
14364
- state = { ...state, showHelp: !state.showHelp };
14365
- render();
14366
- }
14367
- };
14368
- input.on("keypress", onKeypress);
14369
- void session.catch(() => stopUi?.());
14370
- stopUi = () => {
14371
- input.removeListener("keypress", onKeypress);
14372
- resolve();
14373
- };
14374
- });
14375
- await session;
14376
- } finally {
14377
- process.removeListener("SIGINT", stop);
14378
- process.removeListener("SIGTERM", stop);
14379
- input.setRawMode(wasRaw);
14380
- if (wasPaused) input.pause();
14381
- output.write("\x1B[?25h\x1B[?1049l");
14382
- }
14383
- }
14384
-
14385
- // src/first-run.ts
14386
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
14387
- import path5 from "node:path";
14388
- var CONNECT_HINT_FILE = "connect-hint-shown";
14389
- var CONNECT_HINT_URL = "https://glossa.sh/docs/quickstart";
14390
- function shouldShowConnectHint(relayOrigin) {
14391
- return relayOrigin === DEFAULT_RELAY_ORIGIN;
14392
- }
14393
- function connectHintStore(directory = configDirectory()) {
14394
- const file2 = path5.join(directory, CONNECT_HINT_FILE);
13847
+ content: utf8PrefixWithinBudget(safePrefix(prefix), prefixBudget) + utf8SuffixWithinBudget(safeSuffix(suffix), tailBudget),
13848
+ truncated: true
13849
+ };
13850
+ }
13851
+ function renderOutput(stdout, stderr, complete) {
13852
+ const half = Math.floor(MAX_COMMAND_OUTPUT_BYTES / 2);
13853
+ const stdoutAvailable = retainedBytes(stdout, complete);
13854
+ const stderrAvailable = retainedBytes(stderr, complete);
13855
+ let stdoutBudget = Math.min(stdoutAvailable, half);
13856
+ let stderrBudget = Math.min(stderrAvailable, half);
13857
+ let remaining = MAX_COMMAND_OUTPUT_BYTES - stdoutBudget - stderrBudget;
13858
+ const stderrExtra = Math.min(remaining, stderrAvailable - stderrBudget);
13859
+ stderrBudget += stderrExtra;
13860
+ remaining -= stderrExtra;
13861
+ stdoutBudget += Math.min(remaining, stdoutAvailable - stdoutBudget);
14395
13862
  return {
14396
- async exists() {
14397
- try {
14398
- await readFile2(file2, "utf8");
14399
- return true;
14400
- } catch (error46) {
14401
- if (error46.code === "ENOENT") return false;
14402
- throw error46;
14403
- }
14404
- },
14405
- async mark() {
14406
- await mkdir2(directory, { recursive: true, mode: 448 });
14407
- await writeFile2(file2, "", { encoding: "utf8", mode: 384 });
14408
- }
13863
+ stdout: renderStream(stdout, stdoutBudget, complete),
13864
+ stderr: renderStream(stderr, stderrBudget, complete)
14409
13865
  };
14410
13866
  }
14411
- async function announceConnectHint(store3, log) {
14412
- if (await store3.exists()) return false;
14413
- log(`Next: add Glossa in ChatGPT. Follow the quickstart at ${CONNECT_HINT_URL}.`);
14414
- await store3.mark();
14415
- return true;
14416
- }
14417
-
14418
- // src/worker/command-service.ts
14419
- import { spawn as spawn2 } from "node:child_process";
14420
- import { randomUUID } from "node:crypto";
14421
- import { StringDecoder } from "node:string_decoder";
14422
- import { setTimeout as delay2 } from "node:timers/promises";
14423
-
14424
- // src/worker/errors.ts
14425
- var WorkerError = class extends Error {
14426
- constructor(code, message) {
14427
- super(message);
14428
- this.code = code;
14429
- this.name = "WorkerError";
14430
- }
14431
- code;
14432
- };
14433
-
14434
- // src/worker/command-service.ts
14435
- function capture(stream, chunk) {
14436
- if (stream.bytes >= MAX_TEXT_BYTES) {
14437
- stream.truncated = true;
14438
- return;
14439
- }
14440
- const remaining = MAX_TEXT_BYTES - stream.bytes;
14441
- const accepted = chunk.subarray(0, remaining);
14442
- stream.chunks.push(accepted);
14443
- stream.bytes += accepted.byteLength;
14444
- if (accepted.byteLength < chunk.byteLength) stream.truncated = true;
14445
- }
14446
- function emptyCapture() {
14447
- return { chunks: [], bytes: 0, truncated: false };
14448
- }
14449
- function decodeCapture(stream) {
14450
- const content = Buffer.concat(stream.chunks);
14451
- return stream.truncated ? new StringDecoder("utf8").write(content) : content.toString("utf8");
14452
- }
14453
13867
  function shellInvocation(command) {
14454
13868
  if (process.platform === "win32") {
14455
13869
  const file2 = process.env.GLOSSA_WINDOWS_SHELL ?? "powershell.exe";
@@ -14540,6 +13954,8 @@ var CommandService = class {
14540
13954
  id,
14541
13955
  child,
14542
13956
  status: "running",
13957
+ sequence: 0,
13958
+ changeWaiters: /* @__PURE__ */ new Set(),
14543
13959
  startedAt: Date.now(),
14544
13960
  stdout: emptyCapture(),
14545
13961
  stderr: emptyCapture(),
@@ -14554,15 +13970,20 @@ var CommandService = class {
14554
13970
  record2.timeout.unref();
14555
13971
  this.#commands.set(id, record2);
14556
13972
  this.#activeCommandId = id;
14557
- child.stdout.on("data", (chunk) => capture(record2.stdout, chunk));
14558
- child.stderr.on("data", (chunk) => capture(record2.stderr, chunk));
13973
+ child.stdout.on("data", (chunk) => {
13974
+ if (capture(record2, record2.stdout, chunk)) markChanged(record2);
13975
+ });
13976
+ child.stderr.on("data", (chunk) => {
13977
+ if (capture(record2, record2.stderr, chunk)) markChanged(record2);
13978
+ });
14559
13979
  child.once("error", (error46) => {
14560
13980
  if (record2.status !== "running") return;
14561
13981
  if (record2.timeout) clearTimeout(record2.timeout);
14562
13982
  record2.status = "failed";
14563
13983
  record2.finishedAt = Date.now();
14564
- capture(record2.stderr, Buffer.from(error46.message, "utf8"));
13984
+ capture(record2, record2.stderr, Buffer.from(error46.message, "utf8"));
14565
13985
  this.#activeCommandId = null;
13986
+ markChanged(record2);
14566
13987
  record2.complete();
14567
13988
  });
14568
13989
  child.once("close", (exitCode, signal) => {
@@ -14573,6 +13994,7 @@ var CommandService = class {
14573
13994
  record2.signal = signal;
14574
13995
  record2.status = record2.requestedTerminal ?? (exitCode === 0 ? "succeeded" : "failed");
14575
13996
  this.#activeCommandId = null;
13997
+ markChanged(record2);
14576
13998
  record2.complete();
14577
13999
  setTimeout(() => this.#commands.delete(id), 5 * 60 * 1e3).unref();
14578
14000
  });
@@ -14601,21 +14023,31 @@ var CommandService = class {
14601
14023
  }
14602
14024
  return this.snapshot(record2);
14603
14025
  }
14604
- async get(commandId, waitMs = 0) {
14026
+ async get(commandId, waitMs = 0, afterSequence) {
14605
14027
  const record2 = this.#commands.get(commandId);
14606
14028
  if (!record2) throw new WorkerError("command_not_found", "The command was not found.");
14607
14029
  if (!Number.isInteger(waitMs) || waitMs < 0 || waitMs > MAX_COMMAND_STATUS_WAIT_MS) {
14608
14030
  throw new WorkerError("invalid_wait", "Status wait must be between 0 and 15 seconds.");
14609
14031
  }
14032
+ if (afterSequence !== void 0 && (!Number.isInteger(afterSequence) || afterSequence < 0 || afterSequence > record2.sequence)) {
14033
+ throw new WorkerError(
14034
+ "invalid_sequence",
14035
+ "The command sequence is invalid for this command."
14036
+ );
14037
+ }
14610
14038
  if (record2.status === "running" && waitMs > 0) {
14611
- const waitController = new AbortController();
14612
- try {
14613
- await Promise.race([
14614
- record2.completion,
14615
- delay2(waitMs, void 0, { signal: waitController.signal })
14616
- ]);
14617
- } finally {
14618
- waitController.abort();
14039
+ if (afterSequence === void 0) {
14040
+ const waitController = new AbortController();
14041
+ try {
14042
+ await Promise.race([
14043
+ record2.completion,
14044
+ delay2(waitMs, void 0, { signal: waitController.signal })
14045
+ ]);
14046
+ } finally {
14047
+ waitController.abort();
14048
+ }
14049
+ } else {
14050
+ await waitForChange(record2, afterSequence, waitMs);
14619
14051
  }
14620
14052
  }
14621
14053
  return this.snapshot(record2);
@@ -14638,31 +14070,225 @@ var CommandService = class {
14638
14070
  await record2.completion;
14639
14071
  }
14640
14072
  snapshot(record2) {
14073
+ const output = renderOutput(
14074
+ record2.stdout,
14075
+ record2.stderr,
14076
+ record2.status !== "running"
14077
+ );
14641
14078
  const base = {
14642
14079
  commandId: record2.id,
14643
14080
  status: record2.status,
14644
- startedAt: new Date(record2.startedAt).toISOString()
14081
+ sequence: record2.sequence,
14082
+ startedAt: new Date(record2.startedAt).toISOString(),
14083
+ stdout: output.stdout.content,
14084
+ stderr: output.stderr.content,
14085
+ stdoutTruncated: output.stdout.truncated,
14086
+ stderrTruncated: output.stderr.truncated
14645
14087
  };
14646
14088
  if (record2.finishedAt !== void 0) {
14647
14089
  base.finishedAt = new Date(record2.finishedAt).toISOString();
14648
14090
  base.exitCode = record2.exitCode ?? null;
14649
14091
  base.signal = record2.signal ?? null;
14650
- base.stdout = decodeCapture(record2.stdout);
14651
- base.stderr = decodeCapture(record2.stderr);
14652
- base.stdoutTruncated = record2.stdout.truncated;
14653
- base.stderrTruncated = record2.stderr.truncated;
14654
14092
  }
14655
14093
  return base;
14656
14094
  }
14657
14095
  };
14658
14096
 
14659
14097
  // src/worker/file-service.ts
14660
- import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
14661
- import { chmod as chmod2, lstat, readFile as readFile3, rename, rm as rm2, stat, writeFile as writeFile3 } from "node:fs/promises";
14662
- import path6 from "node:path";
14098
+ import { createHash, randomUUID as randomUUID2 } from "node:crypto";
14099
+ import {
14100
+ chmod as chmod2,
14101
+ lstat,
14102
+ opendir,
14103
+ open,
14104
+ readFile as readFile3,
14105
+ rename,
14106
+ rm as rm2,
14107
+ stat,
14108
+ writeFile as writeFile3
14109
+ } from "node:fs/promises";
14110
+ import path5 from "node:path";
14111
+ import { performance } from "node:perf_hooks";
14663
14112
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
14664
14113
  function sha256(content) {
14665
- return createHash2("sha256").update(content).digest("hex");
14114
+ return createHash("sha256").update(content).digest("hex");
14115
+ }
14116
+ var DEFAULT_LIST_FILES_LIMIT = 100;
14117
+ var DEFAULT_SEARCH_TEXT_RESULTS = 50;
14118
+ var DEFAULT_READ_RANGE_LINES = 200;
14119
+ var MAX_REPOSITORY_SCAN_ENTRIES = 2e4;
14120
+ var MAX_SEARCH_FILES = 5e3;
14121
+ var MAX_SEARCH_BYTES = 32 * 1024 * 1024;
14122
+ var SKIPPED_RECURSIVE_DIRECTORIES = /* @__PURE__ */ new Set([
14123
+ ".git",
14124
+ ".hg",
14125
+ ".svn",
14126
+ "node_modules"
14127
+ ]);
14128
+ function compareNames(left, right) {
14129
+ return left < right ? -1 : left > right ? 1 : 0;
14130
+ }
14131
+ function relativeSortKey(root, target) {
14132
+ const relative = path5.relative(root, target);
14133
+ if (!relative) return ".";
14134
+ return process.platform === "win32" ? relative.replaceAll("\\", "/") : relative;
14135
+ }
14136
+ function displayPath(root, target) {
14137
+ const relative = relativeSortKey(root, target);
14138
+ if (process.platform === "win32") return relative;
14139
+ return relative.includes("\\") ? "./" + relative : relative;
14140
+ }
14141
+ function isUnavailableFileError(error46) {
14142
+ const code = error46?.code;
14143
+ return code === "ENOENT" || code === "EACCES" || code === "EPERM" || code === "ENOTDIR";
14144
+ }
14145
+ function isUnavailableDiscoveredPathError(error46) {
14146
+ return isUnavailableFileError(error46) || error46 instanceof WorkerError && error46.code === "path_not_found";
14147
+ }
14148
+ function isLinkedPathError(error46) {
14149
+ return error46 instanceof WorkerError && error46.code === "linked_path";
14150
+ }
14151
+ async function readBoundedFile(target, maximumBytes, expectedBytes) {
14152
+ const handle = await open(target, "r");
14153
+ try {
14154
+ const openedStat = await handle.stat();
14155
+ if (!openedStat.isFile()) {
14156
+ throw new WorkerError("not_file", "The requested path is not a file.");
14157
+ }
14158
+ if (openedStat.size !== expectedBytes) {
14159
+ throw new WorkerError("file_changed", "The file changed while it was being read.");
14160
+ }
14161
+ if (openedStat.size > maximumBytes) {
14162
+ throw new WorkerError("search_byte_limit", "The search byte limit was reached.");
14163
+ }
14164
+ const buffer = Buffer.allocUnsafe(openedStat.size);
14165
+ let offset = 0;
14166
+ while (offset < buffer.byteLength) {
14167
+ const { bytesRead } = await handle.read(
14168
+ buffer,
14169
+ offset,
14170
+ buffer.byteLength - offset,
14171
+ offset
14172
+ );
14173
+ if (bytesRead === 0) break;
14174
+ offset += bytesRead;
14175
+ }
14176
+ const completedStat = await handle.stat();
14177
+ if (completedStat.size !== openedStat.size || offset !== openedStat.size) {
14178
+ throw new WorkerError("file_changed", "The file changed while it was being read.");
14179
+ }
14180
+ return Buffer.from(buffer);
14181
+ } finally {
14182
+ await handle.close();
14183
+ }
14184
+ }
14185
+ function normalizedLines(content) {
14186
+ if (content.length === 0) return [];
14187
+ const normalized = content.replace(/\r\n?/g, "\n");
14188
+ const lines = normalized.split("\n");
14189
+ if (normalized.endsWith("\n")) lines.pop();
14190
+ return lines;
14191
+ }
14192
+ function escapeRegExp(value) {
14193
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14194
+ }
14195
+ function heapPush(heap, value) {
14196
+ heap.push(value);
14197
+ let index = heap.length - 1;
14198
+ while (index > 0) {
14199
+ const parent = Math.floor((index - 1) / 2);
14200
+ if (compareNames(heap[parent].sortKey, value.sortKey) <= 0) break;
14201
+ heap[index] = heap[parent];
14202
+ index = parent;
14203
+ }
14204
+ heap[index] = value;
14205
+ }
14206
+ function heapPop(heap) {
14207
+ const first = heap[0];
14208
+ const last = heap.pop();
14209
+ if (!first || !last || heap.length === 0) return first;
14210
+ let index = 0;
14211
+ while (true) {
14212
+ const left = index * 2 + 1;
14213
+ if (left >= heap.length) break;
14214
+ const right = left + 1;
14215
+ const child = right < heap.length && compareNames(heap[right].sortKey, heap[left].sortKey) < 0 ? right : left;
14216
+ if (compareNames(last.sortKey, heap[child].sortKey) <= 0) break;
14217
+ heap[index] = heap[child];
14218
+ index = child;
14219
+ }
14220
+ heap[index] = last;
14221
+ return first;
14222
+ }
14223
+ function scanTimeoutError() {
14224
+ return new WorkerError(
14225
+ "scan_timeout",
14226
+ "The structured repository operation exceeded its local deadline."
14227
+ );
14228
+ }
14229
+ async function defaultBeforeDeadline(operation, deadlineAt, onLateValue) {
14230
+ const settled = operation.then(
14231
+ (value) => ({ ok: true, value }),
14232
+ (error46) => ({ ok: false, error: error46 })
14233
+ );
14234
+ const remainingMs = deadlineAt - performance.now();
14235
+ const expired = /* @__PURE__ */ Symbol("structured_read_deadline");
14236
+ let winner;
14237
+ let timer;
14238
+ if (remainingMs <= 0) {
14239
+ winner = expired;
14240
+ } else {
14241
+ try {
14242
+ winner = await Promise.race([
14243
+ settled,
14244
+ new Promise((resolve) => {
14245
+ timer = setTimeout(() => resolve(expired), remainingMs);
14246
+ })
14247
+ ]);
14248
+ } finally {
14249
+ if (timer) clearTimeout(timer);
14250
+ }
14251
+ }
14252
+ if (winner === expired) {
14253
+ const eventual = await settled;
14254
+ if (eventual.ok && onLateValue) {
14255
+ await onLateValue(eventual.value);
14256
+ }
14257
+ throw scanTimeoutError();
14258
+ }
14259
+ if (!winner.ok) throw winner.error;
14260
+ return winner.value;
14261
+ }
14262
+ async function closeDirectory(handle) {
14263
+ await handle.close().catch((error46) => {
14264
+ if (error46.code !== "ERR_DIR_CLOSED") throw error46;
14265
+ });
14266
+ }
14267
+ function boundedPositiveInteger(value, fallback, maximum, label) {
14268
+ const resolved = value ?? fallback;
14269
+ if (!Number.isInteger(resolved) || resolved < 1 || resolved > maximum) {
14270
+ throw new WorkerError(
14271
+ "invalid_limit",
14272
+ `${label} must be between 1 and ${maximum}.`
14273
+ );
14274
+ }
14275
+ return resolved;
14276
+ }
14277
+ function searchSnippet(line, matchIndex, matchLength) {
14278
+ if (line.length <= MAX_SEARCH_TEXT_SNIPPET_CHARS) {
14279
+ return { text: line, truncated: false };
14280
+ }
14281
+ const windowLength = MAX_SEARCH_TEXT_SNIPPET_CHARS - 6;
14282
+ let start = Math.max(0, matchIndex - 120);
14283
+ if (start + windowLength < matchIndex + matchLength) {
14284
+ start = matchIndex + matchLength - windowLength;
14285
+ }
14286
+ start = Math.min(start, line.length - windowLength);
14287
+ const end = start + windowLength;
14288
+ return {
14289
+ text: `${start > 0 ? "..." : ""}${line.slice(start, end)}${end < line.length ? "..." : ""}`,
14290
+ truncated: true
14291
+ };
14666
14292
  }
14667
14293
  function lineNumberAt(content, index) {
14668
14294
  let line = 1;
@@ -14708,8 +14334,8 @@ function createUnifiedDiff(relativePath, original, updated, edits) {
14708
14334
  hunks.push(candidate);
14709
14335
  }
14710
14336
  }
14711
- const displayPath = relativePath.replaceAll("\\", "/");
14712
- const lines = [`--- a/${displayPath}`, `+++ b/${displayPath}`];
14337
+ const displayPath2 = process.platform === "win32" ? relativePath.replaceAll("\\", "/") : relativePath;
14338
+ const lines = [`--- a/${displayPath2}`, `+++ b/${displayPath2}`];
14713
14339
  for (const hunk of hunks) {
14714
14340
  const newStart = mappedIndex(hunk.oldStart, edits);
14715
14341
  const newEnd = mappedIndex(hunk.oldEnd, edits);
@@ -14738,12 +14364,81 @@ function boundDiff(diff) {
14738
14364
  return { diff: `${prefix}${marker}`, truncated: true };
14739
14365
  }
14740
14366
  var FileService = class {
14741
- constructor(policy) {
14367
+ constructor(policy, dependencies = {}) {
14742
14368
  this.policy = policy;
14369
+ this.#now = dependencies.now ?? (() => performance.now());
14370
+ this.#beforeDeadline = dependencies.beforeDeadline ?? defaultBeforeDeadline;
14371
+ this.#openDirectory = dependencies.openDirectory ?? ((directory) => opendir(directory));
14372
+ this.#readFileBytes = dependencies.readFileBytes ?? readBoundedFile;
14373
+ this.#lstatPath = dependencies.lstatPath ?? ((target) => lstat(target));
14374
+ this.#maxRepositoryScanEntries = dependencies.maxRepositoryScanEntries ?? MAX_REPOSITORY_SCAN_ENTRIES;
14375
+ this.#maxSearchBytes = dependencies.maxSearchBytes ?? MAX_SEARCH_BYTES;
14376
+ if (!Number.isInteger(this.#maxRepositoryScanEntries) || this.#maxRepositoryScanEntries < 1 || this.#maxRepositoryScanEntries > MAX_REPOSITORY_SCAN_ENTRIES) {
14377
+ throw new WorkerError(
14378
+ "invalid_limit",
14379
+ `Repository scan limit must be between 1 and ${MAX_REPOSITORY_SCAN_ENTRIES}.`
14380
+ );
14381
+ }
14382
+ if (!Number.isInteger(this.#maxSearchBytes) || this.#maxSearchBytes < 1 || this.#maxSearchBytes > MAX_SEARCH_BYTES) {
14383
+ throw new WorkerError(
14384
+ "invalid_limit",
14385
+ `Search byte limit must be between 1 and ${MAX_SEARCH_BYTES}.`
14386
+ );
14387
+ }
14743
14388
  }
14744
14389
  policy;
14745
- async readText(relativePath) {
14746
- const target = await this.policy.resolveExisting(relativePath);
14390
+ #now;
14391
+ #beforeDeadline;
14392
+ #openDirectory;
14393
+ #readFileBytes;
14394
+ #lstatPath;
14395
+ #maxRepositoryScanEntries;
14396
+ #maxSearchBytes;
14397
+ #scanDeadline(timeoutMs = MAX_STRUCTURED_READ_TIMEOUT_MS) {
14398
+ if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_STRUCTURED_READ_TIMEOUT_MS) {
14399
+ throw new WorkerError(
14400
+ "invalid_timeout",
14401
+ `Structured read timeout must be between 1 and ${MAX_STRUCTURED_READ_TIMEOUT_MS}.`
14402
+ );
14403
+ }
14404
+ return this.#now() + timeoutMs;
14405
+ }
14406
+ async #withinDeadline(operation, deadlineAt, onLateValue) {
14407
+ return await this.#beforeDeadline(operation, deadlineAt, onLateValue);
14408
+ }
14409
+ #assertBeforeDeadline(deadlineAt) {
14410
+ if (this.#now() >= deadlineAt) {
14411
+ throw new WorkerError(
14412
+ "scan_timeout",
14413
+ "The structured repository operation exceeded its local deadline."
14414
+ );
14415
+ }
14416
+ }
14417
+ async #readDirectoryBounded(directory, deadlineAt, remainingEntries) {
14418
+ const handle = await this.#withinDeadline(
14419
+ this.#openDirectory(directory),
14420
+ deadlineAt,
14421
+ closeDirectory
14422
+ );
14423
+ const children = [];
14424
+ try {
14425
+ while (true) {
14426
+ this.#assertBeforeDeadline(deadlineAt);
14427
+ const child = await this.#withinDeadline(handle.read(), deadlineAt);
14428
+ if (!child) break;
14429
+ if (children.length >= remainingEntries) {
14430
+ return { children: [], overflow: true };
14431
+ }
14432
+ children.push(child);
14433
+ }
14434
+ } finally {
14435
+ await closeDirectory(handle);
14436
+ }
14437
+ children.sort((left, right) => compareNames(left.name, right.name));
14438
+ return { children, overflow: false };
14439
+ }
14440
+ async #readResolvedText(target, maximumBytes = MAX_TEXT_BYTES) {
14441
+ const boundedMaximum = Math.min(maximumBytes, MAX_TEXT_BYTES);
14747
14442
  const targetStat = await stat(target);
14748
14443
  if (!targetStat.isFile()) {
14749
14444
  throw new WorkerError("not_file", "The requested path is not a file.");
@@ -14751,15 +14446,471 @@ var FileService = class {
14751
14446
  if (targetStat.size > MAX_TEXT_BYTES) {
14752
14447
  throw new WorkerError("file_too_large", "The file exceeds the 1 MiB text limit.");
14753
14448
  }
14754
- const content = await readFile3(target);
14755
- let text;
14756
- try {
14757
- text = new TextDecoder("utf-8", { fatal: true }).decode(content);
14758
- } catch {
14759
- throw new WorkerError("not_text", "The file is not valid UTF-8 text.");
14449
+ if (targetStat.size > boundedMaximum) {
14450
+ throw new WorkerError("search_byte_limit", "The search byte limit was reached.");
14451
+ }
14452
+ const content = await this.#readFileBytes(
14453
+ target,
14454
+ boundedMaximum,
14455
+ targetStat.size
14456
+ );
14457
+ if (content.byteLength > MAX_TEXT_BYTES) {
14458
+ throw new WorkerError("file_too_large", "The file exceeds the 1 MiB text limit.");
14459
+ }
14460
+ if (content.byteLength > boundedMaximum) {
14461
+ throw new WorkerError("search_byte_limit", "The search byte limit was reached.");
14462
+ }
14463
+ if (content.byteLength > targetStat.size) {
14464
+ throw new WorkerError("file_changed", "The file changed while it was being read.");
14465
+ }
14466
+ let text;
14467
+ try {
14468
+ text = new TextDecoder("utf-8", { fatal: true }).decode(content);
14469
+ } catch {
14470
+ throw new WorkerError("not_text", "The file is not valid UTF-8 text.");
14760
14471
  }
14761
14472
  return { content: text, sha256: sha256(content), bytes: content.byteLength };
14762
14473
  }
14474
+ async readText(relativePath) {
14475
+ const target = await this.policy.resolveExisting(relativePath);
14476
+ return await this.#readResolvedText(target);
14477
+ }
14478
+ async listFiles(options = {}) {
14479
+ const deadlineAt = this.#scanDeadline(options.timeoutMs);
14480
+ const startPath = options.path ?? ".";
14481
+ const start = await this.#withinDeadline(
14482
+ this.policy.resolveExisting(startPath),
14483
+ deadlineAt
14484
+ );
14485
+ const startStat = await this.#withinDeadline(stat(start), deadlineAt);
14486
+ if (!startStat.isDirectory()) {
14487
+ throw new WorkerError("not_directory", "The requested path is not a directory.");
14488
+ }
14489
+ const limit = boundedPositiveInteger(
14490
+ options.limit,
14491
+ DEFAULT_LIST_FILES_LIMIT,
14492
+ MAX_LIST_FILES_RESULTS,
14493
+ "List limit"
14494
+ );
14495
+ const cursor = process.platform === "win32" ? options.cursor?.replaceAll("\\", "/") : options.cursor;
14496
+ const entries = [];
14497
+ const frontier = [];
14498
+ let scannedEntries = 0;
14499
+ let skippedLinks = 0;
14500
+ let hasMore = false;
14501
+ const scanLimitError = () => new WorkerError(
14502
+ "scan_limit",
14503
+ `The directory scan exceeds ${this.#maxRepositoryScanEntries} entries. Narrow the requested path.`
14504
+ );
14505
+ const enqueueChildren = async (directory) => {
14506
+ const batch = await this.#readDirectoryBounded(
14507
+ directory,
14508
+ deadlineAt,
14509
+ this.#maxRepositoryScanEntries - scannedEntries
14510
+ );
14511
+ if (batch.overflow) throw scanLimitError();
14512
+ scannedEntries += batch.children.length;
14513
+ for (const child of batch.children) {
14514
+ const target = path5.join(directory, child.name);
14515
+ heapPush(frontier, {
14516
+ target,
14517
+ sortKey: relativeSortKey(this.policy.root, target),
14518
+ path: displayPath(this.policy.root, target),
14519
+ name: child.name
14520
+ });
14521
+ }
14522
+ };
14523
+ const hasListableChild = async (directory) => {
14524
+ const handle = await this.#withinDeadline(
14525
+ this.#openDirectory(directory),
14526
+ deadlineAt,
14527
+ closeDirectory
14528
+ );
14529
+ try {
14530
+ while (true) {
14531
+ this.#assertBeforeDeadline(deadlineAt);
14532
+ const child = await this.#withinDeadline(handle.read(), deadlineAt);
14533
+ if (!child) return false;
14534
+ if (scannedEntries >= this.#maxRepositoryScanEntries) {
14535
+ throw scanLimitError();
14536
+ }
14537
+ scannedEntries += 1;
14538
+ const target = path5.join(directory, child.name);
14539
+ let childStat;
14540
+ try {
14541
+ childStat = await this.#withinDeadline(
14542
+ this.#lstatPath(target),
14543
+ deadlineAt
14544
+ );
14545
+ } catch (error46) {
14546
+ if (isUnavailableFileError(error46)) continue;
14547
+ throw error46;
14548
+ }
14549
+ if (childStat.isSymbolicLink()) {
14550
+ skippedLinks += 1;
14551
+ continue;
14552
+ }
14553
+ if (childStat.isFile() || childStat.isDirectory()) return true;
14554
+ }
14555
+ } finally {
14556
+ await closeDirectory(handle);
14557
+ }
14558
+ };
14559
+ await enqueueChildren(start);
14560
+ while (frontier.length > 0 && entries.length <= limit) {
14561
+ this.#assertBeforeDeadline(deadlineAt);
14562
+ const candidate = heapPop(frontier);
14563
+ let targetStat;
14564
+ try {
14565
+ targetStat = await this.#withinDeadline(
14566
+ this.#lstatPath(candidate.target),
14567
+ deadlineAt
14568
+ );
14569
+ } catch (error46) {
14570
+ if (isUnavailableFileError(error46)) continue;
14571
+ throw error46;
14572
+ }
14573
+ if (targetStat.isSymbolicLink()) {
14574
+ skippedLinks += 1;
14575
+ continue;
14576
+ }
14577
+ const type = targetStat.isDirectory() ? "directory" : targetStat.isFile() ? "file" : void 0;
14578
+ if (!type) continue;
14579
+ if (entries.length === limit && (!cursor || compareNames(candidate.sortKey, cursor) > 0)) {
14580
+ try {
14581
+ const safeCandidate = await this.#withinDeadline(
14582
+ this.policy.resolveDiscoveredExisting(candidate.target),
14583
+ deadlineAt
14584
+ );
14585
+ if (type === "directory" && options.recursive === true && !SKIPPED_RECURSIVE_DIRECTORIES.has(candidate.name.toLowerCase())) {
14586
+ const handle = await this.#withinDeadline(
14587
+ this.#openDirectory(safeCandidate),
14588
+ deadlineAt,
14589
+ closeDirectory
14590
+ );
14591
+ await closeDirectory(handle);
14592
+ }
14593
+ } catch (error46) {
14594
+ if (isUnavailableDiscoveredPathError(error46)) continue;
14595
+ if (isLinkedPathError(error46)) {
14596
+ skippedLinks += 1;
14597
+ continue;
14598
+ }
14599
+ throw error46;
14600
+ }
14601
+ hasMore = true;
14602
+ break;
14603
+ }
14604
+ const shouldReturn = !cursor || compareNames(candidate.sortKey, cursor) > 0;
14605
+ const shouldRecurse = type === "directory" && options.recursive === true && !SKIPPED_RECURSIVE_DIRECTORIES.has(candidate.name.toLowerCase());
14606
+ let safeDirectory;
14607
+ if (shouldRecurse) {
14608
+ try {
14609
+ safeDirectory = await this.#withinDeadline(
14610
+ this.policy.resolveDiscoveredExisting(candidate.target),
14611
+ deadlineAt
14612
+ );
14613
+ } catch (error46) {
14614
+ if (isUnavailableDiscoveredPathError(error46)) continue;
14615
+ if (isLinkedPathError(error46)) {
14616
+ skippedLinks += 1;
14617
+ continue;
14618
+ }
14619
+ throw error46;
14620
+ }
14621
+ }
14622
+ const fillsPage = shouldReturn && entries.length + 1 === limit;
14623
+ if (fillsPage && safeDirectory) {
14624
+ let hasChild;
14625
+ try {
14626
+ hasChild = await hasListableChild(safeDirectory);
14627
+ } catch (error46) {
14628
+ if (isUnavailableDiscoveredPathError(error46)) continue;
14629
+ throw error46;
14630
+ }
14631
+ entries.push({
14632
+ entry: { path: candidate.path, type },
14633
+ cursor: candidate.sortKey
14634
+ });
14635
+ if (hasChild) {
14636
+ hasMore = true;
14637
+ break;
14638
+ }
14639
+ continue;
14640
+ }
14641
+ if (safeDirectory) {
14642
+ try {
14643
+ await enqueueChildren(safeDirectory);
14644
+ } catch (error46) {
14645
+ if (isUnavailableDiscoveredPathError(error46)) continue;
14646
+ throw error46;
14647
+ }
14648
+ }
14649
+ if (shouldReturn) {
14650
+ entries.push({
14651
+ entry: {
14652
+ path: candidate.path,
14653
+ type,
14654
+ ...type === "file" ? { bytes: targetStat.size } : {}
14655
+ },
14656
+ cursor: candidate.sortKey
14657
+ });
14658
+ }
14659
+ }
14660
+ return {
14661
+ entries: entries.map(({ entry }) => entry),
14662
+ truncated: hasMore,
14663
+ scannedEntries,
14664
+ skippedLinks,
14665
+ ...hasMore && entries.length > 0 ? { nextCursor: entries.at(-1).cursor } : {}
14666
+ };
14667
+ }
14668
+ async searchText(options) {
14669
+ const deadlineAt = this.#scanDeadline(options.timeoutMs);
14670
+ if (options.query.length === 0 || options.query.length > 256 || /[\r\n\u0000]/.test(options.query)) {
14671
+ throw new WorkerError(
14672
+ "invalid_search",
14673
+ "Search text must be one non-empty line of at most 256 characters."
14674
+ );
14675
+ }
14676
+ const maxResults = boundedPositiveInteger(
14677
+ options.maxResults,
14678
+ DEFAULT_SEARCH_TEXT_RESULTS,
14679
+ MAX_SEARCH_TEXT_RESULTS,
14680
+ "Search result limit"
14681
+ );
14682
+ const matchLimit = maxResults + 1;
14683
+ const extensions = options.extensions?.map((extension) => extension.toLowerCase());
14684
+ const matcher = new RegExp(
14685
+ escapeRegExp(options.query),
14686
+ options.caseSensitive === true ? "u" : "iu"
14687
+ );
14688
+ const start = await this.#withinDeadline(
14689
+ this.policy.resolveExisting(options.path ?? "."),
14690
+ deadlineAt
14691
+ );
14692
+ const startStat = await this.#withinDeadline(stat(start), deadlineAt);
14693
+ const matches = [];
14694
+ let scannedEntries = 0;
14695
+ let scannedFiles = 0;
14696
+ let scannedBytes = 0;
14697
+ let skippedFiles = 0;
14698
+ let skippedLinks = 0;
14699
+ let scanTruncated = false;
14700
+ const searchFile = async (target) => {
14701
+ const relative = displayPath(this.policy.root, target);
14702
+ if (extensions && !extensions.some(
14703
+ (extension) => path5.basename(target).toLowerCase().endsWith(extension)
14704
+ )) {
14705
+ return false;
14706
+ }
14707
+ if (scannedFiles >= MAX_SEARCH_FILES) {
14708
+ scanTruncated = true;
14709
+ return true;
14710
+ }
14711
+ const remainingBytes = this.#maxSearchBytes - scannedBytes;
14712
+ if (remainingBytes <= 0) {
14713
+ scanTruncated = true;
14714
+ return true;
14715
+ }
14716
+ let result;
14717
+ try {
14718
+ const safeTarget = await this.#withinDeadline(
14719
+ this.policy.resolveDiscoveredExisting(target),
14720
+ deadlineAt
14721
+ );
14722
+ result = await this.#withinDeadline(
14723
+ this.#readResolvedText(safeTarget, remainingBytes),
14724
+ deadlineAt
14725
+ );
14726
+ } catch (error46) {
14727
+ if (error46 instanceof WorkerError && error46.code === "search_byte_limit") {
14728
+ scanTruncated = true;
14729
+ return true;
14730
+ }
14731
+ if (isLinkedPathError(error46)) {
14732
+ skippedLinks += 1;
14733
+ return false;
14734
+ }
14735
+ if (isUnavailableFileError(error46) || error46 instanceof WorkerError && [
14736
+ "not_text",
14737
+ "file_too_large",
14738
+ "file_changed",
14739
+ "path_not_found"
14740
+ ].includes(
14741
+ error46.code
14742
+ )) {
14743
+ skippedFiles += 1;
14744
+ return false;
14745
+ }
14746
+ throw error46;
14747
+ }
14748
+ if (scannedBytes + result.bytes > this.#maxSearchBytes) {
14749
+ scanTruncated = true;
14750
+ return true;
14751
+ }
14752
+ scannedFiles += 1;
14753
+ scannedBytes += result.bytes;
14754
+ const lines = result.content.replace(/\r\n?/g, "\n").split("\n");
14755
+ for (const [index, line] of lines.entries()) {
14756
+ if (index % 256 === 0) this.#assertBeforeDeadline(deadlineAt);
14757
+ const match = matcher.exec(line);
14758
+ if (!match) continue;
14759
+ const matchIndex = match.index;
14760
+ const snippet = searchSnippet(line, matchIndex, match[0].length);
14761
+ matches.push({
14762
+ path: relative,
14763
+ line: index + 1,
14764
+ column: matchIndex + 1,
14765
+ text: snippet.text,
14766
+ lineTruncated: snippet.truncated
14767
+ });
14768
+ if (matches.length >= matchLimit) return true;
14769
+ }
14770
+ return false;
14771
+ };
14772
+ const visit = async (directory) => {
14773
+ let batch;
14774
+ try {
14775
+ batch = await this.#readDirectoryBounded(
14776
+ directory,
14777
+ deadlineAt,
14778
+ this.#maxRepositoryScanEntries - scannedEntries
14779
+ );
14780
+ } catch (error46) {
14781
+ if (isUnavailableFileError(error46)) {
14782
+ skippedFiles += 1;
14783
+ return false;
14784
+ }
14785
+ throw error46;
14786
+ }
14787
+ if (batch.overflow) {
14788
+ scanTruncated = true;
14789
+ scannedEntries = this.#maxRepositoryScanEntries;
14790
+ return true;
14791
+ }
14792
+ scannedEntries += batch.children.length;
14793
+ for (const child of batch.children) {
14794
+ this.#assertBeforeDeadline(deadlineAt);
14795
+ const target = path5.join(directory, child.name);
14796
+ let targetStat;
14797
+ try {
14798
+ targetStat = await this.#withinDeadline(this.#lstatPath(target), deadlineAt);
14799
+ } catch (error46) {
14800
+ if (isUnavailableFileError(error46)) {
14801
+ skippedFiles += 1;
14802
+ continue;
14803
+ }
14804
+ throw error46;
14805
+ }
14806
+ if (targetStat.isSymbolicLink()) {
14807
+ skippedLinks += 1;
14808
+ continue;
14809
+ }
14810
+ if (targetStat.isDirectory()) {
14811
+ if (SKIPPED_RECURSIVE_DIRECTORIES.has(child.name.toLowerCase())) continue;
14812
+ try {
14813
+ await this.#withinDeadline(
14814
+ this.policy.resolveDiscoveredExisting(target),
14815
+ deadlineAt
14816
+ );
14817
+ } catch (error46) {
14818
+ if (isLinkedPathError(error46)) {
14819
+ skippedLinks += 1;
14820
+ continue;
14821
+ }
14822
+ if (isUnavailableDiscoveredPathError(error46)) {
14823
+ skippedFiles += 1;
14824
+ continue;
14825
+ }
14826
+ throw error46;
14827
+ }
14828
+ if (await visit(target)) return true;
14829
+ } else if (targetStat.isFile() && await searchFile(target)) {
14830
+ return true;
14831
+ }
14832
+ }
14833
+ return false;
14834
+ };
14835
+ if (startStat.isFile()) {
14836
+ await searchFile(start);
14837
+ } else if (startStat.isDirectory()) {
14838
+ await visit(start);
14839
+ } else {
14840
+ throw new WorkerError("not_file", "The requested path is not a file or directory.");
14841
+ }
14842
+ return {
14843
+ matches: matches.slice(0, maxResults),
14844
+ truncated: scanTruncated || matches.length > maxResults,
14845
+ scannedFiles,
14846
+ scannedBytes,
14847
+ skippedFiles,
14848
+ skippedLinks
14849
+ };
14850
+ }
14851
+ async readTextRange(relativePath, startLine = 1, lineCount = DEFAULT_READ_RANGE_LINES, timeoutMs = MAX_STRUCTURED_READ_TIMEOUT_MS) {
14852
+ const deadlineAt = this.#scanDeadline(timeoutMs);
14853
+ if (!Number.isInteger(startLine) || startLine < 1) {
14854
+ throw new WorkerError("invalid_range", "Start line must be a positive integer.");
14855
+ }
14856
+ lineCount = boundedPositiveInteger(
14857
+ lineCount,
14858
+ DEFAULT_READ_RANGE_LINES,
14859
+ MAX_READ_FILE_RANGE_LINES,
14860
+ "Line count"
14861
+ );
14862
+ const file2 = await this.#withinDeadline(
14863
+ this.readText(relativePath),
14864
+ deadlineAt
14865
+ );
14866
+ const lines = normalizedLines(file2.content);
14867
+ if (lines.length === 0) {
14868
+ if (startLine !== 1) {
14869
+ throw new WorkerError("line_out_of_range", "The requested line is outside the file.");
14870
+ }
14871
+ return {
14872
+ content: "",
14873
+ startLine: 1,
14874
+ endLine: 0,
14875
+ totalLines: 0,
14876
+ sha256: file2.sha256,
14877
+ bytes: file2.bytes,
14878
+ contentBytes: 0
14879
+ };
14880
+ }
14881
+ if (startLine > lines.length) {
14882
+ throw new WorkerError("line_out_of_range", "The requested line is outside the file.");
14883
+ }
14884
+ const selected = [];
14885
+ let contentBytes = 0;
14886
+ const requestedEnd = Math.min(lines.length, startLine - 1 + lineCount);
14887
+ for (let index = startLine - 1; index < requestedEnd; index += 1) {
14888
+ const line = lines[index];
14889
+ const addedBytes = Buffer.byteLength(line, "utf8") + (selected.length === 0 ? 0 : 1);
14890
+ if (contentBytes + addedBytes > MAX_READ_FILE_RANGE_BYTES) {
14891
+ if (selected.length === 0) {
14892
+ throw new WorkerError(
14893
+ "line_too_large",
14894
+ "The first requested line exceeds the 64 KiB range limit. Use read_file instead."
14895
+ );
14896
+ }
14897
+ break;
14898
+ }
14899
+ selected.push(line);
14900
+ contentBytes += addedBytes;
14901
+ }
14902
+ const endLine = startLine + selected.length - 1;
14903
+ return {
14904
+ content: selected.join("\n"),
14905
+ startLine,
14906
+ endLine,
14907
+ totalLines: lines.length,
14908
+ sha256: file2.sha256,
14909
+ bytes: file2.bytes,
14910
+ contentBytes,
14911
+ ...endLine < lines.length ? { nextLine: endLine + 1 } : {}
14912
+ };
14913
+ }
14763
14914
  async editText(relativePath, edits, expectedSha256) {
14764
14915
  const original = await this.readText(relativePath);
14765
14916
  if (expectedSha256 && original.sha256 !== expectedSha256) {
@@ -14827,7 +14978,7 @@ var FileService = class {
14827
14978
  throw new WorkerError("stale_revision", "The file revision has changed.");
14828
14979
  }
14829
14980
  }
14830
- const temporary = path6.join(path6.dirname(target), `.glossa-${randomUUID2()}.tmp`);
14981
+ const temporary = path5.join(path5.dirname(target), `.glossa-${randomUUID2()}.tmp`);
14831
14982
  try {
14832
14983
  await writeFile3(temporary, bytes, { flag: "wx", mode: 384 });
14833
14984
  target = await this.policy.resolveWritableFile(relativePath);
@@ -14849,29 +15000,37 @@ var FileService = class {
14849
15000
  // src/worker/path-policy.ts
14850
15001
  import { lstat as lstat2, realpath, stat as stat2 } from "node:fs/promises";
14851
15002
  import os3 from "node:os";
14852
- import path7 from "node:path";
15003
+ import path6 from "node:path";
14853
15004
  function samePath(left, right) {
14854
15005
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
14855
15006
  }
15007
+ function accountHomeDirectory() {
15008
+ try {
15009
+ return os3.userInfo().homedir;
15010
+ } catch {
15011
+ return os3.homedir();
15012
+ }
15013
+ }
14856
15014
  function isWithin(root, candidate) {
14857
- const relative = path7.relative(root, candidate);
14858
- return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
15015
+ const relative = path6.relative(root, candidate);
15016
+ return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
14859
15017
  }
14860
15018
  function validateRelativePath(value) {
14861
15019
  if (value.includes("\0")) {
14862
15020
  throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
14863
15021
  }
14864
- if (path7.isAbsolute(value) || path7.win32.isAbsolute(value) || path7.posix.isAbsolute(value)) {
15022
+ const explicitNativePosixPath = process.platform !== "win32" && value.startsWith("./");
15023
+ if (path6.isAbsolute(value) || path6.posix.isAbsolute(value) || !explicitNativePosixPath && path6.win32.isAbsolute(value)) {
14865
15024
  throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
14866
15025
  }
14867
- const segments = value.split(/[\\/]+/);
15026
+ const segments = explicitNativePosixPath ? value.split(/\/+/).filter(Boolean) : value.split(/[\\/]+/);
14868
15027
  if (segments.includes("..")) {
14869
15028
  throw new WorkerError("path_traversal", "Parent path traversal is not allowed.");
14870
15029
  }
14871
15030
  return value === "" ? "." : value;
14872
15031
  }
14873
- async function canonicalizeRoot(candidate, allowBroadRoot = false) {
14874
- const root = await realpath(path7.resolve(candidate)).catch((error46) => {
15032
+ async function canonicalizeRoot(candidate) {
15033
+ const root = await realpath(path6.resolve(candidate)).catch((error46) => {
14875
15034
  if (error46.code === "ENOENT") {
14876
15035
  throw new WorkerError("root_not_found", "The workspace directory does not exist.");
14877
15036
  }
@@ -14881,16 +15040,19 @@ async function canonicalizeRoot(candidate, allowBroadRoot = false) {
14881
15040
  if (!rootStat.isDirectory()) {
14882
15041
  throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
14883
15042
  }
14884
- if (!allowBroadRoot) {
14885
- const filesystemRoot = path7.parse(root).root;
14886
- const home = await realpath(os3.homedir()).catch(() => path7.resolve(os3.homedir()));
14887
- if (samePath(root, filesystemRoot) || samePath(root, home)) {
14888
- const kind = samePath(root, home) ? "your home directory" : "a filesystem root";
14889
- throw new WorkerError(
14890
- "broad_root_refused",
14891
- `The selected root is ${kind}, which Glossa protects by default. Add --allow-broad-root to expose it anyway.`
14892
- );
14893
- }
15043
+ const filesystemRoot = path6.parse(root).root;
15044
+ const homes = await Promise.all(
15045
+ [os3.homedir(), accountHomeDirectory()].map(
15046
+ async (home) => await realpath(home).catch(() => path6.resolve(home))
15047
+ )
15048
+ );
15049
+ const isHomeDirectory = homes.some((home) => samePath(root, home));
15050
+ if (samePath(root, filesystemRoot) || isHomeDirectory) {
15051
+ const kind = isHomeDirectory ? "your home directory" : "a filesystem root";
15052
+ throw new WorkerError(
15053
+ "broad_root_refused",
15054
+ `The selected root is ${kind}, which Glossa will not expose. Choose a project directory instead.`
15055
+ );
14894
15056
  }
14895
15057
  return root;
14896
15058
  }
@@ -14899,8 +15061,8 @@ var PathPolicy = class _PathPolicy {
14899
15061
  this.root = root;
14900
15062
  }
14901
15063
  root;
14902
- static async create(candidate, allowBroadRoot = false) {
14903
- return new _PathPolicy(await canonicalizeRoot(candidate, allowBroadRoot));
15064
+ static async create(candidate) {
15065
+ return new _PathPolicy(await canonicalizeRoot(candidate));
14904
15066
  }
14905
15067
  async resolveExisting(relativePath) {
14906
15068
  const lexical = this.resolveLexical(relativePath);
@@ -14916,9 +15078,26 @@ var PathPolicy = class _PathPolicy {
14916
15078
  }
14917
15079
  return canonical;
14918
15080
  }
15081
+ async resolveDiscoveredExisting(candidate) {
15082
+ const lexical = path6.resolve(candidate);
15083
+ if (!isWithin(this.root, lexical)) {
15084
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
15085
+ }
15086
+ await this.rejectLinkedComponents(lexical);
15087
+ const canonical = await realpath(lexical).catch((error46) => {
15088
+ if (error46.code === "ENOENT") {
15089
+ throw new WorkerError("path_not_found", "The requested path does not exist.");
15090
+ }
15091
+ throw error46;
15092
+ });
15093
+ if (!isWithin(this.root, canonical)) {
15094
+ throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
15095
+ }
15096
+ return canonical;
15097
+ }
14919
15098
  async resolveWritableFile(relativePath) {
14920
15099
  const lexical = this.resolveLexical(relativePath);
14921
- const parent = path7.dirname(lexical);
15100
+ const parent = path6.dirname(lexical);
14922
15101
  await this.rejectLinkedComponents(parent);
14923
15102
  const canonicalParent = await realpath(parent).catch((error46) => {
14924
15103
  if (error46.code === "ENOENT") {
@@ -14944,11 +15123,11 @@ var PathPolicy = class _PathPolicy {
14944
15123
  if (error46 instanceof WorkerError) throw error46;
14945
15124
  if (error46.code !== "ENOENT") throw error46;
14946
15125
  }
14947
- return path7.join(canonicalParent, path7.basename(lexical));
15126
+ return path6.join(canonicalParent, path6.basename(lexical));
14948
15127
  }
14949
15128
  resolveLexical(relativePath) {
14950
15129
  const validated = validateRelativePath(relativePath);
14951
- const candidate = path7.resolve(this.root, validated);
15130
+ const candidate = path6.resolve(this.root, validated);
14952
15131
  if (!isWithin(this.root, candidate)) {
14953
15132
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
14954
15133
  }
@@ -14958,11 +15137,11 @@ var PathPolicy = class _PathPolicy {
14958
15137
  if (!isWithin(this.root, candidate)) {
14959
15138
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
14960
15139
  }
14961
- const relative = path7.relative(this.root, candidate);
15140
+ const relative = path6.relative(this.root, candidate);
14962
15141
  if (!relative) return;
14963
15142
  let current = this.root;
14964
- for (const segment of relative.split(path7.sep)) {
14965
- current = path7.join(current, segment);
15143
+ for (const segment of relative.split(path6.sep)) {
15144
+ current = path6.join(current, segment);
14966
15145
  try {
14967
15146
  const currentStat = await lstat2(current);
14968
15147
  if (currentStat.isSymbolicLink()) {
@@ -14982,16 +15161,16 @@ var PathPolicy = class _PathPolicy {
14982
15161
 
14983
15162
  // src/worker/local-worker.ts
14984
15163
  var LocalWorker = class _LocalWorker {
14985
- constructor(policy, files, commands2) {
15164
+ constructor(policy, files, commands) {
14986
15165
  this.policy = policy;
14987
15166
  this.files = files;
14988
- this.commands = commands2;
15167
+ this.commands = commands;
14989
15168
  }
14990
15169
  policy;
14991
15170
  files;
14992
15171
  commands;
14993
- static async create(root, allowBroadRoot = false) {
14994
- const policy = await PathPolicy.create(root, allowBroadRoot);
15172
+ static async create(root) {
15173
+ const policy = await PathPolicy.create(root);
14995
15174
  return new _LocalWorker(
14996
15175
  policy,
14997
15176
  new FileService(policy),
@@ -15005,6 +15184,33 @@ var LocalWorker = class _LocalWorker {
15005
15184
  case "read_file":
15006
15185
  value = await this.files.readText(job.path);
15007
15186
  break;
15187
+ case "list_files":
15188
+ value = await this.files.listFiles({
15189
+ ...job.path ? { path: job.path } : {},
15190
+ ...job.recursive === void 0 ? {} : { recursive: job.recursive },
15191
+ ...job.cursor ? { cursor: job.cursor } : {},
15192
+ ...job.limit === void 0 ? {} : { limit: job.limit },
15193
+ timeoutMs: job.timeoutMs
15194
+ });
15195
+ break;
15196
+ case "search_text":
15197
+ value = await this.files.searchText({
15198
+ query: job.query,
15199
+ ...job.path ? { path: job.path } : {},
15200
+ ...job.caseSensitive === void 0 ? {} : { caseSensitive: job.caseSensitive },
15201
+ ...job.maxResults === void 0 ? {} : { maxResults: job.maxResults },
15202
+ ...job.extensions ? { extensions: job.extensions } : {},
15203
+ timeoutMs: job.timeoutMs
15204
+ });
15205
+ break;
15206
+ case "read_file_range":
15207
+ value = await this.files.readTextRange(
15208
+ job.path,
15209
+ job.startLine,
15210
+ job.lineCount,
15211
+ job.timeoutMs
15212
+ );
15213
+ break;
15008
15214
  case "write_file":
15009
15215
  value = await this.files.writeText(
15010
15216
  job.path,
@@ -15029,7 +15235,11 @@ var LocalWorker = class _LocalWorker {
15029
15235
  });
15030
15236
  break;
15031
15237
  case "get_command":
15032
- value = await this.commands.get(job.commandId, job.waitMs);
15238
+ value = await this.commands.get(
15239
+ job.commandId,
15240
+ job.waitMs,
15241
+ job.afterSequence
15242
+ );
15033
15243
  break;
15034
15244
  case "cancel_command":
15035
15245
  value = await this.commands.cancel(job.commandId);
@@ -15056,6 +15266,9 @@ var WORKER_REQUEST_TIMEOUT_MS = 19e3;
15056
15266
  var DEFAULT_RECONNECT_BASE_MS = 500;
15057
15267
  var DEFAULT_RECONNECT_MAX_MS = 1e4;
15058
15268
  var DEFAULT_HEARTBEAT_MS = 15e3;
15269
+ var CONCURRENT_WORKER_POLL_MS = 1e3;
15270
+ var MAX_CONCURRENT_JOBS = 5;
15271
+ var WORKER_TOKEN_PATTERN = /^glw_[A-Za-z0-9_-]{43}$/;
15059
15272
  var DeviceRejectedError = class extends Error {
15060
15273
  constructor() {
15061
15274
  super("The relay rejected the device credential.");
@@ -15070,6 +15283,53 @@ var RelayResponseError = class extends Error {
15070
15283
  }
15071
15284
  status;
15072
15285
  };
15286
+ function optionalWorkerToken(value) {
15287
+ if (value === void 0) return void 0;
15288
+ if (typeof value !== "string" || !WORKER_TOKEN_PATTERN.test(value)) {
15289
+ throw new Error("The relay returned an invalid worker credential.");
15290
+ }
15291
+ return value;
15292
+ }
15293
+ function supportsCapability(value, capability) {
15294
+ if (typeof value !== "object" || value === null) return false;
15295
+ if (!("capabilities" in value)) return false;
15296
+ const capabilities = value.capabilities;
15297
+ if (typeof capabilities !== "object" || capabilities === null) return false;
15298
+ return capabilities[capability] === true;
15299
+ }
15300
+ function jobLane(job) {
15301
+ switch (job.type) {
15302
+ case "get_command":
15303
+ return "status";
15304
+ case "cancel_command":
15305
+ return "cancel";
15306
+ case "read_file":
15307
+ case "list_files":
15308
+ case "search_text":
15309
+ case "read_file_range":
15310
+ return "read";
15311
+ case "write_file":
15312
+ case "edit_file":
15313
+ case "run_command":
15314
+ return "mutation";
15315
+ }
15316
+ }
15317
+ function acceptedJobTypes(counts, total, structuredReads) {
15318
+ if (total >= MAX_CONCURRENT_JOBS) return [];
15319
+ const accepted = [];
15320
+ if (counts.status < 1) accepted.push("get_command");
15321
+ if (counts.cancel < 1) accepted.push("cancel_command");
15322
+ if (counts.read < 2) {
15323
+ accepted.push("read_file");
15324
+ if (structuredReads) {
15325
+ accepted.push("list_files", "search_text", "read_file_range");
15326
+ }
15327
+ }
15328
+ if (counts.mutation < 1) {
15329
+ accepted.push("write_file", "edit_file", "run_command");
15330
+ }
15331
+ return accepted;
15332
+ }
15073
15333
  function defaultSleep(milliseconds, signal) {
15074
15334
  if (signal.aborted) return Promise.reject(signal.reason);
15075
15335
  return new Promise((resolve, reject) => {
@@ -15092,6 +15352,7 @@ function reconnectDelayMs(failureCount, random, baseMs = DEFAULT_RECONNECT_BASE_
15092
15352
  var RemoteWorker = class {
15093
15353
  #origin;
15094
15354
  #deviceToken;
15355
+ #workspaceLabel;
15095
15356
  #worker;
15096
15357
  #signal;
15097
15358
  #fetcher;
@@ -15105,6 +15366,7 @@ var RemoteWorker = class {
15105
15366
  constructor(options) {
15106
15367
  this.#origin = new URL(options.origin);
15107
15368
  this.#deviceToken = options.deviceToken;
15369
+ this.#workspaceLabel = options.workspaceLabel;
15108
15370
  this.#worker = options.worker;
15109
15371
  this.#signal = options.signal;
15110
15372
  this.#fetcher = options.fetcher ?? fetch;
@@ -15119,15 +15381,18 @@ var RemoteWorker = class {
15119
15381
  async run() {
15120
15382
  let failures = 0;
15121
15383
  let connectedBefore = false;
15384
+ let registeredSession;
15122
15385
  this.#onStatus({ state: "connecting" });
15123
15386
  try {
15124
15387
  while (!this.#signal.aborted) {
15125
15388
  try {
15126
15389
  const session = await this.#register();
15390
+ registeredSession = session;
15127
15391
  this.#onStatus({
15128
15392
  state: "connected",
15129
15393
  reconnected: connectedBefore,
15130
- legacyRelay: session.legacyRelay
15394
+ legacyRelay: session.legacyRelay,
15395
+ workspaceLabelAccepted: session.workspaceLabelAccepted
15131
15396
  });
15132
15397
  connectedBefore = true;
15133
15398
  failures = 0;
@@ -15156,350 +15421,1230 @@ var RemoteWorker = class {
15156
15421
  }
15157
15422
  }
15158
15423
  } finally {
15159
- await this.#unregister();
15424
+ await this.#unregister(registeredSession);
15160
15425
  this.#onStatus({ state: "disconnected" });
15161
15426
  }
15162
15427
  }
15163
15428
  async #register() {
15164
- let response;
15165
- try {
15166
- response = await this.#post("/device/register", {
15167
- workerId: this.#workerId
15168
- });
15169
- } catch (error46) {
15170
- if (!(error46 instanceof RelayResponseError) || error46.status !== 400) {
15429
+ const structuredBody = {
15430
+ workerId: this.#workerId,
15431
+ capabilities: {
15432
+ commandProgress: true,
15433
+ concurrentJobs: true,
15434
+ structuredReads: true
15435
+ }
15436
+ };
15437
+ const concurrentBody = {
15438
+ workerId: this.#workerId,
15439
+ capabilities: { commandProgress: true, concurrentJobs: true }
15440
+ };
15441
+ const attempts = [
15442
+ ...this.#workspaceLabel ? [{
15443
+ body: {
15444
+ ...structuredBody,
15445
+ workspaceLabel: this.#workspaceLabel
15446
+ },
15447
+ legacyRelay: false
15448
+ }] : [],
15449
+ {
15450
+ body: structuredBody,
15451
+ legacyRelay: false
15452
+ },
15453
+ ...this.#workspaceLabel ? [{
15454
+ body: {
15455
+ ...concurrentBody,
15456
+ workspaceLabel: this.#workspaceLabel
15457
+ },
15458
+ legacyRelay: false
15459
+ }] : [],
15460
+ {
15461
+ body: concurrentBody,
15462
+ legacyRelay: false
15463
+ },
15464
+ {
15465
+ body: {
15466
+ workerId: this.#workerId,
15467
+ capabilities: { commandProgress: true }
15468
+ },
15469
+ legacyRelay: false
15470
+ },
15471
+ { body: { workerId: this.#workerId }, legacyRelay: false },
15472
+ { body: {}, legacyRelay: true }
15473
+ ];
15474
+ for (const [index, attempt] of attempts.entries()) {
15475
+ let response;
15476
+ try {
15477
+ response = await this.#post("/device/register", attempt.body);
15478
+ } catch (error46) {
15479
+ if (error46 instanceof RelayResponseError && error46.status === 400 && index < attempts.length - 1) {
15480
+ continue;
15481
+ }
15171
15482
  throw error46;
15172
15483
  }
15173
- response = await this.#post("/device/register", {});
15174
- const legacyValue = await response.json();
15175
- if (typeof legacyValue !== "object" || legacyValue === null || !("generation" in legacyValue) || typeof legacyValue.generation !== "string") {
15484
+ const value = await response.json();
15485
+ if (typeof value !== "object" || value === null || !("generation" in value) || typeof value.generation !== "string") {
15176
15486
  throw new Error("The relay returned an invalid registration response.");
15177
15487
  }
15178
- return { generation: legacyValue.generation, legacyRelay: true };
15179
- }
15180
- const value = await response.json();
15181
- if (typeof value !== "object" || value === null || !("generation" in value) || typeof value.generation !== "string" || !("workerId" in value) || value.workerId !== this.#workerId) {
15182
- throw new Error("The relay returned an invalid registration response.");
15488
+ if (!attempt.legacyRelay && (!("workerId" in value) || value.workerId !== this.#workerId)) {
15489
+ throw new Error("The relay returned an invalid registration response.");
15490
+ }
15491
+ const workerToken = "workerToken" in value ? optionalWorkerToken(value.workerToken) : void 0;
15492
+ return {
15493
+ generation: value.generation,
15494
+ legacyRelay: attempt.legacyRelay,
15495
+ concurrentJobs: !attempt.legacyRelay && supportsCapability(value, "concurrentJobs"),
15496
+ structuredReads: !attempt.legacyRelay && supportsCapability(value, "structuredReads"),
15497
+ workspaceLabelAccepted: this.#workspaceLabel === void 0 || "workspaceLabel" in value && value.workspaceLabel === this.#workspaceLabel,
15498
+ ...workerToken ? { workerToken } : {}
15499
+ };
15183
15500
  }
15184
- return { generation: value.generation, legacyRelay: false };
15501
+ throw new Error("The relay rejected every supported registration shape.");
15185
15502
  }
15186
15503
  async #pollGeneration(session) {
15504
+ if (session.legacyRelay || !session.concurrentJobs) {
15505
+ await this.#pollSequentially(session);
15506
+ return;
15507
+ }
15508
+ await this.#pollConcurrently(session);
15509
+ }
15510
+ async #pollSequentially(session) {
15187
15511
  while (!this.#signal.aborted) {
15188
- const response = await this.#post(
15189
- "/device/poll",
15190
- session.legacyRelay ? { generation: session.generation } : { workerId: this.#workerId, generation: session.generation }
15191
- );
15192
- if (response.status === 204) continue;
15193
- const value = await response.json();
15194
- const parsed = workerJobSchema.safeParse(
15195
- typeof value === "object" && value !== null && "job" in value ? value.job : void 0
15196
- );
15197
- if (!parsed.success) {
15198
- throw new Error("The relay returned an invalid worker job.");
15199
- }
15200
- const heartbeat = session.legacyRelay ? void 0 : setInterval(() => {
15201
- void this.#post("/device/heartbeat", {
15202
- workerId: this.#workerId,
15203
- generation: session.generation
15204
- }).catch(() => {
15512
+ const job = await this.#pollForJob(session);
15513
+ if (!job) continue;
15514
+ await this.#handleAndPost(session, job, true);
15515
+ }
15516
+ }
15517
+ async #pollConcurrently(session) {
15518
+ const counts = {
15519
+ status: 0,
15520
+ cancel: 0,
15521
+ read: 0,
15522
+ mutation: 0
15523
+ };
15524
+ const inFlight = /* @__PURE__ */ new Set();
15525
+ let failure;
15526
+ let heartbeat;
15527
+ const stopHeartbeat = () => {
15528
+ if (!heartbeat) return;
15529
+ clearInterval(heartbeat);
15530
+ heartbeat = void 0;
15531
+ };
15532
+ const ensureHeartbeat = () => {
15533
+ if (heartbeat) return;
15534
+ heartbeat = setInterval(() => {
15535
+ void this.#post(
15536
+ "/device/heartbeat",
15537
+ { workerId: this.#workerId, generation: session.generation },
15538
+ session.workerToken
15539
+ ).catch(() => {
15205
15540
  });
15206
15541
  }, this.#heartbeatMs);
15207
- heartbeat?.unref();
15208
- let result;
15209
- try {
15210
- result = await this.#worker.handle(parsed.data);
15211
- } finally {
15212
- if (heartbeat) clearInterval(heartbeat);
15542
+ heartbeat.unref();
15543
+ };
15544
+ const dispatch = (job) => {
15545
+ const lane = jobLane(job);
15546
+ counts[lane] += 1;
15547
+ ensureHeartbeat();
15548
+ let task;
15549
+ task = this.#handleAndPost(session, job, false).catch((error46) => {
15550
+ failure ??= error46;
15551
+ }).finally(() => {
15552
+ counts[lane] -= 1;
15553
+ inFlight.delete(task);
15554
+ if (inFlight.size === 0) stopHeartbeat();
15555
+ });
15556
+ inFlight.add(task);
15557
+ };
15558
+ try {
15559
+ while (!this.#signal.aborted) {
15560
+ if (failure !== void 0) throw failure;
15561
+ const acceptedTypes = acceptedJobTypes(
15562
+ counts,
15563
+ inFlight.size,
15564
+ session.structuredReads
15565
+ );
15566
+ if (acceptedTypes.length === 0) {
15567
+ await Promise.race(inFlight);
15568
+ continue;
15569
+ }
15570
+ const job = await this.#pollForJob(
15571
+ session,
15572
+ acceptedTypes,
15573
+ inFlight.size > 0 ? CONCURRENT_WORKER_POLL_MS : void 0
15574
+ );
15575
+ if (!job) continue;
15576
+ if (!acceptedTypes.includes(job.type)) {
15577
+ throw new Error("The relay delivered a job outside worker capacity.");
15578
+ }
15579
+ dispatch(job);
15213
15580
  }
15581
+ } finally {
15582
+ stopHeartbeat();
15583
+ await Promise.allSettled(inFlight);
15584
+ }
15585
+ if (failure !== void 0) throw failure;
15586
+ }
15587
+ async #pollForJob(session, acceptedTypes, waitMs) {
15588
+ const response = await this.#post(
15589
+ "/device/poll",
15590
+ session.legacyRelay ? { generation: session.generation } : {
15591
+ workerId: this.#workerId,
15592
+ generation: session.generation,
15593
+ ...acceptedTypes ? { acceptedTypes } : {},
15594
+ ...waitMs === void 0 ? {} : { waitMs }
15595
+ },
15596
+ session.workerToken
15597
+ );
15598
+ if (response.status === 204) return null;
15599
+ const value = await response.json();
15600
+ const parsed = workerJobSchema.safeParse(
15601
+ typeof value === "object" && value !== null && "job" in value ? value.job : void 0
15602
+ );
15603
+ if (!parsed.success) {
15604
+ throw new Error("The relay returned an invalid worker job.");
15605
+ }
15606
+ return parsed.data;
15607
+ }
15608
+ async #handleAndPost(session, job, heartbeatWhileRunning) {
15609
+ const heartbeat = heartbeatWhileRunning && !session.legacyRelay ? setInterval(() => {
15610
+ void this.#post(
15611
+ "/device/heartbeat",
15612
+ { workerId: this.#workerId, generation: session.generation },
15613
+ session.workerToken
15614
+ ).catch(() => {
15615
+ });
15616
+ }, this.#heartbeatMs) : void 0;
15617
+ heartbeat?.unref();
15618
+ let result;
15619
+ try {
15620
+ result = await this.#worker.handle(job);
15621
+ } finally {
15622
+ if (heartbeat) clearInterval(heartbeat);
15623
+ }
15624
+ try {
15214
15625
  await this.#post(
15215
15626
  "/device/result",
15216
- session.legacyRelay ? result : { workerId: this.#workerId, result }
15627
+ session.legacyRelay ? result : { workerId: this.#workerId, result },
15628
+ session.workerToken
15217
15629
  );
15630
+ } catch (error46) {
15631
+ if (error46 instanceof RelayResponseError && error46.status === 410) return;
15632
+ throw error46;
15218
15633
  }
15219
15634
  }
15220
- async #unregister() {
15221
- try {
15222
- await this.#fetcher(new URL("/device/unregister", this.#origin), {
15223
- method: "POST",
15224
- headers: {
15225
- authorization: `Device ${this.#deviceToken}`,
15226
- "content-type": "application/json"
15227
- },
15228
- body: JSON.stringify({ workerId: this.#workerId }),
15229
- signal: AbortSignal.timeout(3e3)
15230
- });
15231
- } catch {
15635
+ async #unregister(session) {
15636
+ const unregister = async (authorization) => {
15637
+ try {
15638
+ const response = await this.#fetcher(
15639
+ new URL("/device/unregister", this.#origin),
15640
+ {
15641
+ method: "POST",
15642
+ headers: {
15643
+ authorization,
15644
+ "content-type": "application/json"
15645
+ },
15646
+ body: JSON.stringify({ workerId: this.#workerId }),
15647
+ signal: AbortSignal.timeout(3e3)
15648
+ }
15649
+ );
15650
+ if (response.ok) return "accepted";
15651
+ return [400, 401, 404, 409].includes(response.status) ? "rejected" : "unreachable";
15652
+ } catch {
15653
+ return "unreachable";
15654
+ }
15655
+ };
15656
+ if (!session?.workerToken) {
15657
+ await unregister(`Device ${this.#deviceToken}`);
15658
+ return;
15659
+ }
15660
+ const result = await unregister(`Worker ${session.workerToken}`);
15661
+ if (result === "rejected") {
15662
+ await unregister(`Device ${this.#deviceToken}`);
15232
15663
  }
15233
15664
  }
15234
- async #post(path8, body) {
15665
+ async #post(path8, body, workerToken) {
15235
15666
  const timeout = AbortSignal.timeout(WORKER_REQUEST_TIMEOUT_MS);
15236
15667
  const signal = AbortSignal.any([this.#signal, timeout]);
15237
15668
  const response = await this.#fetcher(new URL(path8, this.#origin), {
15238
15669
  method: "POST",
15239
15670
  headers: {
15240
- authorization: `Device ${this.#deviceToken}`,
15671
+ authorization: workerToken ? `Worker ${workerToken}` : `Device ${this.#deviceToken}`,
15241
15672
  "content-type": "application/json"
15242
15673
  },
15243
15674
  body: JSON.stringify(body),
15244
15675
  signal
15245
15676
  });
15246
- if (response.status === 401) throw new DeviceRejectedError();
15677
+ if (response.status === 401 && !workerToken) throw new DeviceRejectedError();
15247
15678
  if (!response.ok) throw new RelayResponseError(response.status);
15248
15679
  return response;
15249
15680
  }
15250
15681
  };
15251
15682
 
15252
15683
  // src/worker/managed-session.ts
15253
- var visibleActivity = /* @__PURE__ */ new Set([
15254
- "write_file",
15255
- "edit_file",
15256
- "run_command",
15257
- "cancel_command"
15258
- ]);
15259
- function activityLabel2(type, finished, ok = true) {
15260
- if (type === "run_command") {
15261
- return finished ? ok ? "Command started" : "Command rejected" : "Command requested";
15684
+ function report(options, event, message) {
15685
+ options.onEvent?.(event);
15686
+ if (!options.quiet) console.error(message);
15687
+ }
15688
+ function activityResultLabel(job, result) {
15689
+ if (!result.ok) return `${job.type} failed`;
15690
+ if (job.type === "run_command" && result.value && typeof result.value === "object" && "status" in result.value && result.value.status === "running") {
15691
+ return "run_command started";
15692
+ }
15693
+ return `${job.type} completed`;
15694
+ }
15695
+ function visibleWorker(worker, options) {
15696
+ return {
15697
+ async handle(job) {
15698
+ options.onEvent?.({ type: "activity", phase: "started", job });
15699
+ try {
15700
+ const result = await worker.handle(job);
15701
+ report(
15702
+ options,
15703
+ { type: "activity", phase: "returned", job, ok: result.ok },
15704
+ `${activityResultLabel(job, result)} (${job.requestId}).`
15705
+ );
15706
+ return result;
15707
+ } catch (error46) {
15708
+ report(
15709
+ options,
15710
+ { type: "activity", phase: "returned", job, ok: false },
15711
+ `${job.type} failed (${job.requestId}).`
15712
+ );
15713
+ throw error46;
15714
+ }
15715
+ }
15716
+ };
15717
+ }
15718
+ async function deviceForSession(endpoints, dependencies = {}, signal) {
15719
+ const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
15720
+ const loadLogin = dependencies.loadCredentials ?? loadCredentials;
15721
+ const validate = dependencies.validCredentials ?? validCredentials;
15722
+ const subjectFor = dependencies.accessTokenSubject ?? accessTokenSubject;
15723
+ const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
15724
+ const enroll = dependencies.enrollDevice ?? enrollDevice;
15725
+ const saveDevice = dependencies.saveDeviceCredential ?? saveDeviceCredential;
15726
+ const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
15727
+ const name = dependencies.defaultDeviceName ?? defaultDeviceName;
15728
+ const baseFetch = dependencies.fetch ?? fetch;
15729
+ const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
15730
+ signal?.throwIfAborted();
15731
+ const stored = await loadDevice();
15732
+ let credentials = dependencies.credentials;
15733
+ const currentCredentials = async () => {
15734
+ if (credentials) return credentials;
15735
+ const loaded = await loadLogin();
15736
+ if (!loaded) throw new Error("Not signed in. Run Glossa again to sign in.");
15737
+ credentials = await validate(loaded.credentials, { fetch: fetchRequest });
15738
+ return credentials;
15739
+ };
15740
+ if (stored?.relayOrigin === endpoints.relayOrigin) {
15741
+ const current2 = await currentCredentials();
15742
+ const accountSubject = subjectFor(current2);
15743
+ if (stored.accountSubject === accountSubject) return stored;
15744
+ if (stored.accountSubject === void 0 && await ownsDevice(endpoints, current2, stored.deviceId, fetchRequest)) {
15745
+ const migrated = { ...stored, accountSubject };
15746
+ await saveDevice(migrated);
15747
+ return migrated;
15748
+ }
15749
+ await removeDevice();
15750
+ }
15751
+ signal?.throwIfAborted();
15752
+ const current = await currentCredentials();
15753
+ const enrolled = await enroll(
15754
+ endpoints,
15755
+ current,
15756
+ name(),
15757
+ fetchRequest
15758
+ );
15759
+ const bound = {
15760
+ ...enrolled,
15761
+ accountSubject: subjectFor(current)
15762
+ };
15763
+ await saveDevice(bound);
15764
+ return bound;
15765
+ }
15766
+ async function reenrollRejectedDevice(endpoints, dependencies = {}, signal) {
15767
+ const remove = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
15768
+ await remove();
15769
+ return await deviceForSession(endpoints, dependencies, signal);
15770
+ }
15771
+ function retryMessage(retryInMs) {
15772
+ const seconds = Math.max(1, Math.ceil(retryInMs / 1e3));
15773
+ return `Retrying in ${seconds} ${seconds === 1 ? "second" : "seconds"}.`;
15774
+ }
15775
+ function workspaceLabelNotice(status, requestedLabel) {
15776
+ if (status.state !== "connected" || !requestedLabel || status.workspaceLabelAccepted !== false) {
15777
+ return void 0;
15778
+ }
15779
+ return "The relay needs an update before workspace labels are available. This workspace is online without the requested label.";
15780
+ }
15781
+ var legacyRelayNotice = "The relay needs an update before this computer can expose several workspaces at once.";
15782
+ function combinedCompatibilityNotice(labelNotice, includeLegacyRelayNotice) {
15783
+ const messages = [
15784
+ labelNotice,
15785
+ includeLegacyRelayNotice ? legacyRelayNotice : void 0
15786
+ ].filter((message) => Boolean(message));
15787
+ return messages.length > 0 ? messages.join(" ") : void 0;
15788
+ }
15789
+ function statusMessage(status, connectedBefore) {
15790
+ if (status.state === "connecting") return "Connecting to Glossa...";
15791
+ if (status.state === "connected") {
15792
+ return status.reconnected ? "Reconnected to Glossa." : "Connected to Glossa. ChatGPT can now use this workspace.";
15793
+ }
15794
+ if (status.state === "retrying") {
15795
+ const prefix = connectedBefore ? "Connection lost" : "Could not connect";
15796
+ return `${prefix}: ${status.error.message} ${retryMessage(status.retryInMs)}`;
15797
+ }
15798
+ return "Disconnected from Glossa.";
15799
+ }
15800
+ async function connectRemoteWorker(endpoints, device, worker, options, signal, onConnected) {
15801
+ let connectionState;
15802
+ let connectedBefore = false;
15803
+ let labelNoticeShown = false;
15804
+ let legacyNoticeShown = false;
15805
+ let connectHintTask;
15806
+ const remoteWorker = new RemoteWorker({
15807
+ origin: endpoints.workerOrigin,
15808
+ deviceToken: device.token,
15809
+ ...options.workspaceLabel ? { workspaceLabel: options.workspaceLabel } : {},
15810
+ worker: visibleWorker(worker, options),
15811
+ signal,
15812
+ onStatus(status) {
15813
+ if (status.state === "connected") {
15814
+ connectedBefore = true;
15815
+ onConnected();
15816
+ }
15817
+ if (status.state !== "retrying" || connectionState !== "retrying") {
15818
+ report(options, { type: "status", status }, statusMessage(status, connectedBefore));
15819
+ } else {
15820
+ options.onEvent?.({ type: "status", status });
15821
+ }
15822
+ const labelNotice = labelNoticeShown ? void 0 : workspaceLabelNotice(status, options.workspaceLabel);
15823
+ const includeLegacyNotice = status.state === "connected" && status.legacyRelay && !legacyNoticeShown;
15824
+ const compatibilityNotice = combinedCompatibilityNotice(
15825
+ labelNotice,
15826
+ includeLegacyNotice
15827
+ );
15828
+ if (labelNotice) labelNoticeShown = true;
15829
+ if (includeLegacyNotice) legacyNoticeShown = true;
15830
+ if (compatibilityNotice) {
15831
+ report(
15832
+ options,
15833
+ { type: "notice", message: compatibilityNotice },
15834
+ compatibilityNotice
15835
+ );
15836
+ }
15837
+ if (status.state === "connected" && !status.reconnected && !compatibilityNotice && shouldShowConnectHint(endpoints.relayOrigin) && !connectHintTask) {
15838
+ connectHintTask = announceConnectHint(
15839
+ connectHintStore(),
15840
+ (message) => {
15841
+ report(
15842
+ options,
15843
+ { type: "notice", message, persistAfterExit: true },
15844
+ message
15845
+ );
15846
+ }
15847
+ ).then(() => void 0).catch(() => void 0);
15848
+ }
15849
+ connectionState = status.state;
15850
+ }
15851
+ });
15852
+ try {
15853
+ await remoteWorker.run();
15854
+ } finally {
15855
+ await connectHintTask;
15856
+ }
15857
+ }
15858
+ function shouldRecoverRejectedDevice(error46, recoveredRejectedDevice, connected) {
15859
+ return error46 instanceof DeviceRejectedError && !recoveredRejectedDevice && !connected;
15860
+ }
15861
+ async function runManagedSession(root, endpoints, options = {}) {
15862
+ const controller = new AbortController();
15863
+ const stop = () => controller.abort();
15864
+ const handleProcessSignals = options.handleProcessSignals ?? true;
15865
+ let worker;
15866
+ if (options.signal?.aborted) controller.abort();
15867
+ else options.signal?.addEventListener("abort", stop, { once: true });
15868
+ if (handleProcessSignals) {
15869
+ process.once("SIGINT", stop);
15870
+ process.once("SIGTERM", stop);
15871
+ }
15872
+ try {
15873
+ let device = await deviceForSession(
15874
+ endpoints,
15875
+ options.credentials ? { credentials: options.credentials } : {},
15876
+ controller.signal
15877
+ );
15878
+ controller.signal.throwIfAborted();
15879
+ worker = await LocalWorker.create(root);
15880
+ controller.signal.throwIfAborted();
15881
+ report(
15882
+ options,
15883
+ { type: "session", root: worker.policy.root, deviceName: device.deviceName },
15884
+ `Glossa worker root: ${worker.policy.root}`
15885
+ );
15886
+ if (!options.quiet) {
15887
+ console.error(`Glossa device: ${device.deviceName}`);
15888
+ console.error(
15889
+ "Files may be modified and commands have the full environment and permissions of this account. Press Ctrl+C to disconnect."
15890
+ );
15891
+ }
15892
+ let recoveredRejectedDevice = false;
15893
+ while (!controller.signal.aborted) {
15894
+ let connected = false;
15895
+ try {
15896
+ await connectRemoteWorker(
15897
+ endpoints,
15898
+ device,
15899
+ worker,
15900
+ options,
15901
+ controller.signal,
15902
+ () => {
15903
+ connected = true;
15904
+ }
15905
+ );
15906
+ break;
15907
+ } catch (error46) {
15908
+ if (!shouldRecoverRejectedDevice(
15909
+ error46,
15910
+ recoveredRejectedDevice,
15911
+ connected
15912
+ )) {
15913
+ throw error46;
15914
+ }
15915
+ recoveredRejectedDevice = true;
15916
+ device = await reenrollRejectedDevice(
15917
+ endpoints,
15918
+ options.credentials ? { credentials: options.credentials } : {},
15919
+ controller.signal
15920
+ );
15921
+ }
15922
+ }
15923
+ } catch (error46) {
15924
+ if (error46 instanceof DeviceRejectedError) {
15925
+ await deleteDeviceCredential();
15926
+ throw new Error("The relay rejected this device. Run Glossa again to reenroll it.");
15927
+ }
15928
+ throw error46;
15929
+ } finally {
15930
+ options.signal?.removeEventListener("abort", stop);
15931
+ if (handleProcessSignals) {
15932
+ process.removeListener("SIGINT", stop);
15933
+ process.removeListener("SIGTERM", stop);
15934
+ }
15935
+ await worker?.shutdown();
15936
+ }
15937
+ }
15938
+
15939
+ // src/ui-hud.ts
15940
+ function retainPostExitNotice(current, event) {
15941
+ return event.type === "notice" && event.persistAfterExit ? event.message : current;
15942
+ }
15943
+ function initialHudState(workspace) {
15944
+ return {
15945
+ workspace,
15946
+ connection: "starting",
15947
+ connectedBefore: false,
15948
+ message: void 0,
15949
+ activities: [],
15950
+ view: "session",
15951
+ status: void 0,
15952
+ statusLoading: false,
15953
+ prompt: void 0,
15954
+ busy: false,
15955
+ notice: void 0
15956
+ };
15957
+ }
15958
+ function truncate(value, width) {
15959
+ if (width <= 0) return "";
15960
+ if (value.length <= width) return value;
15961
+ if (width === 1) return "\u2026";
15962
+ return `${value.slice(0, width - 1)}\u2026`;
15963
+ }
15964
+ function compactJobBody(job) {
15965
+ const entries = Object.entries(job).filter(
15966
+ ([key]) => key !== "type" && key !== "requestId"
15967
+ );
15968
+ const body = JSON.stringify(Object.fromEntries(entries));
15969
+ return truncate(body, 360);
15970
+ }
15971
+ function applyHudEvent(state, event) {
15972
+ if (event.type === "session") {
15973
+ return { ...state, workspace: event.root, deviceName: event.deviceName };
15974
+ }
15975
+ if (event.type === "status") {
15976
+ if (event.status.state === "retrying") {
15977
+ return {
15978
+ ...state,
15979
+ connection: "retrying",
15980
+ message: statusMessage(event.status, state.connectedBefore)
15981
+ };
15982
+ }
15983
+ return {
15984
+ ...state,
15985
+ connection: event.status.state,
15986
+ connectedBefore: state.connectedBefore || event.status.state === "connected",
15987
+ message: void 0
15988
+ };
15989
+ }
15990
+ if (event.type === "notice") {
15991
+ return { ...state, notice: event.message };
15992
+ }
15993
+ const requestId = event.job.requestId;
15994
+ const existingIndex = state.activities.findIndex(
15995
+ (activity2) => activity2.requestId === requestId
15996
+ );
15997
+ const activity = {
15998
+ tool: event.job.type,
15999
+ body: compactJobBody(event.job),
16000
+ requestId,
16001
+ state: event.phase === "started" ? "working" : event.ok ? "returned" : "failed"
16002
+ };
16003
+ const activities = [...state.activities];
16004
+ if (existingIndex >= 0) activities[existingIndex] = activity;
16005
+ else activities.push(activity);
16006
+ return { ...state, activities: activities.slice(-20) };
16007
+ }
16008
+ var ANSI_BASE = "\x1B[22;38;2;244;241;251;48;2;17;16;22m";
16009
+ var PALETTE = {
16010
+ ink: "38;2;244;241;251",
16011
+ muted: "38;2;170;164;181",
16012
+ purple: "38;2;128;84;255",
16013
+ purpleReadable: "38;2;173;152;255",
16014
+ coral: "38;2;255;102;95",
16015
+ line: "38;2;92;85;110"
16016
+ };
16017
+ function style(enabled, code, value) {
16018
+ return enabled ? `\x1B[${code}m${value}${ANSI_BASE}` : value;
16019
+ }
16020
+ function sectionTitle(label, color, tone = PALETTE.purpleReadable) {
16021
+ return style(color, `${tone};1`, label.toUpperCase());
16022
+ }
16023
+ function connectionLabel(state) {
16024
+ if (state.connection === "connected") return "Connected";
16025
+ if (state.connection === "connecting" || state.connection === "starting") {
16026
+ return "Connecting";
16027
+ }
16028
+ if (state.connection === "retrying") return "Reconnecting";
16029
+ if (state.connection === "error") return "Error";
16030
+ return "Disconnected";
16031
+ }
16032
+ function headerLabel(state) {
16033
+ const running = [...state.activities].reverse().find(
16034
+ (activity) => activity.state === "working"
16035
+ );
16036
+ return running?.tool ?? connectionLabel(state);
16037
+ }
16038
+ function renderHeader(state, usable, color) {
16039
+ const brand = "Glossa";
16040
+ const label = truncate(
16041
+ headerLabel(state),
16042
+ Math.max(1, usable - brand.length - 1)
16043
+ );
16044
+ const gap = " ".repeat(Math.max(1, usable - brand.length - label.length));
16045
+ const tone = state.connection === "error" ? PALETTE.coral : PALETTE.purpleReadable;
16046
+ return [
16047
+ `${style(color, `${PALETTE.purple};1`, brand)}${gap}${style(color, `${tone};1`, label)}`,
16048
+ style(color, PALETTE.line, "\u2500".repeat(usable))
16049
+ ];
16050
+ }
16051
+ function renderSession(state, usable, color) {
16052
+ const lines = [
16053
+ "",
16054
+ sectionTitle("Workspace", color),
16055
+ style(color, PALETTE.ink, truncate(state.workspace, usable))
16056
+ ];
16057
+ if (state.deviceName) {
16058
+ lines.push(
16059
+ "",
16060
+ sectionTitle("Device", color),
16061
+ style(color, PALETTE.ink, truncate(state.deviceName, usable))
16062
+ );
16063
+ }
16064
+ if (state.message && (state.connection === "retrying" || state.connection === "error")) {
16065
+ lines.push(
16066
+ "",
16067
+ style(color, PALETTE.coral, truncate(state.message, usable))
16068
+ );
16069
+ }
16070
+ return lines;
16071
+ }
16072
+ function activityGlyph(activity, color) {
16073
+ if (activity.state === "working") {
16074
+ return style(color, PALETTE.muted, "\u25CC");
16075
+ }
16076
+ return activity.state === "failed" ? style(color, PALETTE.coral, "\xD7") : style(color, PALETTE.purpleReadable, "\u25CF");
16077
+ }
16078
+ function renderActivity(state, usable, color, bodyBudget) {
16079
+ const lines = ["", sectionTitle("Recent activity", color)];
16080
+ if (state.activities.length === 0) {
16081
+ lines.push("", style(color, PALETTE.muted, "No activity yet."));
16082
+ return lines;
16083
+ }
16084
+ const visibleEntryCount = Math.min(
16085
+ 8,
16086
+ Math.max(0, Math.floor((bodyBudget - lines.length) / 3))
16087
+ );
16088
+ if (visibleEntryCount === 0) return lines;
16089
+ for (const activity of state.activities.slice(-visibleEntryCount)) {
16090
+ lines.push(
16091
+ "",
16092
+ `${activityGlyph(activity, color)} ${style(color, `${PALETTE.ink};1`, truncate(activity.tool, Math.max(1, usable - 2)))}`,
16093
+ style(color, PALETTE.muted, truncate(activity.body, usable))
16094
+ );
16095
+ }
16096
+ return lines;
16097
+ }
16098
+ function metric(label, value, usable, color) {
16099
+ const visibleValue = truncate(value, Math.max(1, usable - 1));
16100
+ const visibleLabel = truncate(
16101
+ label,
16102
+ Math.max(0, usable - visibleValue.length - 1)
16103
+ );
16104
+ const gap = " ".repeat(
16105
+ Math.max(1, usable - visibleLabel.length - visibleValue.length)
16106
+ );
16107
+ return `${style(color, PALETTE.muted, visibleLabel)}${gap}${style(color, PALETTE.ink, visibleValue)}`;
16108
+ }
16109
+ function tableCell(value, width) {
16110
+ return truncate(value, width).padEnd(width);
16111
+ }
16112
+ function renderDeviceRows(device, index, usable, color) {
16113
+ const number4 = String(index + 1).padStart(2);
16114
+ const statusTone = device.status.includes("active") ? PALETTE.purpleReadable : PALETTE.muted;
16115
+ if (usable < 64) {
16116
+ const prefix = `${number4} `;
16117
+ const details = `${device.name} \xB7 ${device.status} \xB7 ${device.platform} \xB7 ${device.lastSeen}`;
16118
+ return [
16119
+ `${style(color, `${PALETTE.purpleReadable};1`, number4)} ${style(
16120
+ color,
16121
+ statusTone,
16122
+ truncate(details, Math.max(1, usable - prefix.length))
16123
+ )}`
16124
+ ];
16125
+ }
16126
+ const statusWidth = 16;
16127
+ const platformWidth = 12;
16128
+ const lastSeenWidth = Math.min(
16129
+ 18,
16130
+ Math.max(10, Math.floor(usable * 0.18))
16131
+ );
16132
+ const nameWidth = usable - 38 - lastSeenWidth;
16133
+ return [
16134
+ `${style(color, `${PALETTE.purpleReadable};1`, number4)} ${style(color, PALETTE.ink, tableCell(device.name, nameWidth))} ${style(color, statusTone, tableCell(device.status, statusWidth))} ${style(color, PALETTE.muted, tableCell(device.platform, platformWidth))} ${style(color, PALETTE.muted, tableCell(device.lastSeen, lastSeenWidth))}`
16135
+ ];
16136
+ }
16137
+ function deviceTableHeading(usable, color) {
16138
+ if (usable < 64) return void 0;
16139
+ const statusWidth = 16;
16140
+ const platformWidth = 12;
16141
+ const lastSeenWidth = Math.min(
16142
+ 18,
16143
+ Math.max(10, Math.floor(usable * 0.18))
16144
+ );
16145
+ const nameWidth = usable - 38 - lastSeenWidth;
16146
+ return style(
16147
+ color,
16148
+ PALETTE.muted,
16149
+ ` ${tableCell("Device", nameWidth)} ${tableCell("Workers", statusWidth)} ${tableCell("Platform", platformWidth)} ${tableCell("Last seen", lastSeenWidth)}`
16150
+ );
16151
+ }
16152
+ function renderStatus(state, usable, color, visibleDeviceCount) {
16153
+ const lines = ["", sectionTitle("Account", color)];
16154
+ if (state.statusLoading) {
16155
+ lines.push("", style(color, PALETTE.muted, "Loading status\u2026"));
16156
+ return lines;
15262
16157
  }
15263
- if (type === "write_file") {
15264
- return finished ? ok ? "File write completed" : "File write rejected" : "File write started";
16158
+ if (!state.status) {
16159
+ lines.push("", style(color, PALETTE.muted, "Status is not loaded."));
16160
+ return lines;
15265
16161
  }
15266
- if (type === "edit_file") {
15267
- return finished ? ok ? "File edit completed" : "File edit rejected" : "File edit started";
16162
+ const workerCount = state.status.activeWorkers === null ? "Unavailable" : String(state.status.activeWorkers);
16163
+ lines.push(
16164
+ style(color, PALETTE.ink, truncate(state.status.account, usable)),
16165
+ style(color, PALETTE.muted, truncate(state.status.relay, usable)),
16166
+ "",
16167
+ sectionTitle("Overview", color),
16168
+ metric("Active workspaces", workerCount, usable, color),
16169
+ metric("Devices", String(state.status.devices.length), usable, color),
16170
+ "",
16171
+ sectionTitle("Devices", color)
16172
+ );
16173
+ if (state.status.devices.length === 0) {
16174
+ lines.push("", style(color, PALETTE.muted, "No active devices."));
16175
+ return lines;
16176
+ }
16177
+ const heading = deviceTableHeading(usable, color);
16178
+ if (heading) lines.push(heading);
16179
+ state.status.devices.slice(0, visibleDeviceCount).forEach((device, index) => {
16180
+ lines.push(...renderDeviceRows(device, index, usable, color));
16181
+ });
16182
+ const hiddenCount = state.status.devices.length - visibleDeviceCount;
16183
+ if (hiddenCount > 0) {
16184
+ lines.push(
16185
+ style(
16186
+ color,
16187
+ PALETTE.muted,
16188
+ truncate(
16189
+ `${hiddenCount} more. Use glossa devices revoke <id>.`,
16190
+ usable
16191
+ )
16192
+ )
16193
+ );
15268
16194
  }
15269
- return finished ? ok ? "Command cancellation completed" : "Command cancellation rejected" : "Command cancellation requested";
16195
+ return lines;
15270
16196
  }
15271
- function report(options, event, message) {
15272
- options.onEvent?.(event);
15273
- if (!options.quiet) console.error(message);
16197
+ function helpRows(key, label, usable, color, tone = PALETTE.purpleReadable) {
16198
+ const available = Math.max(1, usable - key.length - 2);
16199
+ return [
16200
+ `${style(color, `${tone};1`, key)} ${truncate(label, available)}`
16201
+ ];
15274
16202
  }
15275
- function visibleWorker(worker, options) {
15276
- return {
15277
- async handle(job) {
15278
- if (visibleActivity.has(job.type)) {
15279
- report(
15280
- options,
15281
- { type: "activity", phase: "requested", jobType: job.type, requestId: job.requestId },
15282
- `${activityLabel2(job.type, false)} (${job.requestId}).`
15283
- );
15284
- }
15285
- const result = await worker.handle(job);
15286
- if (visibleActivity.has(job.type)) {
15287
- report(
15288
- options,
15289
- {
15290
- type: "activity",
15291
- phase: "finished",
15292
- jobType: job.type,
15293
- requestId: job.requestId,
15294
- ok: result.ok
15295
- },
15296
- `${activityLabel2(job.type, true, result.ok)} (${job.requestId}).`
15297
- );
15298
- }
15299
- return result;
15300
- }
15301
- };
16203
+ function renderHelp(usable, color) {
16204
+ return [
16205
+ "",
16206
+ sectionTitle("Navigate", color),
16207
+ ...helpRows("D", "Recent activity", usable, color),
16208
+ ...helpRows("S", "Account and devices", usable, color),
16209
+ ...helpRows("?", "Close help", usable, color),
16210
+ "",
16211
+ sectionTitle("Manage", color, PALETTE.coral),
16212
+ ...helpRows(
16213
+ "R",
16214
+ "Revoke a device from status",
16215
+ usable,
16216
+ color,
16217
+ PALETTE.coral
16218
+ ),
16219
+ ...helpRows("L", "Sign out", usable, color, PALETTE.coral),
16220
+ "",
16221
+ sectionTitle("Session", color),
16222
+ ...helpRows("Q", "Disconnect and quit", usable, color, PALETTE.coral),
16223
+ ...helpRows(
16224
+ "Ctrl+C",
16225
+ "Disconnect and quit",
16226
+ usable,
16227
+ color,
16228
+ PALETTE.coral
16229
+ )
16230
+ ];
15302
16231
  }
15303
- async function deviceForSession(endpoints, dependencies = {}, signal) {
15304
- const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
15305
- const loadLogin = dependencies.loadCredentials ?? loadCredentials;
15306
- const validate = dependencies.validCredentials ?? validCredentials;
15307
- const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
15308
- const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
15309
- const enroll = dependencies.enrollDevice ?? enrollDevice;
15310
- const saveDevice = dependencies.saveDeviceCredential ?? saveDeviceCredential;
15311
- const name = dependencies.defaultDeviceName ?? defaultDeviceName;
15312
- const baseFetch = dependencies.fetch ?? fetch;
15313
- const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
15314
- signal?.throwIfAborted();
15315
- const stored = await loadDevice();
15316
- const loaded = await loadLogin();
15317
- if (!loaded) throw new Error("Not signed in. Run Glossa again to sign in.");
15318
- const credentials = await validate(loaded.credentials, { fetch: fetchRequest });
15319
- signal?.throwIfAborted();
15320
- if (stored?.relayOrigin === endpoints.relayOrigin) {
15321
- if (await ownsDevice(
15322
- endpoints,
15323
- credentials,
15324
- stored.deviceId,
15325
- fetchRequest
15326
- )) {
15327
- return stored;
15328
- }
15329
- await removeDevice();
16232
+ function promptText(state) {
16233
+ if (state.busy) return { message: "Working\u2026" };
16234
+ if (!state.prompt) return void 0;
16235
+ if (state.prompt.type === "logout") {
16236
+ return { message: "Sign out and disconnect?", choices: "Y confirm N cancel" };
15330
16237
  }
15331
- const enrolled = await enroll(
15332
- endpoints,
15333
- credentials,
15334
- dependencies.deviceName ?? name(),
15335
- fetchRequest
16238
+ if (state.prompt.type === "revoke-select") {
16239
+ return { message: "Choose a device number to revoke.", choices: "Esc cancel" };
16240
+ }
16241
+ const device = state.status?.devices[state.prompt.deviceIndex];
16242
+ return {
16243
+ message: `Revoke ${device?.name ?? "this device"}?`,
16244
+ choices: "Y confirm N cancel"
16245
+ };
16246
+ }
16247
+ function footerHints(state) {
16248
+ if (state.view === "status") {
16249
+ return [
16250
+ { key: "R", label: "Revoke", tone: PALETTE.coral },
16251
+ { key: "L", label: "Sign out", tone: PALETTE.coral },
16252
+ { key: "Esc", label: "Session" },
16253
+ { key: "Q", label: "Quit", tone: PALETTE.coral }
16254
+ ];
16255
+ }
16256
+ if (state.view === "activity") {
16257
+ return [
16258
+ { key: "D", label: "Session" },
16259
+ { key: "S", label: "Status" },
16260
+ { key: "?", label: "Help" },
16261
+ { key: "Q", label: "Quit", tone: PALETTE.coral }
16262
+ ];
16263
+ }
16264
+ if (state.view === "help") {
16265
+ return [
16266
+ { key: "?", label: "Session" },
16267
+ { key: "Q", label: "Quit", tone: PALETTE.coral }
16268
+ ];
16269
+ }
16270
+ return [
16271
+ { key: "D", label: "Activity" },
16272
+ { key: "S", label: "Status" },
16273
+ { key: "?", label: "Help" },
16274
+ { key: "L", label: "Sign out", tone: PALETTE.coral },
16275
+ { key: "Q", label: "Quit", tone: PALETTE.coral }
16276
+ ];
16277
+ }
16278
+ function renderFooter(state, usable, color) {
16279
+ const rows = [[]];
16280
+ let rowLength = 0;
16281
+ for (const hint of footerHints(state)) {
16282
+ const tokenLength = hint.key.length + hint.label.length + 1;
16283
+ if (rows.at(-1).length > 0 && rowLength + 3 + tokenLength > usable) {
16284
+ rows.push([]);
16285
+ rowLength = 0;
16286
+ }
16287
+ rows.at(-1).push(hint);
16288
+ rowLength += (rowLength > 0 ? 3 : 0) + tokenLength;
16289
+ }
16290
+ return rows.map(
16291
+ (row) => row.map(
16292
+ (hint) => `${style(color, `${hint.tone ?? PALETTE.purpleReadable};1`, hint.key)} ${style(color, PALETTE.muted, hint.label)}`
16293
+ ).join(" ")
15336
16294
  );
15337
- await saveDevice(enrolled);
15338
- return enrolled;
15339
16295
  }
15340
- function statusMessage(status, previous) {
15341
- if (status.state === "connecting") return "Connecting to Glossa...";
15342
- if (status.state === "connected") {
15343
- return status.reconnected ? "Reconnected to Glossa." : "Connected to Glossa. ChatGPT can now use this workspace.";
15344
- }
15345
- if (status.state === "retrying") {
15346
- const prefix = previous === "connecting" ? "Could not connect" : "Connection lost";
15347
- return `${prefix}: ${status.error.message} Retrying automatically.`;
16296
+ function renderOverlay(state, usable, color) {
16297
+ const prompt = promptText(state);
16298
+ const message = prompt?.message ?? state.notice;
16299
+ if (!message) return [];
16300
+ const lines = [
16301
+ style(color, PALETTE.line, "\u2500".repeat(usable)),
16302
+ style(
16303
+ color,
16304
+ prompt ? `${PALETTE.coral};1` : PALETTE.coral,
16305
+ truncate(message, usable)
16306
+ )
16307
+ ];
16308
+ if (prompt?.choices) {
16309
+ lines.push(style(color, PALETTE.muted, truncate(prompt.choices, usable)));
15348
16310
  }
15349
- return "Disconnected from Glossa.";
16311
+ return lines;
15350
16312
  }
15351
- async function runManagedSession(root, endpoints, allowBroadRoot = false, options = {}) {
15352
- const controller = new AbortController();
15353
- const stop = () => controller.abort();
15354
- const handleProcessSignals = options.handleProcessSignals ?? true;
15355
- let worker;
15356
- if (options.signal?.aborted) controller.abort();
15357
- else options.signal?.addEventListener("abort", stop, { once: true });
15358
- if (handleProcessSignals) {
15359
- process.once("SIGINT", stop);
15360
- process.once("SIGTERM", stop);
16313
+ function renderHud(state, width = 80, color = !process.env.NO_COLOR, height = 24) {
16314
+ const margin = width >= 24 ? " " : "";
16315
+ const usable = Math.max(8, width - margin.length * 2);
16316
+ const terminalHeight = Math.max(6, height);
16317
+ const header = renderHeader(state, usable, color);
16318
+ const overlay = renderOverlay(state, usable, color);
16319
+ const footer = [
16320
+ style(color, PALETTE.line, "\u2500".repeat(usable)),
16321
+ ...renderFooter(state, usable, color)
16322
+ ];
16323
+ const bodyBudget = Math.max(
16324
+ 0,
16325
+ terminalHeight - header.length - overlay.length - footer.length
16326
+ );
16327
+ const visibleDeviceCount = statusDeviceCapacity(
16328
+ state,
16329
+ bodyBudget,
16330
+ usable
16331
+ );
16332
+ const body = state.view === "activity" ? renderActivity(state, usable, color, bodyBudget) : state.view === "status" ? renderStatus(state, usable, color, visibleDeviceCount) : state.view === "help" ? renderHelp(usable, color) : renderSession(state, usable, color);
16333
+ const visibleBody = body.slice(0, bodyBudget);
16334
+ const padding = Array.from(
16335
+ {
16336
+ length: Math.max(
16337
+ 0,
16338
+ terminalHeight - header.length - visibleBody.length - overlay.length - footer.length
16339
+ )
16340
+ },
16341
+ () => ""
16342
+ );
16343
+ const lines = [...header, ...visibleBody, ...padding, ...overlay, ...footer].slice(-terminalHeight);
16344
+ return lines.map((line) => line ? `${margin}${line}` : "").join("\n");
16345
+ }
16346
+ function statusDeviceCapacity(state, bodyBudget, usable) {
16347
+ if (state.view !== "status" || !state.status || state.statusLoading || state.status.devices.length === 0) {
16348
+ return 0;
16349
+ }
16350
+ const statusPreambleLines = usable >= 64 ? 11 : 10;
16351
+ const available = Math.max(0, bodyBudget - statusPreambleLines);
16352
+ let visible = Math.min(9, state.status.devices.length, available);
16353
+ if (state.status.devices.length > visible && visible > 0 && available - visible < 1) {
16354
+ visible -= 1;
16355
+ }
16356
+ return visible;
16357
+ }
16358
+ function terminalStatusDeviceCapacity(state, width, height) {
16359
+ const marginLength = width >= 24 ? 4 : 0;
16360
+ const usable = Math.max(8, width - marginLength);
16361
+ const terminalHeight = Math.max(6, height);
16362
+ const bodyBudget = Math.max(
16363
+ 0,
16364
+ terminalHeight - renderHeader(state, usable, false).length - renderOverlay(state, usable, false).length - 1 - renderFooter(state, usable, false).length
16365
+ );
16366
+ return statusDeviceCapacity(state, bodyBudget, usable);
16367
+ }
16368
+ async function runSessionHud(actions, input = process.stdin, output = process.stdout) {
16369
+ if (!input.isTTY || !output.isTTY) {
16370
+ throw new Error("Glossa requires an interactive terminal.");
15361
16371
  }
15362
- try {
15363
- const device = await deviceForSession(
15364
- endpoints,
15365
- options.deviceName ? { deviceName: options.deviceName } : {},
15366
- controller.signal
15367
- );
15368
- controller.signal.throwIfAborted();
15369
- worker = await LocalWorker.create(root, allowBroadRoot);
15370
- controller.signal.throwIfAborted();
15371
- report(
15372
- options,
15373
- { type: "session", root: worker.policy.root, deviceName: device.deviceName },
15374
- `Glossa worker root: ${worker.policy.root}`
16372
+ emitKeypressEvents(input);
16373
+ const wasRaw = input.isRaw;
16374
+ const controller = new AbortController();
16375
+ let state = initialHudState(actions.workspace);
16376
+ let exitAction = "quit";
16377
+ let stopUi;
16378
+ const color = !process.env.NO_COLOR;
16379
+ const render = () => {
16380
+ const view = renderHud(
16381
+ state,
16382
+ output.columns ?? 80,
16383
+ color,
16384
+ output.rows ?? 24
15375
16385
  );
15376
- if (!options.quiet) {
15377
- console.error(`Glossa device: ${device.deviceName}`);
15378
- console.error(
15379
- "Files may be modified and commands have the full environment and permissions of this account. Press Ctrl+C to disconnect."
16386
+ output.write(`${color ? ANSI_BASE : ""}\x1B[H\x1B[2J${view}`);
16387
+ };
16388
+ const resize = () => {
16389
+ if (state.prompt?.type === "revoke-select" || state.prompt?.type === "revoke-confirm") {
16390
+ const deviceCount = terminalStatusDeviceCapacity(
16391
+ state,
16392
+ output.columns ?? 80,
16393
+ output.rows ?? 24
15380
16394
  );
15381
- }
15382
- let connectionState;
15383
- await new RemoteWorker({
15384
- origin: endpoints.workerOrigin,
15385
- deviceToken: device.token,
15386
- worker: visibleWorker(worker, options),
15387
- signal: controller.signal,
15388
- onStatus(status) {
15389
- if (status.state !== "retrying" || connectionState !== "retrying") {
15390
- report(options, { type: "status", status }, statusMessage(status, connectionState));
15391
- } else {
15392
- options.onEvent?.({ type: "status", status });
15393
- }
15394
- if (status.state === "connected" && status.legacyRelay) {
15395
- report(
15396
- options,
15397
- { type: "notice", message: "The relay needs an update before this computer can expose several workspaces at once." },
15398
- "The relay needs an update before this computer can expose several workspaces at once."
15399
- );
15400
- }
15401
- if (status.state === "connected" && !status.reconnected && shouldShowConnectHint(endpoints.relayOrigin)) {
15402
- void announceConnectHint(connectHintStore(), (message) => {
15403
- report(options, { type: "notice", message }, message);
15404
- }).catch(() => void 0);
15405
- }
15406
- connectionState = status.state;
16395
+ const selectedDeviceIsHidden = state.prompt.type === "revoke-confirm" && state.prompt.deviceIndex >= deviceCount;
16396
+ if (deviceCount === 0 || selectedDeviceIsHidden) {
16397
+ state = {
16398
+ ...state,
16399
+ prompt: void 0,
16400
+ notice: "Increase the terminal height to choose a device."
16401
+ };
16402
+ } else if (state.prompt.type === "revoke-select") {
16403
+ state = {
16404
+ ...state,
16405
+ prompt: { type: "revoke-select", deviceCount }
16406
+ };
15407
16407
  }
15408
- }).run();
15409
- } catch (error46) {
15410
- if (error46 instanceof DeviceRejectedError) {
15411
- await deleteDeviceCredential();
15412
- throw new Error("The relay rejected this device. Run Glossa again to reenroll it.");
15413
16408
  }
16409
+ render();
16410
+ };
16411
+ const loadStatus = async () => {
16412
+ if (state.statusLoading) return;
16413
+ if (state.connection !== "connected" && state.connection !== "retrying") {
16414
+ state = { ...state, notice: "Status is available after Glossa connects." };
16415
+ render();
16416
+ return;
16417
+ }
16418
+ state = {
16419
+ ...state,
16420
+ view: "status",
16421
+ statusLoading: true,
16422
+ prompt: void 0,
16423
+ notice: void 0
16424
+ };
16425
+ render();
16426
+ try {
16427
+ const status = await actions.loadStatus(controller.signal);
16428
+ if (controller.signal.aborted) return;
16429
+ state = { ...state, status, statusLoading: false };
16430
+ } catch (error46) {
16431
+ if (controller.signal.aborted) return;
16432
+ state = {
16433
+ ...state,
16434
+ statusLoading: false,
16435
+ notice: error46 instanceof Error ? error46.message : String(error46)
16436
+ };
16437
+ }
16438
+ render();
16439
+ };
16440
+ const session = actions.run(controller.signal, (event) => {
16441
+ state = applyHudEvent(state, event);
16442
+ render();
16443
+ }).then(() => {
16444
+ if (!controller.signal.aborted) {
16445
+ state = { ...state, connection: "disconnected" };
16446
+ }
16447
+ render();
16448
+ }).catch((error46) => {
16449
+ if (controller.signal.aborted) return;
16450
+ state = {
16451
+ ...state,
16452
+ connection: "error",
16453
+ message: error46 instanceof Error ? error46.message : String(error46)
16454
+ };
16455
+ render();
15414
16456
  throw error46;
16457
+ });
16458
+ input.setRawMode(true);
16459
+ input.resume();
16460
+ output.write("\x1B[?1049h\x1B[?25l");
16461
+ output.on("resize", resize);
16462
+ render();
16463
+ const stop = (action = "quit") => {
16464
+ exitAction = action;
16465
+ controller.abort();
16466
+ stopUi?.();
16467
+ };
16468
+ const stopFromSignal = () => stop();
16469
+ process.once("SIGINT", stopFromSignal);
16470
+ process.once("SIGTERM", stopFromSignal);
16471
+ try {
16472
+ await new Promise((resolve) => {
16473
+ const onKeypress = (value, key) => {
16474
+ if (key.ctrl && key.name === "c" || key.name === "q") return stop();
16475
+ if (state.busy) return;
16476
+ if (state.prompt) {
16477
+ if (key.name === "escape" || key.name === "n") {
16478
+ state = { ...state, prompt: void 0, notice: void 0 };
16479
+ render();
16480
+ return;
16481
+ }
16482
+ if (state.prompt.type === "revoke-select") {
16483
+ const deviceIndex = Number(value) - 1;
16484
+ if (Number.isInteger(deviceIndex) && deviceIndex >= 0 && deviceIndex < state.prompt.deviceCount) {
16485
+ state = {
16486
+ ...state,
16487
+ prompt: { type: "revoke-confirm", deviceIndex }
16488
+ };
16489
+ render();
16490
+ }
16491
+ return;
16492
+ }
16493
+ if (key.name !== "y") return;
16494
+ if (state.prompt.type === "logout") return stop("logout");
16495
+ const device = state.status?.devices[state.prompt.deviceIndex];
16496
+ if (!device) return;
16497
+ state = {
16498
+ ...state,
16499
+ busy: true,
16500
+ prompt: void 0,
16501
+ notice: void 0
16502
+ };
16503
+ render();
16504
+ void actions.revokeDevice(device.id, controller.signal).then(
16505
+ async () => {
16506
+ if (controller.signal.aborted) return;
16507
+ state = {
16508
+ ...state,
16509
+ busy: false
16510
+ };
16511
+ await loadStatus();
16512
+ if (controller.signal.aborted) return;
16513
+ state = {
16514
+ ...state,
16515
+ notice: `Revoked ${device.name}.`
16516
+ };
16517
+ render();
16518
+ }
16519
+ ).catch((error46) => {
16520
+ if (controller.signal.aborted) return;
16521
+ state = {
16522
+ ...state,
16523
+ busy: false,
16524
+ notice: error46 instanceof Error ? error46.message : String(error46)
16525
+ };
16526
+ render();
16527
+ });
16528
+ return;
16529
+ }
16530
+ if (key.name === "escape") {
16531
+ state = { ...state, view: "session", notice: void 0 };
16532
+ render();
16533
+ } else if (key.name === "d") {
16534
+ state = {
16535
+ ...state,
16536
+ view: state.view === "activity" ? "session" : "activity",
16537
+ notice: void 0
16538
+ };
16539
+ render();
16540
+ } else if (key.name === "s") {
16541
+ void loadStatus();
16542
+ } else if (key.name === "r" && state.view === "status") {
16543
+ if ((state.status?.devices.length ?? 0) === 0) {
16544
+ state = { ...state, notice: "There are no devices to revoke." };
16545
+ } else {
16546
+ const promptState = {
16547
+ ...state,
16548
+ prompt: { type: "revoke-select", deviceCount: 0 },
16549
+ notice: void 0
16550
+ };
16551
+ const deviceCount = terminalStatusDeviceCapacity(
16552
+ promptState,
16553
+ output.columns ?? 80,
16554
+ output.rows ?? 24
16555
+ );
16556
+ state = deviceCount === 0 ? {
16557
+ ...state,
16558
+ notice: "Increase the terminal height to choose a device."
16559
+ } : {
16560
+ ...promptState,
16561
+ prompt: { type: "revoke-select", deviceCount }
16562
+ };
16563
+ }
16564
+ render();
16565
+ } else if (key.name === "l") {
16566
+ state = {
16567
+ ...state,
16568
+ prompt: { type: "logout" },
16569
+ notice: void 0
16570
+ };
16571
+ render();
16572
+ } else if (value === "?" || key.sequence === "?") {
16573
+ state = {
16574
+ ...state,
16575
+ view: state.view === "help" ? "session" : "help",
16576
+ notice: void 0
16577
+ };
16578
+ render();
16579
+ }
16580
+ };
16581
+ input.on("keypress", onKeypress);
16582
+ void session.catch(() => stopUi?.());
16583
+ stopUi = () => {
16584
+ input.removeListener("keypress", onKeypress);
16585
+ resolve();
16586
+ };
16587
+ });
16588
+ await session;
16589
+ return exitAction;
15415
16590
  } finally {
15416
- options.signal?.removeEventListener("abort", stop);
15417
- if (handleProcessSignals) {
15418
- process.removeListener("SIGINT", stop);
15419
- process.removeListener("SIGTERM", stop);
15420
- }
15421
- await worker?.shutdown();
16591
+ output.removeListener("resize", resize);
16592
+ process.removeListener("SIGINT", stopFromSignal);
16593
+ process.removeListener("SIGTERM", stopFromSignal);
16594
+ input.setRawMode(wasRaw);
16595
+ input.pause();
16596
+ output.write("\x1B[0m\x1B[?25h\x1B[?1049l");
15422
16597
  }
15423
16598
  }
15424
16599
 
15425
16600
  // src/worker/root-selection.ts
15426
- import { execFile } from "node:child_process";
15427
- import { promisify } from "node:util";
15428
- var execFileAsync = promisify(execFile);
15429
- async function gitWorktreeRoot(cwd) {
15430
- try {
15431
- const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], {
15432
- cwd,
15433
- encoding: "utf8",
15434
- windowsHide: true
15435
- });
15436
- const root = stdout.trim();
15437
- return root || null;
15438
- } catch {
15439
- return null;
16601
+ import { realpath as realpath2 } from "node:fs/promises";
16602
+ import os4 from "node:os";
16603
+ import path7 from "node:path";
16604
+ function containsPath(root, candidate) {
16605
+ const relative = path7.relative(root, candidate);
16606
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
16607
+ }
16608
+ async function rejectImplicitHomeAncestor(root) {
16609
+ const homes = await Promise.all(
16610
+ [os4.homedir(), accountHomeDirectory()].map(
16611
+ async (home) => await realpath2(home).catch(() => path7.resolve(home))
16612
+ )
16613
+ );
16614
+ if (homes.some((home) => containsPath(root, home))) {
16615
+ throw new WorkerError(
16616
+ "broad_root_refused",
16617
+ "The selected root contains a home directory, which Glossa will not expose implicitly. Choose a workspace directory instead."
16618
+ );
15440
16619
  }
15441
16620
  }
15442
- async function selectExposureRoot(explicitPath, allowBroadRoot = false, cwd = process.cwd()) {
15443
- const selected = explicitPath ?? await gitWorktreeRoot(cwd) ?? cwd;
15444
- return await canonicalizeRoot(selected, allowBroadRoot);
16621
+ async function selectExposureRoot(explicitPath, cwd = process.cwd()) {
16622
+ const root = await canonicalizeRoot(explicitPath ?? cwd);
16623
+ if (explicitPath === void 0) await rejectImplicitHomeAncestor(root);
16624
+ return root;
15445
16625
  }
15446
16626
 
15447
16627
  // src/main.ts
15448
- var VERSION = "0.1.0-beta.10";
15449
- var helpText = {
15450
- main: `Glossa ${VERSION}
16628
+ var VERSION = "0.1.0-beta.12";
16629
+ var HELP = `Glossa ${VERSION}
15451
16630
 
15452
16631
  Usage:
15453
- glossa
15454
- glossa [directory]
15455
- glossa ui [directory] [--allow-broad-root] [--device-name <name>]
15456
- glossa start [directory] [--allow-broad-root] [--device-name <name>]
15457
- glossa status [--json]
15458
- glossa doctor [--json]
15459
- glossa devices list [--json]
15460
- glossa devices rename <id> <name>
16632
+ glossa [--label <name>] [directory]
16633
+ glossa status
15461
16634
  glossa devices revoke <id>
15462
- glossa completions <shell>
15463
- glossa update
15464
- glossa login
15465
- glossa logout [--browser]
15466
- glossa --version
16635
+ glossa logout
15467
16636
  glossa --help
16637
+ glossa --version
15468
16638
 
15469
- Glossa signs in automatically and exposes each started workspace through the managed MCP relay.`,
15470
- ui: `Usage: glossa ui [directory] [--allow-broad-root] [--device-name <name>]
15471
-
15472
- Opens an experimental compact session HUD for the current workspace.
15473
- It starts immediately, shows connection and activity, and exits with q or Ctrl+C. --device-name names this computer on first enrollment.`,
15474
- start: `Usage: glossa start [directory] [--allow-broad-root] [--device-name <name>]
15475
-
15476
- Starts a foreground worker in the selected directory. Pass . to select the current directory explicitly.
15477
- --device-name names this computer the first time it enrolls; once enrolled the name is reused. Press Ctrl+C to disconnect.`,
15478
- status: `Usage: glossa status [--json]
15479
-
15480
- Validates Google login, contacts the relay, and reports enrolled devices and active workers.`,
15481
- doctor: `Usage: glossa doctor [--json]
15482
-
15483
- Checks the runtime, relay and worker reachability, and read-only sign-in state, then reports whether Glossa is ready to start.`,
15484
- devices: `Usage:
15485
- glossa devices list [--json]
15486
- glossa devices rename <id> <name>
15487
- glossa devices revoke <id>
15488
-
15489
- Lists, renames, or revokes computers enrolled with the current Google account.`,
15490
- completions: `Usage: glossa completions <shell>
15491
-
15492
- Prints a completion script for powershell, bash, zsh, or fish. Source it from your shell profile, for example: glossa completions powershell | Out-String | Invoke-Expression.`,
15493
- update: `Usage: glossa update
15494
-
15495
- Updates Glossa using the same installation method. glossa upgrade is an alias.`,
15496
- login: `Usage: glossa login
15497
-
15498
- Ensures the CLI has a valid Google session. Starting Glossa also signs in automatically.`,
15499
- logout: `Usage: glossa logout [--browser]
16639
+ Running glossa opens one workspace in an interactive terminal.
15500
16640
 
15501
- Removes local OAuth credentials. --browser also opens the browser-session logout used when switching Google accounts. Running workers remain connected until stopped or revoked.`
15502
- };
16641
+ Keys:
16642
+ d recent activity
16643
+ s account and devices
16644
+ r revoke a device from status
16645
+ l sign out
16646
+ ? help
16647
+ q disconnect and quit`;
15503
16648
  async function withLoginSignal(action) {
15504
16649
  const controller = new AbortController();
15505
16650
  const cancel = () => controller.abort();
@@ -15510,124 +16655,92 @@ async function withLoginSignal(action) {
15510
16655
  process.removeListener("SIGINT", cancel);
15511
16656
  }
15512
16657
  }
15513
- async function authenticatedCredentials(signal) {
15514
- const loginPerformed = signal ? await ensureSignedIn({ ...loadAuthConfig(), signal }) : await withLoginSignal(async (loginSignal) => {
15515
- return await ensureSignedIn({ ...loadAuthConfig(), signal: loginSignal });
15516
- });
15517
- const loaded = await loadCredentials();
15518
- if (!loaded) throw new Error("Glossa could not load the completed login.");
15519
- return {
15520
- credentials: await validCredentials(
15521
- loaded.credentials,
15522
- signal ? { signal } : {}
15523
- ),
15524
- loginPerformed
15525
- };
16658
+ async function authenticatedSession(signal) {
16659
+ if (signal) return await signedInSession({ ...loadAuthConfig(), signal });
16660
+ return await withLoginSignal(
16661
+ async (loginSignal) => await signedInSession({ ...loadAuthConfig(), signal: loginSignal })
16662
+ );
15526
16663
  }
15527
- async function runExposure(path8, allowBroadRoot, deviceName) {
15528
- const root = await selectExposureRoot(path8, allowBroadRoot);
15529
- await authenticatedCredentials();
15530
- await runManagedSession(root, loadRelayEndpoints(), allowBroadRoot, {
15531
- ...deviceName ? { deviceName } : {}
15532
- });
16664
+ async function authenticatedCredentials(signal) {
16665
+ return (await authenticatedSession(signal)).credentials;
15533
16666
  }
15534
- async function showStatus(json2) {
15535
- const { credentials: initial } = await authenticatedCredentials();
15536
- const { credentials, profile } = await loadUserProfile(initial);
16667
+ async function showStatus() {
16668
+ const credentials = await authenticatedCredentials();
15537
16669
  const endpoints = loadRelayEndpoints();
15538
- const devices = await listDevices(endpoints, credentials);
15539
- const account = profile.email ?? profile.name ?? profile.sub;
15540
- const workerCountsCurrent = devices.every((device) => device.activeWorkers !== null);
15541
- const activeWorkers = workerCountsCurrent ? devices.reduce((sum, device) => sum + device.activeWorkers, 0) : null;
15542
- const result = {
15543
- account,
15544
- relay: endpoints.relayOrigin,
15545
- connected: true,
15546
- activeWorkers,
15547
- devices
15548
- };
15549
- if (json2) {
15550
- console.log(JSON.stringify(result, null, 2));
15551
- return;
15552
- }
15553
- console.log(`Signed in as ${account}.`);
15554
- console.log(`Relay connected: ${endpoints.relayOrigin}`);
15555
- console.log(
15556
- activeWorkers === null ? "Active workers: unavailable until the relay is updated" : `Active workers: ${activeWorkers}`
15557
- );
15558
- if (devices.length === 0) {
15559
- console.log("No devices enrolled. Run glossa start in a workspace.");
15560
- return;
15561
- }
15562
- const hint = noActiveWorkerHint(activeWorkers, devices.length);
15563
- if (hint) console.log(hint);
15564
- for (const device of devices) {
15565
- console.log(formatDeviceRow(device));
15566
- }
16670
+ const status = await new WorkspaceStatusService(
16671
+ credentials,
16672
+ endpoints
16673
+ ).refresh();
16674
+ for (const line of formatStatus(status)) console.log(line);
15567
16675
  }
15568
- async function deviceCredentials() {
16676
+ function hudStatus(status) {
15569
16677
  return {
15570
- credentials: (await authenticatedCredentials()).credentials,
15571
- endpoints: loadRelayEndpoints()
15572
- };
15573
- }
15574
- async function showDevices(json2) {
15575
- const { endpoints, credentials } = await deviceCredentials();
15576
- const devices = await listDevices(endpoints, credentials);
15577
- if (json2) console.log(JSON.stringify({ devices }, null, 2));
15578
- else if (devices.length === 0) console.log("No devices enrolled.");
15579
- else for (const device of devices) console.log(formatDeviceRow(device));
15580
- }
15581
- async function runInteractive(path8, allowBroadRoot, deviceName) {
15582
- const root = await selectExposureRoot(path8, allowBroadRoot);
15583
- await runSessionHud({
16678
+ ...status,
16679
+ devices: status.devices.map((device) => ({
16680
+ id: device.id,
16681
+ name: device.name,
16682
+ platform: device.platform ?? "Unknown platform",
16683
+ lastSeen: formatRelativeTime(device.lastSeenAt),
16684
+ status: deviceStatus(device)
16685
+ }))
16686
+ };
16687
+ }
16688
+ async function revokeKnownDevice(deviceId) {
16689
+ const credentials = await authenticatedCredentials();
16690
+ await revokeDevice(loadRelayEndpoints(), credentials, deviceId);
16691
+ }
16692
+ async function runWorkspace(path8, label) {
16693
+ const root = await selectExposureRoot(path8);
16694
+ const endpoints = loadRelayEndpoints();
16695
+ let credentials = (await authenticatedSession()).credentials;
16696
+ const statusService = new WorkspaceStatusService(credentials, endpoints);
16697
+ let postExitNotice;
16698
+ const exitAction = await runSessionHud({
15584
16699
  workspace: root,
15585
16700
  run: async (signal, onEvent) => {
15586
- await authenticatedCredentials(signal);
15587
- await runManagedSession(root, loadRelayEndpoints(), allowBroadRoot, {
16701
+ await runManagedSession(root, endpoints, {
16702
+ credentials,
16703
+ ...label ? { workspaceLabel: label } : {},
15588
16704
  signal,
15589
- onEvent,
16705
+ onEvent: (event) => {
16706
+ postExitNotice = retainPostExitNotice(postExitNotice, event);
16707
+ onEvent(event);
16708
+ },
15590
16709
  quiet: true,
15591
- handleProcessSignals: false,
15592
- ...deviceName ? { deviceName } : {}
16710
+ handleProcessSignals: false
15593
16711
  });
16712
+ },
16713
+ loadStatus: async (signal) => {
16714
+ return hudStatus(await statusService.refresh(signal));
16715
+ },
16716
+ revokeDevice: async (deviceId, signal) => {
16717
+ credentials = await validCredentials(credentials, { signal });
16718
+ await revokeDevice(
16719
+ endpoints,
16720
+ credentials,
16721
+ deviceId,
16722
+ async (input, init) => await fetch(input, { ...init, signal })
16723
+ );
15594
16724
  }
15595
16725
  });
16726
+ if (postExitNotice) console.error(postExitNotice);
16727
+ if (exitAction === "logout") await logoutFromGlossa();
15596
16728
  }
15597
16729
  async function main() {
15598
16730
  const invocation = parseInvocation(process.argv.slice(2));
15599
16731
  if (invocation.command === "help") {
15600
- console.log(helpText[invocation.topic ?? "main"]);
16732
+ console.log(HELP);
15601
16733
  } else if (invocation.command === "version") {
15602
16734
  console.log(VERSION);
15603
- } else if (invocation.command === "ui") {
15604
- await runInteractive(invocation.path, invocation.allowBroadRoot, invocation.deviceName);
15605
- } else if (invocation.command === "start") {
15606
- await runExposure(invocation.path, invocation.allowBroadRoot, invocation.deviceName);
16735
+ } else if (invocation.command === "workspace") {
16736
+ await runWorkspace(invocation.path, invocation.label);
15607
16737
  } else if (invocation.command === "status") {
15608
- await showStatus(invocation.json);
15609
- } else if (invocation.command === "doctor") {
15610
- const ok = await runDoctor(invocation.json);
15611
- if (!ok) process.exitCode = 1;
15612
- } else if (invocation.command === "login") {
15613
- const { loginPerformed } = await authenticatedCredentials();
15614
- if (!loginPerformed) console.log("Signed in to Glossa.");
16738
+ await showStatus();
15615
16739
  } else if (invocation.command === "logout") {
15616
- await logoutFromGlossa({ browser: invocation.browser });
15617
- } else if (invocation.command === "completions") {
15618
- console.log(completionScript(invocation.shell));
15619
- } else if (invocation.command === "update") {
15620
- await updateGlossa({ currentVersion: VERSION });
15621
- } else if (invocation.action === "list") {
15622
- await showDevices(invocation.json);
15623
- } else if (invocation.action === "rename") {
15624
- const { endpoints, credentials } = await deviceCredentials();
15625
- const device = await renameDevice(endpoints, credentials, invocation.deviceId, invocation.name);
15626
- console.log(`Renamed device ${device.id} to ${device.name}.`);
16740
+ await logoutFromGlossa();
15627
16741
  } else {
15628
- const { endpoints, credentials } = await deviceCredentials();
15629
- await revokeDevice(endpoints, credentials, invocation.deviceId);
15630
- console.log(`Revoked device ${invocation.deviceId}. Running workers on it are disconnected.`);
16742
+ await revokeKnownDevice(invocation.deviceId);
16743
+ console.log(`Revoked device ${invocation.deviceId}. Running workspaces on it are disconnected.`);
15631
16744
  }
15632
16745
  }
15633
16746
  main().catch((error46) => {