sleev 0.0.13 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/bin/sleev.js +84 -0
- package/package.json +36 -55
- package/postinstall.mjs +66 -0
- package/dist/auth/browser.d.ts +0 -12
- package/dist/auth/browser.js +0 -89
- package/dist/auth/browser.js.map +0 -1
- package/dist/auth/firebase-tokens.d.ts +0 -39
- package/dist/auth/firebase-tokens.js +0 -173
- package/dist/auth/firebase-tokens.js.map +0 -1
- package/dist/auth/firebase.d.ts +0 -17
- package/dist/auth/firebase.js +0 -55
- package/dist/auth/firebase.js.map +0 -1
- package/dist/auth/login.d.ts +0 -22
- package/dist/auth/login.js +0 -58
- package/dist/auth/login.js.map +0 -1
- package/dist/auth/loopback.d.ts +0 -19
- package/dist/auth/loopback.js +0 -204
- package/dist/auth/loopback.js.map +0 -1
- package/dist/auth/schema.d.ts +0 -26
- package/dist/auth/schema.js +0 -88
- package/dist/auth/schema.js.map +0 -1
- package/dist/auth/token.d.ts +0 -19
- package/dist/auth/token.js +0 -53
- package/dist/auth/token.js.map +0 -1
- package/dist/auth/verify.d.ts +0 -6
- package/dist/auth/verify.js +0 -22
- package/dist/auth/verify.js.map +0 -1
- package/dist/cli.d.ts +0 -8
- package/dist/cli.js +0 -255
- package/dist/cli.js.map +0 -1
- package/dist/config/paths.d.ts +0 -12
- package/dist/config/paths.js +0 -34
- package/dist/config/paths.js.map +0 -1
- package/dist/config/store.d.ts +0 -63
- package/dist/config/store.js +0 -270
- package/dist/config/store.js.map +0 -1
- package/dist/control/local-installations.d.ts +0 -13
- package/dist/control/local-installations.js +0 -37
- package/dist/control/local-installations.js.map +0 -1
- package/dist/daemon/config.d.ts +0 -10
- package/dist/daemon/config.js +0 -52
- package/dist/daemon/config.js.map +0 -1
- package/dist/daemon/controller.d.ts +0 -29
- package/dist/daemon/controller.js +0 -34
- package/dist/daemon/controller.js.map +0 -1
- package/dist/daemon/health.d.ts +0 -8
- package/dist/daemon/health.js +0 -47
- package/dist/daemon/health.js.map +0 -1
- package/dist/daemon/start.d.ts +0 -5
- package/dist/daemon/start.js +0 -24
- package/dist/daemon/start.js.map +0 -1
- package/dist/daemon/systemd.d.ts +0 -35
- package/dist/daemon/systemd.js +0 -181
- package/dist/daemon/systemd.js.map +0 -1
- package/dist/gateway/acquire.d.ts +0 -14
- package/dist/gateway/acquire.js +0 -112
- package/dist/gateway/acquire.js.map +0 -1
- package/dist/gateway/manifest.d.ts +0 -26
- package/dist/gateway/manifest.js +0 -98
- package/dist/gateway/manifest.js.map +0 -1
- package/dist/gateway/platform.d.ts +0 -5
- package/dist/gateway/platform.js +0 -27
- package/dist/gateway/platform.js.map +0 -1
- package/dist/harness/claude.d.ts +0 -8
- package/dist/harness/claude.js +0 -151
- package/dist/harness/claude.js.map +0 -1
- package/dist/harness/codex.d.ts +0 -10
- package/dist/harness/codex.js +0 -213
- package/dist/harness/codex.js.map +0 -1
- package/dist/harness/index.d.ts +0 -13
- package/dist/harness/index.js +0 -63
- package/dist/harness/index.js.map +0 -1
- package/dist/harness/opencode-config.d.ts +0 -8
- package/dist/harness/opencode-config.js +0 -301
- package/dist/harness/opencode-config.js.map +0 -1
- package/dist/harness/opencode.d.ts +0 -14
- package/dist/harness/opencode.js +0 -50
- package/dist/harness/opencode.js.map +0 -1
- package/dist/harness/providers.d.ts +0 -8
- package/dist/harness/providers.js +0 -30
- package/dist/harness/providers.js.map +0 -1
- package/dist/harness/types.d.ts +0 -14
- package/dist/harness/types.js +0 -2
- package/dist/harness/types.js.map +0 -1
- package/dist/product.d.ts +0 -12
- package/dist/product.js +0 -70
- package/dist/product.js.map +0 -1
- package/dist/tui/clipboard.d.ts +0 -1
- package/dist/tui/clipboard.js +0 -48
- package/dist/tui/clipboard.js.map +0 -1
- package/dist/tui/openTuiApp.d.ts +0 -1
- package/dist/tui/openTuiApp.js +0 -1436
- package/dist/tui/openTuiApp.js.map +0 -1
- package/dist/tui/pages/flow-screen.d.ts +0 -50
- package/dist/tui/pages/flow-screen.js +0 -435
- package/dist/tui/pages/flow-screen.js.map +0 -1
- package/dist/tui/pages/footer.d.ts +0 -7
- package/dist/tui/pages/footer.js +0 -14
- package/dist/tui/pages/footer.js.map +0 -1
- package/dist/tui/pages/home-screen.d.ts +0 -19
- package/dist/tui/pages/home-screen.js +0 -89
- package/dist/tui/pages/home-screen.js.map +0 -1
- package/dist/tui/pages/index.d.ts +0 -17
- package/dist/tui/pages/index.js +0 -11
- package/dist/tui/pages/index.js.map +0 -1
- package/dist/tui/pages/local-setup-screen.d.ts +0 -23
- package/dist/tui/pages/local-setup-screen.js +0 -134
- package/dist/tui/pages/local-setup-screen.js.map +0 -1
- package/dist/tui/pages/location-card.d.ts +0 -19
- package/dist/tui/pages/location-card.js +0 -116
- package/dist/tui/pages/location-card.js.map +0 -1
- package/dist/tui/pages/menu-screen.d.ts +0 -24
- package/dist/tui/pages/menu-screen.js +0 -117
- package/dist/tui/pages/menu-screen.js.map +0 -1
- package/dist/tui/pages/session-grid.d.ts +0 -23
- package/dist/tui/pages/session-grid.js +0 -552
- package/dist/tui/pages/session-grid.js.map +0 -1
- package/dist/tui/pages/setup-screen.d.ts +0 -15
- package/dist/tui/pages/setup-screen.js +0 -80
- package/dist/tui/pages/setup-screen.js.map +0 -1
- package/dist/tui/pages/terminal-log.d.ts +0 -14
- package/dist/tui/pages/terminal-log.js +0 -104
- package/dist/tui/pages/terminal-log.js.map +0 -1
- package/dist/tui/pages/welcome-screen.d.ts +0 -16
- package/dist/tui/pages/welcome-screen.js +0 -80
- package/dist/tui/pages/welcome-screen.js.map +0 -1
- package/dist/tui/saved-tokens.d.ts +0 -10
- package/dist/tui/saved-tokens.js +0 -104
- package/dist/tui/saved-tokens.js.map +0 -1
- package/dist/tui/theme.d.ts +0 -25
- package/dist/tui/theme.js +0 -27
- package/dist/tui/theme.js.map +0 -1
package/dist/tui/openTuiApp.js
DELETED
|
@@ -1,1436 +0,0 @@
|
|
|
1
|
-
// Runs the OpenTUI app shell and wires page UI to local daemon/auth state.
|
|
2
|
-
import { Writable } from "node:stream";
|
|
3
|
-
import { addTokenConfig, readConfig, setDefaultTokenConfig, setHarnessesConfig, setLocalInstallationConfig, setLocationConfig, setGatewayConfig, signInConfig, signOutConfig, writeConfig, } from "../config/store.js";
|
|
4
|
-
import { acquireGateway } from "../gateway/acquire.js";
|
|
5
|
-
import { gatewayUpdates } from "../product.js";
|
|
6
|
-
import { firebaseLogin } from "../auth/firebase.js";
|
|
7
|
-
import { clearStoredFirebaseAuth, saveFirebaseLogin, } from "../auth/firebase-tokens.js";
|
|
8
|
-
import { createToken } from "../auth/token.js";
|
|
9
|
-
import { createLocalInstallation } from "../control/local-installations.js";
|
|
10
|
-
import { clearGatewayConfig, writeGatewayConfig } from "../daemon/config.js";
|
|
11
|
-
import { createController } from "../daemon/controller.js";
|
|
12
|
-
import { setupHarnesses, unpatchHarnesses, } from "../harness/index.js";
|
|
13
|
-
import { copyText } from "./clipboard.js";
|
|
14
|
-
import { FlowScreen, HOME_CARDS, LocalSetupScreen, MenuScreen, terminalLogHeight, } from "./pages/index.js";
|
|
15
|
-
import { formatSavedTokens, projectSavedTokens, readCachedSavedTokens, refreshSavedTokens, } from "./saved-tokens.js";
|
|
16
|
-
import { theme } from "./theme.js";
|
|
17
|
-
const spinnerFrames = ["-", "\\", "|", "/"];
|
|
18
|
-
const TUI_LOGIN_TIMEOUT_MS = 2 * 60 * 1000;
|
|
19
|
-
const TOKEN_LABEL_MAX = 80;
|
|
20
|
-
const TOKENS_REFRESH_TIMEOUT_MS = 10_000;
|
|
21
|
-
const SLIDE_MS = 500;
|
|
22
|
-
const SLIDE_FRAME_MS = 33;
|
|
23
|
-
// Only Claude Code is offered in the TUI for now. The full registry
|
|
24
|
-
// (opencode, codex) still lives in src/harness/index.ts and its patch/
|
|
25
|
-
// unpatch logic is unchanged — this list is just the presented choice set.
|
|
26
|
-
const HARNESS_CARDS = [
|
|
27
|
-
{
|
|
28
|
-
id: "claude-code",
|
|
29
|
-
label: "Claude Code",
|
|
30
|
-
},
|
|
31
|
-
];
|
|
32
|
-
export async function runTui() {
|
|
33
|
-
if (!isBunRuntime()) {
|
|
34
|
-
console.error([
|
|
35
|
-
"The sleeved TUI uses OpenTUI, which currently requires Bun.",
|
|
36
|
-
"For now, run non-interactive commands with Node or launch the TUI with Bun.",
|
|
37
|
-
"Example: bun dist/cli.js tui",
|
|
38
|
-
].join("\n"));
|
|
39
|
-
process.exitCode = 1;
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
const core = await import("@opentui/core");
|
|
43
|
-
let config = await readConfig();
|
|
44
|
-
let tokens = await readCachedSavedTokens();
|
|
45
|
-
let tokensSaved = renderSavedTokens(tokens);
|
|
46
|
-
// The menu is the signed-in home. Signed-out users land in the wizard,
|
|
47
|
-
// which firstPage pins to "welcome" (the only reachable page until they
|
|
48
|
-
// authenticate) — so the menu is unreachable without sign-in.
|
|
49
|
-
let view = isAuthed(config) ? "menu" : "wizard";
|
|
50
|
-
let page = firstPage(config);
|
|
51
|
-
let stage = stageFor(page);
|
|
52
|
-
let mode = config.location.mode;
|
|
53
|
-
let selectedIndex = selectedIndexFor(mode);
|
|
54
|
-
let menuIndex = firstEnabledMenuIndex(menuItems(config), 0);
|
|
55
|
-
let tokenIndex = 0;
|
|
56
|
-
let harnessIndex = 0;
|
|
57
|
-
let selectedHarnesses = new Set();
|
|
58
|
-
let tokenLabelOpen = false;
|
|
59
|
-
let tokenLabel = "";
|
|
60
|
-
let spinnerIndex = 0;
|
|
61
|
-
let busy = false;
|
|
62
|
-
let cancelLogin = null;
|
|
63
|
-
let closing = false;
|
|
64
|
-
// Local-installation progress model (rendered by LocalSetupScreen).
|
|
65
|
-
const localSetupLabels = [
|
|
66
|
-
"Register installation with control",
|
|
67
|
-
"Select local mode",
|
|
68
|
-
"Install latest gateway binary",
|
|
69
|
-
"Write gateway daemon config",
|
|
70
|
-
"Start gateway daemon",
|
|
71
|
-
];
|
|
72
|
-
let localSetupSteps = [];
|
|
73
|
-
let localSetupNote = null;
|
|
74
|
-
let localSetupState = "running";
|
|
75
|
-
let logs = initialLogs(config);
|
|
76
|
-
let logOffset = 0;
|
|
77
|
-
let notice = null;
|
|
78
|
-
let noticeTimer = null;
|
|
79
|
-
let slideTimer = null;
|
|
80
|
-
const renderer = await core.createCliRenderer({
|
|
81
|
-
exitOnCtrlC: false,
|
|
82
|
-
exitSignals: [
|
|
83
|
-
"SIGTERM",
|
|
84
|
-
"SIGQUIT",
|
|
85
|
-
"SIGABRT",
|
|
86
|
-
"SIGHUP",
|
|
87
|
-
"SIGBREAK",
|
|
88
|
-
"SIGPIPE",
|
|
89
|
-
"SIGBUS",
|
|
90
|
-
],
|
|
91
|
-
useMouse: true,
|
|
92
|
-
enableMouseMovement: true,
|
|
93
|
-
});
|
|
94
|
-
let backdrop = null;
|
|
95
|
-
let backdropColumns = 0;
|
|
96
|
-
let backdropRows = 0;
|
|
97
|
-
let tokensRefresh = null;
|
|
98
|
-
const draw = () => {
|
|
99
|
-
if (closing)
|
|
100
|
-
return;
|
|
101
|
-
const columns = renderer.terminalWidth || renderer.width;
|
|
102
|
-
const rows = renderer.terminalHeight || renderer.height;
|
|
103
|
-
removeContent(true);
|
|
104
|
-
if (!backdrop || backdropColumns !== columns || backdropRows !== rows) {
|
|
105
|
-
if (backdrop) {
|
|
106
|
-
renderer.root.remove(backdrop.id);
|
|
107
|
-
backdrop.destroyRecursively();
|
|
108
|
-
}
|
|
109
|
-
backdrop = background(core, renderer, columns, rows);
|
|
110
|
-
backdropColumns = columns;
|
|
111
|
-
backdropRows = rows;
|
|
112
|
-
renderer.root.add(backdrop);
|
|
113
|
-
}
|
|
114
|
-
const screen = view === "local-setup"
|
|
115
|
-
? LocalSetupScreen(core, {
|
|
116
|
-
steps: localSetupSteps,
|
|
117
|
-
note: localSetupNote,
|
|
118
|
-
state: localSetupState,
|
|
119
|
-
spinner: spinnerFrames[spinnerIndex],
|
|
120
|
-
columns,
|
|
121
|
-
rows,
|
|
122
|
-
logs,
|
|
123
|
-
logOffset,
|
|
124
|
-
logNotice: notice,
|
|
125
|
-
onLogScroll: scrollLogs,
|
|
126
|
-
onLogLink: (url) => void copyLink(url),
|
|
127
|
-
})
|
|
128
|
-
: view === "menu"
|
|
129
|
-
? MenuScreen(core, {
|
|
130
|
-
items: menuItems(config),
|
|
131
|
-
selectedIndex: menuIndex,
|
|
132
|
-
columns,
|
|
133
|
-
rows,
|
|
134
|
-
logs,
|
|
135
|
-
logOffset,
|
|
136
|
-
logNotice: notice,
|
|
137
|
-
onHover: selectMenu,
|
|
138
|
-
onChoose: (index) => void chooseMenu(index),
|
|
139
|
-
onLogScroll: scrollLogs,
|
|
140
|
-
onLogLink: (url) => void copyLink(url),
|
|
141
|
-
})
|
|
142
|
-
: FlowScreen(core, {
|
|
143
|
-
page,
|
|
144
|
-
stage,
|
|
145
|
-
selectedIndex,
|
|
146
|
-
tokenIndex,
|
|
147
|
-
harnessIndex,
|
|
148
|
-
mode,
|
|
149
|
-
tokens: config.tokens.items,
|
|
150
|
-
defaultTokenId: config.tokens.defaultId,
|
|
151
|
-
harnesses: harnessCards(config, selectedHarnesses),
|
|
152
|
-
columns,
|
|
153
|
-
rows,
|
|
154
|
-
logs,
|
|
155
|
-
logOffset,
|
|
156
|
-
logNotice: notice,
|
|
157
|
-
busy,
|
|
158
|
-
spinner: spinnerFrames[spinnerIndex],
|
|
159
|
-
onFlowScroll: scrollFlow,
|
|
160
|
-
onLogScroll: scrollLogs,
|
|
161
|
-
onLogLink: (url) => void copyLink(url),
|
|
162
|
-
onSignIn: () => void signIn(),
|
|
163
|
-
onCreateToken: openTokenLabel,
|
|
164
|
-
onTokenHover: selectToken,
|
|
165
|
-
onTokenChoose: (index) => void chooseToken(index),
|
|
166
|
-
onHarnessHover: selectHarness,
|
|
167
|
-
onHarnessToggle: toggleHarness,
|
|
168
|
-
onConfigureHarnesses: () => void configureHarnesses(),
|
|
169
|
-
onHover: select,
|
|
170
|
-
onChoose: (index) => void chooseLocation(index),
|
|
171
|
-
});
|
|
172
|
-
const content = [
|
|
173
|
-
screen,
|
|
174
|
-
TokensSaved(core, tokensSaved),
|
|
175
|
-
AppControls(core, {
|
|
176
|
-
mode,
|
|
177
|
-
onModeClick: showLocation,
|
|
178
|
-
onMenuClick: showMenu,
|
|
179
|
-
}),
|
|
180
|
-
];
|
|
181
|
-
if (tokenLabelOpen) {
|
|
182
|
-
content.push(TokenLabelPrompt(core, {
|
|
183
|
-
columns,
|
|
184
|
-
rows,
|
|
185
|
-
value: tokenLabel,
|
|
186
|
-
max: TOKEN_LABEL_MAX,
|
|
187
|
-
onCancel: closeTokenLabel,
|
|
188
|
-
onSubmit: submitTokenLabel,
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
for (const child of content) {
|
|
192
|
-
renderer.root.add(child);
|
|
193
|
-
}
|
|
194
|
-
renderer.requestRender();
|
|
195
|
-
};
|
|
196
|
-
const removeContent = (destroy) => {
|
|
197
|
-
for (const child of renderer.root.getChildren()) {
|
|
198
|
-
if (child === backdrop)
|
|
199
|
-
continue;
|
|
200
|
-
renderer.root.remove(child.id);
|
|
201
|
-
if (destroy)
|
|
202
|
-
child.destroyRecursively();
|
|
203
|
-
}
|
|
204
|
-
};
|
|
205
|
-
const refresh = async () => {
|
|
206
|
-
config = await readConfig();
|
|
207
|
-
};
|
|
208
|
-
const refreshTokens = async () => {
|
|
209
|
-
if (closing || tokensRefresh)
|
|
210
|
-
return;
|
|
211
|
-
const controller = new AbortController();
|
|
212
|
-
const timer = setTimeout(() => controller.abort(), TOKENS_REFRESH_TIMEOUT_MS);
|
|
213
|
-
timer.unref();
|
|
214
|
-
tokensRefresh = controller;
|
|
215
|
-
try {
|
|
216
|
-
const next = await refreshSavedTokens(controller.signal);
|
|
217
|
-
if (closing || next === null)
|
|
218
|
-
return;
|
|
219
|
-
tokens = next;
|
|
220
|
-
tickTokens();
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
// The cached footer value is acceptable when the public counter is offline.
|
|
224
|
-
}
|
|
225
|
-
finally {
|
|
226
|
-
clearTimeout(timer);
|
|
227
|
-
if (tokensRefresh === controller)
|
|
228
|
-
tokensRefresh = null;
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
const tickTokens = () => {
|
|
232
|
-
const next = renderSavedTokens(tokens);
|
|
233
|
-
if (closing)
|
|
234
|
-
return;
|
|
235
|
-
if (busy)
|
|
236
|
-
spinnerIndex = (spinnerIndex + 1) % spinnerFrames.length;
|
|
237
|
-
if (next === tokensSaved && !busy)
|
|
238
|
-
return;
|
|
239
|
-
tokensSaved = next;
|
|
240
|
-
draw();
|
|
241
|
-
};
|
|
242
|
-
const select = (index) => {
|
|
243
|
-
if (busy || selectedIndex === index || !HOME_CARDS[index])
|
|
244
|
-
return;
|
|
245
|
-
selectedIndex = index;
|
|
246
|
-
draw();
|
|
247
|
-
};
|
|
248
|
-
const selectMenu = (index) => {
|
|
249
|
-
const items = menuItems(config);
|
|
250
|
-
if (busy || !items[index] || menuIndex === index)
|
|
251
|
-
return;
|
|
252
|
-
menuIndex = index;
|
|
253
|
-
draw();
|
|
254
|
-
};
|
|
255
|
-
const chooseMenu = async (index = menuIndex) => {
|
|
256
|
-
if (busy)
|
|
257
|
-
return;
|
|
258
|
-
const item = menuItems(config)[index];
|
|
259
|
-
if (!item?.enabled)
|
|
260
|
-
return;
|
|
261
|
-
menuIndex = index;
|
|
262
|
-
if (item.action === "local-setup") {
|
|
263
|
-
await setupLocalInstallation();
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
if (item.action === "wizard") {
|
|
267
|
-
showPage(firstPage(config));
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
if (item.action === "auth") {
|
|
271
|
-
if (isAuthed(config)) {
|
|
272
|
-
await signOut();
|
|
273
|
-
}
|
|
274
|
-
else {
|
|
275
|
-
await signIn();
|
|
276
|
-
}
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
if (item.action === "location") {
|
|
280
|
-
showLocation();
|
|
281
|
-
return;
|
|
282
|
-
}
|
|
283
|
-
if (item.action === "tokens") {
|
|
284
|
-
showPage("tokens");
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
if (item.action === "harnesses") {
|
|
288
|
-
showSetup();
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
};
|
|
292
|
-
const showMenu = () => {
|
|
293
|
-
// The menu is gated on auth: signed-out users must complete the
|
|
294
|
-
// welcome/sign-in flow first. This is the single chokepoint for the
|
|
295
|
-
// `m` key and the on-screen menu button.
|
|
296
|
-
if (busy || !isAuthed(config))
|
|
297
|
-
return;
|
|
298
|
-
view = "menu";
|
|
299
|
-
menuIndex = firstEnabledMenuIndex(menuItems(config), menuIndex);
|
|
300
|
-
draw();
|
|
301
|
-
};
|
|
302
|
-
const signIn = async () => {
|
|
303
|
-
if (busy)
|
|
304
|
-
return;
|
|
305
|
-
busy = true;
|
|
306
|
-
spinnerIndex = 0;
|
|
307
|
-
appendLog("> auth: opening browser for Sleev sign in");
|
|
308
|
-
appendLog("> auth: waiting for browser confirmation");
|
|
309
|
-
draw();
|
|
310
|
-
const controller = new AbortController();
|
|
311
|
-
cancelLogin = () => controller.abort();
|
|
312
|
-
try {
|
|
313
|
-
const result = await firebaseLogin({
|
|
314
|
-
output: mutedOutput(),
|
|
315
|
-
timeoutMs: TUI_LOGIN_TIMEOUT_MS,
|
|
316
|
-
signal: controller.signal,
|
|
317
|
-
onUrl: (url) => {
|
|
318
|
-
appendLog("> auth: open this link if the browser does not appear");
|
|
319
|
-
appendLog(` ${url}`);
|
|
320
|
-
draw();
|
|
321
|
-
},
|
|
322
|
-
});
|
|
323
|
-
await saveFirebaseLogin(result);
|
|
324
|
-
await writeConfig(signInConfig(config, {
|
|
325
|
-
label: result.label,
|
|
326
|
-
accountId: result.accountId,
|
|
327
|
-
email: result.email,
|
|
328
|
-
createdAt: new Date().toISOString(),
|
|
329
|
-
}));
|
|
330
|
-
await refresh();
|
|
331
|
-
mode = config.location.mode;
|
|
332
|
-
// Sign-in complete: land on the menu (the signed-in home), not
|
|
333
|
-
// the wizard. The wizard stays reachable from the menu and is
|
|
334
|
-
// pre-positioned at the next required step.
|
|
335
|
-
page = firstPage(config);
|
|
336
|
-
stage = stageFor(page);
|
|
337
|
-
view = "menu";
|
|
338
|
-
menuIndex = firstEnabledMenuIndex(menuItems(config), 0);
|
|
339
|
-
selectedIndex = selectedIndexFor(mode);
|
|
340
|
-
tokenIndex = 0;
|
|
341
|
-
selectedHarnesses = new Set();
|
|
342
|
-
appendLog(`> auth: signed in${result.email ? ` as ${result.email}` : ""}`);
|
|
343
|
-
}
|
|
344
|
-
catch (error) {
|
|
345
|
-
appendLog(`> auth: ${authError(error, "Sign in failed.")}`);
|
|
346
|
-
if (!(error instanceof Error))
|
|
347
|
-
throw error;
|
|
348
|
-
}
|
|
349
|
-
finally {
|
|
350
|
-
cancelLogin = null;
|
|
351
|
-
busy = false;
|
|
352
|
-
if (!closing)
|
|
353
|
-
draw();
|
|
354
|
-
}
|
|
355
|
-
};
|
|
356
|
-
const chooseLocation = async (index = selectedIndex) => {
|
|
357
|
-
if (busy || page !== "location")
|
|
358
|
-
return;
|
|
359
|
-
const action = HOME_CARDS[index]?.action;
|
|
360
|
-
if (!action)
|
|
361
|
-
return;
|
|
362
|
-
select(index);
|
|
363
|
-
await writeConfig(setLocationConfig(config, action));
|
|
364
|
-
await refresh();
|
|
365
|
-
mode = config.location.mode;
|
|
366
|
-
selectedIndex = selectedIndexFor(mode);
|
|
367
|
-
appendLog(`> mode: ${action} service selected`);
|
|
368
|
-
if (action === "local" && !config.localInstallation) {
|
|
369
|
-
await provisionLocalInstallation();
|
|
370
|
-
}
|
|
371
|
-
showPage(firstPage(config));
|
|
372
|
-
};
|
|
373
|
-
// Trade the Firebase identity for a long-lived installation credential the
|
|
374
|
-
// local daemon will use. Idempotent: the caller skips this when a
|
|
375
|
-
// credential already exists. A failure here does not block mode
|
|
376
|
-
// selection; the user stays in local mode and can retry by reselecting.
|
|
377
|
-
const provisionLocalInstallation = async () => {
|
|
378
|
-
busy = true;
|
|
379
|
-
spinnerIndex = 0;
|
|
380
|
-
appendLog("> local: registering installation with control");
|
|
381
|
-
draw();
|
|
382
|
-
try {
|
|
383
|
-
const created = await createLocalInstallation({
|
|
384
|
-
label: config.auth.label ?? null,
|
|
385
|
-
});
|
|
386
|
-
await writeConfig(setLocalInstallationConfig(config, {
|
|
387
|
-
installationId: created.installationId,
|
|
388
|
-
credential: created.credential,
|
|
389
|
-
controlUrl: created.controlUrl,
|
|
390
|
-
}));
|
|
391
|
-
await refresh();
|
|
392
|
-
appendLog("> local: installation registered");
|
|
393
|
-
// Materialize the daemon config now so the bridge file exists
|
|
394
|
-
// before the daemon is started (write-then-start contract).
|
|
395
|
-
await writeGatewayConfig(config);
|
|
396
|
-
appendLog("> local: gateway config written");
|
|
397
|
-
}
|
|
398
|
-
catch (error) {
|
|
399
|
-
appendLog(`> local: ${authError(error, "Installation registration failed.")}`);
|
|
400
|
-
if (!(error instanceof Error))
|
|
401
|
-
throw error;
|
|
402
|
-
}
|
|
403
|
-
finally {
|
|
404
|
-
busy = false;
|
|
405
|
-
}
|
|
406
|
-
};
|
|
407
|
-
// End-to-end local installation: register the installation credential
|
|
408
|
-
// with control, install the latest gateway binary, write the daemon
|
|
409
|
-
// bridge config, and bring the supervised daemon up. Each step is
|
|
410
|
-
// idempotent so re-running repairs a partial setup.
|
|
411
|
-
const setLocalStep = (index, status, note) => {
|
|
412
|
-
const step = localSetupSteps[index];
|
|
413
|
-
if (step)
|
|
414
|
-
step.status = status;
|
|
415
|
-
if (note !== undefined)
|
|
416
|
-
localSetupNote = note;
|
|
417
|
-
if (!closing)
|
|
418
|
-
draw();
|
|
419
|
-
};
|
|
420
|
-
const setupLocalInstallation = async () => {
|
|
421
|
-
if (busy)
|
|
422
|
-
return;
|
|
423
|
-
busy = true;
|
|
424
|
-
spinnerIndex = 0;
|
|
425
|
-
view = "local-setup";
|
|
426
|
-
localSetupSteps = localSetupLabels.map((label) => ({ label, status: "pending" }));
|
|
427
|
-
localSetupNote = null;
|
|
428
|
-
localSetupState = "running";
|
|
429
|
-
appendLog("> local: starting local installation setup");
|
|
430
|
-
draw();
|
|
431
|
-
try {
|
|
432
|
-
// 1. Long-lived installation credential from control (skip if
|
|
433
|
-
// we already have one).
|
|
434
|
-
if (!config.localInstallation) {
|
|
435
|
-
setLocalStep(0, "active", "Registering this machine with the control server…");
|
|
436
|
-
appendLog("> local: registering installation with control");
|
|
437
|
-
const created = await createLocalInstallation({
|
|
438
|
-
label: config.auth.label ?? null,
|
|
439
|
-
});
|
|
440
|
-
await writeConfig(setLocalInstallationConfig(config, {
|
|
441
|
-
installationId: created.installationId,
|
|
442
|
-
credential: created.credential,
|
|
443
|
-
controlUrl: created.controlUrl,
|
|
444
|
-
}));
|
|
445
|
-
await refresh();
|
|
446
|
-
appendLog("> local: installation registered");
|
|
447
|
-
setLocalStep(0, "done");
|
|
448
|
-
}
|
|
449
|
-
else {
|
|
450
|
-
appendLog("> local: installation already registered");
|
|
451
|
-
setLocalStep(0, "skipped", "Installation already registered with control.");
|
|
452
|
-
}
|
|
453
|
-
// 2. Keep the rest of the UI consistent with local mode.
|
|
454
|
-
if (config.location.mode !== "local") {
|
|
455
|
-
setLocalStep(1, "active", "Switching to local mode…");
|
|
456
|
-
await writeConfig(setLocationConfig(config, "local"));
|
|
457
|
-
await refresh();
|
|
458
|
-
mode = config.location.mode;
|
|
459
|
-
selectedIndex = selectedIndexFor(mode);
|
|
460
|
-
setLocalStep(1, "done");
|
|
461
|
-
}
|
|
462
|
-
else {
|
|
463
|
-
setLocalStep(1, "skipped", "Already in local mode.");
|
|
464
|
-
}
|
|
465
|
-
// 3. Ensure the latest released gateway binary is installed.
|
|
466
|
-
setLocalStep(2, "active", "Resolving the latest gateway release…");
|
|
467
|
-
appendLog("> local: resolving latest gateway release");
|
|
468
|
-
const { channel } = gatewayUpdates();
|
|
469
|
-
const installed = await acquireGateway({
|
|
470
|
-
currentVersion: config.gateway?.version ?? null,
|
|
471
|
-
});
|
|
472
|
-
await writeConfig(setGatewayConfig(config, {
|
|
473
|
-
binPath: installed.binPath,
|
|
474
|
-
version: installed.version,
|
|
475
|
-
channel,
|
|
476
|
-
}));
|
|
477
|
-
await refresh();
|
|
478
|
-
appendLog(`> local: gateway ${installed.version} installed`);
|
|
479
|
-
setLocalStep(2, "done", `Gateway ${installed.version} installed.`);
|
|
480
|
-
// 4. Materialize the daemon bridge config before starting it
|
|
481
|
-
// (write-then-start contract).
|
|
482
|
-
setLocalStep(3, "active", "Writing the gateway daemon config…");
|
|
483
|
-
await writeGatewayConfig(config);
|
|
484
|
-
appendLog("> local: gateway config written");
|
|
485
|
-
setLocalStep(3, "done");
|
|
486
|
-
// 5. Bring up (or confirm) the supervised daemon.
|
|
487
|
-
setLocalStep(4, "active", "Starting the supervised gateway daemon…");
|
|
488
|
-
appendLog("> local: starting gateway daemon");
|
|
489
|
-
const controller = await createController(config);
|
|
490
|
-
const status = await controller.start();
|
|
491
|
-
appendLog(`> local: daemon ${status.backend} ${status.address} ` +
|
|
492
|
-
`(healthy=${status.healthy}, ` +
|
|
493
|
-
`configured=${status.configured ?? "n/a"})`);
|
|
494
|
-
setLocalStep(4, "done", `Daemon running at ${status.address} ` +
|
|
495
|
-
`(healthy=${status.healthy}).`);
|
|
496
|
-
localSetupState = "done";
|
|
497
|
-
appendLog("> local: setup complete");
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
const detail = authError(error, "Local installation setup failed.");
|
|
501
|
-
const active = localSetupSteps.findIndex((step) => step.status === "active");
|
|
502
|
-
if (active >= 0)
|
|
503
|
-
localSetupSteps[active].status = "error";
|
|
504
|
-
localSetupNote = detail;
|
|
505
|
-
localSetupState = "error";
|
|
506
|
-
appendLog(`> local: ${detail}`);
|
|
507
|
-
if (!(error instanceof Error))
|
|
508
|
-
throw error;
|
|
509
|
-
}
|
|
510
|
-
finally {
|
|
511
|
-
busy = false;
|
|
512
|
-
if (closing) {
|
|
513
|
-
// nothing — app is shutting down
|
|
514
|
-
}
|
|
515
|
-
else if (localSetupState === "done") {
|
|
516
|
-
// Installation finished: move the user onward to pick a
|
|
517
|
-
// harness to patch (Claude Code preselected as it's the
|
|
518
|
-
// only option for now).
|
|
519
|
-
harnessIndex = 0;
|
|
520
|
-
selectedHarnesses = new Set(["claude-code"]);
|
|
521
|
-
showPage("setup");
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
draw();
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
};
|
|
528
|
-
const showLocation = () => {
|
|
529
|
-
if (!isAuthed(config) || busy)
|
|
530
|
-
return;
|
|
531
|
-
if (!mode) {
|
|
532
|
-
showPage("location");
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
selectedIndex = Math.max(0, HOME_CARDS.findIndex((card) => card.action === mode));
|
|
536
|
-
showPage("location");
|
|
537
|
-
};
|
|
538
|
-
const selectToken = (index) => {
|
|
539
|
-
if (busy || !config.tokens.items[index] || tokenIndex === index)
|
|
540
|
-
return;
|
|
541
|
-
tokenIndex = index;
|
|
542
|
-
draw();
|
|
543
|
-
};
|
|
544
|
-
const chooseToken = async (index = tokenIndex) => {
|
|
545
|
-
if (busy || page !== "tokens")
|
|
546
|
-
return;
|
|
547
|
-
const token = config.tokens.items[index];
|
|
548
|
-
if (!token)
|
|
549
|
-
return;
|
|
550
|
-
await writeConfig(setDefaultTokenConfig(config, token.id));
|
|
551
|
-
await refresh();
|
|
552
|
-
tokenIndex = index;
|
|
553
|
-
appendLog(`> token: default set to ${token.label ?? maskToken(token.token)}`);
|
|
554
|
-
showSetup();
|
|
555
|
-
};
|
|
556
|
-
const showSetup = () => {
|
|
557
|
-
if (!isAuthed(config) || busy)
|
|
558
|
-
return;
|
|
559
|
-
if (!mode) {
|
|
560
|
-
showPage("location");
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
if (!config.tokens.defaultId) {
|
|
564
|
-
showPage("tokens");
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
showPage("setup");
|
|
568
|
-
};
|
|
569
|
-
const openTokenLabel = () => {
|
|
570
|
-
if (busy || !isAuthed(config))
|
|
571
|
-
return;
|
|
572
|
-
tokenLabelOpen = true;
|
|
573
|
-
tokenLabel = "";
|
|
574
|
-
draw();
|
|
575
|
-
};
|
|
576
|
-
const closeTokenLabel = () => {
|
|
577
|
-
tokenLabelOpen = false;
|
|
578
|
-
tokenLabel = "";
|
|
579
|
-
draw();
|
|
580
|
-
};
|
|
581
|
-
const submitTokenLabel = () => {
|
|
582
|
-
const label = cleanTokenLabel(tokenLabel);
|
|
583
|
-
if (!label) {
|
|
584
|
-
showNotice("enter a token name");
|
|
585
|
-
return;
|
|
586
|
-
}
|
|
587
|
-
tokenLabelOpen = false;
|
|
588
|
-
tokenLabel = "";
|
|
589
|
-
void makeToken(label);
|
|
590
|
-
};
|
|
591
|
-
const makeToken = async (label) => {
|
|
592
|
-
if (busy || !isAuthed(config))
|
|
593
|
-
return;
|
|
594
|
-
busy = true;
|
|
595
|
-
spinnerIndex = 0;
|
|
596
|
-
appendLog(`> token: opening browser to create ${label}`);
|
|
597
|
-
appendLog("> token: waiting for browser confirmation");
|
|
598
|
-
draw();
|
|
599
|
-
const controller = new AbortController();
|
|
600
|
-
cancelLogin = () => controller.abort();
|
|
601
|
-
try {
|
|
602
|
-
const token = await createToken({
|
|
603
|
-
label,
|
|
604
|
-
output: mutedOutput(),
|
|
605
|
-
timeoutMs: TUI_LOGIN_TIMEOUT_MS,
|
|
606
|
-
signal: controller.signal,
|
|
607
|
-
onUrl: (url) => {
|
|
608
|
-
appendLog("> token: open this link if the browser does not appear");
|
|
609
|
-
appendLog(` ${url}`);
|
|
610
|
-
draw();
|
|
611
|
-
},
|
|
612
|
-
});
|
|
613
|
-
await writeConfig(addTokenConfig(config, token));
|
|
614
|
-
await refresh();
|
|
615
|
-
tokenIndex = Math.max(0, config.tokens.items.findIndex((item) => item.id === token.id));
|
|
616
|
-
showPage(firstPage(config));
|
|
617
|
-
appendLog(`> token: created and selected ${token.label ?? maskToken(token.token)}`);
|
|
618
|
-
}
|
|
619
|
-
catch (error) {
|
|
620
|
-
appendLog(`> token: ${authError(error, "Token creation failed.")}`);
|
|
621
|
-
if (!(error instanceof Error))
|
|
622
|
-
throw error;
|
|
623
|
-
}
|
|
624
|
-
finally {
|
|
625
|
-
cancelLogin = null;
|
|
626
|
-
busy = false;
|
|
627
|
-
if (!closing)
|
|
628
|
-
draw();
|
|
629
|
-
}
|
|
630
|
-
};
|
|
631
|
-
const selectHarness = (index) => {
|
|
632
|
-
if (busy || !HARNESS_CARDS[index] || harnessIndex === index)
|
|
633
|
-
return;
|
|
634
|
-
harnessIndex = index;
|
|
635
|
-
draw();
|
|
636
|
-
};
|
|
637
|
-
const toggleHarness = (index = harnessIndex) => {
|
|
638
|
-
if (busy || page !== "setup")
|
|
639
|
-
return;
|
|
640
|
-
const harness = HARNESS_CARDS[index];
|
|
641
|
-
if (!harness)
|
|
642
|
-
return;
|
|
643
|
-
const next = new Set(selectedHarnesses);
|
|
644
|
-
if (next.has(harness.id))
|
|
645
|
-
next.delete(harness.id);
|
|
646
|
-
else
|
|
647
|
-
next.add(harness.id);
|
|
648
|
-
selectedHarnesses = next;
|
|
649
|
-
harnessIndex = index;
|
|
650
|
-
draw();
|
|
651
|
-
};
|
|
652
|
-
const configureHarnesses = async () => {
|
|
653
|
-
if (busy || page !== "setup")
|
|
654
|
-
return;
|
|
655
|
-
// In local-installation mode patch the harness to talk to the local
|
|
656
|
-
// daemon directly (no cloud token needed — the gateway runs in
|
|
657
|
-
// passthrough). Otherwise fall back to the selected cloud token.
|
|
658
|
-
const local = localHarnessAuth(config);
|
|
659
|
-
let auth;
|
|
660
|
-
if (local) {
|
|
661
|
-
auth = local;
|
|
662
|
-
}
|
|
663
|
-
else {
|
|
664
|
-
const token = defaultToken(config);
|
|
665
|
-
if (!token) {
|
|
666
|
-
showPage("tokens");
|
|
667
|
-
return;
|
|
668
|
-
}
|
|
669
|
-
auth = authForToken(token);
|
|
670
|
-
}
|
|
671
|
-
const selected = HARNESS_CARDS.map((harness) => harness.id).filter((id) => selectedHarnesses.has(id));
|
|
672
|
-
if (selected.length === 0)
|
|
673
|
-
return;
|
|
674
|
-
busy = true;
|
|
675
|
-
spinnerIndex = 0;
|
|
676
|
-
appendLog(`> harness: configuring ${selected.map(labelHarness).join(", ")}` +
|
|
677
|
-
(local ? " -> local gateway" : ""));
|
|
678
|
-
draw();
|
|
679
|
-
try {
|
|
680
|
-
const result = await setupHarnesses(auth, selected);
|
|
681
|
-
if (result.ids.length === 0) {
|
|
682
|
-
appendLog("> harness: no harnesses configured");
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
await writeConfig(setHarnessesConfig(config, result.ids));
|
|
686
|
-
await refresh();
|
|
687
|
-
selectedHarnesses = new Set();
|
|
688
|
-
appendLog(`> harness: configured ${result.label}` +
|
|
689
|
-
(local ? ` -> ${auth.proxyUrl}` : ""));
|
|
690
|
-
if (local) {
|
|
691
|
-
// No cloud token in the local flow, so firstPage() would
|
|
692
|
-
// bounce to the tokens page — return to the menu instead.
|
|
693
|
-
view = "menu";
|
|
694
|
-
menuIndex = firstEnabledMenuIndex(menuItems(config), 0);
|
|
695
|
-
}
|
|
696
|
-
else {
|
|
697
|
-
showPage(firstPage(config));
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
catch (error) {
|
|
701
|
-
appendLog(`> harness: ${authError(error, "Harness setup failed.")}`);
|
|
702
|
-
if (!(error instanceof Error))
|
|
703
|
-
throw error;
|
|
704
|
-
}
|
|
705
|
-
finally {
|
|
706
|
-
busy = false;
|
|
707
|
-
if (!closing)
|
|
708
|
-
draw();
|
|
709
|
-
}
|
|
710
|
-
};
|
|
711
|
-
const signOut = async () => {
|
|
712
|
-
if (busy || !isAuthed(config))
|
|
713
|
-
return;
|
|
714
|
-
busy = true;
|
|
715
|
-
appendLog("> auth: signing out");
|
|
716
|
-
try {
|
|
717
|
-
const cleanup = await unpatchHarnesses();
|
|
718
|
-
if (cleanup.failures[0])
|
|
719
|
-
appendLog(`> harness: cleanup failed for ${cleanup.failures.join("; ")}`);
|
|
720
|
-
try {
|
|
721
|
-
const controller = await createController(config);
|
|
722
|
-
await controller.teardown();
|
|
723
|
-
}
|
|
724
|
-
catch {
|
|
725
|
-
// Best-effort: don't block sign-out on daemon teardown.
|
|
726
|
-
}
|
|
727
|
-
await clearStoredFirebaseAuth();
|
|
728
|
-
await clearGatewayConfig();
|
|
729
|
-
await writeConfig(signOutConfig(config));
|
|
730
|
-
await refresh();
|
|
731
|
-
showPage("welcome");
|
|
732
|
-
mode = null;
|
|
733
|
-
selectedIndex = 0;
|
|
734
|
-
tokenIndex = 0;
|
|
735
|
-
selectedHarnesses = new Set();
|
|
736
|
-
appendLog("> auth: signed out");
|
|
737
|
-
}
|
|
738
|
-
catch (error) {
|
|
739
|
-
appendLog(`> auth: sign out failed: ${authError(error, "Sign out failed.")}`);
|
|
740
|
-
}
|
|
741
|
-
finally {
|
|
742
|
-
busy = false;
|
|
743
|
-
if (!closing)
|
|
744
|
-
draw();
|
|
745
|
-
}
|
|
746
|
-
};
|
|
747
|
-
const shutdown = () => {
|
|
748
|
-
if (closing)
|
|
749
|
-
return;
|
|
750
|
-
closing = true;
|
|
751
|
-
cancelLogin?.();
|
|
752
|
-
tokensRefresh?.abort();
|
|
753
|
-
if (noticeTimer) {
|
|
754
|
-
clearTimeout(noticeTimer);
|
|
755
|
-
noticeTimer = null;
|
|
756
|
-
}
|
|
757
|
-
if (slideTimer) {
|
|
758
|
-
clearInterval(slideTimer);
|
|
759
|
-
slideTimer = null;
|
|
760
|
-
}
|
|
761
|
-
clearInterval(tokensTimer);
|
|
762
|
-
clearInterval(refreshTimer);
|
|
763
|
-
removeContent(true);
|
|
764
|
-
if (backdrop) {
|
|
765
|
-
renderer.root.remove(backdrop.id);
|
|
766
|
-
backdrop.destroyRecursively();
|
|
767
|
-
}
|
|
768
|
-
renderer.destroy();
|
|
769
|
-
};
|
|
770
|
-
renderer.keyInput.on("keypress", (key) => {
|
|
771
|
-
if (key.defaultPrevented || key.propagationStopped)
|
|
772
|
-
return;
|
|
773
|
-
if (busy) {
|
|
774
|
-
if (cancelLogin && isCancel(key)) {
|
|
775
|
-
cancelLogin();
|
|
776
|
-
draw();
|
|
777
|
-
return;
|
|
778
|
-
}
|
|
779
|
-
if (isQuit(key)) {
|
|
780
|
-
void shutdown();
|
|
781
|
-
}
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
if (tokenLabelOpen) {
|
|
785
|
-
if (key.ctrl && key.name === "c") {
|
|
786
|
-
void shutdown();
|
|
787
|
-
return;
|
|
788
|
-
}
|
|
789
|
-
if (key.name === "escape") {
|
|
790
|
-
closeTokenLabel();
|
|
791
|
-
return;
|
|
792
|
-
}
|
|
793
|
-
if (isSelect(key)) {
|
|
794
|
-
submitTokenLabel();
|
|
795
|
-
return;
|
|
796
|
-
}
|
|
797
|
-
if (key.name === "backspace" || key.name === "delete") {
|
|
798
|
-
tokenLabel = tokenLabel.slice(0, -1);
|
|
799
|
-
draw();
|
|
800
|
-
return;
|
|
801
|
-
}
|
|
802
|
-
const char = inputChar(key);
|
|
803
|
-
if (char && tokenLabel.length < TOKEN_LABEL_MAX) {
|
|
804
|
-
tokenLabel = `${tokenLabel}${char}`;
|
|
805
|
-
draw();
|
|
806
|
-
}
|
|
807
|
-
return;
|
|
808
|
-
}
|
|
809
|
-
if (isQuit(key)) {
|
|
810
|
-
void shutdown();
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
if (key.name === "pageup") {
|
|
814
|
-
scrollLogs(5);
|
|
815
|
-
return;
|
|
816
|
-
}
|
|
817
|
-
if (key.name === "pagedown") {
|
|
818
|
-
scrollLogs(-5);
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
if (key.name === "home") {
|
|
822
|
-
scrollLogs(Number.POSITIVE_INFINITY);
|
|
823
|
-
return;
|
|
824
|
-
}
|
|
825
|
-
if (key.name === "end") {
|
|
826
|
-
scrollLogs(Number.NEGATIVE_INFINITY);
|
|
827
|
-
return;
|
|
828
|
-
}
|
|
829
|
-
if (busy)
|
|
830
|
-
return;
|
|
831
|
-
if (view === "local-setup") {
|
|
832
|
-
// The run is finished (busy is false here); Esc returns to the
|
|
833
|
-
// menu. Other keys are inert so a stray press can't navigate
|
|
834
|
-
// away from the result.
|
|
835
|
-
if (key.name === "escape") {
|
|
836
|
-
view = "menu";
|
|
837
|
-
menuIndex = firstEnabledMenuIndex(menuItems(config), menuIndex);
|
|
838
|
-
draw();
|
|
839
|
-
}
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
|
-
if (key.sequence === "m") {
|
|
843
|
-
showMenu();
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
|
-
if (view === "menu") {
|
|
847
|
-
const items = menuItems(config);
|
|
848
|
-
if (key.name === "escape") {
|
|
849
|
-
showPage(page);
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
if (isMoveBack(key)) {
|
|
853
|
-
selectMenu(previousEnabledMenuIndex(items, menuIndex));
|
|
854
|
-
return;
|
|
855
|
-
}
|
|
856
|
-
if (isMoveForward(key)) {
|
|
857
|
-
selectMenu(nextEnabledMenuIndex(items, menuIndex));
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
if (isSelect(key)) {
|
|
861
|
-
void chooseMenu(menuIndex);
|
|
862
|
-
}
|
|
863
|
-
return;
|
|
864
|
-
}
|
|
865
|
-
if (page === "welcome" && isSelect(key)) {
|
|
866
|
-
void signIn();
|
|
867
|
-
return;
|
|
868
|
-
}
|
|
869
|
-
if (page === "tokens") {
|
|
870
|
-
if (key.sequence === "c") {
|
|
871
|
-
openTokenLabel();
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
if (isMoveBack(key)) {
|
|
875
|
-
selectToken((tokenIndex + config.tokens.items.length - 1) %
|
|
876
|
-
Math.max(1, config.tokens.items.length));
|
|
877
|
-
return;
|
|
878
|
-
}
|
|
879
|
-
if (isMoveForward(key)) {
|
|
880
|
-
selectToken((tokenIndex + 1) % Math.max(1, config.tokens.items.length));
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
if (isSelect(key)) {
|
|
884
|
-
if (config.tokens.items.length === 0) {
|
|
885
|
-
openTokenLabel();
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
void chooseToken(tokenIndex);
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
if (page === "setup") {
|
|
894
|
-
if (isMoveBack(key)) {
|
|
895
|
-
selectHarness((harnessIndex + HARNESS_CARDS.length - 1) %
|
|
896
|
-
HARNESS_CARDS.length);
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
if (isMoveForward(key)) {
|
|
900
|
-
selectHarness((harnessIndex + 1) % HARNESS_CARDS.length);
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
if (key.name === "space" || key.sequence === " ") {
|
|
904
|
-
toggleHarness(harnessIndex);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
|
-
if (isSelect(key)) {
|
|
908
|
-
void configureHarnesses();
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
return;
|
|
912
|
-
}
|
|
913
|
-
if (page !== "location")
|
|
914
|
-
return;
|
|
915
|
-
if (isMoveBack(key)) {
|
|
916
|
-
select((selectedIndex + HOME_CARDS.length - 1) % HOME_CARDS.length);
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (isMoveForward(key)) {
|
|
920
|
-
select((selectedIndex + 1) % HOME_CARDS.length);
|
|
921
|
-
return;
|
|
922
|
-
}
|
|
923
|
-
if (isSelect(key)) {
|
|
924
|
-
void chooseLocation(selectedIndex);
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
});
|
|
928
|
-
renderer.on(core.CliRenderEvents.RESIZE, () => {
|
|
929
|
-
if (closing)
|
|
930
|
-
return;
|
|
931
|
-
draw();
|
|
932
|
-
});
|
|
933
|
-
const tokensTimer = setInterval(tickTokens, 100);
|
|
934
|
-
const refreshTimer = setInterval(() => void refreshTokens(), 60_000);
|
|
935
|
-
void refreshTokens();
|
|
936
|
-
draw();
|
|
937
|
-
function appendLog(line) {
|
|
938
|
-
logs = [...logs, line].slice(-200);
|
|
939
|
-
if (closing)
|
|
940
|
-
return;
|
|
941
|
-
draw();
|
|
942
|
-
}
|
|
943
|
-
async function copyLink(url) {
|
|
944
|
-
const ok = await copyText(url);
|
|
945
|
-
if (closing)
|
|
946
|
-
return;
|
|
947
|
-
showNotice(ok ? "link copied to clipboard" : "link copy failed");
|
|
948
|
-
}
|
|
949
|
-
function showNotice(value) {
|
|
950
|
-
if (noticeTimer)
|
|
951
|
-
clearTimeout(noticeTimer);
|
|
952
|
-
notice = value;
|
|
953
|
-
draw();
|
|
954
|
-
noticeTimer = setTimeout(() => {
|
|
955
|
-
notice = null;
|
|
956
|
-
noticeTimer = null;
|
|
957
|
-
if (!closing)
|
|
958
|
-
draw();
|
|
959
|
-
}, 1400);
|
|
960
|
-
}
|
|
961
|
-
function showPage(next) {
|
|
962
|
-
view = "wizard";
|
|
963
|
-
page = next;
|
|
964
|
-
animateStage(stageFor(next));
|
|
965
|
-
}
|
|
966
|
-
function scrollFlow(delta) {
|
|
967
|
-
if (busy || delta === 0)
|
|
968
|
-
return;
|
|
969
|
-
const current = stageFor(page);
|
|
970
|
-
const next = clamp(current + delta, minStage(), maxStage());
|
|
971
|
-
if (next === current)
|
|
972
|
-
return;
|
|
973
|
-
showPage(pageForStage(next));
|
|
974
|
-
}
|
|
975
|
-
function minStage() {
|
|
976
|
-
if (!isAuthed(config))
|
|
977
|
-
return 0;
|
|
978
|
-
return 1;
|
|
979
|
-
}
|
|
980
|
-
function maxStage() {
|
|
981
|
-
if (!isAuthed(config))
|
|
982
|
-
return 0;
|
|
983
|
-
if (!config.location.mode)
|
|
984
|
-
return 1;
|
|
985
|
-
if (!config.tokens.defaultId)
|
|
986
|
-
return 2;
|
|
987
|
-
return 3;
|
|
988
|
-
}
|
|
989
|
-
function animateStage(target) {
|
|
990
|
-
if (slideTimer) {
|
|
991
|
-
clearInterval(slideTimer);
|
|
992
|
-
slideTimer = null;
|
|
993
|
-
}
|
|
994
|
-
const start = stage;
|
|
995
|
-
const started = Date.now();
|
|
996
|
-
if (start === target) {
|
|
997
|
-
draw();
|
|
998
|
-
return;
|
|
999
|
-
}
|
|
1000
|
-
slideTimer = setInterval(() => {
|
|
1001
|
-
const elapsed = Date.now() - started;
|
|
1002
|
-
const time = Math.min(1, elapsed / SLIDE_MS);
|
|
1003
|
-
stage = start + (target - start) * ease(time);
|
|
1004
|
-
if (time < 1) {
|
|
1005
|
-
draw();
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
stage = target;
|
|
1009
|
-
if (slideTimer)
|
|
1010
|
-
clearInterval(slideTimer);
|
|
1011
|
-
slideTimer = null;
|
|
1012
|
-
draw();
|
|
1013
|
-
}, SLIDE_FRAME_MS);
|
|
1014
|
-
}
|
|
1015
|
-
function scrollLogs(delta) {
|
|
1016
|
-
if (delta === 0)
|
|
1017
|
-
return;
|
|
1018
|
-
const rows = renderer.terminalHeight || renderer.height;
|
|
1019
|
-
const height = terminalLogHeight(rows);
|
|
1020
|
-
const max = Math.max(0, logs.length - height);
|
|
1021
|
-
logOffset = Math.min(Math.max(logOffset + delta, 0), max);
|
|
1022
|
-
draw();
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
function TokensSaved(core, tokensSaved) {
|
|
1026
|
-
return core.Box({
|
|
1027
|
-
position: "absolute",
|
|
1028
|
-
top: 0,
|
|
1029
|
-
left: 0,
|
|
1030
|
-
height: 1,
|
|
1031
|
-
flexDirection: "row",
|
|
1032
|
-
}, core.Box({
|
|
1033
|
-
height: 1,
|
|
1034
|
-
paddingLeft: 1,
|
|
1035
|
-
paddingRight: 1,
|
|
1036
|
-
backgroundColor: theme.color.accentDim,
|
|
1037
|
-
}, core.Text({
|
|
1038
|
-
content: tokensSaved,
|
|
1039
|
-
fg: theme.color.accent,
|
|
1040
|
-
attributes: core.TextAttributes.BOLD,
|
|
1041
|
-
})), core.Box({
|
|
1042
|
-
height: 1,
|
|
1043
|
-
paddingLeft: 1,
|
|
1044
|
-
paddingRight: 1,
|
|
1045
|
-
backgroundColor: theme.color.accent,
|
|
1046
|
-
}, core.Text({
|
|
1047
|
-
content: "TOKENS SAVED",
|
|
1048
|
-
fg: theme.color.app,
|
|
1049
|
-
bg: theme.color.accent,
|
|
1050
|
-
attributes: core.TextAttributes.NONE,
|
|
1051
|
-
})));
|
|
1052
|
-
}
|
|
1053
|
-
function AppControls(core, props) {
|
|
1054
|
-
return core.Box({
|
|
1055
|
-
position: "absolute",
|
|
1056
|
-
top: 0,
|
|
1057
|
-
right: 0,
|
|
1058
|
-
height: 1,
|
|
1059
|
-
flexDirection: "row",
|
|
1060
|
-
backgroundColor: theme.color.app,
|
|
1061
|
-
}, props.mode ? ModeBadge(core, props.mode, props.onModeClick) : null, MenuButton(core, props.onMenuClick));
|
|
1062
|
-
}
|
|
1063
|
-
function TokenLabelPrompt(core, props) {
|
|
1064
|
-
const width = Math.min(46, Math.max(28, props.columns - 6));
|
|
1065
|
-
const left = Math.max(0, Math.floor((props.columns - width) / 2));
|
|
1066
|
-
const top = Math.max(1, Math.floor((props.rows - 9) / 2));
|
|
1067
|
-
const input = props.value ? props.value : "type a name";
|
|
1068
|
-
return core.Box({
|
|
1069
|
-
position: "absolute",
|
|
1070
|
-
top,
|
|
1071
|
-
left,
|
|
1072
|
-
width,
|
|
1073
|
-
flexDirection: "column",
|
|
1074
|
-
backgroundColor: theme.color.card,
|
|
1075
|
-
paddingLeft: 2,
|
|
1076
|
-
paddingRight: 2,
|
|
1077
|
-
paddingTop: 1,
|
|
1078
|
-
paddingBottom: 1,
|
|
1079
|
-
gap: 1,
|
|
1080
|
-
}, core.Text({
|
|
1081
|
-
content: "Name this Sleev token",
|
|
1082
|
-
fg: theme.color.text,
|
|
1083
|
-
attributes: core.TextAttributes.BOLD,
|
|
1084
|
-
}), core.Text({
|
|
1085
|
-
content: "This label is sent to the browser handoff.",
|
|
1086
|
-
fg: theme.color.muted,
|
|
1087
|
-
}), core.Box({
|
|
1088
|
-
height: 3,
|
|
1089
|
-
paddingLeft: 1,
|
|
1090
|
-
paddingRight: 1,
|
|
1091
|
-
alignItems: "center",
|
|
1092
|
-
backgroundColor: theme.color.footer,
|
|
1093
|
-
}, core.Text({
|
|
1094
|
-
content: input.slice(-Math.max(1, width - 6)),
|
|
1095
|
-
fg: props.value ? theme.color.text : theme.color.quiet,
|
|
1096
|
-
attributes: core.TextAttributes.BOLD,
|
|
1097
|
-
})), core.Text({
|
|
1098
|
-
content: `${props.value.length}/${props.max} Enter creates, Esc cancels`,
|
|
1099
|
-
fg: theme.color.quiet,
|
|
1100
|
-
}), core.Box({
|
|
1101
|
-
flexDirection: "row",
|
|
1102
|
-
gap: 1,
|
|
1103
|
-
}, PromptButton(core, {
|
|
1104
|
-
label: "Create",
|
|
1105
|
-
active: props.value.trim().length > 0,
|
|
1106
|
-
onClick: props.onSubmit,
|
|
1107
|
-
}), PromptButton(core, {
|
|
1108
|
-
label: "Cancel",
|
|
1109
|
-
active: true,
|
|
1110
|
-
onClick: props.onCancel,
|
|
1111
|
-
})));
|
|
1112
|
-
}
|
|
1113
|
-
function PromptButton(core, props) {
|
|
1114
|
-
return core.Box({
|
|
1115
|
-
height: 3,
|
|
1116
|
-
paddingLeft: 2,
|
|
1117
|
-
paddingRight: 2,
|
|
1118
|
-
alignItems: "center",
|
|
1119
|
-
justifyContent: "center",
|
|
1120
|
-
backgroundColor: props.active
|
|
1121
|
-
? theme.color.cardActive
|
|
1122
|
-
: theme.color.accentDim,
|
|
1123
|
-
onMouseDown: (event) => {
|
|
1124
|
-
event.preventDefault();
|
|
1125
|
-
event.stopPropagation();
|
|
1126
|
-
if (props.active)
|
|
1127
|
-
props.onClick();
|
|
1128
|
-
},
|
|
1129
|
-
}, core.Text({
|
|
1130
|
-
content: props.label,
|
|
1131
|
-
fg: props.active ? theme.color.text : theme.color.quiet,
|
|
1132
|
-
attributes: core.TextAttributes.BOLD,
|
|
1133
|
-
}));
|
|
1134
|
-
}
|
|
1135
|
-
function ModeBadge(core, mode, onClick) {
|
|
1136
|
-
return core.Box({
|
|
1137
|
-
height: 1,
|
|
1138
|
-
paddingLeft: 1,
|
|
1139
|
-
paddingRight: 1,
|
|
1140
|
-
backgroundColor: theme.color.accent,
|
|
1141
|
-
onMouseDown: (event) => {
|
|
1142
|
-
event.preventDefault();
|
|
1143
|
-
event.stopPropagation();
|
|
1144
|
-
onClick();
|
|
1145
|
-
},
|
|
1146
|
-
}, core.Text({
|
|
1147
|
-
content: mode.toUpperCase(),
|
|
1148
|
-
fg: theme.color.app,
|
|
1149
|
-
attributes: core.TextAttributes.BOLD,
|
|
1150
|
-
}));
|
|
1151
|
-
}
|
|
1152
|
-
function MenuButton(core, onClick) {
|
|
1153
|
-
return core.Box({
|
|
1154
|
-
height: 1,
|
|
1155
|
-
paddingLeft: 1,
|
|
1156
|
-
paddingRight: 1,
|
|
1157
|
-
backgroundColor: theme.color.accentDim,
|
|
1158
|
-
onMouseDown: (event) => {
|
|
1159
|
-
event.preventDefault();
|
|
1160
|
-
event.stopPropagation();
|
|
1161
|
-
onClick();
|
|
1162
|
-
},
|
|
1163
|
-
}, core.Text({
|
|
1164
|
-
content: "MENU",
|
|
1165
|
-
fg: theme.color.text,
|
|
1166
|
-
attributes: core.TextAttributes.BOLD,
|
|
1167
|
-
}));
|
|
1168
|
-
}
|
|
1169
|
-
function initialLogs(config) {
|
|
1170
|
-
const auth = config.auth.signedIn
|
|
1171
|
-
? `signed in${config.auth.email ? ` as ${config.auth.email}` : ""}`
|
|
1172
|
-
: "signed out";
|
|
1173
|
-
return ["> Welcome to sleev TUI", `> auth: ${auth}`];
|
|
1174
|
-
}
|
|
1175
|
-
function menuItems(config) {
|
|
1176
|
-
const authed = isAuthed(config);
|
|
1177
|
-
return [
|
|
1178
|
-
{
|
|
1179
|
-
action: "local-setup",
|
|
1180
|
-
label: "Setup local installation",
|
|
1181
|
-
detail: "Install + run the local gateway and register it with control.",
|
|
1182
|
-
enabled: authed,
|
|
1183
|
-
},
|
|
1184
|
-
{
|
|
1185
|
-
action: "auth",
|
|
1186
|
-
label: authed ? "Sign out" : "Sign in",
|
|
1187
|
-
detail: authed
|
|
1188
|
-
? "Remove auth state and unpatch configured harnesses."
|
|
1189
|
-
: "Connect this machine to your Sleeve account.",
|
|
1190
|
-
enabled: true,
|
|
1191
|
-
},
|
|
1192
|
-
];
|
|
1193
|
-
}
|
|
1194
|
-
function firstEnabledMenuIndex(items, fallback) {
|
|
1195
|
-
if (items[fallback]?.enabled)
|
|
1196
|
-
return fallback;
|
|
1197
|
-
const first = items.findIndex((item) => item.enabled);
|
|
1198
|
-
return first >= 0 ? first : 0;
|
|
1199
|
-
}
|
|
1200
|
-
function previousEnabledMenuIndex(items, current) {
|
|
1201
|
-
for (let offset = 1; offset <= items.length; offset += 1) {
|
|
1202
|
-
const index = (current - offset + items.length) % items.length;
|
|
1203
|
-
if (items[index]?.enabled)
|
|
1204
|
-
return index;
|
|
1205
|
-
}
|
|
1206
|
-
return current;
|
|
1207
|
-
}
|
|
1208
|
-
function nextEnabledMenuIndex(items, current) {
|
|
1209
|
-
for (let offset = 1; offset <= items.length; offset += 1) {
|
|
1210
|
-
const index = (current + offset) % items.length;
|
|
1211
|
-
if (items[index]?.enabled)
|
|
1212
|
-
return index;
|
|
1213
|
-
}
|
|
1214
|
-
return current;
|
|
1215
|
-
}
|
|
1216
|
-
function firstPage(config) {
|
|
1217
|
-
if (!isAuthed(config))
|
|
1218
|
-
return "welcome";
|
|
1219
|
-
if (!config.location.mode)
|
|
1220
|
-
return "location";
|
|
1221
|
-
if (!config.tokens.defaultId)
|
|
1222
|
-
return "tokens";
|
|
1223
|
-
return "setup";
|
|
1224
|
-
}
|
|
1225
|
-
function harnessCards(config, selected) {
|
|
1226
|
-
const token = defaultToken(config);
|
|
1227
|
-
const tokenLabel = token?.label ? truncate(token.label, 14) : null;
|
|
1228
|
-
return HARNESS_CARDS.map((harness) => ({
|
|
1229
|
-
...harness,
|
|
1230
|
-
tokenLabel,
|
|
1231
|
-
configured: config.harnesses[harness.id]?.configured === true,
|
|
1232
|
-
selected: selected.has(harness.id),
|
|
1233
|
-
}));
|
|
1234
|
-
}
|
|
1235
|
-
function defaultToken(config) {
|
|
1236
|
-
return (config.tokens.items.find((token) => token.id === config.tokens.defaultId) ?? null);
|
|
1237
|
-
}
|
|
1238
|
-
function authForToken(token) {
|
|
1239
|
-
return {
|
|
1240
|
-
token: token.token,
|
|
1241
|
-
label: token.label,
|
|
1242
|
-
proxyUrl: token.proxyUrl,
|
|
1243
|
-
createdAt: token.createdAt,
|
|
1244
|
-
};
|
|
1245
|
-
}
|
|
1246
|
-
// Harness auth that points at the locally running daemon instead of the
|
|
1247
|
-
// Sleeve cloud. Returns null unless we're in local mode with a registered
|
|
1248
|
-
// installation. The daemon runs in passthrough, so the credential is only
|
|
1249
|
-
// for traceability (the gateway strips sleeve-* before upstream).
|
|
1250
|
-
function localHarnessAuth(config) {
|
|
1251
|
-
if (config.location.mode !== "local")
|
|
1252
|
-
return null;
|
|
1253
|
-
const installation = config.localInstallation;
|
|
1254
|
-
if (!installation)
|
|
1255
|
-
return null;
|
|
1256
|
-
return {
|
|
1257
|
-
token: installation.credential,
|
|
1258
|
-
label: "local installation",
|
|
1259
|
-
proxyUrl: `http://${config.proxy.host}:${String(config.proxy.port)}`,
|
|
1260
|
-
createdAt: new Date().toISOString(),
|
|
1261
|
-
};
|
|
1262
|
-
}
|
|
1263
|
-
function labelHarness(id) {
|
|
1264
|
-
return HARNESS_CARDS.find((harness) => harness.id === id)?.label ?? id;
|
|
1265
|
-
}
|
|
1266
|
-
function selectedIndexFor(mode) {
|
|
1267
|
-
if (!mode)
|
|
1268
|
-
return 0;
|
|
1269
|
-
return Math.max(0, HOME_CARDS.findIndex((card) => card.action === mode));
|
|
1270
|
-
}
|
|
1271
|
-
function stageFor(page) {
|
|
1272
|
-
if (page === "welcome")
|
|
1273
|
-
return 0;
|
|
1274
|
-
if (page === "location")
|
|
1275
|
-
return 1;
|
|
1276
|
-
if (page === "tokens")
|
|
1277
|
-
return 2;
|
|
1278
|
-
return 3;
|
|
1279
|
-
}
|
|
1280
|
-
function pageForStage(stage) {
|
|
1281
|
-
if (stage === 0)
|
|
1282
|
-
return "welcome";
|
|
1283
|
-
if (stage === 1)
|
|
1284
|
-
return "location";
|
|
1285
|
-
if (stage === 2)
|
|
1286
|
-
return "tokens";
|
|
1287
|
-
return "setup";
|
|
1288
|
-
}
|
|
1289
|
-
function clamp(value, min, max) {
|
|
1290
|
-
return Math.min(Math.max(value, min), max);
|
|
1291
|
-
}
|
|
1292
|
-
function ease(time) {
|
|
1293
|
-
return 1 - (1 - time) ** 3;
|
|
1294
|
-
}
|
|
1295
|
-
function renderSavedTokens(tokens) {
|
|
1296
|
-
if (tokens === null)
|
|
1297
|
-
return "--";
|
|
1298
|
-
return formatSavedTokens(Math.floor(projectSavedTokens(tokens)));
|
|
1299
|
-
}
|
|
1300
|
-
function background(core, renderer, columns, rows) {
|
|
1301
|
-
const canvas = new core.FrameBufferRenderable(renderer, {
|
|
1302
|
-
id: "flow-gradient",
|
|
1303
|
-
width: columns,
|
|
1304
|
-
height: rows,
|
|
1305
|
-
position: "absolute",
|
|
1306
|
-
left: 0,
|
|
1307
|
-
top: 0,
|
|
1308
|
-
zIndex: 0,
|
|
1309
|
-
});
|
|
1310
|
-
paintGradient(core, canvas, columns, rows);
|
|
1311
|
-
return canvas;
|
|
1312
|
-
}
|
|
1313
|
-
function paintGradient(core, canvas, columns, rows) {
|
|
1314
|
-
const top = hex(theme.gradient.top);
|
|
1315
|
-
const right = hex(theme.gradient.right);
|
|
1316
|
-
const bottom = hex(theme.gradient.bottom);
|
|
1317
|
-
const glow = hex(theme.gradient.glow);
|
|
1318
|
-
const safeColumns = Math.max(1, columns - 1);
|
|
1319
|
-
const safeRows = Math.max(1, rows * 2 - 1);
|
|
1320
|
-
for (let y = 0; y < rows; y += 1) {
|
|
1321
|
-
const topY = (y * 2) / safeRows;
|
|
1322
|
-
const bottomY = (y * 2 + 1) / safeRows;
|
|
1323
|
-
for (let x = 0; x < columns; x += 1) {
|
|
1324
|
-
const dx = x / safeColumns;
|
|
1325
|
-
const upper = sampleGradient(top, right, bottom, glow, dx, topY);
|
|
1326
|
-
const lower = sampleGradient(top, right, bottom, glow, dx, bottomY);
|
|
1327
|
-
const fg = core.RGBA.fromValues(upper[0], upper[1], upper[2], 1);
|
|
1328
|
-
const bg = core.RGBA.fromValues(lower[0], lower[1], lower[2], 1);
|
|
1329
|
-
canvas.frameBuffer.setCell(x, y, "▀", fg, bg);
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
function sampleGradient(top, right, bottom, glow, dx, dy) {
|
|
1334
|
-
const vertical = clamp(dy, 0, 1);
|
|
1335
|
-
const horizontal = clamp(dx, 0, 1);
|
|
1336
|
-
const diagonal = Math.abs(horizontal - 0.5) * (1 - vertical * 0.35) * 0.4;
|
|
1337
|
-
const center = Math.max(0, 1 - Math.hypot(horizontal - 0.5, vertical - 0.5) * 1.45) *
|
|
1338
|
-
0.36;
|
|
1339
|
-
const base = [
|
|
1340
|
-
mix(top[0], bottom[0], vertical),
|
|
1341
|
-
mix(top[1], bottom[1], vertical),
|
|
1342
|
-
mix(top[2], bottom[2], vertical),
|
|
1343
|
-
];
|
|
1344
|
-
const side = [
|
|
1345
|
-
channel(base[0], right[0], diagonal),
|
|
1346
|
-
channel(base[1], right[1], diagonal),
|
|
1347
|
-
channel(base[2], right[2], diagonal),
|
|
1348
|
-
];
|
|
1349
|
-
return [
|
|
1350
|
-
channel(side[0], glow[0], center),
|
|
1351
|
-
channel(side[1], glow[1], center),
|
|
1352
|
-
channel(side[2], glow[2], center),
|
|
1353
|
-
];
|
|
1354
|
-
}
|
|
1355
|
-
function hex(color) {
|
|
1356
|
-
return [
|
|
1357
|
-
Number.parseInt(color.slice(1, 3), 16) / 255,
|
|
1358
|
-
Number.parseInt(color.slice(3, 5), 16) / 255,
|
|
1359
|
-
Number.parseInt(color.slice(5, 7), 16) / 255,
|
|
1360
|
-
];
|
|
1361
|
-
}
|
|
1362
|
-
function mix(start, end, amount) {
|
|
1363
|
-
return start + (end - start) * amount;
|
|
1364
|
-
}
|
|
1365
|
-
function channel(base, glow, amount) {
|
|
1366
|
-
return clamp(mix(base, glow, amount), 0, 1);
|
|
1367
|
-
}
|
|
1368
|
-
function isAuthed(config) {
|
|
1369
|
-
return config.auth.signedIn && typeof config.auth.accountId === "string";
|
|
1370
|
-
}
|
|
1371
|
-
function truncate(value, length) {
|
|
1372
|
-
return value.length <= length ? value : value.slice(0, length);
|
|
1373
|
-
}
|
|
1374
|
-
function maskToken(token) {
|
|
1375
|
-
if (token.length <= 14)
|
|
1376
|
-
return "slv_live_...";
|
|
1377
|
-
return `${token.slice(0, 9)}...${token.slice(-4)}`;
|
|
1378
|
-
}
|
|
1379
|
-
function isBunRuntime() {
|
|
1380
|
-
return "Bun" in globalThis;
|
|
1381
|
-
}
|
|
1382
|
-
function isMoveBack(key) {
|
|
1383
|
-
return (key.name === "left" ||
|
|
1384
|
-
key.name === "up" ||
|
|
1385
|
-
key.sequence === "h" ||
|
|
1386
|
-
key.sequence === "k");
|
|
1387
|
-
}
|
|
1388
|
-
function isMoveForward(key) {
|
|
1389
|
-
return (key.name === "right" ||
|
|
1390
|
-
key.name === "down" ||
|
|
1391
|
-
key.sequence === "l" ||
|
|
1392
|
-
key.sequence === "j");
|
|
1393
|
-
}
|
|
1394
|
-
function isSelect(key) {
|
|
1395
|
-
return (key.name === "return" || key.name === "enter" || key.sequence === "\r");
|
|
1396
|
-
}
|
|
1397
|
-
function isQuit(key) {
|
|
1398
|
-
return key.sequence === "q" || (key.ctrl && key.name === "c");
|
|
1399
|
-
}
|
|
1400
|
-
function isCancel(key) {
|
|
1401
|
-
return key.name === "escape" || key.sequence === "q";
|
|
1402
|
-
}
|
|
1403
|
-
function inputChar(key) {
|
|
1404
|
-
if (key.ctrl || key.meta)
|
|
1405
|
-
return null;
|
|
1406
|
-
if (!key.sequence || key.sequence.length !== 1)
|
|
1407
|
-
return null;
|
|
1408
|
-
const code = key.sequence.charCodeAt(0);
|
|
1409
|
-
if (code < 32 || code === 127)
|
|
1410
|
-
return null;
|
|
1411
|
-
return key.sequence;
|
|
1412
|
-
}
|
|
1413
|
-
function cleanTokenLabel(value) {
|
|
1414
|
-
return value
|
|
1415
|
-
.trim()
|
|
1416
|
-
.split("")
|
|
1417
|
-
.filter((char) => {
|
|
1418
|
-
const code = char.charCodeAt(0);
|
|
1419
|
-
return code >= 32 && code !== 127;
|
|
1420
|
-
})
|
|
1421
|
-
.join("")
|
|
1422
|
-
.slice(0, TOKEN_LABEL_MAX);
|
|
1423
|
-
}
|
|
1424
|
-
function authError(error, fallback) {
|
|
1425
|
-
if (error instanceof Error && error.message)
|
|
1426
|
-
return error.message;
|
|
1427
|
-
return fallback;
|
|
1428
|
-
}
|
|
1429
|
-
function mutedOutput() {
|
|
1430
|
-
return new Writable({
|
|
1431
|
-
write(_chunk, _encoding, callback) {
|
|
1432
|
-
callback();
|
|
1433
|
-
},
|
|
1434
|
-
});
|
|
1435
|
-
}
|
|
1436
|
-
//# sourceMappingURL=openTuiApp.js.map
|