@growthub/cli 0.13.8 → 0.14.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 (32) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/codex-sites/route.js +13 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +130 -5
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +5 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +501 -5
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +75 -55
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +215 -13
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/codex-sites-data-model-card.jsx +81 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +31 -14
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/settings-accordion-section.jsx +50 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +176 -5
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +137 -5
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-local-state.js +139 -0
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/codex-sites-workspace-adapter.js +156 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +63 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +7 -1
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
  32. package/package.json +1 -1
@@ -3,6 +3,8 @@ import { promises as fs } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { readWorkspaceConfig } from "@/lib/workspace-config";
5
5
  import { AppsList } from "./apps-list.jsx";
6
+ import { CodexSitesDataModelCard } from "./codex-sites-data-model-card.jsx";
7
+ import { SettingsAccordionGroup, SettingsAccordionSection } from "./settings-accordion-section.jsx";
6
8
 
7
9
  async function readForkMetadata() {
8
10
  try {
@@ -82,24 +84,39 @@ async function AppsSettingsPage() {
82
84
  <div className="workspace-settings-card-heading">
83
85
  <div>
84
86
  <h2>Apps</h2>
85
- <p>Read-only workspace app, bridge, and fork metadata already available to this workspace.</p>
87
+ <p>Workspace apps discovered from the local apps directory and governed Data Model configuration.</p>
86
88
  </div>
87
89
  </div>
88
90
 
89
- <section className="workspace-settings-section workspace-apps-linkage-section">
90
- <h3>Workspace Linkage</h3>
91
- <div className="workspace-settings-kv">
92
- <span>Workspace</span><code>{workspaceConfig.id || "workspace-builder-default"}</code>
93
- <span>Fork</span><code>{fork?.forkId || "local fork metadata unavailable"}</code>
94
- <span>Kit</span><code>{fork?.kitId || workspaceConfig.provenance?.mirrors || "growthub-custom-workspace-starter-v1"}</code>
95
- <span>Bridge</span><code>{bridge?.status || bridge?.id || "not connected"}</code>
96
- </div>
97
- </section>
91
+ <SettingsAccordionGroup defaultOpenId="workspace-apps">
92
+ <SettingsAccordionSection
93
+ id="workspace-apps"
94
+ title="Workspace Apps"
95
+ summary={`${apps.length} app${apps.length === 1 ? "" : "s"} discovered from directory and config.`}
96
+ className="workspace-apps-list-section"
97
+ >
98
+ <AppsList apps={apps} />
99
+ </SettingsAccordionSection>
100
+
101
+ <SettingsAccordionSection
102
+ id="workspace-linkage"
103
+ title="Workspace Linkage"
104
+ summary="Fork, kit, and bridge identity for this workspace."
105
+ className="workspace-apps-linkage-section"
106
+ >
107
+ <div className="workspace-settings-kv">
108
+ <span>Workspace</span><code>{workspaceConfig.id || "workspace-builder-default"}</code>
109
+ <span>Fork</span><code>{fork?.forkId || "local fork metadata unavailable"}</code>
110
+ <span>Kit</span><code>{fork?.kitId || workspaceConfig.provenance?.mirrors || "growthub-custom-workspace-starter-v1"}</code>
111
+ <span>Bridge</span><code>{bridge?.status || bridge?.id || "not connected"}</code>
112
+ </div>
113
+ </SettingsAccordionSection>
98
114
 
99
- <section className="workspace-settings-section workspace-apps-list-section">
100
- <h3>Workspace Apps</h3>
101
- <AppsList apps={apps} />
102
- </section>
115
+ <CodexSitesDataModelCard
116
+ apps={apps}
117
+ dataModel={workspaceConfig.dataModel || {}}
118
+ />
119
+ </SettingsAccordionGroup>
103
120
  </section>
104
121
  </SettingsShell>;
105
122
  }
@@ -0,0 +1,50 @@
1
+ "use client";
2
+
3
+ import { createContext, useContext, useState } from "react";
4
+ import { ChevronDown } from "lucide-react";
5
+
6
+ const SettingsAccordionContext = createContext(null);
7
+
8
+ function SettingsAccordionGroup({ defaultOpenId, children }) {
9
+ const [openId, setOpenId] = useState(defaultOpenId || null);
10
+ return <SettingsAccordionContext.Provider value={{ openId, setOpenId }}>
11
+ {children}
12
+ </SettingsAccordionContext.Provider>;
13
+ }
14
+
15
+ function SettingsAccordionSection({ id, title, summary, className = "", defaultOpen = false, children }) {
16
+ const group = useContext(SettingsAccordionContext);
17
+ const [localOpen, setLocalOpen] = useState(defaultOpen);
18
+ const open = group ? group.openId === id : localOpen;
19
+ const sectionClass = [
20
+ "workspace-settings-section",
21
+ "workspace-settings-accordion",
22
+ open ? "is-open" : "is-collapsed",
23
+ className
24
+ ].filter(Boolean).join(" ");
25
+ const toggle = () => {
26
+ if (group) {
27
+ group.setOpenId(open ? null : id);
28
+ return;
29
+ }
30
+ setLocalOpen((value) => !value);
31
+ };
32
+
33
+ return <section className={sectionClass}>
34
+ <button
35
+ type="button"
36
+ className="workspace-settings-accordion-trigger"
37
+ aria-expanded={open}
38
+ onClick={toggle}
39
+ >
40
+ <span>
41
+ <h3>{title}</h3>
42
+ {summary ? <em>{summary}</em> : null}
43
+ </span>
44
+ <ChevronDown size={16} aria-hidden="true" />
45
+ </button>
46
+ {open ? <div className="workspace-settings-accordion-body">{children}</div> : null}
47
+ </section>;
48
+ }
49
+
50
+ export { SettingsAccordionGroup, SettingsAccordionSection };
@@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useState } from "react";
4
4
  import Link from "next/link";
5
5
  import { useRouter, useSearchParams } from "next/navigation";
6
6
  import {
7
+ ArrowUpCircle,
7
8
  Bot,
8
9
  ChevronDown,
9
10
  ChevronUp,
@@ -51,7 +52,31 @@ import { AgentSwarmPanel } from "../data-model/components/AgentSwarmPanel.jsx";
51
52
  import { RunSetupPanel } from "./RunSetupPanel.jsx";
52
53
  import { describeRunInputMetadataItems, discoverRunInputSchema } from "@/lib/orchestration-run-inputs";
53
54
  import { selectWorkflowNodeInputSchema } from "@/lib/workspace-metadata-selectors";
54
- import { deriveProvenance, hasConnectionId } from "@/lib/workspace-activation";
55
+ import { deriveProvenance, hasConnectionId, readUiCacheFlag } from "@/lib/workspace-activation";
56
+ import { ApiRegistryCreationCockpit } from "../data-model/components/ApiRegistryCreationCockpit.jsx";
57
+ import { deriveSandboxServerlessState } from "@/lib/sandbox-serverless-flow";
58
+ import { deriveServerlessUpgradeState, SERVERLESS_UPGRADE_DISMISS_FLAG } from "@/lib/serverless-upgrade";
59
+
60
+ // Set a flag on the governed workspace-ui-cache "activation" row (pure helper,
61
+ // same transform the rail/lens one-time dismisses use).
62
+ function withUiCacheFlag(workspaceConfig, flag, value) {
63
+ const dm = workspaceConfig?.dataModel && typeof workspaceConfig.dataModel === "object" ? workspaceConfig.dataModel : {};
64
+ const objects = Array.isArray(dm.objects) ? dm.objects : [];
65
+ const existing = objects.find((o) => o?.id === "workspace-ui-cache");
66
+ const baseRow = (existing?.rows || []).find((r) => r?.id === "activation") || { id: "activation" };
67
+ const nextRow = { ...baseRow, [flag]: value };
68
+ const nextCache = existing
69
+ ? { ...existing, rows: [nextRow, ...(existing.rows || []).filter((r) => r?.id !== "activation")] }
70
+ : {
71
+ id: "workspace-ui-cache", label: "Workspace UI Cache", source: "Workspace UI Cache",
72
+ objectType: "custom", columns: ["id", flag], rows: [nextRow],
73
+ binding: { mode: "manual", source: "Workspace UI Cache" },
74
+ };
75
+ const nextObjects = existing
76
+ ? objects.map((o) => (o?.id === "workspace-ui-cache" ? nextCache : o))
77
+ : [...objects, nextCache];
78
+ return { ...workspaceConfig, dataModel: { ...dm, objects: nextObjects } };
79
+ }
55
80
 
56
81
  // Workspace Metadata Graph V1 — read-only dependency metadata for workflow
57
82
  // sidecars. The runtime path (sandbox-run, publish, draft/live) is
@@ -63,20 +88,33 @@ const WORKFLOW_METADATA_SELECTORS = Object.freeze({
63
88
  });
64
89
 
65
90
  function resolveRegistryRowForSandbox(workspaceConfig, sandboxRow) {
91
+ return resolveRegistryRefForSandbox(workspaceConfig, sandboxRow)?.row || null;
92
+ }
93
+
94
+ function resolveRegistryRefForSandbox(workspaceConfig, sandboxRow) {
66
95
  const graph = parseOrchestrationGraph(sandboxRow?.orchestrationConfig || sandboxRow?.orchestrationGraph);
67
96
  const apiNode = graph?.nodes?.find((n) => n?.type === "api-registry-call");
68
97
  const registryId = String(
69
98
  apiNode?.config?.registryId || apiNode?.config?.integrationId || sandboxRow?.schedulerRegistryId || ""
70
99
  ).trim();
71
- if (!registryId || !workspaceConfig) return null;
100
+ if (!workspaceConfig) return null;
72
101
  const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
102
+ let firstRegistryRow = null;
103
+ let firstRegistryObject = null;
73
104
  for (const objectItem of objects) {
74
105
  if (objectItem?.objectType !== "api-registry") continue;
75
106
  const rows = Array.isArray(objectItem.rows) ? objectItem.rows : [];
76
- const match = rows.find((r) => String(r?.integrationId || "").trim() === registryId);
77
- if (match) return match;
107
+ const firstRow = rows.find((r) => String(r?.integrationId || "").trim());
108
+ if (!firstRegistryRow && firstRow) {
109
+ firstRegistryRow = firstRow;
110
+ firstRegistryObject = objectItem;
111
+ }
112
+ if (registryId) {
113
+ const match = rows.find((r) => String(r?.integrationId || "").trim() === registryId);
114
+ if (match) return { object: objectItem, row: match };
115
+ }
78
116
  }
79
- return null;
117
+ return firstRegistryRow ? { object: firstRegistryObject, row: firstRegistryRow } : null;
80
118
  }
81
119
 
82
120
  function patchSandboxRowInConfig(workspaceConfig, objectId, rowIndex, fields) {
@@ -349,6 +387,23 @@ export default function WorkflowSurface() {
349
387
  const [orchestrationGraph, setOrchestrationGraph] = useState(null);
350
388
  const [dirty, setDirty] = useState(false);
351
389
  const [runSetupOpen, setRunSetupOpen] = useState(false);
390
+ const [upgradeOpen, setUpgradeOpen] = useState(false);
391
+ const [serverlessSignals, setServerlessSignals] = useState({ configuredEnvRefs: [], persistenceAdapters: [] });
392
+
393
+ useEffect(() => {
394
+ let cancelled = false;
395
+ fetch("/api/workspace/env-status", { cache: "no-store" })
396
+ .then((res) => (res.ok ? res.json() : {}))
397
+ .then((payload) => {
398
+ if (cancelled) return;
399
+ setServerlessSignals({
400
+ configuredEnvRefs: Array.isArray(payload.configuredEnvRefs) ? payload.configuredEnvRefs : [],
401
+ persistenceAdapters: Array.isArray(payload.persistenceAdapters) ? payload.persistenceAdapters : [],
402
+ });
403
+ })
404
+ .catch(() => {});
405
+ return () => { cancelled = true; };
406
+ }, [objectId, rowId]);
352
407
 
353
408
  const load = useCallback(async () => {
354
409
  setLoading(true);
@@ -807,6 +862,73 @@ export default function WorkflowSurface() {
807
862
  const showSaveDraft = dirty && !graphUnset;
808
863
  const workflowModeLabel = isDraftMode ? "draft" : lifecycle || "live";
809
864
 
865
+ // Serverless upgrade — same derivation + cockpit as the sandbox/API lanes.
866
+ const upgradeState = deriveServerlessUpgradeState(workspaceConfig || {}, {
867
+ dismissed: readUiCacheFlag(workspaceConfig || {}, SERVERLESS_UPGRADE_DISMISS_FLAG),
868
+ });
869
+ const serverlessState = sandboxRow
870
+ ? deriveSandboxServerlessState({
871
+ sandboxRow,
872
+ workspaceConfig: workspaceConfig || {},
873
+ configuredEnvRefs: serverlessSignals.configuredEnvRefs,
874
+ persistenceAdapters: serverlessSignals.persistenceAdapters,
875
+ })
876
+ : null;
877
+ const isServerlessWorkflow = Boolean(serverlessState?.isServerless);
878
+
879
+ async function patchSandboxAndPersist(fields) {
880
+ if (resolved.rowIndex < 0 || !objectId || !workspaceConfig) return;
881
+ try {
882
+ const next = patchSandboxRowInConfig(workspaceConfig, objectId, resolved.rowIndex, fields);
883
+ await persistWorkspace(next);
884
+ setSaveMessage(fields.runLocality === "serverless"
885
+ ? "Upgraded to serverless. Link a scheduler and configure persistence to close the loop."
886
+ : fields.runLocality === "local"
887
+ ? "Reverted to local execution."
888
+ : "Saved.");
889
+ } catch (err) {
890
+ setSaveMessage(err.message || "Failed to save");
891
+ }
892
+ }
893
+
894
+ async function handleUpgradeAction(action) {
895
+ if (!action) return;
896
+ if (action.id === "toggle-locality") {
897
+ if (isServerlessWorkflow) {
898
+ await patchSandboxAndPersist({ runLocality: "local" });
899
+ return;
900
+ }
901
+ const registryRow = resolveRegistryRowForSandbox(workspaceConfig, sandboxRow);
902
+ const adapterId = String(sandboxRow?.adapter || "").trim();
903
+ await patchSandboxAndPersist({
904
+ runLocality: "serverless",
905
+ schedulerRegistryId: String(registryRow?.integrationId || "").trim(),
906
+ adapter: ["local-agent-host", "local-intelligence"].includes(adapterId) ? "local-process" : adapterId,
907
+ });
908
+ } else if (action.id === "open-settings") {
909
+ router.push(action.href || "/settings");
910
+ } else if (action.id === "link-scheduler") {
911
+ const registryRef = resolveRegistryRefForSandbox(workspaceConfig, sandboxRow);
912
+ if (registryRef?.object?.id && registryRef?.row?.integrationId) {
913
+ router.push(`/data-model?object=${encodeURIComponent(registryRef.object.id)}&row=${encodeURIComponent(registryRef.row.integrationId)}`);
914
+ } else {
915
+ router.push(`/data-model?object=${encodeURIComponent(objectId)}&row=${encodeURIComponent(rowId)}`);
916
+ }
917
+ } else if (action.id === "edit-adapter") {
918
+ // Full scheduler/adapter config lives on the sandbox object's drawer.
919
+ router.push(`/data-model?object=${encodeURIComponent(objectId)}&row=${encodeURIComponent(rowId)}`);
920
+ }
921
+ }
922
+
923
+ async function dismissUpgradeOnboarding() {
924
+ if (!workspaceConfig) return;
925
+ try {
926
+ await persistWorkspace(withUiCacheFlag(workspaceConfig, SERVERLESS_UPGRADE_DISMISS_FLAG, true));
927
+ } catch {
928
+ /* non-fatal */
929
+ }
930
+ }
931
+
810
932
  return (
811
933
  <main className="workspace-builder dm-workflow-page">
812
934
  <WorkspaceRail
@@ -854,6 +976,18 @@ export default function WorkflowSurface() {
854
976
  >
855
977
  <ChevronUp size={14} />
856
978
  </button>
979
+ {sandboxRow && (
980
+ <button
981
+ type="button"
982
+ className={"dm-workflow-icon-btn dm-workflow-upgrade-btn" + (isServerlessWorkflow ? " is-serverless" : (upgradeState.showOnboarding ? " is-pulse" : ""))}
983
+ aria-label={isServerlessWorkflow ? "Serverless workflow — review persistence & scheduling" : "Upgrade to serverless environment to ensure persistence"}
984
+ data-tooltip={isServerlessWorkflow ? "Serverless — review persistence & scheduling" : "Upgrade to serverless environment to ensure persistence"}
985
+ aria-pressed={upgradeOpen}
986
+ onClick={() => setUpgradeOpen((open) => !open)}
987
+ >
988
+ <ArrowUpCircle size={14} />
989
+ </button>
990
+ )}
857
991
  {showDiscardDraft && (
858
992
  <button
859
993
  type="button"
@@ -920,6 +1054,43 @@ export default function WorkflowSurface() {
920
1054
  </div>
921
1055
  ) : null}
922
1056
 
1057
+ {/* One-time serverless upgrade onboarding — shows only when the operator
1058
+ has workflows but none are serverless, and hasn't dismissed it. */}
1059
+ {sandboxRow && !upgradeOpen && upgradeState.showOnboarding ? (
1060
+ <div className="workspace-template-context-banner dm-workflow-upgrade-nudge" role="note">
1061
+ <div>
1062
+ <strong>{upgradeState.headline}</strong>
1063
+ <span style={{ display: "block", marginTop: 2 }}>{upgradeState.subheadline}</span>
1064
+ </div>
1065
+ <div className="dm-workflow-upgrade-nudge-actions">
1066
+ <button type="button" className="dm-btn-primary-sm" onClick={() => setUpgradeOpen(true)}>
1067
+ <ArrowUpCircle size={13} /> Upgrade this workflow
1068
+ </button>
1069
+ <button type="button" className="dm-btn-ghost" onClick={dismissUpgradeOnboarding}>Not now</button>
1070
+ </div>
1071
+ </div>
1072
+ ) : null}
1073
+
1074
+ {/* Serverless cockpit — same derivation + cockpit interface as the API
1075
+ Registry and sandbox lanes. Toggles patch the sandbox row; deep config
1076
+ (scheduler/adapter) routes to the object's Data Model drawer. */}
1077
+ {sandboxRow && upgradeOpen && serverlessState ? (
1078
+ <div className="dm-workflow-upgrade-panel">
1079
+ <div className="dm-workflow-upgrade-panel-head">
1080
+ <span className="dm-api-action-card-eyebrow">Persistence &amp; scheduling</span>
1081
+ <button type="button" className="dm-workflow-icon-btn" aria-label="Close upgrade panel" onClick={() => { setUpgradeOpen(false); dismissUpgradeOnboarding(); }}>
1082
+ <X size={14} />
1083
+ </button>
1084
+ </div>
1085
+ <ApiRegistryCreationCockpit
1086
+ state={serverlessState}
1087
+ onAction={handleUpgradeAction}
1088
+ disabled={saving || publishing || running}
1089
+ eyebrow={isServerlessWorkflow ? "Serverless workflow" : "Upgrade to serverless"}
1090
+ />
1091
+ </div>
1092
+ ) : null}
1093
+
923
1094
  {loading ? (
924
1095
  <p className="dm-workflow-empty">Loading workflow…</p>
925
1096
  ) : error ? (
@@ -45,6 +45,7 @@ import {
45
45
  Plus,
46
46
  Quote,
47
47
  RefreshCw,
48
+ Rocket,
48
49
  Rows3,
49
50
  Save,
50
51
  Search,
@@ -89,6 +90,11 @@ import {
89
90
  } from "@/lib/workspace-chart-values";
90
91
  import { selectObjectFilterableFields, selectObjectSortableFields } from "@/lib/workspace-metadata-selectors";
91
92
  import { deriveWorkspaceActivationState } from "@/lib/workspace-activation";
93
+ import {
94
+ CODEX_SITES_OBJECT_ID,
95
+ ensureCodexSitesDataModel,
96
+ isCodexSiteUrl
97
+ } from "@/lib/codex-sites-workspace-adapter";
92
98
  import { HelperSidecar } from "./data-model/components/HelperSidecar.jsx";
93
99
  import { WorkspaceRail } from "./workspace-rail.jsx";
94
100
  import { WorkspaceActivationPanel } from "./components/WorkspaceActivationPanel.jsx";
@@ -503,6 +509,34 @@ function listBuilderWorkflowItems(config) {
503
509
  });
504
510
  }
505
511
 
512
+ function listBuilderSiteItems(config) {
513
+ const object = getDataModelObject(config, CODEX_SITES_OBJECT_ID);
514
+ const rows = Array.isArray(object?.rows) ? object.rows : [];
515
+ return rows.flatMap((row, index) => {
516
+ const url = String(row?.url || "").trim();
517
+ if (!isCodexSiteUrl(url)) return [];
518
+ const title = String(row?.Name || row?.name || `Codex Site ${index + 1}`).trim();
519
+ const rawStatus = String(row?.status || "").trim().toLowerCase();
520
+ const status = rawStatus === "active" ? "live" : rawStatus || "draft";
521
+ return [{
522
+ type: "site",
523
+ id: String(row?.id || row?.Name || `codex-site-${index + 1}`),
524
+ title,
525
+ itemKind: "Site",
526
+ updatedAt: formatBuilderTimestamp(row?.lastRecordedAt || ""),
527
+ status,
528
+ site: {
529
+ row,
530
+ rowIndex: index,
531
+ title,
532
+ url,
533
+ app: String(row?.app || "apps/workspace").trim(),
534
+ client: String(row?.client || "Workspace").trim()
535
+ }
536
+ }];
537
+ });
538
+ }
539
+
506
540
  function formatBuilderTimestamp(value) {
507
541
  const raw = String(value || "").trim();
508
542
  if (!raw || raw === "new") return raw;
@@ -4246,6 +4280,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4246
4280
  const canvas = config.canvas;
4247
4281
  const dashboards = config.dashboards || [];
4248
4282
  const workflows = useMemo(() => listBuilderWorkflowItems(config), [config]);
4283
+ const sites = useMemo(() => listBuilderSiteItems(config), [config]);
4249
4284
  const builderItems = useMemo(() => {
4250
4285
  const dashboardItems = dashboards.map((dashboard, index) => ({
4251
4286
  type: "dashboard",
@@ -4266,13 +4301,17 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4266
4301
  status: workflow.lifecycleStatus || "draft",
4267
4302
  workflow
4268
4303
  }));
4304
+ const siteItems = sites.map((site) => ({
4305
+ ...site,
4306
+ updatedAt: site.updatedAt || "new"
4307
+ }));
4269
4308
  const q = builderListFilter.query.trim().toLowerCase();
4270
- return [...dashboardItems, ...workflowItems].filter((item) => {
4309
+ return [...dashboardItems, ...siteItems, ...workflowItems].filter((item) => {
4271
4310
  if (builderListFilter.type !== "all" && item.type !== builderListFilter.type) return false;
4272
4311
  if (!q) return true;
4273
- return [item.title, item.itemKind, item.status, item.type].some((part) => String(part || "").toLowerCase().includes(q));
4312
+ return [item.title, item.itemKind, item.status, item.type, item.site?.client, item.site?.app, item.site?.url].some((part) => String(part || "").toLowerCase().includes(q));
4274
4313
  });
4275
- }, [builderListFilter, dashboards, workflows]);
4314
+ }, [builderListFilter, dashboards, sites, workflows]);
4276
4315
  const resolvedActiveDashboardId = getActiveDashboardId(dashboards, activeDashboardId);
4277
4316
  const resolvedActiveDashboardIndex = activeDashboardIndex(dashboards, resolvedActiveDashboardId);
4278
4317
  const widgetTypes = config.widgetTypes;
@@ -4709,6 +4748,50 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
4709
4748
  }
4710
4749
  }, [config, saving]);
4711
4750
 
4751
+ const createCodexSite = useCallback(async () => {
4752
+ if (saving) return;
4753
+ const objects = Array.isArray(config.dataModel?.objects) ? config.dataModel.objects : [];
4754
+ const hasCodexSitesObject = objects.some((object) => object?.id === CODEX_SITES_OBJECT_ID);
4755
+ if (hasCodexSitesObject) {
4756
+ window.open(`/data-model?object=${encodeURIComponent(CODEX_SITES_OBJECT_ID)}`, "_self");
4757
+ return;
4758
+ }
4759
+ const nextDataModel = ensureCodexSitesDataModel(config.dataModel, []);
4760
+ setSaving(true);
4761
+ try {
4762
+ const response = await fetch("/api/workspace", {
4763
+ method: "PATCH",
4764
+ headers: { "content-type": "application/json" },
4765
+ body: JSON.stringify({ dataModel: nextDataModel })
4766
+ });
4767
+ const payload = await response.json();
4768
+ if (!response.ok || !payload.workspaceConfig) {
4769
+ throw new Error(payload.error || "Failed to create Codex Sites object");
4770
+ }
4771
+ setConfig((prev) => ({ ...prev, dataModel: payload.workspaceConfig.dataModel }));
4772
+ setBuilderListFilter({ type: "site", query: "" });
4773
+ setConfigMessage("Created Codex Sites object");
4774
+ window.open(`/data-model?object=${encodeURIComponent(CODEX_SITES_OBJECT_ID)}`, "_self");
4775
+ } catch (error) {
4776
+ setConfigMessage(error.message || "Failed to create Codex Sites object");
4777
+ } finally {
4778
+ setSaving(false);
4779
+ }
4780
+ }, [config, saving]);
4781
+
4782
+ const manageSite = useCallback((site) => {
4783
+ const rowParam = site?.rowIndex !== undefined ? `&row=${encodeURIComponent(String(site.rowIndex))}` : "";
4784
+ window.open(`/data-model?object=${encodeURIComponent(CODEX_SITES_OBJECT_ID)}${rowParam}`, "_self");
4785
+ }, []);
4786
+
4787
+ const openSite = useCallback((site) => {
4788
+ if (site?.url) {
4789
+ window.open(site.url, "_blank", "noopener,noreferrer");
4790
+ return;
4791
+ }
4792
+ manageSite(site);
4793
+ }, [manageSite]);
4794
+
4712
4795
  const selectDashboard = useCallback((index) => {
4713
4796
  setConfig((prev) => {
4714
4797
  const synced = syncActiveDashboard(prev, activeDashboardId);
@@ -5234,7 +5317,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5234
5317
  }
5235
5318
  const rect = event.currentTarget.getBoundingClientRect();
5236
5319
  const menuWidth = 148;
5237
- const menuHeight = item?.type === "dashboard" ? 136 : 76;
5320
+ const menuHeight = item?.type === "dashboard" ? 136 : item?.type === "site" ? 108 : 76;
5238
5321
  const margin = 8;
5239
5322
  const left = Math.min(
5240
5323
  Math.max(margin, rect.right - menuWidth),
@@ -5916,6 +5999,7 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5916
5999
  </span>
5917
6000
  ) : null}
5918
6001
  <button type="button" onClick={addDashboard}><Plus size={15} />New Dashboard</button>
6002
+ <button type="button" onClick={createCodexSite} disabled={saving}><Rocket size={15} />New Codex Site</button>
5919
6003
  <button type="button" onClick={createWorkflow} disabled={saving}><GitBranch size={15} />New Workflow</button>
5920
6004
  <button type="button" onClick={() => importInputRef.current?.click()}><Import size={15} />Import</button>
5921
6005
  </div>}
@@ -5952,13 +6036,14 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
5952
6036
  <section className="workspace-table" id="dashboards" aria-label="Builder">
5953
6037
  <div className="workspace-table-heading">
5954
6038
  <strong>Builder</strong>
5955
- <span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"} · {workflows.length} workflow{workflows.length === 1 ? "" : "s"}</span>
6039
+ <span>{dashboards.length} dashboard{dashboards.length === 1 ? "" : "s"} · {sites.length} site{sites.length === 1 ? "" : "s"} · {workflows.length} workflow{workflows.length === 1 ? "" : "s"}</span>
5956
6040
  </div>
5957
6041
  <div className="workspace-builder-filterbar">
5958
6042
  <div className="workspace-builder-filterbar__segments" role="group" aria-label="Builder item type">
5959
6043
  {[
5960
6044
  ["all", "All"],
5961
6045
  ["dashboard", "Dashboards"],
6046
+ ["site", "Sites"],
5962
6047
  ["workflow", "Workflows"]
5963
6048
  ].map(([type, label]) => (
5964
6049
  <button
@@ -6052,6 +6137,53 @@ function WorkspaceBuilder({ initialConfig, initialSourceRecords, adapterConfig,
6052
6137
  </span>
6053
6138
  )}
6054
6139
  </span>
6140
+ </div> : item.type === "site" ? <div className="workspace-table-row" key={item.id}>
6141
+ <span className="workspace-dashboard-title">
6142
+ {item.site.url ? (
6143
+ <a href={item.site.url} target="_blank" rel="noreferrer">{item.title}</a>
6144
+ ) : (
6145
+ <button
6146
+ className="active"
6147
+ onClick={() => manageSite(item.site)}
6148
+ type="button"
6149
+ >{item.title}</button>
6150
+ )}
6151
+ </span>
6152
+ <span>{item.itemKind}</span>
6153
+ <span>{item.updatedAt}</span>
6154
+ <span>
6155
+ <select
6156
+ aria-label={`Status for ${item.title}`}
6157
+ value={item.status || "draft"}
6158
+ disabled
6159
+ >
6160
+ <option value="draft">draft</option>
6161
+ <option value="review">review</option>
6162
+ <option value="live">live</option>
6163
+ <option value="paused">paused</option>
6164
+ </select>
6165
+ </span>
6166
+ <span className="workspace-dashboard-actions">
6167
+ <button
6168
+ type="button"
6169
+ className="workspace-row-action-trigger"
6170
+ aria-label={`Actions for ${item.title}`}
6171
+ onClick={(event) => openBuilderActionMenu(item, event)}
6172
+ >
6173
+ <MoreVertical size={16} aria-hidden="true" />
6174
+ </button>
6175
+ {builderActionMenuId === item.id && (
6176
+ <span className="workspace-row-action-menu" style={builderActionMenuPlacement || undefined}>
6177
+ {item.site.url ? (
6178
+ <a href={item.site.url} target="_blank" rel="noreferrer" onClick={closeBuilderActionMenu}>Open URL</a>
6179
+ ) : (
6180
+ <button type="button" disabled>Open URL</button>
6181
+ )}
6182
+ <button type="button" onClick={() => { closeBuilderActionMenu(); manageSite(item.site); }}>Manage</button>
6183
+ <button type="button" onClick={() => { closeBuilderActionMenu(); window.open("/settings/apps", "_self"); }}>Apps</button>
6184
+ </span>
6185
+ )}
6186
+ </span>
6055
6187
  </div> : <div className="workspace-table-row" key={item.id}>
6056
6188
  <span className="workspace-dashboard-title">
6057
6189
  {editingWorkflowId === item.workflow.id ? <span className="workspace-dashboard-title-editor">
@@ -28,11 +28,9 @@ import path from "node:path";
28
28
  import { pathToFileURL } from "node:url";
29
29
 
30
30
  const staticLoaded = new Set();
31
- let staticLoadDone = false;
31
+ const nativeImport = new Function("specifier", "return import(specifier)");
32
32
 
33
33
  async function loadStaticResolversOnce() {
34
- if (staticLoadDone) return;
35
- staticLoadDone = true;
36
34
  const resolversDir = path.resolve(/*turbopackIgnore: true*/ process.cwd(), "lib/adapters/integrations/resolvers");
37
35
  try {
38
36
  const entries = await fs.readdir(resolversDir);
@@ -42,7 +40,7 @@ async function loadStaticResolversOnce() {
42
40
  if (staticLoaded.has(file)) return;
43
41
  try {
44
42
  const absolutePath = path.join(resolversDir, file);
45
- await import(/*turbopackIgnore: true*/ pathToFileURL(absolutePath).href);
43
+ await nativeImport(pathToFileURL(absolutePath).href);
46
44
  staticLoaded.add(file);
47
45
  } catch {
48
46
  // Malformed resolver — skip silently; operator needs to fix the file