@stackable-labs/cli-app-extension 1.18.0 → 1.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +287 -92
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -2,10 +2,8 @@
2
2
 
3
3
  // src/index.tsx
4
4
  import { createRequire } from "module";
5
- import { createServer } from "http";
6
5
  import { program } from "commander";
7
6
  import { render } from "ink";
8
- import open from "open";
9
7
 
10
8
  // src/App.tsx
11
9
  import { join as join3 } from "path";
@@ -860,25 +858,37 @@ var gradientColor = (row, col, rows, cols) => {
860
858
  const hi = Math.min(lo + 1, COLORS.length - 1);
861
859
  return lerp(COLORS[lo], COLORS[hi], idx - lo);
862
860
  };
863
- var Banner = () => {
861
+ var Banner = ({ userId, orgId } = {}) => {
864
862
  const termWidth = process.stdout.columns ?? 80;
865
863
  const maxLen = Math.max(...WORDMARK.map((l) => l.length));
866
864
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
867
865
  /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(termWidth) }),
868
- /* @__PURE__ */ jsx10(Box10, { flexDirection: "column", paddingX: 1, paddingY: 1, children: WORDMARK.map((line, row) => /* @__PURE__ */ jsx10(Box10, { children: line.split("").map((ch, col) => /* @__PURE__ */ jsx10(Text10, { bold: true, color: ch === " " ? void 0 : gradientColor(row, col, WORDMARK.length, maxLen), children: ch }, col)) }, row)) })
866
+ /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
867
+ WORDMARK.map((line, row) => /* @__PURE__ */ jsx10(Box10, { children: line.split("").map((ch, col) => /* @__PURE__ */ jsx10(Text10, { bold: true, color: ch === " " ? void 0 : gradientColor(row, col, WORDMARK.length, maxLen), children: ch }, col)) }, row)),
868
+ userId && orgId && /* @__PURE__ */ jsxs10(Box10, { gap: 2, marginTop: 1, children: [
869
+ /* @__PURE__ */ jsxs10(Box10, { gap: 1, children: [
870
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "User:" }),
871
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: userId })
872
+ ] }),
873
+ /* @__PURE__ */ jsxs10(Box10, { gap: 1, children: [
874
+ /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "Org:" }),
875
+ /* @__PURE__ */ jsx10(Text10, { color: "cyan", children: orgId })
876
+ ] })
877
+ ] })
878
+ ] })
869
879
  ] });
870
880
  };
871
881
 
872
882
  // src/components/AppSelect.tsx
873
883
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
874
- var AppSelect = ({ token, onSubmit }) => {
884
+ var AppSelect = ({ token, userId, orgId, onSubmit }) => {
875
885
  const [apps, setApps] = useState6([]);
876
886
  const [loading, setLoading] = useState6(true);
877
887
  const [error, setError] = useState6();
878
888
  const [cursor, setCursor] = useState6(0);
879
889
  useEffect2(() => {
880
890
  fetchApps(token).then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
881
- }, []);
891
+ }, [token]);
882
892
  useInput6((_, key) => {
883
893
  if (loading || error || apps.length === 0) return;
884
894
  if (key.upArrow) {
@@ -919,7 +929,7 @@ var AppSelect = ({ token, onSubmit }) => {
919
929
  }) });
920
930
  };
921
931
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
922
- /* @__PURE__ */ jsx11(Banner, {}),
932
+ /* @__PURE__ */ jsx11(Banner, { userId, orgId }),
923
933
  /* @__PURE__ */ jsx11(StepShell, { title: "Select the App you are building an Extension for:", children: renderContent() })
924
934
  ] });
925
935
  };
@@ -936,7 +946,7 @@ var ExtensionSelect = ({ appId, token, command, onSubmit, onBack }) => {
936
946
  const [cursor, setCursor] = useState7(0);
937
947
  useEffect3(() => {
938
948
  fetchExtensions(appId, token).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
939
- }, [appId]);
949
+ }, [appId, token]);
940
950
  useInput7((_, key) => {
941
951
  if (key.upArrow) {
942
952
  if (!loading && !error && extensions.length > 0) {
@@ -1511,7 +1521,7 @@ var derivePermissions2 = (targets) => {
1511
1521
  }
1512
1522
  return [...permissions];
1513
1523
  };
1514
- var App = ({ command, token, initialName, initialExtensionId, options }) => {
1524
+ var App = ({ command, token, userId, orgId, initialName, initialExtensionId, options }) => {
1515
1525
  const { exit } = useApp();
1516
1526
  const [step, setStep] = useState8("app");
1517
1527
  const [name, setName] = useState8(initialName ?? options?.name ?? "");
@@ -1716,7 +1726,7 @@ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1716
1726
  };
1717
1727
  switch (step) {
1718
1728
  case "app": {
1719
- return /* @__PURE__ */ jsx13(AppSelect, { token, onSubmit: handleAppSelect });
1729
+ return /* @__PURE__ */ jsx13(AppSelect, { token, userId, orgId, onSubmit: handleAppSelect });
1720
1730
  }
1721
1731
  case "extensionSelect": {
1722
1732
  return /* @__PURE__ */ jsx13(
@@ -1856,7 +1866,7 @@ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1856
1866
  };
1857
1867
 
1858
1868
  // src/components/DevApp.tsx
1859
- import { Box as Box16, Text as Text16 } from "ink";
1869
+ import { Box as Box16, Text as Text16, useInput as useInput9 } from "ink";
1860
1870
  import { useRef, useState as useState10, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1861
1871
 
1862
1872
  // src/lib/tunnel.ts
@@ -1937,6 +1947,8 @@ import { Box as Box15, Text as Text15, useInput as useInput8 } from "ink";
1937
1947
  import { useEffect as useEffect5 } from "react";
1938
1948
  import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1939
1949
  var DevDashboard = ({
1950
+ userId,
1951
+ orgId,
1940
1952
  extensionName,
1941
1953
  extensionId,
1942
1954
  appId,
@@ -1962,7 +1974,7 @@ var DevDashboard = ({
1962
1974
  }
1963
1975
  });
1964
1976
  return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
1965
- /* @__PURE__ */ jsx15(Banner, {}),
1977
+ /* @__PURE__ */ jsx15(Banner, { userId, orgId }),
1966
1978
  /* @__PURE__ */ jsxs15(
1967
1979
  StepShell,
1968
1980
  {
@@ -2030,7 +2042,7 @@ var DevDashboard = ({
2030
2042
 
2031
2043
  // src/components/DevApp.tsx
2032
2044
  import { jsx as jsx16 } from "react/jsx-runtime";
2033
- var DevApp = ({ token, options = {} }) => {
2045
+ var DevApp = ({ token, userId, orgId, options = {} }) => {
2034
2046
  const [state, setState] = useState10("setup");
2035
2047
  const [devContext, setDevContext] = useState10(null);
2036
2048
  const [resolvedContext, setResolvedContext] = useState10(null);
@@ -2123,6 +2135,15 @@ var DevApp = ({ token, options = {} }) => {
2123
2135
  console.log("[dev] Done");
2124
2136
  process.exit(0);
2125
2137
  };
2138
+ useInput9((input, key) => {
2139
+ if (input === "c" && key.ctrl) {
2140
+ if (state === "running") {
2141
+ handleQuit();
2142
+ } else {
2143
+ process.exit(0);
2144
+ }
2145
+ }
2146
+ });
2126
2147
  if (state === "setup" && devContext) {
2127
2148
  if (!devContext.appId || !devContext.extensionId) {
2128
2149
  return /* @__PURE__ */ jsx16(
@@ -2140,6 +2161,8 @@ var DevApp = ({ token, options = {} }) => {
2140
2161
  return /* @__PURE__ */ jsx16(
2141
2162
  DevDashboard,
2142
2163
  {
2164
+ userId,
2165
+ orgId,
2143
2166
  extensionName: devContext.extensionName,
2144
2167
  extensionId: resolvedContext.extensionId,
2145
2168
  appId: resolvedContext.appId,
@@ -2158,6 +2181,13 @@ var DevApp = ({ token, options = {} }) => {
2158
2181
  return /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsx16(Text16, { children: "Loading..." }) });
2159
2182
  };
2160
2183
 
2184
+ // src/components/AuthLogin.tsx
2185
+ import { createServer } from "http";
2186
+ import { Box as Box17, Text as Text17, useApp as useApp2 } from "ink";
2187
+ import Spinner4 from "ink-spinner";
2188
+ import open from "open";
2189
+ import { useState as useState11, useEffect as useEffect7 } from "react";
2190
+
2161
2191
  // src/lib/auth.ts
2162
2192
  import { readFile as readFile3, writeFile as writeFile3, mkdir, unlink } from "fs/promises";
2163
2193
  import { join as join4 } from "path";
@@ -2195,17 +2225,217 @@ var decodeJwtPayload = (token) => {
2195
2225
  var getToken = async () => {
2196
2226
  const state = await readAuthState();
2197
2227
  if (!state) {
2198
- throw new Error("Not authenticated. Run `stackable auth login` first.");
2228
+ throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
2199
2229
  }
2200
2230
  const payload = decodeJwtPayload(state.token);
2201
2231
  if (payload?.exp && typeof payload.exp === "number") {
2202
2232
  if (Date.now() >= payload.exp * 1e3) {
2203
- throw new Error("Session expired. Run `stackable auth login` to re-authenticate.");
2233
+ throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
2204
2234
  }
2205
2235
  }
2206
2236
  return state.token;
2207
2237
  };
2208
2238
 
2239
+ // src/components/AuthLogin.tsx
2240
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2241
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2242
+ var callbackPage = (heading, sub, redirectUrl) => `<!DOCTYPE html>
2243
+ <html><head><meta charset="utf-8"><title>Stackable CLI</title>
2244
+ <style>body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;background:#0a0a0a;color:#fafafa;font-family:system-ui,sans-serif}
2245
+ .card{text-align:center;padding:2rem}.card h2{margin:0 0 .5rem}.card p{color:#888;margin:0}.hint{margin-top:1rem;color:#555;font-size:.85rem}</style>
2246
+ </head><body><div class="card"><h2>${heading}</h2><p>${sub}</p><p class="hint" id="h"></p></div>
2247
+ ${redirectUrl ? `<script>(function(){var s=3,el=document.getElementById('h');function t(){if(s<=0){location.href='${redirectUrl}';return}el.textContent='Redirecting in '+s+'s\u2026';s--;setTimeout(t,1000)}t()})()</script>` : ""}
2248
+ </body></html>`;
2249
+ var AuthLogin = ({ dashboardUrl }) => {
2250
+ const { exit } = useApp2();
2251
+ const [state, setState] = useState11("waiting");
2252
+ const [loginUrl, setLoginUrl] = useState11("");
2253
+ const [userIdLabel, setUserIdLabel] = useState11("");
2254
+ const [orgIdLabel, setOrgIdLabel] = useState11("");
2255
+ const [errorMessage, setErrorMessage] = useState11("");
2256
+ useEffect7(() => {
2257
+ let server;
2258
+ let timeout;
2259
+ const run = async () => {
2260
+ let resolveToken;
2261
+ let rejectToken;
2262
+ const tokenPromise = new Promise((resolve, reject) => {
2263
+ resolveToken = resolve;
2264
+ rejectToken = reject;
2265
+ });
2266
+ server = createServer((req, res) => {
2267
+ const url2 = new URL(req.url, "http://localhost");
2268
+ if (url2.pathname === "/callback") {
2269
+ const error = url2.searchParams.get("error");
2270
+ if (error) {
2271
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2272
+ res.end(callbackPage("Authentication failed", "You can close this tab."));
2273
+ rejectToken(new Error(error));
2274
+ return;
2275
+ }
2276
+ const token2 = url2.searchParams.get("token");
2277
+ if (token2) {
2278
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2279
+ res.end(callbackPage("CLI authenticated", "You can return to your terminal.", dashboardUrl));
2280
+ resolveToken(token2);
2281
+ } else {
2282
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
2283
+ res.end("Missing token");
2284
+ }
2285
+ } else {
2286
+ res.writeHead(404);
2287
+ res.end();
2288
+ }
2289
+ });
2290
+ await new Promise((r) => server.listen(0, "127.0.0.1", r));
2291
+ const port = server.address().port;
2292
+ const callbackUrl = `http://localhost:${port}/callback`;
2293
+ const url = `${dashboardUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
2294
+ setLoginUrl(url);
2295
+ await open(url);
2296
+ timeout = setTimeout(() => {
2297
+ server.close();
2298
+ rejectToken(new Error("Login timed out. Please try again."));
2299
+ }, LOGIN_TIMEOUT_MS);
2300
+ let token;
2301
+ try {
2302
+ token = await tokenPromise;
2303
+ } catch (err) {
2304
+ clearTimeout(timeout);
2305
+ server.close();
2306
+ setErrorMessage(err instanceof Error ? err.message : String(err));
2307
+ setState("error");
2308
+ exit();
2309
+ return;
2310
+ }
2311
+ clearTimeout(timeout);
2312
+ server.close();
2313
+ const [, payloadB64] = token.split(".");
2314
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
2315
+ await writeAuthState({
2316
+ token,
2317
+ userId: payload.sub,
2318
+ orgId: payload.orgId
2319
+ });
2320
+ setUserIdLabel(payload.sub);
2321
+ setOrgIdLabel(payload.orgId);
2322
+ setState("success");
2323
+ exit();
2324
+ };
2325
+ run();
2326
+ return () => {
2327
+ clearTimeout(timeout);
2328
+ server?.close();
2329
+ };
2330
+ }, []);
2331
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
2332
+ /* @__PURE__ */ jsx17(Banner, {}),
2333
+ /* @__PURE__ */ jsxs16(StepShell, { title: "Authenticate with Stackable", children: [
2334
+ state === "waiting" && /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", gap: 1, children: [
2335
+ /* @__PURE__ */ jsxs16(Box17, { gap: 1, children: [
2336
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", children: /* @__PURE__ */ jsx17(Spinner4, { type: "dots" }) }),
2337
+ /* @__PURE__ */ jsx17(Text17, { children: "Waiting for browser authentication..." })
2338
+ ] }),
2339
+ loginUrl && /* @__PURE__ */ jsxs16(Text17, { dimColor: true, children: [
2340
+ " ",
2341
+ loginUrl
2342
+ ] })
2343
+ ] }),
2344
+ state === "success" && /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", gap: 1, children: [
2345
+ /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
2346
+ /* @__PURE__ */ jsxs16(Box17, { gap: 2, children: [
2347
+ /* @__PURE__ */ jsx17(Text17, { dimColor: true, children: "User:" }),
2348
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", children: userIdLabel })
2349
+ ] }),
2350
+ /* @__PURE__ */ jsxs16(Box17, { gap: 2, children: [
2351
+ /* @__PURE__ */ jsx17(Text17, { dimColor: true, children: "Org: " }),
2352
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", children: orgIdLabel })
2353
+ ] })
2354
+ ] }),
2355
+ /* @__PURE__ */ jsxs16(Box17, { gap: 1, children: [
2356
+ /* @__PURE__ */ jsx17(Text17, { color: "green", bold: true, children: "\u2714" }),
2357
+ /* @__PURE__ */ jsx17(Text17, { bold: true, children: "Authenticated" })
2358
+ ] })
2359
+ ] }),
2360
+ state === "error" && /* @__PURE__ */ jsxs16(Box17, { gap: 1, children: [
2361
+ /* @__PURE__ */ jsx17(Text17, { color: "red", children: "\u2716" }),
2362
+ /* @__PURE__ */ jsx17(Text17, { children: errorMessage })
2363
+ ] })
2364
+ ] })
2365
+ ] });
2366
+ };
2367
+
2368
+ // src/components/AuthLogout.tsx
2369
+ import { useEffect as useEffect8 } from "react";
2370
+ import { Box as Box18, Text as Text18, useApp as useApp3 } from "ink";
2371
+ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
2372
+ var AuthLogout = () => {
2373
+ const { exit } = useApp3();
2374
+ useEffect8(() => {
2375
+ exit();
2376
+ }, [exit]);
2377
+ return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", children: [
2378
+ /* @__PURE__ */ jsx18(Banner, {}),
2379
+ /* @__PURE__ */ jsx18(StepShell, { title: "Authenticate with Stackable", children: /* @__PURE__ */ jsxs17(Box18, { gap: 1, children: [
2380
+ /* @__PURE__ */ jsx18(Text18, { color: "green", bold: true, children: "\u2714" }),
2381
+ /* @__PURE__ */ jsx18(Text18, { bold: true, children: "Logged out" })
2382
+ ] }) })
2383
+ ] });
2384
+ };
2385
+
2386
+ // src/components/AuthStatus.tsx
2387
+ import { useEffect as useEffect9 } from "react";
2388
+ import { Box as Box19, Text as Text19, useApp as useApp4 } from "ink";
2389
+ import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
2390
+ var AuthStatus = ({ state, userId, orgId, expiry }) => {
2391
+ const { exit } = useApp4();
2392
+ useEffect9(() => {
2393
+ exit();
2394
+ }, [exit]);
2395
+ return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", children: [
2396
+ /* @__PURE__ */ jsx19(Banner, {}),
2397
+ /* @__PURE__ */ jsxs18(StepShell, { title: "Authenticate with Stackable", children: [
2398
+ state === "authenticated" && /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", gap: 1, children: [
2399
+ /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", children: [
2400
+ /* @__PURE__ */ jsxs18(Box19, { gap: 2, children: [
2401
+ /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "User:" }),
2402
+ /* @__PURE__ */ jsx19(Text19, { color: "cyan", children: userId })
2403
+ ] }),
2404
+ /* @__PURE__ */ jsxs18(Box19, { gap: 2, children: [
2405
+ /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "Org: " }),
2406
+ /* @__PURE__ */ jsx19(Text19, { color: "cyan", children: orgId })
2407
+ ] }),
2408
+ expiry && /* @__PURE__ */ jsxs18(Box19, { gap: 2, children: [
2409
+ /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "Exp: " }),
2410
+ /* @__PURE__ */ jsx19(Text19, { color: "cyan", children: expiry.toLocaleDateString() })
2411
+ ] })
2412
+ ] }),
2413
+ /* @__PURE__ */ jsxs18(Box19, { gap: 1, children: [
2414
+ /* @__PURE__ */ jsx19(Text19, { color: "green", bold: true, children: "\u2714" }),
2415
+ /* @__PURE__ */ jsx19(Text19, { bold: true, children: "Authenticated" })
2416
+ ] })
2417
+ ] }),
2418
+ state === "expired" && /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", gap: 1, children: [
2419
+ /* @__PURE__ */ jsxs18(Box19, { gap: 1, children: [
2420
+ /* @__PURE__ */ jsx19(Text19, { color: "red", children: "\u2716" }),
2421
+ /* @__PURE__ */ jsxs18(Text19, { children: [
2422
+ "Session expired",
2423
+ expiry ? ` (${expiry.toLocaleDateString()})` : ""
2424
+ ] })
2425
+ ] }),
2426
+ /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "Run `stackable-app-extension auth login` to re-authenticate." })
2427
+ ] }),
2428
+ state === "not-logged-in" && /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", gap: 1, children: [
2429
+ /* @__PURE__ */ jsxs18(Box19, { gap: 1, children: [
2430
+ /* @__PURE__ */ jsx19(Text19, { color: "red", children: "\u2716" }),
2431
+ /* @__PURE__ */ jsx19(Text19, { children: "Not logged in" })
2432
+ ] }),
2433
+ /* @__PURE__ */ jsx19(Text19, { dimColor: true, children: "Run `stackable-app-extension auth login`" })
2434
+ ] })
2435
+ ] })
2436
+ ] });
2437
+ };
2438
+
2209
2439
  // src/lib/versionCheck.ts
2210
2440
  import https from "https";
2211
2441
  var PACKAGE_NAME = "@stackable-labs/cli-app-extension";
@@ -2254,112 +2484,77 @@ var checkForUpdate = (currentVersion) => {
2254
2484
  };
2255
2485
 
2256
2486
  // src/index.tsx
2257
- import { jsx as jsx17 } from "react/jsx-runtime";
2487
+ import { jsx as jsx20 } from "react/jsx-runtime";
2258
2488
  var require2 = createRequire(import.meta.url);
2259
2489
  var { version } = require2("../package.json");
2260
2490
  checkForUpdate(version);
2491
+ var ensureToken = async () => {
2492
+ try {
2493
+ const token = await getToken();
2494
+ const state = await readAuthState();
2495
+ return { token, userId: state.userId, orgId: state.orgId };
2496
+ } catch (err) {
2497
+ const message = err instanceof Error ? err.message : String(err);
2498
+ const isExpired = message.toLowerCase().includes("expired");
2499
+ render(/* @__PURE__ */ jsx20(AuthStatus, { state: isExpired ? "expired" : "not-logged-in" }));
2500
+ return null;
2501
+ }
2502
+ };
2261
2503
  program.name("stackable-app-extension").description("Stackable Labs - App Extension developer CLI").version(version);
2262
2504
  program.command("create" /* CREATE */).description("Create a new Extension project").argument("[name]", "Extension project name").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) => {
2263
- const token = await getToken();
2264
- render(/* @__PURE__ */ jsx17(App, { command: "create" /* CREATE */, initialName: name, options, token }));
2505
+ const auth2 = await ensureToken();
2506
+ if (!auth2) {
2507
+ return;
2508
+ }
2509
+ const { token, userId, orgId } = auth2;
2510
+ render(/* @__PURE__ */ jsx20(App, { command: "create" /* CREATE */, initialName: name, options, token, userId, orgId }));
2265
2511
  });
2266
2512
  program.command("scaffold" /* SCAFFOLD */).description("Scaffold a local project from an existing Extension").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 (options) => {
2267
- const token = await getToken();
2268
- render(/* @__PURE__ */ jsx17(App, { command: "scaffold" /* SCAFFOLD */, options, token }));
2513
+ const auth2 = await ensureToken();
2514
+ if (!auth2) {
2515
+ return;
2516
+ }
2517
+ const { token, userId, orgId } = auth2;
2518
+ render(/* @__PURE__ */ jsx20(App, { command: "scaffold" /* SCAFFOLD */, options, token, userId, orgId }));
2269
2519
  });
2270
2520
  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(async (extensionId, options) => {
2271
- const token = await getToken();
2272
- render(/* @__PURE__ */ jsx17(App, { command: "update" /* UPDATE */, initialExtensionId: extensionId, options, token }));
2521
+ const auth2 = await ensureToken();
2522
+ if (!auth2) {
2523
+ return;
2524
+ }
2525
+ const { token, userId, orgId } = auth2;
2526
+ render(/* @__PURE__ */ jsx20(App, { command: "update" /* UPDATE */, initialExtensionId: extensionId, options, token, userId, orgId }));
2273
2527
  });
2274
2528
  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) => {
2275
- const token = await getToken();
2276
- render(/* @__PURE__ */ jsx17(DevApp, { options, token }), { exitOnCtrlC: false });
2529
+ const auth2 = await ensureToken();
2530
+ if (!auth2) {
2531
+ return;
2532
+ }
2533
+ const { token, userId, orgId } = auth2;
2534
+ render(/* @__PURE__ */ jsx20(DevApp, { options, token, userId, orgId }), { exitOnCtrlC: false });
2277
2535
  });
2278
2536
  var DASHBOARD_URL = process.env.ADMIN_DASHBOARD_URL ?? "https://admin.stackablelabs.io";
2279
2537
  var auth = program.command("auth").description("Manage CLI authentication");
2280
- var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2281
2538
  auth.command("login").description("Authenticate with Stackable via browser").action(async () => {
2282
- let resolveToken;
2283
- let rejectToken;
2284
- const tokenPromise = new Promise((resolve, reject) => {
2285
- resolveToken = resolve;
2286
- rejectToken = reject;
2287
- });
2288
- const server = createServer((req, res) => {
2289
- const url = new URL(req.url, "http://localhost");
2290
- if (url.pathname === "/callback") {
2291
- const error = url.searchParams.get("error");
2292
- if (error) {
2293
- res.writeHead(200, { "content-type": "text/html" });
2294
- res.end("<html><body><h2>Authentication failed \u2014 you can close this tab.</h2></body></html>");
2295
- rejectToken(new Error(error));
2296
- return;
2297
- }
2298
- const token2 = url.searchParams.get("token");
2299
- if (token2) {
2300
- res.writeHead(200, { "content-type": "text/html" });
2301
- res.end("<html><body><h2>CLI authenticated \u2014 you can close this tab.</h2></body></html>");
2302
- resolveToken(token2);
2303
- } else {
2304
- res.writeHead(400, { "content-type": "text/plain" });
2305
- res.end("Missing token");
2306
- }
2307
- } else {
2308
- res.writeHead(404);
2309
- res.end();
2310
- }
2311
- });
2312
- await new Promise((r) => server.listen(0, "127.0.0.1", r));
2313
- const port = server.address().port;
2314
- const callbackUrl = `http://localhost:${port}/callback`;
2315
- const loginUrl = `${DASHBOARD_URL}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
2316
- console.log(`
2317
- Opening browser to authenticate...
2318
- ${loginUrl}
2319
- `);
2320
- await open(loginUrl);
2321
- const timeout = setTimeout(() => {
2322
- server.close();
2323
- rejectToken(new Error("Login timed out. Please try again."));
2324
- }, LOGIN_TIMEOUT_MS);
2325
- let token;
2326
- try {
2327
- token = await tokenPromise;
2328
- } catch (err) {
2329
- clearTimeout(timeout);
2330
- server.close();
2331
- console.error(err instanceof Error ? err.message : String(err));
2332
- process.exit(1);
2333
- }
2334
- clearTimeout(timeout);
2335
- server.close();
2336
- const [, payloadB64] = token.split(".");
2337
- const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
2338
- await writeAuthState({
2339
- token,
2340
- userId: payload.sub,
2341
- orgId: payload.orgId,
2342
- orgSlug: payload.orgSlug
2343
- });
2344
- console.log(`Logged in \u2014 org: ${payload.orgSlug ?? payload.orgId}`);
2539
+ render(/* @__PURE__ */ jsx20(AuthLogin, { dashboardUrl: DASHBOARD_URL }));
2345
2540
  });
2346
2541
  auth.command("logout").description("Clear stored CLI credentials").action(async () => {
2347
2542
  await clearAuthState();
2348
- console.log("Logged out.");
2543
+ render(/* @__PURE__ */ jsx20(AuthLogout, {}));
2349
2544
  });
2350
2545
  auth.command("status").description("Show current authentication status").action(async () => {
2351
2546
  const state = await readAuthState();
2352
2547
  if (!state) {
2353
- console.log("Not logged in. Run `stackable auth login`.");
2548
+ render(/* @__PURE__ */ jsx20(AuthStatus, { state: "not-logged-in" }));
2354
2549
  return;
2355
2550
  }
2356
2551
  const [, payloadB64] = state.token.split(".");
2357
2552
  const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
2358
2553
  const expiry = payload.exp ? new Date(payload.exp * 1e3) : null;
2359
2554
  if (expiry && Date.now() >= expiry.getTime()) {
2360
- console.log(`Session expired (${expiry.toLocaleDateString()}). Run \`stackable auth login\` to re-authenticate.`);
2555
+ render(/* @__PURE__ */ jsx20(AuthStatus, { state: "expired", expiry }));
2361
2556
  return;
2362
2557
  }
2363
- console.log(`Logged in \u2014 org: ${state.orgSlug ?? state.orgId}${expiry ? `. Token expires: ${expiry.toLocaleDateString()}.` : ""}`);
2558
+ render(/* @__PURE__ */ jsx20(AuthStatus, { state: "authenticated", userId: state.userId, orgId: state.orgId, expiry }));
2364
2559
  });
2365
2560
  program.parse(process.argv.filter((arg) => arg !== "--"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackable-labs/cli-app-extension",
3
- "version": "1.18.0",
3
+ "version": "1.19.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {