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

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 +8 -11
  2. package/dist/app.js +1163 -1190
  3. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -239,6 +239,21 @@ var SessionExpiredError = class extends Error {
239
239
  this.name = "SessionExpiredError";
240
240
  }
241
241
  };
242
+ function accessTokenSubject(credentials) {
243
+ try {
244
+ const parts = credentials.accessToken.split(".");
245
+ if (parts.length !== 3) throw new Error();
246
+ const payload = JSON.parse(
247
+ Buffer.from(parts[1], "base64url").toString("utf8")
248
+ );
249
+ if (typeof payload.sub !== "string" || payload.sub.length === 0) {
250
+ throw new Error();
251
+ }
252
+ return payload.sub;
253
+ } catch {
254
+ throw new Error("Glossa could not identify the signed-in account.");
255
+ }
256
+ }
242
257
  function sessionExpiredError() {
243
258
  return new SessionExpiredError();
244
259
  }
@@ -500,29 +515,164 @@ 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
546
  // src/cli-options.ts
524
- import { existsSync } from "node:fs";
525
- import path3 from "node:path";
547
+ var UsageError = class extends Error {
548
+ };
549
+ function parseWorkspace(args) {
550
+ let selectedPath;
551
+ let optionsEnded = false;
552
+ for (const argument of args) {
553
+ if (!optionsEnded && argument === "--") {
554
+ optionsEnded = true;
555
+ } else if (!optionsEnded && argument.startsWith("-")) {
556
+ throw new UsageError(`Unknown option: ${argument}`);
557
+ } else if (selectedPath) {
558
+ throw new UsageError("Glossa accepts at most one directory.");
559
+ } else {
560
+ selectedPath = argument;
561
+ }
562
+ }
563
+ return {
564
+ command: "workspace",
565
+ ...selectedPath ? { path: selectedPath } : {}
566
+ };
567
+ }
568
+ function parseJsonOption(command, args) {
569
+ if (args.length === 0) return false;
570
+ if (args.length === 1 && args[0] === "--json") return true;
571
+ throw new UsageError(`${command} accepts only --json.`);
572
+ }
573
+ function parseDevices(args) {
574
+ if (args.length === 0 || args.length === 1 && args[0] === "--json") {
575
+ return {
576
+ command: "devices",
577
+ action: "list",
578
+ json: parseJsonOption("Devices", args)
579
+ };
580
+ }
581
+ if (args[0] === "revoke" && args.length === 2) {
582
+ return { command: "devices", action: "revoke", deviceId: args[1] };
583
+ }
584
+ throw new UsageError("Use: glossa devices [--json] or glossa devices revoke <id>.");
585
+ }
586
+ function noOptions(command, args) {
587
+ if (args.length > 0) throw new UsageError(`${command} accepts no options.`);
588
+ }
589
+ function parseInvocation(args) {
590
+ const [command, ...options] = args;
591
+ if (!command) return parseWorkspace([]);
592
+ if (command === "--help" || command === "-h") {
593
+ noOptions("Help", options);
594
+ return { command: "help" };
595
+ }
596
+ if (command === "--version" || command === "-v") {
597
+ noOptions("Version", options);
598
+ return { command: "version" };
599
+ }
600
+ if (command === "--") return parseWorkspace(args);
601
+ if (options.includes("--help") || options.includes("-h")) {
602
+ return { command: "help" };
603
+ }
604
+ if (command === "status") {
605
+ return { command, json: parseJsonOption("Status", options) };
606
+ }
607
+ if (command === "devices") return parseDevices(options);
608
+ if (command === "update" || command === "login" || command === "logout") {
609
+ noOptions(command[0].toUpperCase() + command.slice(1), options);
610
+ return { command };
611
+ }
612
+ return parseWorkspace(args);
613
+ }
614
+
615
+ // src/device-format.ts
616
+ function deviceStatus(device) {
617
+ if (device.revokedAt) return "revoked";
618
+ if (device.activeWorkers === null) return "worker count unavailable";
619
+ if (device.activeWorkers === 0) return "offline";
620
+ return `${device.activeWorkers} active ${device.activeWorkers === 1 ? "worker" : "workers"}`;
621
+ }
622
+ function formatRelativeTime(iso, now = Date.now()) {
623
+ if (!iso) return "never";
624
+ const parsed = Date.parse(iso);
625
+ if (!Number.isFinite(parsed)) return "unknown";
626
+ const seconds = Math.max(0, Math.round((now - parsed) / 1e3));
627
+ if (seconds < 60) return "just now";
628
+ const minutes = Math.round(seconds / 60);
629
+ if (minutes < 60) return `${minutes}m ago`;
630
+ const hours = Math.round(minutes / 60);
631
+ if (hours < 24) return `${hours}h ago`;
632
+ const days = Math.round(hours / 24);
633
+ return `${days}d ago`;
634
+ }
635
+ function formatDeviceRow(device, now = Date.now()) {
636
+ const platform = device.platform ?? "unknown platform";
637
+ return `${device.id} ${device.name} ${platform} last seen ${formatRelativeTime(device.lastSeenAt, now)} ${deviceStatus(device)}`;
638
+ }
639
+
640
+ // src/logout.ts
641
+ function browserLogoutUrl(issuer) {
642
+ return new URL(
643
+ "v2/logout",
644
+ issuer.endsWith("/") ? issuer : `${issuer}/`
645
+ ).toString();
646
+ }
647
+ async function logoutFromGlossa(dependencies = {}) {
648
+ const remove = dependencies.deleteCredentials ?? deleteCredentials;
649
+ const peek = dependencies.peekCredentials ?? peekCredentials;
650
+ const browse = dependencies.openBrowser ?? openBrowser;
651
+ const log = dependencies.log ?? console.log;
652
+ let stored = null;
653
+ let present = true;
654
+ try {
655
+ stored = await peek();
656
+ present = stored !== null;
657
+ } catch {
658
+ }
659
+ const issuer = dependencies.issuer ?? stored?.credentials.issuer;
660
+ await remove();
661
+ log(
662
+ present ? "Signed out of Glossa." : "Already signed out of Glossa."
663
+ );
664
+ const url2 = browserLogoutUrl(issuer ?? loadAuthConfig().issuer);
665
+ const opened = await browse(url2);
666
+ if (opened) {
667
+ log("Opened Glossa browser sign-out.");
668
+ } else {
669
+ log("Open this URL to finish signing out in your browser:");
670
+ log(url2);
671
+ }
672
+ }
673
+
674
+ // src/relay-client.ts
675
+ import os2 from "node:os";
526
676
 
527
677
  // ../../node_modules/zod/v4/classic/external.js
528
678
  var external_exports = {};
@@ -1255,10 +1405,10 @@ function mergeDefs(...defs) {
1255
1405
  function cloneDef(schema) {
1256
1406
  return mergeDefs(schema._zod.def);
1257
1407
  }
1258
- function getElementAtPath(obj, path8) {
1259
- if (!path8)
1408
+ function getElementAtPath(obj, path7) {
1409
+ if (!path7)
1260
1410
  return obj;
1261
- return path8.reduce((acc, key) => acc?.[key], obj);
1411
+ return path7.reduce((acc, key) => acc?.[key], obj);
1262
1412
  }
1263
1413
  function promiseAllObject(promisesObj) {
1264
1414
  const keys = Object.keys(promisesObj);
@@ -1619,11 +1769,11 @@ function aborted(x, startIndex = 0) {
1619
1769
  }
1620
1770
  return false;
1621
1771
  }
1622
- function prefixIssues(path8, issues) {
1772
+ function prefixIssues(path7, issues) {
1623
1773
  return issues.map((iss) => {
1624
1774
  var _a;
1625
1775
  (_a = iss).path ?? (_a.path = []);
1626
- iss.path.unshift(path8);
1776
+ iss.path.unshift(path7);
1627
1777
  return iss;
1628
1778
  });
1629
1779
  }
@@ -1785,7 +1935,7 @@ function formatError(error46, mapper = (issue2) => issue2.message) {
1785
1935
  }
1786
1936
  function treeifyError(error46, mapper = (issue2) => issue2.message) {
1787
1937
  const result = { errors: [] };
1788
- const processError = (error47, path8 = []) => {
1938
+ const processError = (error47, path7 = []) => {
1789
1939
  var _a, _b;
1790
1940
  for (const issue2 of error47.issues) {
1791
1941
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1795,7 +1945,7 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
1795
1945
  } else if (issue2.code === "invalid_element") {
1796
1946
  processError({ issues: issue2.issues }, issue2.path);
1797
1947
  } else {
1798
- const fullpath = [...path8, ...issue2.path];
1948
+ const fullpath = [...path7, ...issue2.path];
1799
1949
  if (fullpath.length === 0) {
1800
1950
  result.errors.push(mapper(issue2));
1801
1951
  continue;
@@ -1827,8 +1977,8 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
1827
1977
  }
1828
1978
  function toDotPath(_path) {
1829
1979
  const segs = [];
1830
- const path8 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1831
- for (const seg of path8) {
1980
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1981
+ for (const seg of path7) {
1832
1982
  if (typeof seg === "number")
1833
1983
  segs.push(`[${seg}]`);
1834
1984
  else if (typeof seg === "symbol")
@@ -13170,472 +13320,7 @@ var workerResultSchema = external_exports.object({
13170
13320
  }).optional()
13171
13321
  });
13172
13322
 
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
- // src/cli-options.ts
13372
- var UsageError = class extends Error {
13373
- };
13374
- var helpTopics = /* @__PURE__ */ new Set([
13375
- "ui",
13376
- "start",
13377
- "status",
13378
- "doctor",
13379
- "devices",
13380
- "completions",
13381
- "update",
13382
- "login",
13383
- "logout"
13384
- ]);
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
- }
13398
- let selectedPath;
13399
- let allowBroadRoot = false;
13400
- let deviceName;
13401
- let optionsEnded = false;
13402
- for (let index = 0; index < args.length; index += 1) {
13403
- const argument = args[index];
13404
- if (!optionsEnded && argument === "--") {
13405
- optionsEnded = true;
13406
- } else if (!optionsEnded && argument === "--allow-broad-root") {
13407
- allowBroadRoot = true;
13408
- } else if (!optionsEnded && argument === "--device-name") {
13409
- const value = args[index + 1];
13410
- if (value === void 0 || value.startsWith("-")) {
13411
- throw new UsageError("--device-name requires a value.");
13412
- }
13413
- deviceName = parseDeviceName(value);
13414
- index += 1;
13415
- } else if (!optionsEnded && argument.startsWith("--device-name=")) {
13416
- deviceName = parseDeviceName(argument.slice("--device-name=".length));
13417
- } else if (!optionsEnded && argument.startsWith("-")) {
13418
- throw new UsageError(`Unknown ${command} option: ${argument}`);
13419
- } else if (selectedPath) {
13420
- throw new UsageError(`${command === "ui" ? "UI" : "Start"} accepts at most one directory.`);
13421
- } else {
13422
- selectedPath = argument;
13423
- }
13424
- }
13425
- return {
13426
- command,
13427
- ...selectedPath ? { path: selectedPath } : {},
13428
- allowBroadRoot,
13429
- ...deviceName ? { deviceName } : {}
13430
- };
13431
- }
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
- 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] };
13450
- }
13451
- throw new UsageError("Use: glossa devices list, rename <id> <name>, or revoke <id>.");
13452
- }
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;
13507
- }
13508
- function parseInvocation(args) {
13509
- const [command, ...options] = args;
13510
- if (!command) return parseWorkspaceCommand("start", []);
13511
- if (command === "--help" || command === "-h") {
13512
- if (options.length > 0) throw new UsageError("Help accepts one command name.");
13513
- return { command: "help" };
13514
- }
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
- if (command === "--version" || command === "-v") {
13525
- if (options.length > 0) throw new UsageError("Version accepts no arguments.");
13526
- return { command: "version" };
13527
- }
13528
- if (command === "ui") return parseWorkspaceCommand("ui", options);
13529
- if (command === "start") return parseWorkspaceCommand("start", options);
13530
- 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) };
13541
- }
13542
- 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
- 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.");
13583
- }
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
- );
13591
- }
13592
-
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;
13617
- }
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;
13626
- }
13627
- async function peekDeviceCredential() {
13628
- return (await store2.peek())?.value ?? null;
13629
- }
13630
- async function saveDeviceCredential(credential) {
13631
- await store2.save(credential);
13632
- }
13633
- async function deleteDeviceCredential() {
13634
- await store2.delete();
13635
- }
13636
-
13637
13323
  // src/relay-client.ts
13638
- import os2 from "node:os";
13639
13324
  var DEFAULT_RELAY_ORIGIN = "https://mcp.glossa.sh";
13640
13325
  function isLoopback(hostname3) {
13641
13326
  return hostname3 === "localhost" || hostname3 === "127.0.0.1" || hostname3 === "[::1]";
@@ -13686,7 +13371,9 @@ function relayError(status, data) {
13686
13371
  );
13687
13372
  }
13688
13373
  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.");
13374
+ return new Error(
13375
+ "Glossa could not choose a unique name for this computer. Try again."
13376
+ );
13690
13377
  }
13691
13378
  if (status === 404 && data.error === "device_not_found") {
13692
13379
  return new Error("The Glossa device was not found.");
@@ -13733,44 +13420,48 @@ async function accountOwnsDevice(endpoints, credentials, deviceId, fetchRequest
13733
13420
  );
13734
13421
  }
13735
13422
  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.");
13423
+ const baseName = deviceNameSchema.parse(deviceName);
13424
+ let name = baseName;
13425
+ for (let attempt = 0; attempt < 3; attempt += 1) {
13426
+ const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/enroll`, {
13427
+ method: "POST",
13428
+ headers: {
13429
+ authorization: `${credentials.tokenType} ${credentials.accessToken}`,
13430
+ "content-type": "application/json"
13431
+ },
13432
+ body: JSON.stringify({ name, platform: `${process.platform}-${process.arch}` })
13433
+ });
13434
+ let data = {};
13435
+ try {
13436
+ data = await response.json();
13437
+ } catch {
13438
+ }
13439
+ if (response.status === 409 && data.error === "device_name_conflict" && attempt < 2) {
13440
+ const activeNames = new Set(
13441
+ (await listDevices(endpoints, credentials, fetchRequest)).filter((device) => device.revokedAt === null).map((device) => device.name)
13442
+ );
13443
+ for (let suffix = 2; ; suffix += 1) {
13444
+ const ending = `-${suffix}`;
13445
+ const candidate = `${baseName.slice(0, 80 - ending.length)}${ending}`;
13446
+ if (!activeNames.has(candidate)) {
13447
+ name = deviceNameSchema.parse(candidate);
13448
+ break;
13449
+ }
13450
+ }
13451
+ continue;
13452
+ }
13453
+ if (!response.ok) throw relayError(response.status, data);
13454
+ if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
13455
+ throw new Error("The Glossa relay returned an invalid device enrollment response.");
13456
+ }
13457
+ return {
13458
+ relayOrigin: endpoints.relayOrigin,
13459
+ deviceId: data.device.id,
13460
+ deviceName: data.device.name,
13461
+ token: data.device_token
13462
+ };
13753
13463
  }
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];
13464
+ throw new Error("Glossa could not enroll this computer.");
13774
13465
  }
13775
13466
  async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
13776
13467
  const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
@@ -13785,258 +13476,605 @@ async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fet
13785
13476
  }
13786
13477
  }
13787
13478
 
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
- });
13818
- } 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
- });
13479
+ // src/status-service.ts
13480
+ function accountLabel(profile) {
13481
+ return profile.email ?? profile.name ?? profile.sub;
13482
+ }
13483
+ function activeWorkerCount(devices) {
13484
+ if (devices.some((device) => device.activeWorkers === null)) return null;
13485
+ return devices.reduce((sum, device) => sum + device.activeWorkers, 0);
13486
+ }
13487
+ var WorkspaceStatusService = class {
13488
+ constructor(credentials, endpoints, dependencies = {}) {
13489
+ this.endpoints = endpoints;
13490
+ this.dependencies = dependencies;
13491
+ this.#credentials = credentials;
13492
+ }
13493
+ endpoints;
13494
+ dependencies;
13495
+ #credentials;
13496
+ #account;
13497
+ #accountUnavailable = false;
13498
+ #cached;
13499
+ #inFlight;
13500
+ #profileInFlight;
13501
+ #listeners = /* @__PURE__ */ new Set();
13502
+ peek() {
13503
+ return this.#cached;
13504
+ }
13505
+ subscribe(listener) {
13506
+ this.#listeners.add(listener);
13507
+ return () => this.#listeners.delete(listener);
13508
+ }
13509
+ async refresh(signal, waitForAccount = false) {
13510
+ let status;
13511
+ if (this.#inFlight) {
13512
+ status = await this.#inFlight;
13513
+ } else {
13514
+ const pending = this.#load(signal);
13515
+ this.#inFlight = pending;
13516
+ try {
13517
+ status = await pending;
13518
+ } finally {
13519
+ if (this.#inFlight === pending) this.#inFlight = void 0;
13520
+ }
13521
+ }
13522
+ if (waitForAccount && this.#profileInFlight) {
13523
+ await this.#profileInFlight;
13524
+ return this.#cached ?? status;
13525
+ }
13526
+ return status;
13828
13527
  }
13829
- let endpoints = dependencies.endpoints;
13830
- if (!endpoints) {
13831
- 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."
13528
+ async #load(signal) {
13529
+ const validate = this.dependencies.validCredentials ?? validCredentials;
13530
+ const devicesForAccount = this.dependencies.listDevices ?? listDevices;
13531
+ const baseFetch = this.dependencies.fetch ?? fetch;
13532
+ const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
13533
+ this.#credentials = await validate(
13534
+ this.#credentials,
13535
+ signal ? { signal } : {}
13536
+ );
13537
+ const requestCredentials = this.#credentials;
13538
+ if (!this.#account && !this.#profileInFlight) {
13539
+ this.#accountUnavailable = false;
13540
+ const pending = this.#loadAccount(
13541
+ requestCredentials,
13542
+ fetchRequest,
13543
+ signal
13544
+ ).catch(() => {
13545
+ if (signal?.aborted) return;
13546
+ this.#accountUnavailable = true;
13547
+ if (this.#cached) {
13548
+ this.#cached = { ...this.#cached, account: "Account unavailable" };
13549
+ this.#publish(this.#cached);
13550
+ }
13551
+ });
13552
+ this.#profileInFlight = pending;
13553
+ void pending.finally(() => {
13554
+ if (this.#profileInFlight === pending) this.#profileInFlight = void 0;
13840
13555
  });
13841
13556
  }
13557
+ const devices = await devicesForAccount(
13558
+ this.endpoints,
13559
+ requestCredentials,
13560
+ fetchRequest
13561
+ );
13562
+ const status = {
13563
+ account: this.#account ?? (this.#accountUnavailable ? "Account unavailable" : "Loading account\u2026"),
13564
+ relay: this.endpoints.relayOrigin,
13565
+ activeWorkers: activeWorkerCount(devices),
13566
+ devices
13567
+ };
13568
+ this.#cached = status;
13569
+ this.#publish(status);
13570
+ return status;
13842
13571
  }
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." }
13851
- });
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
- });
13572
+ async #loadAccount(credentials, fetchRequest, signal) {
13573
+ const profile = this.dependencies.loadUserProfile ?? loadUserProfile;
13574
+ const result = await profile(
13575
+ credentials,
13576
+ signal ? { signal, fetch: fetchRequest } : { fetch: fetchRequest }
13577
+ );
13578
+ this.#credentials = result.credentials;
13579
+ this.#account = accountLabel(result.profile);
13580
+ this.#accountUnavailable = false;
13581
+ if (this.#cached) {
13582
+ this.#cached = { ...this.#cached, account: this.#account };
13583
+ this.#publish(this.#cached);
13860
13584
  }
13861
13585
  }
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;
13586
+ #publish(status) {
13587
+ for (const listener of this.#listeners) listener(status);
13876
13588
  }
13589
+ };
13590
+
13591
+ // src/ui-hud.ts
13592
+ import { emitKeypressEvents } from "node:readline";
13593
+ function initialHudState(workspace) {
13594
+ return {
13595
+ workspace,
13596
+ connection: "starting",
13597
+ message: void 0,
13598
+ activities: [],
13599
+ view: "session",
13600
+ status: void 0,
13601
+ statusLoading: false,
13602
+ prompt: void 0,
13603
+ busy: false,
13604
+ notice: void 0
13605
+ };
13606
+ }
13607
+ function activityLabel(event) {
13608
+ const noun = event.jobType === "write_file" ? "File write" : event.jobType === "edit_file" ? "File edit" : event.jobType === "run_command" ? "Command" : "Cancellation";
13609
+ if (event.phase === "requested") return `${noun} requested`;
13610
+ if (event.jobType === "run_command") return `Command ${event.ok ? "started" : "rejected"}`;
13611
+ return `${noun} ${event.ok ? "completed" : "rejected"}`;
13877
13612
  }
13878
- function signInCheck(state) {
13879
- if (state === "present") {
13880
- return { name: "Sign-in", status: "pass", detail: "Signed in to Glossa." };
13613
+ function applyHudEvent(state, event) {
13614
+ if (event.type === "session") {
13615
+ return { ...state, workspace: event.root, deviceName: event.deviceName };
13881
13616
  }
13882
- if (state === "absent") {
13883
- 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.'
13888
- };
13617
+ if (event.type === "status") {
13618
+ if (event.status.state === "retrying") {
13619
+ return { ...state, connection: "retrying", message: event.status.error.message };
13620
+ }
13621
+ return { ...state, connection: event.status.state, message: void 0 };
13889
13622
  }
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
- };
13623
+ if (event.type === "notice") return { ...state, message: event.message };
13624
+ const activity = event.phase === "finished" ? { label: activityLabel(event), requestId: event.requestId, ok: event.ok } : { label: activityLabel(event), requestId: event.requestId };
13625
+ return { ...state, activities: [...state.activities.slice(-7), activity] };
13896
13626
  }
13897
- function deviceCredentialCheck(state) {
13898
- if (state === "present") {
13899
- return {
13900
- name: "Device",
13901
- status: "pass",
13902
- detail: "Device credentials are readable."
13903
- };
13627
+ var ANSI_BASE = "\x1B[22;38;2;244;241;251;48;2;17;16;22m";
13628
+ var PALETTE = {
13629
+ ink: "38;2;244;241;251",
13630
+ muted: "38;2;170;164;181",
13631
+ purple: "38;2;128;84;255",
13632
+ purpleReadable: "38;2;173;152;255",
13633
+ coral: "38;2;255;102;95",
13634
+ line: "38;2;92;85;110"
13635
+ };
13636
+ function style(enabled, code, value) {
13637
+ return enabled ? `\x1B[${code}m${value}${ANSI_BASE}` : value;
13638
+ }
13639
+ function truncate(value, width) {
13640
+ if (value.length <= width) return value;
13641
+ if (width <= 1) return "\u2026";
13642
+ return `${value.slice(0, width - 1)}\u2026`;
13643
+ }
13644
+ function wrapText(value, width) {
13645
+ const words = value.split(/\s+/);
13646
+ const lines = [];
13647
+ let line = "";
13648
+ for (const word of words) {
13649
+ if (!line) line = word;
13650
+ else if (`${line} ${word}`.length <= width) line += ` ${word}`;
13651
+ else {
13652
+ lines.push(line);
13653
+ line = word;
13654
+ }
13904
13655
  }
13905
- if (state === "absent") {
13656
+ if (line) lines.push(line);
13657
+ return lines;
13658
+ }
13659
+ function sectionTitle(label, color, tone = PALETTE.purpleReadable) {
13660
+ return style(color, `${tone};1`, label.toUpperCase());
13661
+ }
13662
+ function renderHeader(view, usable, color) {
13663
+ const brand = "Glossa";
13664
+ const fullViewLabel = {
13665
+ session: "SESSION",
13666
+ activity: "ACTIVITY",
13667
+ status: "ACCOUNT & DEVICES",
13668
+ help: "KEYBOARD"
13669
+ }[view];
13670
+ const viewLabel = truncate(
13671
+ fullViewLabel,
13672
+ Math.max(4, usable - brand.length - 1)
13673
+ );
13674
+ const gap = " ".repeat(Math.max(1, usable - brand.length - viewLabel.length));
13675
+ return [
13676
+ `${style(color, `${PALETTE.purple};1`, brand)}${gap}${style(color, `${PALETTE.coral};1`, viewLabel)}`,
13677
+ style(color, PALETTE.line, "\u2500".repeat(usable))
13678
+ ];
13679
+ }
13680
+ function connectionCopy(state) {
13681
+ if (state.connection === "connected") {
13906
13682
  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.'
13683
+ glyph: "\u25CF",
13684
+ label: "Connected",
13685
+ detail: state.message ?? "ChatGPT can use this workspace."
13911
13686
  };
13912
13687
  }
13913
- 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
- };
13688
+ if (state.connection === "connecting" || state.connection === "starting") {
13689
+ return { glyph: "\u25CC", label: "Connecting", detail: "Establishing the managed relay session\u2026" };
13690
+ }
13691
+ if (state.connection === "retrying") {
13692
+ return { glyph: "\u25CC", label: "Reconnecting", detail: state.message ?? "Retrying automatically\u2026" };
13693
+ }
13694
+ if (state.connection === "error") {
13695
+ return { glyph: "\xD7", label: "Error", detail: state.message ?? "The session stopped unexpectedly." };
13696
+ }
13697
+ return { glyph: "\u25CB", label: "Disconnected", detail: "The workspace is no longer exposed." };
13919
13698
  }
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}`);
13929
- }
13699
+ function renderSession(state, usable, color) {
13700
+ const copy = connectionCopy(state);
13701
+ const statusTone = state.connection === "connected" ? PALETTE.purpleReadable : state.connection === "error" ? PALETTE.coral : PALETTE.muted;
13702
+ const lines = [
13703
+ "",
13704
+ style(color, `${statusTone};1`, `${copy.glyph} ${copy.label}`),
13705
+ ...wrapText(copy.detail, usable).map((line) => style(color, PALETTE.muted, line)),
13706
+ "",
13707
+ sectionTitle("Workspace", color),
13708
+ style(color, PALETTE.ink, truncate(state.workspace, usable))
13709
+ ];
13710
+ if (state.deviceName) {
13711
+ lines.push(
13712
+ style(color, PALETTE.muted, `Device ${truncate(state.deviceName, Math.max(8, usable - 8))}`)
13713
+ );
13930
13714
  }
13931
- const failed = checks.filter((check2) => check2.status === "fail").length;
13932
- lines.push("");
13933
13715
  lines.push(
13934
- failed === 0 ? "Glossa is ready to start." : `${failed} check${failed === 1 ? "" : "s"} failed. Resolve the items above before starting.`
13716
+ "",
13717
+ ...wrapText(
13718
+ "Files and commands use your account permissions.",
13719
+ Math.max(8, usable - 2)
13720
+ ).map(
13721
+ (line, index) => `${index === 0 ? style(color, `${PALETTE.coral};1`, "!") : " "} ${style(color, PALETTE.muted, line)}`
13722
+ )
13935
13723
  );
13936
- return lines.join("\n");
13937
- }
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");
13724
+ const latest = state.activities.at(-1);
13725
+ lines.push(
13726
+ "",
13727
+ sectionTitle("Latest activity", color),
13728
+ latest ? `${style(color, latest.ok === false ? PALETTE.coral : PALETTE.purpleReadable, latest.ok === false ? "\xD7" : "\u2022")} ${style(color, PALETTE.ink, truncate(latest.label, Math.max(8, usable - 2)))}` : style(color, PALETTE.muted, "No tool activity yet.")
13729
+ );
13730
+ return lines;
13942
13731
  }
13943
- async function defaultFetchHealthz(origin) {
13944
- 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";
13951
- } catch {
13952
- return false;
13732
+ function renderActivity(state, usable, color) {
13733
+ const lines = ["", sectionTitle("Recent activity", color)];
13734
+ if (state.activities.length === 0) {
13735
+ lines.push("", style(color, PALETTE.muted, "No tool activity yet."));
13953
13736
  }
13954
- }
13955
- async function defaultProbeCredentials() {
13956
- try {
13957
- return await peekCredentials() !== null ? "present" : "absent";
13958
- } catch {
13959
- return "error";
13737
+ for (const activity of state.activities.slice(-8)) {
13738
+ const outcome = activity.ok === false ? style(color, PALETTE.coral, "\xD7") : style(color, activity.ok === true ? PALETTE.purpleReadable : PALETTE.muted, "\u2022");
13739
+ lines.push(
13740
+ "",
13741
+ `${outcome} ${style(color, PALETTE.ink, truncate(activity.label, Math.max(8, usable - 2)))}`,
13742
+ ` ${style(color, PALETTE.muted, `Request ${activity.requestId.slice(0, 8)}`)}`
13743
+ );
13960
13744
  }
13745
+ return lines;
13961
13746
  }
13962
- async function defaultProbeDeviceCredential() {
13963
- try {
13964
- return await peekDeviceCredential() !== null ? "present" : "absent";
13965
- } catch {
13966
- return "error";
13747
+ function renderStatus(state, usable, color) {
13748
+ const lines = ["", sectionTitle("Account", color)];
13749
+ if (state.statusLoading) {
13750
+ lines.push("", style(color, PALETTE.muted, "Loading account and devices\u2026"));
13751
+ return lines;
13967
13752
  }
13753
+ if (!state.status) {
13754
+ lines.push("", style(color, PALETTE.muted, "Status is not loaded."));
13755
+ return lines;
13756
+ }
13757
+ lines.push(
13758
+ style(color, PALETTE.ink, truncate(state.status.account, usable)),
13759
+ style(color, PALETTE.muted, truncate(state.status.relay, usable)),
13760
+ "",
13761
+ sectionTitle("Active workspaces", color, PALETTE.coral),
13762
+ style(
13763
+ color,
13764
+ PALETTE.ink,
13765
+ state.status.activeWorkers === null ? "Unavailable" : String(state.status.activeWorkers)
13766
+ ),
13767
+ "",
13768
+ sectionTitle(`Devices ${state.status.devices.length}`, color)
13769
+ );
13770
+ if (state.status.devices.length === 0) {
13771
+ lines.push("", style(color, PALETTE.muted, "No devices enrolled."));
13772
+ }
13773
+ state.status.devices.slice(0, 9).forEach((device, index) => {
13774
+ const statusTone = device.status.includes("active") ? PALETTE.purpleReadable : device.status === "revoked" ? PALETTE.coral : PALETTE.muted;
13775
+ lines.push(
13776
+ "",
13777
+ `${style(color, `${PALETTE.coral};1`, String(index + 1).padStart(2))} ${style(color, `${PALETTE.ink};1`, truncate(device.name, Math.max(8, usable - 4)))}`,
13778
+ ` ${style(color, statusTone, device.status)}`,
13779
+ ` ${style(color, PALETTE.muted, truncate(`${device.platform} \u2022 seen ${device.lastSeen}`, Math.max(8, usable - 4)))}`
13780
+ );
13781
+ });
13782
+ return lines;
13968
13783
  }
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"}`;
13784
+ function helpRows(key, label, usable, color, tone = PALETTE.purpleReadable) {
13785
+ const indent = " ".repeat(key.length + 2);
13786
+ return wrapText(label, Math.max(8, usable - indent.length)).map(
13787
+ (line, index) => index === 0 ? `${style(color, `${tone};1`, key)} ${line}` : `${indent}${line}`
13788
+ );
13976
13789
  }
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`;
13790
+ function renderHelp(usable, color) {
13791
+ return [
13792
+ "",
13793
+ sectionTitle("Navigate", color),
13794
+ ...helpRows("D", "Recent activity", usable, color),
13795
+ ...helpRows("S", "Account and devices", usable, color),
13796
+ ...helpRows("?", "Close help", usable, color),
13797
+ "",
13798
+ sectionTitle("Manage", color, PALETTE.coral),
13799
+ ...helpRows("R", "Revoke a device from the status view", usable, color, PALETTE.coral),
13800
+ ...helpRows("L", "Sign out", usable, color, PALETTE.coral),
13801
+ ...helpRows("U", "Update Glossa", usable, color, PALETTE.coral),
13802
+ "",
13803
+ sectionTitle("Session", color),
13804
+ ...helpRows("Q", "Disconnect and quit", usable, color, PALETTE.coral),
13805
+ ...helpRows("Ctrl+C", "Disconnect and quit", usable, color, PALETTE.coral)
13806
+ ];
13989
13807
  }
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)}`;
13808
+ function promptText(state) {
13809
+ if (state.busy) return { message: "Working\u2026" };
13810
+ if (!state.prompt) return void 0;
13811
+ if (state.prompt.type === "logout") {
13812
+ return { message: "Sign out and disconnect?", choices: "Y confirm N cancel" };
13813
+ }
13814
+ if (state.prompt.type === "update") {
13815
+ return { message: "Disconnect and update Glossa?", choices: "Y confirm N cancel" };
13816
+ }
13817
+ if (state.prompt.type === "revoke-select") {
13818
+ return { message: "Choose a device number to revoke.", choices: "Esc cancel" };
13819
+ }
13820
+ const device = state.status?.devices[state.prompt.deviceIndex];
13821
+ return {
13822
+ message: `Revoke ${device?.name ?? "this device"}?`,
13823
+ choices: "Y confirm N cancel"
13824
+ };
13825
+ }
13826
+ function footerHints(state) {
13827
+ if (state.view === "status") {
13828
+ return [
13829
+ { key: "R", label: "Revoke", tone: PALETTE.coral },
13830
+ { key: "L", label: "Sign out", tone: PALETTE.coral },
13831
+ { key: "U", label: "Update", tone: PALETTE.coral },
13832
+ { key: "Esc", label: "Session" },
13833
+ { key: "Q", label: "Disconnect", tone: PALETTE.coral }
13834
+ ];
13835
+ }
13836
+ if (state.view === "activity") {
13837
+ return [
13838
+ { key: "D", label: "Session" },
13839
+ { key: "S", label: "Status" },
13840
+ { key: "?", label: "Help" },
13841
+ { key: "Q", label: "Disconnect", tone: PALETTE.coral }
13842
+ ];
13843
+ }
13844
+ if (state.view === "help") {
13845
+ return [
13846
+ { key: "?", label: "Session" },
13847
+ { key: "Q", label: "Disconnect", tone: PALETTE.coral }
13848
+ ];
13849
+ }
13850
+ return [
13851
+ { key: "D", label: "Activity" },
13852
+ { key: "S", label: "Status" },
13853
+ { key: "?", label: "Help" },
13854
+ { key: "Q", label: "Disconnect", tone: PALETTE.coral }
13855
+ ];
13993
13856
  }
13994
-
13995
- // src/logout.ts
13996
- function browserLogoutUrl(issuer) {
13997
- return new URL(
13998
- "v2/logout",
13999
- issuer.endsWith("/") ? issuer : `${issuer}/`
14000
- ).toString();
13857
+ function renderFooter(state, usable, color) {
13858
+ const rows = [[]];
13859
+ let rowLength = 0;
13860
+ for (const hint of footerHints(state)) {
13861
+ const tokenLength = hint.key.length + hint.label.length + 1;
13862
+ if (rows.at(-1).length > 0 && rowLength + 3 + tokenLength > usable) {
13863
+ rows.push([]);
13864
+ rowLength = 0;
13865
+ }
13866
+ rows.at(-1).push(hint);
13867
+ rowLength += (rowLength > 0 ? 3 : 0) + tokenLength;
13868
+ }
13869
+ return rows.map(
13870
+ (row) => row.map(
13871
+ (hint) => `${style(color, `${hint.tone ?? PALETTE.purpleReadable};1`, hint.key)} ${style(color, PALETTE.muted, hint.label)}`
13872
+ ).join(" ")
13873
+ );
14001
13874
  }
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 {
13875
+ function renderHud(state, width = 80, color = !process.env.NO_COLOR) {
13876
+ const usable = Math.max(20, width - 4);
13877
+ const margin = width >= 24 ? " " : "";
13878
+ const lines = [...renderHeader(state.view, usable, color)];
13879
+ if (state.view === "activity") lines.push(...renderActivity(state, usable, color));
13880
+ else if (state.view === "status") lines.push(...renderStatus(state, usable, color));
13881
+ else if (state.view === "help") lines.push(...renderHelp(usable, color));
13882
+ else lines.push(...renderSession(state, usable, color));
13883
+ if (state.notice) {
13884
+ lines.push(
13885
+ "",
13886
+ style(color, PALETTE.line, "\u2500".repeat(usable)),
13887
+ ...wrapText(`! ${state.notice}`, usable).map((line) => style(color, PALETTE.coral, line))
13888
+ );
14013
13889
  }
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);
13890
+ const prompt = promptText(state);
13891
+ if (prompt) {
13892
+ lines.push(
13893
+ "",
13894
+ style(color, PALETTE.line, "\u2500".repeat(usable)),
13895
+ style(color, `${PALETTE.coral};1`, truncate(prompt.message, usable))
13896
+ );
13897
+ if (prompt.choices) lines.push(style(color, PALETTE.muted, prompt.choices));
14027
13898
  }
14028
- log(
14029
- "Reconnect Glossa in ChatGPT, then choose the same Google account when the CLI signs in."
13899
+ lines.push(
13900
+ "",
13901
+ style(color, PALETTE.line, "\u2500".repeat(usable)),
13902
+ ...renderFooter(state, usable, color)
14030
13903
  );
13904
+ return lines.map((line) => line ? `${margin}${line}` : "").join("\n");
14031
13905
  }
14032
-
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.';
13906
+ async function runSessionHud(actions, input = process.stdin, output = process.stdout) {
13907
+ if (!input.isTTY || !output.isTTY) {
13908
+ throw new Error("Glossa requires an interactive terminal.");
13909
+ }
13910
+ emitKeypressEvents(input);
13911
+ const wasRaw = input.isRaw;
13912
+ const controller = new AbortController();
13913
+ let state = initialHudState(actions.workspace);
13914
+ let exitAction = "quit";
13915
+ let stopUi;
13916
+ const color = !process.env.NO_COLOR;
13917
+ const render = () => {
13918
+ const view = renderHud(state, output.columns ?? 80, color);
13919
+ output.write(`${color ? ANSI_BASE : ""}\x1B[H\x1B[2J${view}`);
13920
+ };
13921
+ const loadStatus = async () => {
13922
+ if (state.connection !== "connected" && state.connection !== "retrying") {
13923
+ state = { ...state, notice: "Status is available after Glossa connects." };
13924
+ render();
13925
+ return;
13926
+ }
13927
+ const cached2 = state.status ?? actions.peekStatus?.();
13928
+ state = {
13929
+ ...state,
13930
+ view: "status",
13931
+ status: cached2,
13932
+ statusLoading: !cached2,
13933
+ prompt: void 0,
13934
+ notice: void 0
13935
+ };
13936
+ render();
13937
+ try {
13938
+ const refreshed = await actions.loadStatus(controller.signal);
13939
+ if (controller.signal.aborted) return;
13940
+ const status = actions.peekStatus?.() ?? refreshed;
13941
+ state = { ...state, status, statusLoading: false };
13942
+ } catch (error46) {
13943
+ if (controller.signal.aborted) return;
13944
+ state = {
13945
+ ...state,
13946
+ statusLoading: false,
13947
+ notice: error46 instanceof Error ? error46.message : String(error46)
13948
+ };
13949
+ }
13950
+ render();
13951
+ };
13952
+ const unsubscribeStatus = actions.subscribeStatus?.((status) => {
13953
+ state = { ...state, status, statusLoading: false };
13954
+ if (state.view === "status") render();
13955
+ }) ?? (() => void 0);
13956
+ const session = actions.run(controller.signal, (event) => {
13957
+ state = applyHudEvent(state, event);
13958
+ render();
13959
+ }).then(() => {
13960
+ if (!controller.signal.aborted) state = { ...state, connection: "disconnected" };
13961
+ render();
13962
+ }).catch((error46) => {
13963
+ if (controller.signal.aborted) return;
13964
+ state = {
13965
+ ...state,
13966
+ connection: "error",
13967
+ message: error46 instanceof Error ? error46.message : String(error46)
13968
+ };
13969
+ render();
13970
+ throw error46;
13971
+ });
13972
+ input.setRawMode(true);
13973
+ input.resume();
13974
+ output.write("\x1B[?1049h\x1B[?25l");
13975
+ render();
13976
+ const stop = (action = "quit") => {
13977
+ exitAction = action;
13978
+ controller.abort();
13979
+ stopUi?.();
13980
+ };
13981
+ const stopFromSignal = () => stop();
13982
+ process.once("SIGINT", stopFromSignal);
13983
+ process.once("SIGTERM", stopFromSignal);
13984
+ try {
13985
+ await new Promise((resolve) => {
13986
+ stopUi = resolve;
13987
+ const onKeypress = (value, key) => {
13988
+ if (key.ctrl && key.name === "c" || key.name === "q") return stop();
13989
+ if (state.busy) return;
13990
+ if (state.prompt) {
13991
+ if (key.name === "escape" || key.name === "n") {
13992
+ state = { ...state, prompt: void 0, notice: void 0 };
13993
+ render();
13994
+ return;
13995
+ }
13996
+ if (state.prompt.type === "revoke-select") {
13997
+ const deviceIndex = Number(value) - 1;
13998
+ if (Number.isInteger(deviceIndex) && deviceIndex >= 0 && deviceIndex < (state.status?.devices.length ?? 0)) {
13999
+ state = { ...state, prompt: { type: "revoke-confirm", deviceIndex } };
14000
+ render();
14001
+ }
14002
+ return;
14003
+ }
14004
+ if (key.name !== "y") return;
14005
+ if (state.prompt.type === "logout") return stop("logout");
14006
+ if (state.prompt.type === "update") return stop("update");
14007
+ const device = state.status?.devices[state.prompt.deviceIndex];
14008
+ if (!device) return;
14009
+ state = { ...state, busy: true, prompt: void 0, notice: void 0 };
14010
+ render();
14011
+ void actions.revokeDevice(device.id, controller.signal).then(async () => {
14012
+ if (controller.signal.aborted) return;
14013
+ state = { ...state, busy: false, notice: `Revoked ${device.name}.` };
14014
+ render();
14015
+ await loadStatus();
14016
+ }).catch((error46) => {
14017
+ if (controller.signal.aborted) return;
14018
+ state = {
14019
+ ...state,
14020
+ busy: false,
14021
+ notice: error46 instanceof Error ? error46.message : String(error46)
14022
+ };
14023
+ render();
14024
+ });
14025
+ return;
14026
+ }
14027
+ if (key.name === "escape") {
14028
+ state = { ...state, view: "session", notice: void 0 };
14029
+ render();
14030
+ } else if (key.name === "d") {
14031
+ state = {
14032
+ ...state,
14033
+ view: state.view === "activity" ? "session" : "activity",
14034
+ notice: void 0
14035
+ };
14036
+ render();
14037
+ } else if (key.name === "s") {
14038
+ void loadStatus();
14039
+ } else if (key.name === "r" && state.view === "status") {
14040
+ if ((state.status?.devices.length ?? 0) === 0) {
14041
+ state = { ...state, notice: "There are no devices to revoke." };
14042
+ } else {
14043
+ state = { ...state, prompt: { type: "revoke-select" }, notice: void 0 };
14044
+ }
14045
+ render();
14046
+ } else if (key.name === "l") {
14047
+ state = { ...state, prompt: { type: "logout" }, notice: void 0 };
14048
+ render();
14049
+ } else if (key.name === "u") {
14050
+ state = { ...state, prompt: { type: "update" }, notice: void 0 };
14051
+ render();
14052
+ } else if (value === "?" || key.sequence === "?") {
14053
+ state = {
14054
+ ...state,
14055
+ view: state.view === "help" ? "session" : "help",
14056
+ notice: void 0
14057
+ };
14058
+ render();
14059
+ }
14060
+ };
14061
+ input.on("keypress", onKeypress);
14062
+ void session.catch(() => stopUi?.());
14063
+ stopUi = () => {
14064
+ input.removeListener("keypress", onKeypress);
14065
+ resolve();
14066
+ };
14067
+ });
14068
+ await session;
14069
+ return exitAction;
14070
+ } finally {
14071
+ unsubscribeStatus();
14072
+ process.removeListener("SIGINT", stopFromSignal);
14073
+ process.removeListener("SIGTERM", stopFromSignal);
14074
+ input.setRawMode(wasRaw);
14075
+ input.pause();
14076
+ output.write("\x1B[0m\x1B[?25h\x1B[?1049l");
14038
14077
  }
14039
- return null;
14040
14078
  }
14041
14079
 
14042
14080
  // src/update.ts
@@ -14044,11 +14082,20 @@ import { spawnSync } from "node:child_process";
14044
14082
  import { createHash } from "node:crypto";
14045
14083
  import {
14046
14084
  chmodSync,
14047
- existsSync as existsSync2,
14085
+ existsSync,
14048
14086
  renameSync,
14049
14087
  rmSync,
14050
14088
  writeFileSync
14051
14089
  } from "node:fs";
14090
+
14091
+ // src/runtime.ts
14092
+ function isStandaloneExecutable(value = true ? false : false) {
14093
+ if (typeof value === "boolean") return value;
14094
+ if (!value || typeof value !== "object") return false;
14095
+ return value.isStandaloneExecutable === true;
14096
+ }
14097
+
14098
+ // src/update.ts
14052
14099
  var NPM_PACKAGE = "@ariobarin/glossa@beta";
14053
14100
  var DEFAULT_RELEASES_API = "https://api.github.com/repos/ariobarin/glossa/releases?per_page=20";
14054
14101
  function npmUpdateInvocation(platform = process.platform, environment = process.env) {
@@ -14134,264 +14181,81 @@ async function updateStandalone(dependencies) {
14134
14181
  }
14135
14182
  const [binaryResponse, checksumResponse] = await Promise.all([
14136
14183
  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
- );
14146
- }
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);
14151
- 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
- }
14168
- } finally {
14169
- if (existsSync2(downloadPath)) rmSync(downloadPath, { force: true });
14170
- }
14171
- return `Updated Glossa to ${release.version}.`;
14172
- }
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;
14181
- }
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) {
14203
- 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." };
14184
+ checkedFetch(fetcher, release.checksumUrl)
14185
+ ]);
14186
+ const binary = Buffer.from(await binaryResponse.arrayBuffer());
14187
+ const expected = expectedChecksum(await checksumResponse.text(), assetName);
14188
+ const actual = createHash("sha256").update(binary).digest("hex");
14189
+ if (actual !== expected) {
14190
+ throw new Error(
14191
+ `Glossa refused the update because the SHA-256 checksum did not match.`
14192
+ );
14276
14193
  }
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))}`);
14194
+ const downloadPath = `${executablePath}.download-${process.pid}`;
14195
+ const backupPath = `${executablePath}.old`;
14196
+ writeFileSync(downloadPath, binary, { mode: 493 });
14197
+ if (platform !== "win32") chmodSync(downloadPath, 493);
14198
+ try {
14199
+ if (platform === "win32") {
14200
+ rmSync(backupPath, { force: true });
14201
+ renameSync(executablePath, backupPath);
14202
+ try {
14203
+ renameSync(downloadPath, executablePath);
14204
+ } catch (error46) {
14205
+ renameSync(backupPath, executablePath);
14206
+ throw error46;
14207
+ }
14208
+ try {
14209
+ rmSync(backupPath, { force: true });
14210
+ } catch {
14211
+ }
14212
+ } else {
14213
+ renameSync(downloadPath, executablePath);
14308
14214
  }
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."));
14215
+ } finally {
14216
+ if (existsSync(downloadPath)) rmSync(downloadPath, { force: true });
14312
14217
  }
14313
- lines.push("", style(color, "2", "d details ? help q disconnect"));
14314
- return lines.join("\n");
14218
+ return `Updated Glossa to ${release.version}.`;
14315
14219
  }
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.");
14220
+ async function updateGlossa(dependencies = {}) {
14221
+ const log = dependencies.log ?? console.log;
14222
+ const standalone = dependencies.standalone ?? isStandaloneExecutable();
14223
+ if (standalone) {
14224
+ log("Updating the standalone Glossa executable...");
14225
+ const message = dependencies.updateStandalone ? await dependencies.updateStandalone() : await updateStandalone(dependencies);
14226
+ log(message);
14227
+ return;
14319
14228
  }
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");
14229
+ const run = dependencies.run ?? spawnSync;
14230
+ const invocation = npmUpdateInvocation(
14231
+ dependencies.platform,
14232
+ dependencies.environment
14233
+ );
14234
+ log("Updating Glossa from the npm beta channel...");
14235
+ const result = run(invocation.command, invocation.args, { stdio: "inherit" });
14236
+ if (result.error) {
14237
+ throw new Error(`Glossa could not start npm: ${result.error.message}`);
14238
+ }
14239
+ if (result.status !== 0) {
14240
+ throw new Error(
14241
+ `npm could not update Glossa (exit ${result.status ?? "unknown"}).`
14242
+ );
14382
14243
  }
14244
+ log("Glossa updated.");
14245
+ log("Next: run glossa to reopen this workspace.");
14246
+ log("Inside Glossa, press ? for controls.");
14383
14247
  }
14384
14248
 
14385
14249
  // src/first-run.ts
14386
14250
  import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "node:fs/promises";
14387
- import path5 from "node:path";
14251
+ import path3 from "node:path";
14388
14252
  var CONNECT_HINT_FILE = "connect-hint-shown";
14389
14253
  var CONNECT_HINT_URL = "https://glossa.sh/docs/quickstart";
14390
14254
  function shouldShowConnectHint(relayOrigin) {
14391
14255
  return relayOrigin === DEFAULT_RELAY_ORIGIN;
14392
14256
  }
14393
14257
  function connectHintStore(directory = configDirectory()) {
14394
- const file2 = path5.join(directory, CONNECT_HINT_FILE);
14258
+ const file2 = path3.join(directory, CONNECT_HINT_FILE);
14395
14259
  return {
14396
14260
  async exists() {
14397
14261
  try {
@@ -14415,6 +14279,47 @@ async function announceConnectHint(store3, log) {
14415
14279
  return true;
14416
14280
  }
14417
14281
 
14282
+ // src/device-store.ts
14283
+ import path4 from "node:path";
14284
+ var FILE_DEVICE_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 device credential file.";
14285
+ function parseDeviceCredential(value) {
14286
+ let parsed;
14287
+ try {
14288
+ parsed = JSON.parse(value);
14289
+ } catch {
14290
+ throw new Error("Stored Glossa device credentials are invalid.");
14291
+ }
14292
+ let relayOriginValid = false;
14293
+ if (typeof parsed.relayOrigin === "string") {
14294
+ try {
14295
+ relayOriginValid = new URL(parsed.relayOrigin).origin === parsed.relayOrigin;
14296
+ } catch {
14297
+ relayOriginValid = false;
14298
+ }
14299
+ }
14300
+ 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(
14301
+ parsed.deviceId
14302
+ ) || 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}_`)) {
14303
+ throw new Error("Stored Glossa device credentials are invalid.");
14304
+ }
14305
+ return parsed;
14306
+ }
14307
+ var store2 = new SecureStore({
14308
+ account: "device",
14309
+ file: path4.join(configDirectory(), "device.json"),
14310
+ warning: FILE_DEVICE_WARNING,
14311
+ parse: parseDeviceCredential
14312
+ });
14313
+ async function loadDeviceCredential() {
14314
+ return (await store2.load())?.value ?? null;
14315
+ }
14316
+ async function saveDeviceCredential(credential) {
14317
+ await store2.save(credential);
14318
+ }
14319
+ async function deleteDeviceCredential() {
14320
+ await store2.delete();
14321
+ }
14322
+
14418
14323
  // src/worker/command-service.ts
14419
14324
  import { spawn as spawn2 } from "node:child_process";
14420
14325
  import { randomUUID } from "node:crypto";
@@ -14659,7 +14564,7 @@ var CommandService = class {
14659
14564
  // src/worker/file-service.ts
14660
14565
  import { createHash as createHash2, randomUUID as randomUUID2 } from "node:crypto";
14661
14566
  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";
14567
+ import path5 from "node:path";
14663
14568
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
14664
14569
  function sha256(content) {
14665
14570
  return createHash2("sha256").update(content).digest("hex");
@@ -14827,7 +14732,7 @@ var FileService = class {
14827
14732
  throw new WorkerError("stale_revision", "The file revision has changed.");
14828
14733
  }
14829
14734
  }
14830
- const temporary = path6.join(path6.dirname(target), `.glossa-${randomUUID2()}.tmp`);
14735
+ const temporary = path5.join(path5.dirname(target), `.glossa-${randomUUID2()}.tmp`);
14831
14736
  try {
14832
14737
  await writeFile3(temporary, bytes, { flag: "wx", mode: 384 });
14833
14738
  target = await this.policy.resolveWritableFile(relativePath);
@@ -14849,19 +14754,19 @@ var FileService = class {
14849
14754
  // src/worker/path-policy.ts
14850
14755
  import { lstat as lstat2, realpath, stat as stat2 } from "node:fs/promises";
14851
14756
  import os3 from "node:os";
14852
- import path7 from "node:path";
14757
+ import path6 from "node:path";
14853
14758
  function samePath(left, right) {
14854
14759
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
14855
14760
  }
14856
14761
  function isWithin(root, candidate) {
14857
- const relative = path7.relative(root, candidate);
14858
- return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
14762
+ const relative = path6.relative(root, candidate);
14763
+ return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
14859
14764
  }
14860
14765
  function validateRelativePath(value) {
14861
14766
  if (value.includes("\0")) {
14862
14767
  throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
14863
14768
  }
14864
- if (path7.isAbsolute(value) || path7.win32.isAbsolute(value) || path7.posix.isAbsolute(value)) {
14769
+ if (path6.isAbsolute(value) || path6.win32.isAbsolute(value) || path6.posix.isAbsolute(value)) {
14865
14770
  throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
14866
14771
  }
14867
14772
  const segments = value.split(/[\\/]+/);
@@ -14870,8 +14775,8 @@ function validateRelativePath(value) {
14870
14775
  }
14871
14776
  return value === "" ? "." : value;
14872
14777
  }
14873
- async function canonicalizeRoot(candidate, allowBroadRoot = false) {
14874
- const root = await realpath(path7.resolve(candidate)).catch((error46) => {
14778
+ async function canonicalizeRoot(candidate) {
14779
+ const root = await realpath(path6.resolve(candidate)).catch((error46) => {
14875
14780
  if (error46.code === "ENOENT") {
14876
14781
  throw new WorkerError("root_not_found", "The workspace directory does not exist.");
14877
14782
  }
@@ -14881,16 +14786,14 @@ async function canonicalizeRoot(candidate, allowBroadRoot = false) {
14881
14786
  if (!rootStat.isDirectory()) {
14882
14787
  throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
14883
14788
  }
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
- }
14789
+ const filesystemRoot = path6.parse(root).root;
14790
+ const home = await realpath(os3.homedir()).catch(() => path6.resolve(os3.homedir()));
14791
+ if (samePath(root, filesystemRoot) || samePath(root, home)) {
14792
+ const kind = samePath(root, home) ? "your home directory" : "a filesystem root";
14793
+ throw new WorkerError(
14794
+ "broad_root_refused",
14795
+ `The selected root is ${kind}, which Glossa will not expose. Choose a project directory instead.`
14796
+ );
14894
14797
  }
14895
14798
  return root;
14896
14799
  }
@@ -14899,8 +14802,8 @@ var PathPolicy = class _PathPolicy {
14899
14802
  this.root = root;
14900
14803
  }
14901
14804
  root;
14902
- static async create(candidate, allowBroadRoot = false) {
14903
- return new _PathPolicy(await canonicalizeRoot(candidate, allowBroadRoot));
14805
+ static async create(candidate) {
14806
+ return new _PathPolicy(await canonicalizeRoot(candidate));
14904
14807
  }
14905
14808
  async resolveExisting(relativePath) {
14906
14809
  const lexical = this.resolveLexical(relativePath);
@@ -14918,7 +14821,7 @@ var PathPolicy = class _PathPolicy {
14918
14821
  }
14919
14822
  async resolveWritableFile(relativePath) {
14920
14823
  const lexical = this.resolveLexical(relativePath);
14921
- const parent = path7.dirname(lexical);
14824
+ const parent = path6.dirname(lexical);
14922
14825
  await this.rejectLinkedComponents(parent);
14923
14826
  const canonicalParent = await realpath(parent).catch((error46) => {
14924
14827
  if (error46.code === "ENOENT") {
@@ -14944,11 +14847,11 @@ var PathPolicy = class _PathPolicy {
14944
14847
  if (error46 instanceof WorkerError) throw error46;
14945
14848
  if (error46.code !== "ENOENT") throw error46;
14946
14849
  }
14947
- return path7.join(canonicalParent, path7.basename(lexical));
14850
+ return path6.join(canonicalParent, path6.basename(lexical));
14948
14851
  }
14949
14852
  resolveLexical(relativePath) {
14950
14853
  const validated = validateRelativePath(relativePath);
14951
- const candidate = path7.resolve(this.root, validated);
14854
+ const candidate = path6.resolve(this.root, validated);
14952
14855
  if (!isWithin(this.root, candidate)) {
14953
14856
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
14954
14857
  }
@@ -14958,11 +14861,11 @@ var PathPolicy = class _PathPolicy {
14958
14861
  if (!isWithin(this.root, candidate)) {
14959
14862
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
14960
14863
  }
14961
- const relative = path7.relative(this.root, candidate);
14864
+ const relative = path6.relative(this.root, candidate);
14962
14865
  if (!relative) return;
14963
14866
  let current = this.root;
14964
- for (const segment of relative.split(path7.sep)) {
14965
- current = path7.join(current, segment);
14867
+ for (const segment of relative.split(path6.sep)) {
14868
+ current = path6.join(current, segment);
14966
14869
  try {
14967
14870
  const currentStat = await lstat2(current);
14968
14871
  if (currentStat.isSymbolicLink()) {
@@ -14982,16 +14885,16 @@ var PathPolicy = class _PathPolicy {
14982
14885
 
14983
14886
  // src/worker/local-worker.ts
14984
14887
  var LocalWorker = class _LocalWorker {
14985
- constructor(policy, files, commands2) {
14888
+ constructor(policy, files, commands) {
14986
14889
  this.policy = policy;
14987
14890
  this.files = files;
14988
- this.commands = commands2;
14891
+ this.commands = commands;
14989
14892
  }
14990
14893
  policy;
14991
14894
  files;
14992
14895
  commands;
14993
- static async create(root, allowBroadRoot = false) {
14994
- const policy = await PathPolicy.create(root, allowBroadRoot);
14896
+ static async create(root) {
14897
+ const policy = await PathPolicy.create(root);
14995
14898
  return new _LocalWorker(
14996
14899
  policy,
14997
14900
  new FileService(policy),
@@ -15231,10 +15134,10 @@ var RemoteWorker = class {
15231
15134
  } catch {
15232
15135
  }
15233
15136
  }
15234
- async #post(path8, body) {
15137
+ async #post(path7, body) {
15235
15138
  const timeout = AbortSignal.timeout(WORKER_REQUEST_TIMEOUT_MS);
15236
15139
  const signal = AbortSignal.any([this.#signal, timeout]);
15237
- const response = await this.#fetcher(new URL(path8, this.#origin), {
15140
+ const response = await this.#fetcher(new URL(path7, this.#origin), {
15238
15141
  method: "POST",
15239
15142
  headers: {
15240
15143
  authorization: `Device ${this.#deviceToken}`,
@@ -15304,38 +15207,54 @@ async function deviceForSession(endpoints, dependencies = {}, signal) {
15304
15207
  const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
15305
15208
  const loadLogin = dependencies.loadCredentials ?? loadCredentials;
15306
15209
  const validate = dependencies.validCredentials ?? validCredentials;
15210
+ const subjectFor = dependencies.accessTokenSubject ?? accessTokenSubject;
15307
15211
  const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
15308
- const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
15309
15212
  const enroll = dependencies.enrollDevice ?? enrollDevice;
15310
15213
  const saveDevice = dependencies.saveDeviceCredential ?? saveDeviceCredential;
15214
+ const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
15311
15215
  const name = dependencies.defaultDeviceName ?? defaultDeviceName;
15312
15216
  const baseFetch = dependencies.fetch ?? fetch;
15313
15217
  const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
15314
15218
  signal?.throwIfAborted();
15315
15219
  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();
15220
+ let credentials = dependencies.credentials;
15221
+ const currentCredentials = async () => {
15222
+ if (credentials) return credentials;
15223
+ const loaded = await loadLogin();
15224
+ if (!loaded) throw new Error("Not signed in. Run Glossa again to sign in.");
15225
+ credentials = await validate(loaded.credentials, { fetch: fetchRequest });
15226
+ return credentials;
15227
+ };
15320
15228
  if (stored?.relayOrigin === endpoints.relayOrigin) {
15321
- if (await ownsDevice(
15322
- endpoints,
15323
- credentials,
15324
- stored.deviceId,
15325
- fetchRequest
15326
- )) {
15327
- return stored;
15229
+ const current2 = await currentCredentials();
15230
+ const accountSubject = subjectFor(current2);
15231
+ if (stored.accountSubject === accountSubject) return stored;
15232
+ if (stored.accountSubject === void 0 && await ownsDevice(endpoints, current2, stored.deviceId, fetchRequest)) {
15233
+ const migrated = { ...stored, accountSubject };
15234
+ await saveDevice(migrated);
15235
+ return migrated;
15328
15236
  }
15329
15237
  await removeDevice();
15330
15238
  }
15239
+ signal?.throwIfAborted();
15240
+ const current = await currentCredentials();
15331
15241
  const enrolled = await enroll(
15332
15242
  endpoints,
15333
- credentials,
15334
- dependencies.deviceName ?? name(),
15243
+ current,
15244
+ name(),
15335
15245
  fetchRequest
15336
15246
  );
15337
- await saveDevice(enrolled);
15338
- return enrolled;
15247
+ const bound = {
15248
+ ...enrolled,
15249
+ accountSubject: subjectFor(current)
15250
+ };
15251
+ await saveDevice(bound);
15252
+ return bound;
15253
+ }
15254
+ async function reenrollRejectedDevice(endpoints, dependencies = {}, signal) {
15255
+ const remove = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
15256
+ await remove();
15257
+ return await deviceForSession(endpoints, dependencies, signal);
15339
15258
  }
15340
15259
  function statusMessage(status, previous) {
15341
15260
  if (status.state === "connecting") return "Connecting to Glossa...";
@@ -15348,7 +15267,40 @@ function statusMessage(status, previous) {
15348
15267
  }
15349
15268
  return "Disconnected from Glossa.";
15350
15269
  }
15351
- async function runManagedSession(root, endpoints, allowBroadRoot = false, options = {}) {
15270
+ async function connectRemoteWorker(endpoints, device, worker, options, signal, onConnected) {
15271
+ let connectionState;
15272
+ await new RemoteWorker({
15273
+ origin: endpoints.workerOrigin,
15274
+ deviceToken: device.token,
15275
+ worker: visibleWorker(worker, options),
15276
+ signal,
15277
+ onStatus(status) {
15278
+ if (status.state === "connected") onConnected();
15279
+ if (status.state !== "retrying" || connectionState !== "retrying") {
15280
+ report(options, { type: "status", status }, statusMessage(status, connectionState));
15281
+ } else {
15282
+ options.onEvent?.({ type: "status", status });
15283
+ }
15284
+ if (status.state === "connected" && status.legacyRelay) {
15285
+ report(
15286
+ options,
15287
+ { type: "notice", message: "The relay needs an update before this computer can expose several workspaces at once." },
15288
+ "The relay needs an update before this computer can expose several workspaces at once."
15289
+ );
15290
+ }
15291
+ if (status.state === "connected" && !status.reconnected && shouldShowConnectHint(endpoints.relayOrigin)) {
15292
+ void announceConnectHint(connectHintStore(), (message) => {
15293
+ report(options, { type: "notice", message }, message);
15294
+ }).catch(() => void 0);
15295
+ }
15296
+ connectionState = status.state;
15297
+ }
15298
+ }).run();
15299
+ }
15300
+ function shouldRecoverRejectedDevice(error46, recoveredRejectedDevice, connected) {
15301
+ return error46 instanceof DeviceRejectedError && !recoveredRejectedDevice && !connected;
15302
+ }
15303
+ async function runManagedSession(root, endpoints, options = {}) {
15352
15304
  const controller = new AbortController();
15353
15305
  const stop = () => controller.abort();
15354
15306
  const handleProcessSignals = options.handleProcessSignals ?? true;
@@ -15360,13 +15312,13 @@ async function runManagedSession(root, endpoints, allowBroadRoot = false, option
15360
15312
  process.once("SIGTERM", stop);
15361
15313
  }
15362
15314
  try {
15363
- const device = await deviceForSession(
15315
+ let device = await deviceForSession(
15364
15316
  endpoints,
15365
- options.deviceName ? { deviceName: options.deviceName } : {},
15317
+ options.credentials ? { credentials: options.credentials } : {},
15366
15318
  controller.signal
15367
15319
  );
15368
15320
  controller.signal.throwIfAborted();
15369
- worker = await LocalWorker.create(root, allowBroadRoot);
15321
+ worker = await LocalWorker.create(root);
15370
15322
  controller.signal.throwIfAborted();
15371
15323
  report(
15372
15324
  options,
@@ -15379,33 +15331,37 @@ async function runManagedSession(root, endpoints, allowBroadRoot = false, option
15379
15331
  "Files may be modified and commands have the full environment and permissions of this account. Press Ctrl+C to disconnect."
15380
15332
  );
15381
15333
  }
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;
15334
+ let recoveredRejectedDevice = false;
15335
+ while (!controller.signal.aborted) {
15336
+ let connected = false;
15337
+ try {
15338
+ await connectRemoteWorker(
15339
+ endpoints,
15340
+ device,
15341
+ worker,
15342
+ options,
15343
+ controller.signal,
15344
+ () => {
15345
+ connected = true;
15346
+ }
15347
+ );
15348
+ break;
15349
+ } catch (error46) {
15350
+ if (!shouldRecoverRejectedDevice(
15351
+ error46,
15352
+ recoveredRejectedDevice,
15353
+ connected
15354
+ )) {
15355
+ throw error46;
15356
+ }
15357
+ recoveredRejectedDevice = true;
15358
+ device = await reenrollRejectedDevice(
15359
+ endpoints,
15360
+ options.credentials ? { credentials: options.credentials } : {},
15361
+ controller.signal
15362
+ );
15407
15363
  }
15408
- }).run();
15364
+ }
15409
15365
  } catch (error46) {
15410
15366
  if (error46 instanceof DeviceRejectedError) {
15411
15367
  await deleteDeviceCredential();
@@ -15439,67 +15395,50 @@ async function gitWorktreeRoot(cwd) {
15439
15395
  return null;
15440
15396
  }
15441
15397
  }
15442
- async function selectExposureRoot(explicitPath, allowBroadRoot = false, cwd = process.cwd()) {
15443
- const selected = explicitPath ?? await gitWorktreeRoot(cwd) ?? cwd;
15444
- return await canonicalizeRoot(selected, allowBroadRoot);
15398
+ async function rootRequiredMessage(cwd) {
15399
+ try {
15400
+ await canonicalizeRoot(cwd);
15401
+ return `No Git worktree was found in ${cwd}. Run "glossa ." to expose the current folder, or "glossa <path>" to expose another directory.`;
15402
+ } catch (error46) {
15403
+ if (error46 instanceof WorkerError && error46.code === "broad_root_refused") {
15404
+ return `No Git worktree was found in ${cwd}, and this protected root is too broad to expose. Run "glossa <path>" with a project directory.`;
15405
+ }
15406
+ throw error46;
15407
+ }
15408
+ }
15409
+ async function selectExposureRoot(explicitPath, cwd = process.cwd()) {
15410
+ const selected = explicitPath ?? await gitWorktreeRoot(cwd);
15411
+ if (!selected) {
15412
+ throw new WorkerError("root_required", await rootRequiredMessage(cwd));
15413
+ }
15414
+ return await canonicalizeRoot(selected);
15445
15415
  }
15446
15416
 
15447
15417
  // src/main.ts
15448
- var VERSION = "0.1.0-beta.10";
15449
- var helpText = {
15450
- main: `Glossa ${VERSION}
15418
+ var VERSION = "0.1.0-beta.11";
15419
+ var HELP = `Glossa ${VERSION}
15451
15420
 
15452
15421
  Usage:
15453
- glossa
15454
15422
  glossa [directory]
15455
- glossa ui [directory] [--allow-broad-root] [--device-name <name>]
15456
- glossa start [directory] [--allow-broad-root] [--device-name <name>]
15457
15423
  glossa status [--json]
15458
- glossa doctor [--json]
15459
- glossa devices list [--json]
15460
- glossa devices rename <id> <name>
15424
+ glossa devices [--json]
15461
15425
  glossa devices revoke <id>
15462
- glossa completions <shell>
15463
15426
  glossa update
15464
15427
  glossa login
15465
- glossa logout [--browser]
15428
+ glossa logout
15466
15429
  glossa --version
15467
- glossa --help
15468
-
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
15430
 
15480
- Validates Google login, contacts the relay, and reports enrolled devices and active workers.`,
15481
- doctor: `Usage: glossa doctor [--json]
15431
+ Running glossa opens one workspace in an interactive terminal.
15432
+ Direct commands remain available for scripts and quick checks.
15482
15433
 
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]
15500
-
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
- };
15434
+ Keys:
15435
+ d recent activity
15436
+ s account and devices
15437
+ r revoke a device
15438
+ l sign out
15439
+ u update Glossa
15440
+ ? show all keys
15441
+ q or Ctrl+C disconnect and quit`;
15503
15442
  async function withLoginSignal(action) {
15504
15443
  const controller = new AbortController();
15505
15444
  const cancel = () => controller.abort();
@@ -15510,124 +15449,158 @@ async function withLoginSignal(action) {
15510
15449
  process.removeListener("SIGINT", cancel);
15511
15450
  }
15512
15451
  }
15452
+ async function authenticatedSession(signal) {
15453
+ if (signal) return await signedInSession({ ...loadAuthConfig(), signal });
15454
+ return await withLoginSignal(
15455
+ async (loginSignal) => await signedInSession({ ...loadAuthConfig(), signal: loginSignal })
15456
+ );
15457
+ }
15513
15458
  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.");
15459
+ return (await authenticatedSession(signal)).credentials;
15460
+ }
15461
+ async function loadStatusDetails(signal) {
15462
+ const credentials = await authenticatedCredentials(signal);
15463
+ const endpoints = loadRelayEndpoints();
15464
+ return await new WorkspaceStatusService(
15465
+ credentials,
15466
+ endpoints
15467
+ ).refresh(signal, true);
15468
+ }
15469
+ function hudStatus(status) {
15519
15470
  return {
15520
- credentials: await validCredentials(
15521
- loaded.credentials,
15522
- signal ? { signal } : {}
15523
- ),
15524
- loginPerformed
15471
+ ...status,
15472
+ devices: status.devices.map((device) => ({
15473
+ id: device.id,
15474
+ name: device.name,
15475
+ platform: device.platform ?? "Unknown platform",
15476
+ lastSeen: formatRelativeTime(device.lastSeenAt),
15477
+ status: deviceStatus(device)
15478
+ }))
15525
15479
  };
15526
15480
  }
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
- });
15481
+ async function revokeKnownDevice(deviceId) {
15482
+ const credentials = await authenticatedCredentials();
15483
+ await revokeDevice(loadRelayEndpoints(), credentials, deviceId);
15533
15484
  }
15534
15485
  async function showStatus(json2) {
15535
- const { credentials: initial } = await authenticatedCredentials();
15536
- const { credentials, profile } = await loadUserProfile(initial);
15537
- 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
- };
15486
+ const status = await loadStatusDetails();
15549
15487
  if (json2) {
15550
- console.log(JSON.stringify(result, null, 2));
15488
+ console.log(JSON.stringify({ ...status, connected: true }, null, 2));
15551
15489
  return;
15552
15490
  }
15553
- console.log(`Signed in as ${account}.`);
15554
- console.log(`Relay connected: ${endpoints.relayOrigin}`);
15491
+ console.log(`Signed in as ${status.account}.`);
15492
+ console.log(`Relay connected: ${status.relay}`);
15555
15493
  console.log(
15556
- activeWorkers === null ? "Active workers: unavailable until the relay is updated" : `Active workers: ${activeWorkers}`
15494
+ status.activeWorkers === null ? "Active workspaces: unavailable" : `Active workspaces: ${status.activeWorkers}`
15557
15495
  );
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));
15496
+ if (status.devices.length === 0) {
15497
+ console.log("No devices enrolled.");
15498
+ } else {
15499
+ for (const device of status.devices) console.log(formatDeviceRow(device));
15566
15500
  }
15567
15501
  }
15568
- async function deviceCredentials() {
15569
- return {
15570
- credentials: (await authenticatedCredentials()).credentials,
15571
- endpoints: loadRelayEndpoints()
15572
- };
15573
- }
15574
15502
  async function showDevices(json2) {
15575
- const { endpoints, credentials } = await deviceCredentials();
15576
- const devices = await listDevices(endpoints, credentials);
15503
+ const credentials = await authenticatedCredentials();
15504
+ const devices = await listDevices(loadRelayEndpoints(), credentials);
15577
15505
  if (json2) console.log(JSON.stringify({ devices }, null, 2));
15578
15506
  else if (devices.length === 0) console.log("No devices enrolled.");
15579
15507
  else for (const device of devices) console.log(formatDeviceRow(device));
15580
15508
  }
15581
- async function runInteractive(path8, allowBroadRoot, deviceName) {
15582
- const root = await selectExposureRoot(path8, allowBroadRoot);
15583
- await runSessionHud({
15584
- workspace: root,
15585
- run: async (signal, onEvent) => {
15586
- await authenticatedCredentials(signal);
15587
- await runManagedSession(root, loadRelayEndpoints(), allowBroadRoot, {
15588
- signal,
15589
- onEvent,
15590
- quiet: true,
15591
- handleProcessSignals: false,
15592
- ...deviceName ? { deviceName } : {}
15593
- });
15594
- }
15595
- });
15509
+ async function runWorkspace(path7) {
15510
+ const root = await selectExposureRoot(path7);
15511
+ const endpoints = loadRelayEndpoints();
15512
+ let credentials = (await authenticatedSession()).credentials;
15513
+ let statusService;
15514
+ let statusListener;
15515
+ let postExitNotice;
15516
+ let unsubscribeStatusService = () => void 0;
15517
+ const createStatusService = (sessionCredentials) => {
15518
+ unsubscribeStatusService();
15519
+ const service = new WorkspaceStatusService(sessionCredentials, endpoints);
15520
+ unsubscribeStatusService = service.subscribe((status) => {
15521
+ statusListener?.(hudStatus(status));
15522
+ });
15523
+ statusService = service;
15524
+ return service;
15525
+ };
15526
+ const refreshStatus = async (signal) => {
15527
+ const service = statusService ?? createStatusService(
15528
+ credentials
15529
+ );
15530
+ return hudStatus(await service.refresh(signal));
15531
+ };
15532
+ createStatusService(credentials);
15533
+ let exitAction;
15534
+ try {
15535
+ exitAction = await runSessionHud({
15536
+ workspace: root,
15537
+ peekStatus: () => {
15538
+ const cached2 = statusService?.peek();
15539
+ return cached2 ? hudStatus(cached2) : void 0;
15540
+ },
15541
+ subscribeStatus: (listener) => {
15542
+ statusListener = listener;
15543
+ return () => {
15544
+ if (statusListener === listener) statusListener = void 0;
15545
+ };
15546
+ },
15547
+ run: async (signal, onEvent) => {
15548
+ await runManagedSession(root, endpoints, {
15549
+ credentials,
15550
+ signal,
15551
+ onEvent(event) {
15552
+ onEvent(event);
15553
+ if (event.type === "notice") postExitNotice = event.message;
15554
+ if (event.type === "status" && event.status.state === "connected" && !statusService?.peek()) {
15555
+ void statusService?.refresh(signal).catch(() => void 0);
15556
+ }
15557
+ },
15558
+ quiet: true,
15559
+ handleProcessSignals: false
15560
+ });
15561
+ },
15562
+ loadStatus: refreshStatus,
15563
+ revokeDevice: async (deviceId, signal) => {
15564
+ credentials = await validCredentials(credentials, { signal });
15565
+ await revokeDevice(
15566
+ endpoints,
15567
+ credentials,
15568
+ deviceId,
15569
+ async (input, init) => await fetch(input, { ...init, signal })
15570
+ );
15571
+ }
15572
+ });
15573
+ } finally {
15574
+ unsubscribeStatusService();
15575
+ }
15576
+ if (postExitNotice) console.log(postExitNotice);
15577
+ if (exitAction === "logout") await logoutFromGlossa();
15578
+ else if (exitAction === "update") {
15579
+ await updateGlossa({ currentVersion: VERSION });
15580
+ }
15596
15581
  }
15597
15582
  async function main() {
15598
15583
  const invocation = parseInvocation(process.argv.slice(2));
15599
15584
  if (invocation.command === "help") {
15600
- console.log(helpText[invocation.topic ?? "main"]);
15585
+ console.log(HELP);
15601
15586
  } else if (invocation.command === "version") {
15602
15587
  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);
15588
+ } else if (invocation.command === "workspace") {
15589
+ await runWorkspace(invocation.path);
15607
15590
  } else if (invocation.command === "status") {
15608
15591
  await showStatus(invocation.json);
15609
- } else if (invocation.command === "doctor") {
15610
- const ok = await runDoctor(invocation.json);
15611
- if (!ok) process.exitCode = 1;
15612
15592
  } else if (invocation.command === "login") {
15613
- const { loginPerformed } = await authenticatedCredentials();
15614
- if (!loginPerformed) console.log("Signed in to Glossa.");
15593
+ const session = await authenticatedSession();
15594
+ if (!session.loginPerformed) console.log("Signed in to Glossa.");
15615
15595
  } else if (invocation.command === "logout") {
15616
- await logoutFromGlossa({ browser: invocation.browser });
15617
- } else if (invocation.command === "completions") {
15618
- console.log(completionScript(invocation.shell));
15596
+ await logoutFromGlossa();
15619
15597
  } else if (invocation.command === "update") {
15620
15598
  await updateGlossa({ currentVersion: VERSION });
15621
15599
  } else if (invocation.action === "list") {
15622
15600
  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}.`);
15627
15601
  } 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.`);
15602
+ await revokeKnownDevice(invocation.deviceId);
15603
+ console.log(`Revoked device ${invocation.deviceId}. Running workspaces on it are disconnected.`);
15631
15604
  }
15632
15605
  }
15633
15606
  main().catch((error46) => {