@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.
- package/dist/index.js +189 -61
- 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: [
|
|
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: [
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
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(
|
|
1625
|
-
const wantsContent = targets.includes(
|
|
1626
|
-
const wantsFooter = targets.includes(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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",
|