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