@ariobarin/glossa 0.1.0-beta.3 → 0.1.0-beta.6

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.
package/dist/main.js CHANGED
@@ -6,88 +6,52 @@ var __export = (target, all) => {
6
6
  };
7
7
 
8
8
  // src/config-store.ts
9
+ import path2 from "node:path";
10
+
11
+ // src/secure-store.ts
9
12
  import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
10
13
  import os from "node:os";
11
14
  import path from "node:path";
12
15
  var KEYRING_SERVICE = "Glossa";
13
- var KEYRING_ACCOUNT = "oauth";
14
- var FILE_CREDENTIAL_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 credential file.";
15
16
  function configDirectory() {
16
17
  if (process.platform === "win32") {
17
18
  return path.join(process.env.APPDATA ?? os.homedir(), "Glossa");
18
19
  }
19
- return path.join(process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"), "glossa");
20
- }
21
- function credentialsPath() {
22
- return path.join(configDirectory(), "credentials.json");
23
- }
24
- async function defaultEntryProvider() {
25
- try {
26
- const { AsyncEntry } = await import("@napi-rs/keyring");
27
- return new AsyncEntry(KEYRING_SERVICE, KEYRING_ACCOUNT);
28
- } catch {
29
- return null;
30
- }
31
- }
32
- function parseCredentials(value) {
33
- let parsed;
34
- try {
35
- parsed = JSON.parse(value);
36
- } catch {
37
- throw new Error("Stored Glossa credentials are invalid.");
38
- }
39
- if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string" || typeof parsed.audience !== "string" || typeof parsed.accessToken !== "string" || typeof parsed.expiresAt !== "string" || !Number.isFinite(Date.parse(parsed.expiresAt)) || typeof parsed.tokenType !== "string" || parsed.refreshToken !== void 0 && typeof parsed.refreshToken !== "string" || parsed.scope !== void 0 && typeof parsed.scope !== "string") {
40
- throw new Error("Stored Glossa credentials are invalid.");
41
- }
42
- return parsed;
43
- }
44
- async function readFileCredentials(target) {
45
- try {
46
- return parseCredentials(await readFile(target, "utf8"));
47
- } catch (error46) {
48
- const code = error46.code;
49
- if (code === "ENOENT") return null;
50
- throw error46;
51
- }
52
- }
53
- async function writeFileCredentials(target, credentials) {
54
- await mkdir(path.dirname(target), { recursive: true, mode: 448 });
55
- await writeFile(target, `${JSON.stringify(credentials, null, 2)}
56
- `, {
57
- encoding: "utf8",
58
- mode: 384
59
- });
60
- if (process.platform !== "win32") await chmod(target, 384);
20
+ return path.join(
21
+ process.env.XDG_CONFIG_HOME ?? path.join(os.homedir(), ".config"),
22
+ "glossa"
23
+ );
61
24
  }
62
- var CredentialStore = class {
63
- #credentialsFile;
64
- #entryProvider;
65
- #warn;
66
- constructor(options = {}) {
67
- this.#credentialsFile = options.credentialsFile ?? credentialsPath();
68
- this.#entryProvider = options.entryProvider ?? defaultEntryProvider;
69
- this.#warn = options.warn ?? console.warn;
70
- }
71
- async #entry() {
72
- try {
73
- return await this.#entryProvider();
74
- } catch {
75
- return null;
76
- }
25
+ var SecureStore = class {
26
+ #options;
27
+ constructor(options) {
28
+ this.#options = options;
77
29
  }
78
- async save(credentials) {
79
- const serialized = JSON.stringify(credentials);
30
+ async save(value) {
31
+ const serialized = JSON.stringify(value);
80
32
  const entry = await this.#entry();
81
33
  if (entry) {
82
34
  try {
83
35
  await entry.setPassword(serialized);
84
- await rm(this.#credentialsFile, { force: true });
36
+ await rm(this.#options.file, { force: true });
85
37
  return "keyring";
86
38
  } catch {
87
39
  }
88
40
  }
89
- this.#warn(FILE_CREDENTIAL_WARNING);
90
- await writeFileCredentials(this.#credentialsFile, credentials);
41
+ this.#warn();
42
+ await mkdir(path.dirname(this.#options.file), {
43
+ recursive: true,
44
+ mode: 448
45
+ });
46
+ await writeFile(
47
+ this.#options.file,
48
+ `${JSON.stringify(value, null, 2)}
49
+ `,
50
+ { encoding: "utf8", mode: 384 }
51
+ );
52
+ if (process.platform !== "win32") {
53
+ await chmod(this.#options.file, 384);
54
+ }
91
55
  return "file";
92
56
  }
93
57
  async load() {
@@ -98,22 +62,27 @@ var CredentialStore = class {
98
62
  serialized = await entry.getPassword();
99
63
  } catch {
100
64
  }
101
- if (serialized) {
102
- return { credentials: parseCredentials(serialized), backend: "keyring" };
65
+ if (serialized !== void 0) {
66
+ return { value: this.#options.parse(serialized), backend: "keyring" };
103
67
  }
104
68
  }
105
- const credentials = await readFileCredentials(this.#credentialsFile);
106
- if (!credentials) return null;
69
+ let value;
70
+ try {
71
+ value = this.#options.parse(await readFile(this.#options.file, "utf8"));
72
+ } catch (error46) {
73
+ if (error46.code === "ENOENT") return null;
74
+ throw error46;
75
+ }
107
76
  if (entry) {
108
77
  try {
109
- await entry.setPassword(JSON.stringify(credentials));
110
- await rm(this.#credentialsFile, { force: true });
111
- return { credentials, backend: "keyring" };
78
+ await entry.setPassword(JSON.stringify(value));
79
+ await rm(this.#options.file, { force: true });
80
+ return { value, backend: "keyring" };
112
81
  } catch {
113
82
  }
114
83
  }
115
- this.#warn(FILE_CREDENTIAL_WARNING);
116
- return { credentials, backend: "file" };
84
+ this.#warn();
85
+ return { value, backend: "file" };
117
86
  }
118
87
  async delete() {
119
88
  const entry = await this.#entry();
@@ -123,18 +92,71 @@ var CredentialStore = class {
123
92
  } catch {
124
93
  }
125
94
  }
126
- await rm(this.#credentialsFile, { force: true });
95
+ await rm(this.#options.file, { force: true });
96
+ }
97
+ async #entry() {
98
+ if (this.#options.entryProvider) {
99
+ try {
100
+ return await this.#options.entryProvider();
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ try {
106
+ const { AsyncEntry } = await import("@napi-rs/keyring");
107
+ return new AsyncEntry(KEYRING_SERVICE, this.#options.account);
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+ #warn() {
113
+ (this.#options.warn ?? console.warn)(this.#options.warning);
127
114
  }
128
115
  };
129
- var defaultStore = new CredentialStore();
116
+
117
+ // src/config-store.ts
118
+ var FILE_CREDENTIAL_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 credential file.";
119
+ function parseCredentials(value) {
120
+ let parsed;
121
+ try {
122
+ parsed = JSON.parse(value);
123
+ } catch {
124
+ throw new Error("Stored Glossa credentials are invalid.");
125
+ }
126
+ if (typeof parsed.issuer !== "string" || typeof parsed.clientId !== "string" || typeof parsed.audience !== "string" || typeof parsed.accessToken !== "string" || typeof parsed.expiresAt !== "string" || !Number.isFinite(Date.parse(parsed.expiresAt)) || typeof parsed.tokenType !== "string" || parsed.refreshToken !== void 0 && typeof parsed.refreshToken !== "string" || parsed.scope !== void 0 && typeof parsed.scope !== "string" || parsed.requestedScope !== void 0 && typeof parsed.requestedScope !== "string") {
127
+ throw new Error("Stored Glossa credentials are invalid.");
128
+ }
129
+ return parsed;
130
+ }
131
+ var store = new SecureStore({
132
+ account: "oauth",
133
+ file: path2.join(configDirectory(), "credentials.json"),
134
+ warning: FILE_CREDENTIAL_WARNING,
135
+ parse: parseCredentials
136
+ });
130
137
  async function saveCredentials(credentials) {
131
- return await defaultStore.save(credentials);
138
+ return await store.save(credentials);
132
139
  }
133
140
  async function loadCredentials() {
134
- return await defaultStore.load();
141
+ const loaded = await store.load();
142
+ return loaded ? { credentials: loaded.value, backend: loaded.backend } : null;
135
143
  }
136
144
  async function deleteCredentials() {
137
- await defaultStore.delete();
145
+ await store.delete();
146
+ }
147
+
148
+ // src/auth-scopes.ts
149
+ function normalizedScopes(value) {
150
+ return [...new Set(value?.trim().split(/\s+/).filter(Boolean) ?? [])].sort();
151
+ }
152
+ function scopesMatch(left, right) {
153
+ return JSON.stringify(normalizedScopes(left)) === JSON.stringify(normalizedScopes(right));
154
+ }
155
+ function grantedScopesSatisfyRequest(granted, requested, hasRefreshToken) {
156
+ const grantedSet = new Set(normalizedScopes(granted));
157
+ return normalizedScopes(requested).every(
158
+ (scope) => grantedSet.has(scope) || scope === "offline_access" && hasRefreshToken
159
+ );
138
160
  }
139
161
 
140
162
  // src/auth-session.ts
@@ -142,8 +164,14 @@ var EXPIRY_BUFFER_MS = 6e4;
142
164
  function endpoint(issuer, pathname) {
143
165
  return new URL(pathname, issuer.endsWith("/") ? issuer : `${issuer}/`).toString();
144
166
  }
167
+ var SessionExpiredError = class extends Error {
168
+ constructor() {
169
+ super("Session expired. Run Glossa again to sign in.");
170
+ this.name = "SessionExpiredError";
171
+ }
172
+ };
145
173
  function sessionExpiredError() {
146
- return new Error("Session expired. Run: glossa login");
174
+ return new SessionExpiredError();
147
175
  }
148
176
  function oauthMessage(data, status) {
149
177
  return data.error_description ?? data.error ?? `HTTP ${status}`;
@@ -178,13 +206,22 @@ async function refreshCredentials(credentials, dependencies = {}) {
178
206
  }
179
207
  throw new Error(oauthMessage(oauth, response.status));
180
208
  }
209
+ const grantedScope = data.scope ?? credentials.scope;
210
+ if (credentials.requestedScope && !grantedScopesSatisfyRequest(
211
+ grantedScope,
212
+ credentials.requestedScope,
213
+ Boolean(data.refresh_token ?? credentials.refreshToken)
214
+ )) {
215
+ await remove();
216
+ throw sessionExpiredError();
217
+ }
181
218
  const refreshed = {
182
219
  ...credentials,
183
220
  accessToken: data.access_token,
184
221
  refreshToken: data.refresh_token ?? credentials.refreshToken,
185
222
  expiresAt: new Date(now() + data.expires_in * 1e3).toISOString(),
186
223
  tokenType: data.token_type,
187
- ...data.scope ? { scope: data.scope } : {}
224
+ ...grantedScope ? { scope: grantedScope } : {}
188
225
  };
189
226
  await save(refreshed);
190
227
  return refreshed;
@@ -344,6 +381,10 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
344
381
  if (!data.refresh_token) {
345
382
  throw new Error("Auth0 did not issue a refresh token.");
346
383
  }
384
+ const grantedScope = data.scope ?? options.scope;
385
+ if (!grantedScopesSatisfyRequest(grantedScope, options.scope, true)) {
386
+ throw new Error("Auth0 did not grant the permissions Glossa requires.");
387
+ }
347
388
  await save({
348
389
  issuer: options.issuer,
349
390
  clientId: options.clientId,
@@ -352,7 +393,8 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
352
393
  refreshToken: data.refresh_token,
353
394
  expiresAt: new Date(now() + data.expires_in * 1e3).toISOString(),
354
395
  tokenType: data.token_type,
355
- ...data.scope ? { scope: data.scope } : {}
396
+ scope: grantedScope,
397
+ requestedScope: options.scope
356
398
  });
357
399
  log("Signed in to Glossa.");
358
400
  return;
@@ -376,6 +418,141 @@ async function loginWithDeviceFlow(options, dependencies = {}) {
376
418
  }
377
419
  }
378
420
 
421
+ // src/auth-login.ts
422
+ function normalizedIssuer(value) {
423
+ return value.replace(/\/+$/, "");
424
+ }
425
+ function credentialsMatchLoginOptions(credentials, options) {
426
+ return normalizedIssuer(credentials.issuer) === normalizedIssuer(options.issuer) && credentials.clientId === options.clientId && credentials.audience === options.audience && scopesMatch(credentials.requestedScope, options.scope) && grantedScopesSatisfyRequest(
427
+ credentials.scope,
428
+ options.scope,
429
+ Boolean(credentials.refreshToken)
430
+ );
431
+ }
432
+ async function ensureSignedIn(options, dependencies = {}) {
433
+ const load = dependencies.loadCredentials ?? loadCredentials;
434
+ const validate = dependencies.validCredentials ?? validCredentials;
435
+ const login = dependencies.loginWithDeviceFlow ?? loginWithDeviceFlow;
436
+ const loaded = await load();
437
+ if (loaded && credentialsMatchLoginOptions(loaded.credentials, options)) {
438
+ try {
439
+ await validate(loaded.credentials);
440
+ return false;
441
+ } catch (error46) {
442
+ if (!(error46 instanceof SessionExpiredError)) throw error46;
443
+ }
444
+ }
445
+ await login(options);
446
+ return true;
447
+ }
448
+
449
+ // src/cli-options.ts
450
+ import { existsSync } from "node:fs";
451
+ import path3 from "node:path";
452
+ var UsageError = class extends Error {
453
+ };
454
+ var helpTopics = /* @__PURE__ */ new Set(["start", "status", "devices", "login", "logout"]);
455
+ function parseStart(args) {
456
+ if (args.includes("--help") || args.includes("-h")) {
457
+ return { command: "help", topic: "start" };
458
+ }
459
+ let selectedPath;
460
+ let allowBroadRoot = false;
461
+ let optionsEnded = false;
462
+ for (const argument of args) {
463
+ if (!optionsEnded && argument === "--") {
464
+ optionsEnded = true;
465
+ } else if (!optionsEnded && argument === "--allow-broad-root") {
466
+ allowBroadRoot = true;
467
+ } else if (!optionsEnded && argument.startsWith("-")) {
468
+ throw new UsageError(`Unknown start option: ${argument}`);
469
+ } else if (selectedPath) {
470
+ throw new UsageError("Start accepts at most one directory.");
471
+ } else {
472
+ selectedPath = argument;
473
+ }
474
+ }
475
+ return {
476
+ command: "start",
477
+ ...selectedPath ? { path: selectedPath } : {},
478
+ allowBroadRoot
479
+ };
480
+ }
481
+ function singleJsonOption(command, args) {
482
+ if (args.length === 0) return false;
483
+ if (args.length === 1 && args[0] === "--json") return true;
484
+ throw new UsageError(`${command} accepts only --json.`);
485
+ }
486
+ function parseDevices(args) {
487
+ const [action, ...options] = args;
488
+ if (!action || action === "--help" || action === "-h") {
489
+ return { command: "help", topic: "devices" };
490
+ }
491
+ if (action === "list") {
492
+ return { command: "devices", action, json: singleJsonOption("Devices list", options) };
493
+ }
494
+ if (action === "rename" && options.length === 2) {
495
+ return { command: "devices", action, deviceId: options[0], name: options[1] };
496
+ }
497
+ if (action === "revoke" && options.length === 1) {
498
+ return { command: "devices", action, deviceId: options[0] };
499
+ }
500
+ throw new UsageError("Use: glossa devices list, rename <id> <name>, or revoke <id>.");
501
+ }
502
+ function likelyDirectory(value) {
503
+ return value === "." || value === ".." || path3.isAbsolute(value) || value.includes("/") || value.includes("\\") || existsSync(value);
504
+ }
505
+ function parseInvocation(args) {
506
+ const [command, ...options] = args;
507
+ if (!command) return parseStart([]);
508
+ if (command === "--help" || command === "-h") {
509
+ if (options.length > 0) throw new UsageError("Help accepts one command name.");
510
+ return { command: "help" };
511
+ }
512
+ if (command === "help") {
513
+ if (options.length > 1) throw new UsageError("Help accepts one command name.");
514
+ const topic = options[0];
515
+ if (!topic) return { command: "help" };
516
+ if (!helpTopics.has(topic)) {
517
+ throw new UsageError(`Unknown help topic: ${topic}`);
518
+ }
519
+ return { command: "help", topic };
520
+ }
521
+ if (command === "--version" || command === "-v") {
522
+ if (options.length > 0) throw new UsageError("Version accepts no arguments.");
523
+ return { command: "version" };
524
+ }
525
+ if (command === "start") return parseStart(options);
526
+ if (command === "status") {
527
+ if (options.includes("--help") || options.includes("-h")) {
528
+ return { command: "help", topic: "status" };
529
+ }
530
+ return { command: "status", json: singleJsonOption("Status", options) };
531
+ }
532
+ if (command === "devices") return parseDevices(options);
533
+ if (command === "login") {
534
+ if (options.includes("--help") || options.includes("-h")) {
535
+ return { command: "help", topic: "login" };
536
+ }
537
+ if (options.length > 0) throw new UsageError("Login accepts no arguments.");
538
+ return { command: "login" };
539
+ }
540
+ if (command === "logout") {
541
+ if (options.includes("--help") || options.includes("-h")) {
542
+ return { command: "help", topic: "logout" };
543
+ }
544
+ if (options.length === 0) return { command: "logout", browser: false };
545
+ if (options.length === 1 && options[0] === "--browser") {
546
+ return { command: "logout", browser: true };
547
+ }
548
+ throw new UsageError("Logout accepts only --browser.");
549
+ }
550
+ if (command === "--") return parseStart(options);
551
+ if (command.startsWith("-")) return parseStart(args);
552
+ if (likelyDirectory(command)) return parseStart(args);
553
+ throw new UsageError(`Unknown command: ${command}`);
554
+ }
555
+
379
556
  // src/relay-client.ts
380
557
  import os2 from "node:os";
381
558
 
@@ -1110,10 +1287,10 @@ function mergeDefs(...defs) {
1110
1287
  function cloneDef(schema) {
1111
1288
  return mergeDefs(schema._zod.def);
1112
1289
  }
1113
- function getElementAtPath(obj, path6) {
1114
- if (!path6)
1290
+ function getElementAtPath(obj, path7) {
1291
+ if (!path7)
1115
1292
  return obj;
1116
- return path6.reduce((acc, key) => acc?.[key], obj);
1293
+ return path7.reduce((acc, key) => acc?.[key], obj);
1117
1294
  }
1118
1295
  function promiseAllObject(promisesObj) {
1119
1296
  const keys = Object.keys(promisesObj);
@@ -1474,11 +1651,11 @@ function aborted(x, startIndex = 0) {
1474
1651
  }
1475
1652
  return false;
1476
1653
  }
1477
- function prefixIssues(path6, issues) {
1654
+ function prefixIssues(path7, issues) {
1478
1655
  return issues.map((iss) => {
1479
1656
  var _a;
1480
1657
  (_a = iss).path ?? (_a.path = []);
1481
- iss.path.unshift(path6);
1658
+ iss.path.unshift(path7);
1482
1659
  return iss;
1483
1660
  });
1484
1661
  }
@@ -1640,7 +1817,7 @@ function formatError(error46, mapper = (issue2) => issue2.message) {
1640
1817
  }
1641
1818
  function treeifyError(error46, mapper = (issue2) => issue2.message) {
1642
1819
  const result = { errors: [] };
1643
- const processError = (error47, path6 = []) => {
1820
+ const processError = (error47, path7 = []) => {
1644
1821
  var _a, _b;
1645
1822
  for (const issue2 of error47.issues) {
1646
1823
  if (issue2.code === "invalid_union" && issue2.errors.length) {
@@ -1650,7 +1827,7 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
1650
1827
  } else if (issue2.code === "invalid_element") {
1651
1828
  processError({ issues: issue2.issues }, issue2.path);
1652
1829
  } else {
1653
- const fullpath = [...path6, ...issue2.path];
1830
+ const fullpath = [...path7, ...issue2.path];
1654
1831
  if (fullpath.length === 0) {
1655
1832
  result.errors.push(mapper(issue2));
1656
1833
  continue;
@@ -1682,8 +1859,8 @@ function treeifyError(error46, mapper = (issue2) => issue2.message) {
1682
1859
  }
1683
1860
  function toDotPath(_path) {
1684
1861
  const segs = [];
1685
- const path6 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1686
- for (const seg of path6) {
1862
+ const path7 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
1863
+ for (const seg of path7) {
1687
1864
  if (typeof seg === "number")
1688
1865
  segs.push(`[${seg}]`);
1689
1866
  else if (typeof seg === "symbol")
@@ -12927,65 +13104,64 @@ var DEFAULT_COMMAND_TIMEOUT_MS = 15 * 60 * 1e3;
12927
13104
  var MAX_COMMAND_TIMEOUT_MS = 60 * 60 * 1e3;
12928
13105
  var MAX_COMMAND_STATUS_WAIT_MS = 15e3;
12929
13106
  var deviceNameSchema = external_exports.string().trim().min(1).max(80).regex(/^[^\u0000-\u001f\u007f]+$/, "Device name contains control characters");
12930
- var openWorkspaceJobSchema = external_exports.object({
12931
- type: external_exports.literal("open_workspace"),
12932
- requestId: external_exports.string().uuid(),
12933
- path: external_exports.string().max(4096)
12934
- });
12935
- var readFileJobSchema = external_exports.object({
13107
+ var relativePathSchema = external_exports.string().max(4096).describe("Path relative to the exposed workspace root. Absolute paths and parent traversal are rejected.");
13108
+ var boundedTextSchema = external_exports.string().refine((value) => Buffer.byteLength(value, "utf8") <= MAX_TEXT_BYTES);
13109
+ var readFileRequestSchema = external_exports.object({
13110
+ path: relativePathSchema
13111
+ }).strict();
13112
+ var readFileJobSchema = readFileRequestSchema.extend({
12936
13113
  type: external_exports.literal("read_file"),
12937
- requestId: external_exports.string().uuid(),
12938
- workspaceId: external_exports.string().uuid(),
12939
- path: external_exports.string().max(4096)
12940
- });
12941
- var writeFileJobSchema = external_exports.object({
13114
+ requestId: external_exports.string().uuid()
13115
+ });
13116
+ var writeFileRequestSchema = external_exports.object({
13117
+ path: relativePathSchema,
13118
+ content: boundedTextSchema.describe("Complete UTF-8 text content that will replace the file."),
13119
+ expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional().describe("SHA-256 returned by read_file. When provided, the write fails if the file changed.")
13120
+ }).strict();
13121
+ var writeFileJobSchema = writeFileRequestSchema.extend({
12942
13122
  type: external_exports.literal("write_file"),
12943
- requestId: external_exports.string().uuid(),
12944
- workspaceId: external_exports.string().uuid(),
12945
- path: external_exports.string().max(4096),
12946
- content: external_exports.string(),
12947
- expectedSha256: external_exports.string().regex(/^[a-f0-9]{64}$/).optional()
13123
+ requestId: external_exports.string().uuid()
12948
13124
  });
12949
- var runCommandJobSchema = external_exports.object({
12950
- type: external_exports.literal("run_command"),
12951
- requestId: external_exports.string().uuid(),
12952
- workspaceId: external_exports.string().uuid(),
12953
- argv: external_exports.array(external_exports.string()).min(1).max(256).optional(),
12954
- shellCommand: external_exports.string().max(64 * 1024).optional(),
12955
- stdin: external_exports.string().max(MAX_TEXT_BYTES).optional(),
12956
- timeoutMs: external_exports.number().int().min(1).max(MAX_COMMAND_TIMEOUT_MS).default(DEFAULT_COMMAND_TIMEOUT_MS)
12957
- }).superRefine((value, context) => {
13125
+ function requireOneCommand(value, context) {
12958
13126
  if ((value.argv ? 1 : 0) + (value.shellCommand ? 1 : 0) !== 1) {
12959
13127
  context.addIssue({
12960
13128
  code: "custom",
12961
- message: "Exactly one of argv or shellCommand is required."
13129
+ message: "Exactly one of argv or shellCommand is required.",
13130
+ input: value
12962
13131
  });
12963
13132
  }
13133
+ }
13134
+ var runCommandRequestSchema = external_exports.object({
13135
+ argv: external_exports.array(external_exports.string()).min(1).max(256).optional().describe("Executable and arguments passed without shell parsing. Provide this or shellCommand, not both."),
13136
+ shellCommand: external_exports.string().max(64 * 1024).optional().describe("PowerShell command text. Provide this or argv, not both."),
13137
+ stdin: boundedTextSchema.optional().describe("Optional UTF-8 text sent to the command standard input."),
13138
+ timeoutMs: external_exports.number().int().min(1).max(MAX_COMMAND_TIMEOUT_MS).default(DEFAULT_COMMAND_TIMEOUT_MS).describe("Maximum command runtime in milliseconds. Defaults to 900000 and cannot exceed 3600000.")
13139
+ }).strict().superRefine(requireOneCommand);
13140
+ var runCommandJobSchema = runCommandRequestSchema.safeExtend({
13141
+ type: external_exports.literal("run_command"),
13142
+ requestId: external_exports.string().uuid()
12964
13143
  });
12965
- var getCommandJobSchema = external_exports.object({
13144
+ var getCommandRequestSchema = external_exports.object({
13145
+ commandId: external_exports.string().uuid().describe("Command identifier returned by run_command."),
13146
+ waitMs: external_exports.number().int().min(0).max(MAX_COMMAND_STATUS_WAIT_MS).optional().describe("Optional long-poll duration in milliseconds, from 0 through 15000.")
13147
+ }).strict();
13148
+ var getCommandJobSchema = getCommandRequestSchema.extend({
12966
13149
  type: external_exports.literal("get_command"),
12967
- requestId: external_exports.string().uuid(),
12968
- commandId: external_exports.string().uuid(),
12969
- waitMs: external_exports.number().int().min(0).max(MAX_COMMAND_STATUS_WAIT_MS).optional()
13150
+ requestId: external_exports.string().uuid()
12970
13151
  });
12971
- var cancelCommandJobSchema = external_exports.object({
13152
+ var cancelCommandRequestSchema = external_exports.object({
13153
+ commandId: external_exports.string().uuid().describe("Command identifier returned by run_command.")
13154
+ }).strict();
13155
+ var cancelCommandJobSchema = cancelCommandRequestSchema.extend({
12972
13156
  type: external_exports.literal("cancel_command"),
12973
- requestId: external_exports.string().uuid(),
12974
- commandId: external_exports.string().uuid()
12975
- });
12976
- var closeWorkspaceJobSchema = external_exports.object({
12977
- type: external_exports.literal("close_workspace"),
12978
- requestId: external_exports.string().uuid(),
12979
- workspaceId: external_exports.string().uuid()
13157
+ requestId: external_exports.string().uuid()
12980
13158
  });
12981
13159
  var workerJobSchema = external_exports.discriminatedUnion("type", [
12982
- openWorkspaceJobSchema,
12983
13160
  readFileJobSchema,
12984
13161
  writeFileJobSchema,
12985
13162
  runCommandJobSchema,
12986
13163
  getCommandJobSchema,
12987
- cancelCommandJobSchema,
12988
- closeWorkspaceJobSchema
13164
+ cancelCommandJobSchema
12989
13165
  ]);
12990
13166
  var workerResultSchema = external_exports.object({
12991
13167
  requestId: external_exports.string().uuid(),
@@ -13038,16 +13214,62 @@ function loadRelayEndpoints(environment = process.env) {
13038
13214
  function defaultDeviceName() {
13039
13215
  return deviceNameSchema.parse(os2.hostname());
13040
13216
  }
13041
- function enrollmentError(status, data) {
13042
- if (status === 401) return new Error("Glossa login was rejected. Run: glossa login");
13217
+ function relayError(status, data) {
13218
+ if (status === 401) return new Error("Glossa login was rejected. Sign in again when prompted.");
13043
13219
  if (status === 403 && data.error === "account_disabled") {
13044
13220
  return new Error("This Glossa account is disabled.");
13045
13221
  }
13222
+ if (status === 403 && data.error === "identity_provider_not_allowed") {
13223
+ return new Error(
13224
+ "This Glossa identity provider is not allowed. Sign in with Google."
13225
+ );
13226
+ }
13046
13227
  if (status === 409 && data.error === "device_name_conflict") {
13047
- return new Error("A Glossa device with this computer name already exists and must be revoked before reenrollment.");
13228
+ return new Error("A Glossa device already uses this name. Run glossa devices list, then rename or revoke the old device.");
13229
+ }
13230
+ if (status === 404 && data.error === "device_not_found") {
13231
+ return new Error("The Glossa device was not found.");
13048
13232
  }
13049
13233
  if (status === 429) return new Error("Glossa device enrollment is rate limited. Try again later.");
13050
- return new Error(`Glossa device enrollment failed with HTTP ${status}.`);
13234
+ return new Error(`The Glossa relay returned HTTP ${status}.`);
13235
+ }
13236
+ function validNullableString(value) {
13237
+ return value === null || typeof value === "string";
13238
+ }
13239
+ function parseDevices2(value) {
13240
+ if (!Array.isArray(value)) {
13241
+ throw new Error("The Glossa relay returned an invalid device list response.");
13242
+ }
13243
+ const devices = value;
13244
+ if (devices.some(
13245
+ (device) => typeof device.id !== "string" || typeof device.name !== "string" || !validNullableString(device.platform) || !validNullableString(device.lastSeenAt) || !validNullableString(device.revokedAt) || device.activeWorkers !== void 0 && (!Number.isInteger(device.activeWorkers) || device.activeWorkers < 0)
13246
+ )) {
13247
+ throw new Error("The Glossa relay returned an invalid device list response.");
13248
+ }
13249
+ return devices.map((device) => ({
13250
+ ...device,
13251
+ activeWorkers: device.activeWorkers ?? null
13252
+ }));
13253
+ }
13254
+ async function listDevices(endpoints, credentials, fetchRequest = fetch) {
13255
+ const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices`, {
13256
+ headers: {
13257
+ authorization: `${credentials.tokenType} ${credentials.accessToken}`
13258
+ }
13259
+ });
13260
+ let data = {};
13261
+ try {
13262
+ data = await response.json();
13263
+ } catch {
13264
+ }
13265
+ if (!response.ok) throw relayError(response.status, data);
13266
+ return parseDevices2(data.devices);
13267
+ }
13268
+ async function accountOwnsDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
13269
+ const devices = await listDevices(endpoints, credentials, fetchRequest);
13270
+ return devices.some(
13271
+ (device) => device.id === deviceId && device.revokedAt === null
13272
+ );
13051
13273
  }
13052
13274
  async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = fetch) {
13053
13275
  const name = deviceNameSchema.parse(deviceName);
@@ -13064,7 +13286,7 @@ async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = f
13064
13286
  data = await response.json();
13065
13287
  } catch {
13066
13288
  }
13067
- if (!response.ok) throw enrollmentError(response.status, data);
13289
+ if (!response.ok) throw relayError(response.status, data);
13068
13290
  if (typeof data.device?.id !== "string" || typeof data.device.name !== "string" || typeof data.device_token !== "string") {
13069
13291
  throw new Error("The Glossa relay returned an invalid device enrollment response.");
13070
13292
  }
@@ -13075,9 +13297,108 @@ async function enrollDevice(endpoints, credentials, deviceName, fetchRequest = f
13075
13297
  token: data.device_token
13076
13298
  };
13077
13299
  }
13300
+ async function renameDevice(endpoints, credentials, deviceId, name, fetchRequest = fetch) {
13301
+ const validName = deviceNameSchema.parse(name);
13302
+ const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
13303
+ method: "PATCH",
13304
+ headers: {
13305
+ authorization: `${credentials.tokenType} ${credentials.accessToken}`,
13306
+ "content-type": "application/json"
13307
+ },
13308
+ body: JSON.stringify({ name: validName })
13309
+ });
13310
+ const data = await response.json().catch(() => ({}));
13311
+ if (!response.ok) throw relayError(response.status, data);
13312
+ return parseDevices2([data.device])[0];
13313
+ }
13314
+ async function revokeDevice(endpoints, credentials, deviceId, fetchRequest = fetch) {
13315
+ const response = await fetchRequest(`${endpoints.relayOrigin}/v1/devices/${encodeURIComponent(deviceId)}`, {
13316
+ method: "DELETE",
13317
+ headers: {
13318
+ authorization: `${credentials.tokenType} ${credentials.accessToken}`
13319
+ }
13320
+ });
13321
+ if (!response.ok) {
13322
+ const data = await response.json().catch(() => ({}));
13323
+ throw relayError(response.status, data);
13324
+ }
13325
+ }
13078
13326
 
13079
- // src/worker/local-session.ts
13080
- import readline from "node:readline";
13327
+ // src/logout.ts
13328
+ function browserLogoutUrl(issuer) {
13329
+ return new URL(
13330
+ "v2/logout",
13331
+ issuer.endsWith("/") ? issuer : `${issuer}/`
13332
+ ).toString();
13333
+ }
13334
+ async function logoutFromGlossa(options, dependencies = {}) {
13335
+ const remove = dependencies.deleteCredentials ?? deleteCredentials;
13336
+ const browse = dependencies.openBrowser ?? openBrowser;
13337
+ const log = dependencies.log ?? console.log;
13338
+ let issuer = dependencies.issuer;
13339
+ if (options.browser && issuer === void 0) {
13340
+ const loadStoredIssuer = dependencies.loadStoredIssuer ?? (async () => (await loadCredentials())?.credentials.issuer);
13341
+ try {
13342
+ issuer = await loadStoredIssuer();
13343
+ } catch {
13344
+ }
13345
+ }
13346
+ await remove();
13347
+ log("Signed out of Glossa locally.");
13348
+ if (!options.browser) return;
13349
+ const url2 = browserLogoutUrl(issuer ?? loadAuthConfig().issuer);
13350
+ const opened = await browse(url2);
13351
+ if (opened) {
13352
+ log("Opened Glossa browser sign-out.");
13353
+ } else {
13354
+ log("Open this URL to finish signing out in your browser:");
13355
+ log(url2);
13356
+ }
13357
+ log(
13358
+ "Reconnect Glossa in ChatGPT, then choose the same Google account when the CLI signs in."
13359
+ );
13360
+ }
13361
+
13362
+ // src/device-store.ts
13363
+ import path4 from "node:path";
13364
+ var FILE_DEVICE_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 device credential file.";
13365
+ function parseDeviceCredential(value) {
13366
+ let parsed;
13367
+ try {
13368
+ parsed = JSON.parse(value);
13369
+ } catch {
13370
+ throw new Error("Stored Glossa device credentials are invalid.");
13371
+ }
13372
+ let relayOriginValid = false;
13373
+ if (typeof parsed.relayOrigin === "string") {
13374
+ try {
13375
+ relayOriginValid = new URL(parsed.relayOrigin).origin === parsed.relayOrigin;
13376
+ } catch {
13377
+ relayOriginValid = false;
13378
+ }
13379
+ }
13380
+ if (!relayOriginValid || typeof parsed.deviceId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
13381
+ parsed.deviceId
13382
+ ) || typeof parsed.deviceName !== "string" || parsed.deviceName.length === 0 || typeof parsed.token !== "string" || !parsed.token.startsWith(`gld_${parsed.deviceId}_`)) {
13383
+ throw new Error("Stored Glossa device credentials are invalid.");
13384
+ }
13385
+ return parsed;
13386
+ }
13387
+ var store2 = new SecureStore({
13388
+ account: "device",
13389
+ file: path4.join(configDirectory(), "device.json"),
13390
+ warning: FILE_DEVICE_WARNING,
13391
+ parse: parseDeviceCredential
13392
+ });
13393
+ async function loadDeviceCredential() {
13394
+ return (await store2.load())?.value ?? null;
13395
+ }
13396
+ async function saveDeviceCredential(credential) {
13397
+ await store2.save(credential);
13398
+ }
13399
+ async function deleteDeviceCredential() {
13400
+ await store2.delete();
13401
+ }
13081
13402
 
13082
13403
  // src/worker/command-service.ts
13083
13404
  import { spawn as spawn2 } from "node:child_process";
@@ -13147,10 +13468,10 @@ async function terminateProcessTree(child) {
13147
13468
  }
13148
13469
  }
13149
13470
  var CommandService = class {
13150
- constructor(workspaces) {
13151
- this.workspaces = workspaces;
13471
+ constructor(policy) {
13472
+ this.policy = policy;
13152
13473
  }
13153
- workspaces;
13474
+ policy;
13154
13475
  #commands = /* @__PURE__ */ new Map();
13155
13476
  #activeCommandId = null;
13156
13477
  async start(options) {
@@ -13174,12 +13495,12 @@ var CommandService = class {
13174
13495
  "Command timeout must be between 1 millisecond and 60 minutes."
13175
13496
  );
13176
13497
  }
13177
- const cwd = await this.workspaces.resolve(options.workspaceId, ".");
13498
+ const cwd = this.policy.root;
13178
13499
  const invocation = options.argv ? { file: options.argv[0], args: options.argv.slice(1) } : shellInvocation(options.shellCommand);
13179
13500
  const child = spawn2(invocation.file, invocation.args, {
13180
13501
  cwd,
13181
13502
  env: process.env,
13182
- detached: true,
13503
+ detached: process.platform !== "win32",
13183
13504
  stdio: "pipe",
13184
13505
  windowsHide: true
13185
13506
  });
@@ -13249,7 +13570,15 @@ var CommandService = class {
13249
13570
  throw new WorkerError("invalid_wait", "Status wait must be between 0 and 15 seconds.");
13250
13571
  }
13251
13572
  if (record2.status === "running" && waitMs > 0) {
13252
- await Promise.race([record2.completion, delay2(waitMs)]);
13573
+ const waitController = new AbortController();
13574
+ try {
13575
+ await Promise.race([
13576
+ record2.completion,
13577
+ delay2(waitMs, void 0, { signal: waitController.signal })
13578
+ ]);
13579
+ } finally {
13580
+ waitController.abort();
13581
+ }
13253
13582
  }
13254
13583
  return this.snapshot(record2);
13255
13584
  }
@@ -13292,19 +13621,17 @@ var CommandService = class {
13292
13621
  // src/worker/file-service.ts
13293
13622
  import { createHash, randomUUID as randomUUID2 } from "node:crypto";
13294
13623
  import { lstat, readFile as readFile2, rename, rm as rm2, stat, writeFile as writeFile2 } from "node:fs/promises";
13295
- import path2 from "node:path";
13624
+ import path5 from "node:path";
13296
13625
  function sha256(content) {
13297
13626
  return createHash("sha256").update(content).digest("hex");
13298
13627
  }
13299
13628
  var FileService = class {
13300
- constructor(policy, workspaces) {
13629
+ constructor(policy) {
13301
13630
  this.policy = policy;
13302
- this.workspaces = workspaces;
13303
13631
  }
13304
13632
  policy;
13305
- workspaces;
13306
- async readText(workspaceId, relativePath) {
13307
- const target = await this.workspaces.resolve(workspaceId, relativePath);
13633
+ async readText(relativePath) {
13634
+ const target = await this.policy.resolveExisting(relativePath);
13308
13635
  const targetStat = await stat(target);
13309
13636
  if (!targetStat.isFile()) {
13310
13637
  throw new WorkerError("not_file", "The requested path is not a file.");
@@ -13321,13 +13648,12 @@ var FileService = class {
13321
13648
  }
13322
13649
  return { content: text, sha256: sha256(content), bytes: content.byteLength };
13323
13650
  }
13324
- async writeText(workspaceId, relativePath, content, expectedSha256) {
13651
+ async writeText(relativePath, content, expectedSha256) {
13325
13652
  const bytes = Buffer.from(content, "utf8");
13326
13653
  if (bytes.byteLength > MAX_TEXT_BYTES) {
13327
13654
  throw new WorkerError("file_too_large", "The content exceeds the 1 MiB text limit.");
13328
13655
  }
13329
- const workspaceRelative = this.workspaces.relativePath(workspaceId, relativePath);
13330
- let target = await this.policy.resolveWritableFile(workspaceRelative);
13656
+ let target = await this.policy.resolveWritableFile(relativePath);
13331
13657
  if (expectedSha256) {
13332
13658
  let actual = null;
13333
13659
  try {
@@ -13340,10 +13666,10 @@ var FileService = class {
13340
13666
  throw new WorkerError("stale_revision", "The file revision has changed.");
13341
13667
  }
13342
13668
  }
13343
- const temporary = path2.join(path2.dirname(target), `.glossa-${randomUUID2()}.tmp`);
13669
+ const temporary = path5.join(path5.dirname(target), `.glossa-${randomUUID2()}.tmp`);
13344
13670
  try {
13345
13671
  await writeFile2(temporary, bytes, { flag: "wx", mode: 384 });
13346
- target = await this.policy.resolveWritableFile(workspaceRelative);
13672
+ target = await this.policy.resolveWritableFile(relativePath);
13347
13673
  const tempStat = await lstat(temporary);
13348
13674
  if (!tempStat.isFile() || tempStat.isSymbolicLink()) {
13349
13675
  throw new WorkerError("unsafe_temporary_file", "The atomic write temporary file changed.");
@@ -13359,19 +13685,19 @@ var FileService = class {
13359
13685
  // src/worker/path-policy.ts
13360
13686
  import { lstat as lstat2, realpath, stat as stat2 } from "node:fs/promises";
13361
13687
  import os3 from "node:os";
13362
- import path3 from "node:path";
13688
+ import path6 from "node:path";
13363
13689
  function samePath(left, right) {
13364
13690
  return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
13365
13691
  }
13366
13692
  function isWithin(root, candidate) {
13367
- const relative = path3.relative(root, candidate);
13368
- return relative === "" || !relative.startsWith(`..${path3.sep}`) && relative !== ".." && !path3.isAbsolute(relative);
13693
+ const relative = path6.relative(root, candidate);
13694
+ return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
13369
13695
  }
13370
13696
  function validateRelativePath(value) {
13371
13697
  if (value.includes("\0")) {
13372
13698
  throw new WorkerError("invalid_path", "Paths cannot contain null bytes.");
13373
13699
  }
13374
- if (path3.isAbsolute(value) || path3.win32.isAbsolute(value) || path3.posix.isAbsolute(value)) {
13700
+ if (path6.isAbsolute(value) || path6.win32.isAbsolute(value) || path6.posix.isAbsolute(value)) {
13375
13701
  throw new WorkerError("absolute_path", "Absolute paths are not allowed.");
13376
13702
  }
13377
13703
  const segments = value.split(/[\\/]+/);
@@ -13381,14 +13707,19 @@ function validateRelativePath(value) {
13381
13707
  return value === "" ? "." : value;
13382
13708
  }
13383
13709
  async function canonicalizeRoot(candidate, allowBroadRoot = false) {
13384
- const root = await realpath(path3.resolve(candidate));
13710
+ const root = await realpath(path6.resolve(candidate)).catch((error46) => {
13711
+ if (error46.code === "ENOENT") {
13712
+ throw new WorkerError("root_not_found", "The workspace directory does not exist.");
13713
+ }
13714
+ throw error46;
13715
+ });
13385
13716
  const rootStat = await stat2(root);
13386
13717
  if (!rootStat.isDirectory()) {
13387
13718
  throw new WorkerError("root_not_directory", "The exposed root must be a directory.");
13388
13719
  }
13389
13720
  if (!allowBroadRoot) {
13390
- const filesystemRoot = path3.parse(root).root;
13391
- const home = await realpath(os3.homedir()).catch(() => path3.resolve(os3.homedir()));
13721
+ const filesystemRoot = path6.parse(root).root;
13722
+ const home = await realpath(os3.homedir()).catch(() => path6.resolve(os3.homedir()));
13392
13723
  if (samePath(root, filesystemRoot) || samePath(root, home)) {
13393
13724
  throw new WorkerError(
13394
13725
  "broad_root_refused",
@@ -13420,16 +13751,9 @@ var PathPolicy = class _PathPolicy {
13420
13751
  }
13421
13752
  return canonical;
13422
13753
  }
13423
- async resolveDirectory(relativePath) {
13424
- const resolved = await this.resolveExisting(relativePath);
13425
- if (!(await stat2(resolved)).isDirectory()) {
13426
- throw new WorkerError("not_directory", "The requested workspace is not a directory.");
13427
- }
13428
- return resolved;
13429
- }
13430
13754
  async resolveWritableFile(relativePath) {
13431
13755
  const lexical = this.resolveLexical(relativePath);
13432
- const parent = path3.dirname(lexical);
13756
+ const parent = path6.dirname(lexical);
13433
13757
  await this.rejectLinkedComponents(parent);
13434
13758
  const canonicalParent = await realpath(parent).catch((error46) => {
13435
13759
  if (error46.code === "ENOENT") {
@@ -13455,11 +13779,11 @@ var PathPolicy = class _PathPolicy {
13455
13779
  if (error46 instanceof WorkerError) throw error46;
13456
13780
  if (error46.code !== "ENOENT") throw error46;
13457
13781
  }
13458
- return path3.join(canonicalParent, path3.basename(lexical));
13782
+ return path6.join(canonicalParent, path6.basename(lexical));
13459
13783
  }
13460
13784
  resolveLexical(relativePath) {
13461
13785
  const validated = validateRelativePath(relativePath);
13462
- const candidate = path3.resolve(this.root, validated);
13786
+ const candidate = path6.resolve(this.root, validated);
13463
13787
  if (!isWithin(this.root, candidate)) {
13464
13788
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
13465
13789
  }
@@ -13469,11 +13793,11 @@ var PathPolicy = class _PathPolicy {
13469
13793
  if (!isWithin(this.root, candidate)) {
13470
13794
  throw new WorkerError("path_escape", "The requested path escapes the exposed root.");
13471
13795
  }
13472
- const relative = path3.relative(this.root, candidate);
13796
+ const relative = path6.relative(this.root, candidate);
13473
13797
  if (!relative) return;
13474
13798
  let current = this.root;
13475
- for (const segment of relative.split(path3.sep)) {
13476
- current = path3.join(current, segment);
13799
+ for (const segment of relative.split(path6.sep)) {
13800
+ current = path6.join(current, segment);
13477
13801
  try {
13478
13802
  const currentStat = await lstat2(current);
13479
13803
  if (currentStat.isSymbolicLink()) {
@@ -13491,93 +13815,33 @@ var PathPolicy = class _PathPolicy {
13491
13815
  }
13492
13816
  };
13493
13817
 
13494
- // src/worker/workspace-manager.ts
13495
- import { randomUUID as randomUUID3 } from "node:crypto";
13496
- import path4 from "node:path";
13497
- var DEFAULT_LEASE_MS = 5 * 60 * 1e3;
13498
- var WorkspaceManager = class {
13499
- constructor(policy, leaseMs = DEFAULT_LEASE_MS) {
13500
- this.policy = policy;
13501
- this.leaseMs = leaseMs;
13502
- }
13503
- policy;
13504
- leaseMs;
13505
- #leases = /* @__PURE__ */ new Map();
13506
- async open(relativePath = ".") {
13507
- const resolved = await this.policy.resolveDirectory(relativePath);
13508
- const normalized = path4.relative(this.policy.root, resolved) || ".";
13509
- const lease = {
13510
- id: randomUUID3(),
13511
- relativePath: normalized,
13512
- expiresAt: Date.now() + this.leaseMs
13513
- };
13514
- this.#leases.set(lease.id, lease);
13515
- return {
13516
- workspaceId: lease.id,
13517
- path: normalized,
13518
- expiresAt: new Date(lease.expiresAt).toISOString()
13519
- };
13520
- }
13521
- async resolve(workspaceId, relativePath = ".") {
13522
- const lease = this.#leases.get(workspaceId);
13523
- if (!lease || lease.expiresAt <= Date.now()) {
13524
- this.#leases.delete(workspaceId);
13525
- throw new WorkerError("workspace_expired", "The workspace lease has expired.");
13526
- }
13527
- lease.expiresAt = Date.now() + this.leaseMs;
13528
- return await this.policy.resolveExisting(
13529
- path4.join(lease.relativePath, validateRelativePath(relativePath))
13530
- );
13531
- }
13532
- relativePath(workspaceId, relativePath) {
13533
- const lease = this.#leases.get(workspaceId);
13534
- if (!lease || lease.expiresAt <= Date.now()) {
13535
- this.#leases.delete(workspaceId);
13536
- throw new WorkerError("workspace_expired", "The workspace lease has expired.");
13537
- }
13538
- lease.expiresAt = Date.now() + this.leaseMs;
13539
- return path4.join(lease.relativePath, validateRelativePath(relativePath));
13540
- }
13541
- close(workspaceId) {
13542
- return this.#leases.delete(workspaceId);
13543
- }
13544
- };
13545
-
13546
13818
  // src/worker/local-worker.ts
13547
13819
  var LocalWorker = class _LocalWorker {
13548
- constructor(policy, workspaces, files, commands) {
13820
+ constructor(policy, files, commands) {
13549
13821
  this.policy = policy;
13550
- this.workspaces = workspaces;
13551
13822
  this.files = files;
13552
13823
  this.commands = commands;
13553
13824
  }
13554
13825
  policy;
13555
- workspaces;
13556
13826
  files;
13557
13827
  commands;
13558
13828
  static async create(root, allowBroadRoot = false) {
13559
13829
  const policy = await PathPolicy.create(root, allowBroadRoot);
13560
- const workspaces = new WorkspaceManager(policy);
13561
13830
  return new _LocalWorker(
13562
13831
  policy,
13563
- workspaces,
13564
- new FileService(policy, workspaces),
13565
- new CommandService(workspaces)
13832
+ new FileService(policy),
13833
+ new CommandService(policy)
13566
13834
  );
13567
13835
  }
13568
13836
  async handle(job) {
13569
13837
  try {
13570
13838
  let value;
13571
13839
  switch (job.type) {
13572
- case "open_workspace":
13573
- value = await this.workspaces.open(job.path);
13574
- break;
13575
13840
  case "read_file":
13576
- value = await this.files.readText(job.workspaceId, job.path);
13841
+ value = await this.files.readText(job.path);
13577
13842
  break;
13578
13843
  case "write_file":
13579
13844
  value = await this.files.writeText(
13580
- job.workspaceId,
13581
13845
  job.path,
13582
13846
  job.content,
13583
13847
  job.expectedSha256
@@ -13585,7 +13849,6 @@ var LocalWorker = class _LocalWorker {
13585
13849
  break;
13586
13850
  case "run_command":
13587
13851
  value = await this.commands.start({
13588
- workspaceId: job.workspaceId,
13589
13852
  ...job.argv ? { argv: job.argv } : {},
13590
13853
  ...job.shellCommand ? { shellCommand: job.shellCommand } : {},
13591
13854
  ...job.stdin !== void 0 ? { stdin: job.stdin } : {},
@@ -13598,9 +13861,6 @@ var LocalWorker = class _LocalWorker {
13598
13861
  case "cancel_command":
13599
13862
  value = await this.commands.cancel(job.commandId);
13600
13863
  break;
13601
- case "close_workspace":
13602
- value = { closed: this.workspaces.close(job.workspaceId) };
13603
- break;
13604
13864
  }
13605
13865
  return { requestId: job.requestId, ok: true, value };
13606
13866
  } catch (error46) {
@@ -13617,181 +13877,12 @@ var LocalWorker = class _LocalWorker {
13617
13877
  }
13618
13878
  };
13619
13879
 
13620
- // src/worker/local-session.ts
13621
- var visibleActivity = /* @__PURE__ */ new Set(["write_file", "run_command", "cancel_command"]);
13622
- async function runLocalSession(root, allowBroadRoot = false) {
13623
- const worker = await LocalWorker.create(root, allowBroadRoot);
13624
- console.error(`Glossa local worker root: ${worker.policy.root}`);
13625
- console.error(
13626
- "Commands have the full environment and permissions of this account. Press Ctrl+C to disconnect."
13627
- );
13628
- const lines = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
13629
- const stop = () => lines.close();
13630
- process.once("SIGINT", stop);
13631
- process.once("SIGTERM", stop);
13632
- try {
13633
- for await (const line of lines) {
13634
- if (!line.trim()) continue;
13635
- try {
13636
- const job = workerJobSchema.parse(JSON.parse(line));
13637
- if (visibleActivity.has(job.type)) {
13638
- console.error(`Activity started: ${job.type} (${job.requestId})`);
13639
- }
13640
- const result = await worker.handle(job);
13641
- process.stdout.write(`${JSON.stringify(result)}
13642
- `);
13643
- if (visibleActivity.has(job.type)) {
13644
- console.error(
13645
- `Activity finished: ${job.type} (${job.requestId}), ${result.ok ? "accepted" : "rejected"}`
13646
- );
13647
- }
13648
- } catch {
13649
- process.stdout.write(
13650
- `${JSON.stringify({ ok: false, error: { code: "invalid_job", message: "Invalid worker job." } })}
13651
- `
13652
- );
13653
- }
13654
- }
13655
- } finally {
13656
- process.removeListener("SIGINT", stop);
13657
- process.removeListener("SIGTERM", stop);
13658
- await worker.shutdown();
13659
- }
13660
- }
13661
-
13662
- // src/device-store.ts
13663
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile3, rm as rm3, writeFile as writeFile3 } from "node:fs/promises";
13664
- import path5 from "node:path";
13665
- var KEYRING_SERVICE2 = "Glossa";
13666
- var KEYRING_ACCOUNT2 = "device";
13667
- var FILE_DEVICE_WARNING = "Warning: the operating-system credential store is unavailable. Glossa is using a mode-0600 device credential file.";
13668
- function deviceCredentialPath() {
13669
- return path5.join(configDirectory(), "device.json");
13670
- }
13671
- async function defaultEntryProvider2() {
13672
- try {
13673
- const { AsyncEntry } = await import("@napi-rs/keyring");
13674
- return new AsyncEntry(KEYRING_SERVICE2, KEYRING_ACCOUNT2);
13675
- } catch {
13676
- return null;
13677
- }
13678
- }
13679
- function parseDeviceCredential(value) {
13680
- let parsed;
13681
- try {
13682
- parsed = JSON.parse(value);
13683
- } catch {
13684
- throw new Error("Stored Glossa device credentials are invalid.");
13685
- }
13686
- let relayOriginValid = false;
13687
- if (typeof parsed.relayOrigin === "string") {
13688
- try {
13689
- relayOriginValid = new URL(parsed.relayOrigin).origin === parsed.relayOrigin;
13690
- } catch {
13691
- relayOriginValid = false;
13692
- }
13693
- }
13694
- if (!relayOriginValid || typeof parsed.deviceId !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
13695
- parsed.deviceId
13696
- ) || typeof parsed.deviceName !== "string" || parsed.deviceName.length === 0 || typeof parsed.token !== "string" || !parsed.token.startsWith(`gld_${parsed.deviceId}_`)) {
13697
- throw new Error("Stored Glossa device credentials are invalid.");
13698
- }
13699
- return parsed;
13700
- }
13701
- async function readFileCredential(target) {
13702
- try {
13703
- return parseDeviceCredential(await readFile3(target, "utf8"));
13704
- } catch (error46) {
13705
- if (error46.code === "ENOENT") return null;
13706
- throw error46;
13707
- }
13708
- }
13709
- async function writeFileCredential(target, credential) {
13710
- await mkdir2(path5.dirname(target), { recursive: true, mode: 448 });
13711
- await writeFile3(target, `${JSON.stringify(credential, null, 2)}
13712
- `, {
13713
- encoding: "utf8",
13714
- mode: 384
13715
- });
13716
- if (process.platform !== "win32") await chmod2(target, 384);
13717
- }
13718
- var DeviceStore = class {
13719
- #credentialFile;
13720
- #entryProvider;
13721
- #warn;
13722
- constructor(options = {}) {
13723
- this.#credentialFile = options.credentialFile ?? deviceCredentialPath();
13724
- this.#entryProvider = options.entryProvider ?? defaultEntryProvider2;
13725
- this.#warn = options.warn ?? console.warn;
13726
- }
13727
- async #entry() {
13728
- try {
13729
- return await this.#entryProvider();
13730
- } catch {
13731
- return null;
13732
- }
13733
- }
13734
- async save(credential) {
13735
- const entry = await this.#entry();
13736
- if (entry) {
13737
- try {
13738
- await entry.setPassword(JSON.stringify(credential));
13739
- await rm3(this.#credentialFile, { force: true });
13740
- return;
13741
- } catch {
13742
- }
13743
- }
13744
- this.#warn(FILE_DEVICE_WARNING);
13745
- await writeFileCredential(this.#credentialFile, credential);
13746
- }
13747
- async load() {
13748
- const entry = await this.#entry();
13749
- if (entry) {
13750
- try {
13751
- const value = await entry.getPassword();
13752
- if (value) return parseDeviceCredential(value);
13753
- } catch {
13754
- }
13755
- }
13756
- const credential = await readFileCredential(this.#credentialFile);
13757
- if (!credential) return null;
13758
- if (entry) {
13759
- try {
13760
- await entry.setPassword(JSON.stringify(credential));
13761
- await rm3(this.#credentialFile, { force: true });
13762
- return credential;
13763
- } catch {
13764
- }
13765
- }
13766
- this.#warn(FILE_DEVICE_WARNING);
13767
- return credential;
13768
- }
13769
- async delete() {
13770
- const entry = await this.#entry();
13771
- if (entry) {
13772
- try {
13773
- await entry.deleteCredential();
13774
- } catch {
13775
- }
13776
- }
13777
- await rm3(this.#credentialFile, { force: true });
13778
- }
13779
- };
13780
- var defaultStore2 = new DeviceStore();
13781
- async function loadDeviceCredential() {
13782
- return await defaultStore2.load();
13783
- }
13784
- async function saveDeviceCredential(credential) {
13785
- await defaultStore2.save(credential);
13786
- }
13787
- async function deleteDeviceCredential() {
13788
- await defaultStore2.delete();
13789
- }
13790
-
13791
13880
  // src/worker/remote-worker.ts
13881
+ import { randomUUID as randomUUID3 } from "node:crypto";
13792
13882
  var WORKER_REQUEST_TIMEOUT_MS = 19e3;
13793
13883
  var DEFAULT_RECONNECT_BASE_MS = 500;
13794
13884
  var DEFAULT_RECONNECT_MAX_MS = 1e4;
13885
+ var DEFAULT_HEARTBEAT_MS = 15e3;
13795
13886
  var DeviceRejectedError = class extends Error {
13796
13887
  constructor() {
13797
13888
  super("The relay rejected the device credential.");
@@ -13835,6 +13926,9 @@ var RemoteWorker = class {
13835
13926
  #random;
13836
13927
  #reconnectBaseMs;
13837
13928
  #reconnectMaxMs;
13929
+ #heartbeatMs;
13930
+ #workerId = randomUUID3();
13931
+ #onStatus;
13838
13932
  constructor(options) {
13839
13933
  this.#origin = new URL(options.origin);
13840
13934
  this.#deviceToken = options.deviceToken;
@@ -13845,44 +13939,83 @@ var RemoteWorker = class {
13845
13939
  this.#random = options.random ?? Math.random;
13846
13940
  this.#reconnectBaseMs = options.reconnectBaseMs ?? DEFAULT_RECONNECT_BASE_MS;
13847
13941
  this.#reconnectMaxMs = options.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
13942
+ this.#heartbeatMs = options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS;
13943
+ this.#onStatus = options.onStatus ?? (() => {
13944
+ });
13848
13945
  }
13849
13946
  async run() {
13850
13947
  let failures = 0;
13851
- while (!this.#signal.aborted) {
13852
- try {
13853
- const generation = await this.#register();
13854
- failures = 0;
13855
- await this.#pollGeneration(generation);
13856
- } catch (error46) {
13857
- if (this.#signal.aborted) return;
13858
- if (error46 instanceof DeviceRejectedError) throw error46;
13859
- const delay3 = reconnectDelayMs(
13860
- failures,
13861
- this.#random,
13862
- this.#reconnectBaseMs,
13863
- this.#reconnectMaxMs
13864
- );
13865
- failures += 1;
13948
+ let connectedBefore = false;
13949
+ this.#onStatus({ state: "connecting" });
13950
+ try {
13951
+ while (!this.#signal.aborted) {
13866
13952
  try {
13867
- await this.#sleep(delay3, this.#signal);
13868
- } catch (sleepError) {
13953
+ const session = await this.#register();
13954
+ this.#onStatus({
13955
+ state: "connected",
13956
+ reconnected: connectedBefore,
13957
+ legacyRelay: session.legacyRelay
13958
+ });
13959
+ connectedBefore = true;
13960
+ failures = 0;
13961
+ await this.#pollGeneration(session);
13962
+ } catch (error46) {
13869
13963
  if (this.#signal.aborted) return;
13870
- throw sleepError;
13964
+ if (error46 instanceof DeviceRejectedError) throw error46;
13965
+ const delay3 = reconnectDelayMs(
13966
+ failures,
13967
+ this.#random,
13968
+ this.#reconnectBaseMs,
13969
+ this.#reconnectMaxMs
13970
+ );
13971
+ failures += 1;
13972
+ this.#onStatus({
13973
+ state: "retrying",
13974
+ error: error46 instanceof Error ? error46 : new Error(String(error46)),
13975
+ retryInMs: delay3
13976
+ });
13977
+ try {
13978
+ await this.#sleep(delay3, this.#signal);
13979
+ } catch (sleepError) {
13980
+ if (this.#signal.aborted) return;
13981
+ throw sleepError;
13982
+ }
13871
13983
  }
13872
13984
  }
13985
+ } finally {
13986
+ await this.#unregister();
13987
+ this.#onStatus({ state: "disconnected" });
13873
13988
  }
13874
13989
  }
13875
13990
  async #register() {
13876
- const response = await this.#post("/device/register", {});
13991
+ let response;
13992
+ try {
13993
+ response = await this.#post("/device/register", {
13994
+ workerId: this.#workerId
13995
+ });
13996
+ } catch (error46) {
13997
+ if (!(error46 instanceof RelayResponseError) || error46.status !== 400) {
13998
+ throw error46;
13999
+ }
14000
+ response = await this.#post("/device/register", {});
14001
+ const legacyValue = await response.json();
14002
+ if (typeof legacyValue !== "object" || legacyValue === null || !("generation" in legacyValue) || typeof legacyValue.generation !== "string") {
14003
+ throw new Error("The relay returned an invalid registration response.");
14004
+ }
14005
+ return { generation: legacyValue.generation, legacyRelay: true };
14006
+ }
13877
14007
  const value = await response.json();
13878
- if (typeof value !== "object" || value === null || !("generation" in value) || typeof value.generation !== "string") {
14008
+ if (typeof value !== "object" || value === null || !("generation" in value) || typeof value.generation !== "string" || !("workerId" in value) || value.workerId !== this.#workerId) {
13879
14009
  throw new Error("The relay returned an invalid registration response.");
13880
14010
  }
13881
- return value.generation;
14011
+ return { generation: value.generation, legacyRelay: false };
13882
14012
  }
13883
- async #pollGeneration(generation) {
14013
+ async #pollGeneration(session) {
13884
14014
  while (!this.#signal.aborted) {
13885
- const response = await this.#post("/device/poll", { generation });
14015
+ const response = await this.#post(
14016
+ "/device/poll",
14017
+ session.legacyRelay ? { generation: session.generation } : { workerId: this.#workerId, generation: session.generation }
14018
+ );
13886
14019
  if (response.status === 204) continue;
13887
14020
  const value = await response.json();
13888
14021
  const parsed = workerJobSchema.safeParse(
@@ -13891,14 +14024,44 @@ var RemoteWorker = class {
13891
14024
  if (!parsed.success) {
13892
14025
  throw new Error("The relay returned an invalid worker job.");
13893
14026
  }
13894
- const result = await this.#worker.handle(parsed.data);
13895
- await this.#post("/device/result", result);
14027
+ const heartbeat = session.legacyRelay ? void 0 : setInterval(() => {
14028
+ void this.#post("/device/heartbeat", {
14029
+ workerId: this.#workerId,
14030
+ generation: session.generation
14031
+ }).catch(() => {
14032
+ });
14033
+ }, this.#heartbeatMs);
14034
+ heartbeat?.unref();
14035
+ let result;
14036
+ try {
14037
+ result = await this.#worker.handle(parsed.data);
14038
+ } finally {
14039
+ if (heartbeat) clearInterval(heartbeat);
14040
+ }
14041
+ await this.#post(
14042
+ "/device/result",
14043
+ session.legacyRelay ? result : { workerId: this.#workerId, result }
14044
+ );
13896
14045
  }
13897
14046
  }
13898
- async #post(path6, body) {
14047
+ async #unregister() {
14048
+ try {
14049
+ await this.#fetcher(new URL("/device/unregister", this.#origin), {
14050
+ method: "POST",
14051
+ headers: {
14052
+ authorization: `Device ${this.#deviceToken}`,
14053
+ "content-type": "application/json"
14054
+ },
14055
+ body: JSON.stringify({ workerId: this.#workerId }),
14056
+ signal: AbortSignal.timeout(3e3)
14057
+ });
14058
+ } catch {
14059
+ }
14060
+ }
14061
+ async #post(path7, body) {
13899
14062
  const timeout = AbortSignal.timeout(WORKER_REQUEST_TIMEOUT_MS);
13900
14063
  const signal = AbortSignal.any([this.#signal, timeout]);
13901
- const response = await this.#fetcher(new URL(path6, this.#origin), {
14064
+ const response = await this.#fetcher(new URL(path7, this.#origin), {
13902
14065
  method: "POST",
13903
14066
  headers: {
13904
14067
  authorization: `Device ${this.#deviceToken}`,
@@ -13914,17 +14077,22 @@ var RemoteWorker = class {
13914
14077
  };
13915
14078
 
13916
14079
  // src/worker/managed-session.ts
13917
- var visibleActivity2 = /* @__PURE__ */ new Set(["write_file", "run_command", "cancel_command"]);
14080
+ var visibleActivity = /* @__PURE__ */ new Set(["write_file", "run_command", "cancel_command"]);
14081
+ function activityLabel(type, finished, ok = true) {
14082
+ if (type === "run_command") return finished ? ok ? "Command started" : "Command rejected" : "Command requested";
14083
+ if (type === "write_file") return finished ? ok ? "File write completed" : "File write rejected" : "File write started";
14084
+ return finished ? ok ? "Command cancellation completed" : "Command cancellation rejected" : "Command cancellation requested";
14085
+ }
13918
14086
  function visibleWorker(worker) {
13919
14087
  return {
13920
14088
  async handle(job) {
13921
- if (visibleActivity2.has(job.type)) {
13922
- console.error(`Activity started: ${job.type} (${job.requestId})`);
14089
+ if (visibleActivity.has(job.type)) {
14090
+ console.error(`${activityLabel(job.type, false)} (${job.requestId}).`);
13923
14091
  }
13924
14092
  const result = await worker.handle(job);
13925
- if (visibleActivity2.has(job.type)) {
14093
+ if (visibleActivity.has(job.type)) {
13926
14094
  console.error(
13927
- `Activity finished: ${job.type} (${job.requestId}), ${result.ok ? "accepted" : "rejected"}`
14095
+ `${activityLabel(job.type, true, result.ok)} (${job.requestId}).`
13928
14096
  );
13929
14097
  }
13930
14098
  return result;
@@ -13935,14 +14103,19 @@ async function deviceForSession(endpoints, dependencies = {}) {
13935
14103
  const loadDevice = dependencies.loadDeviceCredential ?? loadDeviceCredential;
13936
14104
  const loadLogin = dependencies.loadCredentials ?? loadCredentials;
13937
14105
  const validate = dependencies.validCredentials ?? validCredentials;
14106
+ const removeDevice = dependencies.deleteDeviceCredential ?? deleteDeviceCredential;
14107
+ const ownsDevice = dependencies.accountOwnsDevice ?? accountOwnsDevice;
13938
14108
  const enroll = dependencies.enrollDevice ?? enrollDevice;
13939
14109
  const saveDevice = dependencies.saveDeviceCredential ?? saveDeviceCredential;
13940
14110
  const name = dependencies.defaultDeviceName ?? defaultDeviceName;
13941
14111
  const stored = await loadDevice();
13942
- if (stored?.relayOrigin === endpoints.relayOrigin) return stored;
13943
14112
  const loaded = await loadLogin();
13944
- if (!loaded) throw new Error("Not signed in. Run: glossa login");
14113
+ if (!loaded) throw new Error("Not signed in. Run Glossa again to sign in.");
13945
14114
  const credentials = await validate(loaded.credentials);
14115
+ if (stored?.relayOrigin === endpoints.relayOrigin) {
14116
+ if (await ownsDevice(endpoints, credentials, stored.deviceId)) return stored;
14117
+ await removeDevice();
14118
+ }
13946
14119
  const enrolled = await enroll(
13947
14120
  endpoints,
13948
14121
  credentials,
@@ -13963,12 +14136,29 @@ async function runManagedSession(root, endpoints, allowBroadRoot = false) {
13963
14136
  console.error(
13964
14137
  "Files may be modified and commands have the full environment and permissions of this account. Press Ctrl+C to disconnect."
13965
14138
  );
14139
+ let connectionState;
13966
14140
  try {
13967
14141
  await new RemoteWorker({
13968
14142
  origin: endpoints.workerOrigin,
13969
14143
  deviceToken: device.token,
13970
14144
  worker: visibleWorker(worker),
13971
- signal: controller.signal
14145
+ signal: controller.signal,
14146
+ onStatus(status) {
14147
+ if (status.state === "connecting") {
14148
+ console.error("Connecting to Glossa...");
14149
+ } else if (status.state === "connected") {
14150
+ console.error(status.reconnected ? "Reconnected to Glossa." : "Connected to Glossa. ChatGPT can now use this workspace.");
14151
+ if (status.legacyRelay) {
14152
+ console.error("The relay needs an update before this computer can expose several workspaces at once.");
14153
+ }
14154
+ } else if (status.state === "retrying" && connectionState !== "retrying") {
14155
+ const prefix = connectionState === "connecting" ? "Could not connect" : "Connection lost";
14156
+ console.error(`${prefix}: ${status.error.message} Retrying automatically.`);
14157
+ } else if (status.state === "disconnected") {
14158
+ console.error("Disconnected from Glossa.");
14159
+ }
14160
+ connectionState = status.state;
14161
+ }
13972
14162
  }).run();
13973
14163
  } catch (error46) {
13974
14164
  if (error46 instanceof DeviceRejectedError) {
@@ -14012,127 +14202,147 @@ async function selectExposureRoot(explicitPath, allowBroadRoot = false, cwd = pr
14012
14202
  }
14013
14203
 
14014
14204
  // src/main.ts
14015
- var VERSION = "0.1.0-beta.3";
14016
- function usage() {
14017
- console.log(`Glossa ${VERSION}
14205
+ var VERSION = "0.1.0-beta.6";
14206
+ var helpText = {
14207
+ main: `Glossa ${VERSION}
14018
14208
 
14019
14209
  Usage:
14020
- glossa [path] [--allow-broad-root]
14210
+ glossa
14211
+ glossa [directory]
14212
+ glossa start [directory] [--allow-broad-root]
14213
+ glossa status [--json]
14214
+ glossa devices list [--json]
14215
+ glossa devices rename <id> <name>
14216
+ glossa devices revoke <id>
14021
14217
  glossa login
14022
- glossa logout
14023
- glossa status
14024
- glossa whoami
14025
- glossa [path] --local [--allow-broad-root]
14218
+ glossa logout [--browser]
14026
14219
  glossa --version
14027
14220
  glossa --help
14028
14221
 
14029
- Running Glossa exposes one local root through the managed MCP relay.
14030
- Local mode reads newline-delimited worker jobs from stdin and writes results to stdout.`);
14222
+ Glossa signs in automatically and exposes each started workspace through the managed MCP relay.`,
14223
+ start: `Usage: glossa start [directory] [--allow-broad-root]
14224
+
14225
+ Starts a foreground worker. Inside Git, the default directory is the worktree root.
14226
+ Outside Git, provide a directory. Press Ctrl+C to disconnect.`,
14227
+ status: `Usage: glossa status [--json]
14228
+
14229
+ Validates Google login, contacts the relay, and reports enrolled devices and active workers.`,
14230
+ devices: `Usage:
14231
+ glossa devices list [--json]
14232
+ glossa devices rename <id> <name>
14233
+ glossa devices revoke <id>
14234
+
14235
+ Lists, renames, or revokes computers enrolled with the current Google account.`,
14236
+ login: `Usage: glossa login
14237
+
14238
+ Ensures the CLI has a valid Google session. Starting Glossa also signs in automatically.`,
14239
+ logout: `Usage: glossa logout [--browser]
14240
+
14241
+ Removes local OAuth credentials. --browser also opens the browser-session logout used when switching Google accounts. Running workers remain connected until stopped or revoked.`
14242
+ };
14243
+ async function withLoginSignal(action) {
14244
+ const controller = new AbortController();
14245
+ const cancel = () => controller.abort();
14246
+ process.once("SIGINT", cancel);
14247
+ try {
14248
+ return await action(controller.signal);
14249
+ } finally {
14250
+ process.removeListener("SIGINT", cancel);
14251
+ }
14031
14252
  }
14032
- function exposeOptions(args) {
14033
- let selectedPath;
14034
- let local = false;
14035
- let allowBroadRoot = false;
14036
- for (const argument of args) {
14037
- if (argument === "--local") local = true;
14038
- else if (argument === "--allow-broad-root") allowBroadRoot = true;
14039
- else if (argument.startsWith("-")) throw new Error(`Unknown expose option: ${argument}`);
14040
- else if (selectedPath) throw new Error("Expose accepts at most one directory.");
14041
- else selectedPath = argument;
14253
+ async function authenticatedCredentials() {
14254
+ const loginPerformed = await withLoginSignal(async (signal) => {
14255
+ return await ensureSignedIn({ ...loadAuthConfig(), signal });
14256
+ });
14257
+ const loaded = await loadCredentials();
14258
+ if (!loaded) throw new Error("Glossa could not load the completed login.");
14259
+ return {
14260
+ credentials: await validCredentials(loaded.credentials),
14261
+ loginPerformed
14262
+ };
14263
+ }
14264
+ async function runExposure(path7, allowBroadRoot) {
14265
+ const root = await selectExposureRoot(path7, allowBroadRoot);
14266
+ await authenticatedCredentials();
14267
+ await runManagedSession(root, loadRelayEndpoints(), allowBroadRoot);
14268
+ }
14269
+ function deviceStatus(device) {
14270
+ if (device.revokedAt) return "revoked";
14271
+ if (device.activeWorkers === null) return "worker count unavailable";
14272
+ if (device.activeWorkers === 0) return "offline";
14273
+ return `${device.activeWorkers} active ${device.activeWorkers === 1 ? "worker" : "workers"}`;
14274
+ }
14275
+ async function showStatus(json2) {
14276
+ const { credentials: initial } = await authenticatedCredentials();
14277
+ const { credentials, profile } = await loadUserProfile(initial);
14278
+ const endpoints = loadRelayEndpoints();
14279
+ const devices = await listDevices(endpoints, credentials);
14280
+ const account = profile.email ?? profile.name ?? profile.sub;
14281
+ const workerCountsCurrent = devices.every((device) => device.activeWorkers !== null);
14282
+ const activeWorkers = workerCountsCurrent ? devices.reduce((sum, device) => sum + device.activeWorkers, 0) : null;
14283
+ const result = {
14284
+ account,
14285
+ relay: endpoints.relayOrigin,
14286
+ connected: true,
14287
+ activeWorkers,
14288
+ devices
14289
+ };
14290
+ if (json2) {
14291
+ console.log(JSON.stringify(result, null, 2));
14292
+ return;
14042
14293
  }
14294
+ console.log(`Signed in as ${account}.`);
14295
+ console.log(`Relay connected: ${endpoints.relayOrigin}`);
14296
+ console.log(
14297
+ activeWorkers === null ? "Active workers: unavailable until the relay is updated" : `Active workers: ${activeWorkers}`
14298
+ );
14299
+ if (devices.length === 0) {
14300
+ console.log("No devices enrolled. Run glossa start in a workspace.");
14301
+ return;
14302
+ }
14303
+ for (const device of devices) {
14304
+ console.log(`${device.id} ${device.name} ${deviceStatus(device)}`);
14305
+ }
14306
+ }
14307
+ async function deviceCredentials() {
14043
14308
  return {
14044
- ...selectedPath ? { path: selectedPath } : {},
14045
- local,
14046
- allowBroadRoot
14309
+ credentials: (await authenticatedCredentials()).credentials,
14310
+ endpoints: loadRelayEndpoints()
14047
14311
  };
14048
14312
  }
14049
14313
  async function main() {
14050
- const args = process.argv.slice(2);
14051
- const [command] = args;
14052
- if (!command || command.startsWith("-") && !["--help", "-h", "--version", "-v"].includes(command)) {
14053
- const options = exposeOptions(args);
14054
- const root = await selectExposureRoot(options.path, options.allowBroadRoot);
14055
- if (options.local) await runLocalSession(root, options.allowBroadRoot);
14056
- else
14057
- await runManagedSession(
14058
- root,
14059
- loadRelayEndpoints(),
14060
- options.allowBroadRoot
14061
- );
14062
- return;
14063
- }
14064
- switch (command) {
14065
- case "--help":
14066
- case "-h":
14067
- case "help":
14068
- usage();
14069
- return;
14070
- case "--version":
14071
- case "-v":
14072
- console.log(VERSION);
14073
- return;
14074
- case "login":
14075
- {
14076
- const authConfig = loadAuthConfig();
14077
- const controller = new AbortController();
14078
- const cancel = () => controller.abort();
14079
- process.once("SIGINT", cancel);
14080
- try {
14081
- await loginWithDeviceFlow({
14082
- ...authConfig,
14083
- signal: controller.signal
14084
- });
14085
- } finally {
14086
- process.removeListener("SIGINT", cancel);
14087
- }
14088
- }
14089
- return;
14090
- case "logout":
14091
- await deleteCredentials();
14092
- console.log("Signed out of Glossa.");
14093
- return;
14094
- case "status": {
14095
- const loaded = await loadCredentials();
14096
- if (!loaded) {
14097
- console.log("Not signed in. Run: glossa login");
14098
- process.exitCode = 1;
14099
- return;
14100
- }
14101
- console.log(
14102
- `Signed in with ${loaded.backend} credentials; access token expires ${loaded.credentials.expiresAt}.`
14103
- );
14104
- return;
14105
- }
14106
- case "whoami": {
14107
- const loaded = await loadCredentials();
14108
- if (!loaded) {
14109
- console.log("Not signed in. Run: glossa login");
14110
- process.exitCode = 1;
14111
- return;
14112
- }
14113
- const { credentials, profile } = await loadUserProfile(loaded.credentials);
14114
- const account = profile.email ?? profile.name ?? profile.sub;
14115
- console.log(
14116
- `Signed in as ${account} (${profile.sub}); access token expires ${credentials.expiresAt}.`
14117
- );
14118
- return;
14119
- }
14120
- default: {
14121
- const options = exposeOptions(args);
14122
- const root = await selectExposureRoot(options.path, options.allowBroadRoot);
14123
- if (options.local) await runLocalSession(root, options.allowBroadRoot);
14124
- else
14125
- await runManagedSession(
14126
- root,
14127
- loadRelayEndpoints(),
14128
- options.allowBroadRoot
14129
- );
14130
- return;
14131
- }
14314
+ const invocation = parseInvocation(process.argv.slice(2));
14315
+ if (invocation.command === "help") {
14316
+ console.log(helpText[invocation.topic ?? "main"]);
14317
+ } else if (invocation.command === "version") {
14318
+ console.log(VERSION);
14319
+ } else if (invocation.command === "start") {
14320
+ await runExposure(invocation.path, invocation.allowBroadRoot);
14321
+ } else if (invocation.command === "status") {
14322
+ await showStatus(invocation.json);
14323
+ } else if (invocation.command === "login") {
14324
+ const { loginPerformed } = await authenticatedCredentials();
14325
+ if (!loginPerformed) console.log("Signed in to Glossa.");
14326
+ } else if (invocation.command === "logout") {
14327
+ await logoutFromGlossa({ browser: invocation.browser });
14328
+ } else if (invocation.action === "list") {
14329
+ const { endpoints, credentials } = await deviceCredentials();
14330
+ const devices = await listDevices(endpoints, credentials);
14331
+ if (invocation.json) console.log(JSON.stringify({ devices }, null, 2));
14332
+ else if (devices.length === 0) console.log("No devices enrolled.");
14333
+ else for (const device of devices) console.log(`${device.id} ${device.name} ${deviceStatus(device)}`);
14334
+ } else if (invocation.action === "rename") {
14335
+ const { endpoints, credentials } = await deviceCredentials();
14336
+ const device = await renameDevice(endpoints, credentials, invocation.deviceId, invocation.name);
14337
+ console.log(`Renamed device ${device.id} to ${device.name}.`);
14338
+ } else {
14339
+ const { endpoints, credentials } = await deviceCredentials();
14340
+ await revokeDevice(endpoints, credentials, invocation.deviceId);
14341
+ console.log(`Revoked device ${invocation.deviceId}. Running workers on it are disconnected.`);
14132
14342
  }
14133
14343
  }
14134
14344
  main().catch((error46) => {
14135
14345
  console.error(error46 instanceof Error ? error46.message : String(error46));
14346
+ if (error46 instanceof UsageError) console.error("Run glossa --help for usage.");
14136
14347
  process.exitCode = 1;
14137
14348
  });
14138
- //# sourceMappingURL=main.js.map