@kizenapps/cli 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/chunk-4Z6S6LWP.js +6134 -0
  2. package/dist/chunk-4Z6S6LWP.js.map +1 -0
  3. package/dist/chunk-HJPLDI45.js +6134 -0
  4. package/dist/chunk-HJPLDI45.js.map +1 -0
  5. package/dist/chunk-LSHHXCC6.js +3639 -0
  6. package/dist/chunk-LSHHXCC6.js.map +1 -0
  7. package/dist/chunk-SEGVTWSK.js +44 -0
  8. package/dist/chunk-SEGVTWSK.js.map +1 -0
  9. package/dist/chunk-ZWE3DS7E.js +39 -0
  10. package/dist/chunk-ZWE3DS7E.js.map +1 -0
  11. package/dist/devtools-LZRAH4XS.js +90 -0
  12. package/dist/devtools-LZRAH4XS.js.map +1 -0
  13. package/dist/dist-C3F6UOWF.js +8472 -0
  14. package/dist/dist-C3F6UOWF.js.map +1 -0
  15. package/dist/dist-NUJOSN7W.js +8472 -0
  16. package/dist/dist-NUJOSN7W.js.map +1 -0
  17. package/dist/dist-QHDMIBVE.js +135 -0
  18. package/dist/dist-QHDMIBVE.js.map +1 -0
  19. package/dist/dist-ZIZQOSY3.js +135 -0
  20. package/dist/dist-ZIZQOSY3.js.map +1 -0
  21. package/dist/gzip-size-K37OQCQ2.js +137 -0
  22. package/dist/gzip-size-K37OQCQ2.js.map +1 -0
  23. package/dist/gzip-size-QRGIYYEQ.js +137 -0
  24. package/dist/gzip-size-QRGIYYEQ.js.map +1 -0
  25. package/dist/index.js +575 -552
  26. package/dist/index.js.map +1 -1
  27. package/dist/viewer/assets/AppDetailPage-BJrg5Wsz.js +1 -0
  28. package/dist/viewer/assets/{BootstrapContext-CEWgv2oI.js → BootstrapContext-D3lhzWap.js} +1 -1
  29. package/dist/viewer/assets/Card-CPMAuqAb.js +1 -0
  30. package/dist/viewer/assets/ConfigurationPage-D8I8rWjF.js +1 -0
  31. package/dist/viewer/assets/SandboxPage-ByixszHi.js +1 -0
  32. package/dist/viewer/assets/{VersionsPage-DOd2Hv8M.js → VersionsPage-BKul6bXI.js} +1 -1
  33. package/dist/viewer/assets/index-BoSQQwMQ.css +2 -0
  34. package/dist/viewer/assets/index-Bomh3C3X.js +5 -0
  35. package/dist/viewer/assets/useObjectLookups-Cx6A7PGj.js +14 -0
  36. package/dist/viewer/index.html +4 -4
  37. package/package.json +10 -10
  38. package/dist/viewer/assets/AppDetailPage-ObugMPkn.js +0 -1
  39. package/dist/viewer/assets/Card-BSwG5koM.js +0 -1
  40. package/dist/viewer/assets/ConfigurationPage-SnFNzZgJ.js +0 -1
  41. package/dist/viewer/assets/SandboxPage-BFVVe-i3.js +0 -1
  42. package/dist/viewer/assets/configStorage-Bb-A9Nvj.js +0 -14
  43. package/dist/viewer/assets/index-B-4D4w_w.js +0 -5
  44. package/dist/viewer/assets/index-D6Un_G7k.css +0 -2
package/dist/index.js CHANGED
@@ -8,8 +8,8 @@ import { createElement } from "react";
8
8
  import { render } from "ink";
9
9
 
10
10
  // src/ui/BuildUI.tsx
11
- import { useEffect, useState } from "react";
12
- import { Box as Box2, Text as Text2, useApp } from "ink";
11
+ import { useEffect as useEffect2, useState as useState2 } from "react";
12
+ import { Box as Box3, Text as Text4, useApp } from "ink";
13
13
 
14
14
  // src/lib/runBuild.ts
15
15
  import { mkdir, writeFile } from "fs/promises";
@@ -88,9 +88,26 @@ async function runBuild(pluginDir, outputDir, onStep) {
88
88
  })).sort((a, b) => b.version.localeCompare(a.version, void 0, { numeric: true })) : [];
89
89
  return serializePlugin(plugin, allReleaseNotes);
90
90
  });
91
- await writeFile(join2(outputDir, "bundle.json"), JSON.stringify(bundle, null, 2), "utf-8");
91
+ const jsonStr = JSON.stringify(bundle, null, 2);
92
+ const bundleSize = Buffer.byteLength(jsonStr, "utf-8");
93
+ await writeFile(join2(outputDir, "bundle.json"), jsonStr, "utf-8");
94
+ return { bundleSize };
92
95
  }
93
96
 
97
+ // src/lib/formatBytes.ts
98
+ function formatBytes(n) {
99
+ if (n < 1024) {
100
+ return `${String(n)} B`;
101
+ }
102
+ if (n < 1024 * 1024) {
103
+ return `${(n / 1024).toFixed(1)} KB`;
104
+ }
105
+ return `${(n / (1024 * 1024)).toFixed(2)} MB`;
106
+ }
107
+
108
+ // src/ui/AppHeader.tsx
109
+ import { Box as Box2, Text as Text2 } from "ink";
110
+
94
111
  // src/ui/Logo.tsx
95
112
  import { Box, Text } from "ink";
96
113
  import { jsx } from "react/jsx-runtime";
@@ -116,8 +133,20 @@ var LOGO_LINES = [
116
133
  ];
117
134
  var Logo = () => /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: LOGO_LINES.map((line, i) => /* @__PURE__ */ jsx(Text, { color: "cyan", children: line }, i)) });
118
135
 
119
- // src/ui/BuildUI.tsx
136
+ // src/ui/AppHeader.tsx
120
137
  import { Fragment, jsx as jsx2, jsxs } from "react/jsx-runtime";
138
+ var AppHeader = ({ marginBottom = 0 }) => /* @__PURE__ */ jsxs(Fragment, { children: [
139
+ /* @__PURE__ */ jsx2(Logo, {}),
140
+ /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", marginTop: 1, marginBottom, children: [
141
+ /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "Kizen App Builder" }),
142
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(24) })
143
+ ] })
144
+ ] });
145
+
146
+ // src/ui/Spinner.tsx
147
+ import { useEffect, useState } from "react";
148
+ import { Text as Text3 } from "ink";
149
+ import { jsx as jsx3 } from "react/jsx-runtime";
121
150
  var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
122
151
  var Spinner = () => {
123
152
  const [frame, setFrame] = useState(0);
@@ -129,8 +158,11 @@ var Spinner = () => {
129
158
  clearInterval(id);
130
159
  };
131
160
  }, []);
132
- return /* @__PURE__ */ jsx2(Text2, { color: "cyan", children: SPINNER_FRAMES[frame] ?? "\u280B" });
161
+ return /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: SPINNER_FRAMES[frame] ?? "\u280B" });
133
162
  };
163
+
164
+ // src/ui/BuildUI.tsx
165
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs2 } from "react/jsx-runtime";
134
166
  var STEPS = [
135
167
  "creating-dir",
136
168
  "reading-files",
@@ -147,10 +179,12 @@ var STEP_LABELS = {
147
179
  };
148
180
  var BuildUI = ({ outputDir, pluginDir }) => {
149
181
  const app = useApp();
150
- const [step, setStep] = useState("creating-dir");
151
- const [errorMessage, setErrorMessage] = useState(null);
152
- useEffect(() => {
153
- void runBuild(pluginDir, outputDir, setStep).then(() => {
182
+ const [step, setStep] = useState2("creating-dir");
183
+ const [errorMessage, setErrorMessage] = useState2(null);
184
+ const [bundleSize, setBundleSize] = useState2(null);
185
+ useEffect2(() => {
186
+ void runBuild(pluginDir, outputDir, setStep).then(({ bundleSize: size }) => {
187
+ setBundleSize(size);
154
188
  setStep("done");
155
189
  app.exit();
156
190
  }).catch((err) => {
@@ -163,32 +197,32 @@ var BuildUI = ({ outputDir, pluginDir }) => {
163
197
  const currentIndex = STEPS.indexOf(step);
164
198
  const isError = step === "error";
165
199
  const isDone = step === "done";
166
- return /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", paddingY: 1, paddingX: 2, children: [
167
- /* @__PURE__ */ jsx2(Logo, {}),
168
- /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [
169
- /* @__PURE__ */ jsx2(Text2, { bold: true, color: "cyan", children: "Kizen App Builder" }),
170
- /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "\u2500".repeat(24) })
171
- ] }),
172
- /* @__PURE__ */ jsxs(Box2, { flexDirection: "column", gap: 0, children: [
200
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, children: [
201
+ /* @__PURE__ */ jsx4(AppHeader, { marginBottom: 1 }),
202
+ /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", gap: 0, children: [
173
203
  STEPS.map((s, i) => {
174
204
  const isActive = step === s;
175
205
  const isStepDone = isDone || currentIndex > i;
176
206
  const isFailed = isError && i === currentIndex;
177
- return /* @__PURE__ */ jsx2(Box2, { gap: 1, children: isFailed ? /* @__PURE__ */ jsxs(Text2, { color: "red", children: [
207
+ return /* @__PURE__ */ jsx4(Box3, { gap: 1, children: isFailed ? /* @__PURE__ */ jsxs2(Text4, { color: "red", children: [
178
208
  "\u2717 ",
179
209
  STEP_LABELS[s]
180
- ] }) : isStepDone ? /* @__PURE__ */ jsxs(Text2, { color: "green", children: [
210
+ ] }) : isStepDone ? /* @__PURE__ */ jsxs2(Text4, { color: "green", children: [
181
211
  "\u2713 ",
182
212
  STEP_LABELS[s]
183
- ] }) : isActive ? /* @__PURE__ */ jsxs(Fragment, { children: [
184
- /* @__PURE__ */ jsx2(Spinner, {}),
185
- /* @__PURE__ */ jsx2(Text2, { children: STEP_LABELS[s] })
186
- ] }) : /* @__PURE__ */ jsxs(Text2, { dimColor: true, children: [
213
+ ] }) : isActive ? /* @__PURE__ */ jsxs2(Fragment2, { children: [
214
+ /* @__PURE__ */ jsx4(Spinner, {}),
215
+ /* @__PURE__ */ jsx4(Text4, { children: STEP_LABELS[s] })
216
+ ] }) : /* @__PURE__ */ jsxs2(Text4, { dimColor: true, children: [
187
217
  " ",
188
218
  STEP_LABELS[s]
189
219
  ] }) }, s);
190
220
  }),
191
- errorMessage !== null && /* @__PURE__ */ jsx2(Box2, { marginTop: 1, borderStyle: "single", borderColor: "red", paddingX: 1, children: /* @__PURE__ */ jsx2(Text2, { color: "red", children: errorMessage }) })
221
+ isDone && bundleSize !== null && /* @__PURE__ */ jsx4(Box3, { marginTop: 1, gap: 1, children: /* @__PURE__ */ jsxs2(Text4, { color: "green", children: [
222
+ "\u2713 bundle.json \u2014 ",
223
+ formatBytes(bundleSize)
224
+ ] }) }),
225
+ errorMessage !== null && /* @__PURE__ */ jsx4(Box3, { marginTop: 1, borderStyle: "single", borderColor: "red", paddingX: 1, children: /* @__PURE__ */ jsx4(Text4, { color: "red", children: errorMessage }) })
192
226
  ] })
193
227
  ] });
194
228
  };
@@ -227,22 +261,350 @@ import { createElement as createElement2 } from "react";
227
261
  import { render as render2 } from "ink";
228
262
 
229
263
  // src/ui/DevUI.tsx
230
- import { useCallback as useCallback2, useEffect as useEffect3, useRef, useState as useState3 } from "react";
231
- import { Box as Box4, Text as Text4, useInput as useInput2 } from "ink";
232
- import { createReadStream, watch } from "fs";
233
- import { access, readFile as readFile5 } from "fs/promises";
264
+ import { useCallback as useCallback3, useEffect as useEffect4, useRef, useState as useState5 } from "react";
265
+ import { Box as Box5, Text as Text6, useInput as useInput2 } from "ink";
266
+ import { watch } from "fs";
234
267
  import { createServer } from "http";
235
- import { dirname as dirname3, extname, join as join7 } from "path";
236
- import { fileURLToPath as fileURLToPath2 } from "url";
268
+ import { join as join8 } from "path";
237
269
  import { WebSocketServer } from "ws";
238
270
 
271
+ // src/server/requestHandler.ts
272
+ import { createReadStream } from "fs";
273
+ import { access, readFile as readFile4 } from "fs/promises";
274
+ import { dirname as dirname2, extname, join as join6 } from "path";
275
+ import { fileURLToPath } from "url";
276
+
277
+ // src/lib/proxyCache.ts
278
+ function createProxyCache(options = {}) {
279
+ const cache = /* @__PURE__ */ new Map();
280
+ const { onChange } = options;
281
+ const emitChange = () => {
282
+ onChange?.(cache.size);
283
+ };
284
+ return {
285
+ get(key, fetcher) {
286
+ const existing = cache.get(key);
287
+ if (existing !== void 0) {
288
+ return existing.then((response) => ({ response, fromCache: true }));
289
+ }
290
+ const pending = fetcher().then(async (response) => {
291
+ const headers = Object.fromEntries(response.headers);
292
+ delete headers["content-encoding"];
293
+ delete headers["content-length"];
294
+ return {
295
+ status: response.status,
296
+ headers,
297
+ body: Buffer.from(await response.arrayBuffer())
298
+ };
299
+ }).catch((error) => {
300
+ cache.delete(key);
301
+ emitChange();
302
+ throw error;
303
+ });
304
+ cache.set(key, pending);
305
+ emitChange();
306
+ return pending.then((response) => {
307
+ if (response.status >= 400) {
308
+ cache.delete(key);
309
+ emitChange();
310
+ }
311
+ return { response, fromCache: false };
312
+ });
313
+ },
314
+ clear() {
315
+ cache.clear();
316
+ emitChange();
317
+ }
318
+ };
319
+ }
320
+
321
+ // src/lib/constants.ts
322
+ var LOG_LIMIT = 50;
323
+ var LOG_DISPLAY = 8;
324
+ var FILE_WATCH_DEBOUNCE_MS = 150;
325
+ var MIME_TYPES = {
326
+ ".html": "text/html; charset=utf-8",
327
+ ".js": "application/javascript; charset=utf-8",
328
+ ".mjs": "application/javascript; charset=utf-8",
329
+ ".css": "text/css; charset=utf-8",
330
+ ".png": "image/png",
331
+ ".jpg": "image/jpeg",
332
+ ".svg": "image/svg+xml",
333
+ ".ico": "image/x-icon",
334
+ ".json": "application/json; charset=utf-8",
335
+ ".woff": "font/woff",
336
+ ".woff2": "font/woff2",
337
+ ".ttf": "font/ttf",
338
+ ".map": "application/json"
339
+ };
340
+
341
+ // src/lib/credentials.ts
342
+ import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
343
+ import { homedir } from "os";
344
+ import { dirname, join as join4 } from "path";
345
+ var ENVIRONMENTS = ["go", "fmo", "staging", "integration", "test1"];
346
+ var GLOBAL_CREDENTIALS_DIR = join4(homedir(), ".kizenappbuilder");
347
+ var GLOBAL_CREDENTIALS_PATH = join4(GLOBAL_CREDENTIALS_DIR, "credentials.json");
348
+ var DEFAULT_PROFILE_NAME = "credentials";
349
+ function isValidEnvironment(value) {
350
+ return ENVIRONMENTS.includes(value);
351
+ }
352
+ function parseCredentials(raw) {
353
+ if (typeof raw !== "object" || raw === null) {
354
+ throw new Error("Credentials must be a JSON object");
355
+ }
356
+ const obj = raw;
357
+ const env = obj.environment;
358
+ return {
359
+ apiKey: typeof obj.apiKey === "string" ? obj.apiKey : "",
360
+ userId: typeof obj.userId === "string" ? obj.userId : "",
361
+ businessId: typeof obj.businessId === "string" ? obj.businessId : "",
362
+ environment: isValidEnvironment(env) ? env : "go"
363
+ };
364
+ }
365
+ async function loadCredentialsFromFile(filePath) {
366
+ const content = await readFile2(filePath, "utf-8");
367
+ return parseCredentials(JSON.parse(content));
368
+ }
369
+ async function loadGlobalCredentials() {
370
+ try {
371
+ return await loadCredentialsFromFile(GLOBAL_CREDENTIALS_PATH);
372
+ } catch {
373
+ return null;
374
+ }
375
+ }
376
+ async function saveGlobalCredentials(credentials) {
377
+ await mkdir2(dirname(GLOBAL_CREDENTIALS_PATH), { recursive: true });
378
+ await writeFile2(GLOBAL_CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), "utf-8");
379
+ }
380
+ function getProfilePath(name) {
381
+ return join4(GLOBAL_CREDENTIALS_DIR, `${name}.json`);
382
+ }
383
+ async function listCredentialProfiles() {
384
+ const profiles = [
385
+ { name: DEFAULT_PROFILE_NAME, path: GLOBAL_CREDENTIALS_PATH, isDefault: true }
386
+ ];
387
+ try {
388
+ const entries = await readdir2(GLOBAL_CREDENTIALS_DIR);
389
+ for (const entry of entries) {
390
+ if (!entry.endsWith(".json")) {
391
+ continue;
392
+ }
393
+ const name = entry.slice(0, -5);
394
+ if (name === DEFAULT_PROFILE_NAME) {
395
+ continue;
396
+ }
397
+ profiles.push({ name, path: join4(GLOBAL_CREDENTIALS_DIR, entry), isDefault: false });
398
+ }
399
+ } catch {
400
+ }
401
+ return profiles;
402
+ }
403
+ async function saveCredentialProfile(name, credentials) {
404
+ if (name === DEFAULT_PROFILE_NAME) {
405
+ await saveGlobalCredentials(credentials);
406
+ return;
407
+ }
408
+ await mkdir2(GLOBAL_CREDENTIALS_DIR, { recursive: true });
409
+ await writeFile2(getProfilePath(name), JSON.stringify(credentials, null, 2), "utf-8");
410
+ }
411
+ async function loadCredentialProfile(name) {
412
+ try {
413
+ return await loadCredentialsFromFile(getProfilePath(name));
414
+ } catch {
415
+ return null;
416
+ }
417
+ }
418
+
419
+ // src/lib/config.ts
420
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
421
+ import { join as join5 } from "path";
422
+ async function loadConfig(outputDir) {
423
+ try {
424
+ const content = await readFile3(join5(outputDir, "config.json"), "utf-8");
425
+ return JSON.parse(content);
426
+ } catch {
427
+ return {};
428
+ }
429
+ }
430
+ async function saveConfig(outputDir, config) {
431
+ await mkdir3(outputDir, { recursive: true });
432
+ await writeFile3(join5(outputDir, "config.json"), JSON.stringify(config, null, 2), "utf-8");
433
+ }
434
+
435
+ // src/server/requestHandler.ts
436
+ function proxyLogEntryToString(entry) {
437
+ if (entry.kind === "info") {
438
+ return entry.message;
439
+ }
440
+ const cache = entry.fromCache ? " [cache]" : "";
441
+ return `${entry.method} ${entry.url} \u2192 ${String(entry.status)}${cache}`;
442
+ }
443
+ function getViewerPath() {
444
+ const filename = fileURLToPath(import.meta.url);
445
+ return join6(dirname2(filename), "viewer");
446
+ }
447
+ async function fileExists(filePath) {
448
+ try {
449
+ await access(filePath);
450
+ return true;
451
+ } catch {
452
+ return false;
453
+ }
454
+ }
455
+ function createRequestHandler(viewerPath, createServerLog, createProxyLog, credentialsRef, activeProfileRef, outputDir, broadcast, onProxyCacheChange) {
456
+ const proxyCache = createProxyCache({ onChange: onProxyCacheChange });
457
+ return (req, res) => {
458
+ void (async () => {
459
+ const url = req.url ?? "/";
460
+ try {
461
+ createServerLog(`Received request: ${url}`);
462
+ if (url === "/api/credentials") {
463
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
464
+ res.end(credentialsRef.current !== null ? JSON.stringify(credentialsRef.current) : "{}");
465
+ return;
466
+ }
467
+ if (url === "/api/credential-profiles" && req.method === "GET") {
468
+ const profiles = await listCredentialProfiles();
469
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
470
+ res.end(JSON.stringify({ profiles, active: activeProfileRef.current }));
471
+ return;
472
+ }
473
+ if (url === "/api/credentials/switch" && req.method === "POST") {
474
+ const chunks = [];
475
+ for await (const chunk of req) {
476
+ chunks.push(chunk);
477
+ }
478
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8") || "{}");
479
+ const profileName = body.profile;
480
+ const creds = profileName === void 0 || profileName === "" ? await loadGlobalCredentials() : await loadCredentialProfile(profileName);
481
+ if (creds !== null) {
482
+ credentialsRef.current = creds;
483
+ activeProfileRef.current = profileName ?? void 0;
484
+ const active = activeProfileRef.current;
485
+ await saveConfig(outputDir, {
486
+ credentialMode: "global",
487
+ ...active !== void 0 && { activeCredentialProfile: active }
488
+ });
489
+ broadcast({ type: "credentials-updated", credentials: creds });
490
+ }
491
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
492
+ res.end(JSON.stringify({ ok: creds !== null }));
493
+ return;
494
+ }
495
+ if (url === "/api/bundle") {
496
+ const bundlePath = join6(process.cwd(), ".kizenapp", "bundle.json");
497
+ try {
498
+ const content = await readFile4(bundlePath, "utf-8");
499
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
500
+ res.end(content);
501
+ } catch {
502
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
503
+ res.end("{}");
504
+ }
505
+ return;
506
+ }
507
+ if (url === "/api/proxy-cache/clear") {
508
+ proxyCache.clear();
509
+ createProxyLog({ kind: "info", message: "Proxy cache cleared" });
510
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
511
+ res.end('{"ok":true}');
512
+ return;
513
+ }
514
+ if (url.startsWith("/api/proxy")) {
515
+ const proxyTarget = req.headers["x-proxy-target"];
516
+ if (typeof proxyTarget !== "string") {
517
+ res.writeHead(400);
518
+ res.end("Missing x-proxy-target header");
519
+ return;
520
+ }
521
+ const upstreamPath = url.slice("/api/proxy".length) || "/";
522
+ const upstreamUrl = `${proxyTarget}${upstreamPath}`;
523
+ const method = req.method ?? "GET";
524
+ const chunks = [];
525
+ for await (const chunk of req) {
526
+ chunks.push(chunk);
527
+ }
528
+ const { host, "x-proxy-target": _drop, ...forwardHeaders } = req.headers;
529
+ if (method === "GET") {
530
+ const cacheKey = `GET::${upstreamUrl}`;
531
+ const { response: cached, fromCache } = await proxyCache.get(
532
+ cacheKey,
533
+ () => fetch(upstreamUrl, {
534
+ method: "GET",
535
+ headers: forwardHeaders
536
+ })
537
+ );
538
+ createProxyLog({
539
+ kind: "request",
540
+ method: "GET",
541
+ status: cached.status,
542
+ fromCache,
543
+ url: upstreamPath
544
+ });
545
+ res.writeHead(cached.status, cached.headers);
546
+ res.end(cached.body);
547
+ return;
548
+ }
549
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
550
+ const resolvedBody = body && body.length > 0 ? body : void 0;
551
+ const upstream = await fetch(upstreamUrl, {
552
+ method,
553
+ headers: forwardHeaders,
554
+ ...resolvedBody !== void 0 && { body: resolvedBody }
555
+ });
556
+ createProxyLog({
557
+ kind: "request",
558
+ method,
559
+ status: upstream.status,
560
+ fromCache: false,
561
+ url: upstreamPath
562
+ });
563
+ const responseHeaders = Object.fromEntries(upstream.headers);
564
+ delete responseHeaders["content-encoding"];
565
+ delete responseHeaders["content-length"];
566
+ res.writeHead(upstream.status, responseHeaders);
567
+ res.end(Buffer.from(await upstream.arrayBuffer()));
568
+ return;
569
+ }
570
+ const rawPath = url === "/" ? "/index.html" : url;
571
+ const filePath = join6(viewerPath, rawPath);
572
+ const resolvedPath = await fileExists(filePath) ? filePath : join6(viewerPath, "index.html");
573
+ const ext = extname(resolvedPath);
574
+ const mimeType = MIME_TYPES[ext] ?? "application/octet-stream";
575
+ res.writeHead(200, { "Content-Type": mimeType });
576
+ createReadStream(resolvedPath).pipe(res);
577
+ } catch (err) {
578
+ if (url.startsWith("/api/proxy")) {
579
+ createProxyLog({
580
+ kind: "request",
581
+ method: req.method ?? "GET",
582
+ status: 502,
583
+ fromCache: false,
584
+ url: url.slice("/api/proxy".length) || "/"
585
+ });
586
+ } else {
587
+ createProxyLog({
588
+ kind: "info",
589
+ message: `Error handling ${url}: ${err instanceof Error ? err.message : String(err)}`
590
+ });
591
+ }
592
+ if (!res.headersSent) {
593
+ res.writeHead(502);
594
+ res.end("Bad Gateway");
595
+ }
596
+ }
597
+ })();
598
+ };
599
+ }
600
+
239
601
  // src/chrome/launcher.ts
240
602
  import { launch } from "chrome-launcher";
241
603
  import { createRequire } from "module";
242
604
  import { mkdirSync } from "fs";
243
- import { readFile as readFile2 } from "fs/promises";
244
- import { dirname, join as join4 } from "path";
245
- import { fileURLToPath } from "url";
605
+ import { readFile as readFile5 } from "fs/promises";
606
+ import { dirname as dirname3, join as join7 } from "path";
607
+ import { fileURLToPath as fileURLToPath2 } from "url";
246
608
  var CDP = createRequire(import.meta.url)("chrome-remote-interface");
247
609
  var CSP_HEADERS = /* @__PURE__ */ new Set([
248
610
  "content-security-policy",
@@ -320,8 +682,8 @@ async function serializeArgAsync(client, arg) {
320
682
  }
321
683
  async function installDockIcon(client) {
322
684
  try {
323
- const iconPath = join4(dirname(fileURLToPath(import.meta.url)), "viewer", "icon.png");
324
- const icon = await readFile2(iconPath);
685
+ const iconPath = join7(dirname3(fileURLToPath2(import.meta.url)), "viewer", "icon.png");
686
+ const icon = await readFile5(iconPath);
325
687
  await client.Browser.setDockTile({ image: icon.toString("base64") });
326
688
  } catch {
327
689
  }
@@ -463,190 +825,65 @@ async function launchChromeViewer(opts) {
463
825
  }
464
826
  });
465
827
  c.on("Runtime.consoleAPICalled", (rawParams) => {
466
- void (async () => {
467
- const { type, args } = rawParams;
468
- if (args[0]?.type === "string" && String(args[0].value).includes("Chrome Security")) {
469
- return;
470
- }
471
- const level = type === "warning" ? "warn" : ["log", "warn", "error", "info"].includes(type) ? type : "log";
472
- const serialized = await Promise.all(args.map((arg) => serializeArgAsync(c, arg)));
473
- opts.broadcast({ type: "console-message", level, args: serialized });
474
- })().catch(() => {
475
- });
476
- });
477
- const kill = () => {
478
- clearInterval(cookieTimer);
479
- try {
480
- void c.close();
481
- } catch {
482
- }
483
- chrome.kill();
484
- };
485
- return { kill };
486
- }
487
-
488
- // src/lib/proxyCache.ts
489
- function createProxyCache(options = {}) {
490
- const cache = /* @__PURE__ */ new Map();
491
- const { onChange } = options;
492
- const emitChange = () => {
493
- onChange?.(cache.size);
494
- };
495
- return {
496
- get(key, fetcher) {
497
- const existing = cache.get(key);
498
- if (existing !== void 0) {
499
- return existing.then((response) => ({ response, fromCache: true }));
500
- }
501
- const pending = fetcher().then(async (response) => {
502
- const headers = Object.fromEntries(response.headers);
503
- delete headers["content-encoding"];
504
- delete headers["content-length"];
505
- return {
506
- status: response.status,
507
- headers,
508
- body: Buffer.from(await response.arrayBuffer())
509
- };
510
- }).catch((error) => {
511
- cache.delete(key);
512
- emitChange();
513
- throw error;
514
- });
515
- cache.set(key, pending);
516
- emitChange();
517
- return pending.then((response) => {
518
- if (response.status >= 400) {
519
- cache.delete(key);
520
- emitChange();
521
- }
522
- return { response, fromCache: false };
523
- });
524
- },
525
- clear() {
526
- cache.clear();
527
- emitChange();
528
- }
529
- };
530
- }
531
-
532
- // src/lib/credentials.ts
533
- import { mkdir as mkdir2, readFile as readFile3, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
534
- import { homedir } from "os";
535
- import { dirname as dirname2, join as join5 } from "path";
536
- var ENVIRONMENTS = ["go", "fmo", "staging", "integration", "test1"];
537
- var GLOBAL_CREDENTIALS_DIR = join5(homedir(), ".kizenappbuilder");
538
- var GLOBAL_CREDENTIALS_PATH = join5(GLOBAL_CREDENTIALS_DIR, "credentials.json");
539
- var DEFAULT_PROFILE_NAME = "credentials";
540
- function isValidEnvironment(value) {
541
- return ENVIRONMENTS.includes(value);
542
- }
543
- function parseCredentials(raw) {
544
- if (typeof raw !== "object" || raw === null) {
545
- throw new Error("Credentials must be a JSON object");
546
- }
547
- const obj = raw;
548
- const env = obj.environment;
549
- return {
550
- apiKey: typeof obj.apiKey === "string" ? obj.apiKey : "",
551
- userId: typeof obj.userId === "string" ? obj.userId : "",
552
- businessId: typeof obj.businessId === "string" ? obj.businessId : "",
553
- environment: isValidEnvironment(env) ? env : "go"
554
- };
555
- }
556
- async function loadCredentialsFromFile(filePath) {
557
- const content = await readFile3(filePath, "utf-8");
558
- return parseCredentials(JSON.parse(content));
559
- }
560
- async function loadGlobalCredentials() {
561
- try {
562
- return await loadCredentialsFromFile(GLOBAL_CREDENTIALS_PATH);
563
- } catch {
564
- return null;
565
- }
566
- }
567
- async function saveGlobalCredentials(credentials) {
568
- await mkdir2(dirname2(GLOBAL_CREDENTIALS_PATH), { recursive: true });
569
- await writeFile2(GLOBAL_CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), "utf-8");
570
- }
571
- function getProfilePath(name) {
572
- return join5(GLOBAL_CREDENTIALS_DIR, `${name}.json`);
573
- }
574
- async function listCredentialProfiles() {
575
- const profiles = [
576
- { name: DEFAULT_PROFILE_NAME, path: GLOBAL_CREDENTIALS_PATH, isDefault: true }
577
- ];
578
- try {
579
- const entries = await readdir2(GLOBAL_CREDENTIALS_DIR);
580
- for (const entry of entries) {
581
- if (!entry.endsWith(".json")) {
582
- continue;
583
- }
584
- const name = entry.slice(0, -5);
585
- if (name === DEFAULT_PROFILE_NAME) {
586
- continue;
828
+ void (async () => {
829
+ const { type, args } = rawParams;
830
+ if (args[0]?.type === "string" && String(args[0].value).includes("Chrome Security")) {
831
+ return;
587
832
  }
588
- profiles.push({ name, path: join5(GLOBAL_CREDENTIALS_DIR, entry), isDefault: false });
833
+ const level = type === "warning" ? "warn" : ["log", "warn", "error", "info"].includes(type) ? type : "log";
834
+ const serialized = await Promise.all(args.map((arg) => serializeArgAsync(c, arg)));
835
+ opts.broadcast({ type: "console-message", level, args: serialized });
836
+ })().catch(() => {
837
+ });
838
+ });
839
+ const kill = () => {
840
+ clearInterval(cookieTimer);
841
+ try {
842
+ void c.close();
843
+ } catch {
589
844
  }
590
- } catch {
591
- }
592
- return profiles;
593
- }
594
- async function saveCredentialProfile(name, credentials) {
595
- if (name === DEFAULT_PROFILE_NAME) {
596
- await saveGlobalCredentials(credentials);
597
- return;
598
- }
599
- await mkdir2(GLOBAL_CREDENTIALS_DIR, { recursive: true });
600
- await writeFile2(getProfilePath(name), JSON.stringify(credentials, null, 2), "utf-8");
601
- }
602
- async function loadCredentialProfile(name) {
603
- try {
604
- return await loadCredentialsFromFile(getProfilePath(name));
605
- } catch {
606
- return null;
607
- }
845
+ chrome.kill();
846
+ };
847
+ return { kill };
608
848
  }
609
849
 
610
- // src/lib/config.ts
611
- import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile3 } from "fs/promises";
612
- import { join as join6 } from "path";
613
- async function loadConfig(outputDir) {
614
- try {
615
- const content = await readFile4(join6(outputDir, "config.json"), "utf-8");
616
- return JSON.parse(content);
617
- } catch {
618
- return {};
619
- }
620
- }
621
- async function saveConfig(outputDir, config) {
622
- await mkdir3(outputDir, { recursive: true });
623
- await writeFile3(join6(outputDir, "config.json"), JSON.stringify(config, null, 2), "utf-8");
850
+ // src/hooks/useLogHistory.ts
851
+ import { useCallback, useState as useState3 } from "react";
852
+ function useLogHistory(limit) {
853
+ const [history, setHistory] = useState3([]);
854
+ const append = useCallback(
855
+ (message) => {
856
+ setHistory((h) => [...h, `${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${message}`].slice(-limit));
857
+ },
858
+ [limit]
859
+ );
860
+ return [history, append];
624
861
  }
625
862
 
626
863
  // src/ui/CredentialSetupUI.tsx
627
- import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
628
- import { Box as Box3, Text as Text3, useInput } from "ink";
629
- import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
864
+ import { useCallback as useCallback2, useEffect as useEffect3, useState as useState4 } from "react";
865
+ import { Box as Box4, Text as Text5, useInput } from "ink";
866
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
630
867
  var FIELDS = ["apiKey", "userId", "businessId"];
631
868
  var FIELD_LABELS = {
632
869
  apiKey: "API Key",
633
870
  userId: "User ID",
634
871
  businessId: "Business ID"
635
872
  };
636
- var Hint = ({ text }) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: text });
873
+ var Hint = ({ text }) => /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: text });
637
874
  var CredentialSetupUI = ({
638
875
  initialMode,
639
876
  showProfileManager,
640
877
  onComplete,
641
878
  onCancel
642
879
  }) => {
643
- const [phase, setPhase] = useState2(
880
+ const [phase, setPhase] = useState4(
644
881
  initialMode === "global" && !showProfileManager ? { type: "loading" } : initialMode === "global" && showProfileManager ? { type: "profile-loading" } : { type: "mode-select", cursor: 0 }
645
882
  );
646
- const [inputBuffer, setInputBuffer] = useState2("");
647
- const [error, setError] = useState2(null);
648
- const [activeProfileName, setActiveProfileName] = useState2(void 0);
649
- const handleModeChosen = useCallback(
883
+ const [inputBuffer, setInputBuffer] = useState4("");
884
+ const [error, setError] = useState4(null);
885
+ const [activeProfileName, setActiveProfileName] = useState4(void 0);
886
+ const handleModeChosen = useCallback2(
650
887
  (mode) => {
651
888
  if (mode === "local") {
652
889
  onComplete({ mode: "local", credentials: null });
@@ -656,7 +893,7 @@ var CredentialSetupUI = ({
656
893
  },
657
894
  [onComplete]
658
895
  );
659
- const handleProfileChosen = useCallback(async (profile) => {
896
+ const handleProfileChosen = useCallback2(async (profile) => {
660
897
  if (profile === null) {
661
898
  setPhase({ type: "profile-loading" });
662
899
  const profiles = await listCredentialProfiles();
@@ -669,7 +906,7 @@ var CredentialSetupUI = ({
669
906
  const envCursor2 = existing ? Math.max(0, ENVIRONMENTS.indexOf(existing.environment)) : 0;
670
907
  setPhase({ type: "creds-entry", field: 0, values: existing ?? {}, envCursor: envCursor2 });
671
908
  }, []);
672
- const handleSave = useCallback(
909
+ const handleSave = useCallback2(
673
910
  async (values2, envCursor2) => {
674
911
  const environment = ENVIRONMENTS[envCursor2] ?? "go";
675
912
  const credentials = {
@@ -810,16 +1047,16 @@ var CredentialSetupUI = ({
810
1047
  }
811
1048
  }
812
1049
  });
813
- const loadProfileList = useCallback(async () => {
1050
+ const loadProfileList = useCallback2(async () => {
814
1051
  const profiles = await listCredentialProfiles();
815
1052
  setPhase({ type: "profile-select", profiles, cursor: 0 });
816
1053
  }, []);
817
- useEffect2(() => {
1054
+ useEffect3(() => {
818
1055
  if (phase.type === "profile-loading") {
819
1056
  void loadProfileList();
820
1057
  }
821
1058
  }, [phase.type, loadProfileList]);
822
- useEffect2(() => {
1059
+ useEffect3(() => {
823
1060
  if (initialMode === "global" && !showProfileManager) {
824
1061
  void (async () => {
825
1062
  const existing = await loadGlobalCredentials();
@@ -828,14 +1065,14 @@ var CredentialSetupUI = ({
828
1065
  })();
829
1066
  }
830
1067
  }, []);
831
- useEffect2(() => {
1068
+ useEffect3(() => {
832
1069
  if (phase.type === "creds-entry" && phase.field === 0) {
833
1070
  setInputBuffer(phase.values.apiKey ?? "");
834
1071
  }
835
1072
  }, [phase.type]);
836
1073
  if (phase.type === "loading" || phase.type === "saving" || phase.type === "profile-loading") {
837
1074
  const label = phase.type === "saving" ? "Saving credentials\u2026" : phase.type === "profile-loading" ? "Loading profiles\u2026" : "Loading credentials\u2026";
838
- return /* @__PURE__ */ jsx3(Box3, { paddingY: 1, paddingX: 2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: label }) });
1075
+ return /* @__PURE__ */ jsx5(Box4, { paddingY: 1, paddingX: 2, children: /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: label }) });
839
1076
  }
840
1077
  if (phase.type === "done") {
841
1078
  return null;
@@ -845,49 +1082,41 @@ var CredentialSetupUI = ({
845
1082
  { label: "Global", desc: `~/.kizenappbuilder/credentials.json`, mode: "global" },
846
1083
  { label: "Local", desc: "Enter credentials in the browser dev tools", mode: "local" }
847
1084
  ];
848
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
849
- /* @__PURE__ */ jsx3(Logo, {}),
850
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginTop: 1, children: [
851
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Kizen App Builder" }),
852
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(24) })
853
- ] }),
854
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", gap: 0, children: [
855
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Credential mode" }),
1085
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
1086
+ /* @__PURE__ */ jsx5(AppHeader, {}),
1087
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 0, children: [
1088
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Credential mode" }),
856
1089
  options.map((opt, i) => {
857
1090
  const selected = phase.cursor === i;
858
- return /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
859
- /* @__PURE__ */ jsx3(Text3, { ...selected && { color: "cyan" }, children: selected ? "\u276F" : " " }),
860
- /* @__PURE__ */ jsx3(Text3, { bold: selected, ...selected && { color: "cyan" }, children: opt.label }),
861
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: opt.desc })
1091
+ return /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1092
+ /* @__PURE__ */ jsx5(Text5, { ...selected && { color: "cyan" }, children: selected ? "\u276F" : " " }),
1093
+ /* @__PURE__ */ jsx5(Text5, { bold: selected, ...selected && { color: "cyan" }, children: opt.label }),
1094
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: opt.desc })
862
1095
  ] }, opt.mode);
863
1096
  })
864
1097
  ] }),
865
- /* @__PURE__ */ jsx3(Hint, { text: "\u2191\u2193 to move \xB7 Enter to select \xB7 Ctrl+C to quit" })
1098
+ /* @__PURE__ */ jsx5(Hint, { text: "\u2191\u2193 to move \xB7 Enter to select \xB7 Ctrl+C to quit" })
866
1099
  ] });
867
1100
  }
868
1101
  if (phase.type === "profile-select") {
869
1102
  const { profiles, cursor } = phase;
870
1103
  const addNewIdx = profiles.length;
871
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
872
- /* @__PURE__ */ jsx3(Logo, {}),
873
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginTop: 1, children: [
874
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Kizen App Builder" }),
875
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(24) })
876
- ] }),
877
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", gap: 0, children: [
878
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Credential profiles" }),
1104
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
1105
+ /* @__PURE__ */ jsx5(AppHeader, {}),
1106
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 0, children: [
1107
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: "Credential profiles" }),
879
1108
  profiles.map((profile, i) => {
880
1109
  const selected = cursor === i;
881
- return /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
882
- /* @__PURE__ */ jsx3(Text3, { ...selected && { color: "cyan" }, children: selected ? "\u276F" : " " }),
883
- /* @__PURE__ */ jsx3(Text3, { bold: selected, ...selected && { color: "cyan" }, children: profile.isDefault ? "Default" : profile.name }),
884
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: profile.path })
1110
+ return /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1111
+ /* @__PURE__ */ jsx5(Text5, { ...selected && { color: "cyan" }, children: selected ? "\u276F" : " " }),
1112
+ /* @__PURE__ */ jsx5(Text5, { bold: selected, ...selected && { color: "cyan" }, children: profile.isDefault ? "Default" : profile.name }),
1113
+ /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: profile.path })
885
1114
  ] }, profile.name);
886
1115
  }),
887
- /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
888
- /* @__PURE__ */ jsx3(Text3, { ...cursor === addNewIdx && { color: "cyan" }, children: cursor === addNewIdx ? "\u276F" : " " }),
889
- /* @__PURE__ */ jsx3(
890
- Text3,
1116
+ /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1117
+ /* @__PURE__ */ jsx5(Text5, { ...cursor === addNewIdx && { color: "cyan" }, children: cursor === addNewIdx ? "\u276F" : " " }),
1118
+ /* @__PURE__ */ jsx5(
1119
+ Text5,
891
1120
  {
892
1121
  bold: cursor === addNewIdx,
893
1122
  ...cursor === addNewIdx && { color: "cyan" },
@@ -896,77 +1125,69 @@ var CredentialSetupUI = ({
896
1125
  )
897
1126
  ] })
898
1127
  ] }),
899
- /* @__PURE__ */ jsx3(Hint, { text: "\u2191\u2193 to move \xB7 Enter to select \xB7 Esc to cancel" })
1128
+ /* @__PURE__ */ jsx5(Hint, { text: "\u2191\u2193 to move \xB7 Enter to select \xB7 Esc to cancel" })
900
1129
  ] });
901
1130
  }
902
1131
  if (phase.type === "name-entry") {
903
1132
  const { nameBuffer, nameError } = phase;
904
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
905
- /* @__PURE__ */ jsx3(Logo, {}),
906
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginTop: 1, children: [
907
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Kizen App Builder" }),
908
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(24) })
909
- ] }),
910
- /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", gap: 0, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "New credential profile" }) }),
911
- nameError && /* @__PURE__ */ jsxs2(Text3, { color: "red", children: [
1133
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
1134
+ /* @__PURE__ */ jsx5(AppHeader, {}),
1135
+ /* @__PURE__ */ jsx5(Box4, { flexDirection: "column", gap: 0, children: /* @__PURE__ */ jsx5(Text5, { bold: true, children: "New credential profile" }) }),
1136
+ nameError && /* @__PURE__ */ jsxs3(Text5, { color: "red", children: [
912
1137
  "Error: ",
913
1138
  nameError
914
1139
  ] }),
915
- /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
916
- /* @__PURE__ */ jsx3(Box3, { width: 12, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Profile name" }) }),
917
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: ">" }),
918
- /* @__PURE__ */ jsx3(Text3, { children: nameBuffer }),
919
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u2588" })
1140
+ /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1141
+ /* @__PURE__ */ jsx5(Box4, { width: 12, children: /* @__PURE__ */ jsx5(Text5, { bold: true, color: "cyan", children: "Profile name" }) }),
1142
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: ">" }),
1143
+ /* @__PURE__ */ jsx5(Text5, { children: nameBuffer }),
1144
+ /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "\u2588" })
920
1145
  ] }),
921
- /* @__PURE__ */ jsx3(Hint, { text: "Type a name \xB7 Enter to confirm \xB7 Esc to go back" })
1146
+ /* @__PURE__ */ jsx5(Hint, { text: "Type a name \xB7 Enter to confirm \xB7 Esc to go back" })
922
1147
  ] });
923
1148
  }
924
1149
  const { field: activeField, values, envCursor } = phase;
925
1150
  const isEnvField = activeField === FIELDS.length;
926
1151
  const profileLabel = activeProfileName ?? DEFAULT_PROFILE_NAME;
927
- return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
928
- /* @__PURE__ */ jsx3(Logo, {}),
929
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginTop: 1, children: [
930
- /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Kizen App Builder" }),
931
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(24) })
932
- ] }),
933
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", gap: 0, children: [
934
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: activeProfileName !== void 0 ? `Profile: ${activeProfileName}` : "Global credentials" }),
935
- /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
1152
+ return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
1153
+ /* @__PURE__ */ jsx5(AppHeader, {}),
1154
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 0, children: [
1155
+ /* @__PURE__ */ jsx5(Text5, { bold: true, children: activeProfileName !== void 0 ? `Profile: ${activeProfileName}` : "Global credentials" }),
1156
+ /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
936
1157
  "Saved to:",
937
1158
  " ",
938
1159
  activeProfileName !== void 0 ? `~/.kizenappbuilder/${activeProfileName}.json` : GLOBAL_CREDENTIALS_PATH
939
1160
  ] })
940
1161
  ] }),
941
- error && /* @__PURE__ */ jsxs2(Text3, { color: "red", children: [
1162
+ error && /* @__PURE__ */ jsxs3(Text5, { color: "red", children: [
942
1163
  "Error: ",
943
1164
  error
944
1165
  ] }),
945
- /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", gap: 0, children: [
1166
+ /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", gap: 0, children: [
946
1167
  FIELDS.map((name, i) => {
947
1168
  const isActive = activeField === i;
948
1169
  const displayValue = isActive ? inputBuffer : values[name] ?? "";
949
- return /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
950
- /* @__PURE__ */ jsx3(Box3, { width: 12, children: /* @__PURE__ */ jsx3(Text3, { bold: isActive, ...isActive && { color: "cyan" }, children: FIELD_LABELS[name] }) }),
951
- /* @__PURE__ */ jsx3(Text3, { ...isActive && { color: "cyan" }, children: ">" }),
952
- /* @__PURE__ */ jsx3(Text3, { children: displayValue }),
953
- isActive && /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u2588" })
1170
+ return /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1171
+ /* @__PURE__ */ jsx5(Box4, { width: 12, children: /* @__PURE__ */ jsx5(Text5, { bold: isActive, ...isActive && { color: "cyan" }, children: FIELD_LABELS[name] }) }),
1172
+ /* @__PURE__ */ jsx5(Text5, { ...isActive && { color: "cyan" }, children: ">" }),
1173
+ /* @__PURE__ */ jsx5(Text5, { children: displayValue }),
1174
+ isActive && /* @__PURE__ */ jsx5(Text5, { color: "cyan", children: "\u2588" })
954
1175
  ] }, name);
955
1176
  }),
956
- /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
957
- /* @__PURE__ */ jsx3(Box3, { width: 12, children: /* @__PURE__ */ jsx3(Text3, { bold: isEnvField, ...isEnvField && { color: "cyan" }, children: "Environment" }) }),
958
- /* @__PURE__ */ jsx3(Text3, { ...isEnvField && { color: "cyan" }, children: ">" }),
959
- /* @__PURE__ */ jsx3(Box3, { gap: 1, children: ENVIRONMENTS.map((env, i) => {
1177
+ /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1178
+ /* @__PURE__ */ jsx5(Box4, { width: 12, children: /* @__PURE__ */ jsx5(Text5, { bold: isEnvField, ...isEnvField && { color: "cyan" }, children: "Environment" }) }),
1179
+ /* @__PURE__ */ jsx5(Text5, { ...isEnvField && { color: "cyan" }, children: ">" }),
1180
+ /* @__PURE__ */ jsx5(Box4, { gap: 1, children: ENVIRONMENTS.map((env, i) => {
960
1181
  const isSelected = i === envCursor;
961
1182
  if (isEnvField) {
962
- return /* @__PURE__ */ jsx3(Text3, { bold: isSelected, ...isSelected && { color: "cyan" }, children: isSelected ? `[${env}]` : env }, env);
1183
+ return /* @__PURE__ */ jsx5(Text5, { bold: isSelected, ...isSelected && { color: "cyan" }, children: isSelected ? `[${env}]` : env }, env);
963
1184
  }
964
- return /* @__PURE__ */ jsx3(Text3, { dimColor: !isSelected, bold: isSelected, children: env }, env);
1185
+ return /* @__PURE__ */ jsx5(Text5, { dimColor: !isSelected, bold: isSelected, children: env }, env);
965
1186
  }) })
966
1187
  ] })
967
1188
  ] }),
968
- isEnvField ? /* @__PURE__ */ jsx3(Hint, { text: "\u2190\u2192 to select \xB7 \u2191\u2193 to move \xB7 Enter to save \xB7 Esc to cancel" }) : /* @__PURE__ */ jsx3(Hint, { text: "\u2191\u2193 to move \xB7 Backspace to delete \xB7 Esc to cancel" }),
969
- /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
1189
+ isEnvField ? /* @__PURE__ */ jsx5(Hint, { text: "\u2190\u2192 to select \xB7 \u2191\u2193 to move \xB7 Enter to save \xB7 Esc to cancel" }) : /* @__PURE__ */ jsx5(Hint, { text: "\u2191\u2193 to move \xB7 Backspace to delete \xB7 Esc to cancel" }),
1190
+ /* @__PURE__ */ jsxs3(Text5, { dimColor: true, children: [
970
1191
  "Profile: ",
971
1192
  profileLabel
972
1193
  ] })
@@ -974,202 +1195,8 @@ var CredentialSetupUI = ({
974
1195
  };
975
1196
 
976
1197
  // src/ui/DevUI.tsx
977
- import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
978
- function proxyLogEntryToString(entry) {
979
- if (entry.kind === "info") {
980
- return entry.message;
981
- }
982
- const cache = entry.fromCache ? " [cache]" : "";
983
- return `${entry.method} ${entry.url} \u2192 ${String(entry.status)}${cache}`;
984
- }
1198
+ import { jsx as jsx6, jsxs as jsxs4 } from "react/jsx-runtime";
985
1199
  var SKIP_WATCH_PREFIXES = [".kizenapp", ".git"];
986
- var LOG_LIMIT = 50;
987
- var LOG_DISPLAY = 8;
988
- var MIME_TYPES = {
989
- ".html": "text/html; charset=utf-8",
990
- ".js": "application/javascript; charset=utf-8",
991
- ".mjs": "application/javascript; charset=utf-8",
992
- ".css": "text/css; charset=utf-8",
993
- ".png": "image/png",
994
- ".jpg": "image/jpeg",
995
- ".svg": "image/svg+xml",
996
- ".ico": "image/x-icon",
997
- ".json": "application/json; charset=utf-8",
998
- ".woff": "font/woff",
999
- ".woff2": "font/woff2",
1000
- ".ttf": "font/ttf",
1001
- ".map": "application/json"
1002
- };
1003
- var SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
1004
- var Spinner2 = () => {
1005
- const [frame, setFrame] = useState3(0);
1006
- useEffect3(() => {
1007
- const id = setInterval(() => {
1008
- setFrame((prev) => (prev + 1) % SPINNER_FRAMES2.length);
1009
- }, 80);
1010
- return () => {
1011
- clearInterval(id);
1012
- };
1013
- }, []);
1014
- return /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: SPINNER_FRAMES2[frame] ?? "\u280B" });
1015
- };
1016
- function getViewerPath() {
1017
- const filename = fileURLToPath2(import.meta.url);
1018
- return join7(dirname3(filename), "viewer");
1019
- }
1020
- async function fileExists(filePath) {
1021
- try {
1022
- await access(filePath);
1023
- return true;
1024
- } catch {
1025
- return false;
1026
- }
1027
- }
1028
- function createRequestHandler(viewerPath, createServerLog, createProxyLog, credentialsRef, activeProfileRef, outputDir, broadcast, onProxyCacheChange) {
1029
- const proxyCache = createProxyCache({ onChange: onProxyCacheChange });
1030
- return (req, res) => {
1031
- void (async () => {
1032
- const url = req.url ?? "/";
1033
- try {
1034
- createServerLog(`Received request: ${url}`);
1035
- if (url === "/api/credentials") {
1036
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1037
- res.end(credentialsRef.current !== null ? JSON.stringify(credentialsRef.current) : "{}");
1038
- return;
1039
- }
1040
- if (url === "/api/credential-profiles" && req.method === "GET") {
1041
- const profiles = await listCredentialProfiles();
1042
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1043
- res.end(JSON.stringify({ profiles, active: activeProfileRef.current }));
1044
- return;
1045
- }
1046
- if (url === "/api/credentials/switch" && req.method === "POST") {
1047
- const chunks = [];
1048
- for await (const chunk of req) {
1049
- chunks.push(chunk);
1050
- }
1051
- const body = JSON.parse(Buffer.concat(chunks).toString("utf-8") || "{}");
1052
- const profileName = body.profile;
1053
- const creds = profileName === void 0 || profileName === "" ? await loadGlobalCredentials() : await loadCredentialProfile(profileName);
1054
- if (creds !== null) {
1055
- credentialsRef.current = creds;
1056
- activeProfileRef.current = profileName ?? void 0;
1057
- const active = activeProfileRef.current;
1058
- await saveConfig(outputDir, {
1059
- credentialMode: "global",
1060
- ...active !== void 0 && { activeCredentialProfile: active }
1061
- });
1062
- broadcast({ type: "credentials-updated", credentials: creds });
1063
- }
1064
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1065
- res.end(JSON.stringify({ ok: creds !== null }));
1066
- return;
1067
- }
1068
- if (url === "/api/bundle") {
1069
- const bundlePath = join7(process.cwd(), ".kizenapp", "bundle.json");
1070
- try {
1071
- const content = await readFile5(bundlePath, "utf-8");
1072
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1073
- res.end(content);
1074
- } catch {
1075
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1076
- res.end("{}");
1077
- }
1078
- return;
1079
- }
1080
- if (url === "/api/proxy-cache/clear") {
1081
- proxyCache.clear();
1082
- createProxyLog({ kind: "info", message: "Proxy cache cleared" });
1083
- res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
1084
- res.end('{"ok":true}');
1085
- return;
1086
- }
1087
- if (url.startsWith("/api/proxy")) {
1088
- const proxyTarget = req.headers["x-proxy-target"];
1089
- if (typeof proxyTarget !== "string") {
1090
- res.writeHead(400);
1091
- res.end("Missing x-proxy-target header");
1092
- return;
1093
- }
1094
- const upstreamPath = url.slice("/api/proxy".length) || "/";
1095
- const upstreamUrl = `${proxyTarget}${upstreamPath}`;
1096
- const method = req.method ?? "GET";
1097
- const chunks = [];
1098
- for await (const chunk of req) {
1099
- chunks.push(chunk);
1100
- }
1101
- const { host, "x-proxy-target": _drop, ...forwardHeaders } = req.headers;
1102
- if (method === "GET") {
1103
- const cacheKey = `GET::${upstreamUrl}`;
1104
- const { response: cached, fromCache } = await proxyCache.get(
1105
- cacheKey,
1106
- () => fetch(upstreamUrl, {
1107
- method: "GET",
1108
- headers: forwardHeaders
1109
- })
1110
- );
1111
- createProxyLog({
1112
- kind: "request",
1113
- method: "GET",
1114
- status: cached.status,
1115
- fromCache,
1116
- url: upstreamPath
1117
- });
1118
- res.writeHead(cached.status, cached.headers);
1119
- res.end(cached.body);
1120
- return;
1121
- }
1122
- const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
1123
- const resolvedBody = body && body.length > 0 ? body : void 0;
1124
- const upstream = await fetch(upstreamUrl, {
1125
- method,
1126
- headers: forwardHeaders,
1127
- ...resolvedBody !== void 0 && { body: resolvedBody }
1128
- });
1129
- createProxyLog({
1130
- kind: "request",
1131
- method,
1132
- status: upstream.status,
1133
- fromCache: false,
1134
- url: upstreamPath
1135
- });
1136
- const responseHeaders = Object.fromEntries(upstream.headers);
1137
- delete responseHeaders["content-encoding"];
1138
- delete responseHeaders["content-length"];
1139
- res.writeHead(upstream.status, responseHeaders);
1140
- res.end(Buffer.from(await upstream.arrayBuffer()));
1141
- return;
1142
- }
1143
- const rawPath = url === "/" ? "/index.html" : url;
1144
- const filePath = join7(viewerPath, rawPath);
1145
- const resolvedPath = await fileExists(filePath) ? filePath : join7(viewerPath, "index.html");
1146
- const ext = extname(resolvedPath);
1147
- const mimeType = MIME_TYPES[ext] ?? "application/octet-stream";
1148
- res.writeHead(200, { "Content-Type": mimeType });
1149
- createReadStream(resolvedPath).pipe(res);
1150
- } catch (err) {
1151
- if (url.startsWith("/api/proxy")) {
1152
- createProxyLog({
1153
- kind: "request",
1154
- method: req.method ?? "GET",
1155
- status: 502,
1156
- fromCache: false,
1157
- url: url.slice("/api/proxy".length) || "/"
1158
- });
1159
- } else {
1160
- createProxyLog({
1161
- kind: "info",
1162
- message: `Error handling ${url}: ${err instanceof Error ? err.message : String(err)}`
1163
- });
1164
- }
1165
- if (!res.headersSent) {
1166
- res.writeHead(502);
1167
- res.end("Bad Gateway");
1168
- }
1169
- }
1170
- })();
1171
- };
1172
- }
1173
1200
  var DevUI = ({
1174
1201
  port,
1175
1202
  pluginDir,
@@ -1180,29 +1207,30 @@ var DevUI = ({
1180
1207
  }) => {
1181
1208
  const credentialsRef = useRef(initialCredentials);
1182
1209
  const activeProfileRef = useRef(initialActiveProfile);
1183
- const [credMode, setCredMode] = useState3("main");
1184
- const [credentialMode, setCredentialMode] = useState3(
1210
+ const [credMode, setCredMode] = useState5("main");
1211
+ const [credentialMode, setCredentialMode] = useState5(
1185
1212
  initialCredentialMode
1186
1213
  );
1187
- const [status, setStatus] = useState3("starting");
1188
- const [errorMessage, setErrorMessage] = useState3(null);
1189
- const [serverLogHistory, setServerLogHistory] = useState3([]);
1190
- const [buildLogHistory, setBuildLogHistory] = useState3([]);
1191
- const [proxyLogHistory, setProxyLogHistory] = useState3([]);
1192
- const [buildStatus, setBuildStatus] = useState3("pending");
1193
- const [buildError, setBuildError] = useState3(null);
1194
- const [lastBuilt, setLastBuilt] = useState3(null);
1195
- const [wsClientCount, setWsClientCount] = useState3(0);
1196
- const [proxyCacheSize, setProxyCacheSize] = useState3(0);
1197
- const [proxyCacheHits, setProxyCacheHits] = useState3(0);
1198
- const [proxyCacheMisses, setProxyCacheMisses] = useState3(0);
1214
+ const [status, setStatus] = useState5("starting");
1215
+ const [errorMessage, setErrorMessage] = useState5(null);
1216
+ const [serverLogHistory, appendServerLog] = useLogHistory(LOG_LIMIT);
1217
+ const [buildLogHistory, appendBuildLog] = useLogHistory(LOG_LIMIT);
1218
+ const [proxyLogHistory, setProxyLogHistory] = useState5([]);
1219
+ const [buildStatus, setBuildStatus] = useState5("pending");
1220
+ const [buildError, setBuildError] = useState5(null);
1221
+ const [lastBuilt, setLastBuilt] = useState5(null);
1222
+ const [lastBundleSize, setLastBundleSize] = useState5(null);
1223
+ const [wsClientCount, setWsClientCount] = useState5(0);
1224
+ const [proxyCacheSize, setProxyCacheSize] = useState5(0);
1225
+ const [proxyCacheHits, setProxyCacheHits] = useState5(0);
1226
+ const [proxyCacheMisses, setProxyCacheMisses] = useState5(0);
1199
1227
  const buildingRef = useRef(false);
1200
1228
  const viewerLaunchedRef = useRef(false);
1201
1229
  const chromeHandleRef = useRef(null);
1202
1230
  const debounceTimerRef = useRef(null);
1203
1231
  const wsClientsRef = useRef(/* @__PURE__ */ new Set());
1204
1232
  const pendingMessagesRef = useRef([]);
1205
- const broadcast = useCallback2((msg) => {
1233
+ const broadcast = useCallback3((msg) => {
1206
1234
  const json = JSON.stringify(msg);
1207
1235
  if (wsClientsRef.current.size === 0) {
1208
1236
  pendingMessagesRef.current = [...pendingMessagesRef.current, json].slice(-LOG_LIMIT);
@@ -1214,17 +1242,13 @@ var DevUI = ({
1214
1242
  }
1215
1243
  }
1216
1244
  }, []);
1217
- const createServerLog = useCallback2((message) => {
1218
- setServerLogHistory(
1219
- (h) => [...h, `${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${message}`].slice(-LOG_LIMIT)
1220
- );
1221
- }, []);
1222
- const launchChromeViewerCb = useCallback2(() => {
1245
+ const createServerLog = appendServerLog;
1246
+ const launchChromeViewerCb = useCallback3(() => {
1223
1247
  chromeHandleRef.current?.kill();
1224
1248
  chromeHandleRef.current = null;
1225
1249
  void launchChromeViewer({
1226
1250
  port,
1227
- userDataDir: join7(outputDir, ".chrome"),
1251
+ userDataDir: join8(outputDir, ".chrome"),
1228
1252
  broadcast
1229
1253
  }).then((handle) => {
1230
1254
  chromeHandleRef.current = handle;
@@ -1247,14 +1271,14 @@ var DevUI = ({
1247
1271
  },
1248
1272
  { isActive: credMode === "main" }
1249
1273
  );
1250
- useEffect3(() => {
1274
+ useEffect4(() => {
1251
1275
  if (status !== "running" || viewerLaunchedRef.current) {
1252
1276
  return;
1253
1277
  }
1254
1278
  viewerLaunchedRef.current = true;
1255
1279
  launchChromeViewerCb();
1256
1280
  }, [status, launchChromeViewerCb]);
1257
- const handleCredentialsDone = useCallback2(
1281
+ const handleCredentialsDone = useCallback3(
1258
1282
  (result) => {
1259
1283
  credentialsRef.current = result.credentials;
1260
1284
  activeProfileRef.current = result.profileName;
@@ -1269,7 +1293,7 @@ var DevUI = ({
1269
1293
  },
1270
1294
  [outputDir, broadcast]
1271
1295
  );
1272
- const createProxyLog = useCallback2(
1296
+ const createProxyLog = useCallback3(
1273
1297
  (entry) => {
1274
1298
  setProxyLogHistory(
1275
1299
  (h) => [...h, `${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${proxyLogEntryToString(entry)}`].slice(
@@ -1287,16 +1311,14 @@ var DevUI = ({
1287
1311
  },
1288
1312
  [broadcast]
1289
1313
  );
1290
- const createBuildLog = useCallback2(
1314
+ const createBuildLog = useCallback3(
1291
1315
  (message) => {
1292
- setBuildLogHistory(
1293
- (h) => [...h, `${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${message}`].slice(-LOG_LIMIT)
1294
- );
1316
+ appendBuildLog(message);
1295
1317
  broadcast({ type: "log", message });
1296
1318
  },
1297
- [broadcast]
1319
+ [appendBuildLog, broadcast]
1298
1320
  );
1299
- const triggerBuild = useCallback2(() => {
1321
+ const triggerBuild = useCallback3(() => {
1300
1322
  if (buildingRef.current) {
1301
1323
  return;
1302
1324
  }
@@ -1304,10 +1326,11 @@ var DevUI = ({
1304
1326
  setBuildStatus("building");
1305
1327
  setBuildError(null);
1306
1328
  createBuildLog("Build started");
1307
- void runBuild(pluginDir, outputDir).then(() => {
1308
- createBuildLog("Build finished");
1329
+ void runBuild(pluginDir, outputDir).then(({ bundleSize }) => {
1330
+ createBuildLog(`Build finished \u2014 bundle: ${formatBytes(bundleSize)}`);
1309
1331
  setBuildStatus("done");
1310
1332
  setLastBuilt(/* @__PURE__ */ new Date());
1333
+ setLastBundleSize(bundleSize);
1311
1334
  createBuildLog("Notifying viewers to reload");
1312
1335
  for (const client of wsClientsRef.current) {
1313
1336
  if (client.readyState === 1) {
@@ -1322,7 +1345,7 @@ var DevUI = ({
1322
1345
  buildingRef.current = false;
1323
1346
  });
1324
1347
  }, [pluginDir, outputDir, createBuildLog]);
1325
- useEffect3(() => {
1348
+ useEffect4(() => {
1326
1349
  triggerBuild();
1327
1350
  const watcher = watch(pluginDir, { recursive: true }, (_, filename) => {
1328
1351
  if (!filename) {
@@ -1336,7 +1359,7 @@ var DevUI = ({
1336
1359
  if (debounceTimerRef.current !== null) {
1337
1360
  clearTimeout(debounceTimerRef.current);
1338
1361
  }
1339
- debounceTimerRef.current = setTimeout(triggerBuild, 150);
1362
+ debounceTimerRef.current = setTimeout(triggerBuild, FILE_WATCH_DEBOUNCE_MS);
1340
1363
  });
1341
1364
  return () => {
1342
1365
  watcher.close();
@@ -1345,9 +1368,9 @@ var DevUI = ({
1345
1368
  }
1346
1369
  };
1347
1370
  }, [pluginDir, triggerBuild, createBuildLog]);
1348
- useEffect3(() => {
1371
+ useEffect4(() => {
1349
1372
  const viewerPath = getViewerPath();
1350
- void fileExists(join7(viewerPath, "index.html")).then((viewerBuilt) => {
1373
+ void fileExists(join8(viewerPath, "index.html")).then((viewerBuilt) => {
1351
1374
  if (!viewerBuilt) {
1352
1375
  setErrorMessage("Viewer not built. Run 'pnpm build:viewer' first.");
1353
1376
  setStatus("error");
@@ -1395,7 +1418,7 @@ var DevUI = ({
1395
1418
  });
1396
1419
  }, [port, createServerLog, createProxyLog, broadcast, outputDir]);
1397
1420
  if (credMode === "editing") {
1398
- return /* @__PURE__ */ jsx4(
1421
+ return /* @__PURE__ */ jsx6(
1399
1422
  CredentialSetupUI,
1400
1423
  {
1401
1424
  ...credentialMode !== void 0 && { initialMode: credentialMode },
@@ -1409,80 +1432,80 @@ var DevUI = ({
1409
1432
  }
1410
1433
  const elapsedSeconds = lastBuilt !== null ? Math.round((Date.now() - lastBuilt.getTime()) / 1e3) : null;
1411
1434
  const elapsedLabel = elapsedSeconds === null ? "" : elapsedSeconds < 5 ? " (just now)" : ` (${String(elapsedSeconds)}s ago)`;
1412
- return /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", paddingY: 1, paddingX: 2, children: [
1413
- /* @__PURE__ */ jsx4(Logo, {}),
1414
- /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: [
1415
- /* @__PURE__ */ jsx4(Text4, { bold: true, color: "cyan", children: "Kizen App Builder" }),
1416
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "\u2500".repeat(24) })
1417
- ] }),
1418
- /* @__PURE__ */ jsxs3(Box4, { flexDirection: "column", children: [
1419
- /* @__PURE__ */ jsxs3(Box4, { gap: 2, marginBottom: 0, children: [
1420
- /* @__PURE__ */ jsx4(Text4, { bold: true, dimColor: true, children: "App Build" }),
1421
- buildStatus === "pending" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "waiting\u2026" }),
1422
- buildStatus === "building" && /* @__PURE__ */ jsxs3(Box4, { gap: 1, children: [
1423
- /* @__PURE__ */ jsx4(Spinner2, {}),
1424
- /* @__PURE__ */ jsx4(Text4, { children: "Building plugin package\u2026" })
1435
+ return /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", paddingY: 1, paddingX: 2, children: [
1436
+ /* @__PURE__ */ jsx6(AppHeader, { marginBottom: 1 }),
1437
+ /* @__PURE__ */ jsxs4(Box5, { flexDirection: "column", children: [
1438
+ /* @__PURE__ */ jsxs4(Box5, { gap: 2, marginBottom: 0, children: [
1439
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "App Build" }),
1440
+ buildStatus === "pending" && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "waiting\u2026" }),
1441
+ buildStatus === "building" && /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
1442
+ /* @__PURE__ */ jsx6(Spinner, {}),
1443
+ /* @__PURE__ */ jsx6(Text6, { children: "Building plugin package\u2026" })
1425
1444
  ] }),
1426
- buildStatus === "done" && /* @__PURE__ */ jsxs3(Box4, { gap: 1, children: [
1427
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713 Built" }),
1428
- lastBuilt !== null && /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1445
+ buildStatus === "done" && /* @__PURE__ */ jsxs4(Box5, { gap: 1, children: [
1446
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u2713 Built" }),
1447
+ lastBuilt !== null && /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
1429
1448
  "at ",
1430
1449
  lastBuilt.toLocaleTimeString(),
1431
1450
  elapsedLabel
1451
+ ] }),
1452
+ lastBundleSize !== null && /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
1453
+ "\xB7 bundle: ",
1454
+ formatBytes(lastBundleSize)
1432
1455
  ] })
1433
1456
  ] }),
1434
- buildStatus === "error" && /* @__PURE__ */ jsxs3(Text4, { color: "red", children: [
1457
+ buildStatus === "error" && /* @__PURE__ */ jsxs4(Text6, { color: "red", children: [
1435
1458
  "\u2717 ",
1436
1459
  buildError ?? "unknown error"
1437
1460
  ] })
1438
1461
  ] }),
1439
- /* @__PURE__ */ jsx4(
1440
- Box4,
1462
+ /* @__PURE__ */ jsx6(
1463
+ Box5,
1441
1464
  {
1442
1465
  flexDirection: "column",
1443
1466
  height: LOG_DISPLAY + 2,
1444
1467
  borderStyle: "single",
1445
1468
  borderColor: "gray",
1446
1469
  overflow: "hidden",
1447
- children: buildLogHistory.slice(-LOG_DISPLAY).map((log, index) => /* @__PURE__ */ jsx4(Text4, { dimColor: true, wrap: "truncate", children: log }, index))
1470
+ children: buildLogHistory.slice(-LOG_DISPLAY).map((log, index) => /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate", children: log }, index))
1448
1471
  }
1449
1472
  )
1450
1473
  ] }),
1451
- /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
1452
- /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1453
- /* @__PURE__ */ jsx4(Text4, { bold: true, dimColor: true, children: "Web Server" }),
1454
- status === "starting" && /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "starting\u2026" }),
1455
- status === "running" && /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1456
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713 Running" }),
1457
- /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1474
+ /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
1475
+ /* @__PURE__ */ jsxs4(Box5, { gap: 2, children: [
1476
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Web Server" }),
1477
+ status === "starting" && /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "starting\u2026" }),
1478
+ status === "running" && /* @__PURE__ */ jsxs4(Box5, { gap: 2, children: [
1479
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u2713 Running" }),
1480
+ /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
1458
1481
  "\u25CF ",
1459
1482
  wsClientCount,
1460
1483
  " viewer",
1461
1484
  wsClientCount !== 1 ? "s" : ""
1462
1485
  ] })
1463
1486
  ] }),
1464
- status === "error" && /* @__PURE__ */ jsxs3(Text4, { color: "red", children: [
1487
+ status === "error" && /* @__PURE__ */ jsxs4(Text6, { color: "red", children: [
1465
1488
  "\u2717 ",
1466
1489
  errorMessage ?? "Server error"
1467
1490
  ] })
1468
1491
  ] }),
1469
- /* @__PURE__ */ jsx4(
1470
- Box4,
1492
+ /* @__PURE__ */ jsx6(
1493
+ Box5,
1471
1494
  {
1472
1495
  flexDirection: "column",
1473
1496
  height: LOG_DISPLAY + 2,
1474
1497
  borderStyle: "single",
1475
1498
  borderColor: "gray",
1476
1499
  overflow: "hidden",
1477
- children: serverLogHistory.slice(-LOG_DISPLAY).map((log, index) => /* @__PURE__ */ jsx4(Text4, { dimColor: true, wrap: "truncate", children: log }, index))
1500
+ children: serverLogHistory.slice(-LOG_DISPLAY).map((log, index) => /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate", children: log }, index))
1478
1501
  }
1479
1502
  )
1480
1503
  ] }),
1481
- /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, flexDirection: "column", children: [
1482
- /* @__PURE__ */ jsxs3(Box4, { gap: 2, children: [
1483
- /* @__PURE__ */ jsx4(Text4, { bold: true, dimColor: true, children: "Network Proxy" }),
1484
- /* @__PURE__ */ jsx4(Text4, { color: "green", children: "\u2713 Active" }),
1485
- /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1504
+ /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, flexDirection: "column", children: [
1505
+ /* @__PURE__ */ jsxs4(Box5, { gap: 2, children: [
1506
+ /* @__PURE__ */ jsx6(Text6, { bold: true, dimColor: true, children: "Network Proxy" }),
1507
+ /* @__PURE__ */ jsx6(Text6, { color: "green", children: "\u2713 Active" }),
1508
+ /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
1486
1509
  "\u25CF ",
1487
1510
  proxyCacheSize,
1488
1511
  " key",
@@ -1495,26 +1518,26 @@ var DevUI = ({
1495
1518
  " miss"
1496
1519
  ] })
1497
1520
  ] }),
1498
- /* @__PURE__ */ jsx4(
1499
- Box4,
1521
+ /* @__PURE__ */ jsx6(
1522
+ Box5,
1500
1523
  {
1501
1524
  flexDirection: "column",
1502
1525
  height: LOG_DISPLAY + 2,
1503
1526
  borderStyle: "single",
1504
1527
  borderColor: "gray",
1505
1528
  overflow: "hidden",
1506
- children: proxyLogHistory.length === 0 ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: " No requests yet." }) : proxyLogHistory.slice(-LOG_DISPLAY).map((log, i) => /* @__PURE__ */ jsx4(Text4, { dimColor: true, wrap: "truncate", children: log }, i))
1529
+ children: proxyLogHistory.length === 0 ? /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " No requests yet." }) : proxyLogHistory.slice(-LOG_DISPLAY).map((log, i) => /* @__PURE__ */ jsx6(Text6, { dimColor: true, wrap: "truncate", children: log }, i))
1507
1530
  }
1508
1531
  )
1509
1532
  ] }),
1510
- /* @__PURE__ */ jsxs3(Box4, { marginTop: 1, gap: 1, children: [
1511
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "[" }),
1512
- /* @__PURE__ */ jsx4(Text4, { children: "q" }),
1513
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "] quit [" }),
1514
- /* @__PURE__ */ jsx4(Text4, { children: "c" }),
1515
- /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "] credentials [" }),
1516
- /* @__PURE__ */ jsx4(Text4, { children: "v" }),
1517
- /* @__PURE__ */ jsxs3(Text4, { dimColor: true, children: [
1533
+ /* @__PURE__ */ jsxs4(Box5, { marginTop: 1, gap: 1, children: [
1534
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "[" }),
1535
+ /* @__PURE__ */ jsx6(Text6, { children: "q" }),
1536
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "] quit [" }),
1537
+ /* @__PURE__ */ jsx6(Text6, { children: "c" }),
1538
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "] credentials [" }),
1539
+ /* @__PURE__ */ jsx6(Text6, { children: "v" }),
1540
+ /* @__PURE__ */ jsxs4(Text6, { dimColor: true, children: [
1518
1541
  "] ",
1519
1542
  wsClientCount === 0 ? "open viewer" : "relaunch viewer"
1520
1543
  ] })