@ollie-shop/cli 1.4.1 → 1.6.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
@@ -70,6 +70,10 @@ function HelpCommand() {
70
70
  /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "init" }) }),
71
71
  /* @__PURE__ */ jsx(Text, { children: "Write store/version IDs to ollie.json" })
72
72
  ] }),
73
+ /* @__PURE__ */ jsxs(Box, { children: [
74
+ /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "setup" }) }),
75
+ /* @__PURE__ */ jsx(Text, { children: "Migrate legacy meta.json into the config components map" })
76
+ ] }),
73
77
  /* @__PURE__ */ jsxs(Box, { children: [
74
78
  /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "help" }) }),
75
79
  /* @__PURE__ */ jsx(Text, { children: "Show this help message" })
@@ -111,6 +115,10 @@ function HelpCommand() {
111
115
  /* @__PURE__ */ jsxs(Box, { children: [
112
116
  /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "--no-open" }) }),
113
117
  /* @__PURE__ */ jsx(Text, { children: "start: don't auto-open Studio (also honored via CI env)" })
118
+ ] }),
119
+ /* @__PURE__ */ jsxs(Box, { children: [
120
+ /* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "yellow", children: "--browser-logs" }) }),
121
+ /* @__PURE__ */ jsx(Text, { children: "start: stream custom components' browser console to terminal" })
114
122
  ] })
115
123
  ] }),
116
124
  /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(Text, { bold: true, children: "Examples:" }) }),
@@ -118,6 +126,7 @@ function HelpCommand() {
118
126
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop login" }),
119
127
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --stage dev" }),
120
128
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --no-open" }),
129
+ /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop start --browser-logs" }),
121
130
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop whoami -o json" }),
122
131
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop schema store.create" }),
123
132
  /* @__PURE__ */ jsx(Text, { dimColor: true, children: '$ ollieshop store create --name "My Store" --platform vtex --platform-store-id mystore' }),
@@ -386,9 +395,14 @@ import { useCallback, useEffect as useEffect2, useRef, useState as useState2 } f
386
395
  import fs2 from "fs/promises";
387
396
  import path2 from "path";
388
397
  import { z } from "zod";
398
+ var ComponentEntrySchema = z.object({
399
+ path: z.string(),
400
+ slot: z.string().optional()
401
+ });
389
402
  var OllieConfigSchema = z.object({
390
403
  storeId: z.string().uuid(),
391
- versionId: z.string().uuid().optional()
404
+ versionId: z.string().uuid().optional(),
405
+ components: z.record(z.string(), ComponentEntrySchema).optional()
392
406
  }).passthrough();
393
407
  var CONFIG_FILE = "ollie.json";
394
408
  function getConfigFileName(stage) {
@@ -443,6 +457,51 @@ async function saveConfig(config, options = {}) {
443
457
  };
444
458
  await fs2.writeFile(configPath, JSON.stringify(merged, null, 2));
445
459
  }
460
+ async function listStages(cwd = process.cwd()) {
461
+ let files;
462
+ try {
463
+ files = await fs2.readdir(cwd);
464
+ } catch {
465
+ return [];
466
+ }
467
+ const seen = /* @__PURE__ */ new Set();
468
+ const stages = [];
469
+ for (const file of files.sort()) {
470
+ const match = file.match(/^ollie(?:\.([^.]+))?\.json$/);
471
+ if (!match) continue;
472
+ const stage = match[1] ?? "prod";
473
+ if (seen.has(stage)) continue;
474
+ seen.add(stage);
475
+ let versionId;
476
+ try {
477
+ const config = await loadConfig({ cwd, stage: match[1] });
478
+ versionId = config?.versionId;
479
+ } catch {
480
+ }
481
+ stages.push({ stage, versionId });
482
+ }
483
+ return stages;
484
+ }
485
+ async function upsertComponentEntry(id, values, options = {}) {
486
+ const { cwd = process.cwd(), stage } = options;
487
+ const configPath = path2.join(cwd, getConfigFileName(stage));
488
+ const existing = await loadConfigFile(configPath) ?? {};
489
+ const components = {
490
+ ...existing.components ?? {}
491
+ };
492
+ const prev = components[id];
493
+ const slot = values.slot ?? prev?.slot;
494
+ const entry = {
495
+ path: prev?.path ?? values.defaultPath,
496
+ ...slot !== void 0 ? { slot } : {}
497
+ };
498
+ components[id] = entry;
499
+ await fs2.writeFile(
500
+ configPath,
501
+ JSON.stringify({ ...existing, components }, null, 2)
502
+ );
503
+ return entry;
504
+ }
446
505
  function resolveStage(cliStage) {
447
506
  return cliStage || process.env.OLLIE_STAGE || void 0;
448
507
  }
@@ -579,19 +638,84 @@ async function readComponentMeta(componentDir, stage) {
579
638
  return null;
580
639
  }
581
640
  }
641
+ async function componentsByDir(cwd, stageLabel) {
642
+ const byDir = /* @__PURE__ */ new Map();
643
+ let config = null;
644
+ try {
645
+ config = await loadConfig({ cwd, stage: stageLabel });
646
+ } catch {
647
+ return byDir;
648
+ }
649
+ for (const [id, entry] of Object.entries(config?.components ?? {})) {
650
+ const dir = path4.resolve(cwd, entry.path);
651
+ if (!byDir.has(dir)) byDir.set(dir, { id, slot: entry.slot });
652
+ }
653
+ return byDir;
654
+ }
582
655
  async function discoverComponents(options = {}) {
583
656
  const { cwd = process.cwd(), stage } = options;
657
+ const activeStage = stage ?? "prod";
658
+ const buildDir = path4.join(cwd, "node_modules/.ollie", "build");
659
+ const components = [];
660
+ const mappedDirs = /* @__PURE__ */ new Set();
661
+ const activeByDir = await componentsByDir(cwd, activeStage);
662
+ const otherStageByDir = /* @__PURE__ */ new Map();
663
+ try {
664
+ for (const { stage: label } of await listStages(cwd)) {
665
+ if (label === activeStage) continue;
666
+ for (const [dir, entry] of await componentsByDir(cwd, label)) {
667
+ if (!activeByDir.has(dir) && !otherStageByDir.has(dir)) {
668
+ otherStageByDir.set(dir, entry);
669
+ }
670
+ }
671
+ }
672
+ } catch {
673
+ }
674
+ for (const [componentDir, entry] of activeByDir) {
675
+ const entryPoint = path4.join(componentDir, "index.tsx");
676
+ try {
677
+ await fs4.access(entryPoint);
678
+ } catch {
679
+ console.warn(
680
+ `${entry.id}: ${componentDir}/index.tsx not found - skipping`
681
+ );
682
+ continue;
683
+ }
684
+ const name = path4.basename(componentDir);
685
+ mappedDirs.add(componentDir);
686
+ components.push({
687
+ id: entry.id,
688
+ name,
689
+ slot: entry.slot,
690
+ entryPoint,
691
+ outfile: path4.join(buildDir, name, "index.js")
692
+ });
693
+ }
584
694
  const componentsDir = path4.join(cwd, "components");
585
695
  try {
586
696
  await fs4.access(componentsDir);
587
697
  } catch {
588
- return [];
698
+ return components;
589
699
  }
590
700
  const entries = await glob("*/index.tsx", { cwd: componentsDir });
591
- const components = [];
592
701
  for (const entry of entries) {
593
702
  const name = path4.dirname(entry);
594
- const componentDir = path4.join(componentsDir, name);
703
+ const componentDir = path4.resolve(componentsDir, name);
704
+ if (mappedDirs.has(componentDir)) continue;
705
+ const entryPoint = path4.join(componentsDir, entry);
706
+ const outfile = path4.join(buildDir, name, "index.js");
707
+ const other = otherStageByDir.get(componentDir);
708
+ if (other) {
709
+ components.push({
710
+ id: other.id,
711
+ name,
712
+ slot: other.slot,
713
+ otherStage: true,
714
+ entryPoint,
715
+ outfile
716
+ });
717
+ continue;
718
+ }
595
719
  const meta = await readComponentMeta(componentDir, stage);
596
720
  const id = meta?.id ?? `studio-${name}`;
597
721
  const isUnlinked = !meta?.id;
@@ -604,14 +728,113 @@ async function discoverComponents(options = {}) {
604
728
  id,
605
729
  name,
606
730
  slot: meta?.slot,
607
- entryPoint: path4.join(componentsDir, entry),
608
- outfile: path4.join(cwd, "node_modules/.ollie", "build", name, "index.js")
731
+ ...isUnlinked ? { unlinked: true } : {},
732
+ entryPoint,
733
+ outfile
609
734
  });
610
735
  }
611
736
  return components;
612
737
  }
738
+ function browserLogBridge(endpoint) {
739
+ const target = JSON.stringify(endpoint);
740
+ return `(() => {
741
+ if (typeof window === "undefined" || window.__ollieLogBridge) return;
742
+ window.__ollieLogBridge = true;
743
+
744
+ const endpoint = ${target};
745
+ const SLOT_FRAME = /ollie-slot\\/([^.\\s:]+)\\.js/;
746
+ const LEVELS = ["log", "info", "warn", "error", "debug"];
747
+ const MAX_MESSAGE = 2000;
748
+ const MAX_BATCH = 20;
749
+ const FLUSH_MS = 200;
750
+
751
+ let queue = [];
752
+ let flushTimer = null;
753
+
754
+ function format(value) {
755
+ if (typeof value === "string") return value;
756
+ try {
757
+ return JSON.stringify(value);
758
+ } catch (_) {
759
+ return String(value);
760
+ }
761
+ }
762
+
763
+ function slotName(stack) {
764
+ const match = (stack || "").match(SLOT_FRAME);
765
+ return match ? match[1] : null;
766
+ }
767
+
768
+ function flush() {
769
+ if (flushTimer) {
770
+ clearTimeout(flushTimer);
771
+ flushTimer = null;
772
+ }
773
+ if (queue.length === 0) return;
774
+ const batch = queue;
775
+ queue = [];
776
+ try {
777
+ fetch(endpoint, {
778
+ method: "POST",
779
+ mode: "cors",
780
+ keepalive: true,
781
+ headers: { "Content-Type": "text/plain" },
782
+ body: JSON.stringify({ batch: batch }),
783
+ }).catch(function () {});
784
+ } catch (_) {}
785
+ }
786
+
787
+ function forward(level, component, args) {
788
+ try {
789
+ let message = args.map(format).join(" ");
790
+ if (message.length > MAX_MESSAGE) {
791
+ message = message.slice(0, MAX_MESSAGE) + " \u2026(truncated)";
792
+ }
793
+ queue.push({ level: level, component: component, message: message });
794
+ if (queue.length >= MAX_BATCH) {
795
+ flush();
796
+ } else if (!flushTimer) {
797
+ flushTimer = setTimeout(flush, FLUSH_MS);
798
+ }
799
+ } catch (_) {}
800
+ }
801
+
802
+ for (const level of LEVELS) {
803
+ const original = console[level]
804
+ ? console[level].bind(console)
805
+ : function () {};
806
+ console[level] = function () {
807
+ const args = Array.prototype.slice.call(arguments);
808
+ original.apply(null, args);
809
+ const component = slotName(new Error().stack);
810
+ if (component) forward(level, component, args);
811
+ };
812
+ }
813
+
814
+ window.addEventListener("error", function (event) {
815
+ const component = slotName(event.error && event.error.stack);
816
+ if (component) forward("error", component, [event.message]);
817
+ });
818
+
819
+ window.addEventListener("unhandledrejection", function (event) {
820
+ const reason = event.reason;
821
+ const component = slotName(reason && reason.stack);
822
+ if (component) {
823
+ const message = reason && reason.message ? reason.message : String(reason);
824
+ forward("error", component, [message]);
825
+ }
826
+ });
827
+
828
+ window.addEventListener("pagehide", flush);
829
+ })();`;
830
+ }
613
831
  async function createBuildContext(components, options = {}) {
614
- const { cwd = process.cwd(), stage, onBuildEnd } = options;
832
+ const {
833
+ cwd = process.cwd(),
834
+ stage,
835
+ onBuildEnd,
836
+ browserLogsEndpoint
837
+ } = options;
615
838
  const outdir = path4.join(cwd, "node_modules/.ollie", "build");
616
839
  await fs4.mkdir(outdir, { recursive: true });
617
840
  const entryPoints = {};
@@ -669,7 +892,8 @@ async function createBuildContext(components, options = {}) {
669
892
  logLevel: "silent",
670
893
  // We handle logging ourselves
671
894
  jsx: "automatic",
672
- plugins: [manifestPlugin]
895
+ plugins: [manifestPlugin],
896
+ ...browserLogsEndpoint ? { banner: { js: browserLogBridge(browserLogsEndpoint) } } : {}
673
897
  });
674
898
  return ctx;
675
899
  }
@@ -680,29 +904,37 @@ async function startDevServer(options = {}) {
680
904
  cwd = process.cwd(),
681
905
  stage,
682
906
  onRequest,
683
- onBuildEnd
907
+ onBuildEnd,
908
+ onBrowserLogs
684
909
  } = options;
685
910
  const servedir = path4.join(cwd, "node_modules/.ollie", "build");
686
911
  const componentsDir = path4.join(cwd, "components");
687
912
  const internalPort = port + 1;
913
+ const browserLogsEndpoint = onBrowserLogs ? `http://${host}:${port}/__log` : void 0;
914
+ let activeStage = stage ?? "prod";
688
915
  let ctx = null;
689
916
  let entryNames = /* @__PURE__ */ new Set();
690
917
  async function buildAndServe(components) {
691
918
  entryNames = new Set(components.map((c) => c.name));
692
- ctx = await createBuildContext(components, { cwd, stage, onBuildEnd });
919
+ ctx = await createBuildContext(components, {
920
+ cwd,
921
+ stage: activeStage,
922
+ onBuildEnd,
923
+ browserLogsEndpoint
924
+ });
693
925
  await ctx.rebuild();
694
926
  await ctx.watch();
695
927
  await ctx.serve({ port: internalPort, host, servedir, onRequest });
696
928
  }
697
929
  async function recreate() {
698
- const components = await discoverComponents({ cwd, stage });
930
+ const components = await discoverComponents({ cwd, stage: activeStage });
699
931
  const oldCtx = ctx;
700
932
  ctx = null;
701
933
  if (oldCtx) await oldCtx.dispose();
702
934
  await buildAndServe(components);
703
935
  notifyComponentsChanged(components);
704
936
  }
705
- await buildAndServe(await discoverComponents({ cwd, stage }));
937
+ await buildAndServe(await discoverComponents({ cwd, stage: activeStage }));
706
938
  async function currentComponentNames() {
707
939
  try {
708
940
  const entries = await glob("*/index.tsx", { cwd: componentsDir });
@@ -859,6 +1091,55 @@ data: ${payload}
859
1091
  }
860
1092
  return;
861
1093
  }
1094
+ if (url.pathname === "/stages" && req.method === "GET") {
1095
+ res.setHeader("Access-Control-Allow-Origin", "*");
1096
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
1097
+ res.setHeader("Content-Type", "application/json");
1098
+ const stages = await listStages(cwd);
1099
+ res.end(JSON.stringify({ active: activeStage, stages }));
1100
+ return;
1101
+ }
1102
+ if (url.pathname === "/stage" && req.method === "POST") {
1103
+ res.setHeader("Access-Control-Allow-Origin", "*");
1104
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
1105
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
1106
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
1107
+ let body = "";
1108
+ req.on("data", (chunk) => {
1109
+ body += chunk.toString();
1110
+ });
1111
+ req.on("end", async () => {
1112
+ try {
1113
+ const { stage: next } = JSON.parse(body);
1114
+ if (typeof next !== "string" || !next) {
1115
+ res.statusCode = 400;
1116
+ res.setHeader("Content-Type", "application/json");
1117
+ res.end(JSON.stringify({ error: "Missing 'stage' in body" }));
1118
+ return;
1119
+ }
1120
+ activeStage = next;
1121
+ await recreate();
1122
+ let versionId;
1123
+ try {
1124
+ versionId = (await loadConfig({ cwd, stage: activeStage }))?.versionId;
1125
+ } catch {
1126
+ }
1127
+ res.setHeader("Content-Type", "application/json");
1128
+ res.end(
1129
+ JSON.stringify({ success: true, stage: activeStage, versionId })
1130
+ );
1131
+ } catch (err) {
1132
+ res.statusCode = 400;
1133
+ res.setHeader("Content-Type", "application/json");
1134
+ res.end(
1135
+ JSON.stringify({
1136
+ error: err instanceof Error ? err.message : "Invalid JSON body"
1137
+ })
1138
+ );
1139
+ }
1140
+ });
1141
+ return;
1142
+ }
862
1143
  if (url.pathname === "/meta" && req.method === "POST") {
863
1144
  const componentName = url.searchParams.get("component");
864
1145
  if (!componentName) {
@@ -880,6 +1161,32 @@ data: ${payload}
880
1161
  req.on("end", async () => {
881
1162
  try {
882
1163
  const updates = JSON.parse(body);
1164
+ let cfg = null;
1165
+ try {
1166
+ cfg = await loadConfig({ cwd, stage: activeStage });
1167
+ } catch {
1168
+ }
1169
+ if (cfg?.components !== void 0) {
1170
+ const id = typeof updates.id === "string" ? updates.id : void 0;
1171
+ if (!id) {
1172
+ res.statusCode = 400;
1173
+ res.setHeader("Content-Type", "application/json");
1174
+ res.end(JSON.stringify({ error: "Missing 'id' in body" }));
1175
+ return;
1176
+ }
1177
+ const slot = typeof updates.slot === "string" ? updates.slot : void 0;
1178
+ const entry = await upsertComponentEntry(
1179
+ id,
1180
+ { defaultPath: `components/${componentName}`, slot },
1181
+ { cwd, stage: activeStage }
1182
+ );
1183
+ await ctx?.rebuild();
1184
+ res.setHeader("Content-Type", "application/json");
1185
+ res.end(
1186
+ JSON.stringify({ success: true, component: { id, ...entry } })
1187
+ );
1188
+ return;
1189
+ }
883
1190
  const metaPath = path4.join(
884
1191
  cwd,
885
1192
  "components",
@@ -918,6 +1225,45 @@ data: ${payload}
918
1225
  });
919
1226
  return;
920
1227
  }
1228
+ if (url.pathname === "/__log" && req.method === "POST") {
1229
+ if (!onBrowserLogs) {
1230
+ res.statusCode = 404;
1231
+ res.end();
1232
+ return;
1233
+ }
1234
+ res.setHeader("Access-Control-Allow-Origin", "*");
1235
+ res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
1236
+ res.setHeader("Access-Control-Allow-Private-Network", "true");
1237
+ const MAX_LOG_BODY = 64 * 1024;
1238
+ let body = "";
1239
+ let aborted = false;
1240
+ req.on("data", (chunk) => {
1241
+ if (aborted) return;
1242
+ body += chunk.toString();
1243
+ if (body.length > MAX_LOG_BODY) {
1244
+ aborted = true;
1245
+ res.statusCode = 413;
1246
+ res.end();
1247
+ req.destroy();
1248
+ }
1249
+ });
1250
+ req.on("end", () => {
1251
+ if (aborted) return;
1252
+ try {
1253
+ const parsed2 = JSON.parse(body);
1254
+ const batch = parsed2.batch;
1255
+ const candidates = Array.isArray(batch) ? batch : [parsed2];
1256
+ const entries = candidates.filter(
1257
+ (entry) => typeof entry?.message === "string"
1258
+ );
1259
+ if (entries.length > 0) onBrowserLogs(entries);
1260
+ } catch {
1261
+ }
1262
+ res.statusCode = 204;
1263
+ res.end();
1264
+ });
1265
+ return;
1266
+ }
921
1267
  if (req.method === "OPTIONS") {
922
1268
  res.setHeader("Access-Control-Allow-Origin", "*");
923
1269
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
@@ -986,7 +1332,9 @@ async function writeManifest(components, buildResult, cwd = process.cwd()) {
986
1332
  id: c.id,
987
1333
  name: c.name,
988
1334
  js: `/${c.name}/index.js`,
989
- slot: c.slot
1335
+ slot: c.slot,
1336
+ ...c.otherStage ? { otherStage: true } : {},
1337
+ ...c.unlinked ? { unlinked: true } : {}
990
1338
  };
991
1339
  const cssPath = path4.join(outdir, c.name, "index.css");
992
1340
  try {
@@ -1020,13 +1368,16 @@ function StartCommand({ args }) {
1020
1368
  const [state, setState] = useState2({ status: "initializing" });
1021
1369
  const [components, setComponents] = useState2([]);
1022
1370
  const [logs, setLogs] = useState2([]);
1371
+ const [browserLogs, setBrowserLogs] = useState2([]);
1023
1372
  const [buildCount, setBuildCount] = useState2(0);
1024
1373
  const [lastBuildTime, setLastBuildTime] = useState2(null);
1025
1374
  const logIdRef = useRef(0);
1375
+ const browserLogIdRef = useRef(0);
1026
1376
  const rebuildRef = useRef(null);
1027
1377
  const stopRef = useRef(null);
1028
1378
  const stage = resolveStage(parseArg(args, "--stage", "-s"));
1029
1379
  const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
1380
+ const browserLogsEnabled = args.includes("--browser-logs");
1030
1381
  const isInteractive = Boolean(process.stdin.isTTY);
1031
1382
  const addLog = useCallback((log) => {
1032
1383
  setLogs((prev) => {
@@ -1049,6 +1400,18 @@ function StartCommand({ args }) {
1049
1400
  },
1050
1401
  [addLog]
1051
1402
  );
1403
+ const addBrowserLogs = useCallback((entries) => {
1404
+ if (entries.length === 0) return;
1405
+ setBrowserLogs((prev) => {
1406
+ const mapped = entries.map((entry) => ({
1407
+ ...entry,
1408
+ message: sanitizeLogMessage(entry.message),
1409
+ id: ++browserLogIdRef.current,
1410
+ timestamp: /* @__PURE__ */ new Date()
1411
+ }));
1412
+ return [...prev, ...mapped].slice(-MAX_LOGS);
1413
+ });
1414
+ }, []);
1052
1415
  useEffect2(() => {
1053
1416
  let mounted = true;
1054
1417
  async function init() {
@@ -1079,6 +1442,7 @@ function StartCommand({ args }) {
1079
1442
  port: PORT,
1080
1443
  stage,
1081
1444
  onRequest: handleRequest,
1445
+ onBrowserLogs: browserLogsEnabled ? addBrowserLogs : void 0,
1082
1446
  onBuildEnd: (updatedComponents) => {
1083
1447
  setComponents(updatedComponents);
1084
1448
  setBuildCount((c) => c + 1);
@@ -1119,7 +1483,7 @@ function StartCommand({ args }) {
1119
1483
  mounted = false;
1120
1484
  stopRef.current?.();
1121
1485
  };
1122
- }, [stage, handleRequest, noOpen]);
1486
+ }, [stage, handleRequest, addBrowserLogs, browserLogsEnabled, noOpen]);
1123
1487
  useInput(
1124
1488
  (input, key) => {
1125
1489
  if (input === "q" || input === "c" && key.ctrl) {
@@ -1176,6 +1540,7 @@ function StartCommand({ args }) {
1176
1540
  /* @__PURE__ */ jsx3(ComponentList, { components }),
1177
1541
  /* @__PURE__ */ jsx3(BuildInfo, { buildCount, lastBuildTime }),
1178
1542
  /* @__PURE__ */ jsx3(RequestLogs, { logs }),
1543
+ browserLogsEnabled && /* @__PURE__ */ jsx3(BrowserLogs, { logs: browserLogs }),
1179
1544
  /* @__PURE__ */ jsx3(Footer, { interactive: isInteractive })
1180
1545
  ] })
1181
1546
  ] });
@@ -1304,6 +1669,38 @@ function RequestLogs({ logs }) {
1304
1669
  ] }, log.id)) })
1305
1670
  ] });
1306
1671
  }
1672
+ function sanitizeLogMessage(message) {
1673
+ let out = "";
1674
+ for (const ch of message) {
1675
+ const code = ch.codePointAt(0) ?? 0;
1676
+ out += code < 32 || code >= 127 && code <= 159 ? " " : ch;
1677
+ if (out.length >= 2e3) break;
1678
+ }
1679
+ return out;
1680
+ }
1681
+ function browserLogColor(level) {
1682
+ if (level === "error") return "red";
1683
+ if (level === "warn") return "yellow";
1684
+ if (level === "info") return "cyan";
1685
+ return "gray";
1686
+ }
1687
+ function BrowserLogs({ logs }) {
1688
+ return /* @__PURE__ */ jsxs3(Box3, { marginTop: 1, flexDirection: "column", children: [
1689
+ /* @__PURE__ */ jsx3(Text3, { bold: true, children: "Browser Logs:" }),
1690
+ /* @__PURE__ */ jsx3(Box3, { marginLeft: 2, flexDirection: "column", children: logs.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "No browser logs yet..." }) : logs.map((log) => /* @__PURE__ */ jsxs3(Box3, { children: [
1691
+ /* @__PURE__ */ jsxs3(Text3, { color: browserLogColor(log.level), children: [
1692
+ log.level.toUpperCase(),
1693
+ " "
1694
+ ] }),
1695
+ log.component && /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
1696
+ "[",
1697
+ log.component,
1698
+ "] "
1699
+ ] }),
1700
+ /* @__PURE__ */ jsx3(Text3, { children: log.message })
1701
+ ] }, log.id)) })
1702
+ ] });
1703
+ }
1307
1704
  function Footer({ interactive = true }) {
1308
1705
  if (!interactive) {
1309
1706
  return /* @__PURE__ */ jsx3(Box3, { marginTop: 1, borderStyle: "single", borderColor: "gray", paddingX: 1, children: /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "Headless (no TTY) \u2014 Ctrl+C to stop" }) });
@@ -1340,7 +1737,7 @@ function App({ command, args }) {
1340
1737
  }
1341
1738
  }
1342
1739
  function VersionCommand() {
1343
- const version = "1.4.1" ? "1.4.1" : "unknown";
1740
+ const version = "1.6.0" ? "1.6.0" : "unknown";
1344
1741
  return /* @__PURE__ */ jsx4(Box4, { children: /* @__PURE__ */ jsxs4(Text4, { children: [
1345
1742
  "ollieshop v",
1346
1743
  version
@@ -2493,6 +2890,115 @@ async function schemaCommand(parsed2) {
2493
2890
  `);
2494
2891
  }
2495
2892
 
2893
+ // src/commands/setup-cmd.ts
2894
+ import fs5 from "fs/promises";
2895
+ import path5 from "path";
2896
+ import { glob as glob2 } from "glob";
2897
+ var META_FILE_RE = /^meta(?:\.([^.]+))?\.json$/;
2898
+ function configFileForStage(stage) {
2899
+ return !stage || stage === "prod" ? "ollie.json" : `ollie.${stage}.json`;
2900
+ }
2901
+ async function setupCommand(parsed2) {
2902
+ const format = detectOutputFormat(parsed2.global.output);
2903
+ const cwd = process.cwd();
2904
+ const dryRun = parsed2.global.dryRun;
2905
+ try {
2906
+ const componentsDir = path5.join(cwd, "components");
2907
+ let folders = [];
2908
+ try {
2909
+ await fs5.access(componentsDir);
2910
+ const entries = await glob2("*/index.tsx", { cwd: componentsDir });
2911
+ folders = entries.map((entry) => path5.dirname(entry));
2912
+ } catch {
2913
+ }
2914
+ const migrated = [];
2915
+ const skipped = [];
2916
+ for (const name of folders) {
2917
+ const folderDir = path5.join(componentsDir, name);
2918
+ const metaFiles = await glob2("meta*.json", { cwd: folderDir });
2919
+ for (const metaFile of metaFiles) {
2920
+ const match = metaFile.match(META_FILE_RE);
2921
+ if (!match) continue;
2922
+ const stage = match[1];
2923
+ let meta;
2924
+ try {
2925
+ meta = JSON.parse(
2926
+ await fs5.readFile(path5.join(folderDir, metaFile), "utf-8")
2927
+ );
2928
+ } catch {
2929
+ skipped.push({
2930
+ component: name,
2931
+ metaFile,
2932
+ reason: "unreadable meta"
2933
+ });
2934
+ continue;
2935
+ }
2936
+ const id = typeof meta.id === "string" ? meta.id : void 0;
2937
+ if (!id) {
2938
+ skipped.push({
2939
+ component: name,
2940
+ metaFile,
2941
+ reason: "no id (unlinked) - left as-is"
2942
+ });
2943
+ continue;
2944
+ }
2945
+ const slot = typeof meta.slot === "string" ? meta.slot : void 0;
2946
+ if (!dryRun) {
2947
+ await upsertComponentEntry(
2948
+ id,
2949
+ { defaultPath: `components/${name}`, slot },
2950
+ { cwd, stage }
2951
+ );
2952
+ await fs5.rm(path5.join(folderDir, metaFile));
2953
+ }
2954
+ migrated.push({
2955
+ component: name,
2956
+ stage: stage ?? "prod",
2957
+ configFile: configFileForStage(stage),
2958
+ metaFile,
2959
+ id,
2960
+ slot
2961
+ });
2962
+ }
2963
+ }
2964
+ if (format === "json") {
2965
+ outputResult(
2966
+ { data: { dryRun, migrated, skipped } },
2967
+ format,
2968
+ parsed2.global.fields
2969
+ );
2970
+ return;
2971
+ }
2972
+ const prefix = dryRun ? "\x1B[33m[dry-run]\x1B[0m " : "";
2973
+ if (migrated.length === 0) {
2974
+ console.log(`${prefix}No legacy meta files to migrate.`);
2975
+ } else {
2976
+ console.log(
2977
+ `${prefix}\x1B[1mMigrated ${migrated.length} meta file(s) to config:\x1B[0m`
2978
+ );
2979
+ for (const entry of migrated) {
2980
+ const slotLabel = entry.slot ? `, slot ${entry.slot}` : "";
2981
+ const removed = dryRun ? "" : ` [removed ${entry.metaFile}]`;
2982
+ console.log(
2983
+ ` ${entry.configFile.padEnd(16)} ${entry.component} (id ${entry.id}${slotLabel})${removed}`
2984
+ );
2985
+ }
2986
+ }
2987
+ if (skipped.length > 0) {
2988
+ console.log("\n\x1B[2mSkipped:\x1B[0m");
2989
+ for (const entry of skipped) {
2990
+ console.log(` ${entry.component}/${entry.metaFile}: ${entry.reason}`);
2991
+ }
2992
+ }
2993
+ } catch (err) {
2994
+ outputResult(
2995
+ { error: { message: err instanceof Error ? err.message : String(err) } },
2996
+ format
2997
+ );
2998
+ process.exit(1);
2999
+ }
3000
+ }
3001
+
2496
3002
  // src/commands/status-cmd.ts
2497
3003
  async function statusCommand(parsed2) {
2498
3004
  const format = detectOutputFormat(parsed2.global.output);
@@ -2876,7 +3382,8 @@ var AGENT_COMMANDS = {
2876
3382
  schema: schemaCommand,
2877
3383
  init: initCommand,
2878
3384
  deploy: deployCommand,
2879
- status: statusCommand
3385
+ status: statusCommand,
3386
+ setup: setupCommand
2880
3387
  };
2881
3388
  var parsed = parseArgs(process.argv);
2882
3389
  if (parsed.command in AGENT_COMMANDS) {