@stackable-labs/cli-app-extension 1.18.0 → 1.20.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 +517 -123
  2. package/package.json +3 -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";
@@ -777,7 +775,7 @@ var fetchApps = async (token) => {
777
775
  iconUrl
778
776
  }));
779
777
  };
780
- var createExtensionRemote = async (appId, payload, token) => {
778
+ var createExtensionRemote = async (appId, token, payload) => {
781
779
  const baseUrl = getAdminApiBaseUrl();
782
780
  const res = await fetch(`${baseUrl}/app-extension/${appId}/extensions`, {
783
781
  method: "POST",
@@ -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,17 +1726,25 @@ 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(
1730
+ AppSelect,
1731
+ {
1732
+ token,
1733
+ orgId,
1734
+ userId,
1735
+ onSubmit: handleAppSelect
1736
+ }
1737
+ );
1720
1738
  }
1721
1739
  case "extensionSelect": {
1722
1740
  return /* @__PURE__ */ jsx13(
1723
1741
  ExtensionSelect,
1724
1742
  {
1725
1743
  appId: selectedApp.id,
1726
- token,
1727
1744
  command,
1728
- onSubmit: handleExtensionSelect,
1729
- onBack: goBack
1745
+ token,
1746
+ onBack: goBack,
1747
+ onSubmit: handleExtensionSelect
1730
1748
  }
1731
1749
  );
1732
1750
  }
@@ -1736,8 +1754,8 @@ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1736
1754
  {
1737
1755
  availableTargets: selectedApp?.targets ?? [],
1738
1756
  preSelected: targets,
1739
- onSubmit: handleConfirmTargets,
1740
- onBack: goBack
1757
+ onBack: goBack,
1758
+ onSubmit: handleConfirmTargets
1741
1759
  }
1742
1760
  );
1743
1761
  }
@@ -1746,8 +1764,8 @@ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1746
1764
  NamePrompt,
1747
1765
  {
1748
1766
  initialValue: name,
1749
- onSubmit: handleName,
1750
- onBack: goBack
1767
+ onBack: goBack,
1768
+ onSubmit: handleName
1751
1769
  }
1752
1770
  );
1753
1771
  }
@@ -1777,10 +1795,10 @@ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1777
1795
  {
1778
1796
  command,
1779
1797
  name,
1780
- extensionPort,
1781
- previewPort,
1782
1798
  targets,
1783
1799
  outputDir,
1800
+ previewPort,
1801
+ extensionPort,
1784
1802
  extensionVersion: command !== "create" /* CREATE */ ? extensionVersion : void 0,
1785
1803
  bundleUrl: command === "update" /* UPDATE */ ? bundleUrl : void 0,
1786
1804
  enabled: command === "update" /* UPDATE */ ? enabled : void 0,
@@ -1801,9 +1819,9 @@ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1801
1819
  {
1802
1820
  name,
1803
1821
  targets,
1804
- availableTargets: selectedApp?.targets ?? [],
1805
- bundleUrl,
1806
1822
  enabled,
1823
+ bundleUrl,
1824
+ availableTargets: selectedApp?.targets ?? [],
1807
1825
  onSubmit: (updated) => {
1808
1826
  setName(updated.name);
1809
1827
  setTargets(updated.targets);
@@ -1856,8 +1874,8 @@ var App = ({ command, token, initialName, initialExtensionId, options }) => {
1856
1874
  };
1857
1875
 
1858
1876
  // src/components/DevApp.tsx
1859
- import { Box as Box16, Text as Text16 } from "ink";
1860
1877
  import { useRef, useState as useState10, useEffect as useEffect6, useCallback as useCallback2 } from "react";
1878
+ import { useInput as useInput9, Box as Box16, Text as Text16 } from "ink";
1861
1879
 
1862
1880
  // src/lib/tunnel.ts
1863
1881
  import { Tunnel } from "cloudflared";
@@ -1916,7 +1934,13 @@ var DevSetup = ({ initialContext, token, onReady }) => {
1916
1934
  if (step === "app") {
1917
1935
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
1918
1936
  /* @__PURE__ */ jsx14(Box14, { marginBottom: 1, children: /* @__PURE__ */ jsx14(Text14, { children: "Select the App for your extension:" }) }),
1919
- /* @__PURE__ */ jsx14(AppSelect, { token, onSubmit: handleAppSelect })
1937
+ /* @__PURE__ */ jsx14(
1938
+ AppSelect,
1939
+ {
1940
+ token,
1941
+ onSubmit: handleAppSelect
1942
+ }
1943
+ )
1920
1944
  ] });
1921
1945
  }
1922
1946
  return /* @__PURE__ */ jsxs14(Box14, { flexDirection: "column", children: [
@@ -1924,8 +1948,8 @@ var DevSetup = ({ initialContext, token, onReady }) => {
1924
1948
  /* @__PURE__ */ jsx14(
1925
1949
  ExtensionSelect,
1926
1950
  {
1927
- appId: selectedApp?.id || initialContext.appId,
1928
1951
  token,
1952
+ appId: selectedApp?.id || initialContext.appId,
1929
1953
  onSubmit: handleExtensionSelect
1930
1954
  }
1931
1955
  )
@@ -1937,14 +1961,16 @@ import { Box as Box15, Text as Text15, useInput as useInput8 } from "ink";
1937
1961
  import { useEffect as useEffect5 } from "react";
1938
1962
  import { jsx as jsx15, jsxs as jsxs15 } from "react/jsx-runtime";
1939
1963
  var DevDashboard = ({
1940
- extensionName,
1941
- extensionId,
1964
+ previewTunnelUrl,
1965
+ tunnelUrl,
1966
+ userId,
1967
+ orgId,
1942
1968
  appId,
1943
1969
  appName,
1944
- tunnelUrl,
1945
- previewTunnelUrl,
1946
- previewPort,
1970
+ extensionId,
1971
+ extensionName,
1947
1972
  extensionPort,
1973
+ previewPort,
1948
1974
  onQuit
1949
1975
  }) => {
1950
1976
  useEffect5(() => {
@@ -1962,7 +1988,7 @@ var DevDashboard = ({
1962
1988
  }
1963
1989
  });
1964
1990
  return /* @__PURE__ */ jsxs15(Box15, { flexDirection: "column", children: [
1965
- /* @__PURE__ */ jsx15(Banner, {}),
1991
+ /* @__PURE__ */ jsx15(Banner, { userId, orgId }),
1966
1992
  /* @__PURE__ */ jsxs15(
1967
1993
  StepShell,
1968
1994
  {
@@ -2030,7 +2056,7 @@ var DevDashboard = ({
2030
2056
 
2031
2057
  // src/components/DevApp.tsx
2032
2058
  import { jsx as jsx16 } from "react/jsx-runtime";
2033
- var DevApp = ({ token, options = {} }) => {
2059
+ var DevApp = ({ token, userId, orgId, options = {} }) => {
2034
2060
  const [state, setState] = useState10("setup");
2035
2061
  const [devContext, setDevContext] = useState10(null);
2036
2062
  const [resolvedContext, setResolvedContext] = useState10(null);
@@ -2123,13 +2149,22 @@ var DevApp = ({ token, options = {} }) => {
2123
2149
  console.log("[dev] Done");
2124
2150
  process.exit(0);
2125
2151
  };
2152
+ useInput9((input, key) => {
2153
+ if (input === "c" && key.ctrl) {
2154
+ if (state === "running") {
2155
+ handleQuit();
2156
+ } else {
2157
+ process.exit(0);
2158
+ }
2159
+ }
2160
+ });
2126
2161
  if (state === "setup" && devContext) {
2127
2162
  if (!devContext.appId || !devContext.extensionId) {
2128
2163
  return /* @__PURE__ */ jsx16(
2129
2164
  DevSetup,
2130
2165
  {
2131
- initialContext: devContext,
2132
2166
  token,
2167
+ initialContext: devContext,
2133
2168
  onReady: handleSetupReady
2134
2169
  }
2135
2170
  );
@@ -2140,14 +2175,16 @@ var DevApp = ({ token, options = {} }) => {
2140
2175
  return /* @__PURE__ */ jsx16(
2141
2176
  DevDashboard,
2142
2177
  {
2143
- extensionName: devContext.extensionName,
2144
- extensionId: resolvedContext.extensionId,
2178
+ previewTunnelUrl,
2179
+ tunnelUrl,
2180
+ orgId,
2181
+ userId,
2145
2182
  appId: resolvedContext.appId,
2146
2183
  appName: resolvedContext.appName,
2184
+ extensionName: devContext.extensionName,
2185
+ extensionId: resolvedContext.extensionId,
2147
2186
  extensionPort: options.extensionPort ? parseInt(options.extensionPort, 10) : devContext.extensionPort,
2148
2187
  previewPort: options.previewPort ? parseInt(options.previewPort, 10) : devContext.previewPort,
2149
- tunnelUrl,
2150
- previewTunnelUrl,
2151
2188
  onQuit: handleQuit
2152
2189
  }
2153
2190
  );
@@ -2158,12 +2195,138 @@ var DevApp = ({ token, options = {} }) => {
2158
2195
  return /* @__PURE__ */ jsx16(Box16, { children: /* @__PURE__ */ jsx16(Text16, { children: "Loading..." }) });
2159
2196
  };
2160
2197
 
2198
+ // src/components/AIScaffold.tsx
2199
+ import { Box as Box17, Text as Text17, useApp as useApp2 } from "ink";
2200
+ import Spinner4 from "ink-spinner";
2201
+ import { useState as useState11, useEffect as useEffect7 } from "react";
2202
+
2203
+ // src/lib/aiDocs.ts
2204
+ import { existsSync, readFileSync } from "fs";
2205
+ import { join as join4, dirname } from "path";
2206
+ import { mkdir, writeFile as writeFile3 } from "fs/promises";
2207
+ import AdmZip from "adm-zip";
2208
+ var DEFAULT_STATIC_CDN_URL = "https://static.stackablelabs.io";
2209
+ var AI_DOCS_FILENAME = "extension-ai-docs.zip";
2210
+ var getStaticCdnBaseUrl = () => process.env.STATIC_CDN_BASE_URL ?? DEFAULT_STATIC_CDN_URL;
2211
+ var isValidManifest = (data) => {
2212
+ if (!data || typeof data !== "object") return false;
2213
+ const m = data;
2214
+ return typeof m.name === "string" && m.name.length > 0 && typeof m.version === "string" && Array.isArray(m.targets) && Array.isArray(m.permissions) && Array.isArray(m.allowedDomains);
2215
+ };
2216
+ var isExtensionProject = (dir) => {
2217
+ const manifestPath = join4(dir, "packages/extension/public/manifest.json");
2218
+ if (!existsSync(manifestPath)) {
2219
+ return { valid: false, reason: "No manifest.json found at packages/extension/public/manifest.json \u2014 is this an Extension project?" };
2220
+ }
2221
+ try {
2222
+ const raw = readFileSync(manifestPath, "utf8");
2223
+ const data = JSON.parse(raw);
2224
+ if (!isValidManifest(data)) {
2225
+ return { valid: false, reason: "Invalid manifest: missing required fields (name, version, targets, permissions, allowedDomains)" };
2226
+ }
2227
+ } catch {
2228
+ return { valid: false, reason: "Failed to parse manifest.json" };
2229
+ }
2230
+ return { valid: true };
2231
+ };
2232
+ var downloadAndExtractAiDocs = async (targetDir, version2) => {
2233
+ const baseUrl = getStaticCdnBaseUrl();
2234
+ const zipUrl = `${baseUrl}/ai-docs/${version2}/${AI_DOCS_FILENAME}`;
2235
+ const response = await fetch(zipUrl);
2236
+ if (!response.ok) {
2237
+ throw new Error(`Failed to download AI docs from ${zipUrl}: ${response.status} ${response.statusText}`);
2238
+ }
2239
+ const buffer = Buffer.from(await response.arrayBuffer());
2240
+ const zip = new AdmZip(buffer);
2241
+ const entries = zip.getEntries();
2242
+ const extractedFiles = [];
2243
+ for (const entry of entries) {
2244
+ if (entry.isDirectory) continue;
2245
+ const targetPath = join4(targetDir, entry.entryName);
2246
+ await mkdir(dirname(targetPath), { recursive: true });
2247
+ await writeFile3(targetPath, entry.getData());
2248
+ extractedFiles.push(entry.entryName);
2249
+ }
2250
+ return { files: extractedFiles.sort() };
2251
+ };
2252
+
2253
+ // src/components/AIScaffold.tsx
2254
+ import { jsx as jsx17, jsxs as jsxs16 } from "react/jsx-runtime";
2255
+ var AIScaffold = ({ version: version2 }) => {
2256
+ const { exit } = useApp2();
2257
+ const [state, setState] = useState11("validating");
2258
+ const [files, setFiles] = useState11([]);
2259
+ const [errorMessage, setErrorMessage] = useState11("");
2260
+ useEffect7(() => {
2261
+ const run = async () => {
2262
+ const projectDir = process.cwd();
2263
+ const check = isExtensionProject(projectDir);
2264
+ if (!check.valid) {
2265
+ setErrorMessage(check.reason);
2266
+ setState("error");
2267
+ exit();
2268
+ return;
2269
+ }
2270
+ setState("downloading");
2271
+ try {
2272
+ const result = await downloadAndExtractAiDocs(projectDir, version2);
2273
+ setFiles(result.files);
2274
+ setState("done");
2275
+ } catch (err) {
2276
+ setErrorMessage(err instanceof Error ? err.message : String(err));
2277
+ setState("error");
2278
+ }
2279
+ exit();
2280
+ };
2281
+ run();
2282
+ }, []);
2283
+ return /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", children: [
2284
+ /* @__PURE__ */ jsx17(Banner, {}),
2285
+ /* @__PURE__ */ jsxs16(StepShell, { title: "AI Editor Config", children: [
2286
+ state === "validating" && /* @__PURE__ */ jsxs16(Box17, { gap: 1, children: [
2287
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", children: /* @__PURE__ */ jsx17(Spinner4, { type: "dots" }) }),
2288
+ /* @__PURE__ */ jsx17(Text17, { children: "Checking project..." })
2289
+ ] }),
2290
+ state === "downloading" && /* @__PURE__ */ jsxs16(Box17, { gap: 1, children: [
2291
+ /* @__PURE__ */ jsx17(Text17, { color: "cyan", children: /* @__PURE__ */ jsx17(Spinner4, { type: "dots" }) }),
2292
+ /* @__PURE__ */ jsxs16(Text17, { children: [
2293
+ "Downloading AI editor configs (",
2294
+ version2,
2295
+ ")..."
2296
+ ] })
2297
+ ] }),
2298
+ state === "done" && /* @__PURE__ */ jsxs16(Box17, { flexDirection: "column", gap: 1, children: [
2299
+ /* @__PURE__ */ jsxs16(Box17, { gap: 1, children: [
2300
+ /* @__PURE__ */ jsx17(Text17, { color: "green", bold: true, children: "\u2714" }),
2301
+ /* @__PURE__ */ jsxs16(Text17, { bold: true, children: [
2302
+ "AI editor configs installed (",
2303
+ files.length,
2304
+ " files)"
2305
+ ] })
2306
+ ] }),
2307
+ /* @__PURE__ */ jsx17(Box17, { flexDirection: "column", marginLeft: 2, children: files.map((f) => /* @__PURE__ */ jsx17(Text17, { dimColor: true, children: f }, f)) })
2308
+ ] }),
2309
+ state === "error" && /* @__PURE__ */ jsxs16(Box17, { gap: 1, children: [
2310
+ /* @__PURE__ */ jsx17(Text17, { color: "red", children: "\u2716" }),
2311
+ /* @__PURE__ */ jsx17(Text17, { children: errorMessage })
2312
+ ] })
2313
+ ] })
2314
+ ] });
2315
+ };
2316
+
2317
+ // src/components/AuthLogin.tsx
2318
+ import { createServer } from "http";
2319
+ import { Box as Box18, Text as Text18, useApp as useApp3 } from "ink";
2320
+ import Spinner5 from "ink-spinner";
2321
+ import open from "open";
2322
+ import { useState as useState12, useEffect as useEffect8 } from "react";
2323
+
2161
2324
  // src/lib/auth.ts
2162
- import { readFile as readFile3, writeFile as writeFile3, mkdir, unlink } from "fs/promises";
2163
- import { join as join4 } from "path";
2325
+ import { readFile as readFile3, writeFile as writeFile4, mkdir as mkdir2, unlink } from "fs/promises";
2326
+ import { join as join5 } from "path";
2164
2327
  import { homedir } from "os";
2165
- var AUTH_DIR = join4(homedir(), ".stackable");
2166
- var AUTH_FILE = join4(AUTH_DIR, "auth.json");
2328
+ var AUTH_DIR = join5(homedir(), ".stackable");
2329
+ var AUTH_FILE = join5(AUTH_DIR, "auth.json");
2167
2330
  var readAuthState = async () => {
2168
2331
  try {
2169
2332
  const content = await readFile3(AUTH_FILE, "utf8");
@@ -2173,8 +2336,8 @@ var readAuthState = async () => {
2173
2336
  }
2174
2337
  };
2175
2338
  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 });
2339
+ await mkdir2(AUTH_DIR, { recursive: true, mode: 448 });
2340
+ await writeFile4(AUTH_FILE, JSON.stringify(state, null, 2), { mode: 384 });
2178
2341
  };
2179
2342
  var clearAuthState = async () => {
2180
2343
  try {
@@ -2185,7 +2348,9 @@ var clearAuthState = async () => {
2185
2348
  var decodeJwtPayload = (token) => {
2186
2349
  try {
2187
2350
  const [, payload] = token.split(".");
2188
- if (!payload) return null;
2351
+ if (!payload) {
2352
+ return null;
2353
+ }
2189
2354
  const json = Buffer.from(payload, "base64url").toString("utf8");
2190
2355
  return JSON.parse(json);
2191
2356
  } catch {
@@ -2195,17 +2360,217 @@ var decodeJwtPayload = (token) => {
2195
2360
  var getToken = async () => {
2196
2361
  const state = await readAuthState();
2197
2362
  if (!state) {
2198
- throw new Error("Not authenticated. Run `stackable auth login` first.");
2363
+ throw new Error("Not authenticated. Run `stackable-app-extension auth login` first.");
2199
2364
  }
2200
2365
  const payload = decodeJwtPayload(state.token);
2201
2366
  if (payload?.exp && typeof payload.exp === "number") {
2202
2367
  if (Date.now() >= payload.exp * 1e3) {
2203
- throw new Error("Session expired. Run `stackable auth login` to re-authenticate.");
2368
+ throw new Error("Session expired. Run `stackable-app-extension auth login` to re-authenticate.");
2204
2369
  }
2205
2370
  }
2206
2371
  return state.token;
2207
2372
  };
2208
2373
 
2374
+ // src/components/AuthLogin.tsx
2375
+ import { jsx as jsx18, jsxs as jsxs17 } from "react/jsx-runtime";
2376
+ var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2377
+ var callbackPage = (heading, sub, redirectUrl) => `<!DOCTYPE html>
2378
+ <html><head><meta charset="utf-8"><title>Stackable CLI</title>
2379
+ <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}
2380
+ .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>
2381
+ </head><body><div class="card"><h2>${heading}</h2><p>${sub}</p><p class="hint" id="h"></p></div>
2382
+ ${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>` : ""}
2383
+ </body></html>`;
2384
+ var AuthLogin = ({ dashboardUrl }) => {
2385
+ const { exit } = useApp3();
2386
+ const [state, setState] = useState12("waiting");
2387
+ const [loginUrl, setLoginUrl] = useState12("");
2388
+ const [userIdLabel, setUserIdLabel] = useState12("");
2389
+ const [orgIdLabel, setOrgIdLabel] = useState12("");
2390
+ const [errorMessage, setErrorMessage] = useState12("");
2391
+ useEffect8(() => {
2392
+ let server;
2393
+ let timeout;
2394
+ const run = async () => {
2395
+ let resolveToken;
2396
+ let rejectToken;
2397
+ const tokenPromise = new Promise((resolve, reject) => {
2398
+ resolveToken = resolve;
2399
+ rejectToken = reject;
2400
+ });
2401
+ server = createServer((req, res) => {
2402
+ const url2 = new URL(req.url, "http://localhost");
2403
+ if (url2.pathname === "/callback") {
2404
+ const error = url2.searchParams.get("error");
2405
+ if (error) {
2406
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2407
+ res.end(callbackPage("Authentication failed", "You can close this tab."));
2408
+ rejectToken(new Error(error));
2409
+ return;
2410
+ }
2411
+ const token2 = url2.searchParams.get("token");
2412
+ if (token2) {
2413
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
2414
+ res.end(callbackPage("CLI authenticated", "You can return to your terminal.", dashboardUrl));
2415
+ resolveToken(token2);
2416
+ } else {
2417
+ res.writeHead(400, { "content-type": "text/plain; charset=utf-8" });
2418
+ res.end("Missing token");
2419
+ }
2420
+ } else {
2421
+ res.writeHead(404);
2422
+ res.end();
2423
+ }
2424
+ });
2425
+ await new Promise((r) => server.listen(0, "127.0.0.1", r));
2426
+ const port = server.address().port;
2427
+ const callbackUrl = `http://localhost:${port}/callback`;
2428
+ const url = `${dashboardUrl}/cli-auth?callback=${encodeURIComponent(callbackUrl)}`;
2429
+ setLoginUrl(url);
2430
+ await open(url);
2431
+ timeout = setTimeout(() => {
2432
+ server.close();
2433
+ rejectToken(new Error("Login timed out. Please try again."));
2434
+ }, LOGIN_TIMEOUT_MS);
2435
+ let token;
2436
+ try {
2437
+ token = await tokenPromise;
2438
+ } catch (err) {
2439
+ clearTimeout(timeout);
2440
+ server.close();
2441
+ setErrorMessage(err instanceof Error ? err.message : String(err));
2442
+ setState("error");
2443
+ exit();
2444
+ return;
2445
+ }
2446
+ clearTimeout(timeout);
2447
+ server.close();
2448
+ const [, payloadB64] = token.split(".");
2449
+ const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
2450
+ await writeAuthState({
2451
+ token,
2452
+ userId: payload.sub,
2453
+ orgId: payload.orgId
2454
+ });
2455
+ setUserIdLabel(payload.sub);
2456
+ setOrgIdLabel(payload.orgId);
2457
+ setState("success");
2458
+ exit();
2459
+ };
2460
+ run();
2461
+ return () => {
2462
+ clearTimeout(timeout);
2463
+ server?.close();
2464
+ };
2465
+ }, []);
2466
+ return /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", children: [
2467
+ /* @__PURE__ */ jsx18(Banner, {}),
2468
+ /* @__PURE__ */ jsxs17(StepShell, { title: "Authenticate with Stackable", children: [
2469
+ state === "waiting" && /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", gap: 1, children: [
2470
+ /* @__PURE__ */ jsxs17(Box18, { gap: 1, children: [
2471
+ /* @__PURE__ */ jsx18(Text18, { color: "cyan", children: /* @__PURE__ */ jsx18(Spinner5, { type: "dots" }) }),
2472
+ /* @__PURE__ */ jsx18(Text18, { children: "Waiting for browser authentication..." })
2473
+ ] }),
2474
+ loginUrl && /* @__PURE__ */ jsxs17(Text18, { dimColor: true, children: [
2475
+ " ",
2476
+ loginUrl
2477
+ ] })
2478
+ ] }),
2479
+ state === "success" && /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", gap: 1, children: [
2480
+ /* @__PURE__ */ jsxs17(Box18, { flexDirection: "column", children: [
2481
+ /* @__PURE__ */ jsxs17(Box18, { gap: 2, children: [
2482
+ /* @__PURE__ */ jsx18(Text18, { dimColor: true, children: "User:" }),
2483
+ /* @__PURE__ */ jsx18(Text18, { color: "cyan", children: userIdLabel })
2484
+ ] }),
2485
+ /* @__PURE__ */ jsxs17(Box18, { gap: 2, children: [
2486
+ /* @__PURE__ */ jsx18(Text18, { dimColor: true, children: "Org: " }),
2487
+ /* @__PURE__ */ jsx18(Text18, { color: "cyan", children: orgIdLabel })
2488
+ ] })
2489
+ ] }),
2490
+ /* @__PURE__ */ jsxs17(Box18, { gap: 1, children: [
2491
+ /* @__PURE__ */ jsx18(Text18, { color: "green", bold: true, children: "\u2714" }),
2492
+ /* @__PURE__ */ jsx18(Text18, { bold: true, children: "Authenticated" })
2493
+ ] })
2494
+ ] }),
2495
+ state === "error" && /* @__PURE__ */ jsxs17(Box18, { gap: 1, children: [
2496
+ /* @__PURE__ */ jsx18(Text18, { color: "red", children: "\u2716" }),
2497
+ /* @__PURE__ */ jsx18(Text18, { children: errorMessage })
2498
+ ] })
2499
+ ] })
2500
+ ] });
2501
+ };
2502
+
2503
+ // src/components/AuthLogout.tsx
2504
+ import { useEffect as useEffect9 } from "react";
2505
+ import { Box as Box19, Text as Text19, useApp as useApp4 } from "ink";
2506
+ import { jsx as jsx19, jsxs as jsxs18 } from "react/jsx-runtime";
2507
+ var AuthLogout = () => {
2508
+ const { exit } = useApp4();
2509
+ useEffect9(() => {
2510
+ exit();
2511
+ }, [exit]);
2512
+ return /* @__PURE__ */ jsxs18(Box19, { flexDirection: "column", children: [
2513
+ /* @__PURE__ */ jsx19(Banner, {}),
2514
+ /* @__PURE__ */ jsx19(StepShell, { title: "Authenticate with Stackable", children: /* @__PURE__ */ jsxs18(Box19, { gap: 1, children: [
2515
+ /* @__PURE__ */ jsx19(Text19, { color: "green", bold: true, children: "\u2714" }),
2516
+ /* @__PURE__ */ jsx19(Text19, { bold: true, children: "Logged out" })
2517
+ ] }) })
2518
+ ] });
2519
+ };
2520
+
2521
+ // src/components/AuthStatus.tsx
2522
+ import { useEffect as useEffect10 } from "react";
2523
+ import { useApp as useApp5, Box as Box20, Text as Text20 } from "ink";
2524
+ import { jsx as jsx20, jsxs as jsxs19 } from "react/jsx-runtime";
2525
+ var AuthStatus = ({ state, userId, orgId, expiry }) => {
2526
+ const { exit } = useApp5();
2527
+ useEffect10(() => {
2528
+ exit();
2529
+ }, [exit]);
2530
+ return /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", children: [
2531
+ /* @__PURE__ */ jsx20(Banner, {}),
2532
+ /* @__PURE__ */ jsxs19(StepShell, { title: "Authenticate with Stackable", children: [
2533
+ state === "authenticated" && /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", gap: 1, children: [
2534
+ /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", children: [
2535
+ /* @__PURE__ */ jsxs19(Box20, { gap: 2, children: [
2536
+ /* @__PURE__ */ jsx20(Text20, { dimColor: true, children: "User:" }),
2537
+ /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: userId })
2538
+ ] }),
2539
+ /* @__PURE__ */ jsxs19(Box20, { gap: 2, children: [
2540
+ /* @__PURE__ */ jsx20(Text20, { dimColor: true, children: "Org: " }),
2541
+ /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: orgId })
2542
+ ] }),
2543
+ expiry && /* @__PURE__ */ jsxs19(Box20, { gap: 2, children: [
2544
+ /* @__PURE__ */ jsx20(Text20, { dimColor: true, children: "Exp: " }),
2545
+ /* @__PURE__ */ jsx20(Text20, { color: "cyan", children: expiry.toLocaleDateString() })
2546
+ ] })
2547
+ ] }),
2548
+ /* @__PURE__ */ jsxs19(Box20, { gap: 1, children: [
2549
+ /* @__PURE__ */ jsx20(Text20, { color: "green", bold: true, children: "\u2714" }),
2550
+ /* @__PURE__ */ jsx20(Text20, { bold: true, children: "Authenticated" })
2551
+ ] })
2552
+ ] }),
2553
+ state === "expired" && /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", gap: 1, children: [
2554
+ /* @__PURE__ */ jsxs19(Box20, { gap: 1, children: [
2555
+ /* @__PURE__ */ jsx20(Text20, { color: "red", children: "\u2716" }),
2556
+ /* @__PURE__ */ jsxs19(Text20, { children: [
2557
+ "Session expired",
2558
+ expiry ? ` (${expiry.toLocaleDateString()})` : ""
2559
+ ] })
2560
+ ] }),
2561
+ /* @__PURE__ */ jsx20(Text20, { dimColor: true, children: "Run `stackable-app-extension auth login` to re-authenticate." })
2562
+ ] }),
2563
+ state === "not-logged-in" && /* @__PURE__ */ jsxs19(Box20, { flexDirection: "column", gap: 1, children: [
2564
+ /* @__PURE__ */ jsxs19(Box20, { gap: 1, children: [
2565
+ /* @__PURE__ */ jsx20(Text20, { color: "red", children: "\u2716" }),
2566
+ /* @__PURE__ */ jsx20(Text20, { children: "Not logged in" })
2567
+ ] }),
2568
+ /* @__PURE__ */ jsx20(Text20, { dimColor: true, children: "Run `stackable-app-extension auth login`" })
2569
+ ] })
2570
+ ] })
2571
+ ] });
2572
+ };
2573
+
2209
2574
  // src/lib/versionCheck.ts
2210
2575
  import https from "https";
2211
2576
  var PACKAGE_NAME = "@stackable-labs/cli-app-extension";
@@ -2254,112 +2619,141 @@ var checkForUpdate = (currentVersion) => {
2254
2619
  };
2255
2620
 
2256
2621
  // src/index.tsx
2257
- import { jsx as jsx17 } from "react/jsx-runtime";
2622
+ import { jsx as jsx21 } from "react/jsx-runtime";
2258
2623
  var require2 = createRequire(import.meta.url);
2259
2624
  var { version } = require2("../package.json");
2260
2625
  checkForUpdate(version);
2626
+ var ensureToken = async () => {
2627
+ try {
2628
+ const token = await getToken();
2629
+ const state = await readAuthState();
2630
+ return { token, userId: state.userId, orgId: state.orgId };
2631
+ } catch (err) {
2632
+ const message = err instanceof Error ? err.message : String(err);
2633
+ const isExpired = message.toLowerCase().includes("expired");
2634
+ render(
2635
+ /* @__PURE__ */ jsx21(AuthStatus, { state: isExpired ? "expired" : "not-logged-in" })
2636
+ );
2637
+ return null;
2638
+ }
2639
+ };
2261
2640
  program.name("stackable-app-extension").description("Stackable Labs - App Extension developer CLI").version(version);
2262
2641
  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 }));
2642
+ const auth2 = await ensureToken();
2643
+ if (!auth2) {
2644
+ return;
2645
+ }
2646
+ const { token, userId, orgId } = auth2;
2647
+ render(
2648
+ /* @__PURE__ */ jsx21(
2649
+ App,
2650
+ {
2651
+ command: "create" /* CREATE */,
2652
+ initialName: name,
2653
+ options,
2654
+ token,
2655
+ orgId,
2656
+ userId
2657
+ }
2658
+ )
2659
+ );
2265
2660
  });
2266
2661
  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 }));
2662
+ const auth2 = await ensureToken();
2663
+ if (!auth2) {
2664
+ return;
2665
+ }
2666
+ const { token, userId, orgId } = auth2;
2667
+ render(
2668
+ /* @__PURE__ */ jsx21(
2669
+ App,
2670
+ {
2671
+ command: "scaffold" /* SCAFFOLD */,
2672
+ options,
2673
+ token,
2674
+ orgId,
2675
+ userId
2676
+ }
2677
+ )
2678
+ );
2269
2679
  });
2270
2680
  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 }));
2681
+ const auth2 = await ensureToken();
2682
+ if (!auth2) {
2683
+ return;
2684
+ }
2685
+ const { token, userId, orgId } = auth2;
2686
+ render(
2687
+ /* @__PURE__ */ jsx21(
2688
+ App,
2689
+ {
2690
+ command: "update" /* UPDATE */,
2691
+ initialExtensionId: extensionId,
2692
+ options,
2693
+ token,
2694
+ userId,
2695
+ orgId
2696
+ }
2697
+ )
2698
+ );
2273
2699
  });
2274
2700
  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 });
2701
+ const auth2 = await ensureToken();
2702
+ if (!auth2) {
2703
+ return;
2704
+ }
2705
+ const { token, userId, orgId } = auth2;
2706
+ render(
2707
+ /* @__PURE__ */ jsx21(
2708
+ DevApp,
2709
+ {
2710
+ options,
2711
+ token,
2712
+ userId,
2713
+ orgId
2714
+ }
2715
+ ),
2716
+ { exitOnCtrlC: false }
2717
+ );
2277
2718
  });
2278
2719
  var DASHBOARD_URL = process.env.ADMIN_DASHBOARD_URL ?? "https://admin.stackablelabs.io";
2279
2720
  var auth = program.command("auth").description("Manage CLI authentication");
2280
- var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
2281
2721
  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}`);
2722
+ render(/* @__PURE__ */ jsx21(AuthLogin, { dashboardUrl: DASHBOARD_URL }));
2345
2723
  });
2346
2724
  auth.command("logout").description("Clear stored CLI credentials").action(async () => {
2347
2725
  await clearAuthState();
2348
- console.log("Logged out.");
2726
+ render(/* @__PURE__ */ jsx21(AuthLogout, {}));
2349
2727
  });
2350
2728
  auth.command("status").description("Show current authentication status").action(async () => {
2351
2729
  const state = await readAuthState();
2352
2730
  if (!state) {
2353
- console.log("Not logged in. Run `stackable auth login`.");
2731
+ render(
2732
+ /* @__PURE__ */ jsx21(AuthStatus, { state: "not-logged-in" })
2733
+ );
2354
2734
  return;
2355
2735
  }
2356
2736
  const [, payloadB64] = state.token.split(".");
2357
2737
  const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
2358
2738
  const expiry = payload.exp ? new Date(payload.exp * 1e3) : null;
2359
2739
  if (expiry && Date.now() >= expiry.getTime()) {
2360
- console.log(`Session expired (${expiry.toLocaleDateString()}). Run \`stackable auth login\` to re-authenticate.`);
2740
+ render(/* @__PURE__ */ jsx21(AuthStatus, { state: "expired", expiry }));
2361
2741
  return;
2362
2742
  }
2363
- console.log(`Logged in \u2014 org: ${state.orgSlug ?? state.orgId}${expiry ? `. Token expires: ${expiry.toLocaleDateString()}.` : ""}`);
2743
+ render(
2744
+ /* @__PURE__ */ jsx21(
2745
+ AuthStatus,
2746
+ {
2747
+ state: "authenticated",
2748
+ userId: state.userId,
2749
+ orgId: state.orgId,
2750
+ expiry
2751
+ }
2752
+ )
2753
+ );
2754
+ });
2755
+ var ai = program.command("ai").description("AI editor configuration tools");
2756
+ ai.command("scaffold").description("Download AI editor config files into your Extension project").option("--version <version>", 'AI docs version (semver or "latest")', "latest").action(async (options) => {
2757
+ render(/* @__PURE__ */ jsx21(AIScaffold, { version: options.version }));
2364
2758
  });
2365
2759
  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.20.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -12,6 +12,8 @@
12
12
  "LICENSE"
13
13
  ],
14
14
  "dependencies": {
15
+ "@stackable-labs/lib-contracts": "workspace:*",
16
+ "adm-zip": "0.x",
15
17
  "cloudflared": "0.x",
16
18
  "commander": "12.x",
17
19
  "giget": "3.x",