@stackable-labs/cli-app-extension 2.21.1 → 2.23.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 +35 -7
- package/dist/index.js +247 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -167,28 +167,56 @@ The `dev` command:
|
|
|
167
167
|
|
|
168
168
|
### Host App Override
|
|
169
169
|
|
|
170
|
-
The dashboard displays
|
|
170
|
+
The dashboard displays one or two copyable query params you can append to your host app URL to override the extension's `bundleUrl` for that browser session only. The `@stackable-labs/embeddables` SDK detects the param and loads from your tunnel instead of the production bundle. No DB changes, no shared state — each developer gets isolated overrides.
|
|
171
|
+
|
|
172
|
+
The CLI emits one of two formats depending on whether the dev session token request succeeded:
|
|
173
|
+
|
|
174
|
+
**Blob format (default — when authenticated):** the second half of `<extensionId>:<value>` is a `base64url`-encoded JSON `{url, token}` blob. The token is short-lived, signed, and verified by the host. Required for `_stackable_staging` mode.
|
|
171
175
|
|
|
172
176
|
```
|
|
173
|
-
?_stackable_dev=ext-123
|
|
177
|
+
?_stackable_dev=ext-123:eyJ1cmwiOiJodHRwczovL2FiYy50cnljbG91ZGZsYXJlLmNvbSIsInRva2VuIjoiZXlKaGJHY2lPaUpJVXp...
|
|
174
178
|
```
|
|
175
179
|
|
|
176
|
-
|
|
180
|
+
The dashboard also surfaces the staging variant when applicable:
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
?_stackable_staging=ext-123:eyJ1cmwiOiJodHRwczovL2FiYy50cnljbG91ZGZsYXJlLmNvbSIsInRva2VuIjoiZXlKaGJHY2lPaUpJVXp...
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
The `_stackable_staging` variant signals the host to apply staging-environment-specific handling and **always requires the token** — there is no legacy fallback. If you don't see a Staging Param in the dashboard, the dev session token request failed; check `auth status` and re-run.
|
|
187
|
+
|
|
188
|
+
**Legacy format (fallback — when no token):** if `requestDevSessionToken` fails (auth misconfigured, network error, etc.), the dashboard falls back to a token-less `<extensionId>:<tunnelUrl>` form for `_stackable_dev` only. The host's parser accepts this format too but cannot apply staging-mode overrides without the token.
|
|
189
|
+
|
|
190
|
+
```
|
|
191
|
+
?_stackable_dev=ext-123:https://abc.trycloudflare.com
|
|
192
|
+
```
|
|
177
193
|
|
|
178
194
|
#### Multiple Extensions
|
|
179
195
|
|
|
180
|
-
To override multiple extensions at once, use either syntax
|
|
196
|
+
To override multiple extensions at once, use either syntax. Both blob entries and legacy entries can be mixed in the same comma-joined string — the parser tries each entry independently.
|
|
197
|
+
|
|
198
|
+
**Comma-separated (single param) — preferred:**
|
|
199
|
+
```
|
|
200
|
+
?_stackable_dev=ext-123:eyJ1cmwi...,ext-456:eyJ1cmwi...
|
|
201
|
+
```
|
|
181
202
|
|
|
182
|
-
|
|
203
|
+
Mixed blob + legacy in one string also works (e.g. when one extension authenticated and another didn't):
|
|
183
204
|
```
|
|
184
|
-
?_stackable_dev=ext-123:
|
|
205
|
+
?_stackable_dev=ext-123:eyJ1cmwi...,ext-456:https://def.trycloudflare.com
|
|
185
206
|
```
|
|
186
207
|
|
|
187
208
|
**Repeated params:**
|
|
188
209
|
```
|
|
189
|
-
?_stackable_dev=ext-123:
|
|
210
|
+
?_stackable_dev=ext-123:eyJ1cmwi...&_stackable_dev=ext-456:eyJ1cmwi...
|
|
190
211
|
```
|
|
191
212
|
|
|
213
|
+
The combined staging variant works the same way (blob-only, since staging requires the token):
|
|
214
|
+
```
|
|
215
|
+
?_stackable_staging=ext-123:eyJ1cmwi...,ext-456:eyJ1cmwi...
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The `stackable-extensions` repo's `pnpm preview:all` orchestrator (which spawns one `cli-app-extension dev --headless` per extension under `--only` / `--except` filters) emits both combined lines automatically in its summary footer — `?_stackable_dev=...` always, `?_stackable_staging=...` only when at least one session got a token.
|
|
219
|
+
|
|
192
220
|
> **Note:** The `dev` command never updates the `bundleUrl` in the database. It is purely local.
|
|
193
221
|
|
|
194
222
|
## Development Workflow
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from 'module';
|
|
|
3
3
|
import { program, InvalidArgumentError } from 'commander';
|
|
4
4
|
import { render, useApp, Box, Text, useInput, useFocus, useFocusManager } from 'ink';
|
|
5
5
|
import { SURFACE_TARGET, TEMPLATE_FLAVORS } from '@stackable-labs/sdk-extension-contracts';
|
|
6
|
-
import { unlink, readFile,
|
|
6
|
+
import { unlink, readFile, mkdir, writeFile, rm, readdir, rmdir } from 'fs/promises';
|
|
7
7
|
import { join, dirname, relative } from 'path';
|
|
8
8
|
import Spinner5 from 'ink-spinner';
|
|
9
9
|
import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
|
|
@@ -2947,6 +2947,157 @@ var startTunnel = async (port) => {
|
|
|
2947
2947
|
}
|
|
2948
2948
|
throw new Error(`Tunnel failed to return a valid URL after ${MAX_RETRIES} attempts`);
|
|
2949
2949
|
};
|
|
2950
|
+
|
|
2951
|
+
// src/lib/devParams.ts
|
|
2952
|
+
var toBase64Url = (input) => btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
2953
|
+
var buildDevParams = ({ extensionId, tunnelUrl, devSessionToken }) => {
|
|
2954
|
+
if (!tunnelUrl) {
|
|
2955
|
+
return {
|
|
2956
|
+
devEntry: null,
|
|
2957
|
+
devParam: null,
|
|
2958
|
+
stagingEntry: null,
|
|
2959
|
+
stagingParam: null,
|
|
2960
|
+
paramBlob: null
|
|
2961
|
+
};
|
|
2962
|
+
}
|
|
2963
|
+
if (devSessionToken) {
|
|
2964
|
+
const paramBlob = toBase64Url(JSON.stringify({ url: tunnelUrl, token: devSessionToken }));
|
|
2965
|
+
const extensionEntry = `${extensionId}:${paramBlob}`;
|
|
2966
|
+
return {
|
|
2967
|
+
paramBlob,
|
|
2968
|
+
devEntry: extensionEntry,
|
|
2969
|
+
stagingEntry: extensionEntry,
|
|
2970
|
+
devParam: `?_stackable_dev=${extensionEntry}`,
|
|
2971
|
+
stagingParam: `?_stackable_staging=${extensionEntry}`
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2974
|
+
const devEntry = `${extensionId}:${tunnelUrl}`;
|
|
2975
|
+
return {
|
|
2976
|
+
paramBlob: null,
|
|
2977
|
+
devEntry,
|
|
2978
|
+
stagingEntry: null,
|
|
2979
|
+
stagingParam: null,
|
|
2980
|
+
devParam: `?_stackable_dev=${devEntry}`
|
|
2981
|
+
};
|
|
2982
|
+
};
|
|
2983
|
+
|
|
2984
|
+
// src/lib/runDev.ts
|
|
2985
|
+
var runDev = async ({ token, devContext, options, resolved }) => {
|
|
2986
|
+
console.log(`[dev] Saving context: appId=${resolved.appId} extensionId=${resolved.extensionId}`);
|
|
2987
|
+
await writeDevContext(devContext.projectRoot, {
|
|
2988
|
+
...devContext,
|
|
2989
|
+
appId: resolved.appId,
|
|
2990
|
+
extensionId: resolved.extensionId,
|
|
2991
|
+
appName: resolved.appName || null
|
|
2992
|
+
});
|
|
2993
|
+
const manifestWarnings = [];
|
|
2994
|
+
if (devContext.manifest) {
|
|
2995
|
+
try {
|
|
2996
|
+
const extensions = await fetchExtensions(token, resolved.appId);
|
|
2997
|
+
const registryExt = extensions[resolved.extensionId];
|
|
2998
|
+
if (registryExt) {
|
|
2999
|
+
const localPerms = new Set(devContext.manifest.permissions);
|
|
3000
|
+
const registryPerms = new Set(registryExt.manifest.permissions);
|
|
3001
|
+
const missingPerms = [...localPerms].filter((p) => !registryPerms.has(p));
|
|
3002
|
+
const extraPerms = [...registryPerms].filter((p) => !localPerms.has(p));
|
|
3003
|
+
const localDomains = new Set(devContext.manifest.allowedDomains);
|
|
3004
|
+
const registryDomains = new Set(registryExt.manifest.allowedDomains);
|
|
3005
|
+
const missingDomains = [...localDomains].filter((d) => !registryDomains.has(d));
|
|
3006
|
+
const extraDomains = [...registryDomains].filter((d) => !localDomains.has(d));
|
|
3007
|
+
if (missingPerms.length > 0) {
|
|
3008
|
+
manifestWarnings.push(`Permissions in local but not registry: ${missingPerms.join(", ")}`);
|
|
3009
|
+
}
|
|
3010
|
+
if (extraPerms.length > 0) {
|
|
3011
|
+
manifestWarnings.push(`Permissions in registry but not local: ${extraPerms.join(", ")}`);
|
|
3012
|
+
}
|
|
3013
|
+
if (missingDomains.length > 0) {
|
|
3014
|
+
manifestWarnings.push(`Domains in local but not registry: ${missingDomains.join(", ")}`);
|
|
3015
|
+
}
|
|
3016
|
+
if (extraDomains.length > 0) {
|
|
3017
|
+
manifestWarnings.push(`Domains in registry but not local: ${extraDomains.join(", ")}`);
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
} catch {
|
|
3021
|
+
}
|
|
3022
|
+
}
|
|
3023
|
+
const useTunnel = options.tunnel !== false;
|
|
3024
|
+
const extensionPort = options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort;
|
|
3025
|
+
const previewPort = options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort;
|
|
3026
|
+
await patchViteAllowedHosts(devContext.projectRoot);
|
|
3027
|
+
let tunnelHandle = null;
|
|
3028
|
+
let previewTunnelHandle = null;
|
|
3029
|
+
let previewTunnelUrl = null;
|
|
3030
|
+
let tunnelUrl = null;
|
|
3031
|
+
if (useTunnel) {
|
|
3032
|
+
try {
|
|
3033
|
+
console.log(`[dev] Starting extension tunnel on port ${extensionPort}...`);
|
|
3034
|
+
const t = await startTunnel(extensionPort);
|
|
3035
|
+
console.log(`[dev] Extension tunnel ready: ${t.url}`);
|
|
3036
|
+
tunnelHandle = t;
|
|
3037
|
+
tunnelUrl = t.url;
|
|
3038
|
+
console.log(`[dev] Starting preview tunnel on port ${previewPort}...`);
|
|
3039
|
+
const pt = await startTunnel(previewPort);
|
|
3040
|
+
console.log(`[dev] Preview tunnel ready: ${pt.url}`);
|
|
3041
|
+
previewTunnelHandle = pt;
|
|
3042
|
+
previewTunnelUrl = pt.url;
|
|
3043
|
+
console.log(`[dev] Writing VITE_EXTENSION_BUNDLE_URL=${t.url} to .env`);
|
|
3044
|
+
await patchProjectEnv(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL", t.url);
|
|
3045
|
+
} catch (err) {
|
|
3046
|
+
console.error("[dev] Failed to start tunnel:", err);
|
|
3047
|
+
}
|
|
3048
|
+
} else {
|
|
3049
|
+
console.log("[dev] Tunnel disabled (--no-tunnel)");
|
|
3050
|
+
}
|
|
3051
|
+
let devSessionToken = null;
|
|
3052
|
+
try {
|
|
3053
|
+
console.log("[dev] Requesting dev session token...");
|
|
3054
|
+
devSessionToken = await requestDevSessionToken(token, resolved.appId, resolved.extensionId);
|
|
3055
|
+
console.log("[dev] Dev session token received");
|
|
3056
|
+
} catch (err) {
|
|
3057
|
+
console.warn("[dev] Failed to obtain dev session token \u2014 dev/staging mode URLs will not include auth token:", err);
|
|
3058
|
+
}
|
|
3059
|
+
console.log(`[dev] Starting dev server in ${devContext.projectRoot}`);
|
|
3060
|
+
const devServerHandle = startDevServer(devContext.projectRoot);
|
|
3061
|
+
const params = buildDevParams({
|
|
3062
|
+
extensionId: resolved.extensionId,
|
|
3063
|
+
tunnelUrl,
|
|
3064
|
+
devSessionToken
|
|
3065
|
+
});
|
|
3066
|
+
console.log("[dev] Ready");
|
|
3067
|
+
return {
|
|
3068
|
+
devContext,
|
|
3069
|
+
extensionPort,
|
|
3070
|
+
previewPort,
|
|
3071
|
+
tunnelUrl,
|
|
3072
|
+
previewTunnelUrl,
|
|
3073
|
+
tunnelHandle,
|
|
3074
|
+
previewTunnelHandle,
|
|
3075
|
+
devServerHandle,
|
|
3076
|
+
devSessionToken,
|
|
3077
|
+
manifestWarnings,
|
|
3078
|
+
params
|
|
3079
|
+
};
|
|
3080
|
+
};
|
|
3081
|
+
var cleanupDev = async (result) => {
|
|
3082
|
+
console.log("[dev] Shutting down...");
|
|
3083
|
+
try {
|
|
3084
|
+
console.log("[dev] Removing VITE_EXTENSION_BUNDLE_URL from .env");
|
|
3085
|
+
await removeProjectEnvKey(result.devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL");
|
|
3086
|
+
if (result.tunnelHandle) {
|
|
3087
|
+
console.log("[dev] Stopping Extension tunnel");
|
|
3088
|
+
result.tunnelHandle.stop();
|
|
3089
|
+
}
|
|
3090
|
+
if (result.previewTunnelHandle) {
|
|
3091
|
+
console.log("[dev] Stopping preview tunnel");
|
|
3092
|
+
result.previewTunnelHandle.stop();
|
|
3093
|
+
}
|
|
3094
|
+
console.log("[dev] Stopping dev server");
|
|
3095
|
+
result.devServerHandle.stop();
|
|
3096
|
+
} catch (err) {
|
|
3097
|
+
console.error("[dev] Error during cleanup:", err);
|
|
3098
|
+
}
|
|
3099
|
+
console.log("[dev] Done");
|
|
3100
|
+
};
|
|
2950
3101
|
var DevSetup = ({ initialContext, token, onReady }) => {
|
|
2951
3102
|
const [step, setStep] = useState("app");
|
|
2952
3103
|
const [selectedApp, setSelectedApp] = useState(null);
|
|
@@ -3043,7 +3194,6 @@ var CopyableField = ({
|
|
|
3043
3194
|
/* @__PURE__ */ jsx(Box, { paddingLeft: 2, children: expanded ? /* @__PURE__ */ jsx(Text, { wrap: "wrap", children: value }) : children || /* @__PURE__ */ jsx(Text, { children: labelColor ? /* @__PURE__ */ jsx(Text, { color: labelColor, children: truncated }) : truncated }) })
|
|
3044
3195
|
] });
|
|
3045
3196
|
};
|
|
3046
|
-
var toBase64Url = (input) => btoa(input).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
3047
3197
|
var DevDashboard = ({
|
|
3048
3198
|
previewTunnelUrl,
|
|
3049
3199
|
tunnelUrl,
|
|
@@ -3068,12 +3218,7 @@ var DevDashboard = ({
|
|
|
3068
3218
|
process.off("SIGINT", handler);
|
|
3069
3219
|
};
|
|
3070
3220
|
}, [onQuit]);
|
|
3071
|
-
const
|
|
3072
|
-
if (!tunnelUrl || !devSessionToken) {
|
|
3073
|
-
return "";
|
|
3074
|
-
}
|
|
3075
|
-
return toBase64Url(JSON.stringify({ url: tunnelUrl, token: devSessionToken }));
|
|
3076
|
-
}, [tunnelUrl, devSessionToken]);
|
|
3221
|
+
const params = useMemo(() => buildDevParams({ extensionId, tunnelUrl, devSessionToken: devSessionToken ?? null }), [extensionId, tunnelUrl, devSessionToken]);
|
|
3077
3222
|
useInput((input, key) => {
|
|
3078
3223
|
if (input === "q" || key.ctrl && input === "c") {
|
|
3079
3224
|
onQuit();
|
|
@@ -3087,7 +3232,7 @@ var DevDashboard = ({
|
|
|
3087
3232
|
title: "dev",
|
|
3088
3233
|
hint: `Live development ${tunnelUrl ? "with" : "w/o"} Tunnel \u2014 ${appName ? `${appName} ` : ""}(${appId})`,
|
|
3089
3234
|
footer: /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
|
|
3090
|
-
|
|
3235
|
+
params.paramBlob && /* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
3091
3236
|
"Press ",
|
|
3092
3237
|
/* @__PURE__ */ jsx(Text, { bold: true, color: "yellow", children: "[d]" }),
|
|
3093
3238
|
" to copy Dev Param, ",
|
|
@@ -3142,12 +3287,12 @@ var DevDashboard = ({
|
|
|
3142
3287
|
] })
|
|
3143
3288
|
] })
|
|
3144
3289
|
] }),
|
|
3145
|
-
|
|
3290
|
+
params.devParam && params.stagingParam && params.paramBlob && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
3146
3291
|
/* @__PURE__ */ jsx(
|
|
3147
3292
|
CopyableField,
|
|
3148
3293
|
{
|
|
3149
3294
|
label: "Dev Param (no encryption):",
|
|
3150
|
-
value:
|
|
3295
|
+
value: params.devParam,
|
|
3151
3296
|
shortcutKey: "d",
|
|
3152
3297
|
children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
3153
3298
|
"?",
|
|
@@ -3155,7 +3300,7 @@ var DevDashboard = ({
|
|
|
3155
3300
|
"=",
|
|
3156
3301
|
/* @__PURE__ */ jsx(Text, { color: "cyan", children: extensionId }),
|
|
3157
3302
|
":",
|
|
3158
|
-
|
|
3303
|
+
params.paramBlob.slice(0, 20),
|
|
3159
3304
|
"..."
|
|
3160
3305
|
] })
|
|
3161
3306
|
}
|
|
@@ -3164,7 +3309,7 @@ var DevDashboard = ({
|
|
|
3164
3309
|
CopyableField,
|
|
3165
3310
|
{
|
|
3166
3311
|
label: "Staging Param (with encryption):",
|
|
3167
|
-
value:
|
|
3312
|
+
value: params.stagingParam,
|
|
3168
3313
|
shortcutKey: "s",
|
|
3169
3314
|
children: /* @__PURE__ */ jsxs(Text, { children: [
|
|
3170
3315
|
"?",
|
|
@@ -3172,13 +3317,13 @@ var DevDashboard = ({
|
|
|
3172
3317
|
"=",
|
|
3173
3318
|
/* @__PURE__ */ jsx(Text, { color: "cyan", children: extensionId }),
|
|
3174
3319
|
":",
|
|
3175
|
-
|
|
3320
|
+
params.paramBlob.slice(0, 20),
|
|
3176
3321
|
"..."
|
|
3177
3322
|
] })
|
|
3178
3323
|
}
|
|
3179
3324
|
)
|
|
3180
3325
|
] }),
|
|
3181
|
-
|
|
3326
|
+
params.devParam && !params.paramBlob && /* @__PURE__ */ jsxs(Box, { gap: 2, children: [
|
|
3182
3327
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "Host Dev - Query Param:" }),
|
|
3183
3328
|
/* @__PURE__ */ jsxs(Text, { children: [
|
|
3184
3329
|
"?",
|
|
@@ -3206,15 +3351,8 @@ var DevApp = ({ token, userId, orgId, options = {} }) => {
|
|
|
3206
3351
|
const [state, setState] = useState("setup");
|
|
3207
3352
|
const [devContext, setDevContext] = useState(null);
|
|
3208
3353
|
const [resolvedContext, setResolvedContext] = useState(null);
|
|
3209
|
-
const [
|
|
3210
|
-
const [previewTunnelUrl, setPreviewTunnelUrl] = useState(null);
|
|
3211
|
-
const [tunnelHandle, setTunnelHandle] = useState(null);
|
|
3212
|
-
const [previewTunnelHandle, setPreviewTunnelHandle] = useState(null);
|
|
3213
|
-
const [devServerHandle, setDevServerHandle] = useState(null);
|
|
3214
|
-
const [manifestWarnings, setManifestWarnings] = useState([]);
|
|
3215
|
-
const [devSessionToken, setDevSessionToken] = useState(null);
|
|
3354
|
+
const [runResult, setRunResult] = useState(null);
|
|
3216
3355
|
const shuttingDown = useRef(false);
|
|
3217
|
-
const useTunnel = options.tunnel !== false;
|
|
3218
3356
|
useEffect(() => {
|
|
3219
3357
|
const projectRoot = options.dir || process.cwd();
|
|
3220
3358
|
console.log(`[dev] Reading context from ${projectRoot}`);
|
|
@@ -3228,83 +3366,10 @@ var DevApp = ({ token, userId, orgId, options = {} }) => {
|
|
|
3228
3366
|
return;
|
|
3229
3367
|
}
|
|
3230
3368
|
setResolvedContext(resolved);
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
...devContext,
|
|
3234
|
-
appId: resolved.appId,
|
|
3235
|
-
extensionId: resolved.extensionId,
|
|
3236
|
-
appName: resolved.appName || null
|
|
3237
|
-
});
|
|
3238
|
-
if (devContext.manifest) {
|
|
3239
|
-
try {
|
|
3240
|
-
const extensions = await fetchExtensions(token, resolved.appId);
|
|
3241
|
-
const registryExt = extensions[resolved.extensionId];
|
|
3242
|
-
if (registryExt) {
|
|
3243
|
-
const localPerms = new Set(devContext.manifest.permissions);
|
|
3244
|
-
const registryPerms = new Set(registryExt.manifest.permissions);
|
|
3245
|
-
const missingPerms = [...localPerms].filter((p) => !registryPerms.has(p));
|
|
3246
|
-
const extraPerms = [...registryPerms].filter((p) => !localPerms.has(p));
|
|
3247
|
-
const localDomains = new Set(devContext.manifest.allowedDomains);
|
|
3248
|
-
const registryDomains = new Set(registryExt.manifest.allowedDomains);
|
|
3249
|
-
const missingDomains = [...localDomains].filter((d) => !registryDomains.has(d));
|
|
3250
|
-
const extraDomains = [...registryDomains].filter((d) => !localDomains.has(d));
|
|
3251
|
-
const warnings = [];
|
|
3252
|
-
if (missingPerms.length > 0) {
|
|
3253
|
-
warnings.push(`Permissions in local but not registry: ${missingPerms.join(", ")}`);
|
|
3254
|
-
}
|
|
3255
|
-
if (extraPerms.length > 0) {
|
|
3256
|
-
warnings.push(`Permissions in registry but not local: ${extraPerms.join(", ")}`);
|
|
3257
|
-
}
|
|
3258
|
-
if (missingDomains.length > 0) {
|
|
3259
|
-
warnings.push(`Domains in local but not registry: ${missingDomains.join(", ")}`);
|
|
3260
|
-
}
|
|
3261
|
-
if (extraDomains.length > 0) {
|
|
3262
|
-
warnings.push(`Domains in registry but not local: ${extraDomains.join(", ")}`);
|
|
3263
|
-
}
|
|
3264
|
-
if (warnings.length > 0) {
|
|
3265
|
-
setManifestWarnings(warnings);
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
} catch {
|
|
3269
|
-
}
|
|
3270
|
-
}
|
|
3271
|
-
const extensionPort = options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort;
|
|
3272
|
-
const previewPort = options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort;
|
|
3273
|
-
await patchViteAllowedHosts(devContext.projectRoot);
|
|
3274
|
-
if (useTunnel) {
|
|
3275
|
-
try {
|
|
3276
|
-
console.log(`[dev] Starting extension tunnel on port ${extensionPort}...`);
|
|
3277
|
-
const tunnelResult = await startTunnel(extensionPort);
|
|
3278
|
-
console.log(`[dev] Extension tunnel ready: ${tunnelResult.url}`);
|
|
3279
|
-
setTunnelHandle(tunnelResult);
|
|
3280
|
-
setTunnelUrl(tunnelResult.url);
|
|
3281
|
-
console.log(`[dev] Starting preview tunnel on port ${previewPort}...`);
|
|
3282
|
-
const previewTunnelResult = await startTunnel(previewPort);
|
|
3283
|
-
console.log(`[dev] Preview tunnel ready: ${previewTunnelResult.url}`);
|
|
3284
|
-
setPreviewTunnelHandle(previewTunnelResult);
|
|
3285
|
-
setPreviewTunnelUrl(previewTunnelResult.url);
|
|
3286
|
-
console.log(`[dev] Writing VITE_EXTENSION_BUNDLE_URL=${tunnelResult.url} to .env`);
|
|
3287
|
-
await patchProjectEnv(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL", tunnelResult.url);
|
|
3288
|
-
} catch (err) {
|
|
3289
|
-
console.error("[dev] Failed to start tunnel:", err);
|
|
3290
|
-
}
|
|
3291
|
-
} else {
|
|
3292
|
-
console.log("[dev] Tunnel disabled (--no-tunnel)");
|
|
3293
|
-
}
|
|
3294
|
-
try {
|
|
3295
|
-
console.log("[dev] Requesting dev session token...");
|
|
3296
|
-
const sessionToken = await requestDevSessionToken(token, resolved.appId, resolved.extensionId);
|
|
3297
|
-
console.log("[dev] Dev session token received");
|
|
3298
|
-
setDevSessionToken(sessionToken);
|
|
3299
|
-
} catch (err) {
|
|
3300
|
-
console.warn("[dev] Failed to obtain dev session token \u2014 dev/staging mode URLs will not include auth token:", err);
|
|
3301
|
-
}
|
|
3302
|
-
console.log(`[dev] Starting dev server in ${devContext.projectRoot}`);
|
|
3303
|
-
const serverHandle = startDevServer(devContext.projectRoot);
|
|
3304
|
-
setDevServerHandle(serverHandle);
|
|
3305
|
-
console.log("[dev] Ready");
|
|
3369
|
+
const result = await runDev({ token, devContext, options, resolved });
|
|
3370
|
+
setRunResult(result);
|
|
3306
3371
|
setState("running");
|
|
3307
|
-
}, [devContext, token, options
|
|
3372
|
+
}, [devContext, token, options]);
|
|
3308
3373
|
useEffect(() => {
|
|
3309
3374
|
if (state === "setup" && devContext && devContext.appId && devContext.extensionId) {
|
|
3310
3375
|
handleSetupReady({
|
|
@@ -3315,31 +3380,12 @@ var DevApp = ({ token, userId, orgId, options = {} }) => {
|
|
|
3315
3380
|
}
|
|
3316
3381
|
}, [state, devContext, handleSetupReady]);
|
|
3317
3382
|
const handleQuit = async () => {
|
|
3318
|
-
if (!
|
|
3383
|
+
if (!runResult || shuttingDown.current) {
|
|
3319
3384
|
return;
|
|
3320
3385
|
}
|
|
3321
3386
|
shuttingDown.current = true;
|
|
3322
3387
|
setState("stopping");
|
|
3323
|
-
|
|
3324
|
-
try {
|
|
3325
|
-
console.log("[dev] Removing VITE_EXTENSION_BUNDLE_URL from .env");
|
|
3326
|
-
await removeProjectEnvKey(devContext.projectRoot, "VITE_EXTENSION_BUNDLE_URL");
|
|
3327
|
-
if (tunnelHandle) {
|
|
3328
|
-
console.log("[dev] Stopping Extension tunnel");
|
|
3329
|
-
tunnelHandle.stop();
|
|
3330
|
-
}
|
|
3331
|
-
if (previewTunnelHandle) {
|
|
3332
|
-
console.log("[dev] Stopping preview tunnel");
|
|
3333
|
-
previewTunnelHandle.stop();
|
|
3334
|
-
}
|
|
3335
|
-
if (devServerHandle) {
|
|
3336
|
-
console.log("[dev] Stopping dev server");
|
|
3337
|
-
devServerHandle.stop();
|
|
3338
|
-
}
|
|
3339
|
-
} catch (err) {
|
|
3340
|
-
console.error("[dev] Error during cleanup:", err);
|
|
3341
|
-
}
|
|
3342
|
-
console.log("[dev] Done");
|
|
3388
|
+
await cleanupDev(runResult);
|
|
3343
3389
|
process.exit(0);
|
|
3344
3390
|
};
|
|
3345
3391
|
useInput((input, key) => {
|
|
@@ -3364,22 +3410,22 @@ var DevApp = ({ token, userId, orgId, options = {} }) => {
|
|
|
3364
3410
|
}
|
|
3365
3411
|
return null;
|
|
3366
3412
|
}
|
|
3367
|
-
if (state === "running" && devContext && resolvedContext) {
|
|
3413
|
+
if (state === "running" && devContext && resolvedContext && runResult) {
|
|
3368
3414
|
return /* @__PURE__ */ jsx(
|
|
3369
3415
|
DevDashboard,
|
|
3370
3416
|
{
|
|
3371
|
-
previewTunnelUrl,
|
|
3372
|
-
tunnelUrl,
|
|
3417
|
+
previewTunnelUrl: runResult.previewTunnelUrl,
|
|
3418
|
+
tunnelUrl: runResult.tunnelUrl,
|
|
3373
3419
|
orgId,
|
|
3374
3420
|
userId,
|
|
3375
3421
|
appId: resolvedContext.appId,
|
|
3376
3422
|
appName: resolvedContext.appName,
|
|
3377
3423
|
extensionName: devContext.extensionName,
|
|
3378
3424
|
extensionId: resolvedContext.extensionId,
|
|
3379
|
-
extensionPort:
|
|
3380
|
-
previewPort:
|
|
3381
|
-
manifestWarnings,
|
|
3382
|
-
devSessionToken,
|
|
3425
|
+
extensionPort: runResult.extensionPort,
|
|
3426
|
+
previewPort: runResult.previewPort,
|
|
3427
|
+
manifestWarnings: runResult.manifestWarnings,
|
|
3428
|
+
devSessionToken: runResult.devSessionToken,
|
|
3383
3429
|
onQuit: handleQuit
|
|
3384
3430
|
}
|
|
3385
3431
|
);
|
|
@@ -3960,8 +4006,7 @@ var ensureToken = async () => {
|
|
|
3960
4006
|
} catch (err) {
|
|
3961
4007
|
const message = err instanceof Error ? err.message : String(err);
|
|
3962
4008
|
const isExpired = message.toLowerCase().includes("expired");
|
|
3963
|
-
|
|
3964
|
-
let flowError = null;
|
|
4009
|
+
const ref = { state: null, error: null };
|
|
3965
4010
|
const instance = render(
|
|
3966
4011
|
/* @__PURE__ */ jsx(
|
|
3967
4012
|
AuthLogin,
|
|
@@ -3970,25 +4015,86 @@ var ensureToken = async () => {
|
|
|
3970
4015
|
adminApiBaseUrl: getAdminApiBaseUrl(),
|
|
3971
4016
|
reason: isExpired ? AUTH_LOGIN_REASON.EXPIRED : AUTH_LOGIN_REASON.NOT_LOGGED_IN,
|
|
3972
4017
|
onSuccess: (authState) => {
|
|
3973
|
-
|
|
4018
|
+
ref.state = authState;
|
|
3974
4019
|
},
|
|
3975
4020
|
onError: (err2) => {
|
|
3976
|
-
|
|
4021
|
+
ref.error = err2;
|
|
3977
4022
|
}
|
|
3978
4023
|
}
|
|
3979
4024
|
)
|
|
3980
4025
|
);
|
|
3981
4026
|
await instance.waitUntilExit();
|
|
3982
|
-
if (
|
|
4027
|
+
if (ref.error || !ref.state) {
|
|
3983
4028
|
return null;
|
|
3984
4029
|
}
|
|
3985
4030
|
return {
|
|
3986
|
-
token:
|
|
3987
|
-
userId:
|
|
3988
|
-
orgId:
|
|
4031
|
+
token: ref.state.token,
|
|
4032
|
+
userId: ref.state.userId,
|
|
4033
|
+
orgId: ref.state.orgId
|
|
3989
4034
|
};
|
|
3990
4035
|
}
|
|
3991
4036
|
};
|
|
4037
|
+
var ensureTokenOrFail = async () => {
|
|
4038
|
+
try {
|
|
4039
|
+
const token = await getToken();
|
|
4040
|
+
const state = await readAuthState();
|
|
4041
|
+
if (!state) {
|
|
4042
|
+
throw new Error("not authenticated");
|
|
4043
|
+
}
|
|
4044
|
+
return { token, userId: state.userId, orgId: state.orgId };
|
|
4045
|
+
} catch (err) {
|
|
4046
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
4047
|
+
console.error(`[dev] Authentication required: ${message}`);
|
|
4048
|
+
console.error("[dev] Run `stackable-app-extension auth login` interactively first, then re-run with --headless.");
|
|
4049
|
+
process.exit(1);
|
|
4050
|
+
}
|
|
4051
|
+
};
|
|
4052
|
+
var runHeadlessDev = async (options) => {
|
|
4053
|
+
const auth2 = await ensureTokenOrFail();
|
|
4054
|
+
const projectRoot = options.dir || process.cwd();
|
|
4055
|
+
console.log(`[dev] Reading context from ${projectRoot}`);
|
|
4056
|
+
const devContext = await readDevContext(projectRoot);
|
|
4057
|
+
console.log(
|
|
4058
|
+
`[dev] Context loaded: extension="${devContext.extensionName}" appId=${devContext.appId ?? "(missing)"} extensionId=${devContext.extensionId ?? "(missing)"}`
|
|
4059
|
+
);
|
|
4060
|
+
if (!devContext.appId || !devContext.extensionId) {
|
|
4061
|
+
console.error("[dev] No appId/extensionId in .env.stackable.");
|
|
4062
|
+
console.error("[dev] Run `pnpm preview` interactively once to configure this extension, then re-run with --headless.");
|
|
4063
|
+
process.exit(1);
|
|
4064
|
+
}
|
|
4065
|
+
const result = await runDev({
|
|
4066
|
+
token: auth2.token,
|
|
4067
|
+
devContext,
|
|
4068
|
+
options,
|
|
4069
|
+
resolved: {
|
|
4070
|
+
appId: devContext.appId,
|
|
4071
|
+
extensionId: devContext.extensionId,
|
|
4072
|
+
appName: devContext.appName || void 0
|
|
4073
|
+
}
|
|
4074
|
+
});
|
|
4075
|
+
if (result.params.devEntry) {
|
|
4076
|
+
console.log(`[dev] dev-entry: ${result.params.devEntry}`);
|
|
4077
|
+
}
|
|
4078
|
+
if (result.params.devParam) {
|
|
4079
|
+
console.log(`[dev] dev-param: ${result.params.devParam}`);
|
|
4080
|
+
}
|
|
4081
|
+
if (result.params.stagingEntry) {
|
|
4082
|
+
console.log(`[dev] staging-entry: ${result.params.stagingEntry}`);
|
|
4083
|
+
}
|
|
4084
|
+
if (result.params.stagingParam) {
|
|
4085
|
+
console.log(`[dev] staging-param: ${result.params.stagingParam}`);
|
|
4086
|
+
}
|
|
4087
|
+
for (const warning of result.manifestWarnings) {
|
|
4088
|
+
console.warn(`[dev] manifest warning: ${warning}`);
|
|
4089
|
+
}
|
|
4090
|
+
console.log("[dev] Press Ctrl+C to stop");
|
|
4091
|
+
await new Promise((resolve) => {
|
|
4092
|
+
process.once("SIGTERM", () => resolve());
|
|
4093
|
+
process.once("SIGINT", () => resolve());
|
|
4094
|
+
});
|
|
4095
|
+
await cleanupDev(result);
|
|
4096
|
+
process.exit(0);
|
|
4097
|
+
};
|
|
3992
4098
|
program.name("stackable-app-extension").description("Stackable Labs - App Extension developer CLI").version(version);
|
|
3993
4099
|
program.command("create" /* CREATE */).description("Create a new Extension project").argument("[name]", "Extension project name").option("--app-id <id>", "Skip App selection").option("--project-id <id>", "Studio project ID (fetches files/manifest from Studio)").option("--template <flavor>", `Template flavor: ${TEMPLATE_FLAVORS.join(", ")}`, parseTemplateFlavor).option("--extension-port <port>", "Extension dev server port (default: 6543)").option("--preview-port <port>", "Preview dev server port").option("--skip-install", "Skip package manager install").option("--skip-git", "Skip git initialization").action(async (name, options) => {
|
|
3994
4100
|
const auth2 = await ensureToken();
|
|
@@ -4050,7 +4156,12 @@ program.command("update" /* UPDATE */).description("Update an existing Extension
|
|
|
4050
4156
|
)
|
|
4051
4157
|
);
|
|
4052
4158
|
});
|
|
4053
|
-
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").action(async (options) => {
|
|
4159
|
+
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("--headless", "Skip interactive dashboard (auto-enabled when stdin is not a TTY)").action(async (options) => {
|
|
4160
|
+
const headless = options.headless || !process.stdin.isTTY;
|
|
4161
|
+
if (headless) {
|
|
4162
|
+
await runHeadlessDev(options);
|
|
4163
|
+
return;
|
|
4164
|
+
}
|
|
4054
4165
|
const auth2 = await ensureToken();
|
|
4055
4166
|
if (!auth2) {
|
|
4056
4167
|
return;
|