@ariobarin/glossa 0.2.0-beta.2 → 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 +726 -474
  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 {
40474
+ }
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.");
40484
40503
  }
40485
- throw new Error("Glossa could not enroll this computer.");
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;
@@ -52133,7 +52441,7 @@ var RemoteWorker = class {
52133
52441
  #reconnectBaseMs;
52134
52442
  #reconnectMaxMs;
52135
52443
  #heartbeatMs;
52136
- #workerId = randomUUID3();
52444
+ #workerId = randomUUID5();
52137
52445
  #onStatus;
52138
52446
  constructor(options) {
52139
52447
  this.#origin = new URL(options.origin);
@@ -52177,7 +52485,7 @@ var RemoteWorker = class {
52177
52485
  if (error46 instanceof DeviceRejectedError || error46 instanceof RelayAccessProfileUnsupportedError) {
52178
52486
  throw error46;
52179
52487
  }
52180
- const delay4 = reconnectDelayMs(
52488
+ const delay6 = reconnectDelayMs(
52181
52489
  failures,
52182
52490
  this.#random,
52183
52491
  this.#reconnectBaseMs,
@@ -52187,10 +52495,10 @@ var RemoteWorker = class {
52187
52495
  this.#onStatus({
52188
52496
  state: "retrying",
52189
52497
  error: error46 instanceof Error ? error46 : new Error(String(error46)),
52190
- retryInMs: delay4
52498
+ retryInMs: delay6
52191
52499
  });
52192
52500
  try {
52193
- await this.#sleep(delay4, this.#signal);
52501
+ await this.#sleep(delay6, this.#signal);
52194
52502
  } catch (sleepError) {
52195
52503
  if (this.#signal.aborted) return;
52196
52504
  throw sleepError;
@@ -52269,7 +52577,7 @@ var RemoteWorker = class {
52269
52577
  const key = JSON.stringify(body);
52270
52578
  if (seenAttempts.has(key)) return;
52271
52579
  seenAttempts.add(key);
52272
- attempts.push({ body, legacyRelay, profileIncluded: includeProfile });
52580
+ attempts.push({ body, legacyRelay });
52273
52581
  };
52274
52582
  if (this.#accessProfile) {
52275
52583
  for (const metadata of metadataPhases) {
@@ -52294,7 +52602,7 @@ var RemoteWorker = class {
52294
52602
  );
52295
52603
  }
52296
52604
  }
52297
- attempts.push({ body: {}, legacyRelay: true, profileIncluded: false });
52605
+ attempts.push({ body: {}, legacyRelay: true });
52298
52606
  }
52299
52607
  for (const attempt of attempts) {
52300
52608
  let response;
@@ -52314,17 +52622,22 @@ var RemoteWorker = class {
52314
52622
  throw new Error("The relay returned an invalid registration response.");
52315
52623
  }
52316
52624
  const workerToken = "workerToken" in value ? optionalWorkerToken(value.workerToken) : void 0;
52317
- return {
52625
+ const accessProfileAccepted = this.#accessProfile === void 0 || this.#accessProfile === "system" || "accessProfile" in value && value.accessProfile === this.#accessProfile;
52626
+ const session = {
52318
52627
  generation: value.generation,
52319
52628
  legacyRelay: attempt.legacyRelay,
52320
52629
  concurrentJobs: !attempt.legacyRelay && supportsCapability(value, "concurrentJobs"),
52321
52630
  structuredReads: !attempt.legacyRelay && supportsCapability(value, "structuredReads"),
52322
52631
  structuredMutations: !attempt.legacyRelay && supportsCapability(value, "structuredMutations"),
52323
52632
  commandOutputRanges: !attempt.legacyRelay && supportsCapability(value, "commandOutputRanges"),
52324
- accessProfileAccepted: this.#accessProfile === void 0 || "accessProfile" in value && value.accessProfile === this.#accessProfile || this.#accessProfile === "system" && !attempt.profileIncluded,
52633
+ accessProfileAccepted,
52325
52634
  workspaceLabelAccepted: this.#workspaceLabel === void 0 || "workspaceLabel" in value && value.workspaceLabel === this.#workspaceLabel,
52326
52635
  ...workerToken ? { workerToken } : {}
52327
52636
  };
52637
+ if (!accessProfileAccepted && this.#accessProfile) {
52638
+ throw new RelayAccessProfileUnsupportedError(this.#accessProfile);
52639
+ }
52640
+ return session;
52328
52641
  }
52329
52642
  if (this.#accessProfile && this.#accessProfile !== "system") {
52330
52643
  throw new RelayAccessProfileUnsupportedError(this.#accessProfile);
@@ -52567,10 +52880,10 @@ var RemoteWorker = class {
52567
52880
  await unregister(`Device ${this.#deviceToken}`);
52568
52881
  }
52569
52882
  }
52570
- async #post(path13, body, workerToken) {
52883
+ async #post(path14, body, workerToken) {
52571
52884
  const timeout = AbortSignal.timeout(WORKER_REQUEST_TIMEOUT_MS);
52572
52885
  const signal = AbortSignal.any([this.#signal, timeout]);
52573
- const response = await this.#fetcher(new URL(path13, this.#origin), {
52886
+ const response = await this.#fetcher(new URL(path14, this.#origin), {
52574
52887
  method: "POST",
52575
52888
  headers: {
52576
52889
  authorization: workerToken ? `Worker ${workerToken}` : `Device ${this.#deviceToken}`,
@@ -52691,56 +53004,27 @@ function visibleWorker(worker, options) {
52691
53004
  }
52692
53005
  async function deviceForSession(endpoints, dependencies = {}, signal) {
52693
53006
  const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
52694
- const loadLogin = dependencies.loadCredentials ?? loadCredentials;
52695
- const validate2 = dependencies.validCredentials ?? validCredentials;
52696
- const subjectFor = dependencies.accessTokenSubject ?? accessTokenSubject;
52697
53007
  const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
52698
- const enroll = dependencies.enrollDevice ?? enrollDevice;
52699
53008
  const saveDevice = dependencies.saveDeviceCredential ?? saveDeviceCredential;
52700
- const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
52701
- const name = dependencies.defaultDeviceName ?? defaultDeviceName;
52702
- const baseFetch = dependencies.fetch ?? fetch;
52703
- 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;
52704
53012
  signal?.throwIfAborted();
52705
53013
  const stored = await loadDevice();
52706
- let credentials = dependencies.credentials;
52707
- const currentCredentials = async () => {
52708
- if (credentials) return credentials;
52709
- const loaded = await loadLogin();
52710
- if (!loaded) throw new Error("Not signed in. Run Glossa again to sign in.");
52711
- credentials = await validate2(loaded.credentials, { fetch: fetchRequest });
52712
- return credentials;
52713
- };
52714
- if (stored?.relayOrigin === endpoints.relayOrigin) {
52715
- const current2 = await currentCredentials();
52716
- const accountSubject = subjectFor(current2);
52717
- if (stored.accountSubject === accountSubject) return stored;
52718
- if (stored.accountSubject === void 0 && await ownsDevice(endpoints, current2, stored.deviceId, fetchRequest)) {
52719
- const migrated = { ...stored, accountSubject };
52720
- await saveDevice(migrated);
52721
- return migrated;
52722
- }
52723
- await removeDevice();
52724
- }
52725
- signal?.throwIfAborted();
52726
- const current = await currentCredentials();
52727
- const enrolled = await enroll(
52728
- endpoints,
52729
- current,
52730
- name(),
52731
- fetchRequest
52732
- );
52733
- const bound = {
52734
- ...enrolled,
52735
- accountSubject: subjectFor(current)
52736
- };
52737
- await saveDevice(bound);
52738
- return bound;
52739
- }
52740
- async function reenrollRejectedDevice(endpoints, dependencies = {}, signal) {
52741
- const remove = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
52742
- await remove();
52743
- 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);
52744
53028
  }
52745
53029
  function retryMessage(retryInMs) {
52746
53030
  const seconds = Math.max(1, Math.ceil(retryInMs / 1e3));
@@ -52852,9 +53136,6 @@ async function connectRemoteWorker(endpoints, device, worker, options, signal, o
52852
53136
  await connectHintTask;
52853
53137
  }
52854
53138
  }
52855
- function shouldRecoverRejectedDevice(error46, recoveredRejectedDevice, connected) {
52856
- return error46 instanceof DeviceRejectedError && !recoveredRejectedDevice && !connected;
52857
- }
52858
53139
  async function runManagedSession(root, endpoints, options = {}) {
52859
53140
  const controller = new AbortController();
52860
53141
  const stop = () => controller.abort();
@@ -52869,9 +53150,9 @@ async function runManagedSession(root, endpoints, options = {}) {
52869
53150
  try {
52870
53151
  const accessProfile = options.accessProfile ?? DEFAULT_WORKER_ACCESS_PROFILE;
52871
53152
  const sessionOptions = { ...options, accessProfile };
52872
- let device = await deviceForSession(
53153
+ const device = options.device ?? await deviceForSession(
52873
53154
  endpoints,
52874
- options.credentials ? { credentials: options.credentials } : {},
53155
+ {},
52875
53156
  controller.signal
52876
53157
  );
52877
53158
  controller.signal.throwIfAborted();
@@ -52892,41 +53173,18 @@ async function runManagedSession(root, endpoints, options = {}) {
52892
53173
  console.error(accessProfileSummary(accessProfile));
52893
53174
  console.error("Press Ctrl+C to disconnect.");
52894
53175
  }
52895
- let recoveredRejectedDevice = false;
52896
- while (!controller.signal.aborted) {
52897
- let connected = false;
52898
- try {
52899
- await connectRemoteWorker(
52900
- endpoints,
52901
- device,
52902
- worker,
52903
- sessionOptions,
52904
- controller.signal,
52905
- () => {
52906
- connected = true;
52907
- }
52908
- );
52909
- break;
52910
- } catch (error46) {
52911
- if (!shouldRecoverRejectedDevice(
52912
- error46,
52913
- recoveredRejectedDevice,
52914
- connected
52915
- )) {
52916
- throw error46;
52917
- }
52918
- recoveredRejectedDevice = true;
52919
- device = await reenrollRejectedDevice(
52920
- endpoints,
52921
- options.credentials ? { credentials: options.credentials } : {},
52922
- controller.signal
52923
- );
52924
- }
52925
- }
53176
+ await connectRemoteWorker(
53177
+ endpoints,
53178
+ device,
53179
+ worker,
53180
+ sessionOptions,
53181
+ controller.signal,
53182
+ () => void 0
53183
+ );
52926
53184
  } catch (error46) {
52927
53185
  if (error46 instanceof DeviceRejectedError) {
52928
53186
  await deleteDeviceCredential();
52929
- 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.");
52930
53188
  }
52931
53189
  throw error46;
52932
53190
  } finally {
@@ -52951,7 +53209,7 @@ function initialHudState(workspace) {
52951
53209
  message: void 0,
52952
53210
  activities: [],
52953
53211
  activityPage: 0,
52954
- view: "activity",
53212
+ view: "workspace",
52955
53213
  status: void 0,
52956
53214
  deviceSelection: 0,
52957
53215
  pendingAccessProfile: void 0,
@@ -53022,12 +53280,12 @@ function formatByteCount(value) {
53022
53280
  }
53023
53281
  return `${(kibibytes / 1024).toFixed(1)} MiB`;
53024
53282
  }
53025
- function workspacePath(path13) {
53026
- return path13 || ".";
53283
+ function workspacePath(path14) {
53284
+ return path14 || ".";
53027
53285
  }
53028
- function pathSummary(path13, details = []) {
53286
+ function pathSummary(path14, details = []) {
53029
53287
  return {
53030
- target: `path ${quoteActivityInput(path13)}`,
53288
+ target: `path ${quoteActivityInput(path14)}`,
53031
53289
  details,
53032
53290
  truncation: "middle"
53033
53291
  };
@@ -53226,6 +53484,7 @@ var COLORS = {
53226
53484
  muted: "#aaa4b5",
53227
53485
  purple: "#8054ff",
53228
53486
  purpleReadable: "#ad98ff",
53487
+ success: "#65d6a6",
53229
53488
  coral: "#ff665f",
53230
53489
  line: "#5c556e"
53231
53490
  };
@@ -53233,7 +53492,9 @@ function Text2({ color, ...props }) {
53233
53492
  return color === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { ...props }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { ...props, color });
53234
53493
  }
53235
53494
  var ACTIVITY_REFRESH_INTERVAL_MS = 1e4;
53495
+ var ACTIVITY_STATUS_COLUMN_WIDTH = 2;
53236
53496
  var ACTIVITY_TOOL_COLUMN_WIDTH = 15;
53497
+ var ACTIVITY_AGE_COLUMN_WIDTH = 10;
53237
53498
  var ACTIVITY_PREAMBLE_LINES = 1;
53238
53499
  var HudStore = class {
53239
53500
  #state;
@@ -53279,10 +53540,10 @@ function activityAge(updatedAt, working, now) {
53279
53540
  if (elapsedMs < 864e5) return `${Math.floor(elapsedMs / 36e5)}h ago`;
53280
53541
  return `${Math.floor(elapsedMs / 864e5)}d ago`;
53281
53542
  }
53282
- function activityColor(activity) {
53283
- if (activity.state === "working") return COLORS.ink;
53284
- if (activity.state === "failed") return COLORS.coral;
53285
- 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 };
53286
53547
  }
53287
53548
  function activitySummary(activity) {
53288
53549
  return [activity.summary.target, ...activity.summary.details].join(" \xB7 ");
@@ -53298,31 +53559,28 @@ function adjacentAccessProfile(accessProfile, direction) {
53298
53559
  const index = ACCESS_PROFILES.indexOf(accessProfile);
53299
53560
  return ACCESS_PROFILES[index + direction];
53300
53561
  }
53301
- function AccessProfileLadder({ accessProfile, color }) {
53302
- 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: [
53303
- index > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.line : void 0, children: " \u2500 " }) : null,
53304
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53305
- Text2,
53306
- {
53307
- color: color ? profile === accessProfile ? COLORS.coral : COLORS.muted : void 0,
53308
- children: profile === accessProfile ? `[ ${accessProfileLabel(profile)} ]` : accessProfileLabel(profile)
53309
- }
53310
- )
53311
- ] }, profile)) });
53312
- }
53313
- function accessProfileCapability(accessProfile) {
53314
- if (accessProfile === "read-only") return "Read files only \xB7 no changes \xB7 no commands";
53315
- if (accessProfile === "workspace") return "Read + write files \xB7 commands disabled";
53316
- return "Read + write files + commands \xB7 inherits this OS account's permissions";
53317
- }
53318
- var PRIMARY_FOOTER_HINTS = [
53319
- { key: "A", label: "Activity" },
53320
- { key: "W", label: "Workspace" },
53321
- { key: "D", label: "Devices" },
53322
- { key: "?", label: "Help" },
53323
- { key: "L", label: "Sign out", tone: COLORS.coral },
53324
- { key: "Q", label: "Quit", tone: COLORS.coral }
53325
- ];
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
+ }
53326
53584
  function contextualFooterHints(state) {
53327
53585
  if (state.view === "activity") {
53328
53586
  return [
@@ -53370,7 +53628,7 @@ function splitFooterHints(hints, usable) {
53370
53628
  return rows;
53371
53629
  }
53372
53630
  function buildFooterRows(state, usable) {
53373
- const rows = splitFooterHints(PRIMARY_FOOTER_HINTS, usable).map((left) => ({
53631
+ const rows = splitFooterHints(primaryFooterHints(), usable).map((left) => ({
53374
53632
  left,
53375
53633
  right: []
53376
53634
  }));
@@ -53391,7 +53649,10 @@ function promptText(state) {
53391
53649
  if (state.busy) return { message: "Working\u2026" };
53392
53650
  if (!state.prompt) return void 0;
53393
53651
  if (state.prompt.type === "logout") {
53394
- 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
+ };
53395
53656
  }
53396
53657
  if (state.prompt.type === "access-confirm") {
53397
53658
  const detail = state.prompt.accessProfile === "system" ? "Commands will inherit this OS account's permissions." : "This allows guarded file writes.";
@@ -53462,7 +53723,7 @@ function Line({ usable, color }) {
53462
53723
  }
53463
53724
  function Header({ state, usable, bodyBudget, color }) {
53464
53725
  const statusColor = state.connection === "error" ? COLORS.coral : COLORS.purpleReadable;
53465
- 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;
53466
53727
  const activityPage = state.view === "activity" ? activityPageInfo(state, bodyBudget) : void 0;
53467
53728
  const activityRange = activityPage && activityPage.maxPage > 0 ? ` (${activityPage.rangeStart}-${activityPage.rangeEnd}/${state.activities.length})` : "";
53468
53729
  const deviceWindow = state.view === "devices" ? deviceListWindow(state, bodyBudget, usable) : void 0;
@@ -53488,46 +53749,118 @@ function SectionTitle({ children, color, tone = COLORS.purpleReadable }) {
53488
53749
  function Blank() {
53489
53750
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { height: 1 });
53490
53751
  }
53491
- 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 }) {
53492
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) : [];
53493
53795
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { height: bodyBudget, flexDirection: "column", flexShrink: 0, overflow: "hidden", children: [
53494
53796
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53495
53797
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Workspace" }),
53496
53798
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.ink : void 0, wrap: "truncate-middle", children: state.workspace }),
53497
53799
  displayedAccessProfile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53498
53800
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53499
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Access" }),
53500
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AccessProfileLadder, { accessProfile: displayedAccessProfile, color }),
53501
- /* @__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 })
53502
53812
  ] }) : null,
53503
53813
  state.deviceName ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53504
53814
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53505
53815
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Device" }),
53506
53816
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.ink : void 0, wrap: "truncate", children: state.deviceName })
53507
53817
  ] }) : null,
53508
- 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: [
53509
53833
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53510
53834
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.coral : void 0, wrap: "truncate", children: state.message })
53511
53835
  ] }) : null
53512
53836
  ] });
53513
53837
  }
53514
53838
  function ActivityRow({ activity, usable, color, now }) {
53515
- const toolWidth = Math.min(ACTIVITY_TOOL_COLUMN_WIDTH, usable);
53839
+ const { toolWidth, showSummary, showAge } = activityLayout(usable);
53516
53840
  const age = activityAge(activity.updatedAt, activity.state === "working", now);
53517
- const showAge = usable >= toolWidth + age.length + 10;
53518
- const showSummary = usable >= toolWidth + 5;
53841
+ const status = activityStatus(activity);
53519
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 }) }),
53520
53844
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { width: toolWidth, flexShrink: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53521
53845
  Text2,
53522
53846
  {
53523
53847
  bold: true,
53524
- color: color ? activityColor(activity) : void 0,
53848
+ color: color ? COLORS.ink : void 0,
53525
53849
  wrap: "truncate",
53526
53850
  children: activity.tool
53527
53851
  }
53528
53852
  ) }),
53529
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,
53530
- 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
53531
53864
  ] });
53532
53865
  }
53533
53866
  function ActivityView({ state, usable, bodyBudget, color, now }) {
@@ -53662,12 +53995,12 @@ function HelpView({ bodyBudget, color }) {
53662
53995
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "W", label: "Workspace", color }),
53663
53996
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "D", label: "Devices", color }),
53664
53997
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "?", label: "Help", color }),
53665
- /* @__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 }),
53666
53999
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53667
54000
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, tone: COLORS.coral, children: "Manage" }),
53668
54001
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "\u2190/\u2192", label: "Change workspace access", color, tone: COLORS.coral }),
53669
54002
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Enter/R", label: "Revoke selected device", color, tone: COLORS.coral }),
53670
- /* @__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 }),
53671
54004
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53672
54005
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "App" }),
53673
54006
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Q", label: "Disconnect and quit", color, tone: COLORS.coral }),
@@ -53741,7 +54074,16 @@ function HudScreen({ state, columns, rows, color = true, now = Date.now() }) {
53741
54074
  bodyBudget: metrics.bodyBudget,
53742
54075
  color
53743
54076
  }
53744
- ) : 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 }),
53745
54087
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Overlay, { state, usable: metrics.usable, color }),
53746
54088
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Footer, { rows: metrics.footerRows, usable: metrics.usable, color })
53747
54089
  ] });
@@ -53802,7 +54144,7 @@ function HudRuntime({ store: store3, actions, signal, stop }) {
53802
54144
  const { columns, rows } = use_window_size_default();
53803
54145
  const [, setClock] = (0, import_react34.useState)(0);
53804
54146
  (0, import_react34.useEffect)(() => {
53805
- if (state.view !== "activity" || state.activities.length === 0) return;
54147
+ if (state.view !== "activity" && state.view !== "workspace" || state.activities.length === 0) return;
53806
54148
  const timer = setInterval(() => setClock((value) => value + 1), ACTIVITY_REFRESH_INTERVAL_MS);
53807
54149
  return () => clearInterval(timer);
53808
54150
  }, [state.view, state.activities.length]);
@@ -53914,7 +54256,7 @@ function HudRuntime({ store: store3, actions, signal, stop }) {
53914
54256
  return;
53915
54257
  }
53916
54258
  if (key.escape) {
53917
- store3.update((state2) => ({ ...state2, view: "activity", notice: void 0 }));
54259
+ store3.update((state2) => ({ ...state2, view: "workspace", notice: void 0 }));
53918
54260
  return;
53919
54261
  }
53920
54262
  if (value === "a") {
@@ -54061,119 +54403,8 @@ async function runSessionHud(actions, input = process.stdin, output = process.st
54061
54403
  var import_semver = __toESM(require_semver2(), 1);
54062
54404
  import { spawn as spawn3 } from "node:child_process";
54063
54405
  import { createHash as createHash2 } from "node:crypto";
54064
- import { chmod as chmod3, readdir as readdir2, rename as rename2, rm as rm4, writeFile as writeFile5 } from "node:fs/promises";
54065
- import path8 from "node:path";
54066
-
54067
- // src/update-lock.ts
54068
- import { randomUUID as randomUUID4 } from "node:crypto";
54069
- import { mkdir as mkdir4, readdir, rm as rm3, writeFile as writeFile4 } from "node:fs/promises";
54070
- import path7 from "node:path";
54071
- var UPDATE_SUFFIX = ".update";
54072
- var SESSION_SUFFIX = ".session";
54073
- function updateRuntimeDirectory() {
54074
- return path7.join(configDirectory(), "runtime");
54075
- }
54076
- function processIsAlive(pid) {
54077
- if (!Number.isSafeInteger(pid) || pid <= 0) return false;
54078
- try {
54079
- process.kill(pid, 0);
54080
- return true;
54081
- } catch (error46) {
54082
- return error46.code === "EPERM";
54083
- }
54084
- }
54085
- function leasePid(name, suffix) {
54086
- if (!name.endsWith(suffix)) return null;
54087
- const separator = name.indexOf("-");
54088
- if (separator <= 0) return null;
54089
- const pid = Number(name.slice(0, separator));
54090
- return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
54091
- }
54092
- async function activeLeaseFiles(directory, suffix) {
54093
- let entries;
54094
- try {
54095
- entries = await readdir(directory, { withFileTypes: true });
54096
- } catch (error46) {
54097
- if (error46.code === "ENOENT") return [];
54098
- throw error46;
54099
- }
54100
- const active = [];
54101
- for (const entry of entries) {
54102
- if (!entry.isFile() || !entry.name.endsWith(suffix)) continue;
54103
- const pid = leasePid(entry.name, suffix);
54104
- const file2 = path7.join(directory, entry.name);
54105
- if (pid !== null && processIsAlive(pid)) active.push(file2);
54106
- else await rm3(file2, { force: true });
54107
- }
54108
- return active;
54109
- }
54110
- async function updateLockIsActive(directory) {
54111
- return (await activeLeaseFiles(directory, UPDATE_SUFFIX)).length > 0;
54112
- }
54113
- async function activeSessionFiles(directory) {
54114
- return await activeLeaseFiles(directory, SESSION_SUFFIX);
54115
- }
54116
- async function createUpdateLock(directory) {
54117
- await mkdir4(directory, { recursive: true, mode: 448 });
54118
- const lockFile = path7.join(
54119
- directory,
54120
- `${process.pid}-${randomUUID4()}${UPDATE_SUFFIX}`
54121
- );
54122
- const content = `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
54123
- `;
54124
- await writeFile4(lockFile, content, {
54125
- encoding: "utf8",
54126
- flag: "wx",
54127
- mode: 384
54128
- });
54129
- try {
54130
- const contenders = await activeLeaseFiles(directory, UPDATE_SUFFIX);
54131
- if (contenders.some((file2) => file2 !== lockFile)) {
54132
- throw new Error("Another Glossa update is already running.");
54133
- }
54134
- return lockFile;
54135
- } catch (error46) {
54136
- await rm3(lockFile, { force: true });
54137
- throw error46;
54138
- }
54139
- }
54140
- async function withUpdateLease(action, directory = updateRuntimeDirectory()) {
54141
- const lockFile = await createUpdateLock(directory);
54142
- try {
54143
- if ((await activeSessionFiles(directory)).length > 0) {
54144
- throw new Error("Disconnect every running Glossa workspace before updating.");
54145
- }
54146
- return await action();
54147
- } finally {
54148
- await rm3(lockFile, { force: true });
54149
- }
54150
- }
54151
- async function withWorkspaceLease(action, directory = updateRuntimeDirectory()) {
54152
- await mkdir4(directory, { recursive: true, mode: 448 });
54153
- if (await updateLockIsActive(directory)) {
54154
- throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
54155
- }
54156
- const leaseFile = path7.join(
54157
- directory,
54158
- `${process.pid}-${randomUUID4()}${SESSION_SUFFIX}`
54159
- );
54160
- await writeFile4(
54161
- leaseFile,
54162
- `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
54163
- `,
54164
- { encoding: "utf8", flag: "wx", mode: 384 }
54165
- );
54166
- try {
54167
- if (await updateLockIsActive(directory)) {
54168
- throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
54169
- }
54170
- return await action();
54171
- } finally {
54172
- await rm3(leaseFile, { force: true });
54173
- }
54174
- }
54175
-
54176
- // 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";
54177
54408
  var PACKAGE_NAME = "@ariobarin/glossa";
54178
54409
  var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org/@ariobarin%2Fglossa";
54179
54410
  var DEFAULT_RELEASE_BASE_URL = "https://github.com/ariobarin/glossa/releases/download";
@@ -54250,8 +54481,8 @@ async function cleanupUpdateBackups(distribution, options = {}) {
54250
54481
  const platform2 = options.platform ?? process.platform;
54251
54482
  if (distribution !== "standalone" || platform2 !== "win32") return;
54252
54483
  const executable = options.executablePath ?? process.execPath;
54253
- const directory = path8.dirname(executable);
54254
- const prefix = `${path8.basename(executable)}.old-`;
54484
+ const directory = path9.dirname(executable);
54485
+ const prefix = `${path9.basename(executable)}.old-`;
54255
54486
  let entries;
54256
54487
  try {
54257
54488
  entries = await readdir2(directory, { withFileTypes: true });
@@ -54264,7 +54495,7 @@ async function cleanupUpdateBackups(distribution, options = {}) {
54264
54495
  const ownerPid = backupOwnerPid(entry.name, prefix);
54265
54496
  return ownerPid !== null && !processIsAlive(ownerPid);
54266
54497
  }).map(async (entry) => {
54267
- 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);
54268
54499
  })
54269
54500
  );
54270
54501
  }
@@ -54340,12 +54571,12 @@ async function installStandaloneUpdate(info, options) {
54340
54571
  if (actual !== expected) {
54341
54572
  throw new Error("Glossa refused to update because the SHA-256 checksum did not match.");
54342
54573
  }
54343
- const download = path8.join(
54344
- path8.dirname(destination),
54345
- `${path8.basename(destination)}.download-${process.pid}`
54574
+ const download = path9.join(
54575
+ path9.dirname(destination),
54576
+ `${path9.basename(destination)}.download-${process.pid}`
54346
54577
  );
54347
54578
  try {
54348
- await writeFile5(download, binary, { mode: 493 });
54579
+ await writeFile6(download, binary, { mode: 493 });
54349
54580
  if (platform2 === "win32") {
54350
54581
  await replaceWindowsExecutable(download, destination);
54351
54582
  } else {
@@ -54354,7 +54585,7 @@ async function installStandaloneUpdate(info, options) {
54354
54585
  }
54355
54586
  return "installed";
54356
54587
  } catch (error46) {
54357
- await rm4(download, { force: true });
54588
+ await rm5(download, { force: true });
54358
54589
  throw error46;
54359
54590
  }
54360
54591
  }
@@ -54368,14 +54599,14 @@ async function installUpdate(info, distribution, options = {}) {
54368
54599
  }
54369
54600
 
54370
54601
  // src/update-state.ts
54371
- import { chmod as chmod4, mkdir as mkdir5, readFile as readFile4, rename as rename3, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
54372
- 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";
54373
54604
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
54374
54605
  function defaultUpdateChannel(version2) {
54375
54606
  return version2.includes("-") ? "beta" : "stable";
54376
54607
  }
54377
54608
  function updateStateFile() {
54378
- return path9.join(configDirectory(), "updates.json");
54609
+ return path10.join(configDirectory(), "updates.json");
54379
54610
  }
54380
54611
  function isPolicy(value) {
54381
54612
  return value === "notify" || value === "auto" || value === "off";
@@ -54392,7 +54623,7 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
54392
54623
  channel: defaultUpdateChannel(currentVersion)
54393
54624
  };
54394
54625
  try {
54395
- const parsed = JSON.parse(await readFile4(file2, "utf8"));
54626
+ const parsed = JSON.parse(await readFile5(file2, "utf8"));
54396
54627
  const lastCheckedAt = optionalString(parsed.lastCheckedAt);
54397
54628
  const latestVersion = optionalString(parsed.latestVersion);
54398
54629
  return {
@@ -54409,11 +54640,11 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
54409
54640
  }
54410
54641
  }
54411
54642
  async function saveUpdateState(state, file2 = updateStateFile()) {
54412
- const directory = path9.dirname(file2);
54643
+ const directory = path10.dirname(file2);
54413
54644
  const temporary = `${file2}.${process.pid}.tmp`;
54414
- await mkdir5(directory, { recursive: true, mode: 448 });
54645
+ await mkdir6(directory, { recursive: true, mode: 448 });
54415
54646
  try {
54416
- await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
54647
+ await writeFile7(temporary, `${JSON.stringify(state, null, 2)}
54417
54648
  `, {
54418
54649
  encoding: "utf8",
54419
54650
  mode: 384
@@ -54421,7 +54652,7 @@ async function saveUpdateState(state, file2 = updateStateFile()) {
54421
54652
  if (process.platform !== "win32") await chmod4(temporary, 384);
54422
54653
  await rename3(temporary, file2);
54423
54654
  } finally {
54424
- await rm5(temporary, { force: true });
54655
+ await rm6(temporary, { force: true });
54425
54656
  }
54426
54657
  }
54427
54658
  async function configureUpdates(currentVersion, changes, file2 = updateStateFile()) {
@@ -54448,15 +54679,31 @@ function isUpdateCheckDue(lastCheckedAt, now = Date.now()) {
54448
54679
  return !Number.isFinite(checkedAt) || now - checkedAt >= UPDATE_CHECK_INTERVAL_MS;
54449
54680
  }
54450
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
+
54451
54698
  // src/usage-store.ts
54452
- import { appendFile, chmod as chmod5, mkdir as mkdir6, readFile as readFile5 } from "node:fs/promises";
54453
- 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";
54454
54701
  function usageFile() {
54455
- return path10.join(configDirectory(), "usage.jsonl");
54702
+ return path11.join(configDirectory(), "usage.jsonl");
54456
54703
  }
54457
54704
  async function recordUsageEvent(event, file2 = usageFile()) {
54458
- const directory = path10.dirname(file2);
54459
- await mkdir6(directory, { recursive: true, mode: 448 });
54705
+ const directory = path11.dirname(file2);
54706
+ await mkdir7(directory, { recursive: true, mode: 448 });
54460
54707
  await appendFile(file2, `${JSON.stringify(event)}
54461
54708
  `, {
54462
54709
  encoding: "utf8",
@@ -54474,15 +54721,15 @@ async function recordToolUsage(tool, ok, at = /* @__PURE__ */ new Date(), file2
54474
54721
  // src/worker/root-selection.ts
54475
54722
  import { realpath as realpath2 } from "node:fs/promises";
54476
54723
  import os6 from "node:os";
54477
- import path11 from "node:path";
54724
+ import path12 from "node:path";
54478
54725
  function containsPath(root, candidate) {
54479
- const relative = path11.relative(root, candidate);
54480
- 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);
54481
54728
  }
54482
54729
  async function rejectImplicitHomeAncestor(root) {
54483
54730
  const homes = await Promise.all(
54484
54731
  [os6.homedir(), accountHomeDirectory()].map(
54485
- async (home) => await realpath2(home).catch(() => path11.resolve(home))
54732
+ async (home) => await realpath2(home).catch(() => path12.resolve(home))
54486
54733
  )
54487
54734
  );
54488
54735
  if (homes.some((home) => containsPath(root, home))) {
@@ -54500,10 +54747,10 @@ async function selectExposureRoot(explicitPath, cwd2 = process.cwd()) {
54500
54747
 
54501
54748
  // src/worker/workspace-lease.ts
54502
54749
  import { createHash as createHash3 } from "node:crypto";
54503
- 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";
54504
54751
  import net from "node:net";
54505
- import path12 from "node:path";
54506
- import { setTimeout as delay3 } from "node:timers/promises";
54752
+ import path13 from "node:path";
54753
+ import { setTimeout as delay5 } from "node:timers/promises";
54507
54754
  var GUARD_STALE_MS = 5e3;
54508
54755
  var GUARD_RETRY_MS = 10;
54509
54756
  var GUARD_TIMEOUT_MS = 1e4;
@@ -54526,12 +54773,12 @@ function workspaceIdentity(root) {
54526
54773
  }
54527
54774
  function defaultLeaseDirectory() {
54528
54775
  if (typeof process.getuid === "function") {
54529
- return path12.join("/tmp", `glossa-workspaces-${process.getuid()}`);
54776
+ return path13.join("/tmp", `glossa-workspaces-${process.getuid()}`);
54530
54777
  }
54531
- return path12.join(accountHomeDirectory(), ".glossa-workspace-leases");
54778
+ return path13.join(accountHomeDirectory(), ".glossa-workspace-leases");
54532
54779
  }
54533
54780
  async function ensureLeaseDirectory(directory) {
54534
- await mkdir7(directory, { recursive: true, mode: 448 });
54781
+ await mkdir8(directory, { recursive: true, mode: 448 });
54535
54782
  const directoryStat = await lstat3(directory);
54536
54783
  if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
54537
54784
  throw new Error("The Glossa workspace lease location is not a private directory.");
@@ -54548,26 +54795,26 @@ function endpointFor(identity, directory) {
54548
54795
  if (process.platform === "win32") {
54549
54796
  return `\\\\.\\pipe\\glossa-workspace-${identity}`;
54550
54797
  }
54551
- return path12.join(directory, `${identity}.sock`);
54798
+ return path13.join(directory, `${identity}.sock`);
54552
54799
  }
54553
- async function acquireGuard(directory, identity) {
54554
- const guard = path12.join(directory, `${identity}.guard`);
54800
+ async function acquireGuard2(directory, identity) {
54801
+ const guard = path13.join(directory, `${identity}.guard`);
54555
54802
  const deadline = Date.now() + GUARD_TIMEOUT_MS;
54556
54803
  while (true) {
54557
54804
  try {
54558
- await mkdir7(guard, { mode: 448 });
54559
- 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 });
54560
54807
  } catch (error46) {
54561
54808
  if (error46.code !== "EEXIST") throw error46;
54562
54809
  let stale = false;
54563
54810
  try {
54564
- stale = Date.now() - (await stat3(guard)).mtimeMs >= GUARD_STALE_MS;
54811
+ stale = Date.now() - (await stat4(guard)).mtimeMs >= GUARD_STALE_MS;
54565
54812
  } catch (statError) {
54566
54813
  if (statError.code === "ENOENT") continue;
54567
54814
  throw statError;
54568
54815
  }
54569
54816
  if (stale) {
54570
- await rm6(guard, { recursive: true, force: true });
54817
+ await rm7(guard, { recursive: true, force: true });
54571
54818
  continue;
54572
54819
  }
54573
54820
  if (Date.now() >= deadline) {
@@ -54575,7 +54822,7 @@ async function acquireGuard(directory, identity) {
54575
54822
  "Timed out while checking whether this workspace is already active."
54576
54823
  );
54577
54824
  }
54578
- await delay3(GUARD_RETRY_MS);
54825
+ await delay5(GUARD_RETRY_MS);
54579
54826
  }
54580
54827
  }
54581
54828
  }
@@ -54623,7 +54870,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54623
54870
  await ensureLeaseDirectory(userDirectory);
54624
54871
  const identity = workspaceIdentity(root);
54625
54872
  const endpoint3 = endpointFor(identity, userDirectory);
54626
- const releaseGuard = await acquireGuard(userDirectory, identity);
54873
+ const releaseGuard = await acquireGuard2(userDirectory, identity);
54627
54874
  let server;
54628
54875
  try {
54629
54876
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -54637,11 +54884,11 @@ async function acquireWorkspaceLease(root, options = {}) {
54637
54884
  if (released) return;
54638
54885
  released = true;
54639
54886
  await ensureLeaseDirectory(userDirectory);
54640
- const releaseGuard2 = await acquireGuard(userDirectory, identity);
54887
+ const releaseGuard2 = await acquireGuard2(userDirectory, identity);
54641
54888
  try {
54642
54889
  await closeServer(server);
54643
54890
  if (process.platform !== "win32") {
54644
- await rm6(endpoint3, { force: true });
54891
+ await rm7(endpoint3, { force: true });
54645
54892
  }
54646
54893
  } finally {
54647
54894
  await releaseGuard2();
@@ -54654,7 +54901,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54654
54901
  throw new WorkspaceAlreadyActiveError();
54655
54902
  }
54656
54903
  if (error46.code !== "EADDRINUSE") throw error46;
54657
- if (process.platform !== "win32") await rm6(endpoint3, { force: true });
54904
+ if (process.platform !== "win32") await rm7(endpoint3, { force: true });
54658
54905
  }
54659
54906
  }
54660
54907
  throw new Error("Could not reserve this workspace for the current Glossa process.");
@@ -54664,7 +54911,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54664
54911
  }
54665
54912
 
54666
54913
  // src/main.ts
54667
- var VERSION = "0.2.0-beta.2";
54914
+ var VERSION = "0.2.0";
54668
54915
  var DISTRIBUTION = "npm";
54669
54916
  var HELP = `Glossa ${VERSION}
54670
54917
 
@@ -54673,6 +54920,7 @@ Usage:
54673
54920
  glossa status
54674
54921
  glossa devices revoke <id>
54675
54922
  glossa logout
54923
+ glossa unpair
54676
54924
  glossa update [--check]
54677
54925
  glossa update --policy <notify|auto|off>
54678
54926
  glossa update --channel <beta|stable>
@@ -54692,8 +54940,8 @@ Keys:
54692
54940
  \u2191\u2193 select or browse
54693
54941
  \u2190\u2192 change workspace access
54694
54942
  Enter/r revoke selected device
54695
- Esc activity
54696
- l sign out
54943
+ Esc workspace
54944
+ l account sign out
54697
54945
  ? help
54698
54946
  q disconnect and quit`;
54699
54947
  async function withLoginSignal(action) {
@@ -54740,8 +54988,8 @@ async function revokeKnownDevice(deviceId) {
54740
54988
  const credentials = await authenticatedCredentials();
54741
54989
  await revokeDevice(loadRelayEndpoints(), credentials, deviceId);
54742
54990
  }
54743
- async function runWorkspaceSession(path13, label, accessProfile) {
54744
- const root = await selectExposureRoot(path13);
54991
+ async function runWorkspaceSession(path14, label, accessProfile) {
54992
+ const root = await selectExposureRoot(path14);
54745
54993
  const lease = await acquireWorkspaceLease(root);
54746
54994
  let usageWrites = Promise.resolve();
54747
54995
  const queueUsage = (write) => {
@@ -54749,8 +54997,7 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54749
54997
  };
54750
54998
  try {
54751
54999
  const endpoints = loadRelayEndpoints();
54752
- let credentials = (await authenticatedSession()).credentials;
54753
- const statusService = new WorkspaceStatusService(credentials, endpoints);
55000
+ const device = await deviceForSession(endpoints);
54754
55001
  let postExitNotice;
54755
55002
  let requestedAccessProfile = accessProfile;
54756
55003
  let activeSessionController;
@@ -54768,7 +55015,7 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54768
55015
  else signal.addEventListener("abort", stopSession, { once: true });
54769
55016
  try {
54770
55017
  await runManagedSession(root, endpoints, {
54771
- credentials,
55018
+ device,
54772
55019
  workerVersion: VERSION,
54773
55020
  accessProfile: sessionAccessProfile,
54774
55021
  ...label ? { workspaceLabel: label } : {},
@@ -54799,10 +55046,13 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54799
55046
  }
54800
55047
  },
54801
55048
  loadStatus: async (signal) => {
54802
- return hudStatus(await statusService.refresh(signal));
55049
+ const credentials = await authenticatedCredentials(signal);
55050
+ return hudStatus(
55051
+ await new WorkspaceStatusService(credentials, endpoints).refresh(signal)
55052
+ );
54803
55053
  },
54804
55054
  revokeDevice: async (deviceId, signal) => {
54805
- credentials = await validCredentials(credentials, { signal });
55055
+ const credentials = await authenticatedCredentials(signal);
54806
55056
  await revokeDevice(
54807
55057
  endpoints,
54808
55058
  credentials,
@@ -54823,9 +55073,9 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54823
55073
  await lease.release();
54824
55074
  }
54825
55075
  }
54826
- async function runWorkspace(path13, label, accessProfile) {
55076
+ async function runWorkspace(path14, label, accessProfile) {
54827
55077
  await withWorkspaceLease(
54828
- async () => await runWorkspaceSession(path13, label, accessProfile)
55078
+ async () => await runWorkspaceSession(path14, label, accessProfile)
54829
55079
  );
54830
55080
  }
54831
55081
  async function refreshUpdateInfo(timeoutMs) {
@@ -54928,6 +55178,8 @@ async function main() {
54928
55178
  await showStatus();
54929
55179
  } else if (invocation.command === "logout") {
54930
55180
  await logoutFromGlossa();
55181
+ } else if (invocation.command === "unpair") {
55182
+ await unpairComputer();
54931
55183
  } else if (invocation.command === "update") {
54932
55184
  await runUpdateCommand(invocation);
54933
55185
  } else {