@stackable-labs/cli-app-extension 1.7.2 → 1.9.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/README.md +3 -3
- package/dist/index.js +225 -144
- 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)
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
1168
|
+
await writeFile2(filePath, content);
|
|
1082
1169
|
}
|
|
1083
1170
|
}
|
|
1084
1171
|
};
|
|
1085
1172
|
var generateManifest = async (rootDir, extensionName, targets, permissions) => {
|
|
1086
|
-
const manifestPath =
|
|
1087
|
-
const raw = await
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1262
|
+
await upsertOrRemove(join2(surfaceDir, "Footer.tsx"), wantsFooter, buildFooterSurface(targets));
|
|
1176
1263
|
};
|
|
1177
1264
|
var rewriteExtensionIndex = async (rootDir, extensionId, targets) => {
|
|
1178
|
-
const indexPath =
|
|
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
|
|
1291
|
+
await writeFile2(indexPath, content);
|
|
1205
1292
|
};
|
|
1206
1293
|
var rewritePreviewApp = async (rootDir, targets, permissions) => {
|
|
1207
|
-
const appPath =
|
|
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
|
|
1370
|
+
await writeFile2(appPath, appContent);
|
|
1284
1371
|
};
|
|
1285
1372
|
var patchViteAllowedHosts = async (rootDir) => {
|
|
1286
1373
|
const configs = [
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
|
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
|
|
1390
|
+
await writeFile2(configPath, patched);
|
|
1304
1391
|
}
|
|
1305
1392
|
}
|
|
1306
1393
|
};
|
|
1307
1394
|
var rewriteTurboJson = async (rootDir) => {
|
|
1308
|
-
const turboPath =
|
|
1309
|
-
const raw = await
|
|
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
|
|
1400
|
+
await writeFile2(turboPath, `${JSON.stringify(turbo, null, 2)}
|
|
1314
1401
|
`);
|
|
1315
1402
|
};
|
|
1316
|
-
var
|
|
1317
|
-
const envPath =
|
|
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
|
|
1408
|
+
await writeFile2(envPath, content);
|
|
1322
1409
|
};
|
|
1323
1410
|
var updateGitignore = async (dir) => {
|
|
1324
|
-
const gitignorePath =
|
|
1411
|
+
const gitignorePath = join2(dir, ".gitignore");
|
|
1325
1412
|
let gitignore = "";
|
|
1326
1413
|
try {
|
|
1327
|
-
gitignore = await
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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:
|
|
1753
|
+
defaultDir: join3(process.cwd(), toKebabCase2(extensionId || name)),
|
|
1657
1754
|
onSubmit: handleSettings,
|
|
1658
1755
|
onBack: goBack
|
|
1659
1756
|
}
|
|
@@ -1745,86 +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
|
-
appName: stackableEnv.APP_NAME || null,
|
|
1799
|
-
extensionPort,
|
|
1800
|
-
previewPort
|
|
1801
|
-
};
|
|
1802
|
-
};
|
|
1803
|
-
var DEV_LOCAL_ENV = ".env.development.local";
|
|
1804
|
-
var patchProjectEnv = async (projectRoot, key, value) => {
|
|
1805
|
-
const envPath = join3(projectRoot, DEV_LOCAL_ENV);
|
|
1806
|
-
const existing = await readEnvFile(envPath);
|
|
1807
|
-
existing[key] = value;
|
|
1808
|
-
await writeEnvFile2(envPath, existing);
|
|
1809
|
-
};
|
|
1810
|
-
var removeProjectEnvKey = async (projectRoot, key) => {
|
|
1811
|
-
const envPath = join3(projectRoot, DEV_LOCAL_ENV);
|
|
1812
|
-
const existing = await readEnvFile(envPath);
|
|
1813
|
-
delete existing[key];
|
|
1814
|
-
await writeEnvFile2(envPath, existing);
|
|
1815
|
-
};
|
|
1816
|
-
var writeDevContext = async (projectRoot, ctx) => {
|
|
1817
|
-
const env = {
|
|
1818
|
-
APP_ID: ctx.appId || "",
|
|
1819
|
-
EXTENSION_ID: ctx.extensionId || "",
|
|
1820
|
-
EXTENSION_PORT: ctx.extensionPort.toString(),
|
|
1821
|
-
PREVIEW_PORT: ctx.previewPort.toString()
|
|
1822
|
-
};
|
|
1823
|
-
if (ctx.appName) {
|
|
1824
|
-
env.APP_NAME = ctx.appName;
|
|
1825
|
-
}
|
|
1826
|
-
await writeEnvFile2(join3(projectRoot, ".env.stackable"), env);
|
|
1827
|
-
};
|
|
1845
|
+
import { useRef, useState as useState10, useEffect as useEffect6, useCallback as useCallback2 } from "react";
|
|
1828
1846
|
|
|
1829
1847
|
// src/lib/tunnel.ts
|
|
1830
1848
|
import { Tunnel } from "cloudflared";
|
|
@@ -1908,6 +1926,7 @@ var DevDashboard = ({
|
|
|
1908
1926
|
appId,
|
|
1909
1927
|
appName,
|
|
1910
1928
|
tunnelUrl,
|
|
1929
|
+
previewTunnelUrl,
|
|
1911
1930
|
previewPort,
|
|
1912
1931
|
extensionPort,
|
|
1913
1932
|
bundleUrlUpdated,
|
|
@@ -1922,8 +1941,8 @@ var DevDashboard = ({
|
|
|
1922
1941
|
process.off("SIGINT", handler);
|
|
1923
1942
|
};
|
|
1924
1943
|
}, [onQuit]);
|
|
1925
|
-
useInput8((input) => {
|
|
1926
|
-
if (input === "q") {
|
|
1944
|
+
useInput8((input, key) => {
|
|
1945
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
1927
1946
|
onQuit();
|
|
1928
1947
|
}
|
|
1929
1948
|
});
|
|
@@ -1933,7 +1952,7 @@ var DevDashboard = ({
|
|
|
1933
1952
|
StepShell,
|
|
1934
1953
|
{
|
|
1935
1954
|
title: "dev",
|
|
1936
|
-
hint: "Live development with
|
|
1955
|
+
hint: "Live development with Tunnel",
|
|
1937
1956
|
footer: /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", gap: 1, children: [
|
|
1938
1957
|
bundleUrlUpdated && /* @__PURE__ */ jsx15(Text15, { color: "green", children: "bundleUrl updated \u2713" }),
|
|
1939
1958
|
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Press q or Ctrl-C to quit" })
|
|
@@ -1955,23 +1974,34 @@ var DevDashboard = ({
|
|
|
1955
1974
|
] })
|
|
1956
1975
|
] }),
|
|
1957
1976
|
/* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", gap: 1, children: [
|
|
1977
|
+
previewTunnelUrl && /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
|
|
1978
|
+
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Tunnel Dev - Preview:" }),
|
|
1979
|
+
/* @__PURE__ */ jsx15(Text15, { children: previewTunnelUrl })
|
|
1980
|
+
] }),
|
|
1958
1981
|
/* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
|
|
1959
|
-
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Preview:" }),
|
|
1982
|
+
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Local Dev - Preview:" }),
|
|
1960
1983
|
/* @__PURE__ */ jsxs15(Text15, { children: [
|
|
1961
1984
|
"http://localhost:",
|
|
1962
1985
|
previewPort
|
|
1963
1986
|
] })
|
|
1964
1987
|
] }),
|
|
1988
|
+
tunnelUrl && /* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
|
|
1989
|
+
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Tunnel Dev - Extension:" }),
|
|
1990
|
+
/* @__PURE__ */ jsx15(Text15, { children: tunnelUrl })
|
|
1991
|
+
] }),
|
|
1965
1992
|
/* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
|
|
1966
|
-
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Local:" }),
|
|
1993
|
+
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Local Dev - Extension:" }),
|
|
1967
1994
|
/* @__PURE__ */ jsxs15(Text15, { children: [
|
|
1968
1995
|
"http://localhost:",
|
|
1969
1996
|
extensionPort
|
|
1970
1997
|
] })
|
|
1971
1998
|
] }),
|
|
1972
1999
|
/* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
|
|
1973
|
-
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "
|
|
1974
|
-
/* @__PURE__ */
|
|
2000
|
+
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Bundle URL:" }),
|
|
2001
|
+
/* @__PURE__ */ jsxs15(Text15, { children: [
|
|
2002
|
+
tunnelUrl || `http://localhost:${extensionPort}`,
|
|
2003
|
+
bundleUrlUpdated ? " (updated \u2713)" : ""
|
|
2004
|
+
] })
|
|
1975
2005
|
] })
|
|
1976
2006
|
] })
|
|
1977
2007
|
]
|
|
@@ -1982,15 +2012,25 @@ var DevDashboard = ({
|
|
|
1982
2012
|
|
|
1983
2013
|
// src/components/DevApp.tsx
|
|
1984
2014
|
import { jsx as jsx16 } from "react/jsx-runtime";
|
|
2015
|
+
var isEphemeralUrl = (url) => {
|
|
2016
|
+
try {
|
|
2017
|
+
const { hostname } = new URL(url);
|
|
2018
|
+
return hostname.endsWith(".ngrok.io") || hostname.endsWith(".ngrok-free.app") || hostname.endsWith(".trycloudflare.com");
|
|
2019
|
+
} catch {
|
|
2020
|
+
return true;
|
|
2021
|
+
}
|
|
2022
|
+
};
|
|
1985
2023
|
var DevApp = ({ options = {} }) => {
|
|
1986
2024
|
const [state, setState] = useState10("setup");
|
|
1987
2025
|
const [devContext, setDevContext] = useState10(null);
|
|
1988
2026
|
const [resolvedContext, setResolvedContext] = useState10(null);
|
|
1989
2027
|
const [tunnelUrl, setTunnelUrl] = useState10(null);
|
|
2028
|
+
const [previewTunnelUrl, setPreviewTunnelUrl] = useState10(null);
|
|
1990
2029
|
const [bundleUrlUpdated, setBundleUrlUpdated] = useState10(false);
|
|
1991
2030
|
const [tunnelHandle, setTunnelHandle] = useState10(null);
|
|
2031
|
+
const [previewTunnelHandle, setPreviewTunnelHandle] = useState10(null);
|
|
1992
2032
|
const [devServerHandle, setDevServerHandle] = useState10(null);
|
|
1993
|
-
const
|
|
2033
|
+
const shuttingDown = useRef(false);
|
|
1994
2034
|
const useTunnel = options.tunnel !== false;
|
|
1995
2035
|
const useUpdate = options.update !== false;
|
|
1996
2036
|
const useRestore = options.restore !== false;
|
|
@@ -2013,19 +2053,49 @@ var DevApp = ({ options = {} }) => {
|
|
|
2013
2053
|
appName: resolved.appName || null
|
|
2014
2054
|
});
|
|
2015
2055
|
const extensionPort = options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort;
|
|
2056
|
+
const previewPort = options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort;
|
|
2016
2057
|
await patchViteAllowedHosts(devContext.projectRoot);
|
|
2058
|
+
let cachedOriginalUrl = devContext.originalBundleUrl;
|
|
2059
|
+
if (useUpdate) {
|
|
2060
|
+
try {
|
|
2061
|
+
console.log("[dev] Fetching current bundleUrl from API...");
|
|
2062
|
+
const extensions = await fetchExtensions(resolved.appId);
|
|
2063
|
+
const currentExtension = extensions[resolved.extensionId];
|
|
2064
|
+
if (currentExtension?.bundleUrl) {
|
|
2065
|
+
console.log(`[dev] Current bundleUrl: ${currentExtension.bundleUrl}`);
|
|
2066
|
+
if (!isEphemeralUrl(currentExtension.bundleUrl)) {
|
|
2067
|
+
console.log("[dev] Caching stable bundleUrl to .env.stackable");
|
|
2068
|
+
cachedOriginalUrl = currentExtension.bundleUrl;
|
|
2069
|
+
await writeDevContext(devContext.projectRoot, {
|
|
2070
|
+
...devContext,
|
|
2071
|
+
appId: resolved.appId,
|
|
2072
|
+
extensionId: resolved.extensionId,
|
|
2073
|
+
appName: resolved.appName || null,
|
|
2074
|
+
originalBundleUrl: cachedOriginalUrl
|
|
2075
|
+
});
|
|
2076
|
+
} else {
|
|
2077
|
+
console.log("[dev] Current bundleUrl is ephemeral, keeping previous cached value");
|
|
2078
|
+
}
|
|
2079
|
+
}
|
|
2080
|
+
} catch (err) {
|
|
2081
|
+
console.warn("[dev] Failed to fetch current bundleUrl:", err);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2017
2084
|
if (useTunnel) {
|
|
2018
2085
|
try {
|
|
2019
|
-
console.log(`[dev] Starting tunnel on port ${extensionPort}...`);
|
|
2086
|
+
console.log(`[dev] Starting extension tunnel on port ${extensionPort}...`);
|
|
2020
2087
|
const tunnelResult = await startTunnel(extensionPort);
|
|
2021
|
-
console.log(`[dev]
|
|
2088
|
+
console.log(`[dev] Extension tunnel ready: ${tunnelResult.url}`);
|
|
2022
2089
|
setTunnelHandle(tunnelResult);
|
|
2023
2090
|
setTunnelUrl(tunnelResult.url);
|
|
2091
|
+
console.log(`[dev] Starting preview tunnel on port ${previewPort}...`);
|
|
2092
|
+
const previewTunnelResult = await startTunnel(previewPort);
|
|
2093
|
+
console.log(`[dev] Preview tunnel ready: ${previewTunnelResult.url}`);
|
|
2094
|
+
setPreviewTunnelHandle(previewTunnelResult);
|
|
2095
|
+
setPreviewTunnelUrl(previewTunnelResult.url);
|
|
2024
2096
|
console.log(`[dev] Writing VITE_EXTENSION_BUNDLE_URL=${tunnelResult.url} to .env`);
|
|
2025
2097
|
await patchProjectEnv(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL", tunnelResult.url);
|
|
2026
2098
|
if (useUpdate) {
|
|
2027
|
-
const originalUrl = `http://localhost:${extensionPort}`;
|
|
2028
|
-
setOriginalBundleUrl(originalUrl);
|
|
2029
2099
|
console.log(`[dev] Updating bundleUrl to ${tunnelResult.url}`);
|
|
2030
2100
|
await updateExtension(resolved.appId, resolved.extensionId, {
|
|
2031
2101
|
bundleUrl: tunnelResult.url
|
|
@@ -2044,7 +2114,7 @@ var DevApp = ({ options = {} }) => {
|
|
|
2044
2114
|
setDevServerHandle(serverHandle);
|
|
2045
2115
|
console.log("[dev] Ready");
|
|
2046
2116
|
setState("running");
|
|
2047
|
-
}, [devContext, options.extensionPort, useTunnel, useUpdate]);
|
|
2117
|
+
}, [devContext, options.extensionPort, options.previewPort, useTunnel, useUpdate]);
|
|
2048
2118
|
useEffect6(() => {
|
|
2049
2119
|
if (state === "setup" && devContext && devContext.appId && devContext.extensionId) {
|
|
2050
2120
|
handleSetupReady({
|
|
@@ -2055,23 +2125,30 @@ var DevApp = ({ options = {} }) => {
|
|
|
2055
2125
|
}
|
|
2056
2126
|
}, [state, devContext, handleSetupReady]);
|
|
2057
2127
|
const handleQuit = async () => {
|
|
2058
|
-
if (!devContext || !resolvedContext) return;
|
|
2128
|
+
if (!devContext || !resolvedContext || shuttingDown.current) return;
|
|
2129
|
+
shuttingDown.current = true;
|
|
2059
2130
|
setState("stopping");
|
|
2060
2131
|
console.log("[dev] Shutting down...");
|
|
2061
2132
|
try {
|
|
2062
|
-
if (useRestore && originalBundleUrl) {
|
|
2063
|
-
console.log(`[dev] Restoring bundleUrl to ${originalBundleUrl}`);
|
|
2133
|
+
if (useRestore && devContext.originalBundleUrl) {
|
|
2134
|
+
console.log(`[dev] Restoring bundleUrl to ${devContext.originalBundleUrl}`);
|
|
2064
2135
|
await updateExtension(resolvedContext.appId, resolvedContext.extensionId, {
|
|
2065
|
-
bundleUrl: originalBundleUrl
|
|
2136
|
+
bundleUrl: devContext.originalBundleUrl
|
|
2066
2137
|
});
|
|
2067
2138
|
console.log("[dev] bundleUrl restored");
|
|
2139
|
+
} else if (useRestore && !devContext.originalBundleUrl) {
|
|
2140
|
+
console.warn("[dev] No cached original bundleUrl to restore");
|
|
2068
2141
|
}
|
|
2069
2142
|
console.log("[dev] Removing VITE_EXTENSION_BUNDLE_URL from .env");
|
|
2070
2143
|
await removeProjectEnvKey(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL");
|
|
2071
2144
|
if (tunnelHandle) {
|
|
2072
|
-
console.log("[dev] Stopping tunnel");
|
|
2145
|
+
console.log("[dev] Stopping Extension tunnel");
|
|
2073
2146
|
tunnelHandle.stop();
|
|
2074
2147
|
}
|
|
2148
|
+
if (previewTunnelHandle) {
|
|
2149
|
+
console.log("[dev] Stopping preview tunnel");
|
|
2150
|
+
previewTunnelHandle.stop();
|
|
2151
|
+
}
|
|
2075
2152
|
if (devServerHandle) {
|
|
2076
2153
|
console.log("[dev] Stopping dev server");
|
|
2077
2154
|
devServerHandle.stop();
|
|
@@ -2105,11 +2182,15 @@ var DevApp = ({ options = {} }) => {
|
|
|
2105
2182
|
extensionPort: options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort,
|
|
2106
2183
|
previewPort: options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort,
|
|
2107
2184
|
tunnelUrl,
|
|
2185
|
+
previewTunnelUrl,
|
|
2108
2186
|
bundleUrlUpdated,
|
|
2109
2187
|
onQuit: handleQuit
|
|
2110
2188
|
}
|
|
2111
2189
|
);
|
|
2112
2190
|
}
|
|
2191
|
+
if (state === "stopping") {
|
|
2192
|
+
return null;
|
|
2193
|
+
}
|
|
2113
2194
|
return /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsx16(Text16, { children: "Loading..." }) });
|
|
2114
2195
|
};
|
|
2115
2196
|
|
|
@@ -2117,17 +2198,17 @@ var DevApp = ({ options = {} }) => {
|
|
|
2117
2198
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2118
2199
|
var require2 = createRequire(import.meta.url);
|
|
2119
2200
|
var { version } = require2("../package.json");
|
|
2120
|
-
program.name("stackable-app-extension").description("Stackable
|
|
2121
|
-
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
|
|
2201
|
+
program.name("stackable-app-extension").description("Stackable Labs - App Extension developer CLI").version(version);
|
|
2202
|
+
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) => {
|
|
2122
2203
|
render(/* @__PURE__ */ jsx17(App, { command: "create" /* CREATE */, initialName: name, options }));
|
|
2123
2204
|
});
|
|
2124
|
-
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
|
|
2205
|
+
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) => {
|
|
2125
2206
|
render(/* @__PURE__ */ jsx17(App, { command: "scaffold" /* SCAFFOLD */, options }));
|
|
2126
2207
|
});
|
|
2127
|
-
program.command("update" /* UPDATE */).description("Update an existing Extension").argument("[extensionId]", "Extension ID to update").option("--app-id <id>", "Skip
|
|
2208
|
+
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) => {
|
|
2128
2209
|
render(/* @__PURE__ */ jsx17(App, { command: "update" /* UPDATE */, initialExtensionId: extensionId, options }));
|
|
2129
2210
|
});
|
|
2130
|
-
program.command("dev" /* DEV */).description("Start dev servers with a public tunnel").option("--dir <path>", "Project root (default: cwd)").option("--extension-port <port>", "Override
|
|
2131
|
-
render(/* @__PURE__ */ jsx17(DevApp, { options }));
|
|
2211
|
+
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) => {
|
|
2212
|
+
render(/* @__PURE__ */ jsx17(DevApp, { options }), { exitOnCtrlC: false });
|
|
2132
2213
|
});
|
|
2133
2214
|
program.parse(process.argv.filter((arg) => arg !== "--"));
|