@stackable-labs/cli-app-extension 1.17.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 +385 -35
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -752,9 +752,17 @@ import { useEffect as useEffect2, useState as useState6 } from "react";
752
752
  // src/lib/api.ts
753
753
  var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
754
754
  var getAdminApiBaseUrl = () => process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
755
- var fetchApps = async () => {
755
+ var CLI_CLIENT_NAME = "@stackable-labs/cli-app-extension";
756
+ var authHeaders = (token) => ({
757
+ authorization: `Bearer ${token}`,
758
+ "content-type": "application/json",
759
+ "x-client-name": CLI_CLIENT_NAME
760
+ });
761
+ var fetchApps = async (token) => {
756
762
  const baseUrl = getAdminApiBaseUrl();
757
- const res = await fetch(`${baseUrl}/app-extension`);
763
+ const res = await fetch(`${baseUrl}/app-extension`, {
764
+ headers: authHeaders(token)
765
+ });
758
766
  if (!res.ok) {
759
767
  throw new Error(`Failed to fetch apps${baseUrl !== DEFAULT_ADMIN_API_URL ? ` (from ${baseUrl})` : ""}: ${res.status} ${res.statusText}`);
760
768
  }
@@ -767,11 +775,11 @@ var fetchApps = async () => {
767
775
  iconUrl
768
776
  }));
769
777
  };
770
- var createExtensionRemote = async (appId, payload) => {
778
+ var createExtensionRemote = async (appId, payload, token) => {
771
779
  const baseUrl = getAdminApiBaseUrl();
772
780
  const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
773
781
  method: "POST",
774
- headers: { "content-type": "application/json" },
782
+ headers: authHeaders(token),
775
783
  body: JSON.stringify(payload)
776
784
  });
777
785
  if (!res.ok) {
@@ -780,9 +788,11 @@ var createExtensionRemote = async (appId, payload) => {
780
788
  }
781
789
  return res.json();
782
790
  };
783
- var fetchExtensions = async (appId) => {
791
+ var fetchExtensions = async (appId, token) => {
784
792
  const baseUrl = getAdminApiBaseUrl();
785
- const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`);
793
+ const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
794
+ headers: authHeaders(token)
795
+ });
786
796
  if (!res.ok) {
787
797
  throw new Error(`Failed to fetch Extensions: ${res.status} ${res.statusText}`);
788
798
  }
@@ -800,11 +810,11 @@ var fetchExtensions = async (appId) => {
800
810
  ])
801
811
  );
802
812
  };
803
- var updateExtension = async (appId, extensionId, payload) => {
813
+ var updateExtension = async (appId, extensionId, payload, token) => {
804
814
  const baseUrl = getAdminApiBaseUrl();
805
815
  const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions/${extensionId}`, {
806
816
  method: "PUT",
807
- headers: { "content-type": "application/json" },
817
+ headers: authHeaders(token),
808
818
  body: JSON.stringify(payload)
809
819
  });
810
820
  if (!res.ok) {
@@ -848,25 +858,37 @@ var gradientColor = (row, col, rows, cols) => {
848
858
  const hi = Math.min(lo + 1, COLORS.length - 1);
849
859
  return lerp(COLORS[lo], COLORS[hi], idx - lo);
850
860
  };
851
- var Banner = () => {
861
+ var Banner = ({ userId, orgId } = {}) => {
852
862
  const termWidth = process.stdout.columns ?? 80;
853
863
  const maxLen = Math.max(...WORDMARK.map((l) => l.length));
854
864
  return /* @__PURE__ */ jsxs10(Box10, { flexDirection: "column", children: [
855
865
  /* @__PURE__ */ jsx10(Text10, { dimColor: true, children: "\u2500".repeat(termWidth) }),
856
- /* @__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
+ ] })
857
879
  ] });
858
880
  };
859
881
 
860
882
  // src/components/AppSelect.tsx
861
883
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
862
- var AppSelect = ({ onSubmit }) => {
884
+ var AppSelect = ({ token, userId, orgId, onSubmit }) => {
863
885
  const [apps, setApps] = useState6([]);
864
886
  const [loading, setLoading] = useState6(true);
865
887
  const [error, setError] = useState6();
866
888
  const [cursor, setCursor] = useState6(0);
867
889
  useEffect2(() => {
868
- fetchApps().then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
869
- }, []);
890
+ fetchApps(token).then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
891
+ }, [token]);
870
892
  useInput6((_, key) => {
871
893
  if (loading || error || apps.length === 0) return;
872
894
  if (key.upArrow) {
@@ -907,7 +929,7 @@ var AppSelect = ({ onSubmit }) => {
907
929
  }) });
908
930
  };
909
931
  return /* @__PURE__ */ jsxs11(Box11, { flexDirection: "column", children: [
910
- /* @__PURE__ */ jsx11(Banner, {}),
932
+ /* @__PURE__ */ jsx11(Banner, { userId, orgId }),
911
933
  /* @__PURE__ */ jsx11(StepShell, { title: "Select the App you are building an Extension for:", children: renderContent() })
912
934
  ] });
913
935
  };
@@ -917,14 +939,14 @@ import { Box as Box12, Text as Text12, useInput as useInput7 } from "ink";
917
939
  import Spinner3 from "ink-spinner";
918
940
  import { useEffect as useEffect3, useState as useState7 } from "react";
919
941
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
920
- var ExtensionSelect = ({ appId, command, onSubmit, onBack }) => {
942
+ var ExtensionSelect = ({ appId, token, command, onSubmit, onBack }) => {
921
943
  const [extensions, setExtensions] = useState7([]);
922
944
  const [loading, setLoading] = useState7(true);
923
945
  const [error, setError] = useState7();
924
946
  const [cursor, setCursor] = useState7(0);
925
947
  useEffect3(() => {
926
- fetchExtensions(appId).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
927
- }, [appId]);
948
+ fetchExtensions(appId, token).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
949
+ }, [appId, token]);
928
950
  useInput7((_, key) => {
929
951
  if (key.upArrow) {
930
952
  if (!loading && !error && extensions.length > 0) {
@@ -1074,6 +1096,7 @@ var readDevContext = async (projectRoot) => {
1074
1096
  appId: stackableEnv.APP_ID || null,
1075
1097
  extensionId: stackableEnv.EXTENSION_ID || null,
1076
1098
  appName: stackableEnv.APP_NAME || null,
1099
+ orgId: stackableEnv.ORG_ID || null,
1077
1100
  extensionPort,
1078
1101
  previewPort
1079
1102
  };
@@ -1498,7 +1521,7 @@ var derivePermissions2 = (targets) => {
1498
1521
  }
1499
1522
  return [...permissions];
1500
1523
  };
1501
- var App = ({ command, initialName, initialExtensionId, options }) => {
1524
+ var App = ({ command, token, userId, orgId, initialName, initialExtensionId, options }) => {
1502
1525
  const { exit } = useApp();
1503
1526
  const [step, setStep] = useState8("app");
1504
1527
  const [name, setName] = useState8(initialName ?? options?.name ?? "");
@@ -1636,7 +1659,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1636
1659
  },
1637
1660
  bundleUrl: bundleUrl || void 0,
1638
1661
  enabled
1639
- });
1662
+ }, token);
1640
1663
  updateStep(0, "done");
1641
1664
  setExtensionVersion(resolvedVersion);
1642
1665
  setStep("updateDone");
@@ -1664,7 +1687,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1664
1687
  allowedDomains: []
1665
1688
  },
1666
1689
  bundleUrl: `http://localhost:${extensionPort}`
1667
- });
1690
+ }, token);
1668
1691
  resolvedExtensionId = created.id;
1669
1692
  setExtensionId(created.id);
1670
1693
  updateStep(0, "done");
@@ -1703,13 +1726,14 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1703
1726
  };
1704
1727
  switch (step) {
1705
1728
  case "app": {
1706
- return /* @__PURE__ */ jsx13(AppSelect, { onSubmit: handleAppSelect });
1729
+ return /* @__PURE__ */ jsx13(AppSelect, { token, userId, orgId, onSubmit: handleAppSelect });
1707
1730
  }
1708
1731
  case "extensionSelect": {
1709
1732
  return /* @__PURE__ */ jsx13(
1710
1733
  ExtensionSelect,
1711
1734
  {
1712
1735
  appId: selectedApp.id,
1736
+ token,
1713
1737
  command,
1714
1738
  onSubmit: handleExtensionSelect,
1715
1739
  onBack: goBack
@@ -1842,7 +1866,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1842
1866
  };
1843
1867
 
1844
1868
  // src/components/DevApp.tsx
1845
- import { Box as Box16, Text as Text16 } from "ink";
1869
+ import { Box as Box16, Text as Text16, useInput as useInput9 } from "ink";
1846
1870
  import { useRef, useState as useState10, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1847
1871
 
1848
1872
  // src/lib/tunnel.ts
@@ -1880,7 +1904,7 @@ var startDevServer = (projectRoot) => {
1880
1904
  import { Box as Box14, Text as Text14 } from "ink";
1881
1905
  import { useState as useState9, useEffect as useEffect4 } from "react";
1882
1906
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1883
- var DevSetup = ({ initialContext, onReady }) => {
1907
+ var DevSetup = ({ initialContext, token, onReady }) => {
1884
1908
  const [step, setStep] = useState9("app");
1885
1909
  const [selectedApp, setSelectedApp] = useState9(null);
1886
1910
  useEffect4(() => {
@@ -1902,7 +1926,7 @@ var DevSetup = ({ initialContext, onReady }) => {
1902
1926
  if (step === "app") {
1903
1927
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
1904
1928
  /* @__PURE__ */ jsx14(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { children: "Select the App for your extension:" }) }),
1905
- /* @__PURE__ */ jsx14(AppSelect, { onSubmit: handleAppSelect })
1929
+ /* @__PURE__ */ jsx14(AppSelect, { token, onSubmit: handleAppSelect })
1906
1930
  ] });
1907
1931
  }
1908
1932
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
@@ -1911,6 +1935,7 @@ var DevSetup = ({ initialContext, onReady }) => {
1911
1935
  ExtensionSelect,
1912
1936
  {
1913
1937
  appId: selectedApp?.id || initialContext.appId,
1938
+ token,
1914
1939
  onSubmit: handleExtensionSelect
1915
1940
  }
1916
1941
  )
@@ -1922,6 +1947,8 @@ import { Box as Box15, Text as Text15, useInput as useInput8 } from "ink";
1922
1947
  import { useEffect as useEffect5 } from "react";
1923
1948
  import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1924
1949
  var DevDashboard = ({
1950
+ userId,
1951
+ orgId,
1925
1952
  extensionName,
1926
1953
  extensionId,
1927
1954
  appId,
@@ -1947,7 +1974,7 @@ var DevDashboard = ({
1947
1974
  }
1948
1975
  });
1949
1976
  return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
1950
- /* @__PURE__ */ jsx15(Banner, {}),
1977
+ /* @__PURE__ */ jsx15(Banner, { userId, orgId }),
1951
1978
  /* @__PURE__ */ jsxs15(
1952
1979
  StepShell,
1953
1980
  {
@@ -2015,7 +2042,7 @@ var DevDashboard = ({
2015
2042
 
2016
2043
  // src/components/DevApp.tsx
2017
2044
  import { jsx as jsx16 } from "react/jsx-runtime";
2018
- var DevApp = ({ options = {} }) => {
2045
+ var DevApp = ({ token, userId, orgId, options = {} }) => {
2019
2046
  const [state, setState] = useState10("setup");
2020
2047
  const [devContext, setDevContext] = useState10(null);
2021
2048
  const [resolvedContext, setResolvedContext] = useState10(null);
@@ -2108,12 +2135,22 @@ var DevApp = ({ options = {} }) => {
2108
2135
  console.log("[dev] Done");
2109
2136
  process.exit(0);
2110
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
+ });
2111
2147
  if (state === "setup" && devContext) {
2112
2148
  if (!devContext.appId || !devContext.extensionId) {
2113
2149
  return /* @__PURE__ */ jsx16(
2114
2150
  DevSetup,
2115
2151
  {
2116
2152
  initialContext: devContext,
2153
+ token,
2117
2154
  onReady: handleSetupReady
2118
2155
  }
2119
2156
  );
@@ -2124,6 +2161,8 @@ var DevApp = ({ options = {} }) => {
2124
2161
  return /* @__PURE__ */ jsx16(
2125
2162
  DevDashboard,
2126
2163
  {
2164
+ userId,
2165
+ orgId,
2127
2166
  extensionName: devContext.extensionName,
2128
2167
  extensionId: resolvedContext.extensionId,
2129
2168
  appId: resolvedContext.appId,
@@ -2142,6 +2181,261 @@ var DevApp = ({ options = {} }) => {
2142
2181
  return /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsx16(Text16, { children: "Loading..." }) });
2143
2182
  };
2144
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
+
2191
+ // src/lib/auth.ts
2192
+ import { readFile as readFile3, writeFile as writeFile3, mkdir, unlink } from "fs/promises";
2193
+ import { join as join4 } from "path";
2194
+ import { homedir } from "os";
2195
+ var AUTH_DIR = join4(homedir(), ".stackable");
2196
+ var AUTH_FILE = join4(AUTH_DIR, "auth.json");
2197
+ var readAuthState = async () => {
2198
+ try {
2199
+ const content = await readFile3(AUTH_FILE, "utf8");
2200
+ return JSON.parse(content);
2201
+ } catch {
2202
+ return null;
2203
+ }
2204
+ };
2205
+ var writeAuthState = async (state) => {
2206
+ await mkdir(AUTH_DIR, { recursive: true, mode: 448 });
2207
+ await writeFile3(AUTH_FILE, JSON.stringify(state, null, 2), { mode: 384 });
2208
+ };
2209
+ var clearAuthState = async () => {
2210
+ try {
2211
+ await unlink(AUTH_FILE);
2212
+ } catch {
2213
+ }
2214
+ };
2215
+ var decodeJwtPayload = (token) => {
2216
+ try {
2217
+ const [, payload] = token.split(".");
2218
+ if (!payload) return null;
2219
+ const json = Buffer.from(payload, "base64url").toString("utf8");
2220
+ return JSON.parse(json);
2221
+ } catch {
2222
+ return null;
2223
+ }
2224
+ };
2225
+ var getToken = async () => {
2226
+ const state = await readAuthState();
2227
+ if (!state) {
2228
+ throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
2229
+ }
2230
+ const payload = decodeJwtPayload(state.token);
2231
+ if (payload?.exp && typeof payload.exp === "number") {
2232
+ if (Date.now() >= payload.exp * 1e3) {
2233
+ throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
2234
+ }
2235
+ }
2236
+ return state.token;
2237
+ };
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
+
2145
2439
  // src/lib/versionCheck.ts
2146
2440
  import https from "https";
2147
2441
  var PACKAGE_NAME = "@stackable-labs/cli-app-extension";
@@ -2190,21 +2484,77 @@ var checkForUpdate = (currentVersion) => {
2190
2484
  };
2191
2485
 
2192
2486
  // src/index.tsx
2193
- import { jsx as jsx17 } from "react/jsx-runtime";
2487
+ import { jsx as jsx20 } from "react/jsx-runtime";
2194
2488
  var require2 = createRequire(import.meta.url);
2195
2489
  var { version } = require2("../package.json");
2196
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
+ };
2197
2503
  program.name("stackable-app-extension").description("Stackable Labs - App Extension developer CLI").version(version);
2198
- 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((name, options) => {
2199
- render(/* @__PURE__ */ jsx17(App, { command: "create" /* CREATE */, initialName: name, options }));
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) => {
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 }));
2200
2511
  });
2201
- 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((options) => {
2202
- render(/* @__PURE__ */ jsx17(App, { command: "scaffold" /* SCAFFOLD */, options }));
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) => {
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 }));
2519
+ });
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) => {
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 }));
2527
+ });
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) => {
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 });
2535
+ });
2536
+ var DASHBOARD_URL = process.env.ADMIN_DASHBOARD_URL ?? "https://admin.stackablelabs.io";
2537
+ var auth = program.command("auth").description("Manage CLI authentication");
2538
+ auth.command("login").description("Authenticate with Stackable via browser").action(async () => {
2539
+ render(/* @__PURE__ */ jsx20(AuthLogin, { dashboardUrl: DASHBOARD_URL }));
2203
2540
  });
2204
- 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) => {
2205
- render(/* @__PURE__ */ jsx17(App, { command: "update" /* UPDATE */, initialExtensionId: extensionId, options }));
2541
+ auth.command("logout").description("Clear stored CLI credentials").action(async () => {
2542
+ await clearAuthState();
2543
+ render(/* @__PURE__ */ jsx20(AuthLogout, {}));
2206
2544
  });
2207
- 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((options) => {
2208
- render(/* @__PURE__ */ jsx17(DevApp, { options }), { exitOnCtrlC: false });
2545
+ auth.command("status").description("Show current authentication status").action(async () => {
2546
+ const state = await readAuthState();
2547
+ if (!state) {
2548
+ render(/* @__PURE__ */ jsx20(AuthStatus, { state: "not-logged-in" }));
2549
+ return;
2550
+ }
2551
+ const [, payloadB64] = state.token.split(".");
2552
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
2553
+ const expiry = payload.exp ? new Date(payload.exp * 1e3) : null;
2554
+ if (expiry && Date.now() >= expiry.getTime()) {
2555
+ render(/* @__PURE__ */ jsx20(AuthStatus, { state: "expired", expiry }));
2556
+ return;
2557
+ }
2558
+ render(/* @__PURE__ */ jsx20(AuthStatus, { state: "authenticated", userId: state.userId, orgId: state.orgId, expiry }));
2209
2559
  });
2210
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.17.0",
3
+ "version": "1.19.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "ink-spinner": "5.x",
20
20
  "ink-text-input": "6.x",
21
21
  "nypm": "0.4.x",
22
+ "open": "10.x",
22
23
  "react": "18.x"
23
24
  },
24
25
  "description": "CLI for scaffolding Stackable extension projects.",