buildless-cli 0.1.9 → 0.1.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/dist/index.js +2353 -83
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/dist/ai.d.ts +0 -29
- package/dist/ai.d.ts.map +0 -1
- package/dist/ai.js +0 -148
- package/dist/ai.js.map +0 -1
- package/dist/app.d.ts +0 -9
- package/dist/app.d.ts.map +0 -1
- package/dist/app.js +0 -67
- package/dist/app.js.map +0 -1
- package/dist/auth.d.ts +0 -14
- package/dist/auth.d.ts.map +0 -1
- package/dist/auth.js +0 -100
- package/dist/auth.js.map +0 -1
- package/dist/config.d.ts +0 -14
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -45
- package/dist/config.js.map +0 -1
- package/dist/firestore.d.ts +0 -35
- package/dist/firestore.d.ts.map +0 -1
- package/dist/firestore.js +0 -340
- package/dist/firestore.js.map +0 -1
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/log.d.ts +0 -3
- package/dist/log.d.ts.map +0 -1
- package/dist/log.js +0 -10
- package/dist/log.js.map +0 -1
- package/dist/ui/app-search.d.ts +0 -11
- package/dist/ui/app-search.d.ts.map +0 -1
- package/dist/ui/app-search.js +0 -86
- package/dist/ui/app-search.js.map +0 -1
- package/dist/ui/runner.d.ts +0 -14
- package/dist/ui/runner.d.ts.map +0 -1
- package/dist/ui/runner.js +0 -546
- package/dist/ui/runner.js.map +0 -1
- package/dist/ui/useRunnerRenderSource.d.ts +0 -53
- package/dist/ui/useRunnerRenderSource.d.ts.map +0 -1
- package/dist/ui/useRunnerRenderSource.js +0 -69
- package/dist/ui/useRunnerRenderSource.js.map +0 -1
- package/dist/ui/useRunnerSession.d.ts +0 -16
- package/dist/ui/useRunnerSession.d.ts.map +0 -1
- package/dist/ui/useRunnerSession.js +0 -188
- package/dist/ui/useRunnerSession.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,94 +1,2364 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
// src/index.tsx
|
|
3
4
|
import { Command } from "commander";
|
|
4
5
|
import { render } from "ink";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
6
|
+
|
|
7
|
+
// src/app.tsx
|
|
8
|
+
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
9
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
10
|
+
|
|
11
|
+
// src/auth.ts
|
|
12
|
+
import { createServer } from "http";
|
|
13
|
+
import crypto from "crypto";
|
|
14
|
+
import { URL as URL2 } from "url";
|
|
15
|
+
import open from "open";
|
|
16
|
+
|
|
17
|
+
// src/log.ts
|
|
18
|
+
var verbose = process.env.BLO_DEBUG === "1" || process.env.BLO_DEBUG === "true";
|
|
19
|
+
var setVerbose = (enabled) => {
|
|
20
|
+
verbose = enabled;
|
|
21
|
+
};
|
|
22
|
+
var log = (message) => {
|
|
23
|
+
if (verbose) {
|
|
24
|
+
process.stderr.write(`[blo] ${message}
|
|
25
|
+
`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// src/auth.ts
|
|
30
|
+
var decodeJwtPayload = (token) => {
|
|
31
|
+
const parts = token.split(".");
|
|
32
|
+
if (parts.length < 2) return null;
|
|
33
|
+
try {
|
|
34
|
+
const payload = Buffer.from(parts[1], "base64url").toString("utf8");
|
|
35
|
+
return JSON.parse(payload);
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
var getTokenExp = (token) => {
|
|
41
|
+
const payload = decodeJwtPayload(token);
|
|
42
|
+
if (!payload || typeof payload.exp !== "number") return null;
|
|
43
|
+
return payload.exp;
|
|
44
|
+
};
|
|
45
|
+
var isTokenValid = (token, exp) => {
|
|
46
|
+
if (!token) return false;
|
|
47
|
+
if (exp == null) return true;
|
|
48
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
49
|
+
return exp - 30 > now;
|
|
50
|
+
};
|
|
51
|
+
var LOGIN_TIMEOUT_MS = 12e4;
|
|
52
|
+
var startExternalLogin = async (options) => {
|
|
53
|
+
const state = crypto.randomBytes(16).toString("hex");
|
|
54
|
+
const server = createServer();
|
|
55
|
+
const result = await new Promise((resolve, reject) => {
|
|
56
|
+
let settled = false;
|
|
57
|
+
const timeout = setTimeout(() => {
|
|
58
|
+
if (settled) return;
|
|
59
|
+
settled = true;
|
|
60
|
+
server.close();
|
|
61
|
+
reject(new Error("Login timed out after 2 minutes. Please try again."));
|
|
62
|
+
}, LOGIN_TIMEOUT_MS);
|
|
63
|
+
server.on("request", (req, res) => {
|
|
64
|
+
if (!req.url) return;
|
|
65
|
+
const url = new URL2(req.url, "http://localhost");
|
|
66
|
+
if (url.pathname !== "/callback") {
|
|
67
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
68
|
+
res.end("Not found");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (settled) {
|
|
72
|
+
res.writeHead(409, { "Content-Type": "text/plain" });
|
|
73
|
+
res.end("Login already handled. You can close this window.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const token = url.searchParams.get("token") || "";
|
|
77
|
+
const returnedState = url.searchParams.get("state") || "";
|
|
78
|
+
const appCheckToken = url.searchParams.get("appCheckToken") || void 0;
|
|
79
|
+
if (!token || returnedState !== state) {
|
|
80
|
+
settled = true;
|
|
81
|
+
res.writeHead(400, { "Content-Type": "text/plain" });
|
|
82
|
+
res.end("Invalid login response. You can close this window.");
|
|
83
|
+
clearTimeout(timeout);
|
|
84
|
+
log(`Login callback failed: token=${token ? "present" : "missing"}, state=${returnedState === state ? "match" : "mismatch"}`);
|
|
85
|
+
reject(new Error("Login failed: invalid callback. Make sure you completed the login in your browser."));
|
|
86
|
+
server.close();
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
settled = true;
|
|
90
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
91
|
+
res.end("Login complete. You can close this window.");
|
|
92
|
+
clearTimeout(timeout);
|
|
93
|
+
const exp = getTokenExp(token);
|
|
94
|
+
log(`Login callback received: exp=${exp}, appCheck=${appCheckToken ? "yes" : "no"}`);
|
|
95
|
+
resolve({ token, exp, appCheckToken });
|
|
96
|
+
server.close();
|
|
97
|
+
});
|
|
98
|
+
server.listen(0, "127.0.0.1", () => {
|
|
99
|
+
const address = server.address();
|
|
100
|
+
if (!address || typeof address === "string") {
|
|
101
|
+
clearTimeout(timeout);
|
|
102
|
+
reject(new Error("Failed to start local login server."));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const callbackUrl = `http://127.0.0.1:${address.port}/callback`;
|
|
106
|
+
const loginUrl = new URL2("/cli-auth", options.webOrigin);
|
|
107
|
+
loginUrl.searchParams.set("redirect_uri", callbackUrl);
|
|
108
|
+
loginUrl.searchParams.set("state", state);
|
|
109
|
+
log(`Login server listening on port ${address.port}`);
|
|
110
|
+
log(`Opening browser: ${loginUrl.toString()}`);
|
|
111
|
+
process.stderr.write("Opening browser for login...\n");
|
|
112
|
+
void open(loginUrl.toString());
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
return result;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// src/firestore.ts
|
|
119
|
+
var resolveUidFromToken = (idToken) => {
|
|
120
|
+
const payload = decodeJwtPayload(idToken);
|
|
121
|
+
if (!payload) return null;
|
|
122
|
+
const uid = payload.user_id ?? payload.sub;
|
|
123
|
+
return typeof uid === "string" && uid.trim() ? uid.trim() : null;
|
|
124
|
+
};
|
|
125
|
+
var fromFirestoreValue = (value) => {
|
|
126
|
+
if (value == null || typeof value !== "object") return null;
|
|
127
|
+
if ("stringValue" in value) return String(value.stringValue);
|
|
128
|
+
if ("booleanValue" in value) return Boolean(value.booleanValue);
|
|
129
|
+
if ("integerValue" in value) return Number(value.integerValue);
|
|
130
|
+
if ("doubleValue" in value) return Number(value.doubleValue);
|
|
131
|
+
if ("timestampValue" in value) return String(value.timestampValue);
|
|
132
|
+
if ("referenceValue" in value) return String(value.referenceValue);
|
|
133
|
+
if ("mapValue" in value) {
|
|
134
|
+
const fields = value.mapValue?.fields || {};
|
|
135
|
+
const output = {};
|
|
136
|
+
for (const [key, entry] of Object.entries(fields)) {
|
|
137
|
+
output[key] = fromFirestoreValue(entry);
|
|
138
|
+
}
|
|
139
|
+
return output;
|
|
140
|
+
}
|
|
141
|
+
if ("arrayValue" in value) {
|
|
142
|
+
const values = value.arrayValue?.values || [];
|
|
143
|
+
return values.map(fromFirestoreValue);
|
|
144
|
+
}
|
|
145
|
+
if ("nullValue" in value) return null;
|
|
146
|
+
return null;
|
|
147
|
+
};
|
|
148
|
+
var parseDocument = (doc) => {
|
|
149
|
+
const fields = doc?.fields || {};
|
|
150
|
+
const output = {};
|
|
151
|
+
for (const [key, entry] of Object.entries(fields)) {
|
|
152
|
+
output[key] = fromFirestoreValue(entry);
|
|
153
|
+
}
|
|
154
|
+
return output;
|
|
155
|
+
};
|
|
156
|
+
var firestoreDocsBase = (projectId) => `https://firestore.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/databases/(default)/documents`;
|
|
157
|
+
var parseRunQueryDocs = (rows) => rows.map((row) => row.document).filter(Boolean);
|
|
158
|
+
var extractDocIdFromName = (name) => {
|
|
159
|
+
if (!name) return void 0;
|
|
160
|
+
const chunks = String(name).split("/");
|
|
161
|
+
return chunks[chunks.length - 1] || void 0;
|
|
162
|
+
};
|
|
163
|
+
var authHeaders = (idToken, appCheckToken) => {
|
|
164
|
+
const headers = {
|
|
165
|
+
Authorization: `Bearer ${idToken}`
|
|
166
|
+
};
|
|
167
|
+
if (appCheckToken) {
|
|
168
|
+
headers["X-Firebase-AppCheck"] = appCheckToken;
|
|
169
|
+
}
|
|
170
|
+
return headers;
|
|
171
|
+
};
|
|
172
|
+
var fetchRunnableAppsViaServer = async (opts) => {
|
|
173
|
+
const { terminalUrl, idToken, query } = opts;
|
|
174
|
+
const url = new URL("/api/apps", terminalUrl);
|
|
175
|
+
if (query) url.searchParams.set("q", query);
|
|
176
|
+
log(`Fetching apps: GET ${url.toString()}`);
|
|
177
|
+
const response = await fetch(url.toString(), {
|
|
178
|
+
headers: { Authorization: `Bearer ${idToken}` }
|
|
179
|
+
});
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
let message;
|
|
182
|
+
try {
|
|
183
|
+
const body2 = await response.json();
|
|
184
|
+
message = body2.message;
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
log(`Fetch apps failed: ${response.status} ${message || "(no message)"}`);
|
|
188
|
+
if (response.status === 401) {
|
|
189
|
+
throw new Error("Authentication failed. Try 'blo login' to re-authenticate.");
|
|
190
|
+
}
|
|
191
|
+
if (response.status === 403 && message?.toLowerCase().includes("cli access")) {
|
|
192
|
+
const uid = resolveUidFromToken(idToken) || "unknown";
|
|
193
|
+
throw new Error(`CLI access is not enabled for your account (uid: ${uid}). Contact an admin to enable it.`);
|
|
194
|
+
}
|
|
195
|
+
if (response.status >= 500) {
|
|
196
|
+
throw new Error(`Server error (${response.status}). The terminal server may be having issues.`);
|
|
197
|
+
}
|
|
198
|
+
throw new Error(message || `Failed to fetch apps (${response.status})`);
|
|
199
|
+
}
|
|
200
|
+
const body = await response.json();
|
|
201
|
+
const apps = body.apps || [];
|
|
202
|
+
log(`Fetched ${apps.length} apps`);
|
|
203
|
+
return apps;
|
|
204
|
+
};
|
|
205
|
+
var filterApp = (app, term) => [
|
|
206
|
+
app.projectId,
|
|
207
|
+
app.displayName,
|
|
208
|
+
app.name,
|
|
209
|
+
app.tagline,
|
|
210
|
+
app.description,
|
|
211
|
+
app.authorName,
|
|
212
|
+
app.ownerDisplayName,
|
|
213
|
+
app.versionId,
|
|
214
|
+
app.accessTier
|
|
215
|
+
].filter(Boolean).some((field) => String(field).toLowerCase().includes(term));
|
|
216
|
+
|
|
217
|
+
// src/ui/app-search.tsx
|
|
218
|
+
import { useMemo, useState } from "react";
|
|
219
|
+
import { Box, Text, useInput } from "ink";
|
|
220
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
221
|
+
var AppSearch = ({ apps, initialQuery, onSelect, onExit }) => {
|
|
222
|
+
const [query, setQuery] = useState(initialQuery || "");
|
|
223
|
+
const [selected, setSelected] = useState(0);
|
|
224
|
+
const [appFilter, setAppFilter] = useState("all");
|
|
225
|
+
const filtered = useMemo(() => {
|
|
226
|
+
let result = apps;
|
|
227
|
+
if (appFilter === "user") {
|
|
228
|
+
result = result.filter((app) => !app.appSource || app.appSource === "user");
|
|
229
|
+
} else if (appFilter === "published") {
|
|
230
|
+
result = result.filter((app) => app.appSource === "published");
|
|
231
|
+
}
|
|
232
|
+
const term = query.trim().toLowerCase();
|
|
233
|
+
if (!term) return result;
|
|
234
|
+
return result.filter((app) => filterApp(app, term));
|
|
235
|
+
}, [apps, query, appFilter]);
|
|
236
|
+
useInput((input, key) => {
|
|
237
|
+
if (key.ctrl && input === "c") {
|
|
238
|
+
onExit();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (key.escape) {
|
|
242
|
+
setQuery("");
|
|
243
|
+
setSelected(0);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (input === "m" || input === "M") {
|
|
247
|
+
setAppFilter("user");
|
|
248
|
+
setSelected(0);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (input === "p" || input === "P") {
|
|
252
|
+
setAppFilter("published");
|
|
253
|
+
setSelected(0);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (input === "a" || input === "A") {
|
|
257
|
+
setAppFilter("all");
|
|
258
|
+
setSelected(0);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (key.upArrow) {
|
|
262
|
+
setSelected((prev) => filtered.length ? (prev - 1 + filtered.length) % filtered.length : 0);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if (key.downArrow) {
|
|
266
|
+
setSelected((prev) => filtered.length ? (prev + 1) % filtered.length : 0);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (key.return) {
|
|
270
|
+
const app = filtered[selected];
|
|
271
|
+
if (app) onSelect(app);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (key.backspace || key.delete) {
|
|
275
|
+
setQuery((prev) => prev.slice(0, -1));
|
|
276
|
+
setSelected(0);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (input) {
|
|
280
|
+
setQuery((prev) => prev + input);
|
|
281
|
+
setSelected(0);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
const maxVisible = Math.max(3, Math.min(20, (process.stdout.rows || 24) - 6));
|
|
285
|
+
const scrollOffset = Math.max(0, Math.min(selected - maxVisible + 1, filtered.length - maxVisible));
|
|
286
|
+
const visible = filtered.slice(scrollOffset, scrollOffset + maxVisible);
|
|
287
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
288
|
+
/* @__PURE__ */ jsx(Text, { color: "green", children: "BLO CLI Runner Apps" }),
|
|
289
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
290
|
+
"Search apps: ",
|
|
291
|
+
query || "(type to filter)"
|
|
292
|
+
] }),
|
|
293
|
+
/* @__PURE__ */ jsxs(Text, { children: [
|
|
294
|
+
"Filter:",
|
|
295
|
+
" ",
|
|
296
|
+
/* @__PURE__ */ jsx(Text, { color: appFilter === "all" ? "green" : "gray", children: "A" }),
|
|
297
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "ll | " }),
|
|
298
|
+
/* @__PURE__ */ jsx(Text, { color: appFilter === "user" ? "green" : "gray", children: "M" }),
|
|
299
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "y | " }),
|
|
300
|
+
/* @__PURE__ */ jsx(Text, { color: appFilter === "published" ? "green" : "gray", children: "P" }),
|
|
301
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "ublished" })
|
|
302
|
+
] }),
|
|
303
|
+
/* @__PURE__ */ jsx(Text, { color: "gray", children: "Enter = launch, Esc = clear, Ctrl+C = exit" }),
|
|
304
|
+
/* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginTop: 1, children: [
|
|
305
|
+
visible.length === 0 ? /* @__PURE__ */ jsx(Text, { children: "No runnable apps found." }) : visible.map((app, index) => {
|
|
306
|
+
const globalIndex = scrollOffset + index;
|
|
307
|
+
const isSelected = globalIndex === selected;
|
|
308
|
+
const label = app.displayName || app.name || app.projectId;
|
|
309
|
+
const author = app.authorName || app.ownerDisplayName || "Unknown";
|
|
310
|
+
const versionHint = `${app.versionSource}:${app.versionId}`;
|
|
311
|
+
return /* @__PURE__ */ jsxs(Text, { color: isSelected ? "green" : void 0, children: [
|
|
312
|
+
isSelected ? ">" : " ",
|
|
313
|
+
" ",
|
|
314
|
+
label,
|
|
315
|
+
" \u2014 ",
|
|
316
|
+
author,
|
|
317
|
+
" (",
|
|
318
|
+
versionHint,
|
|
319
|
+
")"
|
|
320
|
+
] }, app.id);
|
|
321
|
+
}),
|
|
322
|
+
filtered.length > maxVisible && /* @__PURE__ */ jsxs(Text, { color: "gray", children: [
|
|
323
|
+
scrollOffset + visible.length,
|
|
324
|
+
" of ",
|
|
325
|
+
filtered.length,
|
|
326
|
+
" apps"
|
|
327
|
+
] })
|
|
328
|
+
] })
|
|
329
|
+
] });
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/ui/runner.tsx
|
|
333
|
+
import { useEffect as useEffect2, useMemo as useMemo3, useRef as useRef2, useState as useState3 } from "react";
|
|
334
|
+
import { Box as Box2, Text as Text2, useInput as useInput2 } from "ink";
|
|
335
|
+
|
|
336
|
+
// src/ai.ts
|
|
337
|
+
async function callUnifiedChat(opts) {
|
|
338
|
+
const url = `https://us-central1-${opts.firebaseProjectId}.cloudfunctions.net/aiApi`;
|
|
339
|
+
const headers = {
|
|
340
|
+
"Content-Type": "application/json",
|
|
341
|
+
Authorization: `Bearer ${opts.idToken}`
|
|
342
|
+
};
|
|
343
|
+
if (opts.appCheckToken) {
|
|
344
|
+
headers["X-Firebase-AppCheck"] = opts.appCheckToken;
|
|
345
|
+
}
|
|
346
|
+
const res = await fetch(url, {
|
|
347
|
+
method: "POST",
|
|
348
|
+
headers,
|
|
349
|
+
body: JSON.stringify({ data: { action: "unifiedChat", payload: opts.payload } })
|
|
350
|
+
});
|
|
351
|
+
if (!res.ok) {
|
|
352
|
+
const text = await res.text();
|
|
353
|
+
throw new Error(`AI request failed (${res.status}): ${text}`);
|
|
354
|
+
}
|
|
355
|
+
const json = await res.json();
|
|
356
|
+
const data = json.result?.data;
|
|
357
|
+
if (!data) throw new Error("Unexpected AI response format");
|
|
358
|
+
return data;
|
|
359
|
+
}
|
|
360
|
+
async function loadLatestConversation(opts) {
|
|
361
|
+
const base = firestoreDocsBase(opts.firebaseProjectId);
|
|
362
|
+
const parent = `projects/${opts.projectId}/versions/${opts.versionId}`;
|
|
363
|
+
const convoRes = await fetch(`${base}/${parent}:runQuery`, {
|
|
364
|
+
method: "POST",
|
|
365
|
+
headers: { "Content-Type": "application/json", ...authHeaders(opts.idToken, opts.appCheckToken) },
|
|
366
|
+
body: JSON.stringify({
|
|
367
|
+
structuredQuery: {
|
|
368
|
+
from: [{ collectionId: "ai_conversations" }],
|
|
369
|
+
limit: 50
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
});
|
|
373
|
+
if (!convoRes.ok) return null;
|
|
374
|
+
const convoRows = parseRunQueryDocs(await convoRes.json());
|
|
375
|
+
if (!convoRows.length) return null;
|
|
376
|
+
let bestDoc = convoRows[0];
|
|
377
|
+
let bestTime = 0;
|
|
378
|
+
for (const doc of convoRows) {
|
|
379
|
+
const data = parseDocument(doc);
|
|
380
|
+
const t = Math.max(toMs(data.updatedAt), toMs(data.createdAt));
|
|
381
|
+
if (t > bestTime || bestTime === 0) {
|
|
382
|
+
bestTime = t;
|
|
383
|
+
bestDoc = doc;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const conversationId = extractDocIdFromName(bestDoc?.name);
|
|
387
|
+
if (!conversationId) return null;
|
|
388
|
+
const msgRes = await fetch(
|
|
389
|
+
`${base}/${parent}/ai_conversations/${encodeURIComponent(conversationId)}:runQuery`,
|
|
390
|
+
{
|
|
391
|
+
method: "POST",
|
|
392
|
+
headers: { "Content-Type": "application/json", ...authHeaders(opts.idToken, opts.appCheckToken) },
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
structuredQuery: {
|
|
395
|
+
from: [{ collectionId: "messages" }],
|
|
396
|
+
orderBy: [{ field: { fieldPath: "seq" }, direction: "ASCENDING" }],
|
|
397
|
+
limit: 200
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
);
|
|
402
|
+
if (!msgRes.ok) return { conversationId, messages: [], messageCount: 0 };
|
|
403
|
+
const msgDocs = parseRunQueryDocs(await msgRes.json());
|
|
404
|
+
const messages = msgDocs.map((doc) => {
|
|
405
|
+
const d = parseDocument(doc);
|
|
406
|
+
return { role: d.role, text: d.text, toolName: d.toolName || void 0 };
|
|
407
|
+
});
|
|
408
|
+
return { conversationId, messages, messageCount: messages.length };
|
|
409
|
+
}
|
|
410
|
+
async function createConversation(opts) {
|
|
411
|
+
const base = firestoreDocsBase(opts.firebaseProjectId);
|
|
412
|
+
const colPath = `projects/${opts.projectId}/versions/${opts.versionId}/ai_conversations`;
|
|
413
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
414
|
+
const res = await fetch(`${base}/${colPath}`, {
|
|
415
|
+
method: "POST",
|
|
416
|
+
headers: { "Content-Type": "application/json", ...authHeaders(opts.idToken, opts.appCheckToken) },
|
|
417
|
+
body: JSON.stringify({
|
|
418
|
+
fields: {
|
|
419
|
+
projectId: { stringValue: opts.projectId },
|
|
420
|
+
versionId: { stringValue: opts.versionId },
|
|
421
|
+
createdAt: { timestampValue: now },
|
|
422
|
+
updatedAt: { timestampValue: now }
|
|
423
|
+
}
|
|
424
|
+
})
|
|
425
|
+
});
|
|
426
|
+
if (!res.ok) {
|
|
427
|
+
const text = await res.text();
|
|
428
|
+
throw new Error(`Failed to create conversation (${res.status}): ${text}`);
|
|
429
|
+
}
|
|
430
|
+
const doc = await res.json();
|
|
431
|
+
const id = extractDocIdFromName(doc.name);
|
|
432
|
+
if (!id) throw new Error("Failed to extract conversation ID");
|
|
433
|
+
return id;
|
|
434
|
+
}
|
|
435
|
+
async function appendMessage(opts) {
|
|
436
|
+
const base = firestoreDocsBase(opts.firebaseProjectId);
|
|
437
|
+
const msgPath = `projects/${opts.projectId}/versions/${opts.versionId}/ai_conversations/${opts.conversationId}/messages`;
|
|
438
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
439
|
+
const fields = {
|
|
440
|
+
role: { stringValue: opts.role },
|
|
441
|
+
text: { stringValue: opts.text },
|
|
442
|
+
seq: { integerValue: String(opts.seq) },
|
|
443
|
+
createdAt: { timestampValue: now }
|
|
444
|
+
};
|
|
445
|
+
if (opts.toolName) {
|
|
446
|
+
fields.toolName = { stringValue: opts.toolName };
|
|
447
|
+
}
|
|
448
|
+
const msgRes = await fetch(`${base}/${msgPath}`, {
|
|
449
|
+
method: "POST",
|
|
450
|
+
headers: { "Content-Type": "application/json", ...authHeaders(opts.idToken, opts.appCheckToken) },
|
|
451
|
+
body: JSON.stringify({ fields })
|
|
452
|
+
});
|
|
453
|
+
if (!msgRes.ok) return;
|
|
454
|
+
const convoPath = `projects/${opts.projectId}/versions/${opts.versionId}/ai_conversations/${opts.conversationId}`;
|
|
455
|
+
await fetch(`${base}/${convoPath}?updateMask.fieldPaths=updatedAt`, {
|
|
456
|
+
method: "PATCH",
|
|
457
|
+
headers: { "Content-Type": "application/json", ...authHeaders(opts.idToken, opts.appCheckToken) },
|
|
458
|
+
body: JSON.stringify({
|
|
459
|
+
fields: { updatedAt: { timestampValue: now } }
|
|
460
|
+
})
|
|
461
|
+
}).catch(() => {
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
function toMs(value) {
|
|
465
|
+
if (!value) return 0;
|
|
466
|
+
if (typeof value === "number") return value;
|
|
467
|
+
if (typeof value === "string") {
|
|
468
|
+
const parsed = Date.parse(value);
|
|
469
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
470
|
+
}
|
|
471
|
+
return 0;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// src/ui/useRunnerSession.ts
|
|
475
|
+
import { useEffect, useRef, useState as useState2 } from "react";
|
|
476
|
+
import WebSocket from "ws";
|
|
477
|
+
var toWsBaseUrl = (httpBase) => {
|
|
478
|
+
if (httpBase.startsWith("https://")) return `wss://${httpBase.slice("https://".length)}`;
|
|
479
|
+
if (httpBase.startsWith("http://")) return `ws://${httpBase.slice("http://".length)}`;
|
|
480
|
+
return httpBase;
|
|
481
|
+
};
|
|
482
|
+
var getTerminalSize = () => {
|
|
483
|
+
const cols = Math.max(3, Math.min(300, Number(process.stdout.columns || 80)));
|
|
484
|
+
const rows = Math.max(3, Math.min(200, Number(process.stdout.rows || 24)));
|
|
485
|
+
return { rows, cols };
|
|
486
|
+
};
|
|
487
|
+
var useRunnerSession = ({ projectId, versionId, terminalUrl, idToken }) => {
|
|
488
|
+
const [screen, setScreen] = useState2(null);
|
|
489
|
+
const [error, setError] = useState2(null);
|
|
490
|
+
const [connecting, setConnecting] = useState2(true);
|
|
491
|
+
const [sessionId, setSessionId] = useState2(null);
|
|
492
|
+
const wsRef = useRef(null);
|
|
493
|
+
const lastSentSizeRef = useRef("");
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
let cancelled = false;
|
|
496
|
+
const run = async () => {
|
|
497
|
+
try {
|
|
498
|
+
setConnecting(true);
|
|
499
|
+
setError(null);
|
|
500
|
+
const sessionUrl = `${terminalUrl.replace(/\/+$/, "")}/api/sessions`;
|
|
501
|
+
log(`Creating session: POST ${sessionUrl}`);
|
|
502
|
+
log(` projectId=${projectId}, versionId=${versionId || "(latest)"}`);
|
|
503
|
+
const response = await fetch(sessionUrl, {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers: {
|
|
506
|
+
"Content-Type": "application/json",
|
|
507
|
+
Authorization: `Bearer ${idToken}`
|
|
508
|
+
},
|
|
509
|
+
body: JSON.stringify({
|
|
510
|
+
projectId,
|
|
511
|
+
versionId,
|
|
512
|
+
runnerVersion: "cli-dev"
|
|
513
|
+
})
|
|
514
|
+
});
|
|
515
|
+
if (!response.ok) {
|
|
516
|
+
const text = await response.text();
|
|
517
|
+
log(`Session create failed: ${response.status} ${text}`);
|
|
518
|
+
if (response.status === 401) {
|
|
519
|
+
throw new Error("Authentication failed. Try 'blo login' to re-authenticate.");
|
|
520
|
+
}
|
|
521
|
+
if (response.status === 403) {
|
|
522
|
+
throw new Error(`Access denied for project ${projectId}: ${text}`);
|
|
523
|
+
}
|
|
524
|
+
if (response.status === 429) {
|
|
525
|
+
throw new Error("Rate limited. Please wait a moment and try again.");
|
|
526
|
+
}
|
|
527
|
+
throw new Error(`Session create failed (${response.status}): ${text}`);
|
|
528
|
+
}
|
|
529
|
+
const body = await response.json();
|
|
530
|
+
if (cancelled) return;
|
|
531
|
+
const screenPayload = "screen" in body && body.screen && typeof body.screen === "object" && "sessionId" in body.screen ? body.screen : body;
|
|
532
|
+
const sid = screenPayload.sessionId || ("bootstrap" in body ? body.bootstrap?.sessionId : void 0) || "";
|
|
533
|
+
if (!sid) {
|
|
534
|
+
log(`WARNING: No sessionId in response. Response keys: ${Object.keys(body).join(", ")}`);
|
|
535
|
+
throw new Error("Server returned no session ID. The server may be running an incompatible version.");
|
|
536
|
+
}
|
|
537
|
+
log(`Session created: sessionId=${sid}`);
|
|
538
|
+
log(` screen=${screenPayload.screen}, lines=${screenPayload.lines?.length || 0}`);
|
|
539
|
+
setScreen(screenPayload);
|
|
540
|
+
setSessionId(sid);
|
|
541
|
+
const wsBase = toWsBaseUrl(terminalUrl.replace(/\/+$/, ""));
|
|
542
|
+
const wsUrl = `${wsBase}/ws?sessionId=${encodeURIComponent(sid)}`;
|
|
543
|
+
log(`Connecting WebSocket: ${wsUrl}`);
|
|
544
|
+
const ws = new WebSocket(wsUrl);
|
|
545
|
+
wsRef.current = ws;
|
|
546
|
+
ws.onopen = () => {
|
|
547
|
+
log("WebSocket connected");
|
|
548
|
+
ws.send(JSON.stringify({ type: "init" }));
|
|
549
|
+
const size = getTerminalSize();
|
|
550
|
+
const sizeKey = `${size.rows}x${size.cols}`;
|
|
551
|
+
lastSentSizeRef.current = sizeKey;
|
|
552
|
+
log(`Sending resize: ${size.cols}x${size.rows}`);
|
|
553
|
+
ws.send(
|
|
554
|
+
JSON.stringify({
|
|
555
|
+
type: "resize",
|
|
556
|
+
rows: size.rows,
|
|
557
|
+
cols: size.cols
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
setConnecting(false);
|
|
561
|
+
};
|
|
562
|
+
ws.onmessage = (event) => {
|
|
563
|
+
try {
|
|
564
|
+
const data = JSON.parse(String(event.data));
|
|
565
|
+
if (data.type === "screen") {
|
|
566
|
+
log(`Screen update: screen=${data.screen}, lines=${data.lines?.length || 0}`);
|
|
567
|
+
setScreen(data);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (data.type === "error") {
|
|
571
|
+
log(`Server error: ${data.message}`);
|
|
572
|
+
setError(data.message);
|
|
573
|
+
}
|
|
574
|
+
} catch (err) {
|
|
575
|
+
log(`Failed to parse server message: ${err}`);
|
|
576
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
577
|
+
}
|
|
578
|
+
};
|
|
579
|
+
ws.onclose = (event) => {
|
|
580
|
+
const code = event.code;
|
|
581
|
+
const reason = event.reason || "(no reason)";
|
|
582
|
+
log(`WebSocket closed: code=${code}, reason=${reason}`);
|
|
583
|
+
if (!cancelled) {
|
|
584
|
+
if (code === 1008) {
|
|
585
|
+
setError(`Session rejected by server: ${reason}`);
|
|
586
|
+
} else if (code === 1006) {
|
|
587
|
+
setError("Lost connection to server. Check if the terminal server is running.");
|
|
588
|
+
} else {
|
|
589
|
+
setError(`Connection closed (code ${code}).`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
};
|
|
593
|
+
ws.onerror = (event) => {
|
|
594
|
+
log(`WebSocket error: ${event.message || "unknown"}`);
|
|
595
|
+
if (!cancelled) {
|
|
596
|
+
setError(`Connection error: ${event.message || "Failed to connect to terminal server."}`);
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
} catch (err) {
|
|
600
|
+
if (!cancelled) {
|
|
601
|
+
log(`Runner error: ${err}`);
|
|
602
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
void run();
|
|
607
|
+
return () => {
|
|
608
|
+
cancelled = true;
|
|
609
|
+
wsRef.current?.close();
|
|
610
|
+
wsRef.current = null;
|
|
611
|
+
};
|
|
612
|
+
}, [projectId, versionId, terminalUrl, idToken]);
|
|
613
|
+
useEffect(() => {
|
|
614
|
+
if (!sessionId || !process.stdout.isTTY) return;
|
|
615
|
+
const onTerminalResize = () => {
|
|
616
|
+
const ws = wsRef.current;
|
|
617
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
618
|
+
const size = getTerminalSize();
|
|
619
|
+
const sizeKey = `${size.rows}x${size.cols}`;
|
|
620
|
+
if (lastSentSizeRef.current === sizeKey) return;
|
|
621
|
+
lastSentSizeRef.current = sizeKey;
|
|
622
|
+
log(`Terminal resized: ${size.cols}x${size.rows}`);
|
|
623
|
+
ws.send(
|
|
624
|
+
JSON.stringify({
|
|
625
|
+
type: "resize",
|
|
626
|
+
rows: size.rows,
|
|
627
|
+
cols: size.cols
|
|
628
|
+
})
|
|
629
|
+
);
|
|
25
630
|
};
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
.
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
631
|
+
process.stdout.on("resize", onTerminalResize);
|
|
632
|
+
return () => {
|
|
633
|
+
process.stdout.off("resize", onTerminalResize);
|
|
634
|
+
};
|
|
635
|
+
}, [sessionId]);
|
|
636
|
+
const sendMessage = (message) => {
|
|
637
|
+
const ws = wsRef.current;
|
|
638
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) {
|
|
639
|
+
log(`Cannot send message (ws ${ws ? `state=${ws.readyState}` : "null"}): ${JSON.stringify(message)}`);
|
|
640
|
+
if (ws && ws.readyState >= WebSocket.CLOSING) {
|
|
641
|
+
setError("Connection lost. Press x to return to apps.");
|
|
642
|
+
}
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
log(`Sending: ${JSON.stringify(message)}`);
|
|
646
|
+
ws.send(JSON.stringify(message));
|
|
647
|
+
};
|
|
648
|
+
return { screen, error, connecting, sessionId, sendMessage };
|
|
649
|
+
};
|
|
650
|
+
|
|
651
|
+
// src/ui/useRunnerRenderSource.ts
|
|
652
|
+
import { useMemo as useMemo2 } from "react";
|
|
653
|
+
|
|
654
|
+
// ../shared/src/screen-runtime/parsing/fieldParser.ts
|
|
655
|
+
var isRecord = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
656
|
+
var parseMaxLength = (value) => {
|
|
657
|
+
if (typeof value === "number" && Number.isFinite(value) && value > 0) {
|
|
658
|
+
return Math.min(Math.floor(value), 512);
|
|
659
|
+
}
|
|
660
|
+
if (typeof value === "string") {
|
|
661
|
+
const parsed = Number.parseInt(value, 10);
|
|
662
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
663
|
+
return Math.min(parsed, 512);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return 80;
|
|
667
|
+
};
|
|
668
|
+
var parseRows = (value) => {
|
|
669
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
670
|
+
return Math.max(1, Math.floor(value));
|
|
671
|
+
}
|
|
672
|
+
if (typeof value === "string") {
|
|
673
|
+
const parsed = Number.parseInt(value, 10);
|
|
674
|
+
if (Number.isFinite(parsed)) {
|
|
675
|
+
return Math.max(1, Math.floor(parsed));
|
|
39
676
|
}
|
|
677
|
+
}
|
|
678
|
+
return 1;
|
|
679
|
+
};
|
|
680
|
+
var toFiniteNumber = (value) => {
|
|
681
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
682
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
683
|
+
const parsed = Number(value);
|
|
684
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
685
|
+
}
|
|
686
|
+
return null;
|
|
687
|
+
};
|
|
688
|
+
var normalizeHotkey = (value) => {
|
|
689
|
+
const raw = String(value || "").trim().toLowerCase();
|
|
690
|
+
if (!raw) return null;
|
|
691
|
+
if (!/^[a-z0-9]$/.test(raw)) return null;
|
|
692
|
+
return raw;
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// ../shared/src/screen-runtime/state/pathUtils.ts
|
|
696
|
+
var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
|
|
697
|
+
var isDangerousKey = (key) => DANGEROUS_KEYS.has(key);
|
|
698
|
+
var resolvePath = (source, path2) => {
|
|
699
|
+
if (!path2.trim()) return void 0;
|
|
700
|
+
return path2.split(".").reduce((acc, key) => {
|
|
701
|
+
if (!isRecord(acc)) return void 0;
|
|
702
|
+
if (isDangerousKey(key)) return void 0;
|
|
703
|
+
return acc[key];
|
|
704
|
+
}, source);
|
|
705
|
+
};
|
|
706
|
+
var interpolate = (template, context) => template.replace(/\{\{\s*([^}]+)\s*\}\}/g, (_full, exprRaw) => {
|
|
707
|
+
const expr = String(exprRaw || "").trim();
|
|
708
|
+
const defaultMatch = expr.match(
|
|
709
|
+
/^([A-Za-z0-9_.]+)\s*\|\s*default\s*:\s*(.+)$/
|
|
710
|
+
);
|
|
711
|
+
if (defaultMatch) {
|
|
712
|
+
const path2 = String(defaultMatch[1] || "").trim();
|
|
713
|
+
const fallbackRaw = String(defaultMatch[2] || "").trim();
|
|
714
|
+
const resolved = resolvePath(context, path2);
|
|
715
|
+
if (resolved != null) {
|
|
716
|
+
return String(resolved);
|
|
717
|
+
}
|
|
718
|
+
const fallback = fallbackRaw === "null" ? null : fallbackRaw === "true" ? true : fallbackRaw === "false" ? false : /^-?\d+(\.\d+)?$/.test(fallbackRaw) ? Number(fallbackRaw) : fallbackRaw.replace(/^["']|["']$/g, "");
|
|
719
|
+
return fallback == null ? "" : String(fallback);
|
|
720
|
+
}
|
|
721
|
+
const value = resolvePath(context, expr);
|
|
722
|
+
return value == null ? "" : String(value);
|
|
40
723
|
});
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
724
|
+
|
|
725
|
+
// ../shared/src/screen-runtime/actions/actionBuilder.ts
|
|
726
|
+
var compareSortValues = (left, right) => {
|
|
727
|
+
const leftNum = toFiniteNumber(left);
|
|
728
|
+
const rightNum = toFiniteNumber(right);
|
|
729
|
+
if (leftNum != null && rightNum != null) {
|
|
730
|
+
return leftNum - rightNum;
|
|
731
|
+
}
|
|
732
|
+
const leftText = String(left ?? "").toLowerCase();
|
|
733
|
+
const rightText = String(right ?? "").toLowerCase();
|
|
734
|
+
return leftText.localeCompare(rightText);
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
// ../shared/src/screen-runtime/rendering/widgetHelpers.ts
|
|
738
|
+
var fieldBindKey = (widget) => widget.bind.trim() || widget.id;
|
|
739
|
+
var selectBindKey = (widget) => widget.bind.trim() || widget.id;
|
|
740
|
+
var fieldLabel = (widget) => widget.label || widget.placeholder || widget.id;
|
|
741
|
+
var selectLabel = (widget) => widget.label || widget.placeholder || widget.id;
|
|
742
|
+
var resolveFieldValue = (widget, state) => {
|
|
743
|
+
const bindKey = fieldBindKey(widget);
|
|
744
|
+
const fallback = resolvePath(state.data, bindKey);
|
|
745
|
+
if (state.fields[bindKey] != null) {
|
|
746
|
+
return String(state.fields[bindKey] || "");
|
|
747
|
+
}
|
|
748
|
+
if (typeof fallback === "string" || typeof fallback === "number" || typeof fallback === "boolean") {
|
|
749
|
+
return String(fallback);
|
|
750
|
+
}
|
|
751
|
+
return "";
|
|
752
|
+
};
|
|
753
|
+
var resolveSelectValue = (widget, state) => {
|
|
754
|
+
const bindKey = selectBindKey(widget);
|
|
755
|
+
if (state.fields[bindKey] != null) {
|
|
756
|
+
return String(state.fields[bindKey] || "");
|
|
757
|
+
}
|
|
758
|
+
const fallback = resolvePath(state.data, bindKey);
|
|
759
|
+
if (typeof fallback === "string" || typeof fallback === "number" || typeof fallback === "boolean") {
|
|
760
|
+
return String(fallback);
|
|
761
|
+
}
|
|
762
|
+
return "";
|
|
763
|
+
};
|
|
764
|
+
var checkboxBindKey = (widget) => widget.bind.trim() || widget.id;
|
|
765
|
+
var radioBindKey = (widget) => widget.bind.trim() || widget.id;
|
|
766
|
+
var numberBindKey = (widget) => widget.bind.trim() || widget.id;
|
|
767
|
+
var resolveCheckboxValue = (widget, state) => {
|
|
768
|
+
const bindKey = checkboxBindKey(widget);
|
|
769
|
+
if (state.fields[bindKey] != null) {
|
|
770
|
+
const v = state.fields[bindKey];
|
|
771
|
+
return v === "true" || v === "1";
|
|
772
|
+
}
|
|
773
|
+
const fallback = resolvePath(state.data, bindKey);
|
|
774
|
+
if (typeof fallback === "string") {
|
|
775
|
+
return fallback === "true" || fallback === "1";
|
|
776
|
+
}
|
|
777
|
+
return Boolean(fallback);
|
|
778
|
+
};
|
|
779
|
+
var resolveNumberValue = (widget, state) => {
|
|
780
|
+
const bindKey = numberBindKey(widget);
|
|
781
|
+
if (state.fields[bindKey] != null) {
|
|
782
|
+
const v = Number(state.fields[bindKey]);
|
|
783
|
+
return Number.isFinite(v) ? v : 0;
|
|
784
|
+
}
|
|
785
|
+
const fallback = resolvePath(state.data, bindKey);
|
|
786
|
+
if (typeof fallback === "number" && Number.isFinite(fallback)) return fallback;
|
|
787
|
+
return 0;
|
|
788
|
+
};
|
|
789
|
+
var dateBindKey = (widget) => widget.bind.trim() || widget.id;
|
|
790
|
+
var resolveDateValue = (widget, state) => {
|
|
791
|
+
const bindKey = dateBindKey(widget);
|
|
792
|
+
if (state.fields[bindKey] != null) {
|
|
793
|
+
const v = String(state.fields[bindKey] || "");
|
|
794
|
+
if (parseISODate(v)) return v;
|
|
795
|
+
}
|
|
796
|
+
const fallback = resolvePath(state.data, bindKey);
|
|
797
|
+
if (typeof fallback === "string" && parseISODate(fallback)) return fallback;
|
|
798
|
+
return todayISO();
|
|
799
|
+
};
|
|
800
|
+
var MONTH_NAMES = [
|
|
801
|
+
"January",
|
|
802
|
+
"February",
|
|
803
|
+
"March",
|
|
804
|
+
"April",
|
|
805
|
+
"May",
|
|
806
|
+
"June",
|
|
807
|
+
"July",
|
|
808
|
+
"August",
|
|
809
|
+
"September",
|
|
810
|
+
"October",
|
|
811
|
+
"November",
|
|
812
|
+
"December"
|
|
813
|
+
];
|
|
814
|
+
var MONTH_ABBR = [
|
|
815
|
+
"Jan",
|
|
816
|
+
"Feb",
|
|
817
|
+
"Mar",
|
|
818
|
+
"Apr",
|
|
819
|
+
"May",
|
|
820
|
+
"Jun",
|
|
821
|
+
"Jul",
|
|
822
|
+
"Aug",
|
|
823
|
+
"Sep",
|
|
824
|
+
"Oct",
|
|
825
|
+
"Nov",
|
|
826
|
+
"Dec"
|
|
827
|
+
];
|
|
828
|
+
var parseISODate = (str) => {
|
|
829
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(str);
|
|
830
|
+
if (!match) return null;
|
|
831
|
+
const y = Number(match[1]);
|
|
832
|
+
const m = Number(match[2]);
|
|
833
|
+
const d = Number(match[3]);
|
|
834
|
+
if (m < 1 || m > 12 || d < 1 || d > daysInMonth(y, m)) return null;
|
|
835
|
+
return { y, m, d };
|
|
836
|
+
};
|
|
837
|
+
var formatDisplayDate = (y, m, d) => `${MONTH_ABBR[m - 1]} ${d}, ${y}`;
|
|
838
|
+
var daysInMonth = (y, m) => {
|
|
839
|
+
if (m === 2) return y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0) ? 29 : 28;
|
|
840
|
+
return [0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m];
|
|
841
|
+
};
|
|
842
|
+
var dayOfWeek = (y, m, d) => {
|
|
843
|
+
const t = [0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4];
|
|
844
|
+
const yr = m < 3 ? y - 1 : y;
|
|
845
|
+
return (yr + Math.floor(yr / 4) - Math.floor(yr / 100) + Math.floor(yr / 400) + t[m - 1] + d) % 7;
|
|
846
|
+
};
|
|
847
|
+
var pad2 = (n) => n < 10 ? "0" + n : String(n);
|
|
848
|
+
var toISO = (y, m, d) => `${y}-${pad2(m)}-${pad2(d)}`;
|
|
849
|
+
var shiftDay = (iso, delta) => {
|
|
850
|
+
const parsed = parseISODate(iso);
|
|
851
|
+
if (!parsed) return iso;
|
|
852
|
+
const ms = Date.UTC(parsed.y, parsed.m - 1, parsed.d) + delta * 864e5;
|
|
853
|
+
const dt = new Date(ms);
|
|
854
|
+
return toISO(dt.getUTCFullYear(), dt.getUTCMonth() + 1, dt.getUTCDate());
|
|
855
|
+
};
|
|
856
|
+
var shiftMonth = (iso, delta) => {
|
|
857
|
+
const parsed = parseISODate(iso);
|
|
858
|
+
if (!parsed) return iso;
|
|
859
|
+
let newM = parsed.m + delta;
|
|
860
|
+
let newY = parsed.y;
|
|
861
|
+
while (newM > 12) {
|
|
862
|
+
newM -= 12;
|
|
863
|
+
newY++;
|
|
864
|
+
}
|
|
865
|
+
while (newM < 1) {
|
|
866
|
+
newM += 12;
|
|
867
|
+
newY--;
|
|
868
|
+
}
|
|
869
|
+
const clamped = Math.min(parsed.d, daysInMonth(newY, newM));
|
|
870
|
+
return toISO(newY, newM, clamped);
|
|
871
|
+
};
|
|
872
|
+
var buildCalendarLines = (y, m, selectedDay) => {
|
|
873
|
+
const lines = [];
|
|
874
|
+
lines.push(` Su Mo Tu We Th Fr Sa`);
|
|
875
|
+
const total = daysInMonth(y, m);
|
|
876
|
+
const startDow = dayOfWeek(y, m, 1);
|
|
877
|
+
let line = " ".repeat(startDow * 4);
|
|
878
|
+
for (let d = 1; d <= total; d++) {
|
|
879
|
+
line += d === selectedDay ? `[${pad2(d)}]` : ` ${pad2(d)}`;
|
|
880
|
+
const dow = (startDow + d) % 7;
|
|
881
|
+
if (dow === 0 && d < total) {
|
|
882
|
+
lines.push(line.trimEnd());
|
|
883
|
+
line = "";
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
if (line.trim()) lines.push(line.trimEnd());
|
|
887
|
+
return lines;
|
|
888
|
+
};
|
|
889
|
+
var todayISO = () => {
|
|
890
|
+
const now = /* @__PURE__ */ new Date();
|
|
891
|
+
return toISO(now.getUTCFullYear(), now.getUTCMonth() + 1, now.getUTCDate());
|
|
892
|
+
};
|
|
893
|
+
var CANCEL_PICKER = { type: "data.set", path: "__datePicker", value: null };
|
|
894
|
+
var CONFIRM_PICKER = { type: "date.pick" };
|
|
895
|
+
var isDatePickerActive = (data, activeScreenId) => {
|
|
896
|
+
const raw = data.__datePicker;
|
|
897
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
898
|
+
const picker = raw;
|
|
899
|
+
if (typeof picker.bindPath !== "string" || typeof picker.currentISO !== "string") return null;
|
|
900
|
+
if (picker.returnScreenId !== activeScreenId) return null;
|
|
901
|
+
return picker;
|
|
902
|
+
};
|
|
903
|
+
var renderDatePickerLines = (picker) => {
|
|
904
|
+
const iso = picker.currentISO;
|
|
905
|
+
const parsed = parseISODate(iso);
|
|
906
|
+
if (!parsed) {
|
|
907
|
+
return { lines: [{ text: "Invalid date" }], shortcuts: [], enterAction: CANCEL_PICKER, cancelAction: CANCEL_PICKER };
|
|
908
|
+
}
|
|
909
|
+
const p = "__datePicker.currentISO";
|
|
910
|
+
const setP = (v) => ({ type: "data.set", path: p, value: v });
|
|
911
|
+
return {
|
|
912
|
+
lines: [
|
|
913
|
+
{ text: "" },
|
|
914
|
+
{ text: " === Select Date ===" },
|
|
915
|
+
{ text: "" },
|
|
916
|
+
{ text: ` ${parsed.y}` },
|
|
917
|
+
{ text: ` [<] ${MONTH_NAMES[parsed.m - 1]} [>]` },
|
|
918
|
+
{ text: "" },
|
|
919
|
+
...buildCalendarLines(parsed.y, parsed.m, parsed.d).map((cl) => ({ text: ` ${cl}` })),
|
|
920
|
+
{ text: "" },
|
|
921
|
+
{ text: ` Selected: ${formatDisplayDate(parsed.y, parsed.m, parsed.d)}` },
|
|
922
|
+
{ text: "" },
|
|
923
|
+
{ text: " Arrows/H/L: Day J/K: Week" },
|
|
924
|
+
{ text: " [<] Prev Month [>] Next Month" },
|
|
925
|
+
{ text: " [[] Prev Year []] Next Year" },
|
|
926
|
+
{ text: " [T] Today" },
|
|
927
|
+
{ text: " Enter: Select Esc: Cancel" }
|
|
928
|
+
],
|
|
929
|
+
shortcuts: [
|
|
930
|
+
{ key: "<", label: "Prev month", action: setP(shiftMonth(iso, -1)) },
|
|
931
|
+
{ key: ">", label: "Next month", action: setP(shiftMonth(iso, 1)) },
|
|
932
|
+
{ key: "h", label: "Prev day", action: setP(shiftDay(iso, -1)) },
|
|
933
|
+
{ key: "l", label: "Next day", action: setP(shiftDay(iso, 1)) },
|
|
934
|
+
{ key: "j", label: "Next week", action: setP(shiftDay(iso, 7)) },
|
|
935
|
+
{ key: "k", label: "Prev week", action: setP(shiftDay(iso, -7)) },
|
|
936
|
+
{ key: "[", label: "Prev year", action: setP(shiftMonth(iso, -12)) },
|
|
937
|
+
{ key: "]", label: "Next year", action: setP(shiftMonth(iso, 12)) },
|
|
938
|
+
{ key: "t", label: "Today", action: { type: "date.pick", value: todayISO() } }
|
|
939
|
+
],
|
|
940
|
+
enterAction: CONFIRM_PICKER,
|
|
941
|
+
cancelAction: CANCEL_PICKER
|
|
942
|
+
};
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// ../shared/src/screen-runtime/rendering/screenRenderer.ts
|
|
946
|
+
var collectScreenSpecContext = (screenSpec, state, options = {}) => {
|
|
947
|
+
const ctx = { lines: [], shortcuts: [], inputFields: [], actionLines: [], listScrollInfo: {} };
|
|
948
|
+
const hotkeySet = /* @__PURE__ */ new Set();
|
|
949
|
+
const vars = {
|
|
950
|
+
...state.data,
|
|
951
|
+
fields: state.fields
|
|
952
|
+
};
|
|
953
|
+
const picker = isDatePickerActive(state.data, state.activeScreenId);
|
|
954
|
+
if (picker) {
|
|
955
|
+
const result = renderDatePickerLines(picker);
|
|
956
|
+
return { ...ctx, ...result };
|
|
957
|
+
}
|
|
958
|
+
const sortedWidgets = [...screenSpec.widgets].sort((a, b) => {
|
|
959
|
+
const rowA = Number(a.row || 0);
|
|
960
|
+
const rowB = Number(b.row || 0);
|
|
961
|
+
if (rowA !== rowB) return rowA - rowB;
|
|
962
|
+
const colA = Number(a.col || 0);
|
|
963
|
+
const colB = Number(b.col || 0);
|
|
964
|
+
return colA - colB;
|
|
965
|
+
});
|
|
966
|
+
const pushSpacerLines = (count) => {
|
|
967
|
+
for (let i = 0; i < count; i += 1) {
|
|
968
|
+
ctx.lines.push({ text: "" });
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
sortedWidgets.forEach((widget) => {
|
|
972
|
+
if (widget.kind === "text") {
|
|
973
|
+
ctx.lines.push({
|
|
974
|
+
text: interpolate(widget.text, vars),
|
|
975
|
+
widgetId: widget.id
|
|
976
|
+
});
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
if (widget.kind === "field") {
|
|
980
|
+
const bindKey = fieldBindKey(widget);
|
|
981
|
+
const value = resolveFieldValue(widget, state);
|
|
982
|
+
const label = fieldLabel(widget);
|
|
983
|
+
const perRowLength = Math.max(1, parseMaxLength(widget.length));
|
|
984
|
+
const rowCount = Math.max(1, parseRows(widget.rows));
|
|
985
|
+
const lineStep = Math.max(1, Number(widget.lineStep) || 1);
|
|
986
|
+
const maxLength = Math.min(512, perRowLength * rowCount);
|
|
987
|
+
const labelPrefix = label ? `${label}: ` : "";
|
|
988
|
+
const indent = " ".repeat(labelPrefix.length);
|
|
989
|
+
const rawLines = String(value || "").split(/\r?\n/);
|
|
990
|
+
const wrappedLines = [];
|
|
991
|
+
rawLines.forEach((line) => {
|
|
992
|
+
if (line.length === 0) {
|
|
993
|
+
wrappedLines.push("");
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
for (let i = 0; i < line.length; i += perRowLength) {
|
|
997
|
+
wrappedLines.push(line.slice(i, i + perRowLength));
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
if (wrappedLines.length === 0) wrappedLines.push("");
|
|
1001
|
+
const paddedLines = wrappedLines.slice(0, rowCount);
|
|
1002
|
+
while (paddedLines.length < rowCount) paddedLines.push("");
|
|
1003
|
+
const lineIndex = ctx.lines.length;
|
|
1004
|
+
paddedLines.forEach((line, idx) => {
|
|
1005
|
+
ctx.lines.push({
|
|
1006
|
+
text: `${idx === 0 ? labelPrefix : indent}${line}`,
|
|
1007
|
+
widgetId: widget.id,
|
|
1008
|
+
fieldId: bindKey
|
|
1009
|
+
});
|
|
1010
|
+
const isLast = idx === paddedLines.length - 1;
|
|
1011
|
+
if (lineStep > 1 && !isLast) {
|
|
1012
|
+
for (let gap = 1; gap < lineStep; gap += 1) {
|
|
1013
|
+
ctx.lines.push({
|
|
1014
|
+
text: indent,
|
|
1015
|
+
widgetId: widget.id,
|
|
1016
|
+
fieldId: bindKey
|
|
1017
|
+
});
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
const validationMessage = String(state.fieldErrors?.[bindKey] || "").trim();
|
|
1022
|
+
if (validationMessage) {
|
|
1023
|
+
ctx.lines.push({ text: `! ${validationMessage}`, widgetId: widget.id });
|
|
1024
|
+
}
|
|
1025
|
+
ctx.inputFields.push({
|
|
1026
|
+
id: bindKey,
|
|
1027
|
+
value,
|
|
1028
|
+
maxLength,
|
|
1029
|
+
label,
|
|
1030
|
+
lineIndex,
|
|
1031
|
+
rows: rowCount,
|
|
1032
|
+
lineStep
|
|
1033
|
+
});
|
|
1034
|
+
if (!ctx.firstInput) {
|
|
1035
|
+
ctx.firstInput = {
|
|
1036
|
+
id: bindKey,
|
|
1037
|
+
maxLength,
|
|
1038
|
+
label
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
if (widget.kind === "action") {
|
|
1044
|
+
const renderedLabel = interpolate(widget.label, vars);
|
|
1045
|
+
const hotkey = normalizeHotkey(widget.hotkey);
|
|
1046
|
+
const lineLabel = options.hideHotkeys ? renderedLabel : hotkey ? `[${hotkey.toUpperCase()}] ${renderedLabel}` : `[ ${renderedLabel} ]`;
|
|
1047
|
+
if (widget.action.type === "nav.goto") {
|
|
1048
|
+
ctx.lines.push({
|
|
1049
|
+
text: lineLabel,
|
|
1050
|
+
widgetId: widget.id,
|
|
1051
|
+
target: widget.action.target
|
|
1052
|
+
});
|
|
1053
|
+
if (!options.hideHotkeys && hotkey && !hotkeySet.has(hotkey)) {
|
|
1054
|
+
hotkeySet.add(hotkey);
|
|
1055
|
+
ctx.shortcuts.push({
|
|
1056
|
+
key: hotkey,
|
|
1057
|
+
label: renderedLabel,
|
|
1058
|
+
target: widget.action.target
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
ctx.lines.push({
|
|
1064
|
+
text: lineLabel,
|
|
1065
|
+
widgetId: widget.id,
|
|
1066
|
+
action: widget.action
|
|
1067
|
+
});
|
|
1068
|
+
if (!options.hideHotkeys && hotkey && !hotkeySet.has(hotkey)) {
|
|
1069
|
+
hotkeySet.add(hotkey);
|
|
1070
|
+
ctx.shortcuts.push({
|
|
1071
|
+
key: hotkey,
|
|
1072
|
+
label: renderedLabel,
|
|
1073
|
+
action: widget.action
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
if (widget.kind === "select") {
|
|
1079
|
+
const bindKey = selectBindKey(widget);
|
|
1080
|
+
const label = selectLabel(widget);
|
|
1081
|
+
const currentValue = resolveSelectValue(widget, state).trim();
|
|
1082
|
+
const normalizedCurrent = currentValue.toLowerCase();
|
|
1083
|
+
const gap = Math.max(1, Number(widget.gap ?? 2));
|
|
1084
|
+
const parts = [];
|
|
1085
|
+
const labelPrefix = label ? `${label} ` : "";
|
|
1086
|
+
widget.options.forEach((option) => {
|
|
1087
|
+
const renderedLabel = interpolate(option.label, vars);
|
|
1088
|
+
const hotkey = normalizeHotkey(option.hotkey);
|
|
1089
|
+
const normalizedOption = String(option.value || "").trim().toLowerCase();
|
|
1090
|
+
const selected = normalizedCurrent.length > 0 && normalizedCurrent === normalizedOption;
|
|
1091
|
+
const displayLabel = selected ? `${renderedLabel}*` : renderedLabel;
|
|
1092
|
+
const lineLabel = options.hideHotkeys ? displayLabel : hotkey ? `[${hotkey.toUpperCase()}] ${displayLabel}` : `[ ${displayLabel} ]`;
|
|
1093
|
+
parts.push(lineLabel);
|
|
1094
|
+
if (!options.hideHotkeys && hotkey && !hotkeySet.has(hotkey)) {
|
|
1095
|
+
hotkeySet.add(hotkey);
|
|
1096
|
+
ctx.shortcuts.push({
|
|
1097
|
+
key: hotkey,
|
|
1098
|
+
label: renderedLabel,
|
|
1099
|
+
action: {
|
|
1100
|
+
type: "data.set",
|
|
1101
|
+
path: bindKey,
|
|
1102
|
+
value: option.value
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
});
|
|
1107
|
+
const lineIndex = ctx.lines.length;
|
|
1108
|
+
ctx.lines.push({
|
|
1109
|
+
text: `${labelPrefix}${parts.join(" ".repeat(gap))}`.trimEnd(),
|
|
1110
|
+
widgetId: widget.id,
|
|
1111
|
+
fieldId: bindKey
|
|
1112
|
+
});
|
|
1113
|
+
ctx.inputFields.push({
|
|
1114
|
+
id: bindKey,
|
|
1115
|
+
value: currentValue,
|
|
1116
|
+
maxLength: 0,
|
|
1117
|
+
label,
|
|
1118
|
+
lineIndex,
|
|
1119
|
+
rows: 1
|
|
1120
|
+
});
|
|
1121
|
+
const validationMessage = String(state.fieldErrors?.[bindKey] || "").trim();
|
|
1122
|
+
if (validationMessage) {
|
|
1123
|
+
ctx.lines.push({ text: `! ${validationMessage}`, widgetId: widget.id });
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
if (widget.kind === "checkbox") {
|
|
1128
|
+
const bindKey = checkboxBindKey(widget);
|
|
1129
|
+
const checked = resolveCheckboxValue(widget, state);
|
|
1130
|
+
const marker = checked ? "[X]" : "[ ]";
|
|
1131
|
+
const renderedLabel = interpolate(widget.label, vars);
|
|
1132
|
+
const hotkey = normalizeHotkey(widget.hotkey);
|
|
1133
|
+
const lineLabel = options.hideHotkeys ? `${marker} ${renderedLabel}` : hotkey ? `[${hotkey.toUpperCase()}] ${marker} ${renderedLabel}` : `${marker} ${renderedLabel}`;
|
|
1134
|
+
const lineIndex = ctx.lines.length;
|
|
1135
|
+
ctx.lines.push({ text: lineLabel, widgetId: widget.id, fieldId: bindKey });
|
|
1136
|
+
const toggleAction = widget.action ?? { type: "data.toggleFlag", path: bindKey };
|
|
1137
|
+
if (!options.hideHotkeys && hotkey && !hotkeySet.has(hotkey)) {
|
|
1138
|
+
hotkeySet.add(hotkey);
|
|
1139
|
+
ctx.shortcuts.push({
|
|
1140
|
+
key: hotkey,
|
|
1141
|
+
label: renderedLabel,
|
|
1142
|
+
action: toggleAction
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
ctx.inputFields.push({
|
|
1146
|
+
id: bindKey,
|
|
1147
|
+
value: String(checked),
|
|
1148
|
+
maxLength: 0,
|
|
1149
|
+
label: renderedLabel,
|
|
1150
|
+
lineIndex,
|
|
1151
|
+
rows: 1,
|
|
1152
|
+
action: toggleAction
|
|
1153
|
+
});
|
|
1154
|
+
return;
|
|
1155
|
+
}
|
|
1156
|
+
if (widget.kind === "radio") {
|
|
1157
|
+
const bindKey = radioBindKey(widget);
|
|
1158
|
+
const currentValue = resolveSelectValue(widget, state).trim();
|
|
1159
|
+
const normalizedCurrent = currentValue.toLowerCase();
|
|
1160
|
+
const gap = Math.max(1, Number(widget.gap ?? 2));
|
|
1161
|
+
const parts = [];
|
|
1162
|
+
const label = widget.label ? `${widget.label} ` : "";
|
|
1163
|
+
widget.options.forEach((option) => {
|
|
1164
|
+
const renderedLabel = interpolate(option.label, vars);
|
|
1165
|
+
const hotkey = normalizeHotkey(option.hotkey);
|
|
1166
|
+
const normalizedOption = String(option.value || "").trim().toLowerCase();
|
|
1167
|
+
const selected = normalizedCurrent.length > 0 && normalizedCurrent === normalizedOption;
|
|
1168
|
+
const marker = selected ? "(o)" : "( )";
|
|
1169
|
+
const displayLabel = `${marker} ${renderedLabel}`;
|
|
1170
|
+
const lineLabel = options.hideHotkeys ? displayLabel : hotkey ? `[${hotkey.toUpperCase()}] ${displayLabel}` : displayLabel;
|
|
1171
|
+
parts.push(lineLabel);
|
|
1172
|
+
if (!options.hideHotkeys && hotkey && !hotkeySet.has(hotkey)) {
|
|
1173
|
+
hotkeySet.add(hotkey);
|
|
1174
|
+
ctx.shortcuts.push({
|
|
1175
|
+
key: hotkey,
|
|
1176
|
+
label: renderedLabel,
|
|
1177
|
+
action: { type: "data.set", path: bindKey, value: option.value }
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
const lineIndex = ctx.lines.length;
|
|
1182
|
+
ctx.lines.push({
|
|
1183
|
+
text: `${label}${parts.join(" ".repeat(gap))}`.trimEnd(),
|
|
1184
|
+
widgetId: widget.id,
|
|
1185
|
+
fieldId: bindKey
|
|
1186
|
+
});
|
|
1187
|
+
ctx.inputFields.push({
|
|
1188
|
+
id: bindKey,
|
|
1189
|
+
value: currentValue,
|
|
1190
|
+
maxLength: 0,
|
|
1191
|
+
label: widget.label || widget.id,
|
|
1192
|
+
lineIndex,
|
|
1193
|
+
rows: 1
|
|
1194
|
+
});
|
|
1195
|
+
const validationMessage = String(state.fieldErrors?.[bindKey] || "").trim();
|
|
1196
|
+
if (validationMessage) {
|
|
1197
|
+
ctx.lines.push({ text: `! ${validationMessage}`, widgetId: widget.id });
|
|
1198
|
+
}
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
if (widget.kind === "number") {
|
|
1202
|
+
const bindKey = numberBindKey(widget);
|
|
1203
|
+
const value = resolveNumberValue(widget, state);
|
|
1204
|
+
const label = widget.label || widget.id;
|
|
1205
|
+
const step = typeof widget.step === "number" && Number.isFinite(widget.step) ? widget.step : 1;
|
|
1206
|
+
const incAction = {
|
|
1207
|
+
type: "data.increment",
|
|
1208
|
+
path: bindKey,
|
|
1209
|
+
by: step,
|
|
1210
|
+
...typeof widget.min === "number" ? { min: widget.min } : {},
|
|
1211
|
+
...typeof widget.max === "number" ? { max: widget.max } : {}
|
|
1212
|
+
};
|
|
1213
|
+
const decAction = {
|
|
1214
|
+
type: "data.increment",
|
|
1215
|
+
path: bindKey,
|
|
1216
|
+
by: -step,
|
|
1217
|
+
...typeof widget.min === "number" ? { min: widget.min } : {},
|
|
1218
|
+
...typeof widget.max === "number" ? { max: widget.max } : {}
|
|
1219
|
+
};
|
|
1220
|
+
const lineLabel = options.hideHotkeys ? `${label}: [ ${value} ]` : `${label}: [ ${value} ] [+][-]`;
|
|
1221
|
+
const lineIndex = ctx.lines.length;
|
|
1222
|
+
ctx.lines.push({ text: lineLabel, widgetId: widget.id, fieldId: bindKey });
|
|
1223
|
+
if (!options.hideHotkeys) {
|
|
1224
|
+
if (!hotkeySet.has("+")) {
|
|
1225
|
+
hotkeySet.add("+");
|
|
1226
|
+
ctx.shortcuts.push({ key: "+", label: `${label} +`, action: incAction });
|
|
1227
|
+
}
|
|
1228
|
+
if (!hotkeySet.has("-")) {
|
|
1229
|
+
hotkeySet.add("-");
|
|
1230
|
+
ctx.shortcuts.push({ key: "-", label: `${label} -`, action: decAction });
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
ctx.inputFields.push({
|
|
1234
|
+
id: bindKey,
|
|
1235
|
+
value: String(value),
|
|
1236
|
+
maxLength: 0,
|
|
1237
|
+
label,
|
|
1238
|
+
lineIndex,
|
|
1239
|
+
rows: 1
|
|
1240
|
+
});
|
|
1241
|
+
const validationMessage = String(state.fieldErrors?.[bindKey] || "").trim();
|
|
1242
|
+
if (validationMessage) {
|
|
1243
|
+
ctx.lines.push({ text: `! ${validationMessage}`, widgetId: widget.id });
|
|
1244
|
+
}
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
if (widget.kind === "date") {
|
|
1248
|
+
const bindKey = dateBindKey(widget);
|
|
1249
|
+
const iso = resolveDateValue(widget, state);
|
|
1250
|
+
const parsed = parseISODate(iso);
|
|
1251
|
+
const label = widget.label || widget.id;
|
|
1252
|
+
const display = widget.display || "inline";
|
|
1253
|
+
const dateLineIndex = ctx.lines.length;
|
|
1254
|
+
if (display === "calendar" && parsed) {
|
|
1255
|
+
const monthLabel = `${MONTH_NAMES[parsed.m - 1]} ${parsed.y}`;
|
|
1256
|
+
ctx.lines.push({ text: `${label}: ${monthLabel}`, widgetId: widget.id, fieldId: bindKey });
|
|
1257
|
+
const navLine = options.hideHotkeys ? "" : `[<] [>]`;
|
|
1258
|
+
if (navLine) ctx.lines.push({ text: navLine, widgetId: widget.id });
|
|
1259
|
+
const calLines = buildCalendarLines(parsed.y, parsed.m, parsed.d);
|
|
1260
|
+
for (const cl of calLines) {
|
|
1261
|
+
ctx.lines.push({ text: cl, widgetId: widget.id });
|
|
1262
|
+
}
|
|
1263
|
+
if (!options.hideHotkeys) {
|
|
1264
|
+
if (!hotkeySet.has("<")) {
|
|
1265
|
+
hotkeySet.add("<");
|
|
1266
|
+
ctx.shortcuts.push({
|
|
1267
|
+
key: "<",
|
|
1268
|
+
label: `${label} prev month`,
|
|
1269
|
+
action: { type: "data.set", path: bindKey, value: shiftMonth(iso, -1) }
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
if (!hotkeySet.has(">")) {
|
|
1273
|
+
hotkeySet.add(">");
|
|
1274
|
+
ctx.shortcuts.push({
|
|
1275
|
+
key: ">",
|
|
1276
|
+
label: `${label} next month`,
|
|
1277
|
+
action: { type: "data.set", path: bindKey, value: shiftMonth(iso, 1) }
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
} else if (parsed) {
|
|
1282
|
+
const displayStr = formatDisplayDate(parsed.y, parsed.m, parsed.d);
|
|
1283
|
+
const lineLabel = options.hideHotkeys ? `${label}: ${displayStr}` : `${label}: [<] ${displayStr} [>]`;
|
|
1284
|
+
ctx.lines.push({ text: lineLabel, widgetId: widget.id, fieldId: bindKey });
|
|
1285
|
+
if (!options.hideHotkeys) {
|
|
1286
|
+
if (!hotkeySet.has("<")) {
|
|
1287
|
+
hotkeySet.add("<");
|
|
1288
|
+
ctx.shortcuts.push({
|
|
1289
|
+
key: "<",
|
|
1290
|
+
label: `${label} prev day`,
|
|
1291
|
+
action: { type: "data.set", path: bindKey, value: shiftDay(iso, -1) }
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
if (!hotkeySet.has(">")) {
|
|
1295
|
+
hotkeySet.add(">");
|
|
1296
|
+
ctx.shortcuts.push({
|
|
1297
|
+
key: ">",
|
|
1298
|
+
label: `${label} next day`,
|
|
1299
|
+
action: { type: "data.set", path: bindKey, value: shiftDay(iso, 1) }
|
|
1300
|
+
});
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
if (!ctx.enterAction) {
|
|
1305
|
+
ctx.enterAction = {
|
|
1306
|
+
type: "data.set",
|
|
1307
|
+
path: "__datePicker",
|
|
1308
|
+
value: {
|
|
1309
|
+
bindPath: bindKey,
|
|
1310
|
+
currentISO: iso,
|
|
1311
|
+
returnScreenId: screenSpec.id
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
ctx.inputFields.push({
|
|
1316
|
+
id: bindKey,
|
|
1317
|
+
value: iso,
|
|
1318
|
+
maxLength: 0,
|
|
1319
|
+
label,
|
|
1320
|
+
lineIndex: dateLineIndex,
|
|
1321
|
+
rows: 1
|
|
1322
|
+
});
|
|
1323
|
+
const validationMessage = String(state.fieldErrors?.[bindKey] || "").trim();
|
|
1324
|
+
if (validationMessage) {
|
|
1325
|
+
ctx.lines.push({ text: `! ${validationMessage}`, widgetId: widget.id });
|
|
1326
|
+
}
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
if (widget.kind === "row") {
|
|
1330
|
+
const gap = Math.max(1, Number(widget.gap ?? 2));
|
|
1331
|
+
const parts = [];
|
|
1332
|
+
widget.items.forEach((item) => {
|
|
1333
|
+
if (item.kind === "text") {
|
|
1334
|
+
parts.push(interpolate(item.text, vars));
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
const renderedLabel = interpolate(item.label, vars);
|
|
1338
|
+
const hotkey = normalizeHotkey(item.hotkey);
|
|
1339
|
+
const lineLabel = options.hideHotkeys ? renderedLabel : hotkey ? `[${hotkey.toUpperCase()}] ${renderedLabel}` : `[ ${renderedLabel} ]`;
|
|
1340
|
+
parts.push(lineLabel);
|
|
1341
|
+
if (!options.hideHotkeys && hotkey && !hotkeySet.has(hotkey)) {
|
|
1342
|
+
hotkeySet.add(hotkey);
|
|
1343
|
+
ctx.shortcuts.push({
|
|
1344
|
+
key: hotkey,
|
|
1345
|
+
label: renderedLabel,
|
|
1346
|
+
action: item.action
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
});
|
|
1350
|
+
ctx.lines.push({
|
|
1351
|
+
text: parts.join(" ".repeat(gap)),
|
|
1352
|
+
widgetId: widget.id
|
|
1353
|
+
});
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (widget.kind === "table") {
|
|
1357
|
+
const padCell = (text, width, align = "left") => {
|
|
1358
|
+
const truncated = text.length > width ? text.slice(0, width) : text;
|
|
1359
|
+
return align === "right" ? truncated.padStart(width) : truncated.padEnd(width);
|
|
1360
|
+
};
|
|
1361
|
+
const marginTop = Math.max(0, Number(widget.marginTop ?? 1));
|
|
1362
|
+
if (marginTop) pushSpacerLines(marginTop);
|
|
1363
|
+
const listValue = resolvePath(vars, widget.bind);
|
|
1364
|
+
let rows = Array.isArray(listValue) ? listValue : [];
|
|
1365
|
+
if (widget.whereEqualsPath) {
|
|
1366
|
+
rows = rows.filter((item) => {
|
|
1367
|
+
const raw = resolvePath(item, widget.whereEqualsPath || "");
|
|
1368
|
+
return raw === widget.whereEquals;
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
const sortByPath = String(widget.sortByPath || "").trim();
|
|
1372
|
+
if (sortByPath) {
|
|
1373
|
+
const direction = String(widget.sortDirection || "asc").trim().toLowerCase() === "desc" ? -1 : 1;
|
|
1374
|
+
rows = [...rows].sort((left, right) => {
|
|
1375
|
+
const leftValue = resolvePath(left, sortByPath);
|
|
1376
|
+
const rightValue = resolvePath(right, sortByPath);
|
|
1377
|
+
const primary = compareSortValues(leftValue, rightValue) * direction;
|
|
1378
|
+
if (primary !== 0) return primary;
|
|
1379
|
+
return compareSortValues(resolvePath(left, "id"), resolvePath(right, "id"));
|
|
1380
|
+
});
|
|
1381
|
+
}
|
|
1382
|
+
const cols = widget.columns;
|
|
1383
|
+
const separator = "+" + cols.map((c) => "-".repeat(c.width)).join("+") + "+";
|
|
1384
|
+
const headerRow = "|" + cols.map((c) => padCell(c.header, c.width)).join("|") + "|";
|
|
1385
|
+
ctx.lines.push({ text: separator, widgetId: widget.id });
|
|
1386
|
+
ctx.lines.push({ text: headerRow, widgetId: widget.id });
|
|
1387
|
+
ctx.lines.push({ text: separator, widgetId: widget.id });
|
|
1388
|
+
if (!rows.length) {
|
|
1389
|
+
const emptyText = widget.emptyText || "(no rows)";
|
|
1390
|
+
const totalInner = cols.reduce((sum, c) => sum + c.width, 0) + cols.length - 1;
|
|
1391
|
+
const paddedEmpty = emptyText.length > totalInner ? emptyText.slice(0, totalInner) : emptyText.padEnd(totalInner);
|
|
1392
|
+
ctx.lines.push({ text: "|" + paddedEmpty + "|", widgetId: widget.id });
|
|
1393
|
+
ctx.lines.push({ text: separator, widgetId: widget.id });
|
|
1394
|
+
const marginBottom2 = Math.max(0, Number(widget.marginBottom ?? 1));
|
|
1395
|
+
if (marginBottom2) pushSpacerLines(marginBottom2);
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
const maxVisible = Math.max(1, Number(widget.height) || 10);
|
|
1399
|
+
const tableScrollable = rows.length > maxVisible;
|
|
1400
|
+
const tableOffset = Math.max(0, Math.min(options.listScrollOffsets?.[widget.id] ?? 0, rows.length - maxVisible));
|
|
1401
|
+
const visibleRows = rows.slice(tableOffset, tableOffset + maxVisible);
|
|
1402
|
+
if (tableScrollable) {
|
|
1403
|
+
ctx.lines.push({ text: tableOffset > 0 ? `\u2191 ${tableOffset} more` : "", widgetId: widget.id });
|
|
1404
|
+
}
|
|
1405
|
+
visibleRows.forEach((item, sliceIndex) => {
|
|
1406
|
+
const index = tableOffset + sliceIndex;
|
|
1407
|
+
const fields = {};
|
|
1408
|
+
Object.entries(widget.rowActionFields || {}).forEach(([fieldId, valuePath]) => {
|
|
1409
|
+
const raw = resolvePath(item, String(valuePath || "").trim());
|
|
1410
|
+
fields[String(fieldId)] = raw == null ? "" : String(raw);
|
|
1411
|
+
});
|
|
1412
|
+
const rowActionField = String(widget.rowActionField || "").trim();
|
|
1413
|
+
if (rowActionField) {
|
|
1414
|
+
const valuePath = String(widget.rowActionValuePath || "").trim() || "id";
|
|
1415
|
+
const rawValue = resolvePath(item, valuePath);
|
|
1416
|
+
fields[rowActionField] = rawValue == null ? "" : String(rawValue);
|
|
1417
|
+
}
|
|
1418
|
+
const hasFields = Object.keys(fields).length > 0;
|
|
1419
|
+
const rowAction = widget.rowAction;
|
|
1420
|
+
const cellTexts = cols.map((c) => {
|
|
1421
|
+
const raw = resolvePath(item, c.path);
|
|
1422
|
+
return padCell(raw == null ? "" : String(raw), c.width, c.align);
|
|
1423
|
+
});
|
|
1424
|
+
ctx.lines.push({
|
|
1425
|
+
text: "|" + cellTexts.join("|") + "|",
|
|
1426
|
+
widgetId: widget.id,
|
|
1427
|
+
selectable: Boolean(rowAction),
|
|
1428
|
+
target: rowAction?.type === "nav.goto" ? rowAction.target : void 0,
|
|
1429
|
+
action: rowAction && rowAction.type !== "nav.goto" ? rowAction : void 0,
|
|
1430
|
+
fields: hasFields ? fields : void 0
|
|
1431
|
+
});
|
|
1432
|
+
});
|
|
1433
|
+
const dataRowsEmitted = visibleRows.length;
|
|
1434
|
+
for (let pad = dataRowsEmitted; pad < maxVisible; pad++) {
|
|
1435
|
+
ctx.lines.push({ text: "", widgetId: widget.id });
|
|
1436
|
+
}
|
|
1437
|
+
if (tableScrollable) {
|
|
1438
|
+
const belowCount = rows.length - tableOffset - visibleRows.length;
|
|
1439
|
+
ctx.lines.push({ text: belowCount > 0 ? `\u2193 ${belowCount} more` : "", widgetId: widget.id });
|
|
1440
|
+
}
|
|
1441
|
+
ctx.lines.push({ text: separator, widgetId: widget.id });
|
|
1442
|
+
if (rows.length > maxVisible) {
|
|
1443
|
+
ctx.listScrollInfo[widget.id] = { totalRows: rows.length, visibleRows: visibleRows.length, offset: tableOffset };
|
|
1444
|
+
}
|
|
1445
|
+
const marginBottom = Math.max(0, Number(widget.marginBottom ?? 1));
|
|
1446
|
+
if (marginBottom) pushSpacerLines(marginBottom);
|
|
1447
|
+
return;
|
|
1448
|
+
}
|
|
1449
|
+
if (widget.kind === "list") {
|
|
1450
|
+
const marginTop = Math.max(0, Number(widget.marginTop ?? 1));
|
|
1451
|
+
if (marginTop) pushSpacerLines(marginTop);
|
|
1452
|
+
const listValue = resolvePath(vars, widget.bind);
|
|
1453
|
+
let rows = Array.isArray(listValue) ? listValue : [];
|
|
1454
|
+
if (widget.whereEqualsPath) {
|
|
1455
|
+
rows = rows.filter((item) => {
|
|
1456
|
+
const raw = resolvePath(item, widget.whereEqualsPath || "");
|
|
1457
|
+
return raw === widget.whereEquals;
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
const sortByPath = String(widget.sortByPath || "").trim();
|
|
1461
|
+
if (sortByPath) {
|
|
1462
|
+
const direction = String(widget.sortDirection || "asc").trim().toLowerCase() === "desc" ? -1 : 1;
|
|
1463
|
+
rows = [...rows].sort((left, right) => {
|
|
1464
|
+
const leftValue = resolvePath(left, sortByPath);
|
|
1465
|
+
const rightValue = resolvePath(right, sortByPath);
|
|
1466
|
+
const primary = compareSortValues(leftValue, rightValue) * direction;
|
|
1467
|
+
if (primary !== 0) return primary;
|
|
1468
|
+
return compareSortValues(resolvePath(left, "id"), resolvePath(right, "id"));
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
if (!rows.length) {
|
|
1472
|
+
ctx.lines.push({
|
|
1473
|
+
text: widget.emptyText || "(no rows)",
|
|
1474
|
+
widgetId: widget.id
|
|
1475
|
+
});
|
|
1476
|
+
const marginBottom2 = Math.max(0, Number(widget.marginBottom ?? 1));
|
|
1477
|
+
if (marginBottom2) pushSpacerLines(marginBottom2);
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
const listMaxVisible = Math.max(1, Number(widget.height) || 10);
|
|
1481
|
+
const listScrollable = rows.length > listMaxVisible;
|
|
1482
|
+
const listOffset = Math.max(0, Math.min(options.listScrollOffsets?.[widget.id] ?? 0, rows.length - listMaxVisible));
|
|
1483
|
+
const listVisibleRows = rows.slice(listOffset, listOffset + listMaxVisible);
|
|
1484
|
+
if (listScrollable) {
|
|
1485
|
+
ctx.lines.push({ text: listOffset > 0 ? `\u2191 ${listOffset} more` : "", widgetId: widget.id });
|
|
1486
|
+
}
|
|
1487
|
+
listVisibleRows.forEach((item, sliceIndex) => {
|
|
1488
|
+
const index = listOffset + sliceIndex;
|
|
1489
|
+
const fields = {};
|
|
1490
|
+
Object.entries(widget.rowActionFields || {}).forEach(([fieldId, valuePath]) => {
|
|
1491
|
+
const raw = resolvePath(item, String(valuePath || "").trim());
|
|
1492
|
+
fields[String(fieldId)] = raw == null ? "" : String(raw);
|
|
1493
|
+
});
|
|
1494
|
+
const rowActionField = String(widget.rowActionField || "").trim();
|
|
1495
|
+
if (rowActionField) {
|
|
1496
|
+
const valuePath = String(widget.rowActionValuePath || "").trim() || "id";
|
|
1497
|
+
const rawValue = resolvePath(item, valuePath);
|
|
1498
|
+
fields[rowActionField] = rawValue == null ? "" : String(rawValue);
|
|
1499
|
+
}
|
|
1500
|
+
const hasFields = Object.keys(fields).length > 0;
|
|
1501
|
+
const rowAction = widget.rowAction;
|
|
1502
|
+
const lineIndex = ctx.lines.length;
|
|
1503
|
+
ctx.lines.push({
|
|
1504
|
+
text: interpolate(widget.rowTemplate, {
|
|
1505
|
+
...vars,
|
|
1506
|
+
item,
|
|
1507
|
+
index: index + 1
|
|
1508
|
+
}),
|
|
1509
|
+
widgetId: widget.id,
|
|
1510
|
+
selectable: Boolean(rowAction),
|
|
1511
|
+
target: rowAction?.type === "nav.goto" ? rowAction.target : void 0,
|
|
1512
|
+
action: rowAction && rowAction.type !== "nav.goto" ? rowAction : void 0,
|
|
1513
|
+
fields: hasFields ? fields : void 0
|
|
1514
|
+
});
|
|
1515
|
+
if (rowAction) {
|
|
1516
|
+
ctx.actionLines.push({
|
|
1517
|
+
index: lineIndex,
|
|
1518
|
+
selectable: true,
|
|
1519
|
+
target: rowAction.type === "nav.goto" ? rowAction.target : void 0,
|
|
1520
|
+
action: rowAction.type !== "nav.goto" ? rowAction : void 0,
|
|
1521
|
+
fields: hasFields ? fields : void 0
|
|
1522
|
+
});
|
|
1523
|
+
}
|
|
1524
|
+
});
|
|
1525
|
+
const listDataRowsEmitted = listVisibleRows.length;
|
|
1526
|
+
for (let pad = listDataRowsEmitted; pad < listMaxVisible; pad++) {
|
|
1527
|
+
ctx.lines.push({ text: "", widgetId: widget.id });
|
|
1528
|
+
}
|
|
1529
|
+
if (listScrollable) {
|
|
1530
|
+
const listBelowCount = rows.length - listOffset - listVisibleRows.length;
|
|
1531
|
+
ctx.lines.push({ text: listBelowCount > 0 ? `\u2193 ${listBelowCount} more` : "", widgetId: widget.id });
|
|
1532
|
+
}
|
|
1533
|
+
if (rows.length > listMaxVisible) {
|
|
1534
|
+
ctx.listScrollInfo[widget.id] = { totalRows: rows.length, visibleRows: listVisibleRows.length, offset: listOffset };
|
|
1535
|
+
}
|
|
1536
|
+
const marginBottom = Math.max(0, Number(widget.marginBottom ?? 1));
|
|
1537
|
+
if (marginBottom) pushSpacerLines(marginBottom);
|
|
1538
|
+
}
|
|
1539
|
+
});
|
|
1540
|
+
return ctx;
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
// src/ui/useRunnerRenderSource.ts
|
|
1544
|
+
var useRunnerRenderSource = (screen, listScrollOffsets) => {
|
|
1545
|
+
const result = useMemo2(() => {
|
|
1546
|
+
if (!screen?.screenSpec || !screen?.state) return { screen, listScrollInfo: void 0 };
|
|
1547
|
+
const ctx = collectScreenSpecContext(screen.screenSpec, {
|
|
1548
|
+
activeScreenId: screen.state.activeScreenId || screen.screen,
|
|
1549
|
+
fields: screen.state.fields || {},
|
|
1550
|
+
fieldErrors: screen.state.fieldErrors || {},
|
|
1551
|
+
data: screen.state.data || {},
|
|
1552
|
+
statusLine: screen.state.statusLine || screen.statusLine
|
|
1553
|
+
}, { hideHotkeys: false, listScrollOffsets });
|
|
1554
|
+
const lines = ctx.lines.map((line) => line.text);
|
|
1555
|
+
const lineWidgetIds = ctx.lines.map((line) => line.widgetId);
|
|
1556
|
+
const fieldLineIndexById = /* @__PURE__ */ new Map();
|
|
1557
|
+
ctx.lines.forEach((line, idx) => {
|
|
1558
|
+
if (line.fieldId && !fieldLineIndexById.has(line.fieldId)) {
|
|
1559
|
+
fieldLineIndexById.set(line.fieldId, idx);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1562
|
+
const actionLines = [];
|
|
1563
|
+
ctx.lines.forEach((line, idx) => {
|
|
1564
|
+
if (!(line.target || line.aidAlias || line.action)) return;
|
|
1565
|
+
actionLines.push({
|
|
1566
|
+
index: idx,
|
|
1567
|
+
selectable: line.selectable,
|
|
1568
|
+
target: line.target,
|
|
1569
|
+
aidAlias: line.aidAlias,
|
|
1570
|
+
action: line.action,
|
|
1571
|
+
fields: line.fields
|
|
1572
|
+
});
|
|
1573
|
+
});
|
|
1574
|
+
const inputFields = ctx.inputFields.map((field) => ({
|
|
1575
|
+
...field,
|
|
1576
|
+
lineIndex: fieldLineIndexById.get(field.id)
|
|
1577
|
+
}));
|
|
1578
|
+
const firstMatch = ctx.firstInput ? inputFields.find((f) => f.id === ctx.firstInput?.id) : void 0;
|
|
1579
|
+
const inputField = ctx.firstInput ? {
|
|
1580
|
+
id: ctx.firstInput.id,
|
|
1581
|
+
value: screen.state.fields?.[ctx.firstInput.id] || "",
|
|
1582
|
+
maxLength: ctx.firstInput.maxLength,
|
|
1583
|
+
label: firstMatch?.label,
|
|
1584
|
+
lineIndex: fieldLineIndexById.get(ctx.firstInput.id),
|
|
1585
|
+
rows: firstMatch?.rows,
|
|
1586
|
+
action: firstMatch?.action
|
|
1587
|
+
} : void 0;
|
|
1588
|
+
return {
|
|
1589
|
+
screen: {
|
|
1590
|
+
...screen,
|
|
1591
|
+
lines,
|
|
1592
|
+
actionLines,
|
|
1593
|
+
inputFields,
|
|
1594
|
+
inputField,
|
|
1595
|
+
shortcuts: ctx.shortcuts,
|
|
1596
|
+
statusLine: screen.state.statusLine || screen.statusLine
|
|
1597
|
+
},
|
|
1598
|
+
listScrollInfo: ctx.listScrollInfo,
|
|
1599
|
+
lineWidgetIds
|
|
1600
|
+
};
|
|
1601
|
+
}, [screen, listScrollOffsets]);
|
|
1602
|
+
return result;
|
|
1603
|
+
};
|
|
1604
|
+
|
|
1605
|
+
// src/ui/runner.tsx
|
|
1606
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
1607
|
+
var Runner = ({
|
|
1608
|
+
projectId,
|
|
1609
|
+
versionId,
|
|
1610
|
+
terminalUrl,
|
|
1611
|
+
idToken,
|
|
1612
|
+
firebaseProjectId,
|
|
1613
|
+
appCheckToken,
|
|
1614
|
+
onBackToApps,
|
|
1615
|
+
onExit
|
|
1616
|
+
}) => {
|
|
1617
|
+
const { screen: rawScreen, error, connecting, sessionId, sendMessage } = useRunnerSession({
|
|
1618
|
+
projectId,
|
|
1619
|
+
versionId,
|
|
1620
|
+
terminalUrl,
|
|
1621
|
+
idToken
|
|
1622
|
+
});
|
|
1623
|
+
const [listScrollOffsets, setListScrollOffsets] = useState3({});
|
|
1624
|
+
const { screen, listScrollInfo, lineWidgetIds } = useRunnerRenderSource(rawScreen, listScrollOffsets);
|
|
1625
|
+
const [inputMode, setInputMode] = useState3(false);
|
|
1626
|
+
const [activeInputIndex, setActiveInputIndex] = useState3(0);
|
|
1627
|
+
const [fieldValues, setFieldValues] = useState3({});
|
|
1628
|
+
const [aiMode, setAiMode] = useState3(false);
|
|
1629
|
+
const [aiMessages, setAiMessages] = useState3([]);
|
|
1630
|
+
const [aiInput, setAiInput] = useState3("");
|
|
1631
|
+
const [aiLoading, setAiLoading] = useState3(false);
|
|
1632
|
+
const aiRequestRef = useRef2(0);
|
|
1633
|
+
const [aiConversationId, setAiConversationId] = useState3(null);
|
|
1634
|
+
const aiMessageSeqRef = useRef2(0);
|
|
1635
|
+
const [aiHistoryLoading, setAiHistoryLoading] = useState3(false);
|
|
1636
|
+
useEffect2(() => {
|
|
1637
|
+
let cancelled = false;
|
|
1638
|
+
const vid = versionId || "published";
|
|
1639
|
+
setAiHistoryLoading(true);
|
|
1640
|
+
loadLatestConversation({ firebaseProjectId, idToken, appCheckToken, projectId, versionId: vid }).then((result) => {
|
|
1641
|
+
if (cancelled) return;
|
|
1642
|
+
if (result) {
|
|
1643
|
+
setAiConversationId(result.conversationId);
|
|
1644
|
+
setAiMessages(result.messages);
|
|
1645
|
+
aiMessageSeqRef.current = result.messageCount;
|
|
1646
|
+
}
|
|
1647
|
+
}).catch(() => {
|
|
1648
|
+
}).finally(() => {
|
|
1649
|
+
if (!cancelled) setAiHistoryLoading(false);
|
|
1650
|
+
});
|
|
1651
|
+
return () => {
|
|
1652
|
+
cancelled = true;
|
|
1653
|
+
};
|
|
1654
|
+
}, [firebaseProjectId, idToken, appCheckToken, projectId, versionId]);
|
|
1655
|
+
const sendAiMessage = async (text) => {
|
|
1656
|
+
if (!text.trim() || aiLoading) return;
|
|
1657
|
+
const userMsg = { role: "user", text: text.trim() };
|
|
1658
|
+
const updatedMessages = [...aiMessages, userMsg];
|
|
1659
|
+
setAiMessages(updatedMessages);
|
|
1660
|
+
setAiInput("");
|
|
1661
|
+
setAiLoading(true);
|
|
1662
|
+
const requestId = ++aiRequestRef.current;
|
|
1663
|
+
const vid = versionId || "published";
|
|
1664
|
+
const persistOpts = { firebaseProjectId, idToken, appCheckToken, projectId, versionId: vid };
|
|
1665
|
+
try {
|
|
1666
|
+
let convoId = aiConversationId;
|
|
1667
|
+
if (!convoId) {
|
|
1668
|
+
convoId = await createConversation(persistOpts);
|
|
1669
|
+
setAiConversationId(convoId);
|
|
1670
|
+
}
|
|
1671
|
+
const userSeq = ++aiMessageSeqRef.current;
|
|
1672
|
+
appendMessage({ ...persistOpts, conversationId: convoId, role: "user", text: text.trim(), seq: userSeq }).catch(() => {
|
|
1673
|
+
});
|
|
1674
|
+
const response = await callUnifiedChat({
|
|
1675
|
+
firebaseProjectId,
|
|
1676
|
+
idToken,
|
|
1677
|
+
appCheckToken,
|
|
1678
|
+
payload: {
|
|
1679
|
+
projectId,
|
|
1680
|
+
versionId: vid,
|
|
1681
|
+
message: text.trim(),
|
|
1682
|
+
screenId: rawScreen?.screen,
|
|
1683
|
+
conversation: updatedMessages
|
|
1684
|
+
}
|
|
1685
|
+
});
|
|
1686
|
+
if (requestId !== aiRequestRef.current) return;
|
|
1687
|
+
const newMessages = [];
|
|
1688
|
+
if (response.operations?.length) {
|
|
1689
|
+
for (const op of response.operations) {
|
|
1690
|
+
const isError = op.result && typeof op.result === "object" && "error" in op.result;
|
|
1691
|
+
const summary = isError ? `${op.tool}: error - ${op.result.error}` : `${op.tool}: OK`;
|
|
1692
|
+
const toolMsg = { role: "tool", text: summary, toolName: op.tool };
|
|
1693
|
+
newMessages.push(toolMsg);
|
|
1694
|
+
const toolSeq = ++aiMessageSeqRef.current;
|
|
1695
|
+
appendMessage({ ...persistOpts, conversationId: convoId, role: "tool", text: summary, toolName: op.tool, seq: toolSeq }).catch(() => {
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
const assistantMsg = { role: "assistant", text: response.text };
|
|
1700
|
+
newMessages.push(assistantMsg);
|
|
1701
|
+
const assistantSeq = ++aiMessageSeqRef.current;
|
|
1702
|
+
appendMessage({ ...persistOpts, conversationId: convoId, role: "assistant", text: response.text, seq: assistantSeq }).catch(() => {
|
|
1703
|
+
});
|
|
1704
|
+
setAiMessages((prev) => [...prev, ...newMessages]);
|
|
1705
|
+
} catch (err) {
|
|
1706
|
+
if (requestId !== aiRequestRef.current) return;
|
|
1707
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
1708
|
+
setAiMessages((prev) => [...prev, { role: "assistant", text: `Error: ${errMsg}` }]);
|
|
1709
|
+
} finally {
|
|
1710
|
+
if (requestId === aiRequestRef.current) setAiLoading(false);
|
|
1711
|
+
}
|
|
1712
|
+
};
|
|
1713
|
+
const inputFields = useMemo3(() => {
|
|
1714
|
+
if (!screen) return [];
|
|
1715
|
+
if (screen.inputFields && screen.inputFields.length > 0) return screen.inputFields;
|
|
1716
|
+
if (screen.inputField) return [screen.inputField];
|
|
1717
|
+
return [];
|
|
1718
|
+
}, [screen]);
|
|
1719
|
+
const selectableLines = useMemo3(() => {
|
|
1720
|
+
return (screen?.actionLines || []).filter((line) => line.selectable && (line.target || line.aidAlias || line.action)).sort((a, b) => a.index - b.index);
|
|
1721
|
+
}, [screen?.actionLines]);
|
|
1722
|
+
const shortcutByKey = useMemo3(() => {
|
|
1723
|
+
const map = /* @__PURE__ */ new Map();
|
|
1724
|
+
for (const s of screen?.shortcuts || []) {
|
|
1725
|
+
const k = String(s.key || "").trim().toLowerCase();
|
|
1726
|
+
if (k) map.set(k, s);
|
|
1727
|
+
}
|
|
1728
|
+
return map;
|
|
1729
|
+
}, [screen?.shortcuts]);
|
|
1730
|
+
const [selectedLineIndex, setSelectedLineIndex] = useState3(0);
|
|
1731
|
+
useEffect2(() => {
|
|
1732
|
+
setSelectedLineIndex(0);
|
|
1733
|
+
setListScrollOffsets({});
|
|
1734
|
+
}, [screen?.sessionId]);
|
|
1735
|
+
useEffect2(() => {
|
|
1736
|
+
if (selectableLines.length > 0) {
|
|
1737
|
+
setSelectedLineIndex((prev) => Math.min(prev, selectableLines.length - 1));
|
|
1738
|
+
}
|
|
1739
|
+
}, [selectableLines.length]);
|
|
1740
|
+
useEffect2(() => {
|
|
1741
|
+
if (!inputFields.length) {
|
|
1742
|
+
setInputMode(false);
|
|
1743
|
+
setActiveInputIndex(0);
|
|
1744
|
+
return;
|
|
1745
|
+
}
|
|
1746
|
+
setActiveInputIndex((prev) => Math.min(prev, inputFields.length - 1));
|
|
1747
|
+
}, [inputFields.length]);
|
|
1748
|
+
useEffect2(() => {
|
|
1749
|
+
if (!inputFields.length) return;
|
|
1750
|
+
setFieldValues((prev) => {
|
|
1751
|
+
const next = { ...prev };
|
|
1752
|
+
for (const field of inputFields) {
|
|
1753
|
+
const isNonEditable = (field.maxLength ?? 0) === 0;
|
|
1754
|
+
if (isNonEditable || next[field.id] === void 0) {
|
|
1755
|
+
next[field.id] = String(field.value || "");
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return next;
|
|
1759
|
+
});
|
|
1760
|
+
}, [inputFields]);
|
|
1761
|
+
const collectFields = (extra) => {
|
|
1762
|
+
return { ...fieldValues, ...extra };
|
|
1763
|
+
};
|
|
1764
|
+
const applySelectedLine = (line) => {
|
|
1765
|
+
const fields = collectFields(line.fields);
|
|
1766
|
+
if (line.target) {
|
|
1767
|
+
sendMessage({ type: "nav", target: line.target, fields });
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (line.aidAlias) {
|
|
1771
|
+
sendMessage({ type: "aid", key: line.aidAlias, fields });
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
if (line.action) {
|
|
1775
|
+
sendMessage({ type: "action", action: line.action, fields });
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
const executeShortcut = (shortcut) => {
|
|
1779
|
+
const fields = collectFields(shortcut.fields);
|
|
1780
|
+
if (shortcut.target) {
|
|
1781
|
+
sendMessage({ type: "nav", target: shortcut.target, fields });
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (shortcut.action) {
|
|
1785
|
+
sendMessage({ type: "action", action: shortcut.action, fields });
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
if (shortcut.aidAlias) {
|
|
1789
|
+
sendMessage({ type: "aid", key: shortcut.aidAlias, fields });
|
|
1790
|
+
}
|
|
1791
|
+
};
|
|
1792
|
+
const toggleActiveCheckbox = () => {
|
|
1793
|
+
const activeField2 = inputFields[activeInputIndex];
|
|
1794
|
+
if (!activeField2) return;
|
|
1795
|
+
const toggleAction = activeField2.action ?? { type: "data.toggleFlag", path: activeField2.id };
|
|
1796
|
+
sendMessage({ type: "action", action: toggleAction, fields: collectFields() });
|
|
1797
|
+
};
|
|
1798
|
+
const activeField = inputFields[activeInputIndex] ?? null;
|
|
1799
|
+
const isEditable = activeField != null && (activeField.maxLength ?? 0) > 0;
|
|
1800
|
+
const isMultiLine = isEditable && (activeField.rows ?? 1) > 1;
|
|
1801
|
+
useInput2((input, key) => {
|
|
1802
|
+
if (key.ctrl && input === "c") {
|
|
1803
|
+
onExit();
|
|
1804
|
+
return;
|
|
1805
|
+
}
|
|
1806
|
+
if (!screen) return;
|
|
1807
|
+
if (aiMode) {
|
|
1808
|
+
if (key.escape) {
|
|
1809
|
+
setAiMode(false);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
if (key.backspace || key.delete) {
|
|
1813
|
+
setAiInput((prev) => prev.slice(0, -1));
|
|
1814
|
+
return;
|
|
1815
|
+
}
|
|
1816
|
+
if (key.return && !aiLoading) {
|
|
1817
|
+
void sendAiMessage(aiInput);
|
|
1818
|
+
return;
|
|
1819
|
+
}
|
|
1820
|
+
if (input && !key.ctrl && !key.meta) {
|
|
1821
|
+
setAiInput((prev) => prev + input);
|
|
1822
|
+
}
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (inputMode) {
|
|
1826
|
+
if (!activeField) return;
|
|
1827
|
+
if (key.escape) {
|
|
1828
|
+
sendMessage({ type: "aid", key: "B", fields: fieldValues });
|
|
1829
|
+
setInputMode(false);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (!isEditable) {
|
|
1833
|
+
if (key.tab || input === " " || key.downArrow || key.rightArrow) {
|
|
1834
|
+
setActiveInputIndex((prev) => (prev + 1) % inputFields.length);
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
if (key.upArrow || key.leftArrow) {
|
|
1838
|
+
setActiveInputIndex(
|
|
1839
|
+
(prev) => inputFields.length ? (prev - 1 + inputFields.length) % inputFields.length : 0
|
|
1840
|
+
);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (key.return || input === " ") {
|
|
1844
|
+
toggleActiveCheckbox();
|
|
1845
|
+
return;
|
|
1846
|
+
}
|
|
1847
|
+
if (input) {
|
|
1848
|
+
const shortcut = shortcutByKey.get(input.toLowerCase());
|
|
1849
|
+
if (shortcut) {
|
|
1850
|
+
executeShortcut(shortcut);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
if (key.tab || input === " ") {
|
|
1857
|
+
sendMessage({ type: "aid", key: "ENTER", fields: fieldValues });
|
|
1858
|
+
setInputMode(false);
|
|
1859
|
+
return;
|
|
1860
|
+
}
|
|
1861
|
+
if (key.backspace || key.delete) {
|
|
1862
|
+
setFieldValues((prev) => ({
|
|
1863
|
+
...prev,
|
|
1864
|
+
[activeField.id]: (prev[activeField.id] || "").slice(0, -1)
|
|
1865
|
+
}));
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
if (key.return) {
|
|
1869
|
+
if (isMultiLine) {
|
|
1870
|
+
setFieldValues((prev) => {
|
|
1871
|
+
const current = prev[activeField.id] || "";
|
|
1872
|
+
if (current.length >= activeField.maxLength) return prev;
|
|
1873
|
+
return { ...prev, [activeField.id]: current + "\n" };
|
|
1874
|
+
});
|
|
1875
|
+
return;
|
|
1876
|
+
}
|
|
1877
|
+
sendMessage({ type: "aid", key: "ENTER", fields: fieldValues });
|
|
1878
|
+
setInputMode(false);
|
|
1879
|
+
return;
|
|
1880
|
+
}
|
|
1881
|
+
if (input) {
|
|
1882
|
+
setFieldValues((prev) => {
|
|
1883
|
+
const current = prev[activeField.id] || "";
|
|
1884
|
+
if (current.length >= activeField.maxLength) return prev;
|
|
1885
|
+
return { ...prev, [activeField.id]: current + input };
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
if (input === "x" || input === "q") {
|
|
1891
|
+
onBackToApps();
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (key.escape) {
|
|
1895
|
+
sendMessage({ type: "aid", key: "B", fields: fieldValues });
|
|
1896
|
+
return;
|
|
1897
|
+
}
|
|
1898
|
+
if (key.return && (key.ctrl || key.meta)) {
|
|
1899
|
+
sendMessage({ type: "aid", key: "ENTER_BACK", fields: fieldValues });
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
1902
|
+
if (input === " " && activeField && !isEditable) {
|
|
1903
|
+
toggleActiveCheckbox();
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (key.return) {
|
|
1907
|
+
if (activeField && !isEditable) {
|
|
1908
|
+
toggleActiveCheckbox();
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
if (inputFields.length > 0 && isEditable) {
|
|
1912
|
+
setInputMode(true);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
const line = selectableLines[selectedLineIndex];
|
|
1916
|
+
if (line) {
|
|
1917
|
+
applySelectedLine(line);
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
sendMessage({ type: "aid", key: "ENTER", fields: fieldValues });
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
if (key.pageUp || key.pageDown) {
|
|
1924
|
+
const delta = key.pageDown ? 1 : -1;
|
|
1925
|
+
const currentLine = selectableLines[selectedLineIndex];
|
|
1926
|
+
const wId = currentLine ? lineWidgetIds?.[currentLine.index] : void 0;
|
|
1927
|
+
if (wId && listScrollInfo?.[wId]) {
|
|
1928
|
+
const info = listScrollInfo[wId];
|
|
1929
|
+
const scrollAmount = delta * info.visibleRows;
|
|
1930
|
+
const newOffset = Math.max(0, Math.min(info.offset + scrollAmount, info.totalRows - info.visibleRows));
|
|
1931
|
+
if (newOffset !== info.offset) {
|
|
1932
|
+
setListScrollOffsets((prev) => ({ ...prev, [wId]: newOffset }));
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) {
|
|
1938
|
+
const arrowMap = {
|
|
1939
|
+
// ink key names aren't available as event.key, so we check the boolean flags
|
|
1940
|
+
};
|
|
1941
|
+
let mapped;
|
|
1942
|
+
if (key.leftArrow) mapped = "h";
|
|
1943
|
+
else if (key.rightArrow) mapped = "l";
|
|
1944
|
+
else if (key.upArrow) mapped = "k";
|
|
1945
|
+
else if (key.downArrow) mapped = "j";
|
|
1946
|
+
if (mapped) {
|
|
1947
|
+
const shortcut = shortcutByKey.get(mapped);
|
|
1948
|
+
if (shortcut) {
|
|
1949
|
+
executeShortcut(shortcut);
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
if (key.upArrow || key.downArrow) {
|
|
1954
|
+
const delta = key.downArrow ? 1 : -1;
|
|
1955
|
+
if (inputFields.length > 0) {
|
|
1956
|
+
setActiveInputIndex(
|
|
1957
|
+
(prev) => (prev + delta + inputFields.length) % inputFields.length
|
|
1958
|
+
);
|
|
1959
|
+
} else if (selectableLines.length > 0) {
|
|
1960
|
+
const nextIdx = selectedLineIndex + delta;
|
|
1961
|
+
if (nextIdx < 0 || nextIdx >= selectableLines.length) {
|
|
1962
|
+
const currentLine = selectableLines[selectedLineIndex];
|
|
1963
|
+
const wId = currentLine ? lineWidgetIds?.[currentLine.index] : void 0;
|
|
1964
|
+
if (wId && listScrollInfo?.[wId]) {
|
|
1965
|
+
const info = listScrollInfo[wId];
|
|
1966
|
+
if (delta > 0 && info.offset + info.visibleRows < info.totalRows) {
|
|
1967
|
+
setListScrollOffsets((prev) => ({ ...prev, [wId]: info.offset + 1 }));
|
|
1968
|
+
return;
|
|
1969
|
+
}
|
|
1970
|
+
if (delta < 0 && info.offset > 0) {
|
|
1971
|
+
setListScrollOffsets((prev) => ({ ...prev, [wId]: info.offset - 1 }));
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
setSelectedLineIndex(
|
|
1977
|
+
(prev) => (prev + delta + selectableLines.length) % selectableLines.length
|
|
1978
|
+
);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
if ((key.tab || input === " " || input === "i") && inputFields.length > 0) {
|
|
1984
|
+
setInputMode(true);
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
if (input) {
|
|
1988
|
+
const shortcut = shortcutByKey.get(input.toLowerCase());
|
|
1989
|
+
if (shortcut) {
|
|
1990
|
+
executeShortcut(shortcut);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
if (input === "/" && !shortcutByKey.has("/")) {
|
|
1995
|
+
setAiMode(true);
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
if (input === "b") {
|
|
1999
|
+
sendMessage({ type: "aid", key: "B", fields: fieldValues });
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (input === "c") {
|
|
2003
|
+
sendMessage({ type: "aid", key: "C", fields: fieldValues });
|
|
2004
|
+
return;
|
|
2005
|
+
}
|
|
2006
|
+
});
|
|
2007
|
+
const fieldLineMap = useMemo3(() => {
|
|
2008
|
+
const map = /* @__PURE__ */ new Map();
|
|
2009
|
+
for (const field of inputFields) {
|
|
2010
|
+
if (typeof field.lineIndex !== "number") continue;
|
|
2011
|
+
const rows = Math.max(1, Number(field.rows ?? 1));
|
|
2012
|
+
const lineStep = Math.max(1, Number(field.lineStep ?? 1));
|
|
2013
|
+
for (let r = 0; r < rows; r++) {
|
|
2014
|
+
map.set(field.lineIndex + r * lineStep, {
|
|
2015
|
+
fieldId: field.id,
|
|
2016
|
+
rowOffset: r,
|
|
2017
|
+
label: field.label || field.id,
|
|
2018
|
+
rows
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
return map;
|
|
2023
|
+
}, [inputFields]);
|
|
2024
|
+
const displayLines = useMemo3(() => {
|
|
2025
|
+
if (!screen) return [];
|
|
2026
|
+
const lines = [...screen.lines || []];
|
|
2027
|
+
if (!inputMode || !activeField || !isEditable) return lines;
|
|
2028
|
+
const fieldId = activeField.id;
|
|
2029
|
+
const editValue = fieldValues[fieldId] || "";
|
|
2030
|
+
const label = activeField.label || activeField.id;
|
|
2031
|
+
const labelPrefix = label ? `${label}: ` : "";
|
|
2032
|
+
const indent = " ".repeat(labelPrefix.length);
|
|
2033
|
+
const rows = Math.max(1, Number(activeField.rows ?? 1));
|
|
2034
|
+
const lineStep = Math.max(1, Number(activeField.lineStep ?? 1));
|
|
2035
|
+
if (typeof activeField.lineIndex !== "number") return lines;
|
|
2036
|
+
const editLines = editValue.split(/\r?\n/);
|
|
2037
|
+
const paddedEditLines = editLines.slice(0, rows);
|
|
2038
|
+
while (paddedEditLines.length < rows) paddedEditLines.push("");
|
|
2039
|
+
for (let r = 0; r < rows; r++) {
|
|
2040
|
+
const lineIdx = activeField.lineIndex + r * lineStep;
|
|
2041
|
+
if (lineIdx >= 0 && lineIdx < lines.length) {
|
|
2042
|
+
const prefix = r === 0 ? labelPrefix : indent;
|
|
2043
|
+
const text = paddedEditLines[r] || "";
|
|
2044
|
+
const cursor = r === editLines.length - 1 || r === rows - 1 && editLines.length >= rows ? "\u2588" : "";
|
|
2045
|
+
lines[lineIdx] = `${prefix}${text}${cursor}`;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
return lines;
|
|
2049
|
+
}, [screen, inputMode, activeField, isEditable, fieldValues, inputFields]);
|
|
2050
|
+
const hintText = useMemo3(() => {
|
|
2051
|
+
if (aiMode) return "Type message, Enter=send, Esc=back to app";
|
|
2052
|
+
if (!activeField) return "Enter=select, /=AI, b=back, Tab/i=edit, arrows=nav, x=apps";
|
|
2053
|
+
if (inputMode && isEditable) {
|
|
2054
|
+
return isMultiLine ? "Type to edit, Enter=newline, Tab=submit, Esc=cancel" : "Type to edit, Enter=submit, Tab=submit, Esc=cancel";
|
|
2055
|
+
}
|
|
2056
|
+
if (isEditable) return "Enter=edit, Tab=next field, arrows=nav, b=back, x=apps";
|
|
2057
|
+
return "Enter/Space=toggle, arrows=nav, b=back, x=apps";
|
|
2058
|
+
}, [activeField, aiMode, inputMode, isEditable, isMultiLine]);
|
|
2059
|
+
if (error) {
|
|
2060
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2061
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "red", children: [
|
|
2062
|
+
"Runner error: ",
|
|
2063
|
+
error
|
|
2064
|
+
] }),
|
|
2065
|
+
/* @__PURE__ */ jsx2(Text2, { children: "Press x to return to apps, or Ctrl+C to exit." })
|
|
2066
|
+
] });
|
|
2067
|
+
}
|
|
2068
|
+
if (connecting || !screen) {
|
|
2069
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsx2(Text2, { color: "green", children: "Starting runner..." }) });
|
|
2070
|
+
}
|
|
2071
|
+
const selectedLine = selectableLines[selectedLineIndex];
|
|
2072
|
+
if (aiMode) {
|
|
2073
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2074
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
2075
|
+
"AI CHAT | project=",
|
|
2076
|
+
projectId,
|
|
2077
|
+
" | screen=",
|
|
2078
|
+
rawScreen?.screen || "-"
|
|
2079
|
+
] }),
|
|
2080
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
2081
|
+
aiHistoryLoading && /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "Loading conversation..." }),
|
|
2082
|
+
aiMessages.length === 0 && !aiLoading && !aiHistoryLoading && /* @__PURE__ */ jsx2(Text2, { color: "gray", children: "Ask anything about this app or screen." }),
|
|
2083
|
+
aiMessages.map((msg, idx) => {
|
|
2084
|
+
const prefix = msg.role === "user" ? "You: " : msg.role === "tool" ? `TOOL(${msg.toolName || "?"}): ` : "AI: ";
|
|
2085
|
+
const color = msg.role === "user" ? "cyan" : msg.role === "tool" ? "yellow" : "white";
|
|
2086
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", children: /* @__PURE__ */ jsxs2(Text2, { color, wrap: "wrap", children: [
|
|
2087
|
+
prefix,
|
|
2088
|
+
msg.text
|
|
2089
|
+
] }) }, idx);
|
|
2090
|
+
}),
|
|
2091
|
+
aiLoading && /* @__PURE__ */ jsx2(Text2, { color: "yellow", children: "Thinking..." })
|
|
2092
|
+
] }),
|
|
2093
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
2094
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
2095
|
+
"> ",
|
|
2096
|
+
aiInput,
|
|
2097
|
+
/* @__PURE__ */ jsx2(Text2, { color: "green", children: "\u2588" })
|
|
2098
|
+
] }),
|
|
2099
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: hintText })
|
|
2100
|
+
] })
|
|
2101
|
+
] });
|
|
2102
|
+
}
|
|
2103
|
+
return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
|
|
2104
|
+
/* @__PURE__ */ jsxs2(Text2, { color: "green", children: [
|
|
2105
|
+
"RUNNER | project=",
|
|
2106
|
+
projectId,
|
|
2107
|
+
" | version=",
|
|
2108
|
+
versionId || "published",
|
|
2109
|
+
" | session=",
|
|
2110
|
+
sessionId || "-"
|
|
2111
|
+
] }),
|
|
2112
|
+
/* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: 1, children: displayLines.map((line, idx) => {
|
|
2113
|
+
const match = selectableLines.findIndex((entry) => entry.index === idx);
|
|
2114
|
+
const isSelectedActionLine = match >= 0 && selectableLines[match] === selectedLine;
|
|
2115
|
+
const fieldInfo = fieldLineMap.get(idx);
|
|
2116
|
+
const isActiveInputLine = fieldInfo != null && fieldInfo.fieldId === activeField?.id;
|
|
2117
|
+
const isAnyInputLine = fieldInfo != null;
|
|
2118
|
+
const lineColor = isSelectedActionLine ? "green" : isActiveInputLine ? inputMode ? "yellow" : "cyan" : isAnyInputLine ? "cyan" : void 0;
|
|
2119
|
+
const lineBackground = isActiveInputLine && inputMode ? "blackBright" : void 0;
|
|
2120
|
+
const actionLine = match >= 0 ? selectableLines[match] : void 0;
|
|
2121
|
+
const isButton = actionLine && !actionLine.fields;
|
|
2122
|
+
const prevWidgetId = idx > 0 ? lineWidgetIds?.[idx - 1] : void 0;
|
|
2123
|
+
const curWidgetId = lineWidgetIds?.[idx];
|
|
2124
|
+
const needsTopSpace = isButton && prevWidgetId && prevWidgetId !== curWidgetId;
|
|
2125
|
+
return /* @__PURE__ */ jsx2(Box2, { flexDirection: "column", marginTop: needsTopSpace ? 1 : 0, children: /* @__PURE__ */ jsx2(
|
|
2126
|
+
Text2,
|
|
2127
|
+
{
|
|
2128
|
+
color: lineColor,
|
|
2129
|
+
backgroundColor: lineBackground,
|
|
2130
|
+
bold: isActiveInputLine,
|
|
2131
|
+
children: line
|
|
2132
|
+
}
|
|
2133
|
+
) }, `${idx}-${line}`);
|
|
2134
|
+
}) }),
|
|
2135
|
+
/* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", marginTop: 1, children: [
|
|
2136
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: screen.statusLine }),
|
|
2137
|
+
/* @__PURE__ */ jsx2(Text2, { color: "gray", children: hintText })
|
|
2138
|
+
] })
|
|
2139
|
+
] });
|
|
2140
|
+
};
|
|
2141
|
+
|
|
2142
|
+
// src/app.tsx
|
|
2143
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2144
|
+
var App = ({ config, idToken, initialQuery, onExit }) => {
|
|
2145
|
+
const [apps, setApps] = useState4([]);
|
|
2146
|
+
const [loading, setLoading] = useState4(true);
|
|
2147
|
+
const [error, setError] = useState4(null);
|
|
2148
|
+
const [selectedApp, setSelectedApp] = useState4(null);
|
|
2149
|
+
useEffect3(() => {
|
|
2150
|
+
let mounted = true;
|
|
2151
|
+
const load = async () => {
|
|
2152
|
+
try {
|
|
2153
|
+
setLoading(true);
|
|
2154
|
+
log(`Loading apps from ${config.terminalUrl}`);
|
|
2155
|
+
const results = await fetchRunnableAppsViaServer({
|
|
2156
|
+
terminalUrl: config.terminalUrl,
|
|
2157
|
+
idToken,
|
|
2158
|
+
query: initialQuery
|
|
2159
|
+
});
|
|
2160
|
+
if (!mounted) return;
|
|
2161
|
+
log(`Loaded ${results.length} apps`);
|
|
2162
|
+
setApps(results);
|
|
2163
|
+
setError(null);
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
if (!mounted) return;
|
|
2166
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
2167
|
+
} finally {
|
|
2168
|
+
if (mounted) setLoading(false);
|
|
2169
|
+
}
|
|
2170
|
+
};
|
|
2171
|
+
void load();
|
|
2172
|
+
return () => {
|
|
2173
|
+
mounted = false;
|
|
2174
|
+
};
|
|
2175
|
+
}, [config.firebaseProjectId, config.terminalUrl, idToken, initialQuery]);
|
|
2176
|
+
if (error) {
|
|
2177
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
2178
|
+
/* @__PURE__ */ jsx3(Text3, { color: "red", children: error }),
|
|
2179
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Tip: Set BLO_DEBUG=1 for detailed logs." }),
|
|
2180
|
+
/* @__PURE__ */ jsx3(Text3, { children: "Press Ctrl+C to exit." })
|
|
2181
|
+
] });
|
|
2182
|
+
}
|
|
2183
|
+
if (loading) {
|
|
2184
|
+
return /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", children: /* @__PURE__ */ jsxs3(Text3, { color: "green", children: [
|
|
2185
|
+
"Loading apps from ",
|
|
2186
|
+
config.terminalUrl,
|
|
2187
|
+
"..."
|
|
2188
|
+
] }) });
|
|
2189
|
+
}
|
|
2190
|
+
if (apps.length === 0) {
|
|
2191
|
+
return /* @__PURE__ */ jsxs3(Box3, { flexDirection: "column", children: [
|
|
2192
|
+
/* @__PURE__ */ jsx3(Text3, { color: "yellow", children: "No apps found." }),
|
|
2193
|
+
/* @__PURE__ */ jsx3(Text3, { dimColor: true, children: initialQuery ? `No apps matched "${initialQuery}". Try without a query filter.` : "Your account has no projects and no published apps are available." }),
|
|
2194
|
+
/* @__PURE__ */ jsx3(Text3, { children: "Press Ctrl+C to exit." })
|
|
2195
|
+
] });
|
|
2196
|
+
}
|
|
2197
|
+
if (!selectedApp) {
|
|
2198
|
+
return /* @__PURE__ */ jsx3(
|
|
2199
|
+
AppSearch,
|
|
2200
|
+
{
|
|
2201
|
+
apps,
|
|
2202
|
+
initialQuery,
|
|
2203
|
+
onSelect: (app) => {
|
|
2204
|
+
log(`Selected app: ${app.projectId} (version=${app.versionId}, source=${app.versionSource})`);
|
|
2205
|
+
setSelectedApp(app);
|
|
2206
|
+
},
|
|
2207
|
+
onExit
|
|
2208
|
+
}
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
return /* @__PURE__ */ jsx3(
|
|
2212
|
+
Runner,
|
|
2213
|
+
{
|
|
2214
|
+
projectId: selectedApp.projectId,
|
|
2215
|
+
versionId: selectedApp.versionId,
|
|
2216
|
+
terminalUrl: config.terminalUrl,
|
|
2217
|
+
idToken,
|
|
2218
|
+
firebaseProjectId: config.firebaseProjectId,
|
|
2219
|
+
appCheckToken: config.appCheckToken,
|
|
2220
|
+
onBackToApps: () => {
|
|
2221
|
+
log("Returning to app list");
|
|
2222
|
+
setSelectedApp(null);
|
|
2223
|
+
},
|
|
2224
|
+
onExit
|
|
2225
|
+
}
|
|
2226
|
+
);
|
|
2227
|
+
};
|
|
2228
|
+
|
|
2229
|
+
// src/config.ts
|
|
2230
|
+
import fs from "fs";
|
|
2231
|
+
import path from "path";
|
|
2232
|
+
import os from "os";
|
|
2233
|
+
var defaultWebOrigin = process.env.BLO_WEB_ORIGIN || "https://buildless.online";
|
|
2234
|
+
var defaultTerminalUrl = process.env.BLO_TERMINAL_URL || "https://buildless.online";
|
|
2235
|
+
var defaultProjectId = process.env.BLO_FIREBASE_PROJECT_ID || "buildless-online";
|
|
2236
|
+
var resolveConfigPath = () => {
|
|
2237
|
+
if (process.env.BLO_CONFIG_PATH) return process.env.BLO_CONFIG_PATH;
|
|
2238
|
+
const home = os.homedir();
|
|
2239
|
+
const configDir = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
2240
|
+
return path.join(configDir, "blo", "cli.json");
|
|
2241
|
+
};
|
|
2242
|
+
var loadConfig = () => {
|
|
2243
|
+
const configPath = resolveConfigPath();
|
|
2244
|
+
const defaults = {
|
|
2245
|
+
webOrigin: defaultWebOrigin,
|
|
2246
|
+
terminalUrl: defaultTerminalUrl,
|
|
2247
|
+
firebaseProjectId: defaultProjectId
|
|
2248
|
+
};
|
|
2249
|
+
if (!fs.existsSync(configPath)) {
|
|
2250
|
+
return { path: configPath, config: defaults };
|
|
2251
|
+
}
|
|
2252
|
+
try {
|
|
2253
|
+
const raw = fs.readFileSync(configPath, "utf8");
|
|
2254
|
+
const parsed = JSON.parse(raw);
|
|
2255
|
+
return {
|
|
2256
|
+
path: configPath,
|
|
2257
|
+
config: {
|
|
2258
|
+
// Merge persisted values (tokens) with authoritative defaults.
|
|
2259
|
+
// Service URLs and project ID always come from defaults (env or
|
|
2260
|
+
// hardcoded prod) so a stale config file can't point the
|
|
2261
|
+
// published CLI at the wrong Firebase project.
|
|
2262
|
+
...defaults,
|
|
2263
|
+
idToken: parsed.idToken,
|
|
2264
|
+
tokenExp: parsed.tokenExp,
|
|
2265
|
+
appCheckToken: parsed.appCheckToken
|
|
2266
|
+
}
|
|
2267
|
+
};
|
|
2268
|
+
} catch (err) {
|
|
2269
|
+
process.stderr.write(`Warning: failed to parse ${configPath}, using defaults: ${err instanceof Error ? err.message : String(err)}
|
|
2270
|
+
`);
|
|
2271
|
+
return { path: configPath, config: defaults };
|
|
2272
|
+
}
|
|
2273
|
+
};
|
|
2274
|
+
var saveConfig = (configPath, config) => {
|
|
2275
|
+
const dir = path.dirname(configPath);
|
|
2276
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
2277
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 384 });
|
|
2278
|
+
};
|
|
2279
|
+
|
|
2280
|
+
// src/index.tsx
|
|
2281
|
+
import { jsx as jsx4 } from "react/jsx-runtime";
|
|
2282
|
+
var ensureProjectId = (config) => {
|
|
2283
|
+
if (!config.firebaseProjectId) {
|
|
2284
|
+
throw new Error(
|
|
2285
|
+
"Could not determine project. Please reinstall: npm install -g buildless-cli"
|
|
2286
|
+
);
|
|
2287
|
+
}
|
|
2288
|
+
};
|
|
2289
|
+
var runLogin = async (config, configPath) => {
|
|
2290
|
+
log(`Starting login flow via ${config.webOrigin}/cli-auth`);
|
|
2291
|
+
const result = await startExternalLogin({ webOrigin: config.webOrigin });
|
|
2292
|
+
const payload = decodeJwtPayload(result.token);
|
|
2293
|
+
const uid = payload?.user_id ?? payload?.sub ?? "unknown";
|
|
2294
|
+
log(`Login successful: uid=${uid}, exp=${result.exp}, appCheck=${result.appCheckToken ? "yes" : "no"}`);
|
|
2295
|
+
const next = {
|
|
2296
|
+
...config,
|
|
2297
|
+
idToken: result.token,
|
|
2298
|
+
tokenExp: result.exp ?? void 0,
|
|
2299
|
+
appCheckToken: result.appCheckToken
|
|
2300
|
+
};
|
|
2301
|
+
saveConfig(configPath, next);
|
|
2302
|
+
log(`Config saved to ${configPath}`);
|
|
2303
|
+
return next;
|
|
2304
|
+
};
|
|
2305
|
+
var program = new Command();
|
|
2306
|
+
program.name("blo").description("BLO terminal client").version("0.1.0").option("-v, --verbose", "Enable verbose debug output").hook("preAction", () => {
|
|
2307
|
+
if (program.opts().verbose) {
|
|
2308
|
+
setVerbose(true);
|
|
2309
|
+
}
|
|
2310
|
+
});
|
|
2311
|
+
program.command("login").description("Authenticate via external browser").action(async () => {
|
|
2312
|
+
const { config, path: path2 } = loadConfig();
|
|
2313
|
+
log(`Config loaded from ${path2}`);
|
|
2314
|
+
ensureProjectId(config);
|
|
2315
|
+
const next = await runLogin(config, path2);
|
|
2316
|
+
process.stdout.write(`Logged in for project ${next.firebaseProjectId}.
|
|
2317
|
+
`);
|
|
50
2318
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
2319
|
+
var runAction = async (options) => {
|
|
2320
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
2321
|
+
const { config, path: path2 } = loadConfig();
|
|
2322
|
+
log(`Config loaded from ${path2}`);
|
|
2323
|
+
log(`Project: ${config.firebaseProjectId}`);
|
|
2324
|
+
log(`Terminal URL: ${config.terminalUrl}`);
|
|
2325
|
+
log(`Web origin: ${config.webOrigin}`);
|
|
2326
|
+
ensureProjectId(config);
|
|
2327
|
+
let effective = config;
|
|
2328
|
+
if (!isTokenValid(config.idToken, config.tokenExp ?? null)) {
|
|
2329
|
+
log("Token expired or missing, starting login");
|
|
2330
|
+
effective = await runLogin(config, path2);
|
|
2331
|
+
} else {
|
|
2332
|
+
const payload = decodeJwtPayload(config.idToken || "");
|
|
2333
|
+
const uid = payload?.user_id ?? payload?.sub ?? "unknown";
|
|
2334
|
+
log(`Token valid: uid=${uid}, expires=${config.tokenExp ? new Date(config.tokenExp * 1e3).toISOString() : "unknown"}`);
|
|
2335
|
+
}
|
|
2336
|
+
log(`Rendering app with query=${options.query || "(none)"}`);
|
|
2337
|
+
const { waitUntilExit } = render(
|
|
2338
|
+
/* @__PURE__ */ jsx4(
|
|
2339
|
+
App,
|
|
2340
|
+
{
|
|
2341
|
+
config: effective,
|
|
2342
|
+
idToken: effective.idToken || "",
|
|
2343
|
+
initialQuery: options.query,
|
|
2344
|
+
onExit: () => process.exit(0)
|
|
2345
|
+
}
|
|
2346
|
+
)
|
|
2347
|
+
);
|
|
2348
|
+
await waitUntilExit();
|
|
2349
|
+
};
|
|
2350
|
+
program.command("run").description("Search and run a published app").option("-q, --query <query>", "Initial search query").action(runAction);
|
|
2351
|
+
program.command("list").description("List published apps (interactive)").option("-q, --query <query>", "Initial search query").action(runAction);
|
|
2352
|
+
var main = async () => {
|
|
2353
|
+
if (process.argv.length <= 2) {
|
|
2354
|
+
await program.parseAsync([process.argv[0], process.argv[1], "run"]);
|
|
2355
|
+
return;
|
|
2356
|
+
}
|
|
2357
|
+
await program.parseAsync(process.argv);
|
|
89
2358
|
};
|
|
90
2359
|
main().catch((err) => {
|
|
91
|
-
|
|
92
|
-
|
|
2360
|
+
process.stderr.write(`${err instanceof Error ? err.message : String(err)}
|
|
2361
|
+
`);
|
|
2362
|
+
process.exit(1);
|
|
93
2363
|
});
|
|
94
2364
|
//# sourceMappingURL=index.js.map
|