@growthub/cli 0.13.0 → 0.13.2
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/sandbox-run/route.js +50 -25
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +522 -35
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +242 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +52 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +1203 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +163 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +190 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +64 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +376 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +6 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1062 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +10 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +906 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/page.jsx +12 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +492 -28
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +114 -30
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/data-model/field-contracts.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js +54 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph-runner.js +322 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +734 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-trace.js +73 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-sidecar-routing.js +24 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +2 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +21 -1
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* │ [🏠 Home] [💬 Chat] [✶+ Ask helper] │ Tab row
|
|
13
13
|
* ├──────────────────────────────────────────────┤
|
|
14
14
|
* │ HOME tab body: CHAT tab body: │
|
|
15
|
-
* │
|
|
15
|
+
* │ Builder Latest │
|
|
16
16
|
* │ Data Model 💬 Best Skills │
|
|
17
17
|
* │ Management 💬 Casual greet │
|
|
18
18
|
* │ Workspace Settings (… more threads) │
|
|
@@ -26,20 +26,21 @@
|
|
|
26
26
|
*
|
|
27
27
|
* Surface-specific slots (`dashboardsSlot`, `dataModelSlot`,
|
|
28
28
|
* `managementSlot`, `settingsSlot`) let the page inject its own
|
|
29
|
-
*
|
|
29
|
+
* Builder / Management / Workspace Settings behaviour
|
|
30
30
|
* while keeping the visual treatment identical across every page.
|
|
31
31
|
*/
|
|
32
32
|
|
|
33
33
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
34
34
|
import { createPortal } from "react-dom";
|
|
35
35
|
import Link from "next/link";
|
|
36
|
-
import { usePathname, useRouter } from "next/navigation";
|
|
36
|
+
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
|
37
37
|
import {
|
|
38
38
|
Archive,
|
|
39
39
|
ChevronDown,
|
|
40
40
|
ChevronRight,
|
|
41
41
|
Folder,
|
|
42
42
|
FolderPlus,
|
|
43
|
+
GitBranch,
|
|
43
44
|
Home,
|
|
44
45
|
LayoutDashboard,
|
|
45
46
|
MessageCircle,
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
nextNavFolderId,
|
|
65
66
|
nextNavItemId,
|
|
66
67
|
} from "@/lib/workspace-helper-apply";
|
|
68
|
+
import { listAvailableWorkflows } from "@/lib/nav-workflows";
|
|
67
69
|
import { ICON_PICKER_SET, LucideIcon } from "./data-model/components/dm-shared.jsx";
|
|
68
70
|
|
|
69
71
|
function textColorForAccent(accent) {
|
|
@@ -172,6 +174,7 @@ const NAV_FOLDER_STYLE_DEFAULT = { icon: "Folder", color: "#f97316", iconBg: "#f
|
|
|
172
174
|
const NAV_ITEM_STYLE_DEFAULT = {
|
|
173
175
|
dashboard: { icon: "LayoutDashboard", color: "#3b82f6", iconBg: "#eff6ff" },
|
|
174
176
|
view: { icon: "Table", color: "#14b8a6", iconBg: "#f0fdfa" },
|
|
177
|
+
workflow: { icon: "GitBranch", color: "#8b5cf6", iconBg: "#f5f3ff" },
|
|
175
178
|
};
|
|
176
179
|
|
|
177
180
|
/** Default visible rows before scroll — keeps the rail from growing unbounded. */
|
|
@@ -394,6 +397,7 @@ function NavFoldersSection({
|
|
|
394
397
|
onPatchNavFolders,
|
|
395
398
|
}) {
|
|
396
399
|
const router = useRouter();
|
|
400
|
+
const searchParams = useSearchParams();
|
|
397
401
|
const [creating, setCreating] = useState(false);
|
|
398
402
|
const [createDraft, setCreateDraft] = useState("");
|
|
399
403
|
const [createDiscardWarn, setCreateDiscardWarn] = useState(false);
|
|
@@ -401,15 +405,16 @@ function NavFoldersSection({
|
|
|
401
405
|
const [menuAnchor, setMenuAnchor] = useState(null);
|
|
402
406
|
const [customizeTarget, setCustomizeTarget] = useState(null);
|
|
403
407
|
const [discardWarn, setDiscardWarn] = useState(false);
|
|
404
|
-
const [addPickerFor, setAddPickerFor] = useState(null); // { folderId, kind: "dashboard"|"view" }
|
|
408
|
+
const [addPickerFor, setAddPickerFor] = useState(null); // { folderId, kind: "dashboard"|"view"|"workflow" }
|
|
405
409
|
const [filterQuery, setFilterQuery] = useState("");
|
|
406
|
-
const [filterType, setFilterType] = useState("all"); // all | dashboard | view
|
|
410
|
+
const [filterType, setFilterType] = useState("all"); // all | dashboard | view | workflow
|
|
407
411
|
const [filterMenuOpen, setFilterMenuOpen] = useState(false);
|
|
408
412
|
const [sectionCollapsed, setSectionCollapsed] = useState(true);
|
|
409
413
|
|
|
410
414
|
const rows = useMemo(() => getNavFolderRows(workspaceConfig), [workspaceConfig]);
|
|
411
415
|
const dashboards = useMemo(() => listAvailableDashboards(workspaceConfig), [workspaceConfig]);
|
|
412
416
|
const viewableObjects = useMemo(() => listAvailableObjectsForViews(workspaceConfig), [workspaceConfig]);
|
|
417
|
+
const workflows = useMemo(() => listAvailableWorkflows(workspaceConfig), [workspaceConfig]);
|
|
413
418
|
const filteredEntries = useMemo(
|
|
414
419
|
() => filterNavFolderRows(rows, filterQuery, filterType),
|
|
415
420
|
[rows, filterQuery, filterType],
|
|
@@ -680,6 +685,28 @@ function NavFoldersSection({
|
|
|
680
685
|
await writeRows(next);
|
|
681
686
|
}, [rows, writeRows]);
|
|
682
687
|
|
|
688
|
+
const addWorkflowItem = useCallback(async (folderId, workflow) => {
|
|
689
|
+
setAddPickerFor(null);
|
|
690
|
+
setOpenMenuId(null);
|
|
691
|
+
const style = NAV_ITEM_STYLE_DEFAULT.workflow;
|
|
692
|
+
const item = {
|
|
693
|
+
id: nextNavItemId(),
|
|
694
|
+
type: "workflow",
|
|
695
|
+
objectId: workflow.objectId,
|
|
696
|
+
rowId: workflow.rowId,
|
|
697
|
+
fieldName: "orchestrationGraph",
|
|
698
|
+
label: workflow.label,
|
|
699
|
+
icon: style.icon,
|
|
700
|
+
color: style.color,
|
|
701
|
+
iconBg: style.iconBg,
|
|
702
|
+
};
|
|
703
|
+
const next = rows.map((row) => {
|
|
704
|
+
if (row.id !== folderId) return row;
|
|
705
|
+
return { ...row, items: [...(row.items || []), item] };
|
|
706
|
+
});
|
|
707
|
+
await writeRows(next);
|
|
708
|
+
}, [rows, writeRows]);
|
|
709
|
+
|
|
683
710
|
// ── Drag-and-drop ────────────────────────────────────────────────────
|
|
684
711
|
//
|
|
685
712
|
// HTML5 DnD with a tiny in-ref state machine; mirrors Twenty's
|
|
@@ -768,9 +795,9 @@ function NavFoldersSection({
|
|
|
768
795
|
};
|
|
769
796
|
|
|
770
797
|
const openDashboardItem = (item) => {
|
|
771
|
-
//
|
|
798
|
+
// Builder items are top-level surfaces; the builder reads the active
|
|
772
799
|
// dashboard from query params if present. Other surfaces simply
|
|
773
|
-
// navigate home; the user lands on the
|
|
800
|
+
// navigate home; the user lands on the Builder list. This keeps
|
|
774
801
|
// the rail itself agnostic of surface-specific routing.
|
|
775
802
|
router.push(`/?dashboard=${encodeURIComponent(item.refId)}`);
|
|
776
803
|
};
|
|
@@ -779,6 +806,13 @@ function NavFoldersSection({
|
|
|
779
806
|
router.push(`/data-model?object=${encodeURIComponent(item.objectId || "")}`);
|
|
780
807
|
};
|
|
781
808
|
|
|
809
|
+
const openWorkflowItem = (item) => {
|
|
810
|
+
const objectId = encodeURIComponent(item.objectId || "");
|
|
811
|
+
const row = encodeURIComponent(item.rowId || "");
|
|
812
|
+
const field = encodeURIComponent(item.fieldName || "orchestrationGraph");
|
|
813
|
+
router.push(`/workflows?object=${objectId}&row=${row}&field=${field}`);
|
|
814
|
+
};
|
|
815
|
+
|
|
782
816
|
const renderItemMenu = (folder, item) => {
|
|
783
817
|
const composedId = `${folder.id}::${item.id}`;
|
|
784
818
|
const isMenuOpen = openMenuId === composedId;
|
|
@@ -862,9 +896,13 @@ function NavFoldersSection({
|
|
|
862
896
|
const renderItemRow = (folder, item) => {
|
|
863
897
|
const composedId = `${folder.id}::${item.id}`;
|
|
864
898
|
const isMenuOpen = openMenuId === composedId;
|
|
865
|
-
const isActive = item.type === "view" && pathname.startsWith(`/views/${encodeURIComponent(item.id)}`)
|
|
899
|
+
const isActive = item.type === "view" && pathname.startsWith(`/views/${encodeURIComponent(item.id)}`)
|
|
900
|
+
|| (item.type === "workflow"
|
|
901
|
+
&& pathname.startsWith("/workflows")
|
|
902
|
+
&& searchParams.get("object") === item.objectId
|
|
903
|
+
&& searchParams.get("row") === item.rowId);
|
|
866
904
|
const style = navItemStyle(item);
|
|
867
|
-
const typeHint = item.type === "dashboard" ? "Dashboard" : "View";
|
|
905
|
+
const typeHint = item.type === "dashboard" ? "Dashboard" : item.type === "workflow" ? "Workflow" : "View";
|
|
868
906
|
return (
|
|
869
907
|
<li
|
|
870
908
|
key={item.id}
|
|
@@ -880,7 +918,11 @@ function NavFoldersSection({
|
|
|
880
918
|
<button
|
|
881
919
|
type="button"
|
|
882
920
|
className="workspace-rail-nav-row-main"
|
|
883
|
-
onClick={() =>
|
|
921
|
+
onClick={() => {
|
|
922
|
+
if (item.type === "dashboard") openDashboardItem(item);
|
|
923
|
+
else if (item.type === "workflow") openWorkflowItem(item);
|
|
924
|
+
else openViewItem(item);
|
|
925
|
+
}}
|
|
884
926
|
title={`${item.label || item.refId || item.objectId} · ${typeHint}`}
|
|
885
927
|
>
|
|
886
928
|
<NavIconBadge icon={style.icon} color={style.color} iconBg={style.iconBg} />
|
|
@@ -984,6 +1026,19 @@ function NavFoldersSection({
|
|
|
984
1026
|
>
|
|
985
1027
|
<TableIcon size={13} aria-hidden="true" /> Add view
|
|
986
1028
|
</button>
|
|
1029
|
+
<button
|
|
1030
|
+
type="button"
|
|
1031
|
+
role="menuitem"
|
|
1032
|
+
className="workspace-rail-thread-menu-item"
|
|
1033
|
+
disabled={workflows.length === 0}
|
|
1034
|
+
onClick={() => {
|
|
1035
|
+
setOpenMenuId(null);
|
|
1036
|
+
setMenuAnchor(null);
|
|
1037
|
+
setAddPickerFor({ folderId: folder.id, kind: "workflow" });
|
|
1038
|
+
}}
|
|
1039
|
+
>
|
|
1040
|
+
<GitBranch size={13} aria-hidden="true" /> Add workflow
|
|
1041
|
+
</button>
|
|
987
1042
|
<button
|
|
988
1043
|
type="button"
|
|
989
1044
|
role="menuitem"
|
|
@@ -1060,7 +1115,13 @@ function NavFoldersSection({
|
|
|
1060
1115
|
<NavFolderPickerOverlay onClose={() => setAddPickerFor(null)}>
|
|
1061
1116
|
<div className="workspace-rail-folder-picker" onClick={(e) => e.stopPropagation()}>
|
|
1062
1117
|
<div className="workspace-rail-folder-picker-head">
|
|
1063
|
-
<strong>
|
|
1118
|
+
<strong>
|
|
1119
|
+
{addPickerFor.kind === "dashboard"
|
|
1120
|
+
? "Add dashboard"
|
|
1121
|
+
: addPickerFor.kind === "workflow"
|
|
1122
|
+
? "Add workflow"
|
|
1123
|
+
: "Add view"}
|
|
1124
|
+
</strong>
|
|
1064
1125
|
<button
|
|
1065
1126
|
type="button"
|
|
1066
1127
|
className="workspace-rail-folder-picker-close"
|
|
@@ -1088,23 +1149,45 @@ function NavFoldersSection({
|
|
|
1088
1149
|
</button>
|
|
1089
1150
|
</li>
|
|
1090
1151
|
))
|
|
1091
|
-
:
|
|
1092
|
-
|
|
1093
|
-
<
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1152
|
+
: addPickerFor.kind === "workflow"
|
|
1153
|
+
? workflows.map((w) => (
|
|
1154
|
+
<li key={`${w.objectId}:${w.rowId}`}>
|
|
1155
|
+
<button
|
|
1156
|
+
type="button"
|
|
1157
|
+
className="workspace-rail-folder-picker-item"
|
|
1158
|
+
onClick={() => addWorkflowItem(addPickerFor.folderId, w)}
|
|
1159
|
+
>
|
|
1160
|
+
<NavIconBadge
|
|
1161
|
+
icon={NAV_ITEM_STYLE_DEFAULT.workflow.icon}
|
|
1162
|
+
color={NAV_ITEM_STYLE_DEFAULT.workflow.color}
|
|
1163
|
+
iconBg={NAV_ITEM_STYLE_DEFAULT.workflow.iconBg}
|
|
1164
|
+
/>
|
|
1165
|
+
<span className="workspace-rail-folder-picker-item-text">
|
|
1166
|
+
<span>{w.label}</span>
|
|
1167
|
+
<span className="workspace-rail-folder-picker-hint">
|
|
1168
|
+
{w.objectLabel} · {w.status} · {w.graphNodeCount} node{w.graphNodeCount === 1 ? "" : "s"}
|
|
1169
|
+
</span>
|
|
1170
|
+
</span>
|
|
1171
|
+
</button>
|
|
1172
|
+
</li>
|
|
1173
|
+
))
|
|
1174
|
+
: viewableObjects.map((o) => (
|
|
1175
|
+
<li key={o.id}>
|
|
1176
|
+
<button
|
|
1177
|
+
type="button"
|
|
1178
|
+
className="workspace-rail-folder-picker-item"
|
|
1179
|
+
onClick={() => addViewItem(addPickerFor.folderId, o)}
|
|
1180
|
+
>
|
|
1181
|
+
<NavIconBadge
|
|
1182
|
+
icon={NAV_ITEM_STYLE_DEFAULT.view.icon}
|
|
1183
|
+
color={NAV_ITEM_STYLE_DEFAULT.view.color}
|
|
1184
|
+
iconBg={NAV_ITEM_STYLE_DEFAULT.view.iconBg}
|
|
1185
|
+
/>
|
|
1186
|
+
<span>{o.label}</span>
|
|
1187
|
+
<span className="workspace-rail-folder-picker-hint">{o.columns.length} field{o.columns.length === 1 ? "" : "s"}</span>
|
|
1188
|
+
</button>
|
|
1189
|
+
</li>
|
|
1190
|
+
))}
|
|
1108
1191
|
</ul>
|
|
1109
1192
|
</div>
|
|
1110
1193
|
</NavFolderPickerOverlay>
|
|
@@ -1154,7 +1237,7 @@ function NavFoldersSection({
|
|
|
1154
1237
|
<input
|
|
1155
1238
|
type="search"
|
|
1156
1239
|
className="workspace-rail-folders-search-input"
|
|
1157
|
-
placeholder="Filter folders &
|
|
1240
|
+
placeholder="Filter folders & shortcuts"
|
|
1158
1241
|
value={filterQuery}
|
|
1159
1242
|
onChange={(e) => setFilterQuery(e.target.value)}
|
|
1160
1243
|
aria-label="Filter folders and views by name"
|
|
@@ -1203,6 +1286,7 @@ function NavFoldersSection({
|
|
|
1203
1286
|
{ id: "all", label: "All" },
|
|
1204
1287
|
{ id: "dashboard", label: "Dashboards" },
|
|
1205
1288
|
{ id: "view", label: "Views" },
|
|
1289
|
+
{ id: "workflow", label: "Workflows" },
|
|
1206
1290
|
].map((opt) => (
|
|
1207
1291
|
<button
|
|
1208
1292
|
key={opt.id}
|
|
@@ -1579,7 +1663,7 @@ export function WorkspaceRail({
|
|
|
1579
1663
|
<nav className="workspace-nav" aria-label="Workspace pages">
|
|
1580
1664
|
{dashboardsSlot ?? (
|
|
1581
1665
|
<Link href="/" className={pathname === "/" ? "active" : undefined}>
|
|
1582
|
-
|
|
1666
|
+
Builder
|
|
1583
1667
|
</Link>
|
|
1584
1668
|
)}
|
|
1585
1669
|
{dataModelSlot ?? (
|
|
@@ -27,6 +27,7 @@ const SANDBOX_ENVIRONMENT_FIELDS = {
|
|
|
27
27
|
},
|
|
28
28
|
envRefs: { editor: "env-ref-multiselect" },
|
|
29
29
|
lastResponse: { editor: "json-preview", readonly: true },
|
|
30
|
+
orchestrationGraph: { editor: "orchestration-graph", readonly: true },
|
|
30
31
|
lastRunId: { editor: "readonly-text" },
|
|
31
32
|
lastSourceId: { editor: "readonly-text" },
|
|
32
33
|
resolverTemplateId: { editor: "readonly-text" },
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/nav-workflows.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Folder workflow shortcuts — discover sandbox-environment rows for nav-folders.
|
|
3
|
+
* Shortcuts reference rows; they do not copy orchestrationGraph JSON.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { parseOrchestrationGraph } from "./orchestration-graph.js";
|
|
7
|
+
|
|
8
|
+
const HIDDEN_SANDBOX_OBJECT_IDS = new Set(["workspace-helper-sandbox"]);
|
|
9
|
+
|
|
10
|
+
function sandboxRowId(row) {
|
|
11
|
+
return String(row?.Name || row?.name || row?.slug || row?.id || "").trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function listAvailableWorkflows(workspaceConfig) {
|
|
15
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const object of objects) {
|
|
18
|
+
if (object?.objectType !== "sandbox-environment") continue;
|
|
19
|
+
if (HIDDEN_SANDBOX_OBJECT_IDS.has(String(object?.id || ""))) continue;
|
|
20
|
+
const objectId = String(object?.id || "").trim();
|
|
21
|
+
if (!objectId) continue;
|
|
22
|
+
const objectLabel = String(object?.label || "Sandbox Environment").trim();
|
|
23
|
+
const rows = Array.isArray(object?.rows) ? object.rows : [];
|
|
24
|
+
for (const row of rows) {
|
|
25
|
+
const rowId = sandboxRowId(row);
|
|
26
|
+
if (!rowId) continue;
|
|
27
|
+
const graph = parseOrchestrationGraph(row?.orchestrationGraph || row?.orchestrationConfig);
|
|
28
|
+
const graphNodeCount = Array.isArray(graph?.nodes) ? graph.nodes.length : 0;
|
|
29
|
+
out.push({
|
|
30
|
+
objectId,
|
|
31
|
+
rowId,
|
|
32
|
+
label: rowId,
|
|
33
|
+
status: String(row?.lifecycleStatus || row?.status || "draft").trim(),
|
|
34
|
+
version: String(row?.version || "1").trim(),
|
|
35
|
+
graphNodeCount,
|
|
36
|
+
objectLabel,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return out.sort((a, b) => a.label.localeCompare(b.label));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function findSandboxRowByWorkflowRef(workspaceConfig, objectId, rowId) {
|
|
44
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
45
|
+
const object = objects.find((o) => o?.id === objectId && o?.objectType === "sandbox-environment");
|
|
46
|
+
if (!object) return { object: null, row: null, rowIndex: -1 };
|
|
47
|
+
const wanted = String(rowId || "").trim();
|
|
48
|
+
const rows = Array.isArray(object.rows) ? object.rows : [];
|
|
49
|
+
const rowIndex = rows.findIndex((row) => sandboxRowId(row) === wanted);
|
|
50
|
+
if (rowIndex < 0) return { object, row: null, rowIndex: -1 };
|
|
51
|
+
return { object, row: rows[rowIndex], rowIndex };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { listAvailableWorkflows, findSandboxRowByWorkflowRef, sandboxRowId };
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side growthub-native orchestration graph execution for sandbox-run.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
applyFieldMap,
|
|
7
|
+
applyFilters,
|
|
8
|
+
extractApiRegistryCallNode,
|
|
9
|
+
extractInputNode,
|
|
10
|
+
extractTransformConfig,
|
|
11
|
+
normalizeJsonAtPath,
|
|
12
|
+
parseOrchestrationGraph,
|
|
13
|
+
redactSecretsFromText,
|
|
14
|
+
substituteVariables
|
|
15
|
+
} from "./orchestration-graph.js";
|
|
16
|
+
|
|
17
|
+
function normalizeMethod(value) {
|
|
18
|
+
const method = String(value || "GET").trim().toUpperCase();
|
|
19
|
+
return ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(method) ? method : "GET";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildUrl(record, inputPayload) {
|
|
23
|
+
const baseUrl = String(record?.baseUrl || "").trim();
|
|
24
|
+
let endpoint = String(record?.endpoint || "").trim();
|
|
25
|
+
endpoint = substituteVariables(endpoint, inputPayload);
|
|
26
|
+
const raw = endpoint || baseUrl;
|
|
27
|
+
if (!raw) throw new Error("baseUrl or endpoint is required");
|
|
28
|
+
if (/^https?:\/\//i.test(endpoint)) return endpoint;
|
|
29
|
+
if (!baseUrl) throw new Error("baseUrl is required when endpoint is relative");
|
|
30
|
+
return `${baseUrl.replace(/\/+$/, "")}/${endpoint.replace(/^\/+/, "")}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function envKeyCandidates(ref) {
|
|
34
|
+
const token = String(ref || "")
|
|
35
|
+
.trim()
|
|
36
|
+
.replace(/[^a-z0-9]+/gi, "_")
|
|
37
|
+
.replace(/^_+|_+$/g, "")
|
|
38
|
+
.toUpperCase();
|
|
39
|
+
return Array.from(new Set([
|
|
40
|
+
token,
|
|
41
|
+
token ? `${token}_API_KEY` : "",
|
|
42
|
+
token ? `${token}_TOKEN` : ""
|
|
43
|
+
].filter(Boolean)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function readServerSecret(authRef) {
|
|
47
|
+
for (const key of envKeyCandidates(authRef)) {
|
|
48
|
+
if (process.env[key]) return { key, value: process.env[key] };
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildAuthHeaders(record, secretValue) {
|
|
54
|
+
if (!secretValue) return {};
|
|
55
|
+
const meta = record?.requestHeadersMetadata || {};
|
|
56
|
+
const headerName = String(meta.authHeaderName || record?.authHeaderName || record?.authHeader || "x-api-key").trim();
|
|
57
|
+
if (!headerName) return {};
|
|
58
|
+
const prefix = String(meta.authPrefix || record?.authPrefix || "").trim();
|
|
59
|
+
return { [headerName]: prefix ? `${prefix} ${secretValue}` : secretValue };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function findRegistryRecord(workspaceConfig, registryId) {
|
|
63
|
+
const id = String(registryId || "").trim();
|
|
64
|
+
if (!id) return null;
|
|
65
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
66
|
+
for (const objectItem of objects) {
|
|
67
|
+
if (objectItem?.objectType !== "api-registry") continue;
|
|
68
|
+
const rows = Array.isArray(objectItem.rows) ? objectItem.rows : [];
|
|
69
|
+
const match = rows.find(
|
|
70
|
+
(r) => String(r?.integrationId || "").trim() === id
|
|
71
|
+
|| String(r?.id || "").trim() === id
|
|
72
|
+
|| String(r?.Name || "").trim() === id
|
|
73
|
+
);
|
|
74
|
+
if (match) return match;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseInputPayload(inputNode) {
|
|
80
|
+
const config = inputNode?.config || {};
|
|
81
|
+
const mode = String(config.inputMode || "manual").trim();
|
|
82
|
+
if (mode === "manual") {
|
|
83
|
+
const sample = config.samplePayload;
|
|
84
|
+
if (sample && typeof sample === "object") return sample;
|
|
85
|
+
if (typeof sample === "string" && sample.trim()) {
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(sample);
|
|
88
|
+
} catch {
|
|
89
|
+
return {};
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
return {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function transformProviderPayload(rawPayload, transformConfig) {
|
|
98
|
+
const config = transformConfig || {};
|
|
99
|
+
const rootPath = String(config.rootPath || "").trim();
|
|
100
|
+
let cursor = rootPath ? getValueAtPath(rawPayload, rootPath) : rawPayload;
|
|
101
|
+
if (cursor === undefined) cursor = rawPayload;
|
|
102
|
+
|
|
103
|
+
const fieldMap = config.fieldMap && typeof config.fieldMap === "object" ? config.fieldMap : {};
|
|
104
|
+
if (Object.keys(fieldMap).length) {
|
|
105
|
+
if (Array.isArray(cursor)) {
|
|
106
|
+
cursor = cursor.map((row) => applyFieldMap(row, fieldMap));
|
|
107
|
+
} else if (cursor && typeof cursor === "object") {
|
|
108
|
+
cursor = applyFieldMap(cursor, fieldMap);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (Array.isArray(cursor)) {
|
|
113
|
+
const filtered = applyFilters(cursor, config.filters, config.filterMode);
|
|
114
|
+
const maxRows = Number(config.maxRows);
|
|
115
|
+
if (Number.isFinite(maxRows) && maxRows > 0) {
|
|
116
|
+
return filtered.slice(0, maxRows);
|
|
117
|
+
}
|
|
118
|
+
return filtered;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return cursor;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getValueAtPath(obj, path) {
|
|
125
|
+
if (!path) return obj;
|
|
126
|
+
const parts = String(path).split(".").filter(Boolean);
|
|
127
|
+
let cursor = obj;
|
|
128
|
+
for (const part of parts) {
|
|
129
|
+
if (cursor == null || typeof cursor !== "object") return undefined;
|
|
130
|
+
cursor = cursor[part];
|
|
131
|
+
}
|
|
132
|
+
return cursor;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function executeApiRegistryCall(workspaceConfig, nodeConfig, inputPayload, timeoutMs) {
|
|
136
|
+
const registryId = String(nodeConfig?.registryId || nodeConfig?.integrationId || "").trim();
|
|
137
|
+
const registryRecord = findRegistryRecord(workspaceConfig, registryId);
|
|
138
|
+
if (!registryRecord) {
|
|
139
|
+
return {
|
|
140
|
+
ok: false,
|
|
141
|
+
exitCode: 1,
|
|
142
|
+
durationMs: 0,
|
|
143
|
+
stdout: "",
|
|
144
|
+
stderr: "",
|
|
145
|
+
error: `no API Registry row for integrationId ${registryId}`,
|
|
146
|
+
adapterMeta: { mode: "orchestration-graph", registryId }
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const merged = {
|
|
151
|
+
...registryRecord,
|
|
152
|
+
method: nodeConfig?.method || registryRecord.method,
|
|
153
|
+
endpoint: nodeConfig?.endpoint || registryRecord.endpoint,
|
|
154
|
+
baseUrl: nodeConfig?.baseUrl || registryRecord.baseUrl,
|
|
155
|
+
authRef: nodeConfig?.authRef || registryRecord.authRef || registryId,
|
|
156
|
+
requestHeadersMetadata: {
|
|
157
|
+
...(registryRecord.requestHeadersMetadata || {}),
|
|
158
|
+
...(nodeConfig?.requestHeadersMetadata || {})
|
|
159
|
+
},
|
|
160
|
+
authHeaderName: nodeConfig?.requestHeadersMetadata?.authHeaderName
|
|
161
|
+
|| registryRecord.authHeaderName
|
|
162
|
+
|| registryRecord.authHeader,
|
|
163
|
+
authPrefix: nodeConfig?.requestHeadersMetadata?.authPrefix || registryRecord.authPrefix
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
let url;
|
|
167
|
+
try {
|
|
168
|
+
url = buildUrl(merged, inputPayload);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
exitCode: 1,
|
|
173
|
+
durationMs: 0,
|
|
174
|
+
stdout: "",
|
|
175
|
+
stderr: "",
|
|
176
|
+
error: err.message || "invalid URL",
|
|
177
|
+
adapterMeta: { mode: "orchestration-graph", registryId }
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const method = normalizeMethod(merged.method);
|
|
182
|
+
const authRef = merged.authRef || registryId;
|
|
183
|
+
const secretEntry = readServerSecret(authRef);
|
|
184
|
+
const secret = secretEntry?.value || "";
|
|
185
|
+
const outboundTimeout = Math.min(Math.max(timeoutMs, 1000), 120000);
|
|
186
|
+
const startedAt = Date.now();
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
const timer = setTimeout(() => controller.abort(), outboundTimeout);
|
|
189
|
+
|
|
190
|
+
const meta = nodeConfig?.requestHeadersMetadata || {};
|
|
191
|
+
const contentType = String(meta.contentType || "").trim() || (method === "GET" ? "" : "application/json");
|
|
192
|
+
|
|
193
|
+
let body;
|
|
194
|
+
const bodyTemplate = substituteVariables(String(nodeConfig?.bodyTemplate || ""), inputPayload);
|
|
195
|
+
if (method !== "GET" && bodyTemplate) {
|
|
196
|
+
try {
|
|
197
|
+
body = JSON.parse(bodyTemplate);
|
|
198
|
+
} catch {
|
|
199
|
+
body = bodyTemplate;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
const response = await fetch(url, {
|
|
205
|
+
method,
|
|
206
|
+
headers: {
|
|
207
|
+
accept: "application/json, text/plain;q=0.9,*/*;q=0.8",
|
|
208
|
+
...(contentType ? { "content-type": contentType } : {}),
|
|
209
|
+
...buildAuthHeaders(merged, secret)
|
|
210
|
+
},
|
|
211
|
+
...(body !== undefined ? { body: typeof body === "string" ? body : JSON.stringify(body) } : {}),
|
|
212
|
+
signal: controller.signal
|
|
213
|
+
});
|
|
214
|
+
const durationMs = Date.now() - startedAt;
|
|
215
|
+
const responseContentType = response.headers.get("content-type") || "";
|
|
216
|
+
const payload = responseContentType.includes("application/json") ? await response.json() : await response.text();
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
ok: response.ok,
|
|
220
|
+
exitCode: response.ok ? 0 : 1,
|
|
221
|
+
durationMs,
|
|
222
|
+
stdout: typeof payload === "string" ? payload : JSON.stringify(payload, null, 2),
|
|
223
|
+
stderr: "",
|
|
224
|
+
error: response.ok ? undefined : `HTTP ${response.status}`,
|
|
225
|
+
rawPayload: payload,
|
|
226
|
+
httpStatus: response.status,
|
|
227
|
+
adapterMeta: {
|
|
228
|
+
mode: "orchestration-graph",
|
|
229
|
+
registryId,
|
|
230
|
+
url,
|
|
231
|
+
httpStatus: response.status,
|
|
232
|
+
method,
|
|
233
|
+
authRefSlug: authRef
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
} catch (error) {
|
|
237
|
+
const durationMs = Date.now() - startedAt;
|
|
238
|
+
const safeError = redactSecretsFromText(
|
|
239
|
+
error.name === "AbortError" ? `request timed out after ${outboundTimeout}ms` : (error.message || "fetch failed")
|
|
240
|
+
);
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
exitCode: null,
|
|
244
|
+
durationMs,
|
|
245
|
+
stdout: "",
|
|
246
|
+
stderr: "",
|
|
247
|
+
error: safeError,
|
|
248
|
+
adapterMeta: { mode: "orchestration-graph", registryId, url, aborted: error.name === "AbortError" }
|
|
249
|
+
};
|
|
250
|
+
} finally {
|
|
251
|
+
clearTimeout(timer);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Run a growthub-native orchestration graph when present on the sandbox row.
|
|
257
|
+
* Returns null when the row has no executable graph (caller falls back to adapter path).
|
|
258
|
+
*/
|
|
259
|
+
async function runOrchestrationGraphIfPresent({ workspaceConfig, row, timeoutMs }) {
|
|
260
|
+
const graph = parseOrchestrationGraph(row?.orchestrationGraph || row?.orchestrationConfig);
|
|
261
|
+
if (!graph || String(graph.provider || "").trim() !== "growthub-native") return null;
|
|
262
|
+
|
|
263
|
+
const apiNode = extractApiRegistryCallNode(graph);
|
|
264
|
+
if (!apiNode?.config) {
|
|
265
|
+
return {
|
|
266
|
+
ok: false,
|
|
267
|
+
exitCode: 1,
|
|
268
|
+
durationMs: 0,
|
|
269
|
+
stdout: "",
|
|
270
|
+
stderr: "",
|
|
271
|
+
error: "orchestrationGraph is missing an api-registry-call node",
|
|
272
|
+
adapterMeta: { mode: "orchestration-graph", provider: graph.provider }
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const inputNode = extractInputNode(graph);
|
|
277
|
+
const inputPayload = parseInputPayload(inputNode);
|
|
278
|
+
const transformConfig = extractTransformConfig(graph);
|
|
279
|
+
const resultNode = graph.nodes?.find((n) => n?.type === "tool-result");
|
|
280
|
+
const successCodes = Array.isArray(resultNode?.config?.successStatusCodes)
|
|
281
|
+
? resultNode.config.successStatusCodes.map(Number).filter(Number.isFinite)
|
|
282
|
+
: [200];
|
|
283
|
+
|
|
284
|
+
const raw = await executeApiRegistryCall(
|
|
285
|
+
workspaceConfig,
|
|
286
|
+
apiNode.config,
|
|
287
|
+
inputPayload,
|
|
288
|
+
Number(apiNode.config?.timeoutMs) || timeoutMs
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
if (raw.ok && raw.rawPayload !== undefined) {
|
|
292
|
+
const httpStatus = Number(raw.httpStatus);
|
|
293
|
+
if (successCodes.length && !successCodes.includes(httpStatus)) {
|
|
294
|
+
raw.ok = false;
|
|
295
|
+
raw.exitCode = 1;
|
|
296
|
+
raw.error = `HTTP ${httpStatus} is not in successStatusCodes`;
|
|
297
|
+
}
|
|
298
|
+
const transformed = transformProviderPayload(raw.rawPayload, transformConfig);
|
|
299
|
+
raw.stdout = typeof transformed === "string"
|
|
300
|
+
? transformed
|
|
301
|
+
: normalizeJsonAtPath(transformed, "");
|
|
302
|
+
delete raw.rawPayload;
|
|
303
|
+
delete raw.httpStatus;
|
|
304
|
+
} else if (raw.error) {
|
|
305
|
+
raw.error = redactSecretsFromText(raw.error);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (raw.stdout) {
|
|
309
|
+
raw.stdout = redactSecretsFromText(raw.stdout);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
...raw,
|
|
314
|
+
adapterMeta: {
|
|
315
|
+
...(raw.adapterMeta || {}),
|
|
316
|
+
orchestrationProvider: graph.provider,
|
|
317
|
+
orchestrationVersion: graph.version
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export { runOrchestrationGraphIfPresent };
|