@storifycli/cli 0.1.1 → 0.2.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 (2) hide show
  1. package/dist/cli.js +488 -215
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -301,8 +301,86 @@ async function runInteractiveLoginFlow(options) {
301
301
  }
302
302
 
303
303
  // src/lib/ui.ts
304
- import pc from "picocolors";
304
+ import pc2 from "picocolors";
305
305
  import ora from "ora";
306
+
307
+ // src/lib/brand-banner.ts
308
+ import pc from "picocolors";
309
+ var brandName = "Storify";
310
+ var brandTagline = "Theme development toolkit";
311
+ var LOGO_FACE = [
312
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
313
+ " \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2588\u2588\u2557 \u2588\u2588\u2554\u255D",
314
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u255A\u2588\u2588\u2588\u2588\u2554\u255D ",
315
+ " \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u255A\u2588\u2588\u2554\u255D ",
316
+ " \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 ",
317
+ " \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D "
318
+ ];
319
+ var LOGO_SHADOW = [" \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580 \u2580\u2580\u2580\u2580\u2580\u2580\u2580\u2580 \u2580\u2580 \u2580\u2580"];
320
+ function supportsColor() {
321
+ return pc.isColorSupported;
322
+ }
323
+ function colorFace(line) {
324
+ if (!supportsColor()) return line;
325
+ return pc.bold(pc.cyan(line));
326
+ }
327
+ function colorShadow(line) {
328
+ if (!supportsColor()) return line;
329
+ return pc.dim(pc.blue(line));
330
+ }
331
+ function colorAccent(line) {
332
+ if (!supportsColor()) return line;
333
+ return pc.bold(pc.white(line));
334
+ }
335
+ function paintMuted(text3) {
336
+ if (!supportsColor()) return text3;
337
+ return pc.dim(text3);
338
+ }
339
+ function paintAccent(text3) {
340
+ if (!supportsColor()) return text3;
341
+ return pc.magenta(text3);
342
+ }
343
+ function paintBrand(text3) {
344
+ if (!supportsColor()) return text3;
345
+ return pc.bold(pc.cyan(text3));
346
+ }
347
+ function centerText(text3, width) {
348
+ const trimmed = text3.length > width ? text3.slice(0, width) : text3;
349
+ const pad = Math.max(0, Math.floor((width - trimmed.length) / 2));
350
+ return `${" ".repeat(pad)}${trimmed}${" ".repeat(Math.max(0, width - pad - trimmed.length))}`;
351
+ }
352
+ function printPlatformBanner(subtitle) {
353
+ const logoWidth = LOGO_FACE[0].length;
354
+ const rule = "\u2550".repeat(logoWidth);
355
+ const tagline = subtitle || "Theme live preview";
356
+ console.log("");
357
+ console.log(colorShadow(` ${rule}`));
358
+ for (const line of LOGO_SHADOW) {
359
+ console.log(colorShadow(` ${line}`));
360
+ }
361
+ for (const line of LOGO_FACE) {
362
+ console.log(colorFace(` ${line}`));
363
+ }
364
+ console.log(colorShadow(` ${rule}`));
365
+ console.log("");
366
+ console.log(paintAccent(` ${centerText(tagline, logoWidth)}`));
367
+ console.log(paintMuted(` ${centerText(brandTagline, logoWidth)}`));
368
+ console.log("");
369
+ console.log(colorAccent(` ${"\u2593".repeat(logoWidth)}`));
370
+ console.log("");
371
+ }
372
+ function printBannerWithLogo(version2) {
373
+ console.log("");
374
+ for (const line of LOGO_FACE) {
375
+ console.log(colorFace(` ${line}`));
376
+ }
377
+ console.log("");
378
+ console.log(paintBrand(` ${centerText(`${brandName} CLI v${version2}`, LOGO_FACE[0].length)}`));
379
+ console.log(paintMuted(` ${centerText(brandTagline, LOGO_FACE[0].length)}`));
380
+ console.log("");
381
+ }
382
+
383
+ // src/lib/ui.ts
306
384
  var brand = {
307
385
  name: "Storify",
308
386
  cli: "storify",
@@ -311,26 +389,26 @@ var brand = {
311
389
  function isInteractive() {
312
390
  return Boolean(process.stdin.isTTY && process.stdout.isTTY && !process.env.CI);
313
391
  }
314
- function supportsColor() {
315
- return pc.isColorSupported;
392
+ function supportsColor2() {
393
+ return pc2.isColorSupported;
316
394
  }
317
395
  function paint(text3, tone) {
318
- if (!supportsColor()) return text3;
396
+ if (!supportsColor2()) return text3;
319
397
  switch (tone) {
320
398
  case "brand":
321
- return pc.bold(pc.cyan(text3));
399
+ return pc2.bold(pc2.cyan(text3));
322
400
  case "muted":
323
- return pc.dim(text3);
401
+ return pc2.dim(text3);
324
402
  case "success":
325
- return pc.green(text3);
403
+ return pc2.green(text3);
326
404
  case "warn":
327
- return pc.yellow(text3);
405
+ return pc2.yellow(text3);
328
406
  case "error":
329
- return pc.red(text3);
407
+ return pc2.red(text3);
330
408
  case "info":
331
- return pc.blue(text3);
409
+ return pc2.blue(text3);
332
410
  case "accent":
333
- return pc.magenta(text3);
411
+ return pc2.magenta(text3);
334
412
  default:
335
413
  return text3;
336
414
  }
@@ -341,12 +419,7 @@ function formatBytes(bytes) {
341
419
  return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
342
420
  }
343
421
  function printBanner(version2) {
344
- const line = "\u2500".repeat(52);
345
- console.log("");
346
- console.log(paint(` ${brand.name} CLI`, "brand") + paint(` v${version2}`, "muted"));
347
- console.log(paint(` ${brand.tagline}`, "muted"));
348
- console.log(paint(` ${line}`, "muted"));
349
- console.log("");
422
+ printBannerWithLogo(version2);
350
423
  }
351
424
  function printHeading(title, subtitle) {
352
425
  console.log("");
@@ -377,16 +450,6 @@ function printHint(message) {
377
450
  function printCommandExample(label, command) {
378
451
  console.log(` ${paint(label.padEnd(16), "muted")}${paint(command, "accent")}`);
379
452
  }
380
- function printPreviewPanel(title, rows) {
381
- console.log("");
382
- console.log(paint(` ${title}`, "brand"));
383
- printDivider();
384
- const labelWidth = Math.max(16, ...rows.map((row) => row.label.length + 2));
385
- for (const row of rows) {
386
- printStep(row.label, row.value, labelWidth);
387
- }
388
- console.log("");
389
- }
390
453
  function printQuickStart() {
391
454
  printHeading("Quick start");
392
455
  printCommandExample("Live storefront preview", "storify theme dev");
@@ -407,14 +470,14 @@ function createSpinner(text3) {
407
470
  });
408
471
  }
409
472
  async function withSpinner(text3, task) {
410
- const spinner2 = createSpinner(text3);
411
- spinner2.start();
473
+ const spinner3 = createSpinner(text3);
474
+ spinner3.start();
412
475
  try {
413
476
  const result = await task();
414
- spinner2.stop();
477
+ spinner3.stop();
415
478
  return result;
416
479
  } catch (error) {
417
- spinner2.fail(text3);
480
+ spinner3.fail(text3);
418
481
  throw error;
419
482
  }
420
483
  }
@@ -843,9 +906,106 @@ async function runBuildCommand(options) {
843
906
  import path6 from "path";
844
907
  import { execa as execa3 } from "execa";
845
908
 
909
+ // src/lib/clack-ui.ts
910
+ import * as p2 from "@clack/prompts";
911
+ import pc3 from "picocolors";
912
+ async function runTask(startMessage, doneMessage, task) {
913
+ const spinner3 = p2.spinner();
914
+ spinner3.start(startMessage);
915
+ try {
916
+ const result = await task();
917
+ spinner3.stop(pc3.green("\u2713") + ` ${doneMessage}`);
918
+ return result;
919
+ } catch (error) {
920
+ spinner3.stop(pc3.red("\u2717") + ` ${startMessage}`);
921
+ throw error;
922
+ }
923
+ }
924
+
925
+ // src/lib/dev-dashboard.ts
926
+ import pc4 from "picocolors";
927
+
928
+ // src/lib/terminal-screen.ts
929
+ function clearScreen() {
930
+ if (!isInteractive()) return;
931
+ process.stdout.write("\x1Bc");
932
+ }
933
+ function hideCursor() {
934
+ if (!isInteractive()) return;
935
+ process.stdout.write("\x1B[?25l");
936
+ }
937
+ function showCursor() {
938
+ if (!isInteractive()) return;
939
+ process.stdout.write("\x1B[?25h");
940
+ }
941
+
942
+ // src/lib/dev-dashboard.ts
943
+ function truncateMiddle(value, max = 64) {
944
+ if (value.length <= max) return value;
945
+ const head = Math.ceil((max - 1) / 2);
946
+ const tail = Math.floor((max - 1) / 2);
947
+ return `${value.slice(0, head)}\u2026${value.slice(-tail)}`;
948
+ }
949
+ function renderUrlPanel(urls) {
950
+ const w = 62;
951
+ console.log(paint(` \u256D${"\u2500".repeat(w)}\u256E`, "brand"));
952
+ console.log(
953
+ paint(` \u2502${" ".repeat(Math.floor((w - 13) / 2))}Preview URLs${" ".repeat(Math.ceil((w - 13) / 2))}\u2502`, "brand")
954
+ );
955
+ console.log(paint(` \u251C${"\u2500".repeat(w)}\u2524`, "brand"));
956
+ for (const row of urls) {
957
+ const label = row.label.padEnd(14);
958
+ const value = truncateMiddle(row.value, w - 20);
959
+ const gap = Math.max(1, w - label.length - value.length - 1);
960
+ console.log(` \u2502 ${paint(label, "muted")}${value}${" ".repeat(gap)}\u2502`);
961
+ }
962
+ console.log(paint(` \u2570${"\u2500".repeat(w)}\u256F`, "brand"));
963
+ }
964
+ function renderControls(interactiveShell) {
965
+ const w = 62;
966
+ console.log("");
967
+ console.log(paint(` \u256D${"\u2500".repeat(w)}\u256E`, "brand"));
968
+ console.log(
969
+ paint(` \u2502${" ".repeat(Math.floor((w - 8) / 2))}Controls${" ".repeat(Math.ceil((w - 8) / 2))}\u2502`, "brand")
970
+ );
971
+ console.log(paint(` \u251C${"\u2500".repeat(w)}\u2524`, "brand"));
972
+ console.log(paint(` \u2502 ${paint("Enter / Q".padEnd(14), "muted")}Stop preview \xB7 disable dev link`, "muted"));
973
+ console.log(
974
+ paint(
975
+ ` \u2502 ${paint("Ctrl+C".padEnd(14), "muted")}${interactiveShell ? "Stop \xB7 return to main menu" : "Stop preview \xB7 exit"}`,
976
+ "muted"
977
+ )
978
+ );
979
+ console.log(paint(` \u2502 ${paint("Save files".padEnd(14), "muted")}Vite hot-reloads in the background`, "muted"));
980
+ console.log(paint(` \u2570${"\u2500".repeat(w)}\u256F`, "brand"));
981
+ console.log("");
982
+ }
983
+ function renderDevDashboard(options) {
984
+ clearScreen();
985
+ printPlatformBanner(options.subtitle);
986
+ if (options.storeLabel) {
987
+ console.log(
988
+ paint(` ${paint("Store", "muted")} ${options.storeLabel}`, "info")
989
+ );
990
+ }
991
+ console.log(paint(` ${paint("Theme", "muted")} ${truncateMiddle(options.themeRoot, 70)}`, "info"));
992
+ console.log("");
993
+ for (const step of options.steps) {
994
+ const mark = supportsColor2() ? pc4.green("\u2713") : "\u2713";
995
+ console.log(` ${mark} ${paint(step.label.padEnd(16), "muted")}${step.value}`);
996
+ }
997
+ console.log("");
998
+ renderUrlPanel(options.urls);
999
+ if (options.note) {
1000
+ console.log(paint(` ${options.note}`, "muted"));
1001
+ }
1002
+ renderControls(options.interactiveShell);
1003
+ }
1004
+
846
1005
  // src/lib/dev-link.ts
847
1006
  async function updateDevLink(auth, payload) {
848
- const response = await fetch(`${auth.apiUrl}/theme/dev-link`, {
1007
+ const url = `${auth.apiUrl}/theme/dev-link`;
1008
+ const init = {
849
1009
  method: "PATCH",
850
1010
  headers: {
851
1011
  "Content-Type": "application/json",
@@ -853,18 +1013,72 @@ async function updateDevLink(auth, payload) {
853
1013
  "X-Store-Id": auth.storeId
854
1014
  },
855
1015
  body: JSON.stringify(payload)
856
- });
857
- const body = await response.json().catch(() => ({}));
858
- if (!response.ok) {
859
- const message = (body && typeof body === "object" && "error" in body ? String(body.error) : "") || `Failed to update dev link (${response.status})`;
860
- throw new Error(message);
861
- }
862
- return {
863
- devThemeEnabled: body.devThemeEnabled === true,
864
- devThemeBaseUrl: body.devThemeBaseUrl == null || body.devThemeBaseUrl === "" ? null : String(body.devThemeBaseUrl).trim(),
865
- storefrontPreviewUrl: typeof body.storefrontPreviewUrl === "string" ? body.storefrontPreviewUrl : void 0,
866
- devSessionId: typeof body.devSessionId === "string" ? body.devSessionId : void 0
867
1016
  };
1017
+ const retryDelaysMs = [0, 2e3, 5e3];
1018
+ let lastError = null;
1019
+ for (const delayMs of retryDelaysMs) {
1020
+ if (delayMs > 0) {
1021
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
1022
+ }
1023
+ const response = await fetch(url, init);
1024
+ const body = await response.json().catch(() => ({}));
1025
+ if (response.status === 429) {
1026
+ lastError = new Error(
1027
+ (body && typeof body === "object" && "error" in body ? String(body.error) : "") || "Too many requests, please slow down"
1028
+ );
1029
+ continue;
1030
+ }
1031
+ if (!response.ok) {
1032
+ const message = (body && typeof body === "object" && "error" in body ? String(body.error) : "") || `Failed to update dev link (${response.status})`;
1033
+ throw new Error(message);
1034
+ }
1035
+ return {
1036
+ devThemeEnabled: body.devThemeEnabled === true,
1037
+ devThemeBaseUrl: body.devThemeBaseUrl == null || body.devThemeBaseUrl === "" ? null : String(body.devThemeBaseUrl).trim(),
1038
+ storefrontPreviewUrl: typeof body.storefrontPreviewUrl === "string" ? body.storefrontPreviewUrl : void 0,
1039
+ devSessionId: typeof body.devSessionId === "string" ? body.devSessionId : void 0
1040
+ };
1041
+ }
1042
+ throw lastError ?? new Error("Failed to update dev link (rate limited)");
1043
+ }
1044
+
1045
+ // src/lib/dev-session-wait.ts
1046
+ import readline from "readline";
1047
+ async function waitForDevSessionEnd(viteProcess, options) {
1048
+ if (!isInteractive()) {
1049
+ await viteProcess;
1050
+ return;
1051
+ }
1052
+ hideCursor();
1053
+ await new Promise((resolve, reject) => {
1054
+ let settled = false;
1055
+ const finish = (error) => {
1056
+ if (settled) return;
1057
+ settled = true;
1058
+ showCursor();
1059
+ rl.close();
1060
+ if (error) reject(error);
1061
+ else resolve();
1062
+ };
1063
+ viteProcess.then(() => finish()).catch((error) => finish(error));
1064
+ const rl = readline.createInterface({
1065
+ input: process.stdin,
1066
+ output: process.stdout,
1067
+ terminal: true
1068
+ });
1069
+ const stopSession = () => {
1070
+ void options.stop().then(
1071
+ () => finish(),
1072
+ (error) => finish(error)
1073
+ );
1074
+ };
1075
+ rl.on("line", (line) => {
1076
+ const key = line.trim().toLowerCase();
1077
+ if (key === "" || key === "q" || key === "quit" || key === "stop" || key === "s") {
1078
+ stopSession();
1079
+ }
1080
+ });
1081
+ });
868
1082
  }
869
1083
 
870
1084
  // src/lib/port.ts
@@ -900,7 +1114,7 @@ function buildPreviewUrls(auth, devBaseUrl, storefrontUrlOverride) {
900
1114
  }
901
1115
 
902
1116
  // src/lib/session.ts
903
- import * as p2 from "@clack/prompts";
1117
+ import * as p3 from "@clack/prompts";
904
1118
  async function ensureAuthenticatedSession(options = {}) {
905
1119
  const stored = await readStoredConfig();
906
1120
  const hasCredentials = Boolean(stored.token?.trim() && stored.storeId?.trim());
@@ -927,7 +1141,7 @@ async function ensureAuthenticatedSession(options = {}) {
927
1141
  if (!options.token && !options.forceLogin) {
928
1142
  const valid = await verifyAuthToken(apiUrl, token);
929
1143
  if (!valid && isInteractive()) {
930
- p2.log.warn("Session expired. Sign in again.");
1144
+ p3.log.warn("Session expired. Sign in again.");
931
1145
  const session = await runInteractiveLoginFlow({
932
1146
  apiUrl: options.api,
933
1147
  currentStoreId: options.storeId || stored.storeId
@@ -1001,10 +1215,14 @@ async function waitForDevServer(baseUrl, timeoutMs = 45e3) {
1001
1215
 
1002
1216
  // src/lib/tunnel.ts
1003
1217
  async function startCloudflaredTunnel(localUrl) {
1004
- const child = execa2("cloudflared", ["tunnel", "--url", localUrl], {
1005
- all: true,
1006
- reject: false
1007
- });
1218
+ const child = execa2(
1219
+ "cloudflared",
1220
+ ["tunnel", "--url", localUrl, "--no-autoupdate", "--loglevel", "error"],
1221
+ {
1222
+ all: true,
1223
+ reject: false
1224
+ }
1225
+ );
1008
1226
  const url = await new Promise((resolve, reject) => {
1009
1227
  const timeout = setTimeout(() => {
1010
1228
  reject(new Error("Timed out while waiting for cloudflared public URL."));
@@ -1057,7 +1275,6 @@ async function runDevCommand(options) {
1057
1275
  const preferredPort = toPort(options.port);
1058
1276
  const shouldLinkStore = options.linkStore !== false;
1059
1277
  const shouldUnlinkOnExit = options.unlinkOnExit !== false;
1060
- printHeading("Theme preview", themeRoot);
1061
1278
  const session = await ensureAuthenticatedSession({
1062
1279
  api: options.api,
1063
1280
  storeId: options.storeId,
@@ -1073,33 +1290,36 @@ async function runDevCommand(options) {
1073
1290
  if (useLocalDev) {
1074
1291
  auth.storefrontUrl = localStorefrontBaseUrl(auth.storefrontUrl);
1075
1292
  }
1293
+ const subtitle = options.remote ? "Remote preview \xB7 production storefront" : useLocalDev ? "Local preview \xB7 localhost storefront" : "Theme live preview";
1076
1294
  const port = await findAvailablePort(preferredPort);
1077
1295
  const localUrl = `http://localhost:${port}`;
1078
1296
  const shouldUseTunnel = !useLocalDev && options.tunnel !== false;
1079
- if (port !== preferredPort) {
1080
- printWarn(`Port ${preferredPort} was busy. Using ${port} instead.`);
1081
- }
1297
+ const portNote = port !== preferredPort ? ` (port ${preferredPort} was busy)` : "";
1082
1298
  let tunnelProcess = null;
1083
1299
  let linkedDevUrl = null;
1084
1300
  let liveStorefrontUrl = null;
1301
+ let viteProcess = null;
1302
+ let viteStderr = "";
1085
1303
  const viteBin = path6.join(themeRoot, "node_modules", "vite", "bin", "vite.js");
1086
- const viteArgs = [viteBin, "--host", "0.0.0.0", "--port", String(port), "--strictPort"];
1304
+ const viteArgs = [
1305
+ viteBin,
1306
+ "--host",
1307
+ "0.0.0.0",
1308
+ "--port",
1309
+ String(port),
1310
+ "--strictPort",
1311
+ "--logLevel",
1312
+ "error"
1313
+ ];
1087
1314
  const viteEnv = { ...process.env };
1088
1315
  viteEnv.VITE_DEV_STORE_ID = auth.storeId;
1089
- let viteProcess = null;
1090
1316
  const teardown = async () => {
1091
- if (tunnelProcess) {
1092
- tunnelProcess.kill("SIGTERM");
1093
- }
1094
- if (viteProcess) {
1095
- viteProcess.kill("SIGTERM");
1096
- }
1317
+ if (tunnelProcess) tunnelProcess.kill("SIGTERM");
1318
+ if (viteProcess) viteProcess.kill("SIGTERM");
1097
1319
  if (shouldLinkStore && shouldUnlinkOnExit) {
1098
1320
  try {
1099
1321
  await updateDevLink(auth, { devThemeEnabled: false, devThemeBaseUrl: null });
1100
- printSuccess("Dev link disabled.");
1101
- } catch (error) {
1102
- printWarn(`Failed to disable dev link: ${error.message}`);
1322
+ } catch {
1103
1323
  }
1104
1324
  }
1105
1325
  };
@@ -1109,88 +1329,103 @@ async function runDevCommand(options) {
1109
1329
  tearingDown = true;
1110
1330
  await teardown();
1111
1331
  };
1112
- process.on("SIGINT", async () => {
1332
+ const onSigint = async () => {
1113
1333
  await handleExit();
1114
- process.exit(130);
1115
- });
1116
- process.on("SIGTERM", async () => {
1334
+ if (!options.interactiveShell) process.exit(130);
1335
+ };
1336
+ const onSigterm = async () => {
1117
1337
  await handleExit();
1118
- process.exit(143);
1119
- });
1120
- const viteSpinner = createSpinner(`Starting Vite on ${localUrl}`);
1121
- viteSpinner.start();
1122
- viteProcess = execa3("node", viteArgs, {
1123
- cwd: themeRoot,
1124
- env: viteEnv,
1125
- stdout: "pipe",
1126
- stderr: "pipe"
1127
- });
1128
- viteProcess.stdout?.pipe(process.stdout);
1129
- viteProcess.stderr?.pipe(process.stderr);
1338
+ if (!options.interactiveShell) process.exit(143);
1339
+ };
1340
+ process.on("SIGINT", onSigint);
1341
+ process.on("SIGTERM", onSigterm);
1342
+ const removeSignalHandlers = () => {
1343
+ process.off("SIGINT", onSigint);
1344
+ process.off("SIGTERM", onSigterm);
1345
+ };
1346
+ const dashboardSteps = [];
1130
1347
  try {
1131
- await waitForDevServer(localUrl);
1132
- viteSpinner.succeed(`Vite ready at ${localUrl}`);
1133
- } catch (error) {
1134
- viteSpinner.fail("Vite failed to start");
1135
- viteProcess.kill("SIGTERM");
1136
- throw error;
1137
- }
1138
- if (shouldLinkStore) {
1139
- if (useLocalDev) {
1140
- linkedDevUrl = localUrl;
1141
- printStep("Mode", "Localhost (storefront + theme on your machine)");
1142
- } else if (options.publicUrl) {
1143
- linkedDevUrl = options.publicUrl.replace(/\/+$/, "");
1144
- } else if (shouldUseTunnel) {
1145
- const tunnelSpinner = createSpinner("Creating public tunnel for theme assets");
1146
- tunnelSpinner.start();
1147
- try {
1148
- const tunnel = await startCloudflaredTunnel(localUrl);
1149
- linkedDevUrl = tunnel.url.replace(/\/+$/, "");
1150
- tunnelProcess = tunnel.process;
1151
- tunnelSpinner.succeed(`Public theme URL: ${linkedDevUrl}`);
1152
- } catch (error) {
1153
- tunnelSpinner.warn(`Tunnel unavailable (${error.message})`);
1348
+ await runTask(`Starting Vite${portNote}`, `Vite on :${port}`, async () => {
1349
+ viteProcess = execa3("node", viteArgs, {
1350
+ cwd: themeRoot,
1351
+ env: viteEnv,
1352
+ stdout: "ignore",
1353
+ stderr: "pipe"
1354
+ });
1355
+ viteProcess.stderr?.on("data", (chunk) => {
1356
+ viteStderr += String(chunk);
1357
+ });
1358
+ await waitForDevServer(localUrl);
1359
+ });
1360
+ dashboardSteps.push({ label: "Vite dev server", value: localUrl });
1361
+ if (shouldLinkStore) {
1362
+ if (useLocalDev) {
1363
+ linkedDevUrl = localUrl;
1364
+ } else if (options.publicUrl) {
1365
+ linkedDevUrl = options.publicUrl.replace(/\/+$/, "");
1366
+ } else if (shouldUseTunnel) {
1367
+ try {
1368
+ const tunnel = await runTask(
1369
+ "Creating secure tunnel",
1370
+ "Public tunnel ready",
1371
+ () => startCloudflaredTunnel(localUrl)
1372
+ );
1373
+ linkedDevUrl = tunnel.url.replace(/\/+$/, "");
1374
+ tunnelProcess = tunnel.process;
1375
+ dashboardSteps.push({ label: "Public tunnel", value: linkedDevUrl });
1376
+ } catch {
1377
+ linkedDevUrl = localUrl;
1378
+ dashboardSteps.push({ label: "Public tunnel", value: "Unavailable \u2014 using localhost" });
1379
+ }
1380
+ } else {
1154
1381
  linkedDevUrl = localUrl;
1155
- printWarn("Storefront preview needs a public URL. Use --local or install cloudflared.");
1156
1382
  }
1383
+ const devLinkResult = await runTask(
1384
+ "Linking store preview",
1385
+ "Store preview linked",
1386
+ () => updateDevLink(auth, { devThemeEnabled: true, devThemeBaseUrl: linkedDevUrl })
1387
+ );
1388
+ liveStorefrontUrl = (useLocalDev ? formatLivePreviewUrl(auth, devLinkResult.devSessionId, session.store) : devLinkResult.storefrontPreviewUrl) || formatLivePreviewUrl(auth, devLinkResult.devSessionId, session.store);
1389
+ dashboardSteps.push({
1390
+ label: "Storefront link",
1391
+ value: session.store ? displayStoreName(session.store) : auth.storeId
1392
+ });
1157
1393
  } else {
1158
1394
  linkedDevUrl = localUrl;
1395
+ dashboardSteps.push({ label: "Local only", value: "Store link skipped" });
1159
1396
  }
1160
- const devLinkResult = await withSpinner(
1161
- "Linking dev theme to your store on Storify",
1162
- async () => updateDevLink(auth, { devThemeEnabled: true, devThemeBaseUrl: linkedDevUrl })
1163
- );
1164
- liveStorefrontUrl = (useLocalDev ? formatLivePreviewUrl(auth, devLinkResult.devSessionId, session.store) : devLinkResult.storefrontPreviewUrl) || formatLivePreviewUrl(auth, devLinkResult.devSessionId, session.store);
1165
- const urls = buildPreviewUrls(auth, linkedDevUrl, liveStorefrontUrl);
1166
- printPreviewPanel("Live preview", [
1167
- { label: "Storefront", value: urls.storefrontUrl },
1168
- { label: "Theme (Vite)", value: linkedDevUrl },
1169
- { label: "Theme direct", value: urls.themeDirectUrl },
1170
- { label: "Admin editor", value: urls.adminEditorUrl }
1171
- ]);
1172
- if (useLocalDev) {
1173
- printHint(`Storefront must be running at ${DEFAULT_STOREFRONT_URL.replace(/\/+$/, "")}`);
1174
- } else {
1175
- printHint("Open the storefront link to see your theme with real store data.");
1397
+ const urls = shouldLinkStore && linkedDevUrl && liveStorefrontUrl ? buildPreviewUrls(auth, linkedDevUrl, liveStorefrontUrl) : null;
1398
+ renderDevDashboard({
1399
+ subtitle,
1400
+ themeRoot,
1401
+ storeLabel: session.store ? displayStoreName(session.store) : auth.storeId,
1402
+ steps: dashboardSteps,
1403
+ urls: urls ? [
1404
+ { label: "Storefront", value: urls.storefrontUrl },
1405
+ { label: "Theme (Vite)", value: linkedDevUrl },
1406
+ { label: "Theme direct", value: urls.themeDirectUrl },
1407
+ { label: "Admin editor", value: urls.adminEditorUrl }
1408
+ ] : [{ label: "Vite", value: localUrl }],
1409
+ interactiveShell: options.interactiveShell,
1410
+ note: useLocalDev ? "Ensure the storefront is running on localhost:3004" : "Edit theme files \u2014 changes hot-reload on the storefront"
1411
+ });
1412
+ const openTarget = options.openStorefront && liveStorefrontUrl ? liveStorefrontUrl : options.open ? localUrl : useLocalDev && shouldLinkStore && liveStorefrontUrl ? liveStorefrontUrl : null;
1413
+ if (openTarget) {
1414
+ await openInBrowser(openTarget);
1176
1415
  }
1177
- printHint("Press Ctrl+C to stop and disable dev link.");
1178
- } else {
1179
- printStep("Local URL", localUrl);
1180
- printStep("Store ID", auth.storeId);
1181
- printHint("Run without --no-link-store to preview on the storefront.");
1182
- console.log("");
1183
- }
1184
- const openTarget = options.openStorefront && liveStorefrontUrl ? liveStorefrontUrl : options.open ? localUrl : useLocalDev && shouldLinkStore && liveStorefrontUrl ? liveStorefrontUrl : null;
1185
- if (openTarget) {
1186
- await openInBrowser(openTarget);
1187
- }
1188
- printSuccess("Waiting for file changes...");
1189
- console.log("");
1190
- try {
1191
- await viteProcess;
1416
+ if (!viteProcess) throw new Error("Vite process failed to start.");
1417
+ await waitForDevSessionEnd(viteProcess, {
1418
+ interactiveShell: options.interactiveShell,
1419
+ stop: handleExit
1420
+ });
1421
+ } catch (error) {
1422
+ if (viteStderr.trim()) {
1423
+ console.error("\n" + viteStderr.trim() + "\n");
1424
+ }
1425
+ throw error;
1192
1426
  } finally {
1193
- await handleExit();
1427
+ removeSignalHandlers();
1428
+ if (!tearingDown) await handleExit();
1194
1429
  }
1195
1430
  }
1196
1431
 
@@ -1567,40 +1802,40 @@ async function runValidateCommand(options) {
1567
1802
  const manifestPath = path11.join(themeRoot, "theme-manifest.json");
1568
1803
  const manifestRaw = await fs11.readFile(manifestPath, "utf8");
1569
1804
  const manifest = JSON.parse(manifestRaw);
1570
- let spinner2 = createSpinner("Checking manifest DSL");
1571
- spinner2.start();
1805
+ let spinner3 = createSpinner("Checking manifest DSL");
1806
+ spinner3.start();
1572
1807
  const dslValidation = validateThemeManifestDSL(manifest);
1573
1808
  if (!dslValidation.ok) {
1574
- spinner2.fail("Manifest DSL validation failed");
1809
+ spinner3.fail("Manifest DSL validation failed");
1575
1810
  throw new Error(dslValidation.error);
1576
1811
  }
1577
- spinner2.succeed("Manifest DSL is valid");
1578
- spinner2 = createSpinner("Checking page section references");
1579
- spinner2.start();
1812
+ spinner3.succeed("Manifest DSL is valid");
1813
+ spinner3 = createSpinner("Checking page section references");
1814
+ spinner3.start();
1580
1815
  const refValidation = validateThemeManifestDSLReferences(dslValidation.manifest);
1581
1816
  if (!refValidation.ok) {
1582
- spinner2.fail("Manifest references validation failed");
1817
+ spinner3.fail("Manifest references validation failed");
1583
1818
  throw new Error(refValidation.error);
1584
1819
  }
1585
- spinner2.succeed("Page layout references are valid");
1820
+ spinner3.succeed("Page layout references are valid");
1586
1821
  const sectionRendererPath = path11.join(themeRoot, "src", "SectionRenderer.tsx");
1587
1822
  if (await fileExists3(sectionRendererPath)) {
1588
- spinner2 = createSpinner("Matching SECTION_MAP components");
1589
- spinner2.start();
1823
+ spinner3 = createSpinner("Matching SECTION_MAP components");
1824
+ spinner3.start();
1590
1825
  const sectionMapInfo = await validateManifestSectionMap(themeRoot);
1591
- spinner2.succeed(`SECTION_MAP matched (${sectionMapInfo.sections} sections, ${sectionMapInfo.mapKeys} keys)`);
1826
+ spinner3.succeed(`SECTION_MAP matched (${sectionMapInfo.sections} sections, ${sectionMapInfo.mapKeys} keys)`);
1592
1827
  } else {
1593
1828
  printWarn("Skipped SECTION_MAP check (src/SectionRenderer.tsx not found).");
1594
1829
  }
1595
- spinner2 = createSpinner("Validating config/pages defaults");
1596
- spinner2.start();
1830
+ spinner3 = createSpinner("Validating config/pages defaults");
1831
+ spinner3.start();
1597
1832
  const dryRun = await buildThemeManifest({
1598
1833
  themeRoot,
1599
1834
  writeToDist: false,
1600
1835
  syncEntryFromDist: false,
1601
1836
  quiet: true
1602
1837
  });
1603
- spinner2.succeed("Page defaults are consistent");
1838
+ spinner3.succeed("Page defaults are consistent");
1604
1839
  if (!dslValidation.manifest.entry || !dslValidation.manifest.entry.trim()) {
1605
1840
  throw new Error("Manifest entry is required.");
1606
1841
  }
@@ -1623,79 +1858,117 @@ async function runValidateCommand(options) {
1623
1858
  }
1624
1859
 
1625
1860
  // src/lib/interactive.ts
1626
- import * as p3 from "@clack/prompts";
1627
- import pc2 from "picocolors";
1861
+ import * as p4 from "@clack/prompts";
1862
+ import pc5 from "picocolors";
1863
+ function showMenuShell() {
1864
+ clearScreen();
1865
+ printPlatformBanner("Theme development toolkit");
1866
+ }
1867
+ var MENU_OPTIONS = [
1868
+ { value: "preview", label: "Start live theme preview", hint: "Local or remote storefront" },
1869
+ { value: "validate", label: "Validate theme", hint: "Manifest + sections" },
1870
+ { value: "build", label: "Build theme", hint: "dist/ output" },
1871
+ { value: "pack", label: "Pack zip", hint: "Upload-ready archive" },
1872
+ { value: "upload", label: "Upload theme", hint: "Send zip to Storify" },
1873
+ { value: "new", label: "Create new theme", hint: "From starter template" },
1874
+ { value: "login", label: "Sign in / switch store", hint: "Save credentials" },
1875
+ { value: "status", label: "Check setup", hint: "Config + theme folder" },
1876
+ { value: "help", label: "Show quick start", hint: "Common commands" },
1877
+ { value: "exit", label: "Exit", hint: "Leave the CLI" }
1878
+ ];
1879
+ async function runMenuAction(action) {
1880
+ switch (action) {
1881
+ case "preview": {
1882
+ const mode = await p4.select({
1883
+ message: "Preview mode",
1884
+ options: [
1885
+ { value: "local", label: "Local", hint: "localhost storefront + Vite" },
1886
+ { value: "remote", label: "Remote", hint: "Production storefront + cloudflared tunnel" }
1887
+ ]
1888
+ });
1889
+ if (p4.isCancel(mode)) {
1890
+ p4.cancel("Preview cancelled.");
1891
+ return;
1892
+ }
1893
+ await runDevCommand({
1894
+ linkStore: true,
1895
+ openStorefront: true,
1896
+ local: mode === "local",
1897
+ remote: mode === "remote",
1898
+ interactiveShell: true
1899
+ });
1900
+ p4.log.success("Preview stopped. Back to menu.");
1901
+ break;
1902
+ }
1903
+ case "validate":
1904
+ await runValidateCommand({});
1905
+ break;
1906
+ case "build":
1907
+ await runBuildCommand({});
1908
+ break;
1909
+ case "pack":
1910
+ await runPackCommand({});
1911
+ break;
1912
+ case "upload":
1913
+ await runUploadCommand({});
1914
+ break;
1915
+ case "new": {
1916
+ const name = await p4.text({
1917
+ message: "Theme folder name",
1918
+ placeholder: "my-theme",
1919
+ validate: (value) => value?.trim() ? void 0 : "Name is required"
1920
+ });
1921
+ if (p4.isCancel(name)) {
1922
+ p4.cancel("Cancelled.");
1923
+ return;
1924
+ }
1925
+ await runNewCommand(String(name).trim(), {});
1926
+ break;
1927
+ }
1928
+ case "login":
1929
+ await runLoginCommand({});
1930
+ break;
1931
+ case "status":
1932
+ await runStatusCommand({});
1933
+ break;
1934
+ case "help":
1935
+ printQuickStart();
1936
+ break;
1937
+ case "exit":
1938
+ break;
1939
+ default:
1940
+ break;
1941
+ }
1942
+ }
1628
1943
  async function runInteractiveRoot(version2) {
1629
1944
  if (!isInteractive()) {
1630
1945
  printBanner(version2);
1631
1946
  printQuickStart();
1632
1947
  return;
1633
1948
  }
1634
- p3.intro(`${brand.name} CLI ${pc2.dim(`v${version2}`)}`);
1635
- const action = await p3.select({
1636
- message: "What would you like to do?",
1637
- options: [
1638
- { value: "preview", label: "Start live theme preview", hint: "Login \u2192 pick store \u2192 storefront" },
1639
- { value: "validate", label: "Validate theme", hint: "Manifest + sections" },
1640
- { value: "build", label: "Build theme", hint: "dist/ output" },
1641
- { value: "pack", label: "Pack zip", hint: "Upload-ready archive" },
1642
- { value: "upload", label: "Upload theme", hint: "Send zip to Storify" },
1643
- { value: "new", label: "Create new theme", hint: "From starter template" },
1644
- { value: "login", label: "Sign in / switch store", hint: "Save credentials" },
1645
- { value: "status", label: "Check setup", hint: "Config + theme folder" },
1646
- { value: "help", label: "Show quick start", hint: "Common commands" }
1647
- ]
1648
- });
1649
- if (p3.isCancel(action)) {
1650
- p3.cancel("Cancelled.");
1651
- process.exit(0);
1652
- }
1653
- try {
1654
- switch (action) {
1655
- case "preview":
1656
- await runDevCommand({ linkStore: true, openStorefront: true, local: true });
1657
- break;
1658
- case "validate":
1659
- await runValidateCommand({});
1660
- break;
1661
- case "build":
1662
- await runBuildCommand({});
1663
- break;
1664
- case "pack":
1665
- await runPackCommand({});
1666
- break;
1667
- case "upload":
1668
- await runUploadCommand({});
1669
- break;
1670
- case "new": {
1671
- const name = await p3.text({
1672
- message: "Theme folder name",
1673
- placeholder: "my-theme",
1674
- validate: (value) => value?.trim() ? void 0 : "Name is required"
1675
- });
1676
- if (p3.isCancel(name)) {
1677
- p3.cancel("Cancelled.");
1678
- process.exit(0);
1679
- }
1680
- await runNewCommand(String(name).trim(), {});
1681
- break;
1949
+ showMenuShell();
1950
+ while (true) {
1951
+ const action = await p4.select({
1952
+ message: "What would you like to do?",
1953
+ options: MENU_OPTIONS
1954
+ });
1955
+ if (p4.isCancel(action)) {
1956
+ p4.outro("Goodbye.");
1957
+ break;
1958
+ }
1959
+ if (action === "exit") {
1960
+ p4.outro("Goodbye.");
1961
+ break;
1962
+ }
1963
+ try {
1964
+ await runMenuAction(action);
1965
+ if (action !== "help") {
1966
+ showMenuShell();
1682
1967
  }
1683
- case "login":
1684
- await runLoginCommand({});
1685
- break;
1686
- case "status":
1687
- await runStatusCommand({});
1688
- break;
1689
- case "help":
1690
- printQuickStart();
1691
- p3.outro("Done.");
1692
- break;
1693
- default:
1694
- break;
1968
+ } catch (error) {
1969
+ p4.log.error(error instanceof Error ? error.message : String(error));
1970
+ p4.log.message(pc5.dim("Press \u2191 to pick another command."));
1695
1971
  }
1696
- } catch (error) {
1697
- p3.log.error(error instanceof Error ? error.message : String(error));
1698
- process.exit(1);
1699
1972
  }
1700
1973
  }
1701
1974
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@storifycli/cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "description": "Storify theme development CLI — login, live dev preview, validate, build, pack, and upload themes.",
6
6
  "bin": {