@stackable-labs/cli-app-extension 1.77.0 → 1.78.1

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 +189 -61
  2. package/package.json +2 -1
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import Spinner5 from 'ink-spinner';
8
8
  import { useState, useCallback, useEffect, useRef, useMemo } from 'react';
9
9
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
10
10
  import TextInput from 'ink-text-input';
11
+ import { SURFACE_TARGETS } from '@stackable-labs/sdk-extension-contracts';
11
12
  import { execFile, spawn } from 'child_process';
12
13
  import { promisify } from 'util';
13
14
  import { installDependencies } from 'nypm';
@@ -78,8 +79,6 @@ var Banner = ({ userId, orgId } = {}) => {
78
79
  ] })
79
80
  ] });
80
81
  };
81
-
82
- // src/constants.ts
83
82
  var TEMPLATE_SOURCES = {
84
83
  minimal: "github:stackable-labs/templates/app-extension-minimal",
85
84
  starter: "github:stackable-labs/templates/app-extension",
@@ -90,7 +89,7 @@ var TEMPLATE_FLAVOR_META = {
90
89
  minimal: {
91
90
  label: "Minimal",
92
91
  description: "Bare minimum \u2014 single surface, hello-world component",
93
- defaultTargets: ["slot.content"],
92
+ defaultTargets: [SURFACE_TARGETS.CONTENT],
94
93
  skipTargetStep: true
95
94
  },
96
95
  starter: {
@@ -102,15 +101,15 @@ var TEMPLATE_FLAVOR_META = {
102
101
  "kitchen-sink": {
103
102
  label: "Kitchen Sink",
104
103
  description: "Everything including the kitchen sink \u2014 every component, capability, surface, and hook we could fit",
105
- defaultTargets: ["slot.header", "slot.content", "slot.footer", "slot.footer-links"],
104
+ defaultTargets: [SURFACE_TARGETS.HEADER, SURFACE_TARGETS.CONTENT, SURFACE_TARGETS.FOOTER, SURFACE_TARGETS.FOOTER_LINKS],
106
105
  skipTargetStep: true
107
106
  }
108
107
  };
109
108
  var TARGET_PERMISSION_MAP = {
110
- "slot.header": ["context:read"],
111
- "slot.content": ["context:read", "data:query", "data:fetch", "actions:toast", "actions:invoke"],
112
- "slot.footer": [],
113
- "slot.footer-links": []
109
+ [SURFACE_TARGETS.HEADER]: ["context:read"],
110
+ [SURFACE_TARGETS.CONTENT]: ["context:read", "data:query", "data:fetch", "actions:toast", "actions:invoke"],
111
+ [SURFACE_TARGETS.FOOTER]: [],
112
+ [SURFACE_TARGETS.FOOTER_LINKS]: []
114
113
  };
115
114
  var divider = (width) => "\u2500".repeat(width);
116
115
  var INNER_DIVIDER_WIDTH = 40;
@@ -1424,6 +1423,165 @@ var postScaffold = async (options) => {
1424
1423
  }
1425
1424
  };
1426
1425
 
1426
+ // ../../sdk/extension/ai-docs/src/generated/template-content.ts
1427
+ var PATTERN_SECTIONS = [
1428
+ {
1429
+ path: "index.tsx",
1430
+ title: "Entry Point",
1431
+ code: `import { createExtension } from '@stackable-labs/sdk-extension-react'
1432
+ import { Header } from './surfaces/Header'
1433
+ import { Content } from './surfaces/Content'
1434
+ import { Footer } from './surfaces/Footer'
1435
+
1436
+ const Extension = () => (
1437
+ <>
1438
+ <Header />
1439
+ <Content />
1440
+ <Footer />
1441
+ </>
1442
+ )
1443
+
1444
+ createExtension(() => <Extension />, { extensionId: '__EXTENSION_ID__' })`
1445
+ },
1446
+ {
1447
+ path: "store.ts",
1448
+ title: "Store-Based Navigation",
1449
+ code: `import { createStore } from '@stackable-labs/sdk-extension-react'
1450
+
1451
+ export type ViewState = { type: 'menu' }
1452
+
1453
+ export interface AppState {
1454
+ viewState: ViewState
1455
+ }
1456
+
1457
+ export const appStore = createStore<AppState>({
1458
+ viewState: { type: 'menu' },
1459
+ })`
1460
+ },
1461
+ {
1462
+ path: "surfaces/Content.tsx",
1463
+ title: "Content Surface with Loading State",
1464
+ code: `import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
1465
+ import { appStore } from '../store'
1466
+
1467
+ export function Content() {
1468
+ const viewState = useStore(appStore, (s) => s.viewState)
1469
+ const { loading } = useContextData()
1470
+
1471
+ if (loading) {
1472
+ return (
1473
+ <Surface id="slot.content">
1474
+ <ui.Stack direction="column" gap="2" className="animate-pulse">
1475
+ <ui.Card className="h-24" />
1476
+ <ui.Card className="h-32" />
1477
+ </ui.Stack>
1478
+ </Surface>
1479
+ )
1480
+ }
1481
+
1482
+ return (
1483
+ <Surface id="slot.content">
1484
+ {viewState.type === 'menu' && (
1485
+ <ui.Menu>
1486
+ {/* Add ui.MenuItem entries here */}
1487
+ </ui.Menu>
1488
+ )}
1489
+ </Surface>
1490
+ )
1491
+ }`
1492
+ },
1493
+ {
1494
+ path: "surfaces/Footer.tsx",
1495
+ title: "Surface Composition",
1496
+ code: `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
1497
+
1498
+ export function Footer() {
1499
+ return (
1500
+ <>
1501
+ <Surface id="slot.footer">
1502
+ <ui.Text className="text-xs">Powered by My Extension</ui.Text>
1503
+ </Surface>
1504
+ <Surface id="slot.footer-links">
1505
+ <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>
1506
+ </Surface>
1507
+ </>
1508
+ )
1509
+ }`
1510
+ },
1511
+ {
1512
+ path: "surfaces/Header.tsx",
1513
+ title: "Simple Surface",
1514
+ code: `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
1515
+
1516
+ export function Header() {
1517
+ return (
1518
+ <Surface id="slot.header">
1519
+ <ui.Text>Header content goes here</ui.Text>
1520
+ </Surface>
1521
+ )
1522
+ }`
1523
+ },
1524
+ {
1525
+ path: "lib/api.ts",
1526
+ title: "API Wrapper (data.query + data.fetch)",
1527
+ code: `/**
1528
+ * API wrapper patterns \u2014 choose one based on your integration model.
1529
+ *
1530
+ * data.query \u2192 host-mediated: the host handles the API call, extension sends
1531
+ * an action name + params, host returns data.
1532
+ * Permission: "data:query"
1533
+ *
1534
+ * data.fetch \u2192 direct HTTP: the extension calls external APIs through the
1535
+ * platform proxy. Domains must be in allowedDomains in manifest.
1536
+ * Permission: "data:fetch"
1537
+ *
1538
+ * Usage in a surface:
1539
+ * const capabilities = useCapabilities()
1540
+ * const api = createApi(capabilities.data.query)
1541
+ * // or: const api = createFetchApi(capabilities.data.fetch)
1542
+ */
1543
+
1544
+ import type { ApiRequest, FetchRequestInit, FetchResponse } from '@stackable-labs/sdk-extension-contracts'
1545
+
1546
+ type QueryFn = <T = unknown>(payload: ApiRequest) => Promise<T>
1547
+ type FetchFn = (url: string, init?: FetchRequestInit) => Promise<FetchResponse>
1548
+
1549
+ // \u2500\u2500 data.query wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1550
+
1551
+ export function createApi(query: QueryFn) {
1552
+ return {
1553
+ async getItems(): Promise<unknown[]> {
1554
+ return query<unknown[]>({ action: 'getItems' })
1555
+ },
1556
+
1557
+ async getItem(itemId: string): Promise<unknown> {
1558
+ return query<unknown>({ action: 'getItem', itemId })
1559
+ },
1560
+ }
1561
+ }
1562
+
1563
+ // \u2500\u2500 data.fetch wrapper \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1564
+
1565
+ const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string
1566
+
1567
+ export function createFetchApi(fetch: FetchFn) {
1568
+ return {
1569
+ async getItems(): Promise<unknown[]> {
1570
+ const result = await fetch(\`\${API_BASE_URL}/items\`, { method: 'GET' })
1571
+ if (!result.ok) throw new Error(\`getItems failed: \${result.status}\`)
1572
+ return result.data as unknown[]
1573
+ },
1574
+
1575
+ async getItem(itemId: string): Promise<unknown> {
1576
+ const result = await fetch(\`\${API_BASE_URL}/items/\${itemId}\`, { method: 'GET' })
1577
+ if (!result.ok) throw new Error(\`getItem failed: \${result.status}\`)
1578
+ return result.data as unknown
1579
+ },
1580
+ }
1581
+ }`
1582
+ }
1583
+ ];
1584
+
1427
1585
  // ../../lib/contracts/src/base.ts
1428
1586
  var asClerkUserId = (value) => value;
1429
1587
  var asClerkOrgId = (value) => value;
@@ -1528,6 +1686,13 @@ var writeDevContext = async (projectRoot, ctx) => {
1528
1686
  };
1529
1687
 
1530
1688
  // src/lib/scaffold.ts
1689
+ var getPattern = (title) => {
1690
+ const section = PATTERN_SECTIONS.find((s) => s.title === title);
1691
+ if (!section) {
1692
+ throw new Error(`PATTERN_SECTIONS missing "${title}" \u2014 run pnpm generate:docs`);
1693
+ }
1694
+ return section.code;
1695
+ };
1531
1696
  var isTextFile = (filePath) => /\.(ts|tsx|js|jsx|json|md|html|yml|yaml|env|gitignore|nvmrc)$/i.test(filePath);
1532
1697
  var normalizeTargets = (targets) => Array.from(new Set(targets));
1533
1698
  var deriveScaffoldPermissions = (targets) => {
@@ -1597,16 +1762,13 @@ var generateManifest = async (rootDir, extensionName, targets, permissions) => {
1597
1762
  `);
1598
1763
  };
1599
1764
  var buildFooterSurface = (targets) => {
1765
+ const fullFooter = getPattern("Surface Composition");
1766
+ const surfaceBlockRegex = /<Surface id="(slot\.footer(?:-links)?)">[\s\S]*?<\/Surface>/g;
1600
1767
  const blocks = [];
1601
- if (targets.includes("slot.footer")) {
1602
- blocks.push(
1603
- ' <Surface id="slot.footer">\n <ui.Text className="text-xs">Powered by My Extension</ui.Text>\n </Surface>'
1604
- );
1605
- }
1606
- if (targets.includes("slot.footer-links")) {
1607
- blocks.push(
1608
- ' <Surface id="slot.footer-links">\n <ui.FooterLink href="https://example.com">My Extension</ui.FooterLink>\n </Surface>'
1609
- );
1768
+ for (const match of fullFooter.matchAll(surfaceBlockRegex)) {
1769
+ if (targets.includes(match[1])) {
1770
+ blocks.push(` ${match[0]}`);
1771
+ }
1610
1772
  }
1611
1773
  return `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
1612
1774
 
@@ -1621,60 +1783,26 @@ ${blocks.join("\n")}
1621
1783
  };
1622
1784
  var generateSurfaceFiles = async (rootDir, targets) => {
1623
1785
  const surfaceDir = join(rootDir, "packages/extension/src/surfaces");
1624
- const wantsHeader = targets.includes("slot.header");
1625
- const wantsContent = targets.includes("slot.content");
1626
- const wantsFooter = targets.includes("slot.footer") || targets.includes("slot.footer-links");
1786
+ const wantsHeader = targets.includes(SURFACE_TARGETS.HEADER);
1787
+ const wantsContent = targets.includes(SURFACE_TARGETS.CONTENT);
1788
+ const wantsFooter = targets.includes(SURFACE_TARGETS.FOOTER) || targets.includes(SURFACE_TARGETS.FOOTER_LINKS);
1627
1789
  await upsertOrRemove(
1628
1790
  join(surfaceDir, "Header.tsx"),
1629
1791
  wantsHeader,
1630
- `import { ui, Surface } from '@stackable-labs/sdk-extension-react'
1631
-
1632
- export function Header() {
1633
- return (
1634
- <Surface id="slot.header">
1635
- <ui.Text>Header content goes here</ui.Text>
1636
- </Surface>
1637
- )
1638
- }
1792
+ `${getPattern("Simple Surface")}
1639
1793
  `
1640
1794
  );
1641
1795
  await upsertOrRemove(
1642
1796
  join(surfaceDir, "Content.tsx"),
1643
1797
  wantsContent,
1644
- `import { ui, useStore, useContextData, Surface } from '@stackable-labs/sdk-extension-react'
1645
- import { appStore } from '../store'
1646
-
1647
- export function Content() {
1648
- const viewState = useStore(appStore, (s) => s.viewState)
1649
- const { loading } = useContextData()
1650
-
1651
- if (loading) {
1652
- return (
1653
- <Surface id="slot.content">
1654
- <ui.Stack direction="column" gap="2" className="animate-pulse">
1655
- <ui.Card className="h-24" />
1656
- <ui.Card className="h-32" />
1657
- </ui.Stack>
1658
- </Surface>
1659
- )
1660
- }
1661
-
1662
- return (
1663
- <Surface id="slot.content">
1664
- {viewState.type === 'menu' && (
1665
- <ui.Menu>
1666
- {/* Add ui.MenuItem entries here */}
1667
- </ui.Menu>
1668
- )}
1669
- </Surface>
1670
- )
1671
- }
1798
+ `${getPattern("Content Surface with Loading State")}
1672
1799
  `
1673
1800
  );
1674
1801
  await upsertOrRemove(
1675
1802
  join(rootDir, "packages/extension/src/store.ts"),
1676
1803
  wantsContent,
1677
- "import { createStore } from '@stackable-labs/sdk-extension-react'\n\nexport type ViewState = { type: 'menu' }\n\nexport interface AppState {\n viewState: ViewState\n}\n\nexport const appStore = createStore<AppState>({\n viewState: { type: 'menu' },\n})\n"
1804
+ `${getPattern("Store-Based Navigation")}
1805
+ `
1678
1806
  );
1679
1807
  await upsertOrRemove(join(surfaceDir, "Footer.tsx"), wantsFooter, buildFooterSurface(targets));
1680
1808
  };
@@ -1682,15 +1810,15 @@ var rewriteExtensionIndex = async (rootDir, extensionId, targets) => {
1682
1810
  const indexPath = join(rootDir, "packages/extension/src/index.tsx");
1683
1811
  const imports = ["import { createExtension } from '@stackable-labs/sdk-extension-react'"];
1684
1812
  const components = [];
1685
- if (targets.includes("slot.header")) {
1813
+ if (targets.includes(SURFACE_TARGETS.HEADER)) {
1686
1814
  imports.push("import { Header } from './surfaces/Header'");
1687
1815
  components.push(" <Header />");
1688
1816
  }
1689
- if (targets.includes("slot.content")) {
1817
+ if (targets.includes(SURFACE_TARGETS.CONTENT)) {
1690
1818
  imports.push("import { Content } from './surfaces/Content'");
1691
1819
  components.push(" <Content />");
1692
1820
  }
1693
- if (targets.includes("slot.footer") || targets.includes("slot.footer-links")) {
1821
+ if (targets.includes(SURFACE_TARGETS.FOOTER) || targets.includes(SURFACE_TARGETS.FOOTER_LINKS)) {
1694
1822
  imports.push("import { Footer } from './surfaces/Footer'");
1695
1823
  components.push(" <Footer />");
1696
1824
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackable-labs/cli-app-extension",
3
- "version": "1.77.0",
3
+ "version": "1.78.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "LICENSE"
13
13
  ],
14
14
  "dependencies": {
15
+ "@stackable-labs/sdk-extension-ai-docs": "workspace:*",
15
16
  "adm-zip": "0.x",
16
17
  "clipboardy": "5.x",
17
18
  "cloudflared": "0.x",