@ariobarin/glossa 0.2.0-beta.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +4 -1
  2. package/dist/app.js +795 -556
  3. package/package.json +1 -1
package/dist/app.js CHANGED
@@ -10436,10 +10436,10 @@ var require_react_reconciler_development = __commonJS({
10436
10436
  fiber = fiber.next, id--;
10437
10437
  return fiber;
10438
10438
  }
10439
- function copyWithSetImpl(obj, path13, index, value) {
10440
- if (index >= path13.length) return value;
10441
- var key = path13[index], updated = isArrayImpl(obj) ? obj.slice() : assign({}, obj);
10442
- updated[key] = copyWithSetImpl(obj[key], path13, index + 1, value);
10439
+ function copyWithSetImpl(obj, path14, index, value) {
10440
+ if (index >= path14.length) return value;
10441
+ var key = path14[index], updated = isArrayImpl(obj) ? obj.slice() : assign({}, obj);
10442
+ updated[key] = copyWithSetImpl(obj[key], path14, index + 1, value);
10443
10443
  return updated;
10444
10444
  }
10445
10445
  function copyWithRename(obj, oldPath, newPath) {
@@ -10466,11 +10466,11 @@ var require_react_reconciler_development = __commonJS({
10466
10466
  );
10467
10467
  return updated;
10468
10468
  }
10469
- function copyWithDeleteImpl(obj, path13, index) {
10470
- var key = path13[index], updated = isArrayImpl(obj) ? obj.slice() : assign({}, obj);
10471
- if (index + 1 === path13.length)
10469
+ function copyWithDeleteImpl(obj, path14, index) {
10470
+ var key = path14[index], updated = isArrayImpl(obj) ? obj.slice() : assign({}, obj);
10471
+ if (index + 1 === path14.length)
10472
10472
  return isArrayImpl(updated) ? updated.splice(key, 1) : delete updated[key], updated;
10473
- updated[key] = copyWithDeleteImpl(obj[key], path13, index + 1);
10473
+ updated[key] = copyWithDeleteImpl(obj[key], path14, index + 1);
10474
10474
  return updated;
10475
10475
  }
10476
10476
  function shouldSuspendImpl() {
@@ -23747,29 +23747,29 @@ var require_react_reconciler_development = __commonJS({
23747
23747
  var didWarnAboutNestedUpdates = false;
23748
23748
  var didWarnAboutFindNodeInStrictMode = {};
23749
23749
  var overrideHookState = null, overrideHookStateDeletePath = null, overrideHookStateRenamePath = null, overrideProps = null, overridePropsDeletePath = null, overridePropsRenamePath = null, scheduleUpdate = null, scheduleRetry = null, setErrorHandler = null, setSuspenseHandler = null;
23750
- overrideHookState = function(fiber, id, path13, value) {
23750
+ overrideHookState = function(fiber, id, path14, value) {
23751
23751
  id = findHook(fiber, id);
23752
- null !== id && (path13 = copyWithSetImpl(id.memoizedState, path13, 0, value), id.memoizedState = path13, id.baseState = path13, fiber.memoizedProps = assign({}, fiber.memoizedProps), path13 = enqueueConcurrentRenderForLane(fiber, 2), null !== path13 && scheduleUpdateOnFiber(path13, fiber, 2));
23752
+ null !== id && (path14 = copyWithSetImpl(id.memoizedState, path14, 0, value), id.memoizedState = path14, id.baseState = path14, fiber.memoizedProps = assign({}, fiber.memoizedProps), path14 = enqueueConcurrentRenderForLane(fiber, 2), null !== path14 && scheduleUpdateOnFiber(path14, fiber, 2));
23753
23753
  };
23754
- overrideHookStateDeletePath = function(fiber, id, path13) {
23754
+ overrideHookStateDeletePath = function(fiber, id, path14) {
23755
23755
  id = findHook(fiber, id);
23756
- null !== id && (path13 = copyWithDeleteImpl(id.memoizedState, path13, 0), id.memoizedState = path13, id.baseState = path13, fiber.memoizedProps = assign({}, fiber.memoizedProps), path13 = enqueueConcurrentRenderForLane(fiber, 2), null !== path13 && scheduleUpdateOnFiber(path13, fiber, 2));
23756
+ null !== id && (path14 = copyWithDeleteImpl(id.memoizedState, path14, 0), id.memoizedState = path14, id.baseState = path14, fiber.memoizedProps = assign({}, fiber.memoizedProps), path14 = enqueueConcurrentRenderForLane(fiber, 2), null !== path14 && scheduleUpdateOnFiber(path14, fiber, 2));
23757
23757
  };
23758
23758
  overrideHookStateRenamePath = function(fiber, id, oldPath, newPath) {
23759
23759
  id = findHook(fiber, id);
23760
23760
  null !== id && (oldPath = copyWithRename(id.memoizedState, oldPath, newPath), id.memoizedState = oldPath, id.baseState = oldPath, fiber.memoizedProps = assign({}, fiber.memoizedProps), oldPath = enqueueConcurrentRenderForLane(fiber, 2), null !== oldPath && scheduleUpdateOnFiber(oldPath, fiber, 2));
23761
23761
  };
23762
- overrideProps = function(fiber, path13, value) {
23763
- fiber.pendingProps = copyWithSetImpl(fiber.memoizedProps, path13, 0, value);
23762
+ overrideProps = function(fiber, path14, value) {
23763
+ fiber.pendingProps = copyWithSetImpl(fiber.memoizedProps, path14, 0, value);
23764
23764
  fiber.alternate && (fiber.alternate.pendingProps = fiber.pendingProps);
23765
- path13 = enqueueConcurrentRenderForLane(fiber, 2);
23766
- null !== path13 && scheduleUpdateOnFiber(path13, fiber, 2);
23765
+ path14 = enqueueConcurrentRenderForLane(fiber, 2);
23766
+ null !== path14 && scheduleUpdateOnFiber(path14, fiber, 2);
23767
23767
  };
23768
- overridePropsDeletePath = function(fiber, path13) {
23769
- fiber.pendingProps = copyWithDeleteImpl(fiber.memoizedProps, path13, 0);
23768
+ overridePropsDeletePath = function(fiber, path14) {
23769
+ fiber.pendingProps = copyWithDeleteImpl(fiber.memoizedProps, path14, 0);
23770
23770
  fiber.alternate && (fiber.alternate.pendingProps = fiber.pendingProps);
23771
- path13 = enqueueConcurrentRenderForLane(fiber, 2);
23772
- null !== path13 && scheduleUpdateOnFiber(path13, fiber, 2);
23771
+ path14 = enqueueConcurrentRenderForLane(fiber, 2);
23772
+ null !== path14 && scheduleUpdateOnFiber(path14, fiber, 2);
23773
23773
  };
23774
23774
  overridePropsRenamePath = function(fiber, oldPath, newPath) {
23775
23775
  fiber.pendingProps = copyWithRename(
@@ -27007,21 +27007,6 @@ var SessionExpiredError = class extends Error {
27007
27007
  this.name = "SessionExpiredError";
27008
27008
  }
27009
27009
  };
27010
- function accessTokenSubject(credentials) {
27011
- try {
27012
- const parts = credentials.accessToken.split(".");
27013
- if (parts.length !== 3) throw new Error();
27014
- const payload = JSON.parse(
27015
- Buffer.from(parts[1], "base64url").toString("utf8")
27016
- );
27017
- if (typeof payload.sub !== "string" || payload.sub.length === 0) {
27018
- throw new Error();
27019
- }
27020
- return payload.sub;
27021
- } catch {
27022
- throw new Error("Glossa could not identify the signed-in account.");
27023
- }
27024
- }
27025
27010
  function sessionExpiredError() {
27026
27011
  return new SessionExpiredError();
27027
27012
  }
@@ -28014,10 +27999,10 @@ function mergeDefs(...defs) {
28014
27999
  function cloneDef(schema) {
28015
28000
  return mergeDefs(schema._zod.def);
28016
28001
  }
28017
- function getElementAtPath(obj, path13) {
28018
- if (!path13)
28002
+ function getElementAtPath(obj, path14) {
28003
+ if (!path14)
28019
28004
  return obj;
28020
- return path13.reduce((acc, key) => acc?.[key], obj);
28005
+ return path14.reduce((acc, key) => acc?.[key], obj);
28021
28006
  }
28022
28007
  function promiseAllObject(promisesObj) {
28023
28008
  const keys = Object.keys(promisesObj);
@@ -28378,11 +28363,11 @@ function aborted(x, startIndex = 0) {
28378
28363
  }
28379
28364
  return false;
28380
28365
  }
28381
- function prefixIssues(path13, issues) {
28366
+ function prefixIssues(path14, issues) {
28382
28367
  return issues.map((iss) => {
28383
28368
  var _a;
28384
28369
  (_a = iss).path ?? (_a.path = []);
28385
- iss.path.unshift(path13);
28370
+ iss.path.unshift(path14);
28386
28371
  return iss;
28387
28372
  });
28388
28373
  }
@@ -28544,7 +28529,7 @@ function formatError(error46, mapper = (issue2) => issue2.message) {
28544
28529
  }
28545
28530
  function treeifyError(error46, mapper = (issue2) => issue2.message) {
28546
28531
  const result = { errors: [] };
28547
- const processError = (error47, path13 = []) => {
28532
+ const processError = (error47, path14 = []) => {
28548
28533
  var _a, _b;
28549
28534
  for (const issue2 of error47.issues) {
28550
28535
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -28554,7 +28539,7 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
28554
28539
  } else if (issue2.code === "invalid_element") {
28555
28540
  processError({ issues: issue2.issues }, issue2.path);
28556
28541
  } else {
28557
- const fullpath = [...path13, ...issue2.path];
28542
+ const fullpath = [...path14, ...issue2.path];
28558
28543
  if (fullpath.length === 0) {
28559
28544
  result.errors.push(mapper(issue2));
28560
28545
  continue;
@@ -28586,8 +28571,8 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
28586
28571
  }
28587
28572
  function toDotPath(_path) {
28588
28573
  const segs = [];
28589
- const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
28590
- for (const seg of path13) {
28574
+ const path14 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
28575
+ for (const seg of path14) {
28591
28576
  if (typeof seg === "number")
28592
28577
  segs.push(`[${seg}]`);
28593
28578
  else if (typeof seg === "symbol")
@@ -40271,6 +40256,10 @@ function parseInvocation(args) {
40271
40256
  noOptions("Logout", options);
40272
40257
  return { command };
40273
40258
  }
40259
+ if (command === "unpair") {
40260
+ noOptions("Unpair", options);
40261
+ return { command };
40262
+ }
40274
40263
  if (command === "update") return parseUpdate(options);
40275
40264
  return parseWorkspace(args);
40276
40265
  }
@@ -40322,7 +40311,7 @@ async function logoutFromGlossa(dependencies = {}) {
40322
40311
  const issuer = dependencies.issuer ?? stored?.credentials.issuer;
40323
40312
  await remove();
40324
40313
  log(
40325
- present ? "Signed out of Glossa." : "Already signed out of Glossa."
40314
+ present ? "Signed out of the Glossa account. Computer pairing stays active." : "Already signed out of the Glossa account. Computer pairing stays active."
40326
40315
  );
40327
40316
  const url2 = browserLogoutUrl(issuer ?? loadAuthConfig().issuer);
40328
40317
  const opened = await browse(url2);
@@ -40381,6 +40370,12 @@ function loadRelayEndpoints(environment = process.env) {
40381
40370
  function defaultDeviceName() {
40382
40371
  return deviceNameSchema.parse(os2.hostname());
40383
40372
  }
40373
+ var DevicePairingExpiredError = class extends Error {
40374
+ constructor() {
40375
+ super("The Glossa pairing code expired. Run Glossa again to get a new code.");
40376
+ this.name = "DevicePairingExpiredError";
40377
+ }
40378
+ };
40384
40379
  function relayError(status, data) {
40385
40380
  if (status === 401) return new Error("Glossa login was rejected. Sign in again when prompted.");
40386
40381
  if (status === 403 && data.error === "account_disabled") {
@@ -40434,55 +40429,79 @@ async function listDevices(endpoints, credentials, fetchRequest = fetch) {
40434
40429
  if (!response.ok) throw relayError(response.status, data);
40435
40430
  return parseDevices2(data.devices);
40436
40431
  }
40437
- async function accountOwnsDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
40438
- const devices = await listDevices(endpoints, credentials, fetchRequest);
40439
- return devices.some(
40440
- (device) => device.id === deviceId && device.revokedAt === null
40441
- );
40432
+ async function beginDevicePairing(endpoints, deviceName, fetchRequest = fetch) {
40433
+ const name = deviceNameSchema.parse(deviceName);
40434
+ const response = await fetchRequest(`${endpoints.relayOrigin}/v1/device-pairings`, {
40435
+ method: "POST",
40436
+ headers: { "content-type": "application/json" },
40437
+ body: JSON.stringify({ name, platform: `${process.platform}-${process.arch}` })
40438
+ });
40439
+ let data = {};
40440
+ try {
40441
+ data = await response.json();
40442
+ } catch {
40443
+ }
40444
+ if (!response.ok) {
40445
+ if (response.status === 429) {
40446
+ throw new Error("Glossa pairing is rate limited. Try again later.");
40447
+ }
40448
+ throw new Error(`The Glossa relay returned HTTP ${response.status}.`);
40449
+ }
40450
+ if (typeof data.pairing_id !== "string" || typeof data.user_code !== "string" || typeof data.pairing_secret !== "string" || typeof data.expires_at !== "string" || !Number.isInteger(data.poll_interval_ms) || data.poll_interval_ms < 1e3 || data.poll_interval_ms > 1e4 || !/^[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(data.pairing_id) || !/^[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$/.test(data.user_code) || data.pairing_secret.length < 32 || data.pairing_secret.length > 128 || !Number.isFinite(Date.parse(data.expires_at))) {
40451
+ throw new Error("The Glossa relay returned an invalid pairing response.");
40452
+ }
40453
+ return {
40454
+ pairingId: data.pairing_id,
40455
+ userCode: data.user_code,
40456
+ pairingSecret: data.pairing_secret,
40457
+ expiresAt: data.expires_at,
40458
+ pollIntervalMs: data.poll_interval_ms
40459
+ };
40442
40460
  }
40443
- async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = fetch) {
40444
- const baseName = deviceNameSchema.parse(deviceName);
40445
- let name = baseName;
40446
- for (let attempt = 0; attempt < 3; attempt += 1) {
40447
- const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/enroll`, {
40461
+ async function completeDevicePairing(endpoints, pairing, fetchRequest = fetch) {
40462
+ const response = await fetchRequest(
40463
+ `${endpoints.relayOrigin}/v1/device-pairings/${encodeURIComponent(pairing.pairingId)}/complete`,
40464
+ {
40448
40465
  method: "POST",
40449
- headers: {
40450
- authorization: `${credentials.tokenType} ${credentials.accessToken}`,
40451
- "content-type": "application/json"
40452
- },
40453
- body: JSON.stringify({ name, platform: `${process.platform}-${process.arch}` })
40454
- });
40455
- let data = {};
40456
- try {
40457
- data = await response.json();
40458
- } catch {
40459
- }
40460
- if (response.status === 409 && data.error === "device_name_conflict" && attempt < 2) {
40461
- const activeNames = new Set(
40462
- (await listDevices(endpoints, credentials, fetchRequest)).filter((device) => device.revokedAt === null).map((device) => device.name)
40463
- );
40464
- for (let suffix = 2; ; suffix += 1) {
40465
- const ending = `-${suffix}`;
40466
- const candidate = `${baseName.slice(0, 80 - ending.length)}${ending}`;
40467
- if (!activeNames.has(candidate)) {
40468
- name = deviceNameSchema.parse(candidate);
40469
- break;
40470
- }
40471
- }
40472
- continue;
40473
- }
40474
- if (!response.ok) throw relayError(response.status, data);
40475
- if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
40476
- throw new Error("The Glossa relay returned an invalid device enrollment response.");
40466
+ headers: { "content-type": "application/json" },
40467
+ body: JSON.stringify({ pairing_secret: pairing.pairingSecret })
40477
40468
  }
40478
- return {
40479
- relayOrigin: endpoints.relayOrigin,
40480
- deviceId: data.device.id,
40481
- deviceName: data.device.name,
40482
- token: data.device_token
40483
- };
40469
+ );
40470
+ let data = {};
40471
+ try {
40472
+ data = await response.json();
40473
+ } catch {
40484
40474
  }
40485
- throw new Error("Glossa could not enroll this computer.");
40475
+ if (response.status === 202 && data.status === "authorization_pending") {
40476
+ return null;
40477
+ }
40478
+ if (response.status === 410 && data.error === "pairing_expired" || response.status === 404 && data.error === "pairing_not_found") {
40479
+ throw new DevicePairingExpiredError();
40480
+ }
40481
+ if (response.status === 429) {
40482
+ throw new Error("Glossa pairing is rate limited. Try again later.");
40483
+ }
40484
+ if (!response.ok) throw relayError(response.status, data);
40485
+ if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
40486
+ throw new Error("The Glossa relay returned an invalid device pairing response.");
40487
+ }
40488
+ return {
40489
+ relayOrigin: endpoints.relayOrigin,
40490
+ deviceId: data.device.id,
40491
+ deviceName: data.device.name,
40492
+ token: data.device_token
40493
+ };
40494
+ }
40495
+ async function revokePairedDevice(endpoints, device, fetchRequest = fetch) {
40496
+ const response = await fetchRequest(`${endpoints.relayOrigin}/device`, {
40497
+ method: "DELETE",
40498
+ headers: { authorization: `Device ${device.token}` }
40499
+ });
40500
+ if (response.ok || response.status === 401 || response.status === 404) return;
40501
+ if (response.status === 429) {
40502
+ throw new Error("Glossa device authentication is rate limited. Try again later.");
40503
+ }
40504
+ throw new Error(`The Glossa relay returned HTTP ${response.status}.`);
40486
40505
  }
40487
40506
  async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
40488
40507
  const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
@@ -47670,8 +47689,8 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
47670
47689
  }
47671
47690
 
47672
47691
  // ../../node_modules/ink/build/components/ErrorOverview.js
47673
- var cleanupPath = (path13) => {
47674
- return path13?.replace(`file://${cwd()}/`, "");
47692
+ var cleanupPath = (path14) => {
47693
+ return path14?.replace(`file://${cwd()}/`, "");
47675
47694
  };
47676
47695
  var stackUtils = new import_stack_utils.default({
47677
47696
  cwd: cwd(),
@@ -47845,7 +47864,7 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
47845
47864
  for (const subscriber of animationSubscribersRef.current.values()) {
47846
47865
  nextDueTime = Math.min(nextDueTime, subscriber.nextDueTime);
47847
47866
  }
47848
- const delay4 = Math.max(0, nextDueTime - performance.now());
47867
+ const delay6 = Math.max(0, nextDueTime - performance.now());
47849
47868
  animationTimerRef.current = setTimeout(() => {
47850
47869
  animationTimerRef.current = void 0;
47851
47870
  const currentTime = performance.now();
@@ -47859,7 +47878,7 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
47859
47878
  subscriber.nextDueTime = subscriber.startTime + elapsedFrames * subscriber.interval;
47860
47879
  }
47861
47880
  scheduleAnimationTick();
47862
- }, delay4);
47881
+ }, delay6);
47863
47882
  }, [clearAnimationTimer]);
47864
47883
  const animationSubscribe = (0, import_react15.useCallback)((callback, interval) => {
47865
47884
  const startTime = performance.now();
@@ -49799,7 +49818,7 @@ function connectHintStore(directory = configDirectory()) {
49799
49818
  }
49800
49819
  async function announceConnectHint(store3, log) {
49801
49820
  if (await store3.exists()) return false;
49802
- log(`Next: add Glossa in ChatGPT. Follow the quickstart at ${CONNECT_HINT_URL}.`);
49821
+ log(`Next: return to ChatGPT and use Glossa. If it is not connected yet, follow ${CONNECT_HINT_URL}.`);
49803
49822
  await store3.mark();
49804
49823
  return true;
49805
49824
  }
@@ -49845,11 +49864,298 @@ async function deleteDeviceCredential() {
49845
49864
  await store2.delete();
49846
49865
  }
49847
49866
 
49867
+ // src/device-pairing-lock.ts
49868
+ import { randomUUID as randomUUID2 } from "node:crypto";
49869
+ import { mkdir as mkdir4, readFile as readFile3, rm as rm3, stat, writeFile as writeFile4 } from "node:fs/promises";
49870
+ import path6 from "node:path";
49871
+ import { setTimeout as delay2 } from "node:timers/promises";
49872
+
49873
+ // src/update-lock.ts
49874
+ import { randomUUID } from "node:crypto";
49875
+ import { mkdir as mkdir3, readdir, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
49876
+ import path5 from "node:path";
49877
+ var UPDATE_SUFFIX = ".update";
49878
+ var SESSION_SUFFIX = ".session";
49879
+ function updateRuntimeDirectory() {
49880
+ return path5.join(configDirectory(), "runtime");
49881
+ }
49882
+ function processIsAlive(pid) {
49883
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
49884
+ try {
49885
+ process.kill(pid, 0);
49886
+ return true;
49887
+ } catch (error46) {
49888
+ return error46.code === "EPERM";
49889
+ }
49890
+ }
49891
+ function leasePid(name, suffix) {
49892
+ if (!name.endsWith(suffix)) return null;
49893
+ const separator = name.indexOf("-");
49894
+ if (separator <= 0) return null;
49895
+ const pid = Number(name.slice(0, separator));
49896
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
49897
+ }
49898
+ async function activeLeaseFiles(directory, suffix) {
49899
+ let entries;
49900
+ try {
49901
+ entries = await readdir(directory, { withFileTypes: true });
49902
+ } catch (error46) {
49903
+ if (error46.code === "ENOENT") return [];
49904
+ throw error46;
49905
+ }
49906
+ const active = [];
49907
+ for (const entry of entries) {
49908
+ if (!entry.isFile() || !entry.name.endsWith(suffix)) continue;
49909
+ const pid = leasePid(entry.name, suffix);
49910
+ const file2 = path5.join(directory, entry.name);
49911
+ if (pid !== null && processIsAlive(pid)) active.push(file2);
49912
+ else await rm2(file2, { force: true });
49913
+ }
49914
+ return active;
49915
+ }
49916
+ async function updateLockIsActive(directory) {
49917
+ return (await activeLeaseFiles(directory, UPDATE_SUFFIX)).length > 0;
49918
+ }
49919
+ async function activeSessionFiles(directory) {
49920
+ return await activeLeaseFiles(directory, SESSION_SUFFIX);
49921
+ }
49922
+ async function createUpdateLock(directory) {
49923
+ await mkdir3(directory, { recursive: true, mode: 448 });
49924
+ const lockFile = path5.join(
49925
+ directory,
49926
+ `${process.pid}-${randomUUID()}${UPDATE_SUFFIX}`
49927
+ );
49928
+ const content = `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
49929
+ `;
49930
+ await writeFile3(lockFile, content, {
49931
+ encoding: "utf8",
49932
+ flag: "wx",
49933
+ mode: 384
49934
+ });
49935
+ try {
49936
+ const contenders = await activeLeaseFiles(directory, UPDATE_SUFFIX);
49937
+ if (contenders.some((file2) => file2 !== lockFile)) {
49938
+ throw new Error("Another Glossa update is already running.");
49939
+ }
49940
+ return lockFile;
49941
+ } catch (error46) {
49942
+ await rm2(lockFile, { force: true });
49943
+ throw error46;
49944
+ }
49945
+ }
49946
+ async function withUpdateLease(action, directory = updateRuntimeDirectory()) {
49947
+ const lockFile = await createUpdateLock(directory);
49948
+ try {
49949
+ if ((await activeSessionFiles(directory)).length > 0) {
49950
+ throw new Error("Disconnect every running Glossa workspace before updating.");
49951
+ }
49952
+ return await action();
49953
+ } finally {
49954
+ await rm2(lockFile, { force: true });
49955
+ }
49956
+ }
49957
+ async function withWorkspaceLease(action, directory = updateRuntimeDirectory()) {
49958
+ await mkdir3(directory, { recursive: true, mode: 448 });
49959
+ if (await updateLockIsActive(directory)) {
49960
+ throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
49961
+ }
49962
+ const leaseFile = path5.join(
49963
+ directory,
49964
+ `${process.pid}-${randomUUID()}${SESSION_SUFFIX}`
49965
+ );
49966
+ await writeFile3(
49967
+ leaseFile,
49968
+ `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
49969
+ `,
49970
+ { encoding: "utf8", flag: "wx", mode: 384 }
49971
+ );
49972
+ try {
49973
+ if (await updateLockIsActive(directory)) {
49974
+ throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
49975
+ }
49976
+ return await action();
49977
+ } finally {
49978
+ await rm2(leaseFile, { force: true });
49979
+ }
49980
+ }
49981
+
49982
+ // src/device-pairing-lock.ts
49983
+ var DEVICE_PAIRING_LOCK = "device-pairing.lock";
49984
+ var DEVICE_PAIRING_LOCK_GUARD = "device-pairing.lock.guard";
49985
+ var DEVICE_PAIRING_LOCK_POLL_MS = 100;
49986
+ var DEVICE_PAIRING_LOCK_MAX_AGE_MS = 6 * 6e4;
49987
+ var DEVICE_PAIRING_GUARD_MAX_AGE_MS = 3e4;
49988
+ function lockPath(directory) {
49989
+ return path6.join(directory, DEVICE_PAIRING_LOCK);
49990
+ }
49991
+ function guardPath(directory) {
49992
+ return path6.join(directory, DEVICE_PAIRING_LOCK_GUARD);
49993
+ }
49994
+ function newOwner() {
49995
+ return {
49996
+ pid: process.pid,
49997
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
49998
+ token: randomUUID2()
49999
+ };
50000
+ }
50001
+ async function readOwner(file2) {
50002
+ try {
50003
+ const value = JSON.parse(await readFile3(file2, "utf8"));
50004
+ if (!Number.isSafeInteger(value.pid) || typeof value.startedAt !== "string" || !Number.isFinite(Date.parse(value.startedAt)) || typeof value.token !== "string" || value.token.length === 0) {
50005
+ return void 0;
50006
+ }
50007
+ return value;
50008
+ } catch (error46) {
50009
+ if (error46.code === "ENOENT") return void 0;
50010
+ return void 0;
50011
+ }
50012
+ }
50013
+ async function lockIsStale(file2, maxAgeMs) {
50014
+ const owner = await readOwner(file2);
50015
+ if (!owner) {
50016
+ try {
50017
+ const metadata = await stat(file2);
50018
+ return Date.now() - metadata.mtimeMs > maxAgeMs;
50019
+ } catch (error46) {
50020
+ if (error46.code === "ENOENT") return false;
50021
+ return true;
50022
+ }
50023
+ }
50024
+ const ageMs = Date.now() - Date.parse(owner.startedAt);
50025
+ return ageMs > maxAgeMs || !processIsAlive(owner.pid);
50026
+ }
50027
+ async function acquireGuard(directory, signal) {
50028
+ const file2 = guardPath(directory);
50029
+ for (; ; ) {
50030
+ signal?.throwIfAborted();
50031
+ const owner = newOwner();
50032
+ try {
50033
+ await writeFile4(file2, `${JSON.stringify(owner)}
50034
+ `, {
50035
+ encoding: "utf8",
50036
+ flag: "wx",
50037
+ mode: 384
50038
+ });
50039
+ return { file: file2, token: owner.token };
50040
+ } catch (error46) {
50041
+ if (error46.code !== "EEXIST") throw error46;
50042
+ }
50043
+ if (await lockIsStale(file2, DEVICE_PAIRING_GUARD_MAX_AGE_MS)) {
50044
+ await rm3(file2, { force: true });
50045
+ continue;
50046
+ }
50047
+ await delay2(
50048
+ DEVICE_PAIRING_LOCK_POLL_MS,
50049
+ void 0,
50050
+ signal ? { signal } : void 0
50051
+ );
50052
+ }
50053
+ }
50054
+ async function removeOwnedLock(file2, token) {
50055
+ const owner = await readOwner(file2);
50056
+ if (owner?.token === token) await rm3(file2, { force: true });
50057
+ }
50058
+ async function withGuard(directory, action, signal) {
50059
+ const guard = await acquireGuard(directory, signal);
50060
+ try {
50061
+ return await action();
50062
+ } finally {
50063
+ await removeOwnedLock(guard.file, guard.token);
50064
+ }
50065
+ }
50066
+ async function acquireDevicePairingLock(directory, signal) {
50067
+ await mkdir4(directory, { recursive: true, mode: 448 });
50068
+ const file2 = lockPath(directory);
50069
+ for (; ; ) {
50070
+ signal?.throwIfAborted();
50071
+ const owner = newOwner();
50072
+ try {
50073
+ await writeFile4(file2, `${JSON.stringify(owner)}
50074
+ `, {
50075
+ encoding: "utf8",
50076
+ flag: "wx",
50077
+ mode: 384
50078
+ });
50079
+ return { file: file2, token: owner.token };
50080
+ } catch (error46) {
50081
+ if (error46.code !== "EEXIST") throw error46;
50082
+ }
50083
+ const reclaimed = await withGuard(
50084
+ directory,
50085
+ async () => {
50086
+ if (!await lockIsStale(file2, DEVICE_PAIRING_LOCK_MAX_AGE_MS)) {
50087
+ return false;
50088
+ }
50089
+ await rm3(file2, { force: true });
50090
+ return true;
50091
+ },
50092
+ signal
50093
+ );
50094
+ if (reclaimed) continue;
50095
+ await delay2(
50096
+ DEVICE_PAIRING_LOCK_POLL_MS,
50097
+ void 0,
50098
+ signal ? { signal } : void 0
50099
+ );
50100
+ }
50101
+ }
50102
+ async function withDevicePairingLease(action, signal, directory = updateRuntimeDirectory()) {
50103
+ const lock = await acquireDevicePairingLock(directory, signal);
50104
+ try {
50105
+ return await action();
50106
+ } finally {
50107
+ await withGuard(directory, async () => await removeOwnedLock(lock.file, lock.token));
50108
+ }
50109
+ }
50110
+
50111
+ // src/device-pairing.ts
50112
+ import { setTimeout as delay3 } from "node:timers/promises";
50113
+ async function defaultWait(milliseconds, signal) {
50114
+ await delay3(milliseconds, void 0, signal ? { signal } : void 0);
50115
+ }
50116
+ function canceledError2() {
50117
+ return new Error("Pairing canceled.");
50118
+ }
50119
+ async function pairDevice(endpoints, signal, dependencies = {}) {
50120
+ const begin = dependencies.beginDevicePairing ?? beginDevicePairing;
50121
+ const complete = dependencies.completeDevicePairing ?? completeDevicePairing;
50122
+ const name = dependencies.defaultDeviceName ?? defaultDeviceName;
50123
+ const wait = dependencies.wait ?? defaultWait;
50124
+ const now = dependencies.now ?? Date.now;
50125
+ const log = dependencies.log ?? console.log;
50126
+ const baseFetch = dependencies.fetch ?? fetch;
50127
+ const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
50128
+ try {
50129
+ signal?.throwIfAborted();
50130
+ const challenge = await begin(endpoints, name(), fetchRequest);
50131
+ log("This computer needs to be paired with Glossa.");
50132
+ log("In ChatGPT, send this message:");
50133
+ log(`@Glossa pair ${challenge.userCode}`);
50134
+ log("Keep this terminal open. The pairing code expires in 5 minutes.");
50135
+ const expiresAt = Date.parse(challenge.expiresAt);
50136
+ while (now() < expiresAt) {
50137
+ signal?.throwIfAborted();
50138
+ await wait(challenge.pollIntervalMs, signal);
50139
+ signal?.throwIfAborted();
50140
+ const paired = await complete(endpoints, challenge, fetchRequest);
50141
+ if (!paired) continue;
50142
+ log(`Paired with Glossa as ${paired.deviceName}.`);
50143
+ return paired;
50144
+ }
50145
+ throw new Error("The Glossa pairing code expired. Run Glossa again to get a new code.");
50146
+ } catch (error46) {
50147
+ if (signal?.aborted || error46 instanceof Error && error46.name === "AbortError") {
50148
+ throw canceledError2();
50149
+ }
50150
+ throw error46;
50151
+ }
50152
+ }
50153
+
49848
50154
  // src/worker/command-service.ts
49849
50155
  import { spawn as spawn2 } from "node:child_process";
49850
- import { randomUUID } from "node:crypto";
50156
+ import { randomUUID as randomUUID3 } from "node:crypto";
49851
50157
  import { StringDecoder } from "node:string_decoder";
49852
- import { setTimeout as delay2 } from "node:timers/promises";
50158
+ import { setTimeout as delay4 } from "node:timers/promises";
49853
50159
 
49854
50160
  // src/worker/errors.ts
49855
50161
  var WorkerError = class extends Error {
@@ -49964,7 +50270,7 @@ async function waitForChange(record2, afterSequence, waitMs) {
49964
50270
  try {
49965
50271
  await Promise.race([
49966
50272
  change,
49967
- delay2(waitMs, void 0, { signal: waitController.signal })
50273
+ delay4(waitMs, void 0, { signal: waitController.signal })
49968
50274
  ]);
49969
50275
  } finally {
49970
50276
  record2.changeWaiters.delete(changed);
@@ -50135,7 +50441,7 @@ async function terminateProcessTree(child) {
50135
50441
  } catch {
50136
50442
  child.kill("SIGTERM");
50137
50443
  }
50138
- await delay2(2e3);
50444
+ await delay4(2e3);
50139
50445
  if (child.exitCode === null) {
50140
50446
  try {
50141
50447
  process.kill(-child.pid, "SIGKILL");
@@ -50228,7 +50534,7 @@ var CommandService = class {
50228
50534
  const completion = new Promise((resolve) => {
50229
50535
  complete = resolve;
50230
50536
  });
50231
- const id = randomUUID();
50537
+ const id = randomUUID3();
50232
50538
  const record2 = {
50233
50539
  id,
50234
50540
  child,
@@ -50300,7 +50606,7 @@ var CommandService = class {
50300
50606
  try {
50301
50607
  await Promise.race([
50302
50608
  record2.completion,
50303
- delay2(waitMs, void 0, { signal: waitController.signal })
50609
+ delay4(waitMs, void 0, { signal: waitController.signal })
50304
50610
  ]);
50305
50611
  } finally {
50306
50612
  waitController.abort();
@@ -50326,7 +50632,7 @@ var CommandService = class {
50326
50632
  try {
50327
50633
  await Promise.race([
50328
50634
  record2.completion,
50329
- delay2(waitMs, void 0, { signal: waitController.signal })
50635
+ delay4(waitMs, void 0, { signal: waitController.signal })
50330
50636
  ]);
50331
50637
  } finally {
50332
50638
  waitController.abort();
@@ -50428,29 +50734,29 @@ var CommandService = class {
50428
50734
  };
50429
50735
 
50430
50736
  // src/worker/file-service.ts
50431
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
50737
+ import { createHash, randomUUID as randomUUID4 } from "node:crypto";
50432
50738
  import {
50433
50739
  chmod as chmod2,
50434
50740
  link as link2,
50435
50741
  lstat as lstat2,
50436
- mkdir as mkdir3,
50742
+ mkdir as mkdir5,
50437
50743
  opendir,
50438
50744
  open,
50439
- readFile as readFile3,
50745
+ readFile as readFile4,
50440
50746
  rename,
50441
- rm as rm2,
50747
+ rm as rm4,
50442
50748
  rmdir,
50443
- stat as stat2,
50444
- writeFile as writeFile3
50749
+ stat as stat3,
50750
+ writeFile as writeFile5
50445
50751
  } from "node:fs/promises";
50446
- import path6 from "node:path";
50752
+ import path8 from "node:path";
50447
50753
  import { performance as performance2 } from "node:perf_hooks";
50448
50754
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
50449
50755
 
50450
50756
  // src/worker/path-policy.ts
50451
- import { lstat, realpath, stat } from "node:fs/promises";
50757
+ import { lstat, realpath, stat as stat2 } from "node:fs/promises";
50452
50758
  import os5 from "node:os";
50453
- import path5 from "node:path";
50759
+ import path7 from "node:path";
50454
50760
  function samePath(left, right) {
50455
50761
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
50456
50762
  }
@@ -50462,15 +50768,15 @@ function accountHomeDirectory() {
50462
50768
  }
50463
50769
  }
50464
50770
  function isWithin(root, candidate) {
50465
- const relative = path5.relative(root, candidate);
50466
- return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
50771
+ const relative = path7.relative(root, candidate);
50772
+ return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
50467
50773
  }
50468
50774
  function validateRelativePath(value) {
50469
50775
  if (value.includes("\0")) {
50470
50776
  throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
50471
50777
  }
50472
50778
  const explicitNativePosixPath = process.platform !== "win32" && value.startsWith("./");
50473
- if (path5.isAbsolute(value) || path5.posix.isAbsolute(value) || !explicitNativePosixPath && path5.win32.isAbsolute(value)) {
50779
+ if (path7.isAbsolute(value) || path7.posix.isAbsolute(value) || !explicitNativePosixPath && path7.win32.isAbsolute(value)) {
50474
50780
  throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
50475
50781
  }
50476
50782
  const segments = explicitNativePosixPath ? value.split(/\/+/).filter(Boolean) : value.split(/[\\/]+/);
@@ -50480,20 +50786,20 @@ function validateRelativePath(value) {
50480
50786
  return value === "" ? "." : value;
50481
50787
  }
50482
50788
  async function canonicalizeRoot(candidate) {
50483
- const root = await realpath(path5.resolve(candidate)).catch((error46) => {
50789
+ const root = await realpath(path7.resolve(candidate)).catch((error46) => {
50484
50790
  if (error46.code === "ENOENT") {
50485
50791
  throw new WorkerError("root_not_found", "The workspace directory does not exist.");
50486
50792
  }
50487
50793
  throw error46;
50488
50794
  });
50489
- const rootStat = await stat(root);
50795
+ const rootStat = await stat2(root);
50490
50796
  if (!rootStat.isDirectory()) {
50491
50797
  throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
50492
50798
  }
50493
- const filesystemRoot = path5.parse(root).root;
50799
+ const filesystemRoot = path7.parse(root).root;
50494
50800
  const homes = await Promise.all(
50495
50801
  [os5.homedir(), accountHomeDirectory()].map(
50496
- async (home) => await realpath(home).catch(() => path5.resolve(home))
50802
+ async (home) => await realpath(home).catch(() => path7.resolve(home))
50497
50803
  )
50498
50804
  );
50499
50805
  const isHomeDirectory = homes.some((home) => samePath(root, home));
@@ -50529,7 +50835,7 @@ var PathPolicy = class _PathPolicy {
50529
50835
  return canonical;
50530
50836
  }
50531
50837
  async resolveDiscoveredExisting(candidate) {
50532
- const lexical = path5.resolve(candidate);
50838
+ const lexical = path7.resolve(candidate);
50533
50839
  if (!isWithin(this.root, lexical)) {
50534
50840
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50535
50841
  }
@@ -50547,7 +50853,7 @@ var PathPolicy = class _PathPolicy {
50547
50853
  }
50548
50854
  async resolveWritableFile(relativePath) {
50549
50855
  const lexical = this.resolveLexical(relativePath);
50550
- const parent = path5.dirname(lexical);
50856
+ const parent = path7.dirname(lexical);
50551
50857
  await this.rejectLinkedComponents(parent);
50552
50858
  const canonicalParent = await realpath(parent).catch((error46) => {
50553
50859
  if (error46.code === "ENOENT") {
@@ -50558,7 +50864,7 @@ var PathPolicy = class _PathPolicy {
50558
50864
  if (!isWithin(this.root, canonicalParent)) {
50559
50865
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50560
50866
  }
50561
- if (!(await stat(canonicalParent)).isDirectory()) {
50867
+ if (!(await stat2(canonicalParent)).isDirectory()) {
50562
50868
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50563
50869
  }
50564
50870
  try {
@@ -50578,7 +50884,7 @@ var PathPolicy = class _PathPolicy {
50578
50884
  if (error46 instanceof WorkerError) throw error46;
50579
50885
  if (error46.code !== "ENOENT") throw error46;
50580
50886
  }
50581
- return path5.join(canonicalParent, path5.basename(lexical));
50887
+ return path7.join(canonicalParent, path7.basename(lexical));
50582
50888
  }
50583
50889
  async resolveWritableDirectory(relativePath, recursive) {
50584
50890
  const lexical = this.resolveLexical(relativePath);
@@ -50601,7 +50907,7 @@ var PathPolicy = class _PathPolicy {
50601
50907
  if (error46.code !== "ENOENT") throw error46;
50602
50908
  }
50603
50909
  if (!recursive) {
50604
- const parent = path5.dirname(lexical);
50910
+ const parent = path7.dirname(lexical);
50605
50911
  await this.rejectLinkedComponents(parent);
50606
50912
  const canonicalParent = await realpath(parent).catch(
50607
50913
  (error46) => {
@@ -50617,15 +50923,15 @@ var PathPolicy = class _PathPolicy {
50617
50923
  if (!isWithin(this.root, canonicalParent)) {
50618
50924
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50619
50925
  }
50620
- if (!(await stat(canonicalParent)).isDirectory()) {
50926
+ if (!(await stat2(canonicalParent)).isDirectory()) {
50621
50927
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50622
50928
  }
50623
50929
  return {
50624
- target: path5.join(canonicalParent, path5.basename(lexical)),
50930
+ target: path7.join(canonicalParent, path7.basename(lexical)),
50625
50931
  exists: false
50626
50932
  };
50627
50933
  }
50628
- let existingAncestor = path5.dirname(lexical);
50934
+ let existingAncestor = path7.dirname(lexical);
50629
50935
  while (!samePath(existingAncestor, this.root)) {
50630
50936
  try {
50631
50937
  await this.rejectLinkedComponents(existingAncestor);
@@ -50633,13 +50939,13 @@ var PathPolicy = class _PathPolicy {
50633
50939
  if (!isWithin(this.root, canonicalAncestor)) {
50634
50940
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50635
50941
  }
50636
- if (!(await stat(canonicalAncestor)).isDirectory()) {
50942
+ if (!(await stat2(canonicalAncestor)).isDirectory()) {
50637
50943
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50638
50944
  }
50639
50945
  return {
50640
- target: path5.join(
50946
+ target: path7.join(
50641
50947
  canonicalAncestor,
50642
- path5.relative(existingAncestor, lexical)
50948
+ path7.relative(existingAncestor, lexical)
50643
50949
  ),
50644
50950
  exists: false
50645
50951
  };
@@ -50647,13 +50953,13 @@ var PathPolicy = class _PathPolicy {
50647
50953
  if (error46 instanceof WorkerError) throw error46;
50648
50954
  if (error46.code !== "ENOENT") throw error46;
50649
50955
  }
50650
- existingAncestor = path5.dirname(existingAncestor);
50956
+ existingAncestor = path7.dirname(existingAncestor);
50651
50957
  }
50652
50958
  return { target: lexical, exists: false };
50653
50959
  }
50654
50960
  async resolveVacantPath(relativePath) {
50655
50961
  const lexical = this.resolveLexical(relativePath);
50656
- const parent = path5.dirname(lexical);
50962
+ const parent = path7.dirname(lexical);
50657
50963
  await this.rejectLinkedComponents(parent);
50658
50964
  const canonicalParent = await realpath(parent).catch(
50659
50965
  (error46) => {
@@ -50669,7 +50975,7 @@ var PathPolicy = class _PathPolicy {
50669
50975
  if (!isWithin(this.root, canonicalParent)) {
50670
50976
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50671
50977
  }
50672
- if (!(await stat(canonicalParent)).isDirectory()) {
50978
+ if (!(await stat2(canonicalParent)).isDirectory()) {
50673
50979
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50674
50980
  }
50675
50981
  try {
@@ -50682,11 +50988,11 @@ var PathPolicy = class _PathPolicy {
50682
50988
  if (error46 instanceof WorkerError) throw error46;
50683
50989
  if (error46.code !== "ENOENT") throw error46;
50684
50990
  }
50685
- return path5.join(canonicalParent, path5.basename(lexical));
50991
+ return path7.join(canonicalParent, path7.basename(lexical));
50686
50992
  }
50687
50993
  resolveLexical(relativePath) {
50688
50994
  const validated = validateRelativePath(relativePath);
50689
- const candidate = path5.resolve(this.root, validated);
50995
+ const candidate = path7.resolve(this.root, validated);
50690
50996
  if (!isWithin(this.root, candidate)) {
50691
50997
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50692
50998
  }
@@ -50696,11 +51002,11 @@ var PathPolicy = class _PathPolicy {
50696
51002
  if (!isWithin(this.root, candidate)) {
50697
51003
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50698
51004
  }
50699
- const relative = path5.relative(this.root, candidate);
51005
+ const relative = path7.relative(this.root, candidate);
50700
51006
  if (!relative) return;
50701
51007
  let current = this.root;
50702
- for (const segment of relative.split(path5.sep)) {
50703
- current = path5.join(current, segment);
51008
+ for (const segment of relative.split(path7.sep)) {
51009
+ current = path7.join(current, segment);
50704
51010
  try {
50705
51011
  const currentStat = await lstat(current);
50706
51012
  if (currentStat.isSymbolicLink()) {
@@ -50723,12 +51029,12 @@ function sha256(content) {
50723
51029
  return createHash("sha256").update(content).digest("hex");
50724
51030
  }
50725
51031
  function isPathWithin(root, candidate) {
50726
- const relative = path6.relative(root, candidate);
50727
- return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
51032
+ const relative = path8.relative(root, candidate);
51033
+ return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
50728
51034
  }
50729
51035
  var fileWriteTails = /* @__PURE__ */ new Map();
50730
51036
  async function withFileWriteLock(target, operation) {
50731
- const normalized = path6.normalize(target);
51037
+ const normalized = path8.normalize(target);
50732
51038
  const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
50733
51039
  const predecessor = fileWriteTails.get(key);
50734
51040
  let release;
@@ -50745,7 +51051,7 @@ async function withFileWriteLock(target, operation) {
50745
51051
  }
50746
51052
  }
50747
51053
  async function withFileWriteLocks(targets, operation) {
50748
- const unique = [...new Set(targets.map((target) => path6.normalize(target)))].sort(
51054
+ const unique = [...new Set(targets.map((target) => path8.normalize(target)))].sort(
50749
51055
  (left, right) => left.localeCompare(right)
50750
51056
  );
50751
51057
  const run = async (index) => {
@@ -50757,7 +51063,7 @@ async function withFileWriteLocks(targets, operation) {
50757
51063
  async function requireRevision(target, expectedSha256) {
50758
51064
  let actual = null;
50759
51065
  try {
50760
- actual = sha256(await readFile3(target));
51066
+ actual = sha256(await readFile4(target));
50761
51067
  } catch (error46) {
50762
51068
  if (error46.code !== "ENOENT") throw error46;
50763
51069
  }
@@ -50781,7 +51087,7 @@ function compareNames(left, right) {
50781
51087
  return left < right ? -1 : left > right ? 1 : 0;
50782
51088
  }
50783
51089
  function relativeSortKey(root, target) {
50784
- const relative = path6.relative(root, target);
51090
+ const relative = path8.relative(root, target);
50785
51091
  if (!relative) return ".";
50786
51092
  return process.platform === "win32" ? relative.replaceAll("\\", "/") : relative;
50787
51093
  }
@@ -51093,7 +51399,7 @@ var FileService = class {
51093
51399
  }
51094
51400
  async #readResolvedText(target, maximumBytes = MAX_TEXT_BYTES) {
51095
51401
  const boundedMaximum = Math.min(maximumBytes, MAX_TEXT_BYTES);
51096
- const targetStat = await stat2(target);
51402
+ const targetStat = await stat3(target);
51097
51403
  if (!targetStat.isFile()) {
51098
51404
  throw new WorkerError("not_file", "The requested path is not a file.");
51099
51405
  }
@@ -51136,7 +51442,7 @@ var FileService = class {
51136
51442
  this.policy.resolveExisting(startPath),
51137
51443
  deadlineAt
51138
51444
  );
51139
- const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
51445
+ const startStat = await this.#withinDeadline(stat3(start), deadlineAt);
51140
51446
  if (!startStat.isDirectory()) {
51141
51447
  throw new WorkerError("not_directory", "The requested path is not a directory.");
51142
51448
  }
@@ -51165,7 +51471,7 @@ var FileService = class {
51165
51471
  if (batch.overflow) throw scanLimitError();
51166
51472
  scannedEntries += batch.children.length;
51167
51473
  for (const child of batch.children) {
51168
- const target = path6.join(directory, child.name);
51474
+ const target = path8.join(directory, child.name);
51169
51475
  heapPush(frontier, {
51170
51476
  target,
51171
51477
  sortKey: relativeSortKey(this.policy.root, target),
@@ -51189,7 +51495,7 @@ var FileService = class {
51189
51495
  throw scanLimitError();
51190
51496
  }
51191
51497
  scannedEntries += 1;
51192
- const target = path6.join(directory, child.name);
51498
+ const target = path8.join(directory, child.name);
51193
51499
  let childStat;
51194
51500
  try {
51195
51501
  childStat = await this.#withinDeadline(
@@ -51341,7 +51647,7 @@ var FileService = class {
51341
51647
  if (!patterns) return false;
51342
51648
  const normalized = relativePath.replaceAll("\\", "/");
51343
51649
  try {
51344
- return patterns.some((pattern) => path6.posix.matchesGlob(normalized, pattern));
51650
+ return patterns.some((pattern) => path8.posix.matchesGlob(normalized, pattern));
51345
51651
  } catch {
51346
51652
  throw new WorkerError("invalid_search", "Search glob pattern is invalid.");
51347
51653
  }
@@ -51359,7 +51665,7 @@ var FileService = class {
51359
51665
  this.policy.resolveExisting(options.path ?? "."),
51360
51666
  deadlineAt
51361
51667
  );
51362
- const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
51668
+ const startStat = await this.#withinDeadline(stat3(start), deadlineAt);
51363
51669
  const matches = [];
51364
51670
  let scannedEntries = 0;
51365
51671
  let scannedFiles = 0;
@@ -51370,7 +51676,7 @@ var FileService = class {
51370
51676
  const searchFile = async (target) => {
51371
51677
  const relative = displayPath(this.policy.root, target);
51372
51678
  if (extensions && !extensions.some(
51373
- (extension) => path6.basename(target).toLowerCase().endsWith(extension)
51679
+ (extension) => path8.basename(target).toLowerCase().endsWith(extension)
51374
51680
  )) {
51375
51681
  return false;
51376
51682
  }
@@ -51465,7 +51771,7 @@ var FileService = class {
51465
51771
  scannedEntries += batch.children.length;
51466
51772
  for (const child of batch.children) {
51467
51773
  this.#assertBeforeDeadline(deadlineAt);
51468
- const target = path6.join(directory, child.name);
51774
+ const target = path8.join(directory, child.name);
51469
51775
  let targetType;
51470
51776
  if (this.#trustDirectoryEntryTypes) {
51471
51777
  if (child.isSymbolicLink()) {
@@ -51613,12 +51919,12 @@ var FileService = class {
51613
51919
  );
51614
51920
  if (current.exists) return { created: false };
51615
51921
  try {
51616
- await mkdir3(current.target, { recursive });
51922
+ await mkdir5(current.target, { recursive });
51617
51923
  } catch (error46) {
51618
51924
  if (error46.code !== "EEXIST") throw error46;
51619
51925
  }
51620
51926
  const resolved = await this.policy.resolveExisting(relativePath);
51621
- if (!(await stat2(resolved)).isDirectory()) {
51927
+ if (!(await stat3(resolved)).isDirectory()) {
51622
51928
  throw new WorkerError("not_directory", "The destination is not a directory.");
51623
51929
  }
51624
51930
  return { created: true };
@@ -51650,12 +51956,12 @@ var FileService = class {
51650
51956
  try {
51651
51957
  if (targetStat.isDirectory()) {
51652
51958
  if (recursive) {
51653
- await rm2(target, { recursive: true, force: false });
51959
+ await rm4(target, { recursive: true, force: false });
51654
51960
  } else {
51655
51961
  await rmdir(target);
51656
51962
  }
51657
51963
  } else {
51658
- await rm2(target, { force: false });
51964
+ await rm4(target, { force: false });
51659
51965
  }
51660
51966
  } catch (error46) {
51661
51967
  const code = error46.code;
@@ -51763,7 +52069,7 @@ var FileService = class {
51763
52069
  return await withFileWriteLock(target, async () => {
51764
52070
  let existingMode;
51765
52071
  try {
51766
- const existingStat = await stat2(target);
52072
+ const existingStat = await stat3(target);
51767
52073
  if (existingStat.isFile()) existingMode = existingStat.mode & 4095;
51768
52074
  } catch (error46) {
51769
52075
  if (error46.code !== "ENOENT") throw error46;
@@ -51775,9 +52081,9 @@ var FileService = class {
51775
52081
  );
51776
52082
  }
51777
52083
  if (expectedSha256) await requireRevision(target, expectedSha256);
51778
- const temporary = path6.join(path6.dirname(target), `.glossa-${randomUUID2()}.tmp`);
52084
+ const temporary = path8.join(path8.dirname(target), `.glossa-${randomUUID4()}.tmp`);
51779
52085
  try {
51780
- await writeFile3(temporary, bytes, { flag: "wx", mode: 384 });
52086
+ await writeFile5(temporary, bytes, { flag: "wx", mode: 384 });
51781
52087
  target = await this.policy.resolveWritableFile(relativePath);
51782
52088
  const tempStat = await lstat2(temporary);
51783
52089
  if (!tempStat.isFile() || tempStat.isSymbolicLink()) {
@@ -51803,7 +52109,7 @@ var FileService = class {
51803
52109
  }
51804
52110
  }
51805
52111
  } finally {
51806
- await rm2(temporary, { force: true });
52112
+ await rm4(temporary, { force: true });
51807
52113
  }
51808
52114
  return { sha256: sha256(bytes), bytes: bytes.byteLength };
51809
52115
  });
@@ -51830,7 +52136,9 @@ function jobInputContainsRestrictedData(job) {
51830
52136
  return containsRestrictedAuthenticationData({
51831
52137
  query: job.query,
51832
52138
  path: job.path,
51833
- extensions: job.extensions
52139
+ extensions: job.extensions,
52140
+ includeGlobs: job.includeGlobs,
52141
+ excludeGlobs: job.excludeGlobs
51834
52142
  });
51835
52143
  case "read_file_range":
51836
52144
  return containsRestrictedAuthenticationData(job.path);
@@ -52013,7 +52321,7 @@ var LocalWorker = class _LocalWorker {
52013
52321
  };
52014
52322
 
52015
52323
  // src/worker/remote-worker.ts
52016
- import { randomUUID as randomUUID3 } from "node:crypto";
52324
+ import { randomUUID as randomUUID5 } from "node:crypto";
52017
52325
  var WORKER_REQUEST_TIMEOUT_MS = 19e3;
52018
52326
  var DEFAULT_RECONNECT_BASE_MS = 500;
52019
52327
  var DEFAULT_RECONNECT_MAX_MS = 1e4;
@@ -52027,6 +52335,14 @@ var DeviceRejectedError = class extends Error {
52027
52335
  this.name = "DeviceRejectedError";
52028
52336
  }
52029
52337
  };
52338
+ var RelayAccessProfileUnsupportedError = class extends Error {
52339
+ constructor(profile) {
52340
+ super(
52341
+ `The relay needs an update before it can represent ${profile} access. Update the relay before reconnecting this workspace.`
52342
+ );
52343
+ this.name = "RelayAccessProfileUnsupportedError";
52344
+ }
52345
+ };
52030
52346
  var RelayResponseError = class extends Error {
52031
52347
  constructor(status) {
52032
52348
  super(`The relay returned HTTP ${status}.`);
@@ -52125,7 +52441,7 @@ var RemoteWorker = class {
52125
52441
  #reconnectBaseMs;
52126
52442
  #reconnectMaxMs;
52127
52443
  #heartbeatMs;
52128
- #workerId = randomUUID3();
52444
+ #workerId = randomUUID5();
52129
52445
  #onStatus;
52130
52446
  constructor(options) {
52131
52447
  this.#origin = new URL(options.origin);
@@ -52166,8 +52482,10 @@ var RemoteWorker = class {
52166
52482
  await this.#pollGeneration(session);
52167
52483
  } catch (error46) {
52168
52484
  if (this.#signal.aborted) return;
52169
- if (error46 instanceof DeviceRejectedError) throw error46;
52170
- const delay4 = reconnectDelayMs(
52485
+ if (error46 instanceof DeviceRejectedError || error46 instanceof RelayAccessProfileUnsupportedError) {
52486
+ throw error46;
52487
+ }
52488
+ const delay6 = reconnectDelayMs(
52171
52489
  failures,
52172
52490
  this.#random,
52173
52491
  this.#reconnectBaseMs,
@@ -52177,10 +52495,10 @@ var RemoteWorker = class {
52177
52495
  this.#onStatus({
52178
52496
  state: "retrying",
52179
52497
  error: error46 instanceof Error ? error46 : new Error(String(error46)),
52180
- retryInMs: delay4
52498
+ retryInMs: delay6
52181
52499
  });
52182
52500
  try {
52183
- await this.#sleep(delay4, this.#signal);
52501
+ await this.#sleep(delay6, this.#signal);
52184
52502
  } catch (sleepError) {
52185
52503
  if (this.#signal.aborted) return;
52186
52504
  throw sleepError;
@@ -52224,100 +52542,74 @@ var RemoteWorker = class {
52224
52542
  workerId: this.#workerId,
52225
52543
  capabilities: { commandProgress: true, concurrentJobs: true }
52226
52544
  };
52227
- const versionedCurrentBody = this.#workerVersion ? { ...currentBody, workerVersion: this.#workerVersion } : void 0;
52228
- const versionedMutationBody = this.#workerVersion ? { ...mutationBody, workerVersion: this.#workerVersion } : void 0;
52229
- const versionedStructuredBody = this.#workerVersion ? { ...structuredBody, workerVersion: this.#workerVersion } : void 0;
52230
- const preferredProfileBody = this.#accessProfile ? {
52231
- ...versionedCurrentBody ?? currentBody,
52232
- accessProfile: this.#accessProfile,
52233
- ...this.#workspaceLabel ? { workspaceLabel: this.#workspaceLabel } : {}
52234
- } : void 0;
52235
- const attempts = [
52236
- ...preferredProfileBody ? [{ body: preferredProfileBody, legacyRelay: false }] : [],
52237
- ...versionedCurrentBody && this.#workspaceLabel ? [{
52238
- body: {
52239
- ...versionedCurrentBody,
52240
- workspaceLabel: this.#workspaceLabel
52241
- },
52242
- legacyRelay: false
52243
- }] : [],
52244
- ...this.#workspaceLabel ? [{
52245
- body: {
52246
- ...currentBody,
52247
- workspaceLabel: this.#workspaceLabel
52248
- },
52249
- legacyRelay: false
52250
- }] : [],
52251
- ...versionedCurrentBody ? [{ body: versionedCurrentBody, legacyRelay: false }] : [],
52252
- {
52253
- body: currentBody,
52254
- legacyRelay: false
52255
- },
52256
- ...versionedMutationBody && this.#workspaceLabel ? [{
52257
- body: {
52258
- ...versionedMutationBody,
52259
- workspaceLabel: this.#workspaceLabel
52260
- },
52261
- legacyRelay: false
52262
- }] : [],
52263
- ...this.#workspaceLabel ? [{
52264
- body: {
52265
- ...mutationBody,
52266
- workspaceLabel: this.#workspaceLabel
52267
- },
52268
- legacyRelay: false
52269
- }] : [],
52270
- ...versionedMutationBody ? [{ body: versionedMutationBody, legacyRelay: false }] : [],
52545
+ const capabilityBodies = [
52546
+ currentBody,
52547
+ mutationBody,
52548
+ structuredBody,
52549
+ concurrentBody,
52271
52550
  {
52272
- body: mutationBody,
52273
- legacyRelay: false
52551
+ workerId: this.#workerId,
52552
+ capabilities: { commandProgress: true }
52274
52553
  },
52275
- ...versionedStructuredBody && this.#workspaceLabel ? [{
52276
- body: {
52277
- ...versionedStructuredBody,
52278
- workspaceLabel: this.#workspaceLabel
52279
- },
52280
- legacyRelay: false
52281
- }] : [],
52282
- ...this.#workspaceLabel ? [{
52283
- body: {
52284
- ...structuredBody,
52285
- workspaceLabel: this.#workspaceLabel
52286
- },
52287
- legacyRelay: false
52288
- }] : [],
52289
- ...versionedStructuredBody ? [{ body: versionedStructuredBody, legacyRelay: false }] : [],
52554
+ { workerId: this.#workerId }
52555
+ ];
52556
+ const metadataPhases = [
52290
52557
  {
52291
- body: structuredBody,
52292
- legacyRelay: false
52558
+ includeVersion: Boolean(this.#workerVersion),
52559
+ includeLabel: Boolean(this.#workspaceLabel)
52293
52560
  },
52294
52561
  ...this.#workspaceLabel ? [{
52295
- body: {
52296
- ...concurrentBody,
52297
- workspaceLabel: this.#workspaceLabel
52298
- },
52299
- legacyRelay: false
52562
+ includeVersion: Boolean(this.#workerVersion),
52563
+ includeLabel: false
52300
52564
  }] : [],
52301
- {
52302
- body: concurrentBody,
52303
- legacyRelay: false
52304
- },
52305
- {
52306
- body: {
52307
- workerId: this.#workerId,
52308
- capabilities: { commandProgress: true }
52309
- },
52310
- legacyRelay: false
52311
- },
52312
- { body: { workerId: this.#workerId }, legacyRelay: false },
52313
- { body: {}, legacyRelay: true }
52565
+ ...this.#workerVersion && this.#workspaceLabel ? [{ includeVersion: false, includeLabel: true }] : [],
52566
+ { includeVersion: false, includeLabel: false }
52314
52567
  ];
52315
- for (const [index, attempt] of attempts.entries()) {
52568
+ const attempts = [];
52569
+ const seenAttempts = /* @__PURE__ */ new Set();
52570
+ const pushAttempt = (base, includeProfile, includeVersion, includeLabel, legacyRelay = false) => {
52571
+ const body = {
52572
+ ...base,
52573
+ ...includeProfile && this.#accessProfile ? { accessProfile: this.#accessProfile } : {},
52574
+ ...includeVersion && this.#workerVersion ? { workerVersion: this.#workerVersion } : {},
52575
+ ...includeLabel && this.#workspaceLabel ? { workspaceLabel: this.#workspaceLabel } : {}
52576
+ };
52577
+ const key = JSON.stringify(body);
52578
+ if (seenAttempts.has(key)) return;
52579
+ seenAttempts.add(key);
52580
+ attempts.push({ body, legacyRelay });
52581
+ };
52582
+ if (this.#accessProfile) {
52583
+ for (const metadata of metadataPhases) {
52584
+ for (const body of capabilityBodies) {
52585
+ pushAttempt(
52586
+ body,
52587
+ true,
52588
+ metadata.includeVersion,
52589
+ metadata.includeLabel
52590
+ );
52591
+ }
52592
+ }
52593
+ }
52594
+ if (this.#accessProfile === void 0 || this.#accessProfile === "system") {
52595
+ for (const metadata of metadataPhases) {
52596
+ for (const body of capabilityBodies) {
52597
+ pushAttempt(
52598
+ body,
52599
+ false,
52600
+ metadata.includeVersion,
52601
+ metadata.includeLabel
52602
+ );
52603
+ }
52604
+ }
52605
+ attempts.push({ body: {}, legacyRelay: true });
52606
+ }
52607
+ for (const attempt of attempts) {
52316
52608
  let response;
52317
52609
  try {
52318
52610
  response = await this.#post("/device/register", attempt.body);
52319
52611
  } catch (error46) {
52320
- if (error46 instanceof RelayResponseError && error46.status === 400 && index < attempts.length - 1) {
52612
+ if (error46 instanceof RelayResponseError && error46.status === 400) {
52321
52613
  continue;
52322
52614
  }
52323
52615
  throw error46;
@@ -52330,17 +52622,25 @@ var RemoteWorker = class {
52330
52622
  throw new Error("The relay returned an invalid registration response.");
52331
52623
  }
52332
52624
  const workerToken = "workerToken" in value ? optionalWorkerToken(value.workerToken) : void 0;
52333
- return {
52625
+ const accessProfileAccepted = this.#accessProfile === void 0 || this.#accessProfile === "system" || "accessProfile" in value && value.accessProfile === this.#accessProfile;
52626
+ const session = {
52334
52627
  generation: value.generation,
52335
52628
  legacyRelay: attempt.legacyRelay,
52336
52629
  concurrentJobs: !attempt.legacyRelay && supportsCapability(value, "concurrentJobs"),
52337
52630
  structuredReads: !attempt.legacyRelay && supportsCapability(value, "structuredReads"),
52338
52631
  structuredMutations: !attempt.legacyRelay && supportsCapability(value, "structuredMutations"),
52339
52632
  commandOutputRanges: !attempt.legacyRelay && supportsCapability(value, "commandOutputRanges"),
52340
- accessProfileAccepted: this.#accessProfile === void 0 || "accessProfile" in value && value.accessProfile === this.#accessProfile,
52633
+ accessProfileAccepted,
52341
52634
  workspaceLabelAccepted: this.#workspaceLabel === void 0 || "workspaceLabel" in value && value.workspaceLabel === this.#workspaceLabel,
52342
52635
  ...workerToken ? { workerToken } : {}
52343
52636
  };
52637
+ if (!accessProfileAccepted && this.#accessProfile) {
52638
+ throw new RelayAccessProfileUnsupportedError(this.#accessProfile);
52639
+ }
52640
+ return session;
52641
+ }
52642
+ if (this.#accessProfile && this.#accessProfile !== "system") {
52643
+ throw new RelayAccessProfileUnsupportedError(this.#accessProfile);
52344
52644
  }
52345
52645
  throw new Error("The relay rejected every supported registration shape.");
52346
52646
  }
@@ -52580,10 +52880,10 @@ var RemoteWorker = class {
52580
52880
  await unregister(`Device ${this.#deviceToken}`);
52581
52881
  }
52582
52882
  }
52583
- async #post(path13, body, workerToken) {
52883
+ async #post(path14, body, workerToken) {
52584
52884
  const timeout = AbortSignal.timeout(WORKER_REQUEST_TIMEOUT_MS);
52585
52885
  const signal = AbortSignal.any([this.#signal, timeout]);
52586
- const response = await this.#fetcher(new URL(path13, this.#origin), {
52886
+ const response = await this.#fetcher(new URL(path14, this.#origin), {
52587
52887
  method: "POST",
52588
52888
  headers: {
52589
52889
  authorization: workerToken ? `Worker ${workerToken}` : `Device ${this.#deviceToken}`,
@@ -52704,56 +53004,27 @@ function visibleWorker(worker, options) {
52704
53004
  }
52705
53005
  async function deviceForSession(endpoints, dependencies = {}, signal) {
52706
53006
  const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
52707
- const loadLogin = dependencies.loadCredentials ?? loadCredentials;
52708
- const validate2 = dependencies.validCredentials ?? validCredentials;
52709
- const subjectFor = dependencies.accessTokenSubject ?? accessTokenSubject;
52710
53007
  const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
52711
- const enroll = dependencies.enrollDevice ?? enrollDevice;
52712
53008
  const saveDevice = dependencies.saveDeviceCredential ?? saveDeviceCredential;
52713
- const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
52714
- const name = dependencies.defaultDeviceName ?? defaultDeviceName;
52715
- const baseFetch = dependencies.fetch ?? fetch;
52716
- const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
53009
+ const pair = dependencies.pairDevice ?? pairDevice;
53010
+ const revoke = dependencies.revokePairedDevice ?? revokePairedDevice;
53011
+ const withPairingLease = dependencies.withDevicePairingLease ?? withDevicePairingLease;
52717
53012
  signal?.throwIfAborted();
52718
53013
  const stored = await loadDevice();
52719
- let credentials = dependencies.credentials;
52720
- const currentCredentials = async () => {
52721
- if (credentials) return credentials;
52722
- const loaded = await loadLogin();
52723
- if (!loaded) throw new Error("Not signed in. Run Glossa again to sign in.");
52724
- credentials = await validate2(loaded.credentials, { fetch: fetchRequest });
52725
- return credentials;
52726
- };
52727
- if (stored?.relayOrigin === endpoints.relayOrigin) {
52728
- const current2 = await currentCredentials();
52729
- const accountSubject = subjectFor(current2);
52730
- if (stored.accountSubject === accountSubject) return stored;
52731
- if (stored.accountSubject === void 0 && await ownsDevice(endpoints, current2, stored.deviceId, fetchRequest)) {
52732
- const migrated = { ...stored, accountSubject };
52733
- await saveDevice(migrated);
52734
- return migrated;
52735
- }
52736
- await removeDevice();
52737
- }
52738
- signal?.throwIfAborted();
52739
- const current = await currentCredentials();
52740
- const enrolled = await enroll(
52741
- endpoints,
52742
- current,
52743
- name(),
52744
- fetchRequest
52745
- );
52746
- const bound = {
52747
- ...enrolled,
52748
- accountSubject: subjectFor(current)
52749
- };
52750
- await saveDevice(bound);
52751
- return bound;
52752
- }
52753
- async function reenrollRejectedDevice(endpoints, dependencies = {}, signal) {
52754
- const remove = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
52755
- await remove();
52756
- return await deviceForSession(endpoints, dependencies, signal);
53014
+ if (stored?.relayOrigin === endpoints.relayOrigin) return stored;
53015
+ return await withPairingLease(async () => {
53016
+ signal?.throwIfAborted();
53017
+ const current = await loadDevice();
53018
+ if (current?.relayOrigin === endpoints.relayOrigin) return current;
53019
+ if (current) {
53020
+ await revoke({ relayOrigin: current.relayOrigin }, current);
53021
+ await removeDevice();
53022
+ }
53023
+ signal?.throwIfAborted();
53024
+ const paired = await pair(endpoints, signal);
53025
+ await saveDevice(paired);
53026
+ return paired;
53027
+ }, signal);
52757
53028
  }
52758
53029
  function retryMessage(retryInMs) {
52759
53030
  const seconds = Math.max(1, Math.ceil(retryInMs / 1e3));
@@ -52865,9 +53136,6 @@ async function connectRemoteWorker(endpoints, device, worker, options, signal, o
52865
53136
  await connectHintTask;
52866
53137
  }
52867
53138
  }
52868
- function shouldRecoverRejectedDevice(error46, recoveredRejectedDevice, connected) {
52869
- return error46 instanceof DeviceRejectedError && !recoveredRejectedDevice && !connected;
52870
- }
52871
53139
  async function runManagedSession(root, endpoints, options = {}) {
52872
53140
  const controller = new AbortController();
52873
53141
  const stop = () => controller.abort();
@@ -52882,9 +53150,9 @@ async function runManagedSession(root, endpoints, options = {}) {
52882
53150
  try {
52883
53151
  const accessProfile = options.accessProfile ?? DEFAULT_WORKER_ACCESS_PROFILE;
52884
53152
  const sessionOptions = { ...options, accessProfile };
52885
- let device = await deviceForSession(
53153
+ const device = options.device ?? await deviceForSession(
52886
53154
  endpoints,
52887
- options.credentials ? { credentials: options.credentials } : {},
53155
+ {},
52888
53156
  controller.signal
52889
53157
  );
52890
53158
  controller.signal.throwIfAborted();
@@ -52905,41 +53173,18 @@ async function runManagedSession(root, endpoints, options = {}) {
52905
53173
  console.error(accessProfileSummary(accessProfile));
52906
53174
  console.error("Press Ctrl+C to disconnect.");
52907
53175
  }
52908
- let recoveredRejectedDevice = false;
52909
- while (!controller.signal.aborted) {
52910
- let connected = false;
52911
- try {
52912
- await connectRemoteWorker(
52913
- endpoints,
52914
- device,
52915
- worker,
52916
- sessionOptions,
52917
- controller.signal,
52918
- () => {
52919
- connected = true;
52920
- }
52921
- );
52922
- break;
52923
- } catch (error46) {
52924
- if (!shouldRecoverRejectedDevice(
52925
- error46,
52926
- recoveredRejectedDevice,
52927
- connected
52928
- )) {
52929
- throw error46;
52930
- }
52931
- recoveredRejectedDevice = true;
52932
- device = await reenrollRejectedDevice(
52933
- endpoints,
52934
- options.credentials ? { credentials: options.credentials } : {},
52935
- controller.signal
52936
- );
52937
- }
52938
- }
53176
+ await connectRemoteWorker(
53177
+ endpoints,
53178
+ device,
53179
+ worker,
53180
+ sessionOptions,
53181
+ controller.signal,
53182
+ () => void 0
53183
+ );
52939
53184
  } catch (error46) {
52940
53185
  if (error46 instanceof DeviceRejectedError) {
52941
53186
  await deleteDeviceCredential();
52942
- throw new Error("The relay rejected this device. Run Glossa again to reenroll it.");
53187
+ throw new Error("The relay rejected this paired computer. Run Glossa again to pair it with your account.");
52943
53188
  }
52944
53189
  throw error46;
52945
53190
  } finally {
@@ -52964,7 +53209,7 @@ function initialHudState(workspace) {
52964
53209
  message: void 0,
52965
53210
  activities: [],
52966
53211
  activityPage: 0,
52967
- view: "activity",
53212
+ view: "workspace",
52968
53213
  status: void 0,
52969
53214
  deviceSelection: 0,
52970
53215
  pendingAccessProfile: void 0,
@@ -53035,12 +53280,12 @@ function formatByteCount(value) {
53035
53280
  }
53036
53281
  return `${(kibibytes / 1024).toFixed(1)} MiB`;
53037
53282
  }
53038
- function workspacePath(path13) {
53039
- return path13 || ".";
53283
+ function workspacePath(path14) {
53284
+ return path14 || ".";
53040
53285
  }
53041
- function pathSummary(path13, details = []) {
53286
+ function pathSummary(path14, details = []) {
53042
53287
  return {
53043
- target: `path ${quoteActivityInput(path13)}`,
53288
+ target: `path ${quoteActivityInput(path14)}`,
53044
53289
  details,
53045
53290
  truncation: "middle"
53046
53291
  };
@@ -53239,6 +53484,7 @@ var COLORS = {
53239
53484
  muted: "#aaa4b5",
53240
53485
  purple: "#8054ff",
53241
53486
  purpleReadable: "#ad98ff",
53487
+ success: "#65d6a6",
53242
53488
  coral: "#ff665f",
53243
53489
  line: "#5c556e"
53244
53490
  };
@@ -53246,7 +53492,9 @@ function Text2({ color, ...props }) {
53246
53492
  return color === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { ...props }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { ...props, color });
53247
53493
  }
53248
53494
  var ACTIVITY_REFRESH_INTERVAL_MS = 1e4;
53495
+ var ACTIVITY_STATUS_COLUMN_WIDTH = 2;
53249
53496
  var ACTIVITY_TOOL_COLUMN_WIDTH = 15;
53497
+ var ACTIVITY_AGE_COLUMN_WIDTH = 10;
53250
53498
  var ACTIVITY_PREAMBLE_LINES = 1;
53251
53499
  var HudStore = class {
53252
53500
  #state;
@@ -53292,10 +53540,10 @@ function activityAge(updatedAt, working, now) {
53292
53540
  if (elapsedMs < 864e5) return `${Math.floor(elapsedMs / 36e5)}h ago`;
53293
53541
  return `${Math.floor(elapsedMs / 864e5)}d ago`;
53294
53542
  }
53295
- function activityColor(activity) {
53296
- if (activity.state === "working") return COLORS.ink;
53297
- if (activity.state === "failed") return COLORS.coral;
53298
- return COLORS.purpleReadable;
53543
+ function activityStatus(activity) {
53544
+ if (activity.state === "failed") return { symbol: "\xD7", tone: COLORS.coral };
53545
+ if (activity.state === "working") return { symbol: "\u25CB", tone: COLORS.purpleReadable };
53546
+ return { symbol: "\u2713", tone: COLORS.success };
53299
53547
  }
53300
53548
  function activitySummary(activity) {
53301
53549
  return [activity.summary.target, ...activity.summary.details].join(" \xB7 ");
@@ -53311,31 +53559,28 @@ function adjacentAccessProfile(accessProfile, direction) {
53311
53559
  const index = ACCESS_PROFILES.indexOf(accessProfile);
53312
53560
  return ACCESS_PROFILES[index + direction];
53313
53561
  }
53314
- function AccessProfileLadder({ accessProfile, color }) {
53315
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, children: ACCESS_PROFILES.map((profile, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react34.default.Fragment, { children: [
53316
- index > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.line : void 0, children: " \u2500 " }) : null,
53317
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53318
- Text2,
53319
- {
53320
- color: color ? profile === accessProfile ? COLORS.coral : COLORS.muted : void 0,
53321
- children: profile === accessProfile ? `[ ${accessProfileLabel(profile)} ]` : accessProfileLabel(profile)
53322
- }
53323
- )
53324
- ] }, profile)) });
53325
- }
53326
- function accessProfileCapability(accessProfile) {
53327
- if (accessProfile === "read-only") return "Read files only \xB7 no changes \xB7 no commands";
53328
- if (accessProfile === "workspace") return "Read + write files \xB7 commands disabled";
53329
- return "Read + write files + commands \xB7 inherits this OS account's permissions";
53330
- }
53331
- var PRIMARY_FOOTER_HINTS = [
53332
- { key: "A", label: "Activity" },
53333
- { key: "W", label: "Workspace" },
53334
- { key: "D", label: "Devices" },
53335
- { key: "?", label: "Help" },
53336
- { key: "L", label: "Sign out", tone: COLORS.coral },
53337
- { key: "Q", label: "Quit", tone: COLORS.coral }
53338
- ];
53562
+ function accessProfileCapabilities(accessProfile) {
53563
+ if (accessProfile === "read-only") {
53564
+ return { summary: "Read files only", detail: "No changes \xB7 no commands" };
53565
+ }
53566
+ if (accessProfile === "workspace") {
53567
+ return { summary: "Read + write files", detail: "Commands disabled" };
53568
+ }
53569
+ return {
53570
+ summary: "Read + write files + commands",
53571
+ detail: "OS account permissions apply"
53572
+ };
53573
+ }
53574
+ function primaryFooterHints() {
53575
+ return [
53576
+ { key: "A", label: "Activity" },
53577
+ { key: "W", label: "Workspace" },
53578
+ { key: "D", label: "Devices" },
53579
+ { key: "?", label: "Help" },
53580
+ { key: "L", label: "Account sign out", tone: COLORS.coral },
53581
+ { key: "Q", label: "Quit", tone: COLORS.coral }
53582
+ ];
53583
+ }
53339
53584
  function contextualFooterHints(state) {
53340
53585
  if (state.view === "activity") {
53341
53586
  return [
@@ -53383,7 +53628,7 @@ function splitFooterHints(hints, usable) {
53383
53628
  return rows;
53384
53629
  }
53385
53630
  function buildFooterRows(state, usable) {
53386
- const rows = splitFooterHints(PRIMARY_FOOTER_HINTS, usable).map((left) => ({
53631
+ const rows = splitFooterHints(primaryFooterHints(), usable).map((left) => ({
53387
53632
  left,
53388
53633
  right: []
53389
53634
  }));
@@ -53404,7 +53649,10 @@ function promptText(state) {
53404
53649
  if (state.busy) return { message: "Working\u2026" };
53405
53650
  if (!state.prompt) return void 0;
53406
53651
  if (state.prompt.type === "logout") {
53407
- return { message: "Sign out and disconnect?", choices: "Y confirm N cancel" };
53652
+ return {
53653
+ message: "Sign out the CLI account and disconnect? Computer pairing stays active.",
53654
+ choices: "Y confirm N cancel"
53655
+ };
53408
53656
  }
53409
53657
  if (state.prompt.type === "access-confirm") {
53410
53658
  const detail = state.prompt.accessProfile === "system" ? "Commands will inherit this OS account's permissions." : "This allows guarded file writes.";
@@ -53475,7 +53723,7 @@ function Line({ usable, color }) {
53475
53723
  }
53476
53724
  function Header({ state, usable, bodyBudget, color }) {
53477
53725
  const statusColor = state.connection === "error" ? COLORS.coral : COLORS.purpleReadable;
53478
- const pageTitle = state.view === "activity" ? "Recent Activity" : state.view === "devices" ? "Devices" : state.view === "workspace" ? "Workspace" : state.view === "help" ? "Help" : void 0;
53726
+ const pageTitle = state.view === "activity" ? "Activity" : state.view === "devices" ? "Devices" : state.view === "workspace" ? "Workspace" : state.view === "help" ? "Help" : void 0;
53479
53727
  const activityPage = state.view === "activity" ? activityPageInfo(state, bodyBudget) : void 0;
53480
53728
  const activityRange = activityPage && activityPage.maxPage > 0 ? ` (${activityPage.rangeStart}-${activityPage.rangeEnd}/${state.activities.length})` : "";
53481
53729
  const deviceWindow = state.view === "devices" ? deviceListWindow(state, bodyBudget, usable) : void 0;
@@ -53501,46 +53749,118 @@ function SectionTitle({ children, color, tone = COLORS.purpleReadable }) {
53501
53749
  function Blank() {
53502
53750
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { height: 1 });
53503
53751
  }
53504
- function WorkspaceView({ state, bodyBudget, color }) {
53752
+ function ActivityPreviewHeader({ usable, color }) {
53753
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { width: usable, children: [
53754
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Activity" }) }),
53755
+ usable >= 24 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginLeft: 1, flexShrink: 0, children: [
53756
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, color: color ? COLORS.purpleReadable : void 0, children: "A" }),
53757
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: " View all" })
53758
+ ] }) : null
53759
+ ] });
53760
+ }
53761
+ function AccessSectionTitle({ accessProfile, usable, color }) {
53762
+ const lower = adjacentAccessProfile(accessProfile, -1);
53763
+ const higher = adjacentAccessProfile(accessProfile, 1);
53764
+ const key = lower && higher ? "\u2190/\u2192" : lower ? "\u2190" : "\u2192";
53765
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { width: usable, children: [
53766
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Access" }),
53767
+ usable >= 24 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginLeft: 3, flexShrink: 0, children: [
53768
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, color: color ? COLORS.purpleReadable : void 0, children: key }),
53769
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: " Switch" })
53770
+ ] }) : null
53771
+ ] });
53772
+ }
53773
+ function activityLayout(usable) {
53774
+ const toolWidth = Math.min(
53775
+ ACTIVITY_TOOL_COLUMN_WIDTH,
53776
+ Math.max(0, usable - ACTIVITY_STATUS_COLUMN_WIDTH)
53777
+ );
53778
+ return {
53779
+ toolWidth,
53780
+ showSummary: usable >= ACTIVITY_STATUS_COLUMN_WIDTH + toolWidth + 5,
53781
+ showAge: usable >= ACTIVITY_STATUS_COLUMN_WIDTH + toolWidth + ACTIVITY_AGE_COLUMN_WIDTH + 10
53782
+ };
53783
+ }
53784
+ function WorkspaceView({ state, usable, bodyBudget, color, now }) {
53505
53785
  const displayedAccessProfile = state.pendingAccessProfile ?? state.accessProfile;
53786
+ const showConnectionMessage = Boolean(
53787
+ state.message && (state.connection === "retrying" || state.connection === "error")
53788
+ );
53789
+ let fixedLines = 3;
53790
+ if (displayedAccessProfile) fixedLines += 5;
53791
+ if (state.deviceName) fixedLines += 3;
53792
+ if (showConnectionMessage) fixedLines += 2;
53793
+ const activityCapacity = Math.min(3, Math.max(0, bodyBudget - fixedLines - 2));
53794
+ const activityPreview = activityCapacity > 0 ? state.activities.slice(-activityCapacity) : [];
53506
53795
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { height: bodyBudget, flexDirection: "column", flexShrink: 0, overflow: "hidden", children: [
53507
53796
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53508
53797
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Workspace" }),
53509
53798
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.ink : void 0, wrap: "truncate-middle", children: state.workspace }),
53510
53799
  displayedAccessProfile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53511
53800
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53512
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Access" }),
53513
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccessProfileLadder, { accessProfile: displayedAccessProfile, color }),
53514
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, wrap: "truncate", children: accessProfileCapability(displayedAccessProfile) })
53801
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53802
+ AccessSectionTitle,
53803
+ {
53804
+ accessProfile: displayedAccessProfile,
53805
+ usable,
53806
+ color
53807
+ }
53808
+ ),
53809
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, color: color ? COLORS.ink : void 0, wrap: "truncate", children: accessProfileLabel(displayedAccessProfile) }),
53810
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, wrap: "truncate", children: accessProfileCapabilities(displayedAccessProfile).summary }),
53811
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, wrap: "truncate", children: accessProfileCapabilities(displayedAccessProfile).detail })
53515
53812
  ] }) : null,
53516
53813
  state.deviceName ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53517
53814
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53518
53815
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Device" }),
53519
53816
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.ink : void 0, wrap: "truncate", children: state.deviceName })
53520
53817
  ] }) : null,
53521
- state.message && (state.connection === "retrying" || state.connection === "error") ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53818
+ activityCapacity > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53819
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53820
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActivityPreviewHeader, { usable, color }),
53821
+ state.activities.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: "No activity yet." }) : activityPreview.map((activity) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53822
+ ActivityRow,
53823
+ {
53824
+ activity,
53825
+ usable,
53826
+ color,
53827
+ now
53828
+ },
53829
+ activity.requestId
53830
+ ))
53831
+ ] }) : null,
53832
+ showConnectionMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53522
53833
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53523
53834
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.coral : void 0, wrap: "truncate", children: state.message })
53524
53835
  ] }) : null
53525
53836
  ] });
53526
53837
  }
53527
53838
  function ActivityRow({ activity, usable, color, now }) {
53528
- const toolWidth = Math.min(ACTIVITY_TOOL_COLUMN_WIDTH, usable);
53839
+ const { toolWidth, showSummary, showAge } = activityLayout(usable);
53529
53840
  const age = activityAge(activity.updatedAt, activity.state === "working", now);
53530
- const showAge = usable >= toolWidth + age.length + 10;
53531
- const showSummary = usable >= toolWidth + 5;
53841
+ const status = activityStatus(activity);
53532
53842
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { width: usable, height: 1, flexShrink: 0, children: [
53843
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { width: ACTIVITY_STATUS_COLUMN_WIDTH, flexShrink: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, color: color ? status.tone : void 0, children: status.symbol }) }),
53533
53844
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { width: toolWidth, flexShrink: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53534
53845
  Text2,
53535
53846
  {
53536
53847
  bold: true,
53537
- color: color ? activityColor(activity) : void 0,
53848
+ color: color ? COLORS.ink : void 0,
53538
53849
  wrap: "truncate",
53539
53850
  children: activity.tool
53540
53851
  }
53541
53852
  ) }),
53542
53853
  showSummary ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { marginLeft: 2, flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, wrap: "truncate-middle", children: activitySummary(activity) }) }) : null,
53543
- showAge ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { marginLeft: 2, flexShrink: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: age }) }) : null
53854
+ showAge ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53855
+ Box_default,
53856
+ {
53857
+ width: ACTIVITY_AGE_COLUMN_WIDTH,
53858
+ marginLeft: 2,
53859
+ flexShrink: 0,
53860
+ justifyContent: "flex-end",
53861
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: age })
53862
+ }
53863
+ ) : null
53544
53864
  ] });
53545
53865
  }
53546
53866
  function ActivityView({ state, usable, bodyBudget, color, now }) {
@@ -53675,12 +53995,12 @@ function HelpView({ bodyBudget, color }) {
53675
53995
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "W", label: "Workspace", color }),
53676
53996
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "D", label: "Devices", color }),
53677
53997
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "?", label: "Help", color }),
53678
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Esc", label: "Activity", color }),
53998
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Esc", label: "Workspace", color }),
53679
53999
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53680
54000
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, tone: COLORS.coral, children: "Manage" }),
53681
54001
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "\u2190/\u2192", label: "Change workspace access", color, tone: COLORS.coral }),
53682
54002
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Enter/R", label: "Revoke selected device", color, tone: COLORS.coral }),
53683
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "L", label: "Sign out", color, tone: COLORS.coral }),
54003
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "L", label: "Account sign out", color, tone: COLORS.coral }),
53684
54004
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53685
54005
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "App" }),
53686
54006
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Q", label: "Disconnect and quit", color, tone: COLORS.coral }),
@@ -53754,7 +54074,16 @@ function HudScreen({ state, columns, rows, color = true, now = Date.now() }) {
53754
54074
  bodyBudget: metrics.bodyBudget,
53755
54075
  color
53756
54076
  }
53757
- ) : state.view === "workspace" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WorkspaceView, { state, bodyBudget: metrics.bodyBudget, color }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpView, { bodyBudget: metrics.bodyBudget, color }),
54077
+ ) : state.view === "workspace" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
54078
+ WorkspaceView,
54079
+ {
54080
+ state,
54081
+ usable: metrics.usable,
54082
+ bodyBudget: metrics.bodyBudget,
54083
+ color,
54084
+ now
54085
+ }
54086
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpView, { bodyBudget: metrics.bodyBudget, color }),
53758
54087
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Overlay, { state, usable: metrics.usable, color }),
53759
54088
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Footer, { rows: metrics.footerRows, usable: metrics.usable, color })
53760
54089
  ] });
@@ -53815,7 +54144,7 @@ function HudRuntime({ store: store3, actions, signal, stop }) {
53815
54144
  const { columns, rows } = use_window_size_default();
53816
54145
  const [, setClock] = (0, import_react34.useState)(0);
53817
54146
  (0, import_react34.useEffect)(() => {
53818
- if (state.view !== "activity" || state.activities.length === 0) return;
54147
+ if (state.view !== "activity" && state.view !== "workspace" || state.activities.length === 0) return;
53819
54148
  const timer = setInterval(() => setClock((value) => value + 1), ACTIVITY_REFRESH_INTERVAL_MS);
53820
54149
  return () => clearInterval(timer);
53821
54150
  }, [state.view, state.activities.length]);
@@ -53927,7 +54256,7 @@ function HudRuntime({ store: store3, actions, signal, stop }) {
53927
54256
  return;
53928
54257
  }
53929
54258
  if (key.escape) {
53930
- store3.update((state2) => ({ ...state2, view: "activity", notice: void 0 }));
54259
+ store3.update((state2) => ({ ...state2, view: "workspace", notice: void 0 }));
53931
54260
  return;
53932
54261
  }
53933
54262
  if (value === "a") {
@@ -54074,119 +54403,8 @@ async function runSessionHud(actions, input = process.stdin, output = process.st
54074
54403
  var import_semver = __toESM(require_semver2(), 1);
54075
54404
  import { spawn as spawn3 } from "node:child_process";
54076
54405
  import { createHash as createHash2 } from "node:crypto";
54077
- import { chmod as chmod3, readdir as readdir2, rename as rename2, rm as rm4, writeFile as writeFile5 } from "node:fs/promises";
54078
- import path8 from "node:path";
54079
-
54080
- // src/update-lock.ts
54081
- import { randomUUID as randomUUID4 } from "node:crypto";
54082
- import { mkdir as mkdir4, readdir, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
54083
- import path7 from "node:path";
54084
- var UPDATE_SUFFIX = ".update";
54085
- var SESSION_SUFFIX = ".session";
54086
- function updateRuntimeDirectory() {
54087
- return path7.join(configDirectory(), "runtime");
54088
- }
54089
- function processIsAlive(pid) {
54090
- if (!Number.isSafeInteger(pid) || pid <= 0) return false;
54091
- try {
54092
- process.kill(pid, 0);
54093
- return true;
54094
- } catch (error46) {
54095
- return error46.code === "EPERM";
54096
- }
54097
- }
54098
- function leasePid(name, suffix) {
54099
- if (!name.endsWith(suffix)) return null;
54100
- const separator = name.indexOf("-");
54101
- if (separator <= 0) return null;
54102
- const pid = Number(name.slice(0, separator));
54103
- return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
54104
- }
54105
- async function activeLeaseFiles(directory, suffix) {
54106
- let entries;
54107
- try {
54108
- entries = await readdir(directory, { withFileTypes: true });
54109
- } catch (error46) {
54110
- if (error46.code === "ENOENT") return [];
54111
- throw error46;
54112
- }
54113
- const active = [];
54114
- for (const entry of entries) {
54115
- if (!entry.isFile() || !entry.name.endsWith(suffix)) continue;
54116
- const pid = leasePid(entry.name, suffix);
54117
- const file2 = path7.join(directory, entry.name);
54118
- if (pid !== null && processIsAlive(pid)) active.push(file2);
54119
- else await rm3(file2, { force: true });
54120
- }
54121
- return active;
54122
- }
54123
- async function updateLockIsActive(directory) {
54124
- return (await activeLeaseFiles(directory, UPDATE_SUFFIX)).length > 0;
54125
- }
54126
- async function activeSessionFiles(directory) {
54127
- return await activeLeaseFiles(directory, SESSION_SUFFIX);
54128
- }
54129
- async function createUpdateLock(directory) {
54130
- await mkdir4(directory, { recursive: true, mode: 448 });
54131
- const lockFile = path7.join(
54132
- directory,
54133
- `${process.pid}-${randomUUID4()}${UPDATE_SUFFIX}`
54134
- );
54135
- const content = `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
54136
- `;
54137
- await writeFile4(lockFile, content, {
54138
- encoding: "utf8",
54139
- flag: "wx",
54140
- mode: 384
54141
- });
54142
- try {
54143
- const contenders = await activeLeaseFiles(directory, UPDATE_SUFFIX);
54144
- if (contenders.some((file2) => file2 !== lockFile)) {
54145
- throw new Error("Another Glossa update is already running.");
54146
- }
54147
- return lockFile;
54148
- } catch (error46) {
54149
- await rm3(lockFile, { force: true });
54150
- throw error46;
54151
- }
54152
- }
54153
- async function withUpdateLease(action, directory = updateRuntimeDirectory()) {
54154
- const lockFile = await createUpdateLock(directory);
54155
- try {
54156
- if ((await activeSessionFiles(directory)).length > 0) {
54157
- throw new Error("Disconnect every running Glossa workspace before updating.");
54158
- }
54159
- return await action();
54160
- } finally {
54161
- await rm3(lockFile, { force: true });
54162
- }
54163
- }
54164
- async function withWorkspaceLease(action, directory = updateRuntimeDirectory()) {
54165
- await mkdir4(directory, { recursive: true, mode: 448 });
54166
- if (await updateLockIsActive(directory)) {
54167
- throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
54168
- }
54169
- const leaseFile = path7.join(
54170
- directory,
54171
- `${process.pid}-${randomUUID4()}${SESSION_SUFFIX}`
54172
- );
54173
- await writeFile4(
54174
- leaseFile,
54175
- `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
54176
- `,
54177
- { encoding: "utf8", flag: "wx", mode: 384 }
54178
- );
54179
- try {
54180
- if (await updateLockIsActive(directory)) {
54181
- throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
54182
- }
54183
- return await action();
54184
- } finally {
54185
- await rm3(leaseFile, { force: true });
54186
- }
54187
- }
54188
-
54189
- // src/update-service.ts
54406
+ import { chmod as chmod3, readdir as readdir2, rename as rename2, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
54407
+ import path9 from "node:path";
54190
54408
  var PACKAGE_NAME = "@ariobarin/glossa";
54191
54409
  var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org/@ariobarin%2Fglossa";
54192
54410
  var DEFAULT_RELEASE_BASE_URL = "https://github.com/ariobarin/glossa/releases/download";
@@ -54263,8 +54481,8 @@ async function cleanupUpdateBackups(distribution, options = {}) {
54263
54481
  const platform2 = options.platform ?? process.platform;
54264
54482
  if (distribution !== "standalone" || platform2 !== "win32") return;
54265
54483
  const executable = options.executablePath ?? process.execPath;
54266
- const directory = path8.dirname(executable);
54267
- const prefix = `${path8.basename(executable)}.old-`;
54484
+ const directory = path9.dirname(executable);
54485
+ const prefix = `${path9.basename(executable)}.old-`;
54268
54486
  let entries;
54269
54487
  try {
54270
54488
  entries = await readdir2(directory, { withFileTypes: true });
@@ -54277,7 +54495,7 @@ async function cleanupUpdateBackups(distribution, options = {}) {
54277
54495
  const ownerPid = backupOwnerPid(entry.name, prefix);
54278
54496
  return ownerPid !== null && !processIsAlive(ownerPid);
54279
54497
  }).map(async (entry) => {
54280
- await rm4(path8.join(directory, entry.name), { force: true }).catch(() => void 0);
54498
+ await rm5(path9.join(directory, entry.name), { force: true }).catch(() => void 0);
54281
54499
  })
54282
54500
  );
54283
54501
  }
@@ -54353,12 +54571,12 @@ async function installStandaloneUpdate(info, options) {
54353
54571
  if (actual !== expected) {
54354
54572
  throw new Error("Glossa refused to update because the SHA-256 checksum did not match.");
54355
54573
  }
54356
- const download = path8.join(
54357
- path8.dirname(destination),
54358
- `${path8.basename(destination)}.download-${process.pid}`
54574
+ const download = path9.join(
54575
+ path9.dirname(destination),
54576
+ `${path9.basename(destination)}.download-${process.pid}`
54359
54577
  );
54360
54578
  try {
54361
- await writeFile5(download, binary, { mode: 493 });
54579
+ await writeFile6(download, binary, { mode: 493 });
54362
54580
  if (platform2 === "win32") {
54363
54581
  await replaceWindowsExecutable(download, destination);
54364
54582
  } else {
@@ -54367,7 +54585,7 @@ async function installStandaloneUpdate(info, options) {
54367
54585
  }
54368
54586
  return "installed";
54369
54587
  } catch (error46) {
54370
- await rm4(download, { force: true });
54588
+ await rm5(download, { force: true });
54371
54589
  throw error46;
54372
54590
  }
54373
54591
  }
@@ -54381,14 +54599,14 @@ async function installUpdate(info, distribution, options = {}) {
54381
54599
  }
54382
54600
 
54383
54601
  // src/update-state.ts
54384
- import { chmod as chmod4, mkdir as mkdir5, readFile as readFile4, rename as rename3, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
54385
- import path9 from "node:path";
54602
+ import { chmod as chmod4, mkdir as mkdir6, readFile as readFile5, rename as rename3, rm as rm6, writeFile as writeFile7 } from "node:fs/promises";
54603
+ import path10 from "node:path";
54386
54604
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
54387
54605
  function defaultUpdateChannel(version2) {
54388
54606
  return version2.includes("-") ? "beta" : "stable";
54389
54607
  }
54390
54608
  function updateStateFile() {
54391
- return path9.join(configDirectory(), "updates.json");
54609
+ return path10.join(configDirectory(), "updates.json");
54392
54610
  }
54393
54611
  function isPolicy(value) {
54394
54612
  return value === "notify" || value === "auto" || value === "off";
@@ -54405,7 +54623,7 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
54405
54623
  channel: defaultUpdateChannel(currentVersion)
54406
54624
  };
54407
54625
  try {
54408
- const parsed = JSON.parse(await readFile4(file2, "utf8"));
54626
+ const parsed = JSON.parse(await readFile5(file2, "utf8"));
54409
54627
  const lastCheckedAt = optionalString(parsed.lastCheckedAt);
54410
54628
  const latestVersion = optionalString(parsed.latestVersion);
54411
54629
  return {
@@ -54422,11 +54640,11 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
54422
54640
  }
54423
54641
  }
54424
54642
  async function saveUpdateState(state, file2 = updateStateFile()) {
54425
- const directory = path9.dirname(file2);
54643
+ const directory = path10.dirname(file2);
54426
54644
  const temporary = `${file2}.${process.pid}.tmp`;
54427
- await mkdir5(directory, { recursive: true, mode: 448 });
54645
+ await mkdir6(directory, { recursive: true, mode: 448 });
54428
54646
  try {
54429
- await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
54647
+ await writeFile7(temporary, `${JSON.stringify(state, null, 2)}
54430
54648
  `, {
54431
54649
  encoding: "utf8",
54432
54650
  mode: 384
@@ -54434,7 +54652,7 @@ async function saveUpdateState(state, file2 = updateStateFile()) {
54434
54652
  if (process.platform !== "win32") await chmod4(temporary, 384);
54435
54653
  await rename3(temporary, file2);
54436
54654
  } finally {
54437
- await rm5(temporary, { force: true });
54655
+ await rm6(temporary, { force: true });
54438
54656
  }
54439
54657
  }
54440
54658
  async function configureUpdates(currentVersion, changes, file2 = updateStateFile()) {
@@ -54461,15 +54679,31 @@ function isUpdateCheckDue(lastCheckedAt, now = Date.now()) {
54461
54679
  return !Number.isFinite(checkedAt) || now - checkedAt >= UPDATE_CHECK_INTERVAL_MS;
54462
54680
  }
54463
54681
 
54682
+ // src/unpair.ts
54683
+ async function unpairComputer(dependencies = {}) {
54684
+ const load = dependencies.loadDeviceCredential ?? loadDeviceCredential;
54685
+ const remove = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
54686
+ const revoke = dependencies.revokePairedDevice ?? revokePairedDevice;
54687
+ const log = dependencies.log ?? console.log;
54688
+ const device = await load();
54689
+ if (!device) {
54690
+ log("This computer is not paired with Glossa.");
54691
+ return;
54692
+ }
54693
+ await revoke({ relayOrigin: device.relayOrigin }, device);
54694
+ await remove();
54695
+ log("Unpaired this computer from Glossa.");
54696
+ }
54697
+
54464
54698
  // src/usage-store.ts
54465
- import { appendFile, chmod as chmod5, mkdir as mkdir6, readFile as readFile5 } from "node:fs/promises";
54466
- import path10 from "node:path";
54699
+ import { appendFile, chmod as chmod5, mkdir as mkdir7, readFile as readFile6 } from "node:fs/promises";
54700
+ import path11 from "node:path";
54467
54701
  function usageFile() {
54468
- return path10.join(configDirectory(), "usage.jsonl");
54702
+ return path11.join(configDirectory(), "usage.jsonl");
54469
54703
  }
54470
54704
  async function recordUsageEvent(event, file2 = usageFile()) {
54471
- const directory = path10.dirname(file2);
54472
- await mkdir6(directory, { recursive: true, mode: 448 });
54705
+ const directory = path11.dirname(file2);
54706
+ await mkdir7(directory, { recursive: true, mode: 448 });
54473
54707
  await appendFile(file2, `${JSON.stringify(event)}
54474
54708
  `, {
54475
54709
  encoding: "utf8",
@@ -54487,15 +54721,15 @@ async function recordToolUsage(tool, ok, at = /* @__PURE__ */ new Date(), file2
54487
54721
  // src/worker/root-selection.ts
54488
54722
  import { realpath as realpath2 } from "node:fs/promises";
54489
54723
  import os6 from "node:os";
54490
- import path11 from "node:path";
54724
+ import path12 from "node:path";
54491
54725
  function containsPath(root, candidate) {
54492
- const relative = path11.relative(root, candidate);
54493
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path11.sep}`) && !path11.isAbsolute(relative);
54726
+ const relative = path12.relative(root, candidate);
54727
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path12.sep}`) && !path12.isAbsolute(relative);
54494
54728
  }
54495
54729
  async function rejectImplicitHomeAncestor(root) {
54496
54730
  const homes = await Promise.all(
54497
54731
  [os6.homedir(), accountHomeDirectory()].map(
54498
- async (home) => await realpath2(home).catch(() => path11.resolve(home))
54732
+ async (home) => await realpath2(home).catch(() => path12.resolve(home))
54499
54733
  )
54500
54734
  );
54501
54735
  if (homes.some((home) => containsPath(root, home))) {
@@ -54513,10 +54747,10 @@ async function selectExposureRoot(explicitPath, cwd2 = process.cwd()) {
54513
54747
 
54514
54748
  // src/worker/workspace-lease.ts
54515
54749
  import { createHash as createHash3 } from "node:crypto";
54516
- import { lstat as lstat3, mkdir as mkdir7, rm as rm6, stat as stat3 } from "node:fs/promises";
54750
+ import { lstat as lstat3, mkdir as mkdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
54517
54751
  import net from "node:net";
54518
- import path12 from "node:path";
54519
- import { setTimeout as delay3 } from "node:timers/promises";
54752
+ import path13 from "node:path";
54753
+ import { setTimeout as delay5 } from "node:timers/promises";
54520
54754
  var GUARD_STALE_MS = 5e3;
54521
54755
  var GUARD_RETRY_MS = 10;
54522
54756
  var GUARD_TIMEOUT_MS = 1e4;
@@ -54539,12 +54773,12 @@ function workspaceIdentity(root) {
54539
54773
  }
54540
54774
  function defaultLeaseDirectory() {
54541
54775
  if (typeof process.getuid === "function") {
54542
- return path12.join("/tmp", `glossa-workspaces-${process.getuid()}`);
54776
+ return path13.join("/tmp", `glossa-workspaces-${process.getuid()}`);
54543
54777
  }
54544
- return path12.join(accountHomeDirectory(), ".glossa-workspace-leases");
54778
+ return path13.join(accountHomeDirectory(), ".glossa-workspace-leases");
54545
54779
  }
54546
54780
  async function ensureLeaseDirectory(directory) {
54547
- await mkdir7(directory, { recursive: true, mode: 448 });
54781
+ await mkdir8(directory, { recursive: true, mode: 448 });
54548
54782
  const directoryStat = await lstat3(directory);
54549
54783
  if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
54550
54784
  throw new Error("The Glossa workspace lease location is not a private directory.");
@@ -54561,26 +54795,26 @@ function endpointFor(identity, directory) {
54561
54795
  if (process.platform === "win32") {
54562
54796
  return `\\\\.\\pipe\\glossa-workspace-${identity}`;
54563
54797
  }
54564
- return path12.join(directory, `${identity}.sock`);
54798
+ return path13.join(directory, `${identity}.sock`);
54565
54799
  }
54566
- async function acquireGuard(directory, identity) {
54567
- const guard = path12.join(directory, `${identity}.guard`);
54800
+ async function acquireGuard2(directory, identity) {
54801
+ const guard = path13.join(directory, `${identity}.guard`);
54568
54802
  const deadline = Date.now() + GUARD_TIMEOUT_MS;
54569
54803
  while (true) {
54570
54804
  try {
54571
- await mkdir7(guard, { mode: 448 });
54572
- return async () => await rm6(guard, { recursive: true, force: true });
54805
+ await mkdir8(guard, { mode: 448 });
54806
+ return async () => await rm7(guard, { recursive: true, force: true });
54573
54807
  } catch (error46) {
54574
54808
  if (error46.code !== "EEXIST") throw error46;
54575
54809
  let stale = false;
54576
54810
  try {
54577
- stale = Date.now() - (await stat3(guard)).mtimeMs >= GUARD_STALE_MS;
54811
+ stale = Date.now() - (await stat4(guard)).mtimeMs >= GUARD_STALE_MS;
54578
54812
  } catch (statError) {
54579
54813
  if (statError.code === "ENOENT") continue;
54580
54814
  throw statError;
54581
54815
  }
54582
54816
  if (stale) {
54583
- await rm6(guard, { recursive: true, force: true });
54817
+ await rm7(guard, { recursive: true, force: true });
54584
54818
  continue;
54585
54819
  }
54586
54820
  if (Date.now() >= deadline) {
@@ -54588,7 +54822,7 @@ async function acquireGuard(directory, identity) {
54588
54822
  "Timed out while checking whether this workspace is already active."
54589
54823
  );
54590
54824
  }
54591
- await delay3(GUARD_RETRY_MS);
54825
+ await delay5(GUARD_RETRY_MS);
54592
54826
  }
54593
54827
  }
54594
54828
  }
@@ -54636,7 +54870,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54636
54870
  await ensureLeaseDirectory(userDirectory);
54637
54871
  const identity = workspaceIdentity(root);
54638
54872
  const endpoint3 = endpointFor(identity, userDirectory);
54639
- const releaseGuard = await acquireGuard(userDirectory, identity);
54873
+ const releaseGuard = await acquireGuard2(userDirectory, identity);
54640
54874
  let server;
54641
54875
  try {
54642
54876
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -54650,11 +54884,11 @@ async function acquireWorkspaceLease(root, options = {}) {
54650
54884
  if (released) return;
54651
54885
  released = true;
54652
54886
  await ensureLeaseDirectory(userDirectory);
54653
- const releaseGuard2 = await acquireGuard(userDirectory, identity);
54887
+ const releaseGuard2 = await acquireGuard2(userDirectory, identity);
54654
54888
  try {
54655
54889
  await closeServer(server);
54656
54890
  if (process.platform !== "win32") {
54657
- await rm6(endpoint3, { force: true });
54891
+ await rm7(endpoint3, { force: true });
54658
54892
  }
54659
54893
  } finally {
54660
54894
  await releaseGuard2();
@@ -54667,7 +54901,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54667
54901
  throw new WorkspaceAlreadyActiveError();
54668
54902
  }
54669
54903
  if (error46.code !== "EADDRINUSE") throw error46;
54670
- if (process.platform !== "win32") await rm6(endpoint3, { force: true });
54904
+ if (process.platform !== "win32") await rm7(endpoint3, { force: true });
54671
54905
  }
54672
54906
  }
54673
54907
  throw new Error("Could not reserve this workspace for the current Glossa process.");
@@ -54677,7 +54911,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54677
54911
  }
54678
54912
 
54679
54913
  // src/main.ts
54680
- var VERSION = "0.2.0-beta.1";
54914
+ var VERSION = "0.2.0";
54681
54915
  var DISTRIBUTION = "npm";
54682
54916
  var HELP = `Glossa ${VERSION}
54683
54917
 
@@ -54686,6 +54920,7 @@ Usage:
54686
54920
  glossa status
54687
54921
  glossa devices revoke <id>
54688
54922
  glossa logout
54923
+ glossa unpair
54689
54924
  glossa update [--check]
54690
54925
  glossa update --policy <notify|auto|off>
54691
54926
  glossa update --channel <beta|stable>
@@ -54705,8 +54940,8 @@ Keys:
54705
54940
  \u2191\u2193 select or browse
54706
54941
  \u2190\u2192 change workspace access
54707
54942
  Enter/r revoke selected device
54708
- Esc activity
54709
- l sign out
54943
+ Esc workspace
54944
+ l account sign out
54710
54945
  ? help
54711
54946
  q disconnect and quit`;
54712
54947
  async function withLoginSignal(action) {
@@ -54753,8 +54988,8 @@ async function revokeKnownDevice(deviceId) {
54753
54988
  const credentials = await authenticatedCredentials();
54754
54989
  await revokeDevice(loadRelayEndpoints(), credentials, deviceId);
54755
54990
  }
54756
- async function runWorkspaceSession(path13, label, accessProfile) {
54757
- const root = await selectExposureRoot(path13);
54991
+ async function runWorkspaceSession(path14, label, accessProfile) {
54992
+ const root = await selectExposureRoot(path14);
54758
54993
  const lease = await acquireWorkspaceLease(root);
54759
54994
  let usageWrites = Promise.resolve();
54760
54995
  const queueUsage = (write) => {
@@ -54762,8 +54997,7 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54762
54997
  };
54763
54998
  try {
54764
54999
  const endpoints = loadRelayEndpoints();
54765
- let credentials = (await authenticatedSession()).credentials;
54766
- const statusService = new WorkspaceStatusService(credentials, endpoints);
55000
+ const device = await deviceForSession(endpoints);
54767
55001
  let postExitNotice;
54768
55002
  let requestedAccessProfile = accessProfile;
54769
55003
  let activeSessionController;
@@ -54781,7 +55015,7 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54781
55015
  else signal.addEventListener("abort", stopSession, { once: true });
54782
55016
  try {
54783
55017
  await runManagedSession(root, endpoints, {
54784
- credentials,
55018
+ device,
54785
55019
  workerVersion: VERSION,
54786
55020
  accessProfile: sessionAccessProfile,
54787
55021
  ...label ? { workspaceLabel: label } : {},
@@ -54812,10 +55046,13 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54812
55046
  }
54813
55047
  },
54814
55048
  loadStatus: async (signal) => {
54815
- return hudStatus(await statusService.refresh(signal));
55049
+ const credentials = await authenticatedCredentials(signal);
55050
+ return hudStatus(
55051
+ await new WorkspaceStatusService(credentials, endpoints).refresh(signal)
55052
+ );
54816
55053
  },
54817
55054
  revokeDevice: async (deviceId, signal) => {
54818
- credentials = await validCredentials(credentials, { signal });
55055
+ const credentials = await authenticatedCredentials(signal);
54819
55056
  await revokeDevice(
54820
55057
  endpoints,
54821
55058
  credentials,
@@ -54836,9 +55073,9 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54836
55073
  await lease.release();
54837
55074
  }
54838
55075
  }
54839
- async function runWorkspace(path13, label, accessProfile) {
55076
+ async function runWorkspace(path14, label, accessProfile) {
54840
55077
  await withWorkspaceLease(
54841
- async () => await runWorkspaceSession(path13, label, accessProfile)
55078
+ async () => await runWorkspaceSession(path14, label, accessProfile)
54842
55079
  );
54843
55080
  }
54844
55081
  async function refreshUpdateInfo(timeoutMs) {
@@ -54941,6 +55178,8 @@ async function main() {
54941
55178
  await showStatus();
54942
55179
  } else if (invocation.command === "logout") {
54943
55180
  await logoutFromGlossa();
55181
+ } else if (invocation.command === "unpair") {
55182
+ await unpairComputer();
54944
55183
  } else if (invocation.command === "update") {
54945
55184
  await runUpdateCommand(invocation);
54946
55185
  } else {