@ariobarin/glossa 0.2.0-beta.2 → 0.2.1

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 +676 -446
  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
  }
@@ -27162,11 +27147,10 @@ async function postForm(fetchRequest, url2, values, signal) {
27162
27147
  }
27163
27148
  return data;
27164
27149
  }
27165
- async function loginWithDeviceFlow(options, dependencies = {}) {
27150
+ async function authorizeWithDeviceFlow(options, dependencies = {}) {
27166
27151
  const fetchRequest = dependencies.fetch ?? fetch;
27167
27152
  const wait = dependencies.delay ?? defaultDelay;
27168
27153
  const browse = dependencies.openBrowser ?? openBrowser;
27169
- const save = dependencies.saveCredentials ?? saveCredentials;
27170
27154
  const now = dependencies.now ?? Date.now;
27171
27155
  const log = dependencies.log ?? console.log;
27172
27156
  try {
@@ -27204,26 +27188,25 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
27204
27188
  });
27205
27189
  const data = await response.json();
27206
27190
  if (response.ok && "access_token" in data) {
27207
- if (!data.refresh_token) {
27208
- throw new Error("Auth0 did not issue a refresh token.");
27209
- }
27210
27191
  const grantedScope = data.scope ?? options.scope;
27211
- if (!grantedScopesSatisfyRequest(grantedScope, options.scope, true)) {
27192
+ if (!grantedScopesSatisfyRequest(
27193
+ grantedScope,
27194
+ options.scope,
27195
+ Boolean(data.refresh_token)
27196
+ )) {
27212
27197
  throw new Error("Auth0 did not grant the permissions Glossa requires.");
27213
27198
  }
27214
- await save({
27199
+ return {
27215
27200
  issuer: options.issuer,
27216
27201
  clientId: options.clientId,
27217
27202
  audience: options.audience,
27218
27203
  accessToken: data.access_token,
27219
- refreshToken: data.refresh_token,
27204
+ ...data.refresh_token ? { refreshToken: data.refresh_token } : {},
27220
27205
  expiresAt: new Date(now() + data.expires_in * 1e3).toISOString(),
27221
27206
  tokenType: data.token_type,
27222
27207
  scope: grantedScope,
27223
27208
  requestedScope: options.scope
27224
- });
27225
- log("Signed in to Glossa.");
27226
- return;
27209
+ };
27227
27210
  }
27228
27211
  const error46 = data;
27229
27212
  if (error46.error === "authorization_pending") continue;
@@ -27243,6 +27226,15 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
27243
27226
  throw error46;
27244
27227
  }
27245
27228
  }
27229
+ async function loginWithDeviceFlow(options, dependencies = {}) {
27230
+ const credentials = await authorizeWithDeviceFlow(options, dependencies);
27231
+ if (!credentials.refreshToken) {
27232
+ throw new Error("Auth0 did not issue a refresh token.");
27233
+ }
27234
+ const save = dependencies.saveCredentials ?? saveCredentials;
27235
+ await save(credentials);
27236
+ (dependencies.log ?? console.log)("Signed in to Glossa.");
27237
+ }
27246
27238
 
27247
27239
  // src/auth-login.ts
27248
27240
  function normalizedIssuer(value) {
@@ -28014,10 +28006,10 @@ function mergeDefs(...defs) {
28014
28006
  function cloneDef(schema) {
28015
28007
  return mergeDefs(schema._zod.def);
28016
28008
  }
28017
- function getElementAtPath(obj, path13) {
28018
- if (!path13)
28009
+ function getElementAtPath(obj, path14) {
28010
+ if (!path14)
28019
28011
  return obj;
28020
- return path13.reduce((acc, key) => acc?.[key], obj);
28012
+ return path14.reduce((acc, key) => acc?.[key], obj);
28021
28013
  }
28022
28014
  function promiseAllObject(promisesObj) {
28023
28015
  const keys = Object.keys(promisesObj);
@@ -28378,11 +28370,11 @@ function aborted(x, startIndex = 0) {
28378
28370
  }
28379
28371
  return false;
28380
28372
  }
28381
- function prefixIssues(path13, issues) {
28373
+ function prefixIssues(path14, issues) {
28382
28374
  return issues.map((iss) => {
28383
28375
  var _a;
28384
28376
  (_a = iss).path ?? (_a.path = []);
28385
- iss.path.unshift(path13);
28377
+ iss.path.unshift(path14);
28386
28378
  return iss;
28387
28379
  });
28388
28380
  }
@@ -28544,7 +28536,7 @@ function formatError(error46, mapper = (issue2) => issue2.message) {
28544
28536
  }
28545
28537
  function treeifyError(error46, mapper = (issue2) => issue2.message) {
28546
28538
  const result = { errors: [] };
28547
- const processError = (error47, path13 = []) => {
28539
+ const processError = (error47, path14 = []) => {
28548
28540
  var _a, _b;
28549
28541
  for (const issue2 of error47.issues) {
28550
28542
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -28554,7 +28546,7 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
28554
28546
  } else if (issue2.code === "invalid_element") {
28555
28547
  processError({ issues: issue2.issues }, issue2.path);
28556
28548
  } else {
28557
- const fullpath = [...path13, ...issue2.path];
28549
+ const fullpath = [...path14, ...issue2.path];
28558
28550
  if (fullpath.length === 0) {
28559
28551
  result.errors.push(mapper(issue2));
28560
28552
  continue;
@@ -28586,8 +28578,8 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
28586
28578
  }
28587
28579
  function toDotPath(_path) {
28588
28580
  const segs = [];
28589
- const path13 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
28590
- for (const seg of path13) {
28581
+ const path14 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
28582
+ for (const seg of path14) {
28591
28583
  if (typeof seg === "number")
28592
28584
  segs.push(`[${seg}]`);
28593
28585
  else if (typeof seg === "symbol")
@@ -40271,6 +40263,10 @@ function parseInvocation(args) {
40271
40263
  noOptions("Logout", options);
40272
40264
  return { command };
40273
40265
  }
40266
+ if (command === "unpair") {
40267
+ noOptions("Unpair", options);
40268
+ return { command };
40269
+ }
40274
40270
  if (command === "update") return parseUpdate(options);
40275
40271
  return parseWorkspace(args);
40276
40272
  }
@@ -40322,7 +40318,7 @@ async function logoutFromGlossa(dependencies = {}) {
40322
40318
  const issuer = dependencies.issuer ?? stored?.credentials.issuer;
40323
40319
  await remove();
40324
40320
  log(
40325
- present ? "Signed out of Glossa." : "Already signed out of Glossa."
40321
+ present ? "Signed out of the Glossa account. Computer pairing stays active." : "Already signed out of the Glossa account. Computer pairing stays active."
40326
40322
  );
40327
40323
  const url2 = browserLogoutUrl(issuer ?? loadAuthConfig().issuer);
40328
40324
  const opened = await browse(url2);
@@ -40434,12 +40430,6 @@ async function listDevices(endpoints, credentials, fetchRequest = fetch) {
40434
40430
  if (!response.ok) throw relayError(response.status, data);
40435
40431
  return parseDevices2(data.devices);
40436
40432
  }
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
- );
40442
- }
40443
40433
  async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = fetch) {
40444
40434
  const baseName = deviceNameSchema.parse(deviceName);
40445
40435
  let name = baseName;
@@ -40484,6 +40474,17 @@ async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = f
40484
40474
  }
40485
40475
  throw new Error("Glossa could not enroll this computer.");
40486
40476
  }
40477
+ async function revokePairedDevice(endpoints, device, fetchRequest = fetch) {
40478
+ const response = await fetchRequest(`${endpoints.relayOrigin}/device`, {
40479
+ method: "DELETE",
40480
+ headers: { authorization: `Device ${device.token}` }
40481
+ });
40482
+ if (response.ok || response.status === 401 || response.status === 404) return;
40483
+ if (response.status === 429) {
40484
+ throw new Error("Glossa device authentication is rate limited. Try again later.");
40485
+ }
40486
+ throw new Error(`The Glossa relay returned HTTP ${response.status}.`);
40487
+ }
40487
40488
  async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
40488
40489
  const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
40489
40490
  method: "DELETE",
@@ -47670,8 +47671,8 @@ function Text({ color, backgroundColor, dimColor = false, bold = false, italic =
47670
47671
  }
47671
47672
 
47672
47673
  // ../../node_modules/ink/build/components/ErrorOverview.js
47673
- var cleanupPath = (path13) => {
47674
- return path13?.replace(`file://${cwd()}/`, "");
47674
+ var cleanupPath = (path14) => {
47675
+ return path14?.replace(`file://${cwd()}/`, "");
47675
47676
  };
47676
47677
  var stackUtils = new import_stack_utils.default({
47677
47678
  cwd: cwd(),
@@ -47845,7 +47846,7 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
47845
47846
  for (const subscriber of animationSubscribersRef.current.values()) {
47846
47847
  nextDueTime = Math.min(nextDueTime, subscriber.nextDueTime);
47847
47848
  }
47848
- const delay4 = Math.max(0, nextDueTime - performance.now());
47849
+ const delay5 = Math.max(0, nextDueTime - performance.now());
47849
47850
  animationTimerRef.current = setTimeout(() => {
47850
47851
  animationTimerRef.current = void 0;
47851
47852
  const currentTime = performance.now();
@@ -47859,7 +47860,7 @@ function App({ children, stdin, stdout, stderr, writeToStdout, writeToStderr, ex
47859
47860
  subscriber.nextDueTime = subscriber.startTime + elapsedFrames * subscriber.interval;
47860
47861
  }
47861
47862
  scheduleAnimationTick();
47862
- }, delay4);
47863
+ }, delay5);
47863
47864
  }, [clearAnimationTimer]);
47864
47865
  const animationSubscribe = (0, import_react15.useCallback)((callback, interval) => {
47865
47866
  const startTime = performance.now();
@@ -49799,7 +49800,7 @@ function connectHintStore(directory = configDirectory()) {
49799
49800
  }
49800
49801
  async function announceConnectHint(store3, log) {
49801
49802
  if (await store3.exists()) return false;
49802
- log(`Next: add Glossa in ChatGPT. Follow the quickstart at ${CONNECT_HINT_URL}.`);
49803
+ log(`Next: return to ChatGPT and use Glossa. If it is not connected yet, follow ${CONNECT_HINT_URL}.`);
49803
49804
  await store3.mark();
49804
49805
  return true;
49805
49806
  }
@@ -49845,11 +49846,294 @@ async function deleteDeviceCredential() {
49845
49846
  await store2.delete();
49846
49847
  }
49847
49848
 
49849
+ // src/device-pairing-lock.ts
49850
+ import { randomUUID as randomUUID2 } from "node:crypto";
49851
+ import { mkdir as mkdir4, readFile as readFile3, rm as rm3, stat, writeFile as writeFile4 } from "node:fs/promises";
49852
+ import path6 from "node:path";
49853
+ import { setTimeout as delay2 } from "node:timers/promises";
49854
+
49855
+ // src/update-lock.ts
49856
+ import { randomUUID } from "node:crypto";
49857
+ import { mkdir as mkdir3, readdir, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
49858
+ import path5 from "node:path";
49859
+ var UPDATE_SUFFIX = ".update";
49860
+ var SESSION_SUFFIX = ".session";
49861
+ function updateRuntimeDirectory() {
49862
+ return path5.join(configDirectory(), "runtime");
49863
+ }
49864
+ function processIsAlive(pid) {
49865
+ if (!Number.isSafeInteger(pid) || pid <= 0) return false;
49866
+ try {
49867
+ process.kill(pid, 0);
49868
+ return true;
49869
+ } catch (error46) {
49870
+ return error46.code === "EPERM";
49871
+ }
49872
+ }
49873
+ function leasePid(name, suffix) {
49874
+ if (!name.endsWith(suffix)) return null;
49875
+ const separator = name.indexOf("-");
49876
+ if (separator <= 0) return null;
49877
+ const pid = Number(name.slice(0, separator));
49878
+ return Number.isSafeInteger(pid) && pid > 0 ? pid : null;
49879
+ }
49880
+ async function activeLeaseFiles(directory, suffix) {
49881
+ let entries;
49882
+ try {
49883
+ entries = await readdir(directory, { withFileTypes: true });
49884
+ } catch (error46) {
49885
+ if (error46.code === "ENOENT") return [];
49886
+ throw error46;
49887
+ }
49888
+ const active = [];
49889
+ for (const entry of entries) {
49890
+ if (!entry.isFile() || !entry.name.endsWith(suffix)) continue;
49891
+ const pid = leasePid(entry.name, suffix);
49892
+ const file2 = path5.join(directory, entry.name);
49893
+ if (pid !== null && processIsAlive(pid)) active.push(file2);
49894
+ else await rm2(file2, { force: true });
49895
+ }
49896
+ return active;
49897
+ }
49898
+ async function updateLockIsActive(directory) {
49899
+ return (await activeLeaseFiles(directory, UPDATE_SUFFIX)).length > 0;
49900
+ }
49901
+ async function activeSessionFiles(directory) {
49902
+ return await activeLeaseFiles(directory, SESSION_SUFFIX);
49903
+ }
49904
+ async function createUpdateLock(directory) {
49905
+ await mkdir3(directory, { recursive: true, mode: 448 });
49906
+ const lockFile = path5.join(
49907
+ directory,
49908
+ `${process.pid}-${randomUUID()}${UPDATE_SUFFIX}`
49909
+ );
49910
+ const content = `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
49911
+ `;
49912
+ await writeFile3(lockFile, content, {
49913
+ encoding: "utf8",
49914
+ flag: "wx",
49915
+ mode: 384
49916
+ });
49917
+ try {
49918
+ const contenders = await activeLeaseFiles(directory, UPDATE_SUFFIX);
49919
+ if (contenders.some((file2) => file2 !== lockFile)) {
49920
+ throw new Error("Another Glossa update is already running.");
49921
+ }
49922
+ return lockFile;
49923
+ } catch (error46) {
49924
+ await rm2(lockFile, { force: true });
49925
+ throw error46;
49926
+ }
49927
+ }
49928
+ async function withUpdateLease(action, directory = updateRuntimeDirectory()) {
49929
+ const lockFile = await createUpdateLock(directory);
49930
+ try {
49931
+ if ((await activeSessionFiles(directory)).length > 0) {
49932
+ throw new Error("Disconnect every running Glossa workspace before updating.");
49933
+ }
49934
+ return await action();
49935
+ } finally {
49936
+ await rm2(lockFile, { force: true });
49937
+ }
49938
+ }
49939
+ async function withWorkspaceLease(action, directory = updateRuntimeDirectory()) {
49940
+ await mkdir3(directory, { recursive: true, mode: 448 });
49941
+ if (await updateLockIsActive(directory)) {
49942
+ throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
49943
+ }
49944
+ const leaseFile = path5.join(
49945
+ directory,
49946
+ `${process.pid}-${randomUUID()}${SESSION_SUFFIX}`
49947
+ );
49948
+ await writeFile3(
49949
+ leaseFile,
49950
+ `${JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
49951
+ `,
49952
+ { encoding: "utf8", flag: "wx", mode: 384 }
49953
+ );
49954
+ try {
49955
+ if (await updateLockIsActive(directory)) {
49956
+ throw new Error("Glossa is updating. Run this workspace again after the update finishes.");
49957
+ }
49958
+ return await action();
49959
+ } finally {
49960
+ await rm2(leaseFile, { force: true });
49961
+ }
49962
+ }
49963
+
49964
+ // src/device-pairing-lock.ts
49965
+ var DEVICE_PAIRING_LOCK = "device-pairing.lock";
49966
+ var DEVICE_PAIRING_LOCK_GUARD = "device-pairing.lock.guard";
49967
+ var DEVICE_PAIRING_LOCK_POLL_MS = 100;
49968
+ var DEVICE_PAIRING_LOCK_MAX_AGE_MS = 6 * 6e4;
49969
+ var DEVICE_PAIRING_GUARD_MAX_AGE_MS = 3e4;
49970
+ function lockPath(directory) {
49971
+ return path6.join(directory, DEVICE_PAIRING_LOCK);
49972
+ }
49973
+ function guardPath(directory) {
49974
+ return path6.join(directory, DEVICE_PAIRING_LOCK_GUARD);
49975
+ }
49976
+ function newOwner() {
49977
+ return {
49978
+ pid: process.pid,
49979
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
49980
+ token: randomUUID2()
49981
+ };
49982
+ }
49983
+ async function readOwner(file2) {
49984
+ try {
49985
+ const value = JSON.parse(await readFile3(file2, "utf8"));
49986
+ if (!Number.isSafeInteger(value.pid) || typeof value.startedAt !== "string" || !Number.isFinite(Date.parse(value.startedAt)) || typeof value.token !== "string" || value.token.length === 0) {
49987
+ return void 0;
49988
+ }
49989
+ return value;
49990
+ } catch (error46) {
49991
+ if (error46.code === "ENOENT") return void 0;
49992
+ return void 0;
49993
+ }
49994
+ }
49995
+ async function lockIsStale(file2, maxAgeMs) {
49996
+ const owner = await readOwner(file2);
49997
+ if (!owner) {
49998
+ try {
49999
+ const metadata = await stat(file2);
50000
+ return Date.now() - metadata.mtimeMs > maxAgeMs;
50001
+ } catch (error46) {
50002
+ if (error46.code === "ENOENT") return false;
50003
+ return true;
50004
+ }
50005
+ }
50006
+ const ageMs = Date.now() - Date.parse(owner.startedAt);
50007
+ return ageMs > maxAgeMs || !processIsAlive(owner.pid);
50008
+ }
50009
+ async function acquireGuard(directory, signal) {
50010
+ const file2 = guardPath(directory);
50011
+ for (; ; ) {
50012
+ signal?.throwIfAborted();
50013
+ const owner = newOwner();
50014
+ try {
50015
+ await writeFile4(file2, `${JSON.stringify(owner)}
50016
+ `, {
50017
+ encoding: "utf8",
50018
+ flag: "wx",
50019
+ mode: 384
50020
+ });
50021
+ return { file: file2, token: owner.token };
50022
+ } catch (error46) {
50023
+ if (error46.code !== "EEXIST") throw error46;
50024
+ }
50025
+ if (await lockIsStale(file2, DEVICE_PAIRING_GUARD_MAX_AGE_MS)) {
50026
+ await rm3(file2, { force: true });
50027
+ continue;
50028
+ }
50029
+ await delay2(
50030
+ DEVICE_PAIRING_LOCK_POLL_MS,
50031
+ void 0,
50032
+ signal ? { signal } : void 0
50033
+ );
50034
+ }
50035
+ }
50036
+ async function removeOwnedLock(file2, token) {
50037
+ const owner = await readOwner(file2);
50038
+ if (owner?.token === token) await rm3(file2, { force: true });
50039
+ }
50040
+ async function withGuard(directory, action, signal) {
50041
+ const guard = await acquireGuard(directory, signal);
50042
+ try {
50043
+ return await action();
50044
+ } finally {
50045
+ await removeOwnedLock(guard.file, guard.token);
50046
+ }
50047
+ }
50048
+ async function acquireDevicePairingLock(directory, signal) {
50049
+ await mkdir4(directory, { recursive: true, mode: 448 });
50050
+ const file2 = lockPath(directory);
50051
+ for (; ; ) {
50052
+ signal?.throwIfAborted();
50053
+ const owner = newOwner();
50054
+ try {
50055
+ await writeFile4(file2, `${JSON.stringify(owner)}
50056
+ `, {
50057
+ encoding: "utf8",
50058
+ flag: "wx",
50059
+ mode: 384
50060
+ });
50061
+ return { file: file2, token: owner.token };
50062
+ } catch (error46) {
50063
+ if (error46.code !== "EEXIST") throw error46;
50064
+ }
50065
+ const reclaimed = await withGuard(
50066
+ directory,
50067
+ async () => {
50068
+ if (!await lockIsStale(file2, DEVICE_PAIRING_LOCK_MAX_AGE_MS)) {
50069
+ return false;
50070
+ }
50071
+ await rm3(file2, { force: true });
50072
+ return true;
50073
+ },
50074
+ signal
50075
+ );
50076
+ if (reclaimed) continue;
50077
+ await delay2(
50078
+ DEVICE_PAIRING_LOCK_POLL_MS,
50079
+ void 0,
50080
+ signal ? { signal } : void 0
50081
+ );
50082
+ }
50083
+ }
50084
+ async function withDevicePairingLease(action, signal, directory = updateRuntimeDirectory()) {
50085
+ const lock = await acquireDevicePairingLock(directory, signal);
50086
+ try {
50087
+ return await action();
50088
+ } finally {
50089
+ await withGuard(directory, async () => await removeOwnedLock(lock.file, lock.token));
50090
+ }
50091
+ }
50092
+
50093
+ // src/device-pairing.ts
50094
+ function canceledError2() {
50095
+ return new Error("Pairing canceled.");
50096
+ }
50097
+ function pairingScope(scope) {
50098
+ return scope.split(/\s+/).filter((value) => value && value !== "offline_access").join(" ");
50099
+ }
50100
+ async function pairDevice(endpoints, signal, dependencies = {}) {
50101
+ const authorize = dependencies.authorizeWithDeviceFlow ?? authorizeWithDeviceFlow;
50102
+ const enroll = dependencies.enrollDevice ?? enrollDevice;
50103
+ const authConfig = dependencies.loadAuthConfig ?? loadAuthConfig;
50104
+ const name = dependencies.defaultDeviceName ?? defaultDeviceName;
50105
+ const log = dependencies.log ?? console.log;
50106
+ const baseFetch = dependencies.fetch ?? fetch;
50107
+ const fetchRequest = signal ? async (input, init) => await baseFetch(input, { ...init, signal }) : baseFetch;
50108
+ try {
50109
+ signal?.throwIfAborted();
50110
+ log("This computer needs to be paired with Glossa.");
50111
+ const auth = authConfig();
50112
+ const credentials = await authorize(
50113
+ {
50114
+ ...auth,
50115
+ scope: pairingScope(auth.scope),
50116
+ ...signal ? { signal } : {}
50117
+ },
50118
+ { fetch: fetchRequest, log }
50119
+ );
50120
+ signal?.throwIfAborted();
50121
+ const paired = await enroll(endpoints, credentials, name(), fetchRequest);
50122
+ log(`Paired with Glossa as ${paired.deviceName}.`);
50123
+ return paired;
50124
+ } catch (error46) {
50125
+ if (signal?.aborted || error46 instanceof Error && error46.name === "AbortError") {
50126
+ throw canceledError2();
50127
+ }
50128
+ throw error46;
50129
+ }
50130
+ }
50131
+
49848
50132
  // src/worker/command-service.ts
49849
50133
  import { spawn as spawn2 } from "node:child_process";
49850
- import { randomUUID } from "node:crypto";
50134
+ import { randomUUID as randomUUID3 } from "node:crypto";
49851
50135
  import { StringDecoder } from "node:string_decoder";
49852
- import { setTimeout as delay2 } from "node:timers/promises";
50136
+ import { setTimeout as delay3 } from "node:timers/promises";
49853
50137
 
49854
50138
  // src/worker/errors.ts
49855
50139
  var WorkerError = class extends Error {
@@ -49964,7 +50248,7 @@ async function waitForChange(record2, afterSequence, waitMs) {
49964
50248
  try {
49965
50249
  await Promise.race([
49966
50250
  change,
49967
- delay2(waitMs, void 0, { signal: waitController.signal })
50251
+ delay3(waitMs, void 0, { signal: waitController.signal })
49968
50252
  ]);
49969
50253
  } finally {
49970
50254
  record2.changeWaiters.delete(changed);
@@ -50135,7 +50419,7 @@ async function terminateProcessTree(child) {
50135
50419
  } catch {
50136
50420
  child.kill("SIGTERM");
50137
50421
  }
50138
- await delay2(2e3);
50422
+ await delay3(2e3);
50139
50423
  if (child.exitCode === null) {
50140
50424
  try {
50141
50425
  process.kill(-child.pid, "SIGKILL");
@@ -50228,7 +50512,7 @@ var CommandService = class {
50228
50512
  const completion = new Promise((resolve) => {
50229
50513
  complete = resolve;
50230
50514
  });
50231
- const id = randomUUID();
50515
+ const id = randomUUID3();
50232
50516
  const record2 = {
50233
50517
  id,
50234
50518
  child,
@@ -50300,7 +50584,7 @@ var CommandService = class {
50300
50584
  try {
50301
50585
  await Promise.race([
50302
50586
  record2.completion,
50303
- delay2(waitMs, void 0, { signal: waitController.signal })
50587
+ delay3(waitMs, void 0, { signal: waitController.signal })
50304
50588
  ]);
50305
50589
  } finally {
50306
50590
  waitController.abort();
@@ -50326,7 +50610,7 @@ var CommandService = class {
50326
50610
  try {
50327
50611
  await Promise.race([
50328
50612
  record2.completion,
50329
- delay2(waitMs, void 0, { signal: waitController.signal })
50613
+ delay3(waitMs, void 0, { signal: waitController.signal })
50330
50614
  ]);
50331
50615
  } finally {
50332
50616
  waitController.abort();
@@ -50428,29 +50712,29 @@ var CommandService = class {
50428
50712
  };
50429
50713
 
50430
50714
  // src/worker/file-service.ts
50431
- import { createHash, randomUUID as randomUUID2 } from "node:crypto";
50715
+ import { createHash, randomUUID as randomUUID4 } from "node:crypto";
50432
50716
  import {
50433
50717
  chmod as chmod2,
50434
50718
  link as link2,
50435
50719
  lstat as lstat2,
50436
- mkdir as mkdir3,
50720
+ mkdir as mkdir5,
50437
50721
  opendir,
50438
50722
  open,
50439
- readFile as readFile3,
50723
+ readFile as readFile4,
50440
50724
  rename,
50441
- rm as rm2,
50725
+ rm as rm4,
50442
50726
  rmdir,
50443
- stat as stat2,
50444
- writeFile as writeFile3
50727
+ stat as stat3,
50728
+ writeFile as writeFile5
50445
50729
  } from "node:fs/promises";
50446
- import path6 from "node:path";
50730
+ import path8 from "node:path";
50447
50731
  import { performance as performance2 } from "node:perf_hooks";
50448
50732
  import { StringDecoder as StringDecoder2 } from "node:string_decoder";
50449
50733
 
50450
50734
  // src/worker/path-policy.ts
50451
- import { lstat, realpath, stat } from "node:fs/promises";
50735
+ import { lstat, realpath, stat as stat2 } from "node:fs/promises";
50452
50736
  import os5 from "node:os";
50453
- import path5 from "node:path";
50737
+ import path7 from "node:path";
50454
50738
  function samePath(left, right) {
50455
50739
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
50456
50740
  }
@@ -50462,15 +50746,15 @@ function accountHomeDirectory() {
50462
50746
  }
50463
50747
  }
50464
50748
  function isWithin(root, candidate) {
50465
- const relative = path5.relative(root, candidate);
50466
- return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
50749
+ const relative = path7.relative(root, candidate);
50750
+ return relative === "" || !relative.startsWith(`..${path7.sep}`) && relative !== ".." && !path7.isAbsolute(relative);
50467
50751
  }
50468
50752
  function validateRelativePath(value) {
50469
50753
  if (value.includes("\0")) {
50470
50754
  throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
50471
50755
  }
50472
50756
  const explicitNativePosixPath = process.platform !== "win32" && value.startsWith("./");
50473
- if (path5.isAbsolute(value) || path5.posix.isAbsolute(value) || !explicitNativePosixPath && path5.win32.isAbsolute(value)) {
50757
+ if (path7.isAbsolute(value) || path7.posix.isAbsolute(value) || !explicitNativePosixPath && path7.win32.isAbsolute(value)) {
50474
50758
  throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
50475
50759
  }
50476
50760
  const segments = explicitNativePosixPath ? value.split(/\/+/).filter(Boolean) : value.split(/[\\/]+/);
@@ -50480,20 +50764,20 @@ function validateRelativePath(value) {
50480
50764
  return value === "" ? "." : value;
50481
50765
  }
50482
50766
  async function canonicalizeRoot(candidate) {
50483
- const root = await realpath(path5.resolve(candidate)).catch((error46) => {
50767
+ const root = await realpath(path7.resolve(candidate)).catch((error46) => {
50484
50768
  if (error46.code === "ENOENT") {
50485
50769
  throw new WorkerError("root_not_found", "The workspace directory does not exist.");
50486
50770
  }
50487
50771
  throw error46;
50488
50772
  });
50489
- const rootStat = await stat(root);
50773
+ const rootStat = await stat2(root);
50490
50774
  if (!rootStat.isDirectory()) {
50491
50775
  throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
50492
50776
  }
50493
- const filesystemRoot = path5.parse(root).root;
50777
+ const filesystemRoot = path7.parse(root).root;
50494
50778
  const homes = await Promise.all(
50495
50779
  [os5.homedir(), accountHomeDirectory()].map(
50496
- async (home) => await realpath(home).catch(() => path5.resolve(home))
50780
+ async (home) => await realpath(home).catch(() => path7.resolve(home))
50497
50781
  )
50498
50782
  );
50499
50783
  const isHomeDirectory = homes.some((home) => samePath(root, home));
@@ -50529,7 +50813,7 @@ var PathPolicy = class _PathPolicy {
50529
50813
  return canonical;
50530
50814
  }
50531
50815
  async resolveDiscoveredExisting(candidate) {
50532
- const lexical = path5.resolve(candidate);
50816
+ const lexical = path7.resolve(candidate);
50533
50817
  if (!isWithin(this.root, lexical)) {
50534
50818
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50535
50819
  }
@@ -50547,7 +50831,7 @@ var PathPolicy = class _PathPolicy {
50547
50831
  }
50548
50832
  async resolveWritableFile(relativePath) {
50549
50833
  const lexical = this.resolveLexical(relativePath);
50550
- const parent = path5.dirname(lexical);
50834
+ const parent = path7.dirname(lexical);
50551
50835
  await this.rejectLinkedComponents(parent);
50552
50836
  const canonicalParent = await realpath(parent).catch((error46) => {
50553
50837
  if (error46.code === "ENOENT") {
@@ -50558,7 +50842,7 @@ var PathPolicy = class _PathPolicy {
50558
50842
  if (!isWithin(this.root, canonicalParent)) {
50559
50843
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50560
50844
  }
50561
- if (!(await stat(canonicalParent)).isDirectory()) {
50845
+ if (!(await stat2(canonicalParent)).isDirectory()) {
50562
50846
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50563
50847
  }
50564
50848
  try {
@@ -50578,7 +50862,7 @@ var PathPolicy = class _PathPolicy {
50578
50862
  if (error46 instanceof WorkerError) throw error46;
50579
50863
  if (error46.code !== "ENOENT") throw error46;
50580
50864
  }
50581
- return path5.join(canonicalParent, path5.basename(lexical));
50865
+ return path7.join(canonicalParent, path7.basename(lexical));
50582
50866
  }
50583
50867
  async resolveWritableDirectory(relativePath, recursive) {
50584
50868
  const lexical = this.resolveLexical(relativePath);
@@ -50601,7 +50885,7 @@ var PathPolicy = class _PathPolicy {
50601
50885
  if (error46.code !== "ENOENT") throw error46;
50602
50886
  }
50603
50887
  if (!recursive) {
50604
- const parent = path5.dirname(lexical);
50888
+ const parent = path7.dirname(lexical);
50605
50889
  await this.rejectLinkedComponents(parent);
50606
50890
  const canonicalParent = await realpath(parent).catch(
50607
50891
  (error46) => {
@@ -50617,15 +50901,15 @@ var PathPolicy = class _PathPolicy {
50617
50901
  if (!isWithin(this.root, canonicalParent)) {
50618
50902
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50619
50903
  }
50620
- if (!(await stat(canonicalParent)).isDirectory()) {
50904
+ if (!(await stat2(canonicalParent)).isDirectory()) {
50621
50905
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50622
50906
  }
50623
50907
  return {
50624
- target: path5.join(canonicalParent, path5.basename(lexical)),
50908
+ target: path7.join(canonicalParent, path7.basename(lexical)),
50625
50909
  exists: false
50626
50910
  };
50627
50911
  }
50628
- let existingAncestor = path5.dirname(lexical);
50912
+ let existingAncestor = path7.dirname(lexical);
50629
50913
  while (!samePath(existingAncestor, this.root)) {
50630
50914
  try {
50631
50915
  await this.rejectLinkedComponents(existingAncestor);
@@ -50633,13 +50917,13 @@ var PathPolicy = class _PathPolicy {
50633
50917
  if (!isWithin(this.root, canonicalAncestor)) {
50634
50918
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50635
50919
  }
50636
- if (!(await stat(canonicalAncestor)).isDirectory()) {
50920
+ if (!(await stat2(canonicalAncestor)).isDirectory()) {
50637
50921
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50638
50922
  }
50639
50923
  return {
50640
- target: path5.join(
50924
+ target: path7.join(
50641
50925
  canonicalAncestor,
50642
- path5.relative(existingAncestor, lexical)
50926
+ path7.relative(existingAncestor, lexical)
50643
50927
  ),
50644
50928
  exists: false
50645
50929
  };
@@ -50647,13 +50931,13 @@ var PathPolicy = class _PathPolicy {
50647
50931
  if (error46 instanceof WorkerError) throw error46;
50648
50932
  if (error46.code !== "ENOENT") throw error46;
50649
50933
  }
50650
- existingAncestor = path5.dirname(existingAncestor);
50934
+ existingAncestor = path7.dirname(existingAncestor);
50651
50935
  }
50652
50936
  return { target: lexical, exists: false };
50653
50937
  }
50654
50938
  async resolveVacantPath(relativePath) {
50655
50939
  const lexical = this.resolveLexical(relativePath);
50656
- const parent = path5.dirname(lexical);
50940
+ const parent = path7.dirname(lexical);
50657
50941
  await this.rejectLinkedComponents(parent);
50658
50942
  const canonicalParent = await realpath(parent).catch(
50659
50943
  (error46) => {
@@ -50669,7 +50953,7 @@ var PathPolicy = class _PathPolicy {
50669
50953
  if (!isWithin(this.root, canonicalParent)) {
50670
50954
  throw new WorkerError("path_escape", "The destination escapes the exposed root.");
50671
50955
  }
50672
- if (!(await stat(canonicalParent)).isDirectory()) {
50956
+ if (!(await stat2(canonicalParent)).isDirectory()) {
50673
50957
  throw new WorkerError("not_directory", "The destination parent is not a directory.");
50674
50958
  }
50675
50959
  try {
@@ -50682,11 +50966,11 @@ var PathPolicy = class _PathPolicy {
50682
50966
  if (error46 instanceof WorkerError) throw error46;
50683
50967
  if (error46.code !== "ENOENT") throw error46;
50684
50968
  }
50685
- return path5.join(canonicalParent, path5.basename(lexical));
50969
+ return path7.join(canonicalParent, path7.basename(lexical));
50686
50970
  }
50687
50971
  resolveLexical(relativePath) {
50688
50972
  const validated = validateRelativePath(relativePath);
50689
- const candidate = path5.resolve(this.root, validated);
50973
+ const candidate = path7.resolve(this.root, validated);
50690
50974
  if (!isWithin(this.root, candidate)) {
50691
50975
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50692
50976
  }
@@ -50696,11 +50980,11 @@ var PathPolicy = class _PathPolicy {
50696
50980
  if (!isWithin(this.root, candidate)) {
50697
50981
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
50698
50982
  }
50699
- const relative = path5.relative(this.root, candidate);
50983
+ const relative = path7.relative(this.root, candidate);
50700
50984
  if (!relative) return;
50701
50985
  let current = this.root;
50702
- for (const segment of relative.split(path5.sep)) {
50703
- current = path5.join(current, segment);
50986
+ for (const segment of relative.split(path7.sep)) {
50987
+ current = path7.join(current, segment);
50704
50988
  try {
50705
50989
  const currentStat = await lstat(current);
50706
50990
  if (currentStat.isSymbolicLink()) {
@@ -50723,12 +51007,12 @@ function sha256(content) {
50723
51007
  return createHash("sha256").update(content).digest("hex");
50724
51008
  }
50725
51009
  function isPathWithin(root, candidate) {
50726
- const relative = path6.relative(root, candidate);
50727
- return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
51010
+ const relative = path8.relative(root, candidate);
51011
+ return relative === "" || !relative.startsWith(`..${path8.sep}`) && relative !== ".." && !path8.isAbsolute(relative);
50728
51012
  }
50729
51013
  var fileWriteTails = /* @__PURE__ */ new Map();
50730
51014
  async function withFileWriteLock(target, operation) {
50731
- const normalized = path6.normalize(target);
51015
+ const normalized = path8.normalize(target);
50732
51016
  const key = process.platform === "win32" ? normalized.toLowerCase() : normalized;
50733
51017
  const predecessor = fileWriteTails.get(key);
50734
51018
  let release;
@@ -50745,7 +51029,7 @@ async function withFileWriteLock(target, operation) {
50745
51029
  }
50746
51030
  }
50747
51031
  async function withFileWriteLocks(targets, operation) {
50748
- const unique = [...new Set(targets.map((target) => path6.normalize(target)))].sort(
51032
+ const unique = [...new Set(targets.map((target) => path8.normalize(target)))].sort(
50749
51033
  (left, right) => left.localeCompare(right)
50750
51034
  );
50751
51035
  const run = async (index) => {
@@ -50757,7 +51041,7 @@ async function withFileWriteLocks(targets, operation) {
50757
51041
  async function requireRevision(target, expectedSha256) {
50758
51042
  let actual = null;
50759
51043
  try {
50760
- actual = sha256(await readFile3(target));
51044
+ actual = sha256(await readFile4(target));
50761
51045
  } catch (error46) {
50762
51046
  if (error46.code !== "ENOENT") throw error46;
50763
51047
  }
@@ -50781,7 +51065,7 @@ function compareNames(left, right) {
50781
51065
  return left < right ? -1 : left > right ? 1 : 0;
50782
51066
  }
50783
51067
  function relativeSortKey(root, target) {
50784
- const relative = path6.relative(root, target);
51068
+ const relative = path8.relative(root, target);
50785
51069
  if (!relative) return ".";
50786
51070
  return process.platform === "win32" ? relative.replaceAll("\\", "/") : relative;
50787
51071
  }
@@ -51093,7 +51377,7 @@ var FileService = class {
51093
51377
  }
51094
51378
  async #readResolvedText(target, maximumBytes = MAX_TEXT_BYTES) {
51095
51379
  const boundedMaximum = Math.min(maximumBytes, MAX_TEXT_BYTES);
51096
- const targetStat = await stat2(target);
51380
+ const targetStat = await stat3(target);
51097
51381
  if (!targetStat.isFile()) {
51098
51382
  throw new WorkerError("not_file", "The requested path is not a file.");
51099
51383
  }
@@ -51136,7 +51420,7 @@ var FileService = class {
51136
51420
  this.policy.resolveExisting(startPath),
51137
51421
  deadlineAt
51138
51422
  );
51139
- const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
51423
+ const startStat = await this.#withinDeadline(stat3(start), deadlineAt);
51140
51424
  if (!startStat.isDirectory()) {
51141
51425
  throw new WorkerError("not_directory", "The requested path is not a directory.");
51142
51426
  }
@@ -51165,7 +51449,7 @@ var FileService = class {
51165
51449
  if (batch.overflow) throw scanLimitError();
51166
51450
  scannedEntries += batch.children.length;
51167
51451
  for (const child of batch.children) {
51168
- const target = path6.join(directory, child.name);
51452
+ const target = path8.join(directory, child.name);
51169
51453
  heapPush(frontier, {
51170
51454
  target,
51171
51455
  sortKey: relativeSortKey(this.policy.root, target),
@@ -51189,7 +51473,7 @@ var FileService = class {
51189
51473
  throw scanLimitError();
51190
51474
  }
51191
51475
  scannedEntries += 1;
51192
- const target = path6.join(directory, child.name);
51476
+ const target = path8.join(directory, child.name);
51193
51477
  let childStat;
51194
51478
  try {
51195
51479
  childStat = await this.#withinDeadline(
@@ -51341,7 +51625,7 @@ var FileService = class {
51341
51625
  if (!patterns) return false;
51342
51626
  const normalized = relativePath.replaceAll("\\", "/");
51343
51627
  try {
51344
- return patterns.some((pattern) => path6.posix.matchesGlob(normalized, pattern));
51628
+ return patterns.some((pattern) => path8.posix.matchesGlob(normalized, pattern));
51345
51629
  } catch {
51346
51630
  throw new WorkerError("invalid_search", "Search glob pattern is invalid.");
51347
51631
  }
@@ -51359,7 +51643,7 @@ var FileService = class {
51359
51643
  this.policy.resolveExisting(options.path ?? "."),
51360
51644
  deadlineAt
51361
51645
  );
51362
- const startStat = await this.#withinDeadline(stat2(start), deadlineAt);
51646
+ const startStat = await this.#withinDeadline(stat3(start), deadlineAt);
51363
51647
  const matches = [];
51364
51648
  let scannedEntries = 0;
51365
51649
  let scannedFiles = 0;
@@ -51370,7 +51654,7 @@ var FileService = class {
51370
51654
  const searchFile = async (target) => {
51371
51655
  const relative = displayPath(this.policy.root, target);
51372
51656
  if (extensions && !extensions.some(
51373
- (extension) => path6.basename(target).toLowerCase().endsWith(extension)
51657
+ (extension) => path8.basename(target).toLowerCase().endsWith(extension)
51374
51658
  )) {
51375
51659
  return false;
51376
51660
  }
@@ -51465,7 +51749,7 @@ var FileService = class {
51465
51749
  scannedEntries += batch.children.length;
51466
51750
  for (const child of batch.children) {
51467
51751
  this.#assertBeforeDeadline(deadlineAt);
51468
- const target = path6.join(directory, child.name);
51752
+ const target = path8.join(directory, child.name);
51469
51753
  let targetType;
51470
51754
  if (this.#trustDirectoryEntryTypes) {
51471
51755
  if (child.isSymbolicLink()) {
@@ -51613,12 +51897,12 @@ var FileService = class {
51613
51897
  );
51614
51898
  if (current.exists) return { created: false };
51615
51899
  try {
51616
- await mkdir3(current.target, { recursive });
51900
+ await mkdir5(current.target, { recursive });
51617
51901
  } catch (error46) {
51618
51902
  if (error46.code !== "EEXIST") throw error46;
51619
51903
  }
51620
51904
  const resolved = await this.policy.resolveExisting(relativePath);
51621
- if (!(await stat2(resolved)).isDirectory()) {
51905
+ if (!(await stat3(resolved)).isDirectory()) {
51622
51906
  throw new WorkerError("not_directory", "The destination is not a directory.");
51623
51907
  }
51624
51908
  return { created: true };
@@ -51650,12 +51934,12 @@ var FileService = class {
51650
51934
  try {
51651
51935
  if (targetStat.isDirectory()) {
51652
51936
  if (recursive) {
51653
- await rm2(target, { recursive: true, force: false });
51937
+ await rm4(target, { recursive: true, force: false });
51654
51938
  } else {
51655
51939
  await rmdir(target);
51656
51940
  }
51657
51941
  } else {
51658
- await rm2(target, { force: false });
51942
+ await rm4(target, { force: false });
51659
51943
  }
51660
51944
  } catch (error46) {
51661
51945
  const code = error46.code;
@@ -51763,7 +52047,7 @@ var FileService = class {
51763
52047
  return await withFileWriteLock(target, async () => {
51764
52048
  let existingMode;
51765
52049
  try {
51766
- const existingStat = await stat2(target);
52050
+ const existingStat = await stat3(target);
51767
52051
  if (existingStat.isFile()) existingMode = existingStat.mode & 4095;
51768
52052
  } catch (error46) {
51769
52053
  if (error46.code !== "ENOENT") throw error46;
@@ -51775,9 +52059,9 @@ var FileService = class {
51775
52059
  );
51776
52060
  }
51777
52061
  if (expectedSha256) await requireRevision(target, expectedSha256);
51778
- const temporary = path6.join(path6.dirname(target), `.glossa-${randomUUID2()}.tmp`);
52062
+ const temporary = path8.join(path8.dirname(target), `.glossa-${randomUUID4()}.tmp`);
51779
52063
  try {
51780
- await writeFile3(temporary, bytes, { flag: "wx", mode: 384 });
52064
+ await writeFile5(temporary, bytes, { flag: "wx", mode: 384 });
51781
52065
  target = await this.policy.resolveWritableFile(relativePath);
51782
52066
  const tempStat = await lstat2(temporary);
51783
52067
  if (!tempStat.isFile() || tempStat.isSymbolicLink()) {
@@ -51803,7 +52087,7 @@ var FileService = class {
51803
52087
  }
51804
52088
  }
51805
52089
  } finally {
51806
- await rm2(temporary, { force: true });
52090
+ await rm4(temporary, { force: true });
51807
52091
  }
51808
52092
  return { sha256: sha256(bytes), bytes: bytes.byteLength };
51809
52093
  });
@@ -51830,7 +52114,9 @@ function jobInputContainsRestrictedData(job) {
51830
52114
  return containsRestrictedAuthenticationData({
51831
52115
  query: job.query,
51832
52116
  path: job.path,
51833
- extensions: job.extensions
52117
+ extensions: job.extensions,
52118
+ includeGlobs: job.includeGlobs,
52119
+ excludeGlobs: job.excludeGlobs
51834
52120
  });
51835
52121
  case "read_file_range":
51836
52122
  return containsRestrictedAuthenticationData(job.path);
@@ -52013,7 +52299,7 @@ var LocalWorker = class _LocalWorker {
52013
52299
  };
52014
52300
 
52015
52301
  // src/worker/remote-worker.ts
52016
- import { randomUUID as randomUUID3 } from "node:crypto";
52302
+ import { randomUUID as randomUUID5 } from "node:crypto";
52017
52303
  var WORKER_REQUEST_TIMEOUT_MS = 19e3;
52018
52304
  var DEFAULT_RECONNECT_BASE_MS = 500;
52019
52305
  var DEFAULT_RECONNECT_MAX_MS = 1e4;
@@ -52133,7 +52419,7 @@ var RemoteWorker = class {
52133
52419
  #reconnectBaseMs;
52134
52420
  #reconnectMaxMs;
52135
52421
  #heartbeatMs;
52136
- #workerId = randomUUID3();
52422
+ #workerId = randomUUID5();
52137
52423
  #onStatus;
52138
52424
  constructor(options) {
52139
52425
  this.#origin = new URL(options.origin);
@@ -52177,7 +52463,7 @@ var RemoteWorker = class {
52177
52463
  if (error46 instanceof DeviceRejectedError || error46 instanceof RelayAccessProfileUnsupportedError) {
52178
52464
  throw error46;
52179
52465
  }
52180
- const delay4 = reconnectDelayMs(
52466
+ const delay5 = reconnectDelayMs(
52181
52467
  failures,
52182
52468
  this.#random,
52183
52469
  this.#reconnectBaseMs,
@@ -52187,10 +52473,10 @@ var RemoteWorker = class {
52187
52473
  this.#onStatus({
52188
52474
  state: "retrying",
52189
52475
  error: error46 instanceof Error ? error46 : new Error(String(error46)),
52190
- retryInMs: delay4
52476
+ retryInMs: delay5
52191
52477
  });
52192
52478
  try {
52193
- await this.#sleep(delay4, this.#signal);
52479
+ await this.#sleep(delay5, this.#signal);
52194
52480
  } catch (sleepError) {
52195
52481
  if (this.#signal.aborted) return;
52196
52482
  throw sleepError;
@@ -52269,7 +52555,7 @@ var RemoteWorker = class {
52269
52555
  const key = JSON.stringify(body);
52270
52556
  if (seenAttempts.has(key)) return;
52271
52557
  seenAttempts.add(key);
52272
- attempts.push({ body, legacyRelay, profileIncluded: includeProfile });
52558
+ attempts.push({ body, legacyRelay });
52273
52559
  };
52274
52560
  if (this.#accessProfile) {
52275
52561
  for (const metadata of metadataPhases) {
@@ -52294,7 +52580,7 @@ var RemoteWorker = class {
52294
52580
  );
52295
52581
  }
52296
52582
  }
52297
- attempts.push({ body: {}, legacyRelay: true, profileIncluded: false });
52583
+ attempts.push({ body: {}, legacyRelay: true });
52298
52584
  }
52299
52585
  for (const attempt of attempts) {
52300
52586
  let response;
@@ -52314,17 +52600,22 @@ var RemoteWorker = class {
52314
52600
  throw new Error("The relay returned an invalid registration response.");
52315
52601
  }
52316
52602
  const workerToken = "workerToken" in value ? optionalWorkerToken(value.workerToken) : void 0;
52317
- return {
52603
+ const accessProfileAccepted = this.#accessProfile === void 0 || this.#accessProfile === "system" || "accessProfile" in value && value.accessProfile === this.#accessProfile;
52604
+ const session = {
52318
52605
  generation: value.generation,
52319
52606
  legacyRelay: attempt.legacyRelay,
52320
52607
  concurrentJobs: !attempt.legacyRelay && supportsCapability(value, "concurrentJobs"),
52321
52608
  structuredReads: !attempt.legacyRelay && supportsCapability(value, "structuredReads"),
52322
52609
  structuredMutations: !attempt.legacyRelay && supportsCapability(value, "structuredMutations"),
52323
52610
  commandOutputRanges: !attempt.legacyRelay && supportsCapability(value, "commandOutputRanges"),
52324
- accessProfileAccepted: this.#accessProfile === void 0 || "accessProfile" in value && value.accessProfile === this.#accessProfile || this.#accessProfile === "system" && !attempt.profileIncluded,
52611
+ accessProfileAccepted,
52325
52612
  workspaceLabelAccepted: this.#workspaceLabel === void 0 || "workspaceLabel" in value && value.workspaceLabel === this.#workspaceLabel,
52326
52613
  ...workerToken ? { workerToken } : {}
52327
52614
  };
52615
+ if (!accessProfileAccepted && this.#accessProfile) {
52616
+ throw new RelayAccessProfileUnsupportedError(this.#accessProfile);
52617
+ }
52618
+ return session;
52328
52619
  }
52329
52620
  if (this.#accessProfile && this.#accessProfile !== "system") {
52330
52621
  throw new RelayAccessProfileUnsupportedError(this.#accessProfile);
@@ -52567,10 +52858,10 @@ var RemoteWorker = class {
52567
52858
  await unregister(`Device ${this.#deviceToken}`);
52568
52859
  }
52569
52860
  }
52570
- async #post(path13, body, workerToken) {
52861
+ async #post(path14, body, workerToken) {
52571
52862
  const timeout = AbortSignal.timeout(WORKER_REQUEST_TIMEOUT_MS);
52572
52863
  const signal = AbortSignal.any([this.#signal, timeout]);
52573
- const response = await this.#fetcher(new URL(path13, this.#origin), {
52864
+ const response = await this.#fetcher(new URL(path14, this.#origin), {
52574
52865
  method: "POST",
52575
52866
  headers: {
52576
52867
  authorization: workerToken ? `Worker ${workerToken}` : `Device ${this.#deviceToken}`,
@@ -52691,56 +52982,27 @@ function visibleWorker(worker, options) {
52691
52982
  }
52692
52983
  async function deviceForSession(endpoints, dependencies = {}, signal) {
52693
52984
  const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
52694
- const loadLogin = dependencies.loadCredentials ?? loadCredentials;
52695
- const validate2 = dependencies.validCredentials ?? validCredentials;
52696
- const subjectFor = dependencies.accessTokenSubject ?? accessTokenSubject;
52697
52985
  const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
52698
- const enroll = dependencies.enrollDevice ?? enrollDevice;
52699
52986
  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;
52987
+ const pair = dependencies.pairDevice ?? pairDevice;
52988
+ const revoke = dependencies.revokePairedDevice ?? revokePairedDevice;
52989
+ const withPairingLease = dependencies.withDevicePairingLease ?? withDevicePairingLease;
52704
52990
  signal?.throwIfAborted();
52705
52991
  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);
52992
+ if (stored?.relayOrigin === endpoints.relayOrigin) return stored;
52993
+ return await withPairingLease(async () => {
52994
+ signal?.throwIfAborted();
52995
+ const current = await loadDevice();
52996
+ if (current?.relayOrigin === endpoints.relayOrigin) return current;
52997
+ if (current) {
52998
+ await revoke({ relayOrigin: current.relayOrigin }, current);
52999
+ await removeDevice();
53000
+ }
53001
+ signal?.throwIfAborted();
53002
+ const paired = await pair(endpoints, signal);
53003
+ await saveDevice(paired);
53004
+ return paired;
53005
+ }, signal);
52744
53006
  }
52745
53007
  function retryMessage(retryInMs) {
52746
53008
  const seconds = Math.max(1, Math.ceil(retryInMs / 1e3));
@@ -52852,9 +53114,6 @@ async function connectRemoteWorker(endpoints, device, worker, options, signal, o
52852
53114
  await connectHintTask;
52853
53115
  }
52854
53116
  }
52855
- function shouldRecoverRejectedDevice(error46, recoveredRejectedDevice, connected) {
52856
- return error46 instanceof DeviceRejectedError && !recoveredRejectedDevice && !connected;
52857
- }
52858
53117
  async function runManagedSession(root, endpoints, options = {}) {
52859
53118
  const controller = new AbortController();
52860
53119
  const stop = () => controller.abort();
@@ -52869,9 +53128,9 @@ async function runManagedSession(root, endpoints, options = {}) {
52869
53128
  try {
52870
53129
  const accessProfile = options.accessProfile ?? DEFAULT_WORKER_ACCESS_PROFILE;
52871
53130
  const sessionOptions = { ...options, accessProfile };
52872
- let device = await deviceForSession(
53131
+ const device = options.device ?? await deviceForSession(
52873
53132
  endpoints,
52874
- options.credentials ? { credentials: options.credentials } : {},
53133
+ {},
52875
53134
  controller.signal
52876
53135
  );
52877
53136
  controller.signal.throwIfAborted();
@@ -52892,41 +53151,18 @@ async function runManagedSession(root, endpoints, options = {}) {
52892
53151
  console.error(accessProfileSummary(accessProfile));
52893
53152
  console.error("Press Ctrl+C to disconnect.");
52894
53153
  }
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
- }
53154
+ await connectRemoteWorker(
53155
+ endpoints,
53156
+ device,
53157
+ worker,
53158
+ sessionOptions,
53159
+ controller.signal,
53160
+ () => void 0
53161
+ );
52926
53162
  } catch (error46) {
52927
53163
  if (error46 instanceof DeviceRejectedError) {
52928
53164
  await deleteDeviceCredential();
52929
- throw new Error("The relay rejected this device. Run Glossa again to reenroll it.");
53165
+ throw new Error("The relay rejected this paired computer. Run Glossa again to pair it with your account.");
52930
53166
  }
52931
53167
  throw error46;
52932
53168
  } finally {
@@ -52951,7 +53187,7 @@ function initialHudState(workspace) {
52951
53187
  message: void 0,
52952
53188
  activities: [],
52953
53189
  activityPage: 0,
52954
- view: "activity",
53190
+ view: "workspace",
52955
53191
  status: void 0,
52956
53192
  deviceSelection: 0,
52957
53193
  pendingAccessProfile: void 0,
@@ -53022,12 +53258,12 @@ function formatByteCount(value) {
53022
53258
  }
53023
53259
  return `${(kibibytes / 1024).toFixed(1)} MiB`;
53024
53260
  }
53025
- function workspacePath(path13) {
53026
- return path13 || ".";
53261
+ function workspacePath(path14) {
53262
+ return path14 || ".";
53027
53263
  }
53028
- function pathSummary(path13, details = []) {
53264
+ function pathSummary(path14, details = []) {
53029
53265
  return {
53030
- target: `path ${quoteActivityInput(path13)}`,
53266
+ target: `path ${quoteActivityInput(path14)}`,
53031
53267
  details,
53032
53268
  truncation: "middle"
53033
53269
  };
@@ -53226,6 +53462,7 @@ var COLORS = {
53226
53462
  muted: "#aaa4b5",
53227
53463
  purple: "#8054ff",
53228
53464
  purpleReadable: "#ad98ff",
53465
+ success: "#65d6a6",
53229
53466
  coral: "#ff665f",
53230
53467
  line: "#5c556e"
53231
53468
  };
@@ -53233,7 +53470,9 @@ function Text2({ color, ...props }) {
53233
53470
  return color === void 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { ...props }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text, { ...props, color });
53234
53471
  }
53235
53472
  var ACTIVITY_REFRESH_INTERVAL_MS = 1e4;
53473
+ var ACTIVITY_STATUS_COLUMN_WIDTH = 2;
53236
53474
  var ACTIVITY_TOOL_COLUMN_WIDTH = 15;
53475
+ var ACTIVITY_AGE_COLUMN_WIDTH = 10;
53237
53476
  var ACTIVITY_PREAMBLE_LINES = 1;
53238
53477
  var HudStore = class {
53239
53478
  #state;
@@ -53279,10 +53518,10 @@ function activityAge(updatedAt, working, now) {
53279
53518
  if (elapsedMs < 864e5) return `${Math.floor(elapsedMs / 36e5)}h ago`;
53280
53519
  return `${Math.floor(elapsedMs / 864e5)}d ago`;
53281
53520
  }
53282
- function activityColor(activity) {
53283
- if (activity.state === "working") return COLORS.ink;
53284
- if (activity.state === "failed") return COLORS.coral;
53285
- return COLORS.purpleReadable;
53521
+ function activityStatus(activity) {
53522
+ if (activity.state === "failed") return { symbol: "\xD7", tone: COLORS.coral };
53523
+ if (activity.state === "working") return { symbol: "\u25CB", tone: COLORS.purpleReadable };
53524
+ return { symbol: "\u2713", tone: COLORS.success };
53286
53525
  }
53287
53526
  function activitySummary(activity) {
53288
53527
  return [activity.summary.target, ...activity.summary.details].join(" \xB7 ");
@@ -53298,31 +53537,28 @@ function adjacentAccessProfile(accessProfile, direction) {
53298
53537
  const index = ACCESS_PROFILES.indexOf(accessProfile);
53299
53538
  return ACCESS_PROFILES[index + direction];
53300
53539
  }
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
- ];
53540
+ function accessProfileCapabilities(accessProfile) {
53541
+ if (accessProfile === "read-only") {
53542
+ return { summary: "Read files only", detail: "No changes \xB7 no commands" };
53543
+ }
53544
+ if (accessProfile === "workspace") {
53545
+ return { summary: "Read + write files", detail: "Commands disabled" };
53546
+ }
53547
+ return {
53548
+ summary: "Read + write files + commands",
53549
+ detail: "OS account permissions apply"
53550
+ };
53551
+ }
53552
+ function primaryFooterHints() {
53553
+ return [
53554
+ { key: "A", label: "Activity" },
53555
+ { key: "W", label: "Workspace" },
53556
+ { key: "D", label: "Devices" },
53557
+ { key: "?", label: "Help" },
53558
+ { key: "L", label: "Account sign out", tone: COLORS.coral },
53559
+ { key: "Q", label: "Quit", tone: COLORS.coral }
53560
+ ];
53561
+ }
53326
53562
  function contextualFooterHints(state) {
53327
53563
  if (state.view === "activity") {
53328
53564
  return [
@@ -53370,7 +53606,7 @@ function splitFooterHints(hints, usable) {
53370
53606
  return rows;
53371
53607
  }
53372
53608
  function buildFooterRows(state, usable) {
53373
- const rows = splitFooterHints(PRIMARY_FOOTER_HINTS, usable).map((left) => ({
53609
+ const rows = splitFooterHints(primaryFooterHints(), usable).map((left) => ({
53374
53610
  left,
53375
53611
  right: []
53376
53612
  }));
@@ -53391,7 +53627,10 @@ function promptText(state) {
53391
53627
  if (state.busy) return { message: "Working\u2026" };
53392
53628
  if (!state.prompt) return void 0;
53393
53629
  if (state.prompt.type === "logout") {
53394
- return { message: "Sign out and disconnect?", choices: "Y confirm N cancel" };
53630
+ return {
53631
+ message: "Sign out the CLI account and disconnect? Computer pairing stays active.",
53632
+ choices: "Y confirm N cancel"
53633
+ };
53395
53634
  }
53396
53635
  if (state.prompt.type === "access-confirm") {
53397
53636
  const detail = state.prompt.accessProfile === "system" ? "Commands will inherit this OS account's permissions." : "This allows guarded file writes.";
@@ -53462,7 +53701,7 @@ function Line({ usable, color }) {
53462
53701
  }
53463
53702
  function Header({ state, usable, bodyBudget, color }) {
53464
53703
  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;
53704
+ const pageTitle = state.view === "activity" ? "Activity" : state.view === "devices" ? "Devices" : state.view === "workspace" ? "Workspace" : state.view === "help" ? "Help" : void 0;
53466
53705
  const activityPage = state.view === "activity" ? activityPageInfo(state, bodyBudget) : void 0;
53467
53706
  const activityRange = activityPage && activityPage.maxPage > 0 ? ` (${activityPage.rangeStart}-${activityPage.rangeEnd}/${state.activities.length})` : "";
53468
53707
  const deviceWindow = state.view === "devices" ? deviceListWindow(state, bodyBudget, usable) : void 0;
@@ -53488,46 +53727,118 @@ function SectionTitle({ children, color, tone = COLORS.purpleReadable }) {
53488
53727
  function Blank() {
53489
53728
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { height: 1 });
53490
53729
  }
53491
- function WorkspaceView({ state, bodyBudget, color }) {
53730
+ function ActivityPreviewHeader({ usable, color }) {
53731
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { width: usable, children: [
53732
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { flexGrow: 1, flexShrink: 1, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Activity" }) }),
53733
+ usable >= 24 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginLeft: 1, flexShrink: 0, children: [
53734
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, color: color ? COLORS.purpleReadable : void 0, children: "A" }),
53735
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: " View all" })
53736
+ ] }) : null
53737
+ ] });
53738
+ }
53739
+ function AccessSectionTitle({ accessProfile, usable, color }) {
53740
+ const lower = adjacentAccessProfile(accessProfile, -1);
53741
+ const higher = adjacentAccessProfile(accessProfile, 1);
53742
+ const key = lower && higher ? "\u2190/\u2192" : lower ? "\u2190" : "\u2192";
53743
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { width: usable, children: [
53744
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Access" }),
53745
+ usable >= 24 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { marginLeft: 3, flexShrink: 0, children: [
53746
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, color: color ? COLORS.purpleReadable : void 0, children: key }),
53747
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: " Switch" })
53748
+ ] }) : null
53749
+ ] });
53750
+ }
53751
+ function activityLayout(usable) {
53752
+ const toolWidth = Math.min(
53753
+ ACTIVITY_TOOL_COLUMN_WIDTH,
53754
+ Math.max(0, usable - ACTIVITY_STATUS_COLUMN_WIDTH)
53755
+ );
53756
+ return {
53757
+ toolWidth,
53758
+ showSummary: usable >= ACTIVITY_STATUS_COLUMN_WIDTH + toolWidth + 5,
53759
+ showAge: usable >= ACTIVITY_STATUS_COLUMN_WIDTH + toolWidth + ACTIVITY_AGE_COLUMN_WIDTH + 10
53760
+ };
53761
+ }
53762
+ function WorkspaceView({ state, usable, bodyBudget, color, now }) {
53492
53763
  const displayedAccessProfile = state.pendingAccessProfile ?? state.accessProfile;
53764
+ const showConnectionMessage = Boolean(
53765
+ state.message && (state.connection === "retrying" || state.connection === "error")
53766
+ );
53767
+ let fixedLines = 3;
53768
+ if (displayedAccessProfile) fixedLines += 5;
53769
+ if (state.deviceName) fixedLines += 3;
53770
+ if (showConnectionMessage) fixedLines += 2;
53771
+ const activityCapacity = Math.min(3, Math.max(0, bodyBudget - fixedLines - 2));
53772
+ const activityPreview = activityCapacity > 0 ? state.activities.slice(-activityCapacity) : [];
53493
53773
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { height: bodyBudget, flexDirection: "column", flexShrink: 0, overflow: "hidden", children: [
53494
53774
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53495
53775
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Workspace" }),
53496
53776
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.ink : void 0, wrap: "truncate-middle", children: state.workspace }),
53497
53777
  displayedAccessProfile ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53498
53778
  /* @__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) })
53779
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53780
+ AccessSectionTitle,
53781
+ {
53782
+ accessProfile: displayedAccessProfile,
53783
+ usable,
53784
+ color
53785
+ }
53786
+ ),
53787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { bold: true, color: color ? COLORS.ink : void 0, wrap: "truncate", children: accessProfileLabel(displayedAccessProfile) }),
53788
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, wrap: "truncate", children: accessProfileCapabilities(displayedAccessProfile).summary }),
53789
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, wrap: "truncate", children: accessProfileCapabilities(displayedAccessProfile).detail })
53502
53790
  ] }) : null,
53503
53791
  state.deviceName ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53504
53792
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53505
53793
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "Device" }),
53506
53794
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.ink : void 0, wrap: "truncate", children: state.deviceName })
53507
53795
  ] }) : null,
53508
- state.message && (state.connection === "retrying" || state.connection === "error") ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53796
+ activityCapacity > 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53797
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53798
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ActivityPreviewHeader, { usable, color }),
53799
+ 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)(
53800
+ ActivityRow,
53801
+ {
53802
+ activity,
53803
+ usable,
53804
+ color,
53805
+ now
53806
+ },
53807
+ activity.requestId
53808
+ ))
53809
+ ] }) : null,
53810
+ showConnectionMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
53509
53811
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53510
53812
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.coral : void 0, wrap: "truncate", children: state.message })
53511
53813
  ] }) : null
53512
53814
  ] });
53513
53815
  }
53514
53816
  function ActivityRow({ activity, usable, color, now }) {
53515
- const toolWidth = Math.min(ACTIVITY_TOOL_COLUMN_WIDTH, usable);
53817
+ const { toolWidth, showSummary, showAge } = activityLayout(usable);
53516
53818
  const age = activityAge(activity.updatedAt, activity.state === "working", now);
53517
- const showAge = usable >= toolWidth + age.length + 10;
53518
- const showSummary = usable >= toolWidth + 5;
53819
+ const status = activityStatus(activity);
53519
53820
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Box_default, { width: usable, height: 1, flexShrink: 0, children: [
53821
+ /* @__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
53822
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Box_default, { width: toolWidth, flexShrink: 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53521
53823
  Text2,
53522
53824
  {
53523
53825
  bold: true,
53524
- color: color ? activityColor(activity) : void 0,
53826
+ color: color ? COLORS.ink : void 0,
53525
53827
  wrap: "truncate",
53526
53828
  children: activity.tool
53527
53829
  }
53528
53830
  ) }),
53529
53831
  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
53832
+ showAge ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
53833
+ Box_default,
53834
+ {
53835
+ width: ACTIVITY_AGE_COLUMN_WIDTH,
53836
+ marginLeft: 2,
53837
+ flexShrink: 0,
53838
+ justifyContent: "flex-end",
53839
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Text2, { color: color ? COLORS.muted : void 0, children: age })
53840
+ }
53841
+ ) : null
53531
53842
  ] });
53532
53843
  }
53533
53844
  function ActivityView({ state, usable, bodyBudget, color, now }) {
@@ -53662,12 +53973,12 @@ function HelpView({ bodyBudget, color }) {
53662
53973
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "W", label: "Workspace", color }),
53663
53974
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "D", label: "Devices", color }),
53664
53975
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "?", label: "Help", color }),
53665
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Esc", label: "Activity", color }),
53976
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Esc", label: "Workspace", color }),
53666
53977
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53667
53978
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, tone: COLORS.coral, children: "Manage" }),
53668
53979
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "\u2190/\u2192", label: "Change workspace access", color, tone: COLORS.coral }),
53669
53980
  /* @__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 }),
53981
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "L", label: "Account sign out", color, tone: COLORS.coral }),
53671
53982
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Blank, {}),
53672
53983
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SectionTitle, { color, children: "App" }),
53673
53984
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpRow, { keyLabel: "Q", label: "Disconnect and quit", color, tone: COLORS.coral }),
@@ -53741,7 +54052,16 @@ function HudScreen({ state, columns, rows, color = true, now = Date.now() }) {
53741
54052
  bodyBudget: metrics.bodyBudget,
53742
54053
  color
53743
54054
  }
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 }),
54055
+ ) : state.view === "workspace" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
54056
+ WorkspaceView,
54057
+ {
54058
+ state,
54059
+ usable: metrics.usable,
54060
+ bodyBudget: metrics.bodyBudget,
54061
+ color,
54062
+ now
54063
+ }
54064
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(HelpView, { bodyBudget: metrics.bodyBudget, color }),
53745
54065
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Overlay, { state, usable: metrics.usable, color }),
53746
54066
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Footer, { rows: metrics.footerRows, usable: metrics.usable, color })
53747
54067
  ] });
@@ -53802,7 +54122,7 @@ function HudRuntime({ store: store3, actions, signal, stop }) {
53802
54122
  const { columns, rows } = use_window_size_default();
53803
54123
  const [, setClock] = (0, import_react34.useState)(0);
53804
54124
  (0, import_react34.useEffect)(() => {
53805
- if (state.view !== "activity" || state.activities.length === 0) return;
54125
+ if (state.view !== "activity" && state.view !== "workspace" || state.activities.length === 0) return;
53806
54126
  const timer = setInterval(() => setClock((value) => value + 1), ACTIVITY_REFRESH_INTERVAL_MS);
53807
54127
  return () => clearInterval(timer);
53808
54128
  }, [state.view, state.activities.length]);
@@ -53914,7 +54234,7 @@ function HudRuntime({ store: store3, actions, signal, stop }) {
53914
54234
  return;
53915
54235
  }
53916
54236
  if (key.escape) {
53917
- store3.update((state2) => ({ ...state2, view: "activity", notice: void 0 }));
54237
+ store3.update((state2) => ({ ...state2, view: "workspace", notice: void 0 }));
53918
54238
  return;
53919
54239
  }
53920
54240
  if (value === "a") {
@@ -54061,119 +54381,8 @@ async function runSessionHud(actions, input = process.stdin, output = process.st
54061
54381
  var import_semver = __toESM(require_semver2(), 1);
54062
54382
  import { spawn as spawn3 } from "node:child_process";
54063
54383
  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
54384
+ import { chmod as chmod3, readdir as readdir2, rename as rename2, rm as rm5, writeFile as writeFile6 } from "node:fs/promises";
54385
+ import path9 from "node:path";
54177
54386
  var PACKAGE_NAME = "@ariobarin/glossa";
54178
54387
  var DEFAULT_REGISTRY_URL = "https://registry.npmjs.org/@ariobarin%2Fglossa";
54179
54388
  var DEFAULT_RELEASE_BASE_URL = "https://github.com/ariobarin/glossa/releases/download";
@@ -54250,8 +54459,8 @@ async function cleanupUpdateBackups(distribution, options = {}) {
54250
54459
  const platform2 = options.platform ?? process.platform;
54251
54460
  if (distribution !== "standalone" || platform2 !== "win32") return;
54252
54461
  const executable = options.executablePath ?? process.execPath;
54253
- const directory = path8.dirname(executable);
54254
- const prefix = `${path8.basename(executable)}.old-`;
54462
+ const directory = path9.dirname(executable);
54463
+ const prefix = `${path9.basename(executable)}.old-`;
54255
54464
  let entries;
54256
54465
  try {
54257
54466
  entries = await readdir2(directory, { withFileTypes: true });
@@ -54264,7 +54473,7 @@ async function cleanupUpdateBackups(distribution, options = {}) {
54264
54473
  const ownerPid = backupOwnerPid(entry.name, prefix);
54265
54474
  return ownerPid !== null && !processIsAlive(ownerPid);
54266
54475
  }).map(async (entry) => {
54267
- await rm4(path8.join(directory, entry.name), { force: true }).catch(() => void 0);
54476
+ await rm5(path9.join(directory, entry.name), { force: true }).catch(() => void 0);
54268
54477
  })
54269
54478
  );
54270
54479
  }
@@ -54340,12 +54549,12 @@ async function installStandaloneUpdate(info, options) {
54340
54549
  if (actual !== expected) {
54341
54550
  throw new Error("Glossa refused to update because the SHA-256 checksum did not match.");
54342
54551
  }
54343
- const download = path8.join(
54344
- path8.dirname(destination),
54345
- `${path8.basename(destination)}.download-${process.pid}`
54552
+ const download = path9.join(
54553
+ path9.dirname(destination),
54554
+ `${path9.basename(destination)}.download-${process.pid}`
54346
54555
  );
54347
54556
  try {
54348
- await writeFile5(download, binary, { mode: 493 });
54557
+ await writeFile6(download, binary, { mode: 493 });
54349
54558
  if (platform2 === "win32") {
54350
54559
  await replaceWindowsExecutable(download, destination);
54351
54560
  } else {
@@ -54354,7 +54563,7 @@ async function installStandaloneUpdate(info, options) {
54354
54563
  }
54355
54564
  return "installed";
54356
54565
  } catch (error46) {
54357
- await rm4(download, { force: true });
54566
+ await rm5(download, { force: true });
54358
54567
  throw error46;
54359
54568
  }
54360
54569
  }
@@ -54368,14 +54577,14 @@ async function installUpdate(info, distribution, options = {}) {
54368
54577
  }
54369
54578
 
54370
54579
  // 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";
54580
+ import { chmod as chmod4, mkdir as mkdir6, readFile as readFile5, rename as rename3, rm as rm6, writeFile as writeFile7 } from "node:fs/promises";
54581
+ import path10 from "node:path";
54373
54582
  var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
54374
54583
  function defaultUpdateChannel(version2) {
54375
54584
  return version2.includes("-") ? "beta" : "stable";
54376
54585
  }
54377
54586
  function updateStateFile() {
54378
- return path9.join(configDirectory(), "updates.json");
54587
+ return path10.join(configDirectory(), "updates.json");
54379
54588
  }
54380
54589
  function isPolicy(value) {
54381
54590
  return value === "notify" || value === "auto" || value === "off";
@@ -54392,7 +54601,7 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
54392
54601
  channel: defaultUpdateChannel(currentVersion)
54393
54602
  };
54394
54603
  try {
54395
- const parsed = JSON.parse(await readFile4(file2, "utf8"));
54604
+ const parsed = JSON.parse(await readFile5(file2, "utf8"));
54396
54605
  const lastCheckedAt = optionalString(parsed.lastCheckedAt);
54397
54606
  const latestVersion = optionalString(parsed.latestVersion);
54398
54607
  return {
@@ -54409,11 +54618,11 @@ async function loadUpdateState(currentVersion, file2 = updateStateFile()) {
54409
54618
  }
54410
54619
  }
54411
54620
  async function saveUpdateState(state, file2 = updateStateFile()) {
54412
- const directory = path9.dirname(file2);
54621
+ const directory = path10.dirname(file2);
54413
54622
  const temporary = `${file2}.${process.pid}.tmp`;
54414
- await mkdir5(directory, { recursive: true, mode: 448 });
54623
+ await mkdir6(directory, { recursive: true, mode: 448 });
54415
54624
  try {
54416
- await writeFile6(temporary, `${JSON.stringify(state, null, 2)}
54625
+ await writeFile7(temporary, `${JSON.stringify(state, null, 2)}
54417
54626
  `, {
54418
54627
  encoding: "utf8",
54419
54628
  mode: 384
@@ -54421,7 +54630,7 @@ async function saveUpdateState(state, file2 = updateStateFile()) {
54421
54630
  if (process.platform !== "win32") await chmod4(temporary, 384);
54422
54631
  await rename3(temporary, file2);
54423
54632
  } finally {
54424
- await rm5(temporary, { force: true });
54633
+ await rm6(temporary, { force: true });
54425
54634
  }
54426
54635
  }
54427
54636
  async function configureUpdates(currentVersion, changes, file2 = updateStateFile()) {
@@ -54448,15 +54657,31 @@ function isUpdateCheckDue(lastCheckedAt, now = Date.now()) {
54448
54657
  return !Number.isFinite(checkedAt) || now - checkedAt >= UPDATE_CHECK_INTERVAL_MS;
54449
54658
  }
54450
54659
 
54660
+ // src/unpair.ts
54661
+ async function unpairComputer(dependencies = {}) {
54662
+ const load = dependencies.loadDeviceCredential ?? loadDeviceCredential;
54663
+ const remove = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
54664
+ const revoke = dependencies.revokePairedDevice ?? revokePairedDevice;
54665
+ const log = dependencies.log ?? console.log;
54666
+ const device = await load();
54667
+ if (!device) {
54668
+ log("This computer is not paired with Glossa.");
54669
+ return;
54670
+ }
54671
+ await revoke({ relayOrigin: device.relayOrigin }, device);
54672
+ await remove();
54673
+ log("Unpaired this computer from Glossa.");
54674
+ }
54675
+
54451
54676
  // 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";
54677
+ import { appendFile, chmod as chmod5, mkdir as mkdir7, readFile as readFile6 } from "node:fs/promises";
54678
+ import path11 from "node:path";
54454
54679
  function usageFile() {
54455
- return path10.join(configDirectory(), "usage.jsonl");
54680
+ return path11.join(configDirectory(), "usage.jsonl");
54456
54681
  }
54457
54682
  async function recordUsageEvent(event, file2 = usageFile()) {
54458
- const directory = path10.dirname(file2);
54459
- await mkdir6(directory, { recursive: true, mode: 448 });
54683
+ const directory = path11.dirname(file2);
54684
+ await mkdir7(directory, { recursive: true, mode: 448 });
54460
54685
  await appendFile(file2, `${JSON.stringify(event)}
54461
54686
  `, {
54462
54687
  encoding: "utf8",
@@ -54474,15 +54699,15 @@ async function recordToolUsage(tool, ok, at = /* @__PURE__ */ new Date(), file2
54474
54699
  // src/worker/root-selection.ts
54475
54700
  import { realpath as realpath2 } from "node:fs/promises";
54476
54701
  import os6 from "node:os";
54477
- import path11 from "node:path";
54702
+ import path12 from "node:path";
54478
54703
  function containsPath(root, candidate) {
54479
- const relative = path11.relative(root, candidate);
54480
- return relative !== "" && relative !== ".." && !relative.startsWith(`..${path11.sep}`) && !path11.isAbsolute(relative);
54704
+ const relative = path12.relative(root, candidate);
54705
+ return relative !== "" && relative !== ".." && !relative.startsWith(`..${path12.sep}`) && !path12.isAbsolute(relative);
54481
54706
  }
54482
54707
  async function rejectImplicitHomeAncestor(root) {
54483
54708
  const homes = await Promise.all(
54484
54709
  [os6.homedir(), accountHomeDirectory()].map(
54485
- async (home) => await realpath2(home).catch(() => path11.resolve(home))
54710
+ async (home) => await realpath2(home).catch(() => path12.resolve(home))
54486
54711
  )
54487
54712
  );
54488
54713
  if (homes.some((home) => containsPath(root, home))) {
@@ -54500,10 +54725,10 @@ async function selectExposureRoot(explicitPath, cwd2 = process.cwd()) {
54500
54725
 
54501
54726
  // src/worker/workspace-lease.ts
54502
54727
  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";
54728
+ import { lstat as lstat3, mkdir as mkdir8, rm as rm7, stat as stat4 } from "node:fs/promises";
54504
54729
  import net from "node:net";
54505
- import path12 from "node:path";
54506
- import { setTimeout as delay3 } from "node:timers/promises";
54730
+ import path13 from "node:path";
54731
+ import { setTimeout as delay4 } from "node:timers/promises";
54507
54732
  var GUARD_STALE_MS = 5e3;
54508
54733
  var GUARD_RETRY_MS = 10;
54509
54734
  var GUARD_TIMEOUT_MS = 1e4;
@@ -54526,12 +54751,12 @@ function workspaceIdentity(root) {
54526
54751
  }
54527
54752
  function defaultLeaseDirectory() {
54528
54753
  if (typeof process.getuid === "function") {
54529
- return path12.join("/tmp", `glossa-workspaces-${process.getuid()}`);
54754
+ return path13.join("/tmp", `glossa-workspaces-${process.getuid()}`);
54530
54755
  }
54531
- return path12.join(accountHomeDirectory(), ".glossa-workspace-leases");
54756
+ return path13.join(accountHomeDirectory(), ".glossa-workspace-leases");
54532
54757
  }
54533
54758
  async function ensureLeaseDirectory(directory) {
54534
- await mkdir7(directory, { recursive: true, mode: 448 });
54759
+ await mkdir8(directory, { recursive: true, mode: 448 });
54535
54760
  const directoryStat = await lstat3(directory);
54536
54761
  if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
54537
54762
  throw new Error("The Glossa workspace lease location is not a private directory.");
@@ -54548,26 +54773,26 @@ function endpointFor(identity, directory) {
54548
54773
  if (process.platform === "win32") {
54549
54774
  return `\\\\.\\pipe\\glossa-workspace-${identity}`;
54550
54775
  }
54551
- return path12.join(directory, `${identity}.sock`);
54776
+ return path13.join(directory, `${identity}.sock`);
54552
54777
  }
54553
- async function acquireGuard(directory, identity) {
54554
- const guard = path12.join(directory, `${identity}.guard`);
54778
+ async function acquireGuard2(directory, identity) {
54779
+ const guard = path13.join(directory, `${identity}.guard`);
54555
54780
  const deadline = Date.now() + GUARD_TIMEOUT_MS;
54556
54781
  while (true) {
54557
54782
  try {
54558
- await mkdir7(guard, { mode: 448 });
54559
- return async () => await rm6(guard, { recursive: true, force: true });
54783
+ await mkdir8(guard, { mode: 448 });
54784
+ return async () => await rm7(guard, { recursive: true, force: true });
54560
54785
  } catch (error46) {
54561
54786
  if (error46.code !== "EEXIST") throw error46;
54562
54787
  let stale = false;
54563
54788
  try {
54564
- stale = Date.now() - (await stat3(guard)).mtimeMs >= GUARD_STALE_MS;
54789
+ stale = Date.now() - (await stat4(guard)).mtimeMs >= GUARD_STALE_MS;
54565
54790
  } catch (statError) {
54566
54791
  if (statError.code === "ENOENT") continue;
54567
54792
  throw statError;
54568
54793
  }
54569
54794
  if (stale) {
54570
- await rm6(guard, { recursive: true, force: true });
54795
+ await rm7(guard, { recursive: true, force: true });
54571
54796
  continue;
54572
54797
  }
54573
54798
  if (Date.now() >= deadline) {
@@ -54575,7 +54800,7 @@ async function acquireGuard(directory, identity) {
54575
54800
  "Timed out while checking whether this workspace is already active."
54576
54801
  );
54577
54802
  }
54578
- await delay3(GUARD_RETRY_MS);
54803
+ await delay4(GUARD_RETRY_MS);
54579
54804
  }
54580
54805
  }
54581
54806
  }
@@ -54623,7 +54848,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54623
54848
  await ensureLeaseDirectory(userDirectory);
54624
54849
  const identity = workspaceIdentity(root);
54625
54850
  const endpoint3 = endpointFor(identity, userDirectory);
54626
- const releaseGuard = await acquireGuard(userDirectory, identity);
54851
+ const releaseGuard = await acquireGuard2(userDirectory, identity);
54627
54852
  let server;
54628
54853
  try {
54629
54854
  for (let attempt = 0; attempt < 2; attempt += 1) {
@@ -54637,11 +54862,11 @@ async function acquireWorkspaceLease(root, options = {}) {
54637
54862
  if (released) return;
54638
54863
  released = true;
54639
54864
  await ensureLeaseDirectory(userDirectory);
54640
- const releaseGuard2 = await acquireGuard(userDirectory, identity);
54865
+ const releaseGuard2 = await acquireGuard2(userDirectory, identity);
54641
54866
  try {
54642
54867
  await closeServer(server);
54643
54868
  if (process.platform !== "win32") {
54644
- await rm6(endpoint3, { force: true });
54869
+ await rm7(endpoint3, { force: true });
54645
54870
  }
54646
54871
  } finally {
54647
54872
  await releaseGuard2();
@@ -54654,7 +54879,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54654
54879
  throw new WorkspaceAlreadyActiveError();
54655
54880
  }
54656
54881
  if (error46.code !== "EADDRINUSE") throw error46;
54657
- if (process.platform !== "win32") await rm6(endpoint3, { force: true });
54882
+ if (process.platform !== "win32") await rm7(endpoint3, { force: true });
54658
54883
  }
54659
54884
  }
54660
54885
  throw new Error("Could not reserve this workspace for the current Glossa process.");
@@ -54664,7 +54889,7 @@ async function acquireWorkspaceLease(root, options = {}) {
54664
54889
  }
54665
54890
 
54666
54891
  // src/main.ts
54667
- var VERSION = "0.2.0-beta.2";
54892
+ var VERSION = "0.2.1";
54668
54893
  var DISTRIBUTION = "npm";
54669
54894
  var HELP = `Glossa ${VERSION}
54670
54895
 
@@ -54673,6 +54898,7 @@ Usage:
54673
54898
  glossa status
54674
54899
  glossa devices revoke <id>
54675
54900
  glossa logout
54901
+ glossa unpair
54676
54902
  glossa update [--check]
54677
54903
  glossa update --policy <notify|auto|off>
54678
54904
  glossa update --channel <beta|stable>
@@ -54692,8 +54918,8 @@ Keys:
54692
54918
  \u2191\u2193 select or browse
54693
54919
  \u2190\u2192 change workspace access
54694
54920
  Enter/r revoke selected device
54695
- Esc activity
54696
- l sign out
54921
+ Esc workspace
54922
+ l account sign out
54697
54923
  ? help
54698
54924
  q disconnect and quit`;
54699
54925
  async function withLoginSignal(action) {
@@ -54740,8 +54966,8 @@ async function revokeKnownDevice(deviceId) {
54740
54966
  const credentials = await authenticatedCredentials();
54741
54967
  await revokeDevice(loadRelayEndpoints(), credentials, deviceId);
54742
54968
  }
54743
- async function runWorkspaceSession(path13, label, accessProfile) {
54744
- const root = await selectExposureRoot(path13);
54969
+ async function runWorkspaceSession(path14, label, accessProfile) {
54970
+ const root = await selectExposureRoot(path14);
54745
54971
  const lease = await acquireWorkspaceLease(root);
54746
54972
  let usageWrites = Promise.resolve();
54747
54973
  const queueUsage = (write) => {
@@ -54749,8 +54975,7 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54749
54975
  };
54750
54976
  try {
54751
54977
  const endpoints = loadRelayEndpoints();
54752
- let credentials = (await authenticatedSession()).credentials;
54753
- const statusService = new WorkspaceStatusService(credentials, endpoints);
54978
+ const device = await deviceForSession(endpoints);
54754
54979
  let postExitNotice;
54755
54980
  let requestedAccessProfile = accessProfile;
54756
54981
  let activeSessionController;
@@ -54768,7 +54993,7 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54768
54993
  else signal.addEventListener("abort", stopSession, { once: true });
54769
54994
  try {
54770
54995
  await runManagedSession(root, endpoints, {
54771
- credentials,
54996
+ device,
54772
54997
  workerVersion: VERSION,
54773
54998
  accessProfile: sessionAccessProfile,
54774
54999
  ...label ? { workspaceLabel: label } : {},
@@ -54799,10 +55024,13 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54799
55024
  }
54800
55025
  },
54801
55026
  loadStatus: async (signal) => {
54802
- return hudStatus(await statusService.refresh(signal));
55027
+ const credentials = await authenticatedCredentials(signal);
55028
+ return hudStatus(
55029
+ await new WorkspaceStatusService(credentials, endpoints).refresh(signal)
55030
+ );
54803
55031
  },
54804
55032
  revokeDevice: async (deviceId, signal) => {
54805
- credentials = await validCredentials(credentials, { signal });
55033
+ const credentials = await authenticatedCredentials(signal);
54806
55034
  await revokeDevice(
54807
55035
  endpoints,
54808
55036
  credentials,
@@ -54823,9 +55051,9 @@ async function runWorkspaceSession(path13, label, accessProfile) {
54823
55051
  await lease.release();
54824
55052
  }
54825
55053
  }
54826
- async function runWorkspace(path13, label, accessProfile) {
55054
+ async function runWorkspace(path14, label, accessProfile) {
54827
55055
  await withWorkspaceLease(
54828
- async () => await runWorkspaceSession(path13, label, accessProfile)
55056
+ async () => await runWorkspaceSession(path14, label, accessProfile)
54829
55057
  );
54830
55058
  }
54831
55059
  async function refreshUpdateInfo(timeoutMs) {
@@ -54928,6 +55156,8 @@ async function main() {
54928
55156
  await showStatus();
54929
55157
  } else if (invocation.command === "logout") {
54930
55158
  await logoutFromGlossa();
55159
+ } else if (invocation.command === "unpair") {
55160
+ await unpairComputer();
54931
55161
  } else if (invocation.command === "update") {
54932
55162
  await runUpdateCommand(invocation);
54933
55163
  } else {