@kizenapps/cli 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -238,33 +238,45 @@ import { dirname as dirname2, extname, join as join6 } from "path";
238
238
  import { fileURLToPath } from "url";
239
239
  import { WebSocketServer } from "ws";
240
240
 
241
- // src/lib/config.ts
242
- import { mkdir as mkdir2, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
243
- import { join as join4 } from "path";
244
- async function loadConfig(outputDir) {
245
- try {
246
- const content = await readFile2(join4(outputDir, "config.json"), "utf-8");
247
- return JSON.parse(content);
248
- } catch {
249
- return {};
250
- }
251
- }
252
- async function saveConfig(outputDir, config) {
253
- await mkdir2(outputDir, { recursive: true });
254
- await writeFile2(join4(outputDir, "config.json"), JSON.stringify(config, null, 2), "utf-8");
241
+ // src/lib/proxyCache.ts
242
+ function createProxyCache() {
243
+ const cache = /* @__PURE__ */ new Map();
244
+ return {
245
+ get(key, fetcher) {
246
+ const existing = cache.get(key);
247
+ if (existing !== void 0) {
248
+ return existing.then((response) => ({ response, fromCache: true }));
249
+ }
250
+ const pending = fetcher().then(async (response) => {
251
+ const headers = Object.fromEntries(response.headers);
252
+ delete headers["content-encoding"];
253
+ delete headers["content-length"];
254
+ return {
255
+ status: response.status,
256
+ headers,
257
+ body: Buffer.from(await response.arrayBuffer())
258
+ };
259
+ }).catch((error) => {
260
+ cache.delete(key);
261
+ throw error;
262
+ });
263
+ cache.set(key, pending);
264
+ return pending.then((response) => ({ response, fromCache: false }));
265
+ },
266
+ clear() {
267
+ cache.clear();
268
+ }
269
+ };
255
270
  }
256
271
 
257
- // src/ui/CredentialSetupUI.tsx
258
- import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
259
- import { Box as Box3, Text as Text3, useInput } from "ink";
260
-
261
272
  // src/lib/credentials.ts
262
- import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
273
+ import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, writeFile as writeFile2 } from "fs/promises";
263
274
  import { homedir } from "os";
264
- import { dirname, join as join5 } from "path";
275
+ import { dirname, join as join4 } from "path";
265
276
  var ENVIRONMENTS = ["go", "fmo", "staging", "integration", "test1"];
266
- var GLOBAL_CREDENTIALS_DIR = join5(homedir(), ".kizenappbuilder");
267
- var GLOBAL_CREDENTIALS_PATH = join5(GLOBAL_CREDENTIALS_DIR, "credentials.json");
277
+ var GLOBAL_CREDENTIALS_DIR = join4(homedir(), ".kizenappbuilder");
278
+ var GLOBAL_CREDENTIALS_PATH = join4(GLOBAL_CREDENTIALS_DIR, "credentials.json");
279
+ var DEFAULT_PROFILE_NAME = "credentials";
268
280
  function isValidEnvironment(value) {
269
281
  return ENVIRONMENTS.includes(value);
270
282
  }
@@ -282,7 +294,7 @@ function parseCredentials(raw) {
282
294
  };
283
295
  }
284
296
  async function loadCredentialsFromFile(filePath) {
285
- const content = await readFile3(filePath, "utf-8");
297
+ const content = await readFile2(filePath, "utf-8");
286
298
  return parseCredentials(JSON.parse(content));
287
299
  }
288
300
  async function loadGlobalCredentials() {
@@ -293,11 +305,63 @@ async function loadGlobalCredentials() {
293
305
  }
294
306
  }
295
307
  async function saveGlobalCredentials(credentials) {
296
- await mkdir3(dirname(GLOBAL_CREDENTIALS_PATH), { recursive: true });
297
- await writeFile3(GLOBAL_CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), "utf-8");
308
+ await mkdir2(dirname(GLOBAL_CREDENTIALS_PATH), { recursive: true });
309
+ await writeFile2(GLOBAL_CREDENTIALS_PATH, JSON.stringify(credentials, null, 2), "utf-8");
310
+ }
311
+ function getProfilePath(name) {
312
+ return join4(GLOBAL_CREDENTIALS_DIR, `${name}.json`);
313
+ }
314
+ async function listCredentialProfiles() {
315
+ const profiles = [
316
+ { name: DEFAULT_PROFILE_NAME, path: GLOBAL_CREDENTIALS_PATH, isDefault: true }
317
+ ];
318
+ try {
319
+ const entries = await readdir2(GLOBAL_CREDENTIALS_DIR);
320
+ for (const entry of entries) {
321
+ if (!entry.endsWith(".json")) continue;
322
+ const name = entry.slice(0, -5);
323
+ if (name === DEFAULT_PROFILE_NAME) continue;
324
+ profiles.push({ name, path: join4(GLOBAL_CREDENTIALS_DIR, entry), isDefault: false });
325
+ }
326
+ } catch {
327
+ }
328
+ return profiles;
329
+ }
330
+ async function saveCredentialProfile(name, credentials) {
331
+ if (name === DEFAULT_PROFILE_NAME) {
332
+ await saveGlobalCredentials(credentials);
333
+ return;
334
+ }
335
+ await mkdir2(GLOBAL_CREDENTIALS_DIR, { recursive: true });
336
+ await writeFile2(getProfilePath(name), JSON.stringify(credentials, null, 2), "utf-8");
337
+ }
338
+ async function loadCredentialProfile(name) {
339
+ try {
340
+ return await loadCredentialsFromFile(getProfilePath(name));
341
+ } catch {
342
+ return null;
343
+ }
344
+ }
345
+
346
+ // src/lib/config.ts
347
+ import { mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
348
+ import { join as join5 } from "path";
349
+ async function loadConfig(outputDir) {
350
+ try {
351
+ const content = await readFile3(join5(outputDir, "config.json"), "utf-8");
352
+ return JSON.parse(content);
353
+ } catch {
354
+ return {};
355
+ }
356
+ }
357
+ async function saveConfig(outputDir, config) {
358
+ await mkdir3(outputDir, { recursive: true });
359
+ await writeFile3(join5(outputDir, "config.json"), JSON.stringify(config, null, 2), "utf-8");
298
360
  }
299
361
 
300
362
  // src/ui/CredentialSetupUI.tsx
363
+ import { useCallback, useEffect as useEffect2, useState as useState2 } from "react";
364
+ import { Box as Box3, Text as Text3, useInput } from "ink";
301
365
  import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
302
366
  var FIELDS = ["apiKey", "userId", "businessId"];
303
367
  var FIELD_LABELS = {
@@ -308,32 +372,39 @@ var FIELD_LABELS = {
308
372
  var Hint = ({ text }) => /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: text });
309
373
  var CredentialSetupUI = ({
310
374
  initialMode,
375
+ showProfileManager,
311
376
  onComplete,
312
377
  onCancel
313
378
  }) => {
314
379
  const [phase, setPhase] = useState2(
315
- initialMode === "global" ? { type: "loading" } : { type: "mode-select", cursor: 0 }
380
+ initialMode === "global" && !showProfileManager ? { type: "loading" } : initialMode === "global" && showProfileManager ? { type: "profile-loading" } : { type: "mode-select", cursor: 0 }
316
381
  );
317
382
  const [inputBuffer, setInputBuffer] = useState2("");
318
383
  const [error, setError] = useState2(null);
384
+ const [activeProfileName, setActiveProfileName] = useState2(void 0);
319
385
  const handleModeChosen = useCallback(
320
386
  async (mode) => {
321
387
  if (mode === "local") {
322
388
  onComplete({ mode: "local", credentials: null });
323
389
  return;
324
390
  }
325
- setPhase({ type: "loading" });
326
- const existing = await loadGlobalCredentials();
327
- const envCursor2 = existing ? Math.max(0, ENVIRONMENTS.indexOf(existing.environment)) : 0;
328
- setPhase({
329
- type: "creds-entry",
330
- field: 0,
331
- values: existing ?? {},
332
- envCursor: envCursor2
333
- });
391
+ setPhase({ type: "profile-loading" });
334
392
  },
335
393
  [onComplete]
336
394
  );
395
+ const handleProfileChosen = useCallback(async (profile) => {
396
+ if (profile === null) {
397
+ setPhase({ type: "profile-loading" });
398
+ const profiles = await listCredentialProfiles();
399
+ setPhase({ type: "name-entry", nameBuffer: "", existingProfiles: profiles });
400
+ return;
401
+ }
402
+ setActiveProfileName(profile.isDefault ? void 0 : profile.name);
403
+ setPhase({ type: "loading" });
404
+ const existing = profile.isDefault ? await loadGlobalCredentials() : await loadCredentialProfile(profile.name);
405
+ const envCursor2 = existing ? Math.max(0, ENVIRONMENTS.indexOf(existing.environment)) : 0;
406
+ setPhase({ type: "creds-entry", field: 0, values: existing ?? {}, envCursor: envCursor2 });
407
+ }, []);
337
408
  const handleSave = useCallback(
338
409
  async (values2, envCursor2) => {
339
410
  const environment = ENVIRONMENTS[envCursor2] ?? "go";
@@ -345,20 +416,32 @@ var CredentialSetupUI = ({
345
416
  };
346
417
  setPhase({ type: "saving" });
347
418
  try {
348
- await saveGlobalCredentials(credentials);
349
- onComplete({ mode: "global", credentials });
419
+ const profileName = activeProfileName ?? DEFAULT_PROFILE_NAME;
420
+ await saveCredentialProfile(profileName, credentials);
421
+ onComplete({
422
+ mode: "global",
423
+ credentials,
424
+ ...activeProfileName !== void 0 && { profileName: activeProfileName }
425
+ });
350
426
  } catch (err) {
351
427
  setError(err instanceof Error ? err.message : String(err));
352
428
  setPhase({ type: "creds-entry", field: 0, values: values2, envCursor: envCursor2 });
353
429
  }
354
430
  },
355
- [onComplete]
431
+ [onComplete, activeProfileName]
356
432
  );
357
433
  useInput((input, key) => {
358
434
  if (key.ctrl && input === "c") {
359
435
  process.exit(0);
360
436
  }
361
437
  if (key.escape) {
438
+ if (phase.type === "creds-entry" || phase.type === "name-entry") {
439
+ if (initialMode === "global" && showProfileManager) {
440
+ setPhase({ type: "profile-loading" });
441
+ void loadProfileList();
442
+ return;
443
+ }
444
+ }
362
445
  onCancel?.();
363
446
  return;
364
447
  }
@@ -373,6 +456,49 @@ var CredentialSetupUI = ({
373
456
  }
374
457
  return;
375
458
  }
459
+ if (phase.type === "profile-select") {
460
+ const { profiles, cursor } = phase;
461
+ const addNewIdx = profiles.length;
462
+ const totalItems = profiles.length + 1;
463
+ if (key.upArrow) {
464
+ setPhase({ ...phase, cursor: (cursor - 1 + totalItems) % totalItems });
465
+ } else if (key.downArrow) {
466
+ setPhase({ ...phase, cursor: (cursor + 1) % totalItems });
467
+ } else if (key.return) {
468
+ if (cursor === addNewIdx) {
469
+ setPhase({ type: "name-entry", nameBuffer: "", existingProfiles: profiles });
470
+ } else {
471
+ void handleProfileChosen(profiles[cursor] ?? profiles[0]);
472
+ }
473
+ }
474
+ return;
475
+ }
476
+ if (phase.type === "name-entry") {
477
+ const { nameBuffer, nameError: _nameError, existingProfiles } = phase;
478
+ if (key.backspace || key.delete) {
479
+ setPhase({ type: "name-entry", nameBuffer: nameBuffer.slice(0, -1), existingProfiles });
480
+ } else if (key.return) {
481
+ const trimmed = nameBuffer.trim();
482
+ if (!trimmed) {
483
+ setPhase({ ...phase, nameError: "Name cannot be empty" });
484
+ return;
485
+ }
486
+ if (trimmed === DEFAULT_PROFILE_NAME) {
487
+ setPhase({ ...phase, nameError: `"${DEFAULT_PROFILE_NAME}" is reserved` });
488
+ return;
489
+ }
490
+ if (existingProfiles.some((p) => p.name === trimmed)) {
491
+ setPhase({ ...phase, nameError: `Profile "${trimmed}" already exists` });
492
+ return;
493
+ }
494
+ setActiveProfileName(trimmed);
495
+ setPhase({ type: "creds-entry", field: 0, values: {}, envCursor: 0 });
496
+ setInputBuffer("");
497
+ } else if (input && !key.ctrl && !key.meta) {
498
+ setPhase({ type: "name-entry", nameBuffer: nameBuffer + input, existingProfiles });
499
+ }
500
+ return;
501
+ }
376
502
  if (phase.type === "creds-entry") {
377
503
  const { field, values: values2, envCursor: envCursor2 } = phase;
378
504
  const isEnvField2 = field === FIELDS.length;
@@ -417,9 +543,22 @@ var CredentialSetupUI = ({
417
543
  }
418
544
  }
419
545
  });
546
+ const loadProfileList = useCallback(async () => {
547
+ const profiles = await listCredentialProfiles();
548
+ setPhase({ type: "profile-select", profiles, cursor: 0 });
549
+ }, []);
550
+ useEffect2(() => {
551
+ if (phase.type === "profile-loading") {
552
+ void loadProfileList();
553
+ }
554
+ }, [phase.type, loadProfileList]);
420
555
  useEffect2(() => {
421
- if (initialMode === "global") {
422
- void handleModeChosen("global");
556
+ if (initialMode === "global" && !showProfileManager) {
557
+ void (async () => {
558
+ const existing = await loadGlobalCredentials();
559
+ const envCursor2 = existing ? Math.max(0, ENVIRONMENTS.indexOf(existing.environment)) : 0;
560
+ setPhase({ type: "creds-entry", field: 0, values: existing ?? {}, envCursor: envCursor2 });
561
+ })();
423
562
  }
424
563
  }, []);
425
564
  useEffect2(() => {
@@ -427,8 +566,9 @@ var CredentialSetupUI = ({
427
566
  setInputBuffer(phase.values.apiKey ?? "");
428
567
  }
429
568
  }, [phase.type]);
430
- if (phase.type === "loading" || phase.type === "saving") {
431
- return /* @__PURE__ */ jsx3(Box3, { paddingY: 1, paddingX: 2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: phase.type === "loading" ? "Loading credentials\u2026" : "Saving credentials\u2026" }) });
569
+ if (phase.type === "loading" || phase.type === "saving" || phase.type === "profile-loading") {
570
+ const label = phase.type === "saving" ? "Saving credentials\u2026" : phase.type === "profile-loading" ? "Loading profiles\u2026" : "Loading credentials\u2026";
571
+ return /* @__PURE__ */ jsx3(Box3, { paddingY: 1, paddingX: 2, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: label }) });
432
572
  }
433
573
  if (phase.type === "done") {
434
574
  return null;
@@ -458,8 +598,58 @@ var CredentialSetupUI = ({
458
598
  /* @__PURE__ */ jsx3(Hint, { text: "\u2191\u2193 to move \xB7 Enter to select \xB7 Ctrl+C to quit" })
459
599
  ] });
460
600
  }
601
+ if (phase.type === "profile-select") {
602
+ const { profiles, cursor } = phase;
603
+ const addNewIdx = profiles.length;
604
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
605
+ /* @__PURE__ */ jsx3(Logo, {}),
606
+ /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginTop: 1, children: [
607
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Kizen App Builder" }),
608
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(24) })
609
+ ] }),
610
+ /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", gap: 0, children: [
611
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Credential profiles" }),
612
+ profiles.map((profile, i) => {
613
+ const selected = cursor === i;
614
+ return /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
615
+ /* @__PURE__ */ jsx3(Text3, { ...selected && { color: "cyan" }, children: selected ? "\u276F" : " " }),
616
+ /* @__PURE__ */ jsx3(Text3, { bold: selected, ...selected && { color: "cyan" }, children: profile.isDefault ? "Default" : profile.name }),
617
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: profile.path })
618
+ ] }, profile.name);
619
+ }),
620
+ /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
621
+ /* @__PURE__ */ jsx3(Text3, { ...cursor === addNewIdx && { color: "cyan" }, children: cursor === addNewIdx ? "\u276F" : " " }),
622
+ /* @__PURE__ */ jsx3(Text3, { bold: cursor === addNewIdx, ...cursor === addNewIdx && { color: "cyan" }, children: "+ Add new profile" })
623
+ ] })
624
+ ] }),
625
+ /* @__PURE__ */ jsx3(Hint, { text: "\u2191\u2193 to move \xB7 Enter to select \xB7 Esc to cancel" })
626
+ ] });
627
+ }
628
+ if (phase.type === "name-entry") {
629
+ const { nameBuffer, nameError } = phase;
630
+ return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
631
+ /* @__PURE__ */ jsx3(Logo, {}),
632
+ /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginTop: 1, children: [
633
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Kizen App Builder" }),
634
+ /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(24) })
635
+ ] }),
636
+ /* @__PURE__ */ jsx3(Box3, { flexDirection: "column", gap: 0, children: /* @__PURE__ */ jsx3(Text3, { bold: true, children: "New credential profile" }) }),
637
+ nameError && /* @__PURE__ */ jsxs2(Text3, { color: "red", children: [
638
+ "Error: ",
639
+ nameError
640
+ ] }),
641
+ /* @__PURE__ */ jsxs2(Box3, { gap: 2, children: [
642
+ /* @__PURE__ */ jsx3(Box3, { width: 12, children: /* @__PURE__ */ jsx3(Text3, { bold: true, color: "cyan", children: "Profile name" }) }),
643
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: ">" }),
644
+ /* @__PURE__ */ jsx3(Text3, { children: nameBuffer }),
645
+ /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "\u2588" })
646
+ ] }),
647
+ /* @__PURE__ */ jsx3(Hint, { text: "Type a name \xB7 Enter to confirm \xB7 Esc to go back" })
648
+ ] });
649
+ }
461
650
  const { field: activeField, values, envCursor } = phase;
462
651
  const isEnvField = activeField === FIELDS.length;
652
+ const profileLabel = activeProfileName !== void 0 ? activeProfileName : DEFAULT_PROFILE_NAME;
463
653
  return /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", paddingY: 1, paddingX: 2, gap: 1, children: [
464
654
  /* @__PURE__ */ jsx3(Logo, {}),
465
655
  /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", marginTop: 1, children: [
@@ -467,10 +657,10 @@ var CredentialSetupUI = ({
467
657
  /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "\u2500".repeat(24) })
468
658
  ] }),
469
659
  /* @__PURE__ */ jsxs2(Box3, { flexDirection: "column", gap: 0, children: [
470
- /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Global credentials" }),
660
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: activeProfileName !== void 0 ? `Profile: ${activeProfileName}` : "Global credentials" }),
471
661
  /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
472
662
  "Saved to: ",
473
- GLOBAL_CREDENTIALS_PATH
663
+ activeProfileName !== void 0 ? `~/.kizenappbuilder/${activeProfileName}.json` : GLOBAL_CREDENTIALS_PATH
474
664
  ] })
475
665
  ] }),
476
666
  error && /* @__PURE__ */ jsxs2(Text3, { color: "red", children: [
@@ -500,12 +690,23 @@ var CredentialSetupUI = ({
500
690
  }) })
501
691
  ] })
502
692
  ] }),
503
- 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" })
693
+ 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" }),
694
+ /* @__PURE__ */ jsxs2(Text3, { dimColor: true, children: [
695
+ "Profile: ",
696
+ profileLabel
697
+ ] })
504
698
  ] });
505
699
  };
506
700
 
507
701
  // src/ui/DevUI.tsx
508
702
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
703
+ function proxyLogEntryToString(entry) {
704
+ if (entry.kind === "info") {
705
+ return entry.message;
706
+ }
707
+ const cache = entry.fromCache ? " [cache]" : "";
708
+ return `${entry.method} ${entry.url} \u2192 ${String(entry.status)}${cache}`;
709
+ }
509
710
  var SKIP_WATCH_PREFIXES = [".kizenapp", ".git"];
510
711
  var LOG_LIMIT = 50;
511
712
  var LOG_DISPLAY = 8;
@@ -553,7 +754,8 @@ async function fileExists(filePath) {
553
754
  return false;
554
755
  }
555
756
  }
556
- function createRequestHandler(viewerPath, createServerLog, createProxyLog, credentialsRef) {
757
+ function createRequestHandler(viewerPath, createServerLog, createProxyLog, credentialsRef, activeProfileRef, outputDir, broadcast) {
758
+ const proxyCache = createProxyCache();
557
759
  return (req, res) => {
558
760
  void (async () => {
559
761
  const url = req.url ?? "/";
@@ -564,6 +766,34 @@ function createRequestHandler(viewerPath, createServerLog, createProxyLog, crede
564
766
  res.end(credentialsRef.current !== null ? JSON.stringify(credentialsRef.current) : "{}");
565
767
  return;
566
768
  }
769
+ if (url === "/api/credential-profiles" && req.method === "GET") {
770
+ const profiles = await listCredentialProfiles();
771
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
772
+ res.end(JSON.stringify({ profiles, active: activeProfileRef.current }));
773
+ return;
774
+ }
775
+ if (url === "/api/credentials/switch" && req.method === "POST") {
776
+ const chunks = [];
777
+ for await (const chunk of req) {
778
+ chunks.push(chunk);
779
+ }
780
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8") || "{}");
781
+ const profileName = body.profile;
782
+ const creds = profileName === void 0 || profileName === "" ? await loadGlobalCredentials() : await loadCredentialProfile(profileName);
783
+ if (creds !== null) {
784
+ credentialsRef.current = creds;
785
+ activeProfileRef.current = profileName || void 0;
786
+ const active = activeProfileRef.current;
787
+ await saveConfig(outputDir, {
788
+ credentialMode: "global",
789
+ ...active !== void 0 && { activeCredentialProfile: active }
790
+ });
791
+ broadcast({ type: "credentials-updated", credentials: creds });
792
+ }
793
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
794
+ res.end(JSON.stringify({ ok: creds !== null }));
795
+ return;
796
+ }
567
797
  if (url === "/api/bundle") {
568
798
  const bundlePath = join6(process.cwd(), ".kizenapp", "bundle.json");
569
799
  try {
@@ -576,6 +806,13 @@ function createRequestHandler(viewerPath, createServerLog, createProxyLog, crede
576
806
  }
577
807
  return;
578
808
  }
809
+ if (url === "/api/proxy-cache/clear") {
810
+ proxyCache.clear();
811
+ createProxyLog({ kind: "info", message: "Proxy cache cleared" });
812
+ res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
813
+ res.end('{"ok":true}');
814
+ return;
815
+ }
579
816
  if (url.startsWith("/api/proxy")) {
580
817
  const proxyTarget = req.headers["x-proxy-target"];
581
818
  if (typeof proxyTarget !== "string") {
@@ -585,19 +822,46 @@ function createRequestHandler(viewerPath, createServerLog, createProxyLog, crede
585
822
  }
586
823
  const upstreamPath = url.slice("/api/proxy".length) || "/";
587
824
  const upstreamUrl = `${proxyTarget}${upstreamPath}`;
825
+ const method = req.method ?? "GET";
588
826
  const chunks = [];
589
827
  for await (const chunk of req) {
590
828
  chunks.push(chunk);
591
829
  }
592
- const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
593
830
  const { host, "x-proxy-target": _drop, ...forwardHeaders } = req.headers;
831
+ if (method === "GET") {
832
+ const cacheKey = `GET::${upstreamUrl}`;
833
+ const { response: cached, fromCache } = await proxyCache.get(
834
+ cacheKey,
835
+ () => fetch(upstreamUrl, {
836
+ method: "GET",
837
+ headers: forwardHeaders
838
+ })
839
+ );
840
+ createProxyLog({
841
+ kind: "request",
842
+ method: "GET",
843
+ status: cached.status,
844
+ fromCache,
845
+ url: upstreamPath
846
+ });
847
+ res.writeHead(cached.status, cached.headers);
848
+ res.end(cached.body);
849
+ return;
850
+ }
851
+ const body = chunks.length > 0 ? Buffer.concat(chunks) : void 0;
594
852
  const resolvedBody = body && body.length > 0 ? body : void 0;
595
853
  const upstream = await fetch(upstreamUrl, {
596
- ...req.method !== void 0 && { method: req.method },
854
+ method,
597
855
  headers: forwardHeaders,
598
856
  ...resolvedBody !== void 0 && { body: resolvedBody }
599
857
  });
600
- createProxyLog(`${req.method ?? "GET"} ${upstreamPath} \u2192 ${String(upstream.status)}`);
858
+ createProxyLog({
859
+ kind: "request",
860
+ method,
861
+ status: upstream.status,
862
+ fromCache: false,
863
+ url: upstreamPath
864
+ });
601
865
  const responseHeaders = Object.fromEntries(upstream.headers);
602
866
  delete responseHeaders["content-encoding"];
603
867
  delete responseHeaders["content-length"];
@@ -613,9 +877,20 @@ function createRequestHandler(viewerPath, createServerLog, createProxyLog, crede
613
877
  res.writeHead(200, { "Content-Type": mimeType });
614
878
  createReadStream(resolvedPath).pipe(res);
615
879
  } catch (err) {
616
- createProxyLog(
617
- `Error handling ${url}: ${err instanceof Error ? err.message : String(err)}`
618
- );
880
+ if (url.startsWith("/api/proxy")) {
881
+ createProxyLog({
882
+ kind: "request",
883
+ method: req.method ?? "GET",
884
+ status: 502,
885
+ fromCache: false,
886
+ url: url.slice("/api/proxy".length) || "/"
887
+ });
888
+ } else {
889
+ createProxyLog({
890
+ kind: "info",
891
+ message: `Error handling ${url}: ${err instanceof Error ? err.message : String(err)}`
892
+ });
893
+ }
619
894
  if (!res.headersSent) {
620
895
  res.writeHead(502);
621
896
  res.end("Bad Gateway");
@@ -628,10 +903,16 @@ var DevUI = ({
628
903
  port,
629
904
  pluginDir,
630
905
  outputDir,
631
- credentials: initialCredentials
906
+ credentials: initialCredentials,
907
+ credentialMode: initialCredentialMode,
908
+ activeCredentialProfile: initialActiveProfile
632
909
  }) => {
633
910
  const credentialsRef = useRef(initialCredentials);
911
+ const activeProfileRef = useRef(initialActiveProfile);
634
912
  const [credMode, setCredMode] = useState3("main");
913
+ const [credentialMode, setCredentialMode] = useState3(
914
+ initialCredentialMode
915
+ );
635
916
  const [status, setStatus] = useState3("starting");
636
917
  const [errorMessage, setErrorMessage] = useState3(null);
637
918
  const [serverLogHistory, setServerLogHistory] = useState3([]);
@@ -701,18 +982,26 @@ var DevUI = ({
701
982
  const handleCredentialsDone = useCallback2(
702
983
  (result) => {
703
984
  credentialsRef.current = result.credentials;
985
+ activeProfileRef.current = result.profileName;
704
986
  setCredMode("main");
705
- void saveConfig(outputDir, { credentialMode: result.mode });
987
+ setCredentialMode(result.mode);
988
+ const profile = result.profileName;
989
+ void saveConfig(outputDir, {
990
+ credentialMode: result.mode,
991
+ ...profile !== void 0 && { activeCredentialProfile: profile }
992
+ });
706
993
  broadcast({ type: "credentials-updated", credentials: result.credentials });
707
994
  },
708
995
  [outputDir, broadcast]
709
996
  );
710
997
  const createProxyLog = useCallback2(
711
- (message) => {
998
+ (entry) => {
712
999
  setProxyLogHistory(
713
- (h) => [...h, `${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${message}`].slice(-LOG_LIMIT)
1000
+ (h) => [...h, `${(/* @__PURE__ */ new Date()).toLocaleTimeString()}: ${proxyLogEntryToString(entry)}`].slice(
1001
+ -LOG_LIMIT
1002
+ )
714
1003
  );
715
- broadcast({ type: "proxy-log", message });
1004
+ broadcast({ type: "proxy-log", entry });
716
1005
  },
717
1006
  [broadcast]
718
1007
  );
@@ -786,7 +1075,10 @@ var DevUI = ({
786
1075
  viewerPath,
787
1076
  createServerLog,
788
1077
  createProxyLog,
789
- credentialsRef
1078
+ credentialsRef,
1079
+ activeProfileRef,
1080
+ outputDir,
1081
+ broadcast
790
1082
  );
791
1083
  const server = createServer(handler);
792
1084
  const wss = new WebSocketServer({ server });
@@ -823,6 +1115,8 @@ var DevUI = ({
823
1115
  return /* @__PURE__ */ jsx4(
824
1116
  CredentialSetupUI,
825
1117
  {
1118
+ ...credentialMode !== void 0 && { initialMode: credentialMode },
1119
+ showProfileManager: credentialMode === "global",
826
1120
  onComplete: handleCredentialsDone,
827
1121
  onCancel: () => {
828
1122
  setCredMode("main");
@@ -956,6 +1250,7 @@ function devCommand(program2) {
956
1250
  ensureGitignore(pluginDir);
957
1251
  let credentials = null;
958
1252
  let credentialMode;
1253
+ let activeCredentialProfile;
959
1254
  if (options.credentials) {
960
1255
  credentials = await loadCredentialsFromFile(options.credentials);
961
1256
  } else {
@@ -964,16 +1259,31 @@ function devCommand(program2) {
964
1259
  credentialMode = "local";
965
1260
  } else if (config.credentialMode === "global") {
966
1261
  credentialMode = "global";
967
- credentials = await loadGlobalCredentials();
1262
+ activeCredentialProfile = config.activeCredentialProfile;
1263
+ if (activeCredentialProfile) {
1264
+ credentials = await loadCredentialProfile(activeCredentialProfile);
1265
+ if (!credentials) {
1266
+ credentials = await loadGlobalCredentials();
1267
+ activeCredentialProfile = void 0;
1268
+ }
1269
+ } else {
1270
+ credentials = await loadGlobalCredentials();
1271
+ }
968
1272
  if (!credentials) {
969
1273
  const result = await runSetupUI("global");
970
1274
  credentials = result.credentials;
1275
+ activeCredentialProfile = result.profileName;
971
1276
  }
972
1277
  } else {
973
1278
  const result = await runSetupUI();
974
1279
  credentials = result.credentials;
975
1280
  credentialMode = result.mode;
976
- await saveConfig(outputDir, { credentialMode: result.mode });
1281
+ activeCredentialProfile = result.profileName;
1282
+ const profile = result.profileName;
1283
+ await saveConfig(outputDir, {
1284
+ credentialMode: result.mode,
1285
+ ...profile !== void 0 && { activeCredentialProfile: profile }
1286
+ });
977
1287
  }
978
1288
  }
979
1289
  const { waitUntilExit } = render2(
@@ -982,7 +1292,8 @@ function devCommand(program2) {
982
1292
  pluginDir,
983
1293
  outputDir,
984
1294
  credentials,
985
- ...credentialMode !== void 0 && { credentialMode }
1295
+ ...credentialMode !== void 0 && { credentialMode },
1296
+ ...activeCredentialProfile !== void 0 && { activeCredentialProfile }
986
1297
  }),
987
1298
  { exitOnCtrlC: false }
988
1299
  );