@stackable-labs/cli-app-extension 1.16.0 → 1.18.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 +182 -27
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -2,8 +2,10 @@
2
2
 
3
3
  // src/index.tsx
4
4
  import { createRequire } from "module";
5
+ import { createServer } from "http";
5
6
  import { program } from "commander";
6
7
  import { render } from "ink";
8
+ import open from "open";
7
9
 
8
10
  // src/App.tsx
9
11
  import { join as join3 } from "path";
@@ -752,9 +754,17 @@ import { useEffect as useEffect2, useState as useState6 } from "react";
752
754
  // src/lib/api.ts
753
755
  var DEFAULT_ADMIN_API_URL = "https://api-use1.stackablelabs.io/admin";
754
756
  var getAdminApiBaseUrl = () => process.env.ADMIN_API_BASE_URL ?? DEFAULT_ADMIN_API_URL;
755
- var fetchApps = async () => {
757
+ var CLI_CLIENT_NAME = "@stackable-labs/cli-app-extension";
758
+ var authHeaders = (token) => ({
759
+ authorization: `Bearer ${token}`,
760
+ "content-type": "application/json",
761
+ "x-client-name": CLI_CLIENT_NAME
762
+ });
763
+ var fetchApps = async (token) => {
756
764
  const baseUrl = getAdminApiBaseUrl();
757
- const res = await fetch(`${baseUrl}/app-extension`);
765
+ const res = await fetch(`${baseUrl}/app-extension`, {
766
+ headers: authHeaders(token)
767
+ });
758
768
  if (!res.ok) {
759
769
  throw new Error(`Failed to fetch apps${baseUrl !== DEFAULT_ADMIN_API_URL ? ` (from ${baseUrl})` : ""}: ${res.status} ${res.statusText}`);
760
770
  }
@@ -767,11 +777,11 @@ var fetchApps = async () => {
767
777
  iconUrl
768
778
  }));
769
779
  };
770
- var createExtensionRemote = async (appId, payload) => {
780
+ var createExtensionRemote = async (appId, payload, token) => {
771
781
  const baseUrl = getAdminApiBaseUrl();
772
782
  const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
773
783
  method: "POST",
774
- headers: { "content-type": "application/json" },
784
+ headers: authHeaders(token),
775
785
  body: JSON.stringify(payload)
776
786
  });
777
787
  if (!res.ok) {
@@ -780,9 +790,11 @@ var createExtensionRemote = async (appId, payload) => {
780
790
  }
781
791
  return res.json();
782
792
  };
783
- var fetchExtensions = async (appId) => {
793
+ var fetchExtensions = async (appId, token) => {
784
794
  const baseUrl = getAdminApiBaseUrl();
785
- const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`);
795
+ const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
796
+ headers: authHeaders(token)
797
+ });
786
798
  if (!res.ok) {
787
799
  throw new Error(`Failed to fetch Extensions: ${res.status} ${res.statusText}`);
788
800
  }
@@ -800,11 +812,11 @@ var fetchExtensions = async (appId) => {
800
812
  ])
801
813
  );
802
814
  };
803
- var updateExtension = async (appId, extensionId, payload) => {
815
+ var updateExtension = async (appId, extensionId, payload, token) => {
804
816
  const baseUrl = getAdminApiBaseUrl();
805
817
  const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions/${extensionId}`, {
806
818
  method: "PUT",
807
- headers: { "content-type": "application/json" },
819
+ headers: authHeaders(token),
808
820
  body: JSON.stringify(payload)
809
821
  });
810
822
  if (!res.ok) {
@@ -859,13 +871,13 @@ var Banner = () => {
859
871
 
860
872
  // src/components/AppSelect.tsx
861
873
  import { jsx as jsx11, jsxs as jsxs11 } from "react/jsx-runtime";
862
- var AppSelect = ({ onSubmit }) => {
874
+ var AppSelect = ({ token, onSubmit }) => {
863
875
  const [apps, setApps] = useState6([]);
864
876
  const [loading, setLoading] = useState6(true);
865
877
  const [error, setError] = useState6();
866
878
  const [cursor, setCursor] = useState6(0);
867
879
  useEffect2(() => {
868
- fetchApps().then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
880
+ fetchApps(token).then(setApps).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
869
881
  }, []);
870
882
  useInput6((_, key) => {
871
883
  if (loading || error || apps.length === 0) return;
@@ -917,13 +929,13 @@ import { Box as Box12, Text as Text12, useInput as useInput7 } from "ink";
917
929
  import Spinner3 from "ink-spinner";
918
930
  import { useEffect as useEffect3, useState as useState7 } from "react";
919
931
  import { jsx as jsx12, jsxs as jsxs12 } from "react/jsx-runtime";
920
- var ExtensionSelect = ({ appId, command, onSubmit, onBack }) => {
932
+ var ExtensionSelect = ({ appId, token, command, onSubmit, onBack }) => {
921
933
  const [extensions, setExtensions] = useState7([]);
922
934
  const [loading, setLoading] = useState7(true);
923
935
  const [error, setError] = useState7();
924
936
  const [cursor, setCursor] = useState7(0);
925
937
  useEffect3(() => {
926
- fetchExtensions(appId).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
938
+ fetchExtensions(appId, token).then((byId) => setExtensions(Object.values(byId))).catch((err) => setError(err instanceof Error ? err.message : String(err))).finally(() => setLoading(false));
927
939
  }, [appId]);
928
940
  useInput7((_, key) => {
929
941
  if (key.upArrow) {
@@ -1074,6 +1086,7 @@ var readDevContext = async (projectRoot) => {
1074
1086
  appId: stackableEnv.APP_ID || null,
1075
1087
  extensionId: stackableEnv.EXTENSION_ID || null,
1076
1088
  appName: stackableEnv.APP_NAME || null,
1089
+ orgId: stackableEnv.ORG_ID || null,
1077
1090
  extensionPort,
1078
1091
  previewPort
1079
1092
  };
@@ -1498,7 +1511,7 @@ var derivePermissions2 = (targets) => {
1498
1511
  }
1499
1512
  return [...permissions];
1500
1513
  };
1501
- var App = ({ command, initialName, initialExtensionId, options }) => {
1514
+ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1502
1515
  const { exit } = useApp();
1503
1516
  const [step, setStep] = useState8("app");
1504
1517
  const [name, setName] = useState8(initialName ?? options?.name ?? "");
@@ -1636,7 +1649,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1636
1649
  },
1637
1650
  bundleUrl: bundleUrl || void 0,
1638
1651
  enabled
1639
- });
1652
+ }, token);
1640
1653
  updateStep(0, "done");
1641
1654
  setExtensionVersion(resolvedVersion);
1642
1655
  setStep("updateDone");
@@ -1664,7 +1677,7 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1664
1677
  allowedDomains: []
1665
1678
  },
1666
1679
  bundleUrl: `http://localhost:${extensionPort}`
1667
- });
1680
+ }, token);
1668
1681
  resolvedExtensionId = created.id;
1669
1682
  setExtensionId(created.id);
1670
1683
  updateStep(0, "done");
@@ -1703,13 +1716,14 @@ var App = ({ command, initialName, initialExtensionId, options }) => {
1703
1716
  };
1704
1717
  switch (step) {
1705
1718
  case "app": {
1706
- return /* @__PURE__ */ jsx13(AppSelect, { onSubmit: handleAppSelect });
1719
+ return /* @__PURE__ */ jsx13(AppSelect, { token, onSubmit: handleAppSelect });
1707
1720
  }
1708
1721
  case "extensionSelect": {
1709
1722
  return /* @__PURE__ */ jsx13(
1710
1723
  ExtensionSelect,
1711
1724
  {
1712
1725
  appId: selectedApp.id,
1726
+ token,
1713
1727
  command,
1714
1728
  onSubmit: handleExtensionSelect,
1715
1729
  onBack: goBack
@@ -1880,7 +1894,7 @@ var startDevServer = (projectRoot) => {
1880
1894
  import { Box as Box14, Text as Text14 } from "ink";
1881
1895
  import { useState as useState9, useEffect as useEffect4 } from "react";
1882
1896
  import { jsx as jsx14, jsxs as jsxs14 } from "react/jsx-runtime";
1883
- var DevSetup = ({ initialContext, onReady }) => {
1897
+ var DevSetup = ({ initialContext, token, onReady }) => {
1884
1898
  const [step, setStep] = useState9("app");
1885
1899
  const [selectedApp, setSelectedApp] = useState9(null);
1886
1900
  useEffect4(() => {
@@ -1902,7 +1916,7 @@ var DevSetup = ({ initialContext, onReady }) => {
1902
1916
  if (step === "app") {
1903
1917
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
1904
1918
  /* @__PURE__ */ jsx14(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { children: "Select the App for your extension:" }) }),
1905
- /* @__PURE__ */ jsx14(AppSelect, { onSubmit: handleAppSelect })
1919
+ /* @__PURE__ */ jsx14(AppSelect, { token, onSubmit: handleAppSelect })
1906
1920
  ] });
1907
1921
  }
1908
1922
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
@@ -1911,6 +1925,7 @@ var DevSetup = ({ initialContext, onReady }) => {
1911
1925
  ExtensionSelect,
1912
1926
  {
1913
1927
  appId: selectedApp?.id || initialContext.appId,
1928
+ token,
1914
1929
  onSubmit: handleExtensionSelect
1915
1930
  }
1916
1931
  )
@@ -2015,7 +2030,7 @@ var DevDashboard = ({
2015
2030
 
2016
2031
  // src/components/DevApp.tsx
2017
2032
  import { jsx as jsx16 } from "react/jsx-runtime";
2018
- var DevApp = ({ options = {} }) => {
2033
+ var DevApp = ({ token, options = {} }) => {
2019
2034
  const [state, setState] = useState10("setup");
2020
2035
  const [devContext, setDevContext] = useState10(null);
2021
2036
  const [resolvedContext, setResolvedContext] = useState10(null);
@@ -2114,6 +2129,7 @@ var DevApp = ({ options = {} }) => {
2114
2129
  DevSetup,
2115
2130
  {
2116
2131
  initialContext: devContext,
2132
+ token,
2117
2133
  onReady: handleSetupReady
2118
2134
  }
2119
2135
  );
@@ -2142,6 +2158,54 @@ var DevApp = ({ options = {} }) => {
2142
2158
  return /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsx16(Text16, { children: "Loading..." }) });
2143
2159
  };
2144
2160
 
2161
+ // src/lib/auth.ts
2162
+ import { readFile as readFile3, writeFile as writeFile3, mkdir, unlink } from "fs/promises";
2163
+ import { join as join4 } from "path";
2164
+ import { homedir } from "os";
2165
+ var AUTH_DIR = join4(homedir(), ".stackable");
2166
+ var AUTH_FILE = join4(AUTH_DIR, "auth.json");
2167
+ var readAuthState = async () => {
2168
+ try {
2169
+ const content = await readFile3(AUTH_FILE, "utf8");
2170
+ return JSON.parse(content);
2171
+ } catch {
2172
+ return null;
2173
+ }
2174
+ };
2175
+ var writeAuthState = async (state) => {
2176
+ await mkdir(AUTH_DIR, { recursive: true, mode: 448 });
2177
+ await writeFile3(AUTH_FILE, JSON.stringify(state, null, 2), { mode: 384 });
2178
+ };
2179
+ var clearAuthState = async () => {
2180
+ try {
2181
+ await unlink(AUTH_FILE);
2182
+ } catch {
2183
+ }
2184
+ };
2185
+ var decodeJwtPayload = (token) => {
2186
+ try {
2187
+ const [, payload] = token.split(".");
2188
+ if (!payload) return null;
2189
+ const json = Buffer.from(payload, "base64url").toString("utf8");
2190
+ return JSON.parse(json);
2191
+ } catch {
2192
+ return null;
2193
+ }
2194
+ };
2195
+ var getToken = async () => {
2196
+ const state = await readAuthState();
2197
+ if (!state) {
2198
+ throw new Error("Not authenticated. Run `stackable auth login` first.");
2199
+ }
2200
+ const payload = decodeJwtPayload(state.token);
2201
+ if (payload?.exp && typeof payload.exp === "number") {
2202
+ if (Date.now() >= payload.exp * 1e3) {
2203
+ throw new Error("Session expired. Run `stackable auth login` to re-authenticate.");
2204
+ }
2205
+ }
2206
+ return state.token;
2207
+ };
2208
+
2145
2209
  // src/lib/versionCheck.ts
2146
2210
  import https from "https";
2147
2211
  var PACKAGE_NAME = "@stackable-labs/cli-app-extension";
@@ -2195,16 +2259,107 @@ var require2 = createRequire(import.meta.url);
2195
2259
  var { version } = require2("../package.json");
2196
2260
  checkForUpdate(version);
2197
2261
  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 }));
2262
+ 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 }));
2265
+ });
2266
+ 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 }));
2269
+ });
2270
+ 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 }));
2200
2273
  });
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 }));
2274
+ 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 });
2277
+ });
2278
+ var DASHBOARD_URL = process.env.ADMIN_DASHBOARD_URL ?? "https://admin.stackablelabs.io";
2279
+ var auth = program.command("auth").description("Manage CLI authentication");
2280
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2281
+ 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}`);
2203
2345
  });
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 }));
2346
+ auth.command("logout").description("Clear stored CLI credentials").action(async () => {
2347
+ await clearAuthState();
2348
+ console.log("Logged out.");
2206
2349
  });
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 });
2350
+ auth.command("status").description("Show current authentication status").action(async () => {
2351
+ const state = await readAuthState();
2352
+ if (!state) {
2353
+ console.log("Not logged in. Run `stackable auth login`.");
2354
+ return;
2355
+ }
2356
+ const [, payloadB64] = state.token.split(".");
2357
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
2358
+ const expiry = payload.exp ? new Date(payload.exp * 1e3) : null;
2359
+ if (expiry && Date.now() >= expiry.getTime()) {
2360
+ console.log(`Session expired (${expiry.toLocaleDateString()}). Run \`stackable auth login\` to re-authenticate.`);
2361
+ return;
2362
+ }
2363
+ console.log(`Logged in \u2014 org: ${state.orgSlug ?? state.orgId}${expiry ? `. Token expires: ${expiry.toLocaleDateString()}.` : ""}`);
2209
2364
  });
2210
2365
  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.16.0",
3
+ "version": "1.18.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.",