@stackable-labs/cli-app-extension 1.7.1 → 1.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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +219 -138
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @stackable-labs/cli-app-extension
2
2
 
3
- CLI for scaffolding new [Stackable](https://www.npmjs.com/package/@stackable-labs/sdk-extension-react) extension projects.
3
+ CLI for scaffolding new [Stackable](https://www.npmjs.com/package/@stackable-labs/sdk-extension-react) App Extension projects.
4
4
 
5
5
  ## Quick Start
6
6
 
@@ -79,10 +79,10 @@ my-extension/
79
79
  ```bash
80
80
  cd my-extension
81
81
  pnpm install # if --skip-install was used
82
- pnpm dev # starts both extension + preview with hot-reload
82
+ pnpm dev # starts both Extension + Preview with hot-reload
83
83
  ```
84
84
 
85
- The preview host runs at the configured preview port and hot-reloads whenever the extension changes.
85
+ The preview host runs at the configured preview port and hot-reloads whenever the Extension changes.
86
86
 
87
87
  ## Requirements
88
88
 
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { program } from "commander";
6
6
  import { render } from "ink";
7
7
 
8
8
  // src/App.tsx
9
- import { join as join2 } from "path";
9
+ import { join as join3 } from "path";
10
10
  import { Box as Box13, Text as Text13, useApp } from "ink";
11
11
  import { useCallback, useState as useState8 } from "react";
12
12
 
@@ -776,7 +776,7 @@ var createExtensionRemote = async (appId, payload) => {
776
776
  });
777
777
  if (!res.ok) {
778
778
  const body = await res.text();
779
- throw new Error(`Failed to create extension: ${res.status} ${body}`);
779
+ throw new Error(`Failed to create Extension: ${res.status} ${body}`);
780
780
  }
781
781
  return res.json();
782
782
  };
@@ -784,7 +784,7 @@ var fetchExtensions = async (appId) => {
784
784
  const baseUrl = getAdminApiBaseUrl();
785
785
  const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`);
786
786
  if (!res.ok) {
787
- throw new Error(`Failed to fetch extensions: ${res.status} ${res.statusText}`);
787
+ throw new Error(`Failed to fetch Extensions: ${res.status} ${res.statusText}`);
788
788
  }
789
789
  const adminExtensions = await res.json();
790
790
  return Object.fromEntries(
@@ -1020,9 +1020,96 @@ var postScaffold = async (options) => {
1020
1020
  };
1021
1021
 
1022
1022
  // src/lib/scaffold.ts
1023
- import { readFile, readdir, rm, writeFile } from "fs/promises";
1024
- import { join } from "path";
1023
+ import { readFile as readFile2, readdir, rm, writeFile as writeFile2 } from "fs/promises";
1024
+ import { join as join2 } from "path";
1025
1025
  import { downloadTemplate } from "giget";
1026
+
1027
+ // src/lib/devContext.ts
1028
+ import { readFile, writeFile } from "fs/promises";
1029
+ import { join } from "path";
1030
+ var parseEnvFile = (content) => {
1031
+ const lines = content.split("\n");
1032
+ const env = {};
1033
+ for (const line of lines) {
1034
+ const trimmed = line.trim();
1035
+ if (trimmed && !trimmed.startsWith("#")) {
1036
+ const [key, ...rest] = trimmed.split("=");
1037
+ if (key && rest.length > 0) {
1038
+ env[key.trim()] = rest.join("=").trim();
1039
+ }
1040
+ }
1041
+ }
1042
+ return env;
1043
+ };
1044
+ var readEnvFile = async (filePath) => {
1045
+ try {
1046
+ const content = await readFile(filePath, "utf8");
1047
+ return parseEnvFile(content);
1048
+ } catch {
1049
+ return {};
1050
+ }
1051
+ };
1052
+ var writeEnvFile = async (filePath, env) => {
1053
+ const lines = Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n");
1054
+ await writeFile(filePath, `${lines}
1055
+ `);
1056
+ };
1057
+ var readDevContext = async (projectRoot) => {
1058
+ const stackableEnv = await readEnvFile(join(projectRoot, ".env.stackable"));
1059
+ const env = await readEnvFile(join(projectRoot, ".env"));
1060
+ let extensionName = stackableEnv.EXTENSION_NAME || "Unknown Extension";
1061
+ try {
1062
+ const manifestPath = join(projectRoot, "packages/extension/public/manifest.json");
1063
+ const manifestContent = await readFile(manifestPath, "utf8");
1064
+ const manifest = JSON.parse(manifestContent);
1065
+ extensionName = manifest.name;
1066
+ } catch {
1067
+ }
1068
+ const devLocalEnv = await readEnvFile(join(projectRoot, ".env.development.local"));
1069
+ const extensionPort = parseInt(devLocalEnv.VITE_EXTENSION_PORT || env.VITE_EXTENSION_PORT || "5173", 10);
1070
+ const previewPort = parseInt(devLocalEnv.VITE_PREVIEW_PORT || env.VITE_PREVIEW_PORT || "5174", 10);
1071
+ return {
1072
+ projectRoot,
1073
+ extensionName,
1074
+ appId: stackableEnv.APP_ID || null,
1075
+ extensionId: stackableEnv.EXTENSION_ID || null,
1076
+ appName: stackableEnv.APP_NAME || null,
1077
+ originalBundleUrl: stackableEnv.ORIGINAL_BUNDLE_URL || null,
1078
+ extensionPort,
1079
+ previewPort
1080
+ };
1081
+ };
1082
+ var DEV_LOCAL_ENV = ".env.development.local";
1083
+ var patchProjectEnv = async (projectRoot, key, value) => {
1084
+ const envPath = join(projectRoot, DEV_LOCAL_ENV);
1085
+ const existing = await readEnvFile(envPath);
1086
+ existing[key] = value;
1087
+ await writeEnvFile(envPath, existing);
1088
+ };
1089
+ var removeProjectEnvKey = async (projectRoot, key) => {
1090
+ const envPath = join(projectRoot, DEV_LOCAL_ENV);
1091
+ const existing = await readEnvFile(envPath);
1092
+ delete existing[key];
1093
+ await writeEnvFile(envPath, existing);
1094
+ };
1095
+ var writeDevContext = async (projectRoot, ctx) => {
1096
+ const env = {
1097
+ APP_ID: ctx.appId || "",
1098
+ EXTENSION_ID: ctx.extensionId || ""
1099
+ };
1100
+ if (ctx.appName) {
1101
+ env.APP_NAME = ctx.appName;
1102
+ }
1103
+ if (ctx.extensionName && ctx.extensionName !== "Unknown Extension") {
1104
+ env.EXTENSION_NAME = ctx.extensionName;
1105
+ }
1106
+ if (ctx.originalBundleUrl) {
1107
+ env.ORIGINAL_BUNDLE_URL = ctx.originalBundleUrl;
1108
+ }
1109
+ await writeEnvFile(join(projectRoot, ".env.stackable"), env);
1110
+ };
1111
+
1112
+ // src/lib/scaffold.ts
1026
1113
  var toKebabCase = (value) => value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
1027
1114
  var isTextFile = (filePath) => /\.(ts|tsx|js|jsx|json|md|html|yml|yaml|env|gitignore|nvmrc)$/i.test(filePath);
1028
1115
  var normalizeTargets = (targets) => Array.from(new Set(targets));
@@ -1042,7 +1129,7 @@ var derivePermissions = (targets) => {
1042
1129
  };
1043
1130
  var upsertOrRemove = async (filePath, shouldExist, content) => {
1044
1131
  if (shouldExist) {
1045
- await writeFile(filePath, content);
1132
+ await writeFile2(filePath, content);
1046
1133
  return;
1047
1134
  }
1048
1135
  await rm(filePath, { force: true });
@@ -1054,7 +1141,7 @@ var walkFiles = async (rootDir) => {
1054
1141
  if (entry.name === ".git" || entry.name === "node_modules" || entry.name === "dist") {
1055
1142
  continue;
1056
1143
  }
1057
- const fullPath = join(rootDir, entry.name);
1144
+ const fullPath = join2(rootDir, entry.name);
1058
1145
  if (entry.isDirectory()) {
1059
1146
  files.push(...await walkFiles(fullPath));
1060
1147
  continue;
@@ -1069,7 +1156,7 @@ var replacePlaceholders = async (rootDir, replacements) => {
1069
1156
  if (!isTextFile(filePath)) {
1070
1157
  continue;
1071
1158
  }
1072
- let content = await readFile(filePath, "utf8");
1159
+ let content = await readFile2(filePath, "utf8");
1073
1160
  let changed = false;
1074
1161
  for (const [needle, value] of Object.entries(replacements)) {
1075
1162
  if (content.includes(needle)) {
@@ -1078,18 +1165,18 @@ var replacePlaceholders = async (rootDir, replacements) => {
1078
1165
  }
1079
1166
  }
1080
1167
  if (changed) {
1081
- await writeFile(filePath, content);
1168
+ await writeFile2(filePath, content);
1082
1169
  }
1083
1170
  }
1084
1171
  };
1085
1172
  var generateManifest = async (rootDir, extensionName, targets, permissions) => {
1086
- const manifestPath = join(rootDir, "packages/extension/public/manifest.json");
1087
- const raw = await readFile(manifestPath, "utf8");
1173
+ const manifestPath = join2(rootDir, "packages/extension/public/manifest.json");
1174
+ const raw = await readFile2(manifestPath, "utf8");
1088
1175
  const manifest = JSON.parse(raw);
1089
1176
  manifest.name = extensionName;
1090
1177
  manifest.targets = targets;
1091
1178
  manifest.permissions = permissions;
1092
- await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}
1179
+ await writeFile2(manifestPath, `${JSON.stringify(manifest, null, 2)}
1093
1180
  `);
1094
1181
  };
1095
1182
  var buildFooterSurface = (targets) => {
@@ -1116,12 +1203,12 @@ ${blocks.join("\n")}
1116
1203
  `;
1117
1204
  };
1118
1205
  var generateSurfaceFiles = async (rootDir, targets) => {
1119
- const surfaceDir = join(rootDir, "packages/extension/src/surfaces");
1206
+ const surfaceDir = join2(rootDir, "packages/extension/src/surfaces");
1120
1207
  const wantsHeader = targets.includes("slot.header");
1121
1208
  const wantsContent = targets.includes("slot.content");
1122
1209
  const wantsFooter = targets.includes("slot.footer") || targets.includes("slot.footer-links");
1123
1210
  await upsertOrRemove(
1124
- join(surfaceDir, "Header.tsx"),
1211
+ join2(surfaceDir, "Header.tsx"),
1125
1212
  wantsHeader,
1126
1213
  `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
1127
1214
 
@@ -1135,7 +1222,7 @@ export function Header() {
1135
1222
  `
1136
1223
  );
1137
1224
  await upsertOrRemove(
1138
- join(surfaceDir, "Content.tsx"),
1225
+ join2(surfaceDir, "Content.tsx"),
1139
1226
  wantsContent,
1140
1227
  `import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
1141
1228
  import { appStore } from '../store'
@@ -1168,14 +1255,14 @@ export function Content() {
1168
1255
  `
1169
1256
  );
1170
1257
  await upsertOrRemove(
1171
- join(rootDir, "packages/extension/src/store.ts"),
1258
+ join2(rootDir, "packages/extension/src/store.ts"),
1172
1259
  wantsContent,
1173
1260
  "import { createStore } from '@stackable-labs/sdk-extension-react'\n\nexport type ViewState = { type: 'menu' }\n\nexport interface AppState {\n viewState: ViewState\n}\n\nexport const appStore = createStore<AppState>({\n viewState: { type: 'menu' },\n})\n"
1174
1261
  );
1175
- await upsertOrRemove(join(surfaceDir, "Footer.tsx"), wantsFooter, buildFooterSurface(targets));
1262
+ await upsertOrRemove(join2(surfaceDir, "Footer.tsx"), wantsFooter, buildFooterSurface(targets));
1176
1263
  };
1177
1264
  var rewriteExtensionIndex = async (rootDir, extensionId, targets) => {
1178
- const indexPath = join(rootDir, "packages/extension/src/index.tsx");
1265
+ const indexPath = join2(rootDir, "packages/extension/src/index.tsx");
1179
1266
  const imports = ["import { createExtension } from '@stackable-labs/sdk-extension-react'"];
1180
1267
  const components = [];
1181
1268
  if (targets.includes("slot.header")) {
@@ -1201,10 +1288,10 @@ ${components.join("\n")}
1201
1288
  { extensionId: '${toKebabCase(extensionId)}' },
1202
1289
  )
1203
1290
  `;
1204
- await writeFile(indexPath, content);
1291
+ await writeFile2(indexPath, content);
1205
1292
  };
1206
1293
  var rewritePreviewApp = async (rootDir, targets, permissions) => {
1207
- const appPath = join(rootDir, "packages/preview/src/App.tsx");
1294
+ const appPath = join2(rootDir, "packages/preview/src/App.tsx");
1208
1295
  const includeDataQuery = permissions.includes("data:query");
1209
1296
  const includeToast = permissions.includes("actions:toast");
1210
1297
  const includeInvoke = permissions.includes("actions:invoke");
@@ -1280,17 +1367,17 @@ export default function App() {
1280
1367
  )
1281
1368
  }
1282
1369
  `;
1283
- await writeFile(appPath, appContent);
1370
+ await writeFile2(appPath, appContent);
1284
1371
  };
1285
1372
  var patchViteAllowedHosts = async (rootDir) => {
1286
1373
  const configs = [
1287
- join(rootDir, "packages/extension/vite.config.ts"),
1288
- join(rootDir, "packages/preview/vite.config.ts")
1374
+ join2(rootDir, "packages/extension/vite.config.ts"),
1375
+ join2(rootDir, "packages/preview/vite.config.ts")
1289
1376
  ];
1290
1377
  for (const configPath of configs) {
1291
1378
  let content;
1292
1379
  try {
1293
- content = await readFile(configPath, "utf8");
1380
+ content = await readFile2(configPath, "utf8");
1294
1381
  } catch {
1295
1382
  continue;
1296
1383
  }
@@ -1300,38 +1387,38 @@ var patchViteAllowedHosts = async (rootDir) => {
1300
1387
  "server: {\n allowedHosts: true,"
1301
1388
  );
1302
1389
  if (patched !== content) {
1303
- await writeFile(configPath, patched);
1390
+ await writeFile2(configPath, patched);
1304
1391
  }
1305
1392
  }
1306
1393
  };
1307
1394
  var rewriteTurboJson = async (rootDir) => {
1308
- const turboPath = join(rootDir, "turbo.json");
1309
- const raw = await readFile(turboPath, "utf8");
1395
+ const turboPath = join2(rootDir, "turbo.json");
1396
+ const raw = await readFile2(turboPath, "utf8");
1310
1397
  const turbo = JSON.parse(raw);
1311
1398
  delete turbo["extends"];
1312
1399
  turbo["globalEnv"] = ["VITE_EXTENSION_PORT", "VITE_PREVIEW_PORT", "VITE_EXTENSION_BUNDLE_URL"];
1313
- await writeFile(turboPath, `${JSON.stringify(turbo, null, 2)}
1400
+ await writeFile2(turboPath, `${JSON.stringify(turbo, null, 2)}
1314
1401
  `);
1315
1402
  };
1316
- var writeEnvFile = async (dir, extensionPort, previewPort) => {
1317
- const envPath = join(dir, ".env");
1403
+ var writeEnvFile2 = async (dir, extensionPort, previewPort) => {
1404
+ const envPath = join2(dir, ".env");
1318
1405
  const content = `VITE_EXTENSION_PORT=${extensionPort}
1319
1406
  VITE_PREVIEW_PORT=${previewPort}
1320
1407
  `;
1321
- await writeFile(envPath, content);
1408
+ await writeFile2(envPath, content);
1322
1409
  };
1323
1410
  var updateGitignore = async (dir) => {
1324
- const gitignorePath = join(dir, ".gitignore");
1411
+ const gitignorePath = join2(dir, ".gitignore");
1325
1412
  let gitignore = "";
1326
1413
  try {
1327
- gitignore = await readFile(gitignorePath, "utf8");
1414
+ gitignore = await readFile2(gitignorePath, "utf8");
1328
1415
  } catch {
1329
1416
  }
1330
1417
  if (!gitignore.includes(".env.stackable")) {
1331
1418
  const newGitignore = gitignore ? `${gitignore}
1332
1419
  .env.stackable
1333
1420
  ` : ".env.stackable\n";
1334
- await writeFile(gitignorePath, newGitignore);
1421
+ await writeFile2(gitignorePath, newGitignore);
1335
1422
  }
1336
1423
  };
1337
1424
  var scaffold = async (options) => {
@@ -1353,8 +1440,17 @@ var scaffold = async (options) => {
1353
1440
  await rewritePreviewApp(dir, selectedTargets, derivedPermissions);
1354
1441
  await rewriteTurboJson(dir);
1355
1442
  await patchViteAllowedHosts(dir);
1356
- await writeEnvFile(dir, options.extensionPort, options.previewPort);
1443
+ await writeEnvFile2(dir, options.extensionPort, options.previewPort);
1357
1444
  await updateGitignore(dir);
1445
+ await writeDevContext(dir, {
1446
+ projectRoot: dir,
1447
+ appId: options.appId,
1448
+ extensionId: toKebabCase(options.extensionId || options.name),
1449
+ appName: options.appName || null,
1450
+ extensionName: options.name,
1451
+ extensionPort: options.extensionPort,
1452
+ previewPort: options.previewPort
1453
+ });
1358
1454
  return options;
1359
1455
  };
1360
1456
 
@@ -1369,7 +1465,7 @@ var STEPS = {
1369
1465
  };
1370
1466
  var PROGRESS_STEPS = {
1371
1467
  ["create" /* CREATE */]: [
1372
- { label: "Registering extension", status: "pending" },
1468
+ { label: "Registering Extension", status: "pending" },
1373
1469
  { label: "Fetching template", status: "pending" },
1374
1470
  { label: "Generating files", status: "pending" },
1375
1471
  { label: "Installing dependencies", status: "pending" }
@@ -1380,7 +1476,7 @@ var PROGRESS_STEPS = {
1380
1476
  { label: "Installing dependencies", status: "pending" }
1381
1477
  ],
1382
1478
  ["update" /* UPDATE */]: [
1383
- { label: "Updating extension", status: "pending" }
1479
+ { label: "Updating Extension", status: "pending" }
1384
1480
  ],
1385
1481
  ["dev" /* DEV */]: []
1386
1482
  // Dev command has no progress steps
@@ -1486,7 +1582,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1486
1582
  const handleConfirmTargets = (value) => {
1487
1583
  setTargets(value);
1488
1584
  if (options?.extensionPort || options?.previewPort) {
1489
- setOutputDir(join2(process.cwd(), toKebabCase2(extensionId || name)));
1585
+ setOutputDir(join3(process.cwd(), toKebabCase2(extensionId || name)));
1490
1586
  setStep("confirm");
1491
1587
  } else {
1492
1588
  setStep("settings");
@@ -1500,7 +1596,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1500
1596
  const handleTargets = (value) => {
1501
1597
  setTargets(value);
1502
1598
  if (options?.extensionPort || options?.previewPort) {
1503
- setOutputDir(join2(process.cwd(), toKebabCase2(extensionId || name)));
1599
+ setOutputDir(join3(process.cwd(), toKebabCase2(extensionId || name)));
1504
1600
  setStep("confirm");
1505
1601
  } else {
1506
1602
  setStep("settings");
@@ -1575,6 +1671,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1575
1671
  updateStep(scaffoldStepOffset + 0, "running");
1576
1672
  await scaffold({
1577
1673
  appId: selectedApp.id,
1674
+ appName: selectedApp.name,
1578
1675
  name,
1579
1676
  extensionId: resolvedExtensionId,
1580
1677
  targets,
@@ -1653,7 +1750,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1653
1750
  return /* @__PURE__ */ jsx13(
1654
1751
  SettingsPrompt,
1655
1752
  {
1656
- defaultDir: join2(process.cwd(), toKebabCase2(extensionId || name)),
1753
+ defaultDir: join3(process.cwd(), toKebabCase2(extensionId || name)),
1657
1754
  onSubmit: handleSettings,
1658
1755
  onBack: goBack
1659
1756
  }
@@ -1745,82 +1842,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1745
1842
 
1746
1843
  // src/components/DevApp.tsx
1747
1844
  import { Box as Box16, Text as Text16 } from "ink";
1748
- import { useState as useState10, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1749
-
1750
- // src/lib/devContext.ts
1751
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1752
- import { join as join3 } from "path";
1753
- var parseEnvFile = (content) => {
1754
- const lines = content.split("\n");
1755
- const env = {};
1756
- for (const line of lines) {
1757
- const trimmed = line.trim();
1758
- if (trimmed && !trimmed.startsWith("#")) {
1759
- const [key, ...rest] = trimmed.split("=");
1760
- if (key && rest.length > 0) {
1761
- env[key.trim()] = rest.join("=").trim();
1762
- }
1763
- }
1764
- }
1765
- return env;
1766
- };
1767
- var readEnvFile = async (filePath) => {
1768
- try {
1769
- const content = await readFile2(filePath, "utf8");
1770
- return parseEnvFile(content);
1771
- } catch {
1772
- return {};
1773
- }
1774
- };
1775
- var writeEnvFile2 = async (filePath, env) => {
1776
- const lines = Object.entries(env).map(([key, value]) => `${key}=${value}`).join("\n");
1777
- await writeFile2(filePath, `${lines}
1778
- `);
1779
- };
1780
- var readDevContext = async (projectRoot) => {
1781
- const stackableEnv = await readEnvFile(join3(projectRoot, ".env.stackable"));
1782
- const env = await readEnvFile(join3(projectRoot, ".env"));
1783
- let extensionName = "Unknown Extension";
1784
- try {
1785
- const manifestPath = join3(projectRoot, "packages/extension/public/manifest.json");
1786
- const manifestContent = await readFile2(manifestPath, "utf8");
1787
- const manifest = JSON.parse(manifestContent);
1788
- extensionName = manifest.name;
1789
- } catch {
1790
- }
1791
- const extensionPort = parseInt(env.VITE_EXTENSION_PORT || stackableEnv.EXTENSION_PORT || "5173", 10);
1792
- const previewPort = parseInt(env.VITE_PREVIEW_PORT || stackableEnv.PREVIEW_PORT || "5174", 10);
1793
- return {
1794
- projectRoot,
1795
- extensionName,
1796
- appId: stackableEnv.APP_ID || null,
1797
- extensionId: stackableEnv.EXTENSION_ID || null,
1798
- extensionPort,
1799
- previewPort
1800
- };
1801
- };
1802
- var DEV_LOCAL_ENV = ".env.development.local";
1803
- var patchProjectEnv = async (projectRoot, key, value) => {
1804
- const envPath = join3(projectRoot, DEV_LOCAL_ENV);
1805
- const existing = await readEnvFile(envPath);
1806
- existing[key] = value;
1807
- await writeEnvFile2(envPath, existing);
1808
- };
1809
- var removeProjectEnvKey = async (projectRoot, key) => {
1810
- const envPath = join3(projectRoot, DEV_LOCAL_ENV);
1811
- const existing = await readEnvFile(envPath);
1812
- delete existing[key];
1813
- await writeEnvFile2(envPath, existing);
1814
- };
1815
- var writeDevContext = async (projectRoot, ctx) => {
1816
- const env = {
1817
- APP_ID: ctx.appId || "",
1818
- EXTENSION_ID: ctx.extensionId || "",
1819
- EXTENSION_PORT: ctx.extensionPort.toString(),
1820
- PREVIEW_PORT: ctx.previewPort.toString()
1821
- };
1822
- await writeEnvFile2(join3(projectRoot, ".env.stackable"), env);
1823
- };
1845
+ import { useRef, useState as useState10, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1824
1846
 
1825
1847
  // src/lib/tunnel.ts
1826
1848
  import { Tunnel } from "cloudflared";
@@ -1904,6 +1926,7 @@ var DevDashboard = ({
1904
1926
  appId,
1905
1927
  appName,
1906
1928
  tunnelUrl,
1929
+ previewTunnelUrl,
1907
1930
  previewPort,
1908
1931
  extensionPort,
1909
1932
  bundleUrlUpdated,
@@ -1952,22 +1975,30 @@ var DevDashboard = ({
1952
1975
  ] }),
1953
1976
  /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", gap: 1, children: [
1954
1977
  /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1955
- /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Preview:" }),
1978
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Tunnel Dev - Preview:" }),
1979
+ /* @__PURE__ */ jsx15(Text15, { children: previewTunnelUrl || "Connecting\u2026" })
1980
+ ] }),
1981
+ /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1982
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Local Dev - Preview:" }),
1956
1983
  /* @__PURE__ */ jsxs15(Text15, { children: [
1957
1984
  "http://localhost:",
1958
1985
  previewPort
1959
1986
  ] })
1960
1987
  ] }),
1961
1988
  /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1962
- /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Local:" }),
1989
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Tunnel Dev - Extension:" }),
1990
+ /* @__PURE__ */ jsx15(Text15, { children: tunnelUrl || "Connecting\u2026" })
1991
+ ] }),
1992
+ /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1993
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Local Dev - Extension:" }),
1963
1994
  /* @__PURE__ */ jsxs15(Text15, { children: [
1964
1995
  "http://localhost:",
1965
1996
  extensionPort
1966
1997
  ] })
1967
1998
  ] }),
1968
1999
  /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
1969
- /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Tunnel:" }),
1970
- /* @__PURE__ */ jsx15(Text15, { children: tunnelUrl || "Connecting\u2026" })
2000
+ /* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Bundle URL:" }),
2001
+ /* @__PURE__ */ jsx15(Text15, { children: bundleUrlUpdated ? `(${tunnelUrl}) updated \u2713` : "" })
1971
2002
  ] })
1972
2003
  ] })
1973
2004
  ]
@@ -1978,15 +2009,25 @@ var DevDashboard = ({
1978
2009
 
1979
2010
  // src/components/DevApp.tsx
1980
2011
  import { jsx as jsx16 } from "react/jsx-runtime";
2012
+ var isEphemeralUrl = (url) => {
2013
+ try {
2014
+ const { hostname } = new URL(url);
2015
+ return hostname.endsWith(".ngrok.io") || hostname.endsWith(".ngrok-free.app") || hostname.endsWith(".trycloudflare.com");
2016
+ } catch {
2017
+ return true;
2018
+ }
2019
+ };
1981
2020
  var DevApp = ({ options = {} }) => {
1982
2021
  const [state, setState] = useState10("setup");
1983
2022
  const [devContext, setDevContext] = useState10(null);
1984
2023
  const [resolvedContext, setResolvedContext] = useState10(null);
1985
2024
  const [tunnelUrl, setTunnelUrl] = useState10(null);
2025
+ const [previewTunnelUrl, setPreviewTunnelUrl] = useState10(null);
1986
2026
  const [bundleUrlUpdated, setBundleUrlUpdated] = useState10(false);
1987
2027
  const [tunnelHandle, setTunnelHandle] = useState10(null);
2028
+ const [previewTunnelHandle, setPreviewTunnelHandle] = useState10(null);
1988
2029
  const [devServerHandle, setDevServerHandle] = useState10(null);
1989
- const [originalBundleUrl, setOriginalBundleUrl] = useState10("");
2030
+ const shuttingDown = useRef(false);
1990
2031
  const useTunnel = options.tunnel !== false;
1991
2032
  const useUpdate = options.update !== false;
1992
2033
  const useRestore = options.restore !== false;
@@ -2005,22 +2046,53 @@ var DevApp = ({ options = {} }) => {
2005
2046
  await writeDevContext(devContext.projectRoot, {
2006
2047
  ...devContext,
2007
2048
  appId: resolved.appId,
2008
- extensionId: resolved.extensionId
2049
+ extensionId: resolved.extensionId,
2050
+ appName: resolved.appName || null
2009
2051
  });
2010
2052
  const extensionPort = options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort;
2053
+ const previewPort = options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort;
2011
2054
  await patchViteAllowedHosts(devContext.projectRoot);
2055
+ let cachedOriginalUrl = devContext.originalBundleUrl;
2056
+ if (useUpdate) {
2057
+ try {
2058
+ console.log("[dev] Fetching current bundleUrl from API...");
2059
+ const extensions = await fetchExtensions(resolved.appId);
2060
+ const currentExtension = extensions[resolved.extensionId];
2061
+ if (currentExtension?.bundleUrl) {
2062
+ console.log(`[dev] Current bundleUrl: ${currentExtension.bundleUrl}`);
2063
+ if (!isEphemeralUrl(currentExtension.bundleUrl)) {
2064
+ console.log("[dev] Caching stable bundleUrl to .env.stackable");
2065
+ cachedOriginalUrl = currentExtension.bundleUrl;
2066
+ await writeDevContext(devContext.projectRoot, {
2067
+ ...devContext,
2068
+ appId: resolved.appId,
2069
+ extensionId: resolved.extensionId,
2070
+ appName: resolved.appName || null,
2071
+ originalBundleUrl: cachedOriginalUrl
2072
+ });
2073
+ } else {
2074
+ console.log("[dev] Current bundleUrl is ephemeral, keeping previous cached value");
2075
+ }
2076
+ }
2077
+ } catch (err) {
2078
+ console.warn("[dev] Failed to fetch current bundleUrl:", err);
2079
+ }
2080
+ }
2012
2081
  if (useTunnel) {
2013
2082
  try {
2014
- console.log(`[dev] Starting tunnel on port ${extensionPort}...`);
2083
+ console.log(`[dev] Starting extension tunnel on port ${extensionPort}...`);
2015
2084
  const tunnelResult = await startTunnel(extensionPort);
2016
- console.log(`[dev] Tunnel ready: ${tunnelResult.url}`);
2085
+ console.log(`[dev] Extension tunnel ready: ${tunnelResult.url}`);
2017
2086
  setTunnelHandle(tunnelResult);
2018
2087
  setTunnelUrl(tunnelResult.url);
2088
+ console.log(`[dev] Starting preview tunnel on port ${previewPort}...`);
2089
+ const previewTunnelResult = await startTunnel(previewPort);
2090
+ console.log(`[dev] Preview tunnel ready: ${previewTunnelResult.url}`);
2091
+ setPreviewTunnelHandle(previewTunnelResult);
2092
+ setPreviewTunnelUrl(previewTunnelResult.url);
2019
2093
  console.log(`[dev] Writing VITE_EXTENSION_BUNDLE_URL=${tunnelResult.url} to .env`);
2020
2094
  await patchProjectEnv(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL", tunnelResult.url);
2021
2095
  if (useUpdate) {
2022
- const originalUrl = `http://localhost:${extensionPort}`;
2023
- setOriginalBundleUrl(originalUrl);
2024
2096
  console.log(`[dev] Updating bundleUrl to ${tunnelResult.url}`);
2025
2097
  await updateExtension(resolved.appId, resolved.extensionId, {
2026
2098
  bundleUrl: tunnelResult.url
@@ -2039,33 +2111,41 @@ var DevApp = ({ options = {} }) => {
2039
2111
  setDevServerHandle(serverHandle);
2040
2112
  console.log("[dev] Ready");
2041
2113
  setState("running");
2042
- }, [devContext, options.extensionPort, useTunnel, useUpdate]);
2114
+ }, [devContext, options.extensionPort, options.previewPort, useTunnel, useUpdate]);
2043
2115
  useEffect6(() => {
2044
2116
  if (state === "setup" && devContext && devContext.appId && devContext.extensionId) {
2045
2117
  handleSetupReady({
2046
2118
  appId: devContext.appId,
2047
- extensionId: devContext.extensionId
2119
+ extensionId: devContext.extensionId,
2120
+ appName: devContext.appName || void 0
2048
2121
  });
2049
2122
  }
2050
2123
  }, [state, devContext, handleSetupReady]);
2051
2124
  const handleQuit = async () => {
2052
- if (!devContext || !resolvedContext) return;
2125
+ if (!devContext || !resolvedContext || shuttingDown.current) return;
2126
+ shuttingDown.current = true;
2053
2127
  setState("stopping");
2054
2128
  console.log("[dev] Shutting down...");
2055
2129
  try {
2056
- if (useRestore && originalBundleUrl) {
2057
- console.log(`[dev] Restoring bundleUrl to ${originalBundleUrl}`);
2130
+ if (useRestore && devContext.originalBundleUrl) {
2131
+ console.log(`[dev] Restoring bundleUrl to ${devContext.originalBundleUrl}`);
2058
2132
  await updateExtension(resolvedContext.appId, resolvedContext.extensionId, {
2059
- bundleUrl: originalBundleUrl
2133
+ bundleUrl: devContext.originalBundleUrl
2060
2134
  });
2061
2135
  console.log("[dev] bundleUrl restored");
2136
+ } else if (useRestore && !devContext.originalBundleUrl) {
2137
+ console.warn("[dev] No cached original bundleUrl to restore");
2062
2138
  }
2063
2139
  console.log("[dev] Removing VITE_EXTENSION_BUNDLE_URL from .env");
2064
2140
  await removeProjectEnvKey(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL");
2065
2141
  if (tunnelHandle) {
2066
- console.log("[dev] Stopping tunnel");
2142
+ console.log("[dev] Stopping Extension tunnel");
2067
2143
  tunnelHandle.stop();
2068
2144
  }
2145
+ if (previewTunnelHandle) {
2146
+ console.log("[dev] Stopping preview tunnel");
2147
+ previewTunnelHandle.stop();
2148
+ }
2069
2149
  if (devServerHandle) {
2070
2150
  console.log("[dev] Stopping dev server");
2071
2151
  devServerHandle.stop();
@@ -2099,6 +2179,7 @@ var DevApp = ({ options = {} }) => {
2099
2179
  extensionPort: options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort,
2100
2180
  previewPort: options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort,
2101
2181
  tunnelUrl,
2182
+ previewTunnelUrl,
2102
2183
  bundleUrlUpdated,
2103
2184
  onQuit: handleQuit
2104
2185
  }
@@ -2111,17 +2192,17 @@ var DevApp = ({ options = {} }) => {
2111
2192
  import { jsx as jsx17 } from "react/jsx-runtime";
2112
2193
  var require2 = createRequire(import.meta.url);
2113
2194
  var { version } = require2("../package.json");
2114
- program.name("stackable-app-extension").description("Stackable app Extension developer CLI").version(version);
2115
- program.command("create" /* CREATE */).description("Create a new Extension project").argument("[name]", "Extension project name").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview host dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((name, options) => {
2195
+ program.name("stackable-app-extension").description("Stackable Labs - App Extension developer CLI").version(version);
2196
+ program.command("create" /* CREATE */).description("Create a new Extension project").argument("[name]", "Extension project name").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((name, options) => {
2116
2197
  render(/* @__PURE__ */ jsx17(App, { command: "create" /* CREATE */, initialName: name, options }));
2117
2198
  });
2118
- program.command("scaffold" /* SCAFFOLD */).description("Scaffold a local project from an existing Extension").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview host dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((options) => {
2199
+ program.command("scaffold" /* SCAFFOLD */).description("Scaffold a local project from an existing Extension").option("--extension-port <port>", "Extension dev server port (default: 5173)").option("--preview-port <port>", "Preview dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action((options) => {
2119
2200
  render(/* @__PURE__ */ jsx17(App, { command: "scaffold" /* SCAFFOLD */, options }));
2120
2201
  });
2121
- program.command("update" /* UPDATE */).description("Update an existing Extension").argument("[extensionId]", "Extension ID to update").option("--app-id <id>", "Skip app selection").option("--name <name>", "New extension name").option("--targets <targets>", "Comma-separated target slots (validated against app)").option("--bundle-url <url>", "New bundle URL").option("--enabled <bool>", "Enable/disable extension").option("--set-version <version>", "Explicit version (skips auto-compute)").action((extensionId, options) => {
2202
+ program.command("update" /* UPDATE */).description("Update an existing Extension").argument("[extensionId]", "Extension ID to update").option("--app-id <id>", "Skip App selection").option("--name <name>", "New Extension name").option("--targets <targets>", "Comma-separated target slots (validated against app)").option("--bundle-url <url>", "New bundle URL").option("--enabled <bool>", "Enable/disable Extension").option("--set-version <version>", "Explicit version (skips auto-compute)").action((extensionId, options) => {
2122
2203
  render(/* @__PURE__ */ jsx17(App, { command: "update" /* UPDATE */, initialExtensionId: extensionId, options }));
2123
2204
  });
2124
- program.command("dev" /* DEV */).description("Start dev servers with a public tunnel").option("--dir <path>", "Project root (default: cwd)").option("--extension-port <port>", "Override extension port").option("--preview-port <port>", "Override preview port").option("--no-tunnel", "Skip tunnel, just run vite dev").option("--no-update", "Skip bundleUrl update on server").option("--no-restore", "On exit, keep tunnel URL as bundleUrl").action((options) => {
2205
+ program.command("dev" /* DEV */).description("Start dev servers with a public tunnel").option("--dir <path>", "Project root (default: cwd)").option("--extension-port <port>", "Override Extension port").option("--preview-port <port>", "Override Preview port").option("--no-tunnel", "Skip tunnel, just run vite dev").option("--no-update", "Skip bundleUrl update on server").option("--no-restore", "On exit, keep tunnel URL as bundleUrl").action((options) => {
2125
2206
  render(/* @__PURE__ */ jsx17(DevApp, { options }));
2126
2207
  });
2127
2208
  program.parse(process.argv.filter((arg) => arg !== "--"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackable-labs/cli-app-extension",
3
- "version": "1.7.1",
3
+ "version": "1.8.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {