@stackable-labs/cli-app-extension 1.7.2 → 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.
- package/README.md +3 -3
- package/dist/index.js +215 -140
- 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,
|
|
@@ -1956,22 +1975,30 @@ var DevDashboard = ({
|
|
|
1956
1975
|
] }),
|
|
1957
1976
|
/* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", gap: 1, children: [
|
|
1958
1977
|
/* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
|
|
1959
|
-
/* @__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:" }),
|
|
1960
1983
|
/* @__PURE__ */ jsxs15(Text15, { children: [
|
|
1961
1984
|
"http://localhost:",
|
|
1962
1985
|
previewPort
|
|
1963
1986
|
] })
|
|
1964
1987
|
] }),
|
|
1965
1988
|
/* @__PURE__ */ jsxs15(Box15, { gap: 2, children: [
|
|
1966
|
-
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "
|
|
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:" }),
|
|
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__ */ jsx15(Text15, { children: tunnelUrl
|
|
2000
|
+
/* @__PURE__ */ jsx15(Text15, { dimColor: true, children: "Bundle URL:" }),
|
|
2001
|
+
/* @__PURE__ */ jsx15(Text15, { children: bundleUrlUpdated ? `(${tunnelUrl}) updated \u2713` : "" })
|
|
1975
2002
|
] })
|
|
1976
2003
|
] })
|
|
1977
2004
|
]
|
|
@@ -1982,15 +2009,25 @@ var DevDashboard = ({
|
|
|
1982
2009
|
|
|
1983
2010
|
// src/components/DevApp.tsx
|
|
1984
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
|
+
};
|
|
1985
2020
|
var DevApp = ({ options = {} }) => {
|
|
1986
2021
|
const [state, setState] = useState10("setup");
|
|
1987
2022
|
const [devContext, setDevContext] = useState10(null);
|
|
1988
2023
|
const [resolvedContext, setResolvedContext] = useState10(null);
|
|
1989
2024
|
const [tunnelUrl, setTunnelUrl] = useState10(null);
|
|
2025
|
+
const [previewTunnelUrl, setPreviewTunnelUrl] = useState10(null);
|
|
1990
2026
|
const [bundleUrlUpdated, setBundleUrlUpdated] = useState10(false);
|
|
1991
2027
|
const [tunnelHandle, setTunnelHandle] = useState10(null);
|
|
2028
|
+
const [previewTunnelHandle, setPreviewTunnelHandle] = useState10(null);
|
|
1992
2029
|
const [devServerHandle, setDevServerHandle] = useState10(null);
|
|
1993
|
-
const
|
|
2030
|
+
const shuttingDown = useRef(false);
|
|
1994
2031
|
const useTunnel = options.tunnel !== false;
|
|
1995
2032
|
const useUpdate = options.update !== false;
|
|
1996
2033
|
const useRestore = options.restore !== false;
|
|
@@ -2013,19 +2050,49 @@ var DevApp = ({ options = {} }) => {
|
|
|
2013
2050
|
appName: resolved.appName || null
|
|
2014
2051
|
});
|
|
2015
2052
|
const extensionPort = options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort;
|
|
2053
|
+
const previewPort = options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort;
|
|
2016
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
|
+
}
|
|
2017
2081
|
if (useTunnel) {
|
|
2018
2082
|
try {
|
|
2019
|
-
console.log(`[dev] Starting tunnel on port ${extensionPort}...`);
|
|
2083
|
+
console.log(`[dev] Starting extension tunnel on port ${extensionPort}...`);
|
|
2020
2084
|
const tunnelResult = await startTunnel(extensionPort);
|
|
2021
|
-
console.log(`[dev]
|
|
2085
|
+
console.log(`[dev] Extension tunnel ready: ${tunnelResult.url}`);
|
|
2022
2086
|
setTunnelHandle(tunnelResult);
|
|
2023
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);
|
|
2024
2093
|
console.log(`[dev] Writing VITE_EXTENSION_BUNDLE_URL=${tunnelResult.url} to .env`);
|
|
2025
2094
|
await patchProjectEnv(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL", tunnelResult.url);
|
|
2026
2095
|
if (useUpdate) {
|
|
2027
|
-
const originalUrl = `http://localhost:${extensionPort}`;
|
|
2028
|
-
setOriginalBundleUrl(originalUrl);
|
|
2029
2096
|
console.log(`[dev] Updating bundleUrl to ${tunnelResult.url}`);
|
|
2030
2097
|
await updateExtension(resolved.appId, resolved.extensionId, {
|
|
2031
2098
|
bundleUrl: tunnelResult.url
|
|
@@ -2044,7 +2111,7 @@ var DevApp = ({ options = {} }) => {
|
|
|
2044
2111
|
setDevServerHandle(serverHandle);
|
|
2045
2112
|
console.log("[dev] Ready");
|
|
2046
2113
|
setState("running");
|
|
2047
|
-
}, [devContext, options.extensionPort, useTunnel, useUpdate]);
|
|
2114
|
+
}, [devContext, options.extensionPort, options.previewPort, useTunnel, useUpdate]);
|
|
2048
2115
|
useEffect6(() => {
|
|
2049
2116
|
if (state === "setup" && devContext && devContext.appId && devContext.extensionId) {
|
|
2050
2117
|
handleSetupReady({
|
|
@@ -2055,23 +2122,30 @@ var DevApp = ({ options = {} }) => {
|
|
|
2055
2122
|
}
|
|
2056
2123
|
}, [state, devContext, handleSetupReady]);
|
|
2057
2124
|
const handleQuit = async () => {
|
|
2058
|
-
if (!devContext || !resolvedContext) return;
|
|
2125
|
+
if (!devContext || !resolvedContext || shuttingDown.current) return;
|
|
2126
|
+
shuttingDown.current = true;
|
|
2059
2127
|
setState("stopping");
|
|
2060
2128
|
console.log("[dev] Shutting down...");
|
|
2061
2129
|
try {
|
|
2062
|
-
if (useRestore && originalBundleUrl) {
|
|
2063
|
-
console.log(`[dev] Restoring bundleUrl to ${originalBundleUrl}`);
|
|
2130
|
+
if (useRestore && devContext.originalBundleUrl) {
|
|
2131
|
+
console.log(`[dev] Restoring bundleUrl to ${devContext.originalBundleUrl}`);
|
|
2064
2132
|
await updateExtension(resolvedContext.appId, resolvedContext.extensionId, {
|
|
2065
|
-
bundleUrl: originalBundleUrl
|
|
2133
|
+
bundleUrl: devContext.originalBundleUrl
|
|
2066
2134
|
});
|
|
2067
2135
|
console.log("[dev] bundleUrl restored");
|
|
2136
|
+
} else if (useRestore && !devContext.originalBundleUrl) {
|
|
2137
|
+
console.warn("[dev] No cached original bundleUrl to restore");
|
|
2068
2138
|
}
|
|
2069
2139
|
console.log("[dev] Removing VITE_EXTENSION_BUNDLE_URL from .env");
|
|
2070
2140
|
await removeProjectEnvKey(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL");
|
|
2071
2141
|
if (tunnelHandle) {
|
|
2072
|
-
console.log("[dev] Stopping tunnel");
|
|
2142
|
+
console.log("[dev] Stopping Extension tunnel");
|
|
2073
2143
|
tunnelHandle.stop();
|
|
2074
2144
|
}
|
|
2145
|
+
if (previewTunnelHandle) {
|
|
2146
|
+
console.log("[dev] Stopping preview tunnel");
|
|
2147
|
+
previewTunnelHandle.stop();
|
|
2148
|
+
}
|
|
2075
2149
|
if (devServerHandle) {
|
|
2076
2150
|
console.log("[dev] Stopping dev server");
|
|
2077
2151
|
devServerHandle.stop();
|
|
@@ -2105,6 +2179,7 @@ var DevApp = ({ options = {} }) => {
|
|
|
2105
2179
|
extensionPort: options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort,
|
|
2106
2180
|
previewPort: options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort,
|
|
2107
2181
|
tunnelUrl,
|
|
2182
|
+
previewTunnelUrl,
|
|
2108
2183
|
bundleUrlUpdated,
|
|
2109
2184
|
onQuit: handleQuit
|
|
2110
2185
|
}
|
|
@@ -2117,17 +2192,17 @@ var DevApp = ({ options = {} }) => {
|
|
|
2117
2192
|
import { jsx as jsx17 } from "react/jsx-runtime";
|
|
2118
2193
|
var require2 = createRequire(import.meta.url);
|
|
2119
2194
|
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
|
|
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) => {
|
|
2122
2197
|
render(/* @__PURE__ */ jsx17(App, { command: "create" /* CREATE */, initialName: name, options }));
|
|
2123
2198
|
});
|
|
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
|
|
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) => {
|
|
2125
2200
|
render(/* @__PURE__ */ jsx17(App, { command: "scaffold" /* SCAFFOLD */, options }));
|
|
2126
2201
|
});
|
|
2127
|
-
program.command("update" /* UPDATE */).description("Update an existing Extension").argument("[extensionId]", "Extension ID to update").option("--app-id <id>", "Skip
|
|
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) => {
|
|
2128
2203
|
render(/* @__PURE__ */ jsx17(App, { command: "update" /* UPDATE */, initialExtensionId: extensionId, options }));
|
|
2129
2204
|
});
|
|
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
|
|
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) => {
|
|
2131
2206
|
render(/* @__PURE__ */ jsx17(DevApp, { options }));
|
|
2132
2207
|
});
|
|
2133
2208
|
program.parse(process.argv.filter((arg) => arg !== "--"));
|