@growthub/cli 0.13.7 → 0.13.9
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/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/codex-sites/route.js +13 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +98 -34
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/swarm-condition/route.js +106 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceContributionGraph.jsx +119 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +357 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensPanel.jsx +488 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceLensWalkthrough.jsx +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +105 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +37 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +382 -32
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/codex-sites-data-model-card.jsx +81 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +31 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/settings-accordion-section.jsx +50 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +192 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-lens/page.jsx +76 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +140 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-local-state.js +139 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-workspace-adapter.js +156 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +1025 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper-apply.js +24 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
- package/dist/index.js +5224 -5225
- package/package.json +1 -1
|
@@ -36,14 +36,18 @@ import Link from "next/link";
|
|
|
36
36
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
37
37
|
import {
|
|
38
38
|
Archive,
|
|
39
|
+
BarChart3,
|
|
39
40
|
ChevronDown,
|
|
40
41
|
ChevronRight,
|
|
42
|
+
Database,
|
|
43
|
+
Eye,
|
|
41
44
|
Folder,
|
|
42
45
|
FolderPlus,
|
|
43
46
|
GitBranch,
|
|
44
47
|
Home,
|
|
45
48
|
LayoutDashboard,
|
|
46
49
|
MessageCircle,
|
|
50
|
+
Wrench,
|
|
47
51
|
MessageCirclePlus,
|
|
48
52
|
MoreHorizontal,
|
|
49
53
|
MoreVertical,
|
|
@@ -66,6 +70,34 @@ import {
|
|
|
66
70
|
nextNavItemId,
|
|
67
71
|
} from "@/lib/workspace-helper-apply";
|
|
68
72
|
import { listAvailableWorkflows } from "@/lib/nav-workflows";
|
|
73
|
+
import { deriveWorkspaceActivationState, deriveLensWalkthroughState, LENS_WALKTHROUGH_DISMISS_FLAG } from "@/lib/workspace-activation";
|
|
74
|
+
import { WorkspaceLensWalkthrough } from "./components/WorkspaceLensWalkthrough.jsx";
|
|
75
|
+
import { isHelperConfigured, WorkspaceHelperSetupModal } from "./components/WorkspaceHelperSetupModal.jsx";
|
|
76
|
+
|
|
77
|
+
// Set a flag on the governed workspace-ui-cache "activation" row (pure
|
|
78
|
+
// transform) — the same row the onboarding dismiss persists to. Returned
|
|
79
|
+
// config is PATCHed via the existing /api/workspace boundary.
|
|
80
|
+
function withUiCacheFlag(config, key, value) {
|
|
81
|
+
const dataModel = config?.dataModel && typeof config.dataModel === "object" ? config.dataModel : {};
|
|
82
|
+
const objects = Array.isArray(dataModel.objects) ? dataModel.objects : [];
|
|
83
|
+
const existing = objects.find((o) => o?.id === "workspace-ui-cache");
|
|
84
|
+
const cacheObject = existing || {
|
|
85
|
+
id: "workspace-ui-cache", label: "Workspace UI Cache", source: "Workspace UI Cache",
|
|
86
|
+
objectType: "custom", icon: "Settings", columns: ["id", key], rows: [],
|
|
87
|
+
binding: { mode: "manual", source: "Workspace UI Cache" },
|
|
88
|
+
};
|
|
89
|
+
const columns = Array.from(new Set([...(Array.isArray(cacheObject.columns) ? cacheObject.columns : ["id"]), key]));
|
|
90
|
+
const rows = Array.isArray(cacheObject.rows) ? cacheObject.rows : [];
|
|
91
|
+
const hasRow = rows.some((r) => r?.id === "activation");
|
|
92
|
+
const nextRows = hasRow
|
|
93
|
+
? rows.map((r) => (r?.id === "activation" ? { ...r, [key]: value } : r))
|
|
94
|
+
: [...rows, { id: "activation", [key]: value }];
|
|
95
|
+
const nextCache = { ...cacheObject, columns, rows: nextRows };
|
|
96
|
+
const nextObjects = existing
|
|
97
|
+
? objects.map((o) => (o?.id === "workspace-ui-cache" ? nextCache : o))
|
|
98
|
+
: [...objects, nextCache];
|
|
99
|
+
return { ...config, dataModel: { ...dataModel, objects: nextObjects } };
|
|
100
|
+
}
|
|
69
101
|
import { ICON_PICKER_SET, LucideIcon } from "./data-model/components/dm-shared.jsx";
|
|
70
102
|
|
|
71
103
|
function textColorForAccent(accent) {
|
|
@@ -1504,6 +1536,40 @@ export function WorkspaceRail({
|
|
|
1504
1536
|
const workspaceName = branding.name || workspaceConfig?.name || "Growthub Workspace";
|
|
1505
1537
|
const pathname = usePathname() || "/";
|
|
1506
1538
|
const router = useRouter();
|
|
1539
|
+
// Workspace Lens unlocks only after the primary activation loop completes —
|
|
1540
|
+
// onboarding first, operating surface second. Derived from the same config
|
|
1541
|
+
// the rail already holds, so the gate is consistent across every page.
|
|
1542
|
+
const lensUnlocked = useMemo(
|
|
1543
|
+
() => Boolean(deriveWorkspaceActivationState({ workspaceConfig: workspaceConfig || {} }).complete),
|
|
1544
|
+
[workspaceConfig],
|
|
1545
|
+
);
|
|
1546
|
+
// One-time Workspace Lens reveal: shown anchored to the (newly visible) lens
|
|
1547
|
+
// nav item only in the in-between state, and never on the lens page itself.
|
|
1548
|
+
const lensWalkthrough = useMemo(
|
|
1549
|
+
() => deriveLensWalkthroughState({ workspaceConfig: workspaceConfig || {} }),
|
|
1550
|
+
[workspaceConfig],
|
|
1551
|
+
);
|
|
1552
|
+
const showLensReveal = lensWalkthrough.show && pathname === "/";
|
|
1553
|
+
const lensNavRef = useRef(null);
|
|
1554
|
+
const [lensRevealStyle, setLensRevealStyle] = useState(null);
|
|
1555
|
+
const dismissLensWalkthrough = useCallback(async () => {
|
|
1556
|
+
const next = withUiCacheFlag(workspaceConfig || {}, LENS_WALKTHROUGH_DISMISS_FLAG, true);
|
|
1557
|
+
if (onConfigChange) onConfigChange(next);
|
|
1558
|
+
try {
|
|
1559
|
+
const res = await fetch("/api/workspace", {
|
|
1560
|
+
method: "PATCH",
|
|
1561
|
+
headers: { "content-type": "application/json" },
|
|
1562
|
+
body: JSON.stringify({ dataModel: next.dataModel }),
|
|
1563
|
+
});
|
|
1564
|
+
const body = await res.json();
|
|
1565
|
+
if (body?.workspaceConfig && onConfigChange) onConfigChange(body.workspaceConfig);
|
|
1566
|
+
} catch {
|
|
1567
|
+
/* optimistic update already applied; cache write is best-effort */
|
|
1568
|
+
}
|
|
1569
|
+
}, [workspaceConfig, onConfigChange]);
|
|
1570
|
+
const enterLensWalkthrough = useCallback(() => {
|
|
1571
|
+
router.push("/workspace-lens?walkthrough=2");
|
|
1572
|
+
}, [router]);
|
|
1507
1573
|
|
|
1508
1574
|
const [activeTab, setActiveTab] = useState("home");
|
|
1509
1575
|
const [railCollapsed, setRailCollapsed] = useState(Boolean(defaultCollapsed));
|
|
@@ -1512,6 +1578,7 @@ export function WorkspaceRail({
|
|
|
1512
1578
|
const [renameDraft, setRenameDraft] = useState("");
|
|
1513
1579
|
const [chatSearch, setChatSearch] = useState("");
|
|
1514
1580
|
const [chatExpanded, setChatExpanded] = useState(false);
|
|
1581
|
+
const [helperSetupOpen, setHelperSetupOpen] = useState(false);
|
|
1515
1582
|
const menuWrapRef = useRef(null);
|
|
1516
1583
|
const CHAT_PREVIEW_COUNT = 10;
|
|
1517
1584
|
|
|
@@ -1525,6 +1592,31 @@ export function WorkspaceRail({
|
|
|
1525
1592
|
return () => document.removeEventListener("pointerdown", onPointerDown);
|
|
1526
1593
|
}, [openMenuId]);
|
|
1527
1594
|
|
|
1595
|
+
useEffect(() => {
|
|
1596
|
+
if (!showLensReveal) {
|
|
1597
|
+
setLensRevealStyle(null);
|
|
1598
|
+
return undefined;
|
|
1599
|
+
}
|
|
1600
|
+
const updatePosition = () => {
|
|
1601
|
+
const rect = lensNavRef.current?.getBoundingClientRect();
|
|
1602
|
+
if (!rect) return;
|
|
1603
|
+
const width = 264;
|
|
1604
|
+
const gap = 12;
|
|
1605
|
+
const minLeft = 16;
|
|
1606
|
+
const maxLeft = Math.max(minLeft, window.innerWidth - width - 16);
|
|
1607
|
+
const left = Math.min(Math.max(rect.right + gap, minLeft), maxLeft);
|
|
1608
|
+
const top = Math.max(16, Math.round(rect.top - 4));
|
|
1609
|
+
setLensRevealStyle({ left, top });
|
|
1610
|
+
};
|
|
1611
|
+
updatePosition();
|
|
1612
|
+
window.addEventListener("resize", updatePosition);
|
|
1613
|
+
window.addEventListener("scroll", updatePosition, true);
|
|
1614
|
+
return () => {
|
|
1615
|
+
window.removeEventListener("resize", updatePosition);
|
|
1616
|
+
window.removeEventListener("scroll", updatePosition, true);
|
|
1617
|
+
};
|
|
1618
|
+
}, [showLensReveal, railCollapsed]);
|
|
1619
|
+
|
|
1528
1620
|
const threads = useMemo(() => getHelperThreadRows(workspaceConfig), [workspaceConfig]);
|
|
1529
1621
|
|
|
1530
1622
|
useEffect(() => {
|
|
@@ -1538,6 +1630,10 @@ export function WorkspaceRail({
|
|
|
1538
1630
|
}, [railCollapsed]);
|
|
1539
1631
|
|
|
1540
1632
|
const handleAskHelperClick = () => {
|
|
1633
|
+
if (!isHelperConfigured(workspaceConfig)) {
|
|
1634
|
+
setHelperSetupOpen(true);
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1541
1637
|
if (onOpenHelper) {
|
|
1542
1638
|
onOpenHelper();
|
|
1543
1639
|
return;
|
|
@@ -1749,24 +1845,41 @@ export function WorkspaceRail({
|
|
|
1749
1845
|
<div className="workspace-rail-activation-slot">{activationSlot}</div>
|
|
1750
1846
|
) : null}
|
|
1751
1847
|
{dashboardsSlot ?? (
|
|
1752
|
-
<Link href="/" className={pathname === "/" ? "active" : undefined}>
|
|
1753
|
-
|
|
1848
|
+
<Link href="/" title="Builder" className={pathname === "/" ? "active" : undefined}>
|
|
1849
|
+
<Wrench size={15} aria-hidden="true" />
|
|
1850
|
+
<span className="workspace-nav-label">Builder</span>
|
|
1754
1851
|
</Link>
|
|
1755
1852
|
)}
|
|
1853
|
+
{lensUnlocked ? (
|
|
1854
|
+
<div className="workspace-rail-lens-nav" ref={lensNavRef}>
|
|
1855
|
+
<Link
|
|
1856
|
+
href="/workspace-lens"
|
|
1857
|
+
title="Workspace Lens"
|
|
1858
|
+
className={pathname.startsWith("/workspace-lens") ? "active" : undefined}
|
|
1859
|
+
>
|
|
1860
|
+
<Eye size={15} aria-hidden="true" />
|
|
1861
|
+
<span className="workspace-nav-label">Workspace Lens</span>
|
|
1862
|
+
</Link>
|
|
1863
|
+
</div>
|
|
1864
|
+
) : null}
|
|
1756
1865
|
{dataModelSlot ?? (
|
|
1757
1866
|
<Link
|
|
1758
1867
|
href="/data-model"
|
|
1868
|
+
title="Management"
|
|
1759
1869
|
className={pathname.startsWith("/data-model") ? "active" : undefined}
|
|
1760
1870
|
>
|
|
1761
|
-
|
|
1871
|
+
<Database size={15} aria-hidden="true" />
|
|
1872
|
+
<span className="workspace-nav-label">Management</span>
|
|
1762
1873
|
</Link>
|
|
1763
1874
|
)}
|
|
1764
1875
|
{settingsSlot ?? (
|
|
1765
1876
|
<Link
|
|
1766
1877
|
href="/settings/general"
|
|
1878
|
+
title="Workspace Settings"
|
|
1767
1879
|
className={"workspace-nav-bottom" + (pathname.startsWith("/settings") ? " active" : "")}
|
|
1768
1880
|
>
|
|
1769
|
-
|
|
1881
|
+
<Settings size={15} aria-hidden="true" />
|
|
1882
|
+
<span className="workspace-nav-label">Workspace Settings</span>
|
|
1770
1883
|
</Link>
|
|
1771
1884
|
)}
|
|
1772
1885
|
</nav>
|
|
@@ -1934,6 +2047,29 @@ export function WorkspaceRail({
|
|
|
1934
2047
|
<span className="status-dot" />
|
|
1935
2048
|
{authority || "local-catalog"}
|
|
1936
2049
|
</div>
|
|
2050
|
+
{showLensReveal && lensRevealStyle && typeof document !== "undefined"
|
|
2051
|
+
? createPortal(
|
|
2052
|
+
<WorkspaceLensWalkthrough
|
|
2053
|
+
step={1}
|
|
2054
|
+
className="is-rail-reveal"
|
|
2055
|
+
style={lensRevealStyle}
|
|
2056
|
+
onPrimary={enterLensWalkthrough}
|
|
2057
|
+
onDismiss={dismissLensWalkthrough}
|
|
2058
|
+
/>,
|
|
2059
|
+
document.body,
|
|
2060
|
+
)
|
|
2061
|
+
: null}
|
|
2062
|
+
<WorkspaceHelperSetupModal
|
|
2063
|
+
workspaceConfig={workspaceConfig}
|
|
2064
|
+
open={helperSetupOpen}
|
|
2065
|
+
onClose={() => setHelperSetupOpen(false)}
|
|
2066
|
+
onSaved={(nextConfig) => {
|
|
2067
|
+
setHelperSetupOpen(false);
|
|
2068
|
+
if (onConfigChange) onConfigChange(nextConfig);
|
|
2069
|
+
if (onOpenHelper) onOpenHelper();
|
|
2070
|
+
else router.push("/data-model?helper=open");
|
|
2071
|
+
}}
|
|
2072
|
+
/>
|
|
1937
2073
|
</aside>
|
|
1938
2074
|
);
|
|
1939
2075
|
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalizeCodexSiteRecord } from "./codex-sites-workspace-adapter.js";
|
|
5
|
+
|
|
6
|
+
const MAX_SESSION_FILES = 16;
|
|
7
|
+
|
|
8
|
+
function resolveCodexHome() {
|
|
9
|
+
return process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isPlainObject(value) {
|
|
13
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function stripSensitiveProjectFields(project) {
|
|
17
|
+
if (!isPlainObject(project)) return null;
|
|
18
|
+
const {
|
|
19
|
+
source_repository_credential: _sourceRepositoryCredential,
|
|
20
|
+
access_policy: _accessPolicy,
|
|
21
|
+
auth_client_id: _authClientId,
|
|
22
|
+
...safeProject
|
|
23
|
+
} = project;
|
|
24
|
+
return safeProject;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseJsonMaybe(value) {
|
|
28
|
+
if (isPlainObject(value)) return value;
|
|
29
|
+
if (typeof value !== "string") return null;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
if (!trimmed) return null;
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(trimmed);
|
|
34
|
+
} catch {
|
|
35
|
+
const marker = "Output:";
|
|
36
|
+
const index = trimmed.lastIndexOf(marker);
|
|
37
|
+
if (index === -1) return null;
|
|
38
|
+
const candidate = trimmed.slice(index + marker.length).trim();
|
|
39
|
+
try {
|
|
40
|
+
return JSON.parse(candidate);
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function collectStructuredPayloads(line) {
|
|
48
|
+
const parsed = parseJsonMaybe(line);
|
|
49
|
+
if (!isPlainObject(parsed)) return [];
|
|
50
|
+
const payload = parsed.payload;
|
|
51
|
+
const values = [];
|
|
52
|
+
const structured = payload?.result?.Ok?.structuredContent;
|
|
53
|
+
if (isPlainObject(structured)) values.push(structured);
|
|
54
|
+
const outputPayload = parseJsonMaybe(payload?.output);
|
|
55
|
+
if (isPlainObject(outputPayload)) values.push(outputPayload);
|
|
56
|
+
return values;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function mergeCodexSitePayload(byProjectId, payload) {
|
|
60
|
+
if (!isPlainObject(payload)) return;
|
|
61
|
+
const isProject =
|
|
62
|
+
typeof payload.id === "string" &&
|
|
63
|
+
payload.id.startsWith("appgprj_") &&
|
|
64
|
+
(payload.current_live_url !== undefined || payload.slug || payload.title);
|
|
65
|
+
const isDeployment =
|
|
66
|
+
typeof payload.project_id === "string" &&
|
|
67
|
+
typeof payload.url === "string" &&
|
|
68
|
+
payload.url.startsWith("http");
|
|
69
|
+
if (!isProject && !isDeployment) return;
|
|
70
|
+
|
|
71
|
+
const projectId = isProject ? payload.id : payload.project_id;
|
|
72
|
+
const current = byProjectId.get(projectId) || { id: projectId };
|
|
73
|
+
const next = isProject
|
|
74
|
+
? { ...current, ...stripSensitiveProjectFields(payload) }
|
|
75
|
+
: {
|
|
76
|
+
...current,
|
|
77
|
+
id: projectId,
|
|
78
|
+
title: payload.title || current.title,
|
|
79
|
+
current_live_url: payload.url,
|
|
80
|
+
status: payload.status === "succeeded" ? "live" : payload.status || current.status,
|
|
81
|
+
updated_at: payload.updated_at || current.updated_at,
|
|
82
|
+
dashboardId: current.dashboardId || projectId,
|
|
83
|
+
deployment_id: payload.id,
|
|
84
|
+
version_id: payload.version_id
|
|
85
|
+
};
|
|
86
|
+
byProjectId.set(projectId, next);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function listSessionFiles(root) {
|
|
90
|
+
const entries = [];
|
|
91
|
+
async function visit(dir) {
|
|
92
|
+
let children;
|
|
93
|
+
try {
|
|
94
|
+
children = await fs.readdir(dir, { withFileTypes: true });
|
|
95
|
+
} catch {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
await Promise.all(children.map(async (entry) => {
|
|
99
|
+
const fullPath = path.join(dir, entry.name);
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
await visit(fullPath);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (entry.isFile() && entry.name.endsWith(".jsonl")) {
|
|
105
|
+
const stat = await fs.stat(fullPath).catch(() => null);
|
|
106
|
+
if (stat) entries.push({ path: fullPath, mtimeMs: stat.mtimeMs });
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
}
|
|
110
|
+
await visit(root);
|
|
111
|
+
return entries
|
|
112
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs)
|
|
113
|
+
.slice(0, MAX_SESSION_FILES)
|
|
114
|
+
.map((entry) => entry.path);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function listLocalCodexSites() {
|
|
118
|
+
const codexHome = resolveCodexHome();
|
|
119
|
+
const sessionRoot = path.join(codexHome, "sessions");
|
|
120
|
+
const files = await listSessionFiles(sessionRoot);
|
|
121
|
+
const byProjectId = new Map();
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
const raw = await fs.readFile(file, "utf8").catch(() => "");
|
|
124
|
+
if (!raw.includes("mcp__codex_apps__sites") && !raw.includes("sites_") && !raw.includes("appgprj_")) continue;
|
|
125
|
+
raw.split(/\r?\n/).forEach((line) => {
|
|
126
|
+
collectStructuredPayloads(line).forEach((payload) => mergeCodexSitePayload(byProjectId, payload));
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return Array.from(byProjectId.values())
|
|
130
|
+
.map((site) => normalizeCodexSiteRecord({
|
|
131
|
+
...site,
|
|
132
|
+
url: site.current_live_url || site.url,
|
|
133
|
+
accessMode: site.access_mode,
|
|
134
|
+
dashboardId: site.dashboardId || site.slug || site.id
|
|
135
|
+
}))
|
|
136
|
+
.filter((site) => site.url);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export { listLocalCodexSites };
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
const CODEX_SITES_OBJECT_ID = "workspace-codex-sites";
|
|
2
|
+
const CODEX_SITES_COLUMNS = [
|
|
3
|
+
"Name",
|
|
4
|
+
"app",
|
|
5
|
+
"client",
|
|
6
|
+
"url",
|
|
7
|
+
"status",
|
|
8
|
+
"accessMode",
|
|
9
|
+
"dashboardId",
|
|
10
|
+
"lastRecordedAt",
|
|
11
|
+
"notes"
|
|
12
|
+
];
|
|
13
|
+
const CODEX_SITES_SOURCE_ID = "codex-sites";
|
|
14
|
+
|
|
15
|
+
function isCodexSiteUrl(value) {
|
|
16
|
+
return /^https?:\/\//i.test(String(value || "").trim());
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function defaultAppSource(apps) {
|
|
20
|
+
const first = Array.isArray(apps) ? apps.find((app) => app?.source) : null;
|
|
21
|
+
return first?.source || "apps/workspace";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createCodexSitesObject(apps = []) {
|
|
25
|
+
return {
|
|
26
|
+
id: CODEX_SITES_OBJECT_ID,
|
|
27
|
+
label: "Codex Sites",
|
|
28
|
+
source: "Workspace Apps",
|
|
29
|
+
sourceId: CODEX_SITES_OBJECT_ID,
|
|
30
|
+
objectType: "custom",
|
|
31
|
+
icon: "Rocket",
|
|
32
|
+
columns: CODEX_SITES_COLUMNS,
|
|
33
|
+
rows: [],
|
|
34
|
+
binding: {
|
|
35
|
+
mode: "manual",
|
|
36
|
+
source: "Settings / Apps",
|
|
37
|
+
sourceType: "workspace-data-model",
|
|
38
|
+
sourceAuthority: "workspace-config",
|
|
39
|
+
objectId: CODEX_SITES_OBJECT_ID,
|
|
40
|
+
sourceId: CODEX_SITES_SOURCE_ID,
|
|
41
|
+
entityType: "codex-site",
|
|
42
|
+
app: defaultAppSource(apps)
|
|
43
|
+
},
|
|
44
|
+
fieldSettings: {
|
|
45
|
+
hidden: [],
|
|
46
|
+
order: CODEX_SITES_COLUMNS,
|
|
47
|
+
views: [
|
|
48
|
+
{
|
|
49
|
+
id: "codex-sites-live",
|
|
50
|
+
name: "Live",
|
|
51
|
+
favorite: true,
|
|
52
|
+
order: CODEX_SITES_COLUMNS,
|
|
53
|
+
filter: { op: "and", clauses: [{ fieldId: "status", operator: "eq", value: "live" }] }
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: "codex-sites-review",
|
|
57
|
+
name: "Draft & Review",
|
|
58
|
+
order: CODEX_SITES_COLUMNS,
|
|
59
|
+
filter: { op: "or", clauses: [
|
|
60
|
+
{ fieldId: "status", operator: "eq", value: "draft" },
|
|
61
|
+
{ fieldId: "status", operator: "eq", value: "review" }
|
|
62
|
+
] }
|
|
63
|
+
}
|
|
64
|
+
],
|
|
65
|
+
activeViewId: "codex-sites-live",
|
|
66
|
+
types: {
|
|
67
|
+
Name: "text",
|
|
68
|
+
app: "text",
|
|
69
|
+
client: "text",
|
|
70
|
+
url: "url",
|
|
71
|
+
status: "select",
|
|
72
|
+
accessMode: "select",
|
|
73
|
+
dashboardId: "text",
|
|
74
|
+
lastRecordedAt: "date",
|
|
75
|
+
notes: "text"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeCodexSiteRecord(record = {}) {
|
|
82
|
+
const url = String(record.url || record.liveUrl || record.current_live_url || "").trim();
|
|
83
|
+
return {
|
|
84
|
+
id: String(record.id || record.projectId || record.project_id || record.slug || url).trim(),
|
|
85
|
+
Name: String(record.Name || record.name || record.title || record.slug || "Codex Site").trim(),
|
|
86
|
+
app: String(record.app || record.source || "apps/workspace").trim(),
|
|
87
|
+
client: String(record.client || record.workspace || "Workspace").trim(),
|
|
88
|
+
url,
|
|
89
|
+
status: String(record.status || (url ? "live" : "draft")).trim(),
|
|
90
|
+
accessMode: String(record.accessMode || record.access_mode || "workspace").trim(),
|
|
91
|
+
dashboardId: String(record.dashboardId || record.dashboard_id || record.slug || record.id || "").trim(),
|
|
92
|
+
lastRecordedAt: String(record.lastRecordedAt || record.updated_at || record.created_at || "").trim(),
|
|
93
|
+
notes: String(record.notes || record.description || "").trim()
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function codexSiteRecordToRow(record = {}) {
|
|
98
|
+
const site = normalizeCodexSiteRecord(record);
|
|
99
|
+
return {
|
|
100
|
+
Name: site.Name,
|
|
101
|
+
app: site.app,
|
|
102
|
+
client: site.client,
|
|
103
|
+
url: site.url,
|
|
104
|
+
status: site.status,
|
|
105
|
+
accessMode: site.accessMode,
|
|
106
|
+
dashboardId: site.dashboardId,
|
|
107
|
+
lastRecordedAt: site.lastRecordedAt || new Date().toISOString(),
|
|
108
|
+
notes: site.notes
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function recordsFromSourceEntry(entry) {
|
|
113
|
+
if (Array.isArray(entry)) return entry;
|
|
114
|
+
if (Array.isArray(entry?.records)) return entry.records;
|
|
115
|
+
if (Array.isArray(entry?.sites)) return entry.sites;
|
|
116
|
+
if (Array.isArray(entry?.items)) return entry.items;
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function listAvailableCodexSites(workspaceConfig = {}, workspaceSourceRecords = {}) {
|
|
121
|
+
const sidecarRecords = [
|
|
122
|
+
...recordsFromSourceEntry(workspaceSourceRecords?.[CODEX_SITES_SOURCE_ID]),
|
|
123
|
+
...recordsFromSourceEntry(workspaceSourceRecords?.[CODEX_SITES_OBJECT_ID])
|
|
124
|
+
];
|
|
125
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
126
|
+
const object = objects.find((item) => item?.id === CODEX_SITES_OBJECT_ID);
|
|
127
|
+
const rowRecords = Array.isArray(object?.rows) ? object.rows : [];
|
|
128
|
+
const byUrl = new Map();
|
|
129
|
+
[...sidecarRecords, ...rowRecords].forEach((record) => {
|
|
130
|
+
const site = normalizeCodexSiteRecord(record);
|
|
131
|
+
if (!isCodexSiteUrl(site.url)) return;
|
|
132
|
+
byUrl.set(site.url, site);
|
|
133
|
+
});
|
|
134
|
+
return Array.from(byUrl.values());
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function ensureCodexSitesDataModel(dataModel, apps = []) {
|
|
138
|
+
const objects = Array.isArray(dataModel?.objects) ? dataModel.objects : [];
|
|
139
|
+
if (objects.some((object) => object?.id === CODEX_SITES_OBJECT_ID)) return dataModel || {};
|
|
140
|
+
return {
|
|
141
|
+
...(dataModel || {}),
|
|
142
|
+
objects: [...objects, createCodexSitesObject(apps)]
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export {
|
|
147
|
+
CODEX_SITES_COLUMNS,
|
|
148
|
+
CODEX_SITES_OBJECT_ID,
|
|
149
|
+
CODEX_SITES_SOURCE_ID,
|
|
150
|
+
codexSiteRecordToRow,
|
|
151
|
+
createCodexSitesObject,
|
|
152
|
+
ensureCodexSitesDataModel,
|
|
153
|
+
isCodexSiteUrl,
|
|
154
|
+
listAvailableCodexSites,
|
|
155
|
+
normalizeCodexSiteRecord
|
|
156
|
+
};
|