@growthub/cli 0.13.8 → 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/data-model/components/DataModelShell.jsx +105 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +115 -7
- 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 +137 -5
- 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/package.json +1 -1
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { listLocalCodexSites } from "@/lib/codex-sites-local-state";
|
|
3
|
+
|
|
4
|
+
async function GET() {
|
|
5
|
+
const sites = await listLocalCodexSites();
|
|
6
|
+
return NextResponse.json({
|
|
7
|
+
ok: true,
|
|
8
|
+
source: "codex-local-session-state",
|
|
9
|
+
sites
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { GET };
|
|
@@ -95,6 +95,12 @@ import {
|
|
|
95
95
|
pluralize,
|
|
96
96
|
textColorForAccent,
|
|
97
97
|
} from "./dm-shared.jsx";
|
|
98
|
+
import {
|
|
99
|
+
CODEX_SITES_OBJECT_ID,
|
|
100
|
+
codexSiteRecordToRow,
|
|
101
|
+
isCodexSiteUrl,
|
|
102
|
+
normalizeCodexSiteRecord,
|
|
103
|
+
} from "@/lib/codex-sites-workspace-adapter";
|
|
98
104
|
|
|
99
105
|
// ─── Object type definitions for the type-picker step ────────────────────────
|
|
100
106
|
|
|
@@ -1015,6 +1021,95 @@ function SandboxRecordFields({
|
|
|
1015
1021
|
);
|
|
1016
1022
|
}
|
|
1017
1023
|
|
|
1024
|
+
function CodexSitesRecordFields({ draft, setDraft, table, saving, onSave, rowIndex }) {
|
|
1025
|
+
const [sites, setSites] = useState([]);
|
|
1026
|
+
const [loadingSites, setLoadingSites] = useState(false);
|
|
1027
|
+
const [sitesMessage, setSitesMessage] = useState("");
|
|
1028
|
+
const selectedUrl = String(draft?.url || "").trim();
|
|
1029
|
+
|
|
1030
|
+
useEffect(() => {
|
|
1031
|
+
let cancelled = false;
|
|
1032
|
+
setLoadingSites(true);
|
|
1033
|
+
setSitesMessage("");
|
|
1034
|
+
fetch("/api/workspace/codex-sites", { cache: "no-store" })
|
|
1035
|
+
.then((res) => res.json())
|
|
1036
|
+
.then((payload) => {
|
|
1037
|
+
if (cancelled) return;
|
|
1038
|
+
const nextSites = Array.isArray(payload.sites)
|
|
1039
|
+
? payload.sites.map((site) => normalizeCodexSiteRecord(site)).filter((site) => isCodexSiteUrl(site.url))
|
|
1040
|
+
: [];
|
|
1041
|
+
setSites(nextSites);
|
|
1042
|
+
setSitesMessage(nextSites.length ? "" : "No Codex Sites are available from the workspace adapter.");
|
|
1043
|
+
})
|
|
1044
|
+
.catch((error) => {
|
|
1045
|
+
if (cancelled) return;
|
|
1046
|
+
setSites([]);
|
|
1047
|
+
setSitesMessage(error?.message || "Codex Sites adapter unavailable.");
|
|
1048
|
+
})
|
|
1049
|
+
.finally(() => {
|
|
1050
|
+
if (!cancelled) setLoadingSites(false);
|
|
1051
|
+
});
|
|
1052
|
+
return () => { cancelled = true; };
|
|
1053
|
+
}, []);
|
|
1054
|
+
|
|
1055
|
+
function patchFields(fields) {
|
|
1056
|
+
setDraft((current) => ({ ...current, ...fields }));
|
|
1057
|
+
onSave((config) => Object.entries(fields).reduce(
|
|
1058
|
+
(nextConfig, [column, value]) => updateTableCell(nextConfig, table, rowIndex, column, value),
|
|
1059
|
+
config
|
|
1060
|
+
));
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function selectSite(url) {
|
|
1064
|
+
const site = sites.find((item) => item.url === url);
|
|
1065
|
+
if (!site) return;
|
|
1066
|
+
patchFields(codexSiteRecordToRow(site));
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
return (
|
|
1070
|
+
<div className="dm-codex-sites-config">
|
|
1071
|
+
<DrawerSection title="Codex Site Binding" defaultOpen>
|
|
1072
|
+
<label className="dm-record-field">
|
|
1073
|
+
<span>Available site</span>
|
|
1074
|
+
<StaticSelect
|
|
1075
|
+
value={selectedUrl}
|
|
1076
|
+
disabled={!table.mutable || saving || loadingSites || sites.length === 0}
|
|
1077
|
+
placeholder={loadingSites ? "Loading Codex Sites..." : "Select Codex Site..."}
|
|
1078
|
+
options={sites.map((site) => ({
|
|
1079
|
+
value: site.url,
|
|
1080
|
+
label: site.Name,
|
|
1081
|
+
source: site.url,
|
|
1082
|
+
}))}
|
|
1083
|
+
onChange={selectSite}
|
|
1084
|
+
/>
|
|
1085
|
+
{sitesMessage && <span className="dm-cell-empty">{sitesMessage}</span>}
|
|
1086
|
+
</label>
|
|
1087
|
+
{selectedUrl && (
|
|
1088
|
+
<a className="dm-btn-outline dm-codex-sites-open-link" href={selectedUrl} target="_blank" rel="noreferrer">
|
|
1089
|
+
<Link2 size={13} />Open selected site
|
|
1090
|
+
</a>
|
|
1091
|
+
)}
|
|
1092
|
+
</DrawerSection>
|
|
1093
|
+
<DrawerSection title="Bound Row" defaultOpen>
|
|
1094
|
+
{["Name", "app", "client", "url", "status", "accessMode", "dashboardId", "lastRecordedAt", "notes"].map((column) => (
|
|
1095
|
+
<RecordFieldEditor
|
|
1096
|
+
key={column}
|
|
1097
|
+
table={table}
|
|
1098
|
+
tables={[]}
|
|
1099
|
+
column={column}
|
|
1100
|
+
value={String(draft?.[column] ?? "")}
|
|
1101
|
+
saving={saving}
|
|
1102
|
+
editable={false}
|
|
1103
|
+
onDraft={() => {}}
|
|
1104
|
+
onCommit={() => {}}
|
|
1105
|
+
onExpandJson={() => {}}
|
|
1106
|
+
/>
|
|
1107
|
+
))}
|
|
1108
|
+
</DrawerSection>
|
|
1109
|
+
</div>
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1018
1113
|
function DataModelRecordDrawer({
|
|
1019
1114
|
table,
|
|
1020
1115
|
tables,
|
|
@@ -1086,6 +1181,7 @@ function DataModelRecordDrawer({
|
|
|
1086
1181
|
|
|
1087
1182
|
const isApiRegistry = table.objectType === "api-registry";
|
|
1088
1183
|
const isSandbox = table.objectType === "sandbox-environment";
|
|
1184
|
+
const isCodexSitesObject = table.objectId === CODEX_SITES_OBJECT_ID;
|
|
1089
1185
|
const isDirty = JSON.stringify(draft || {}) !== JSON.stringify(row || {}) || JSON.stringify(pendingColumns) !== JSON.stringify(table.columns || []) || JSON.stringify(pendingHidden) !== JSON.stringify(table.fieldSettings?.hidden || []);
|
|
1090
1186
|
|
|
1091
1187
|
function updateField(column, value) {
|
|
@@ -1561,6 +1657,15 @@ function DataModelRecordDrawer({
|
|
|
1561
1657
|
onOpenGraphSidecar={openWorkflowView}
|
|
1562
1658
|
onOpenTraceSidecar={openTraceSidecar}
|
|
1563
1659
|
/>
|
|
1660
|
+
) : isCodexSitesObject ? (
|
|
1661
|
+
<CodexSitesRecordFields
|
|
1662
|
+
draft={draft}
|
|
1663
|
+
setDraft={setDraft}
|
|
1664
|
+
table={table}
|
|
1665
|
+
saving={saving}
|
|
1666
|
+
onSave={onSave}
|
|
1667
|
+
rowIndex={rowIndex}
|
|
1668
|
+
/>
|
|
1564
1669
|
) : groupRecordColumns(table.columns || []).map((section) => (
|
|
1565
1670
|
<DrawerSection key={section.title} title={section.title}>
|
|
1566
1671
|
{section.columns.map((column) => (
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css
CHANGED
|
@@ -857,7 +857,8 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
857
857
|
.workspace-dashboard-title {
|
|
858
858
|
min-width: 0;
|
|
859
859
|
}
|
|
860
|
-
.workspace-dashboard-title button
|
|
860
|
+
.workspace-dashboard-title button,
|
|
861
|
+
.workspace-dashboard-title a {
|
|
861
862
|
min-width: 0;
|
|
862
863
|
display: inline-flex;
|
|
863
864
|
align-items: center;
|
|
@@ -870,6 +871,7 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
870
871
|
overflow: hidden;
|
|
871
872
|
padding: 0;
|
|
872
873
|
text-align: left;
|
|
874
|
+
text-decoration: none;
|
|
873
875
|
text-overflow: ellipsis;
|
|
874
876
|
white-space: nowrap;
|
|
875
877
|
}
|
|
@@ -953,17 +955,24 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
953
955
|
background: #ffffff;
|
|
954
956
|
box-shadow: 0 12px 30px rgba(15, 23, 42, .16);
|
|
955
957
|
}
|
|
956
|
-
.workspace-row-action-menu button
|
|
958
|
+
.workspace-row-action-menu button,
|
|
959
|
+
.workspace-row-action-menu a {
|
|
957
960
|
width: 100%;
|
|
958
961
|
min-height: 30px;
|
|
962
|
+
display: inline-flex;
|
|
963
|
+
align-items: center;
|
|
959
964
|
justify-content: flex-start;
|
|
960
965
|
border: 0;
|
|
961
966
|
border-radius: 6px;
|
|
962
967
|
background: transparent;
|
|
963
968
|
color: #344054;
|
|
969
|
+
font: inherit;
|
|
970
|
+
font-size: 12px;
|
|
964
971
|
padding: 0 9px;
|
|
972
|
+
text-decoration: none;
|
|
965
973
|
}
|
|
966
|
-
.workspace-row-action-menu button:hover
|
|
974
|
+
.workspace-row-action-menu button:hover,
|
|
975
|
+
.workspace-row-action-menu a:hover {
|
|
967
976
|
background: #f5f5f5;
|
|
968
977
|
border-color: transparent;
|
|
969
978
|
}
|
|
@@ -3079,6 +3088,55 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
3079
3088
|
border-top: 1px solid #eeeeec;
|
|
3080
3089
|
padding: 20px 0;
|
|
3081
3090
|
}
|
|
3091
|
+
.workspace-settings-accordion {
|
|
3092
|
+
gap: 12px;
|
|
3093
|
+
}
|
|
3094
|
+
.workspace-settings-accordion-trigger {
|
|
3095
|
+
width: 100%;
|
|
3096
|
+
min-height: 34px;
|
|
3097
|
+
display: flex;
|
|
3098
|
+
align-items: center;
|
|
3099
|
+
justify-content: space-between;
|
|
3100
|
+
gap: 14px;
|
|
3101
|
+
border: 0;
|
|
3102
|
+
background: transparent;
|
|
3103
|
+
color: inherit;
|
|
3104
|
+
cursor: pointer;
|
|
3105
|
+
font: inherit;
|
|
3106
|
+
padding: 0;
|
|
3107
|
+
text-align: left;
|
|
3108
|
+
}
|
|
3109
|
+
.workspace-settings-accordion-trigger > span {
|
|
3110
|
+
min-width: 0;
|
|
3111
|
+
display: grid;
|
|
3112
|
+
gap: 3px;
|
|
3113
|
+
}
|
|
3114
|
+
.workspace-settings-accordion-trigger h3 {
|
|
3115
|
+
margin: 0;
|
|
3116
|
+
}
|
|
3117
|
+
.workspace-settings-accordion-trigger em {
|
|
3118
|
+
min-width: 0;
|
|
3119
|
+
display: block;
|
|
3120
|
+
color: #888;
|
|
3121
|
+
font-size: 12px;
|
|
3122
|
+
font-style: normal;
|
|
3123
|
+
overflow: hidden;
|
|
3124
|
+
text-overflow: ellipsis;
|
|
3125
|
+
white-space: nowrap;
|
|
3126
|
+
}
|
|
3127
|
+
.workspace-settings-accordion-trigger svg {
|
|
3128
|
+
flex: 0 0 auto;
|
|
3129
|
+
color: #777;
|
|
3130
|
+
transition: transform .16s ease;
|
|
3131
|
+
}
|
|
3132
|
+
.workspace-settings-accordion.is-open .workspace-settings-accordion-trigger svg {
|
|
3133
|
+
transform: rotate(180deg);
|
|
3134
|
+
}
|
|
3135
|
+
.workspace-settings-accordion-body {
|
|
3136
|
+
min-height: 0;
|
|
3137
|
+
display: grid;
|
|
3138
|
+
gap: 12px;
|
|
3139
|
+
}
|
|
3082
3140
|
.workspace-apps-linkage-section {
|
|
3083
3141
|
flex: 0 0 auto;
|
|
3084
3142
|
padding-bottom: 16px;
|
|
@@ -3089,6 +3147,9 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
3089
3147
|
grid-template-rows: auto minmax(0, 1fr);
|
|
3090
3148
|
padding-bottom: 0;
|
|
3091
3149
|
}
|
|
3150
|
+
.workspace-apps-list-section .workspace-settings-accordion-body {
|
|
3151
|
+
min-height: 0;
|
|
3152
|
+
}
|
|
3092
3153
|
.workspace-settings-section:first-of-type {
|
|
3093
3154
|
border-top: 0;
|
|
3094
3155
|
padding-top: 0;
|
|
@@ -3577,6 +3638,52 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
3577
3638
|
font-size: 12px;
|
|
3578
3639
|
line-height: 1.4;
|
|
3579
3640
|
}
|
|
3641
|
+
.workspace-settings-action {
|
|
3642
|
+
min-height: 32px;
|
|
3643
|
+
display: inline-flex;
|
|
3644
|
+
align-items: center;
|
|
3645
|
+
gap: 6px;
|
|
3646
|
+
border: 1px solid #dedede;
|
|
3647
|
+
border-radius: 6px;
|
|
3648
|
+
background: #fff;
|
|
3649
|
+
color: #333;
|
|
3650
|
+
font: inherit;
|
|
3651
|
+
font-size: 12px;
|
|
3652
|
+
padding: 0 10px;
|
|
3653
|
+
}
|
|
3654
|
+
.workspace-settings-codex-sites-list {
|
|
3655
|
+
display: grid;
|
|
3656
|
+
gap: 6px;
|
|
3657
|
+
padding: 0 14px 0;
|
|
3658
|
+
}
|
|
3659
|
+
.workspace-settings-codex-sites-list a,
|
|
3660
|
+
.workspace-settings-codex-sites-list button {
|
|
3661
|
+
min-height: 34px;
|
|
3662
|
+
display: grid;
|
|
3663
|
+
grid-template-columns: minmax(0, 1fr) minmax(0, .8fr) auto;
|
|
3664
|
+
gap: 10px;
|
|
3665
|
+
align-items: center;
|
|
3666
|
+
border: 1px solid #ececea;
|
|
3667
|
+
border-radius: 6px;
|
|
3668
|
+
background: #fff;
|
|
3669
|
+
color: #333;
|
|
3670
|
+
font: inherit;
|
|
3671
|
+
font-size: 12px;
|
|
3672
|
+
text-align: left;
|
|
3673
|
+
text-decoration: none;
|
|
3674
|
+
padding: 0 10px;
|
|
3675
|
+
}
|
|
3676
|
+
.workspace-settings-codex-sites-list span,
|
|
3677
|
+
.workspace-settings-codex-sites-list em {
|
|
3678
|
+
min-width: 0;
|
|
3679
|
+
overflow: hidden;
|
|
3680
|
+
text-overflow: ellipsis;
|
|
3681
|
+
white-space: nowrap;
|
|
3682
|
+
}
|
|
3683
|
+
.workspace-settings-codex-sites-list em {
|
|
3684
|
+
color: #777;
|
|
3685
|
+
font-style: normal;
|
|
3686
|
+
}
|
|
3580
3687
|
|
|
3581
3688
|
/* Source picker */
|
|
3582
3689
|
.workspace-source-list {
|
|
@@ -4639,7 +4746,7 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
4639
4746
|
.dm-detail-v2-title h2 { margin: 0; font-size: 15px; font-weight: 600; color: #111827; flex: 1; }
|
|
4640
4747
|
.dm-detail-v2-meta { display: flex; align-items: center; gap: 10px; padding-left: 21px; font-size: 12px; color: #9ca3af; }
|
|
4641
4748
|
.dm-detail-v2-meta code { font-size: 11px; color: #6b7280; background: #f3f4f6; border-radius: 4px; padding: 2px 6px; }
|
|
4642
|
-
.dm-detail-v3 { border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; overflow:
|
|
4749
|
+
.dm-detail-v3 { border: 1px solid #e5e7eb; border-radius: 12px; background: #fff; overflow: visible; }
|
|
4643
4750
|
.dm-picker { position: relative; min-width: 280px; }
|
|
4644
4751
|
.dm-picker-trigger { width: min(420px, 100%); display: inline-flex; align-items: center; gap: 10px; min-height: 40px; border: 1px solid #dbe2ea; border-radius: 10px; background: #fff; color: #0f172a; box-shadow: 0 1px 2px rgba(15,23,42,.05); font: inherit; padding: 0 12px; cursor: pointer; text-align: left; }
|
|
4645
4752
|
.dm-picker.open .dm-picker-trigger { border-color: #94a3b8; box-shadow: 0 0 0 4px rgba(148,163,184,.14); }
|
|
@@ -4680,6 +4787,7 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
4680
4787
|
.workspace-toolbar-actions .dm-picker-popover {
|
|
4681
4788
|
left: auto;
|
|
4682
4789
|
right: 0;
|
|
4790
|
+
z-index: 220;
|
|
4683
4791
|
}
|
|
4684
4792
|
|
|
4685
4793
|
/* Active-object title block in the workspace toolbar — replaces the
|
|
@@ -4729,13 +4837,13 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
4729
4837
|
font-weight: 400;
|
|
4730
4838
|
color: #6b7280;
|
|
4731
4839
|
}
|
|
4732
|
-
.dm-picker-popover { position: absolute; top: calc(100% + 8px); left: 0; z-index:
|
|
4840
|
+
.dm-picker-popover { position: absolute; top: calc(100% + 8px); left: 0; z-index: 220; width: min(360px, calc(100vw - 32px)); max-height: min(520px, calc(100vh - 120px)); display: grid; gap: 10px; padding: 12px; border: 1px solid #dbe2ea; border-radius: 14px; background: #fff; box-shadow: 0 28px 80px rgba(15,23,42,.24); overflow-y: auto; overflow-x: visible; }
|
|
4733
4841
|
.dm-picker-tabs { display: inline-flex; gap: 6px; padding: 4px; border-radius: 999px; background: #f8fafc; }
|
|
4734
4842
|
.dm-picker-tabs button { height: 28px; border: 0; border-radius: 999px; background: transparent; color: #64748b; font: inherit; font-size: 12px; padding: 0 10px; cursor: pointer; }
|
|
4735
4843
|
.dm-picker-tabs button.active { background: #fff; color: #111827; box-shadow: 0 1px 2px rgba(15,23,42,.08); }
|
|
4736
4844
|
.dm-picker-section { display: grid; gap: 8px; }
|
|
4737
4845
|
.dm-picker-section > p { margin: 0; font-size: 11px; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: .06em; }
|
|
4738
|
-
.dm-picker-scroll { display: grid; gap: 4px; max-height:
|
|
4846
|
+
.dm-picker-scroll { display: grid; gap: 4px; max-height: none; overflow: visible; padding-right: 10px; scrollbar-gutter: stable; }
|
|
4739
4847
|
.dm-picker-item { position: relative; display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 8px; align-items: center; }
|
|
4740
4848
|
.dm-picker-row { width: 100%; display: inline-flex; align-items: center; gap: 8px; min-width: 0; min-height: 34px; border: 0; border-radius: 9px; background: transparent; color: #334155; font: inherit; font-size: 12px; font-weight: 500; padding: 0 10px; cursor: pointer; text-align: left; }
|
|
4741
4849
|
.dm-picker-row:hover, .dm-picker-item.active .dm-picker-row { background: #f1f5f9; color: #111827; }
|
|
@@ -4746,7 +4854,7 @@ body.workspace-rail-collapsed .workspace-builder.dm-workflow-page {
|
|
|
4746
4854
|
.dm-picker-icon-btn:hover { color: #111827; border-color: #cbd5e1; }
|
|
4747
4855
|
.dm-picker-icon-btn.danger:hover { color: #b91c1c; border-color: #fecaca; background: #fef2f2; }
|
|
4748
4856
|
.dm-picker-lock { margin-left: auto; color: #94a3b8; }
|
|
4749
|
-
.dm-picker-menu { position: absolute; top: calc(100% + 6px); right: 0; z-index:
|
|
4857
|
+
.dm-picker-menu { position: absolute; top: calc(100% + 6px); right: 0; z-index: 240; display: grid; gap: 4px; min-width: 156px; padding: 8px; border: 1px solid #dbe2ea; border-radius: 10px; background: #fff; box-shadow: 0 20px 44px rgba(15,23,42,.2), 0 4px 12px rgba(15,23,42,.08); }
|
|
4750
4858
|
.dm-picker-menu button { display: inline-flex; align-items: center; gap: 8px; height: 30px; border: 0; border-radius: 7px; background: transparent; color: #334155; font: inherit; font-size: 12px; padding: 0 10px; cursor: pointer; text-align: left; }
|
|
4751
4859
|
.dm-picker-menu button:hover { background: #f1f5f9; color: #111827; }
|
|
4752
4860
|
.dm-picker-menu button.danger:hover { background: #fef2f2; color: #b91c1c; }
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ExternalLink, Rocket } from "lucide-react";
|
|
4
|
+
import {
|
|
5
|
+
CODEX_SITES_OBJECT_ID,
|
|
6
|
+
ensureCodexSitesDataModel,
|
|
7
|
+
isCodexSiteUrl
|
|
8
|
+
} from "@/lib/codex-sites-workspace-adapter";
|
|
9
|
+
import { SettingsAccordionSection } from "./settings-accordion-section.jsx";
|
|
10
|
+
|
|
11
|
+
function CodexSitesDataModelCard({ apps, dataModel }) {
|
|
12
|
+
const objects = Array.isArray(dataModel?.objects) ? dataModel.objects : [];
|
|
13
|
+
const object = objects.find((item) => item?.id === CODEX_SITES_OBJECT_ID) || null;
|
|
14
|
+
const rows = Array.isArray(object?.rows) ? object.rows : [];
|
|
15
|
+
const liveRows = rows.filter((row) => {
|
|
16
|
+
const status = String(row?.status || "").toLowerCase();
|
|
17
|
+
return isCodexSiteUrl(row?.url) && (status === "live" || status === "active");
|
|
18
|
+
});
|
|
19
|
+
const liveCount = liveRows.length;
|
|
20
|
+
const appCount = new Set(rows.map((row) => row?.app).filter(Boolean)).size;
|
|
21
|
+
|
|
22
|
+
async function openDataModel() {
|
|
23
|
+
if (!object) {
|
|
24
|
+
const nextDataModel = ensureCodexSitesDataModel(dataModel, apps);
|
|
25
|
+
const response = await fetch("/api/workspace", {
|
|
26
|
+
method: "PATCH",
|
|
27
|
+
headers: { "content-type": "application/json" },
|
|
28
|
+
body: JSON.stringify({ dataModel: nextDataModel })
|
|
29
|
+
});
|
|
30
|
+
if (!response.ok) {
|
|
31
|
+
const payload = await response.json().catch(() => ({}));
|
|
32
|
+
window.alert(payload.error || "Failed to create Codex Sites object.");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
window.location.href = `/data-model?object=${encodeURIComponent(CODEX_SITES_OBJECT_ID)}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return <SettingsAccordionSection
|
|
40
|
+
id="codex-sites"
|
|
41
|
+
title="Codex Sites"
|
|
42
|
+
summary={`${rows.length} site${rows.length === 1 ? "" : "s"} · ${liveCount} live · ${appCount || apps?.length || 0} app${(appCount || apps?.length || 0) === 1 ? "" : "s"}`}
|
|
43
|
+
className="workspace-apps-linkage-section workspace-codex-sites-section"
|
|
44
|
+
>
|
|
45
|
+
<div className="workspace-app-row">
|
|
46
|
+
<span className="workspace-provider-mark"><Rocket size={15} /></span>
|
|
47
|
+
<div>
|
|
48
|
+
<strong>Codex Sites</strong>
|
|
49
|
+
<p>Manage Codex-hosted site URLs as a governed custom Data Model object attached to workspace apps and clients.</p>
|
|
50
|
+
<div className="workspace-integration-meta">
|
|
51
|
+
<span>{object ? "configured" : "not configured"}</span>
|
|
52
|
+
<span>{rows.length} site{rows.length === 1 ? "" : "s"}</span>
|
|
53
|
+
<span>{liveCount} live</span>
|
|
54
|
+
<span>{appCount || apps?.length || 0} app{(appCount || apps?.length || 0) === 1 ? "" : "s"}</span>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
<button type="button" className="workspace-settings-action" onClick={openDataModel}>
|
|
58
|
+
<ExternalLink size={14} />{object ? "Manage" : "Set up"}
|
|
59
|
+
</button>
|
|
60
|
+
</div>
|
|
61
|
+
{rows.length ? <div className="workspace-settings-codex-sites-list">
|
|
62
|
+
{rows.slice(0, 4).map((row, index) => isCodexSiteUrl(row?.url) ? (
|
|
63
|
+
<a key={row.id || row.Name || index} href={row.url} target="_blank" rel="noreferrer">
|
|
64
|
+
<span>{row.Name || `Site ${index + 1}`}</span>
|
|
65
|
+
<em>{row.client || "Workspace"} · {row.app || "apps/workspace"}</em>
|
|
66
|
+
<ExternalLink size={13} />
|
|
67
|
+
</a>
|
|
68
|
+
) : (
|
|
69
|
+
<button key={row.id || row.Name || index} type="button" onClick={openDataModel}>
|
|
70
|
+
<span>{row.Name || `Site ${index + 1}`}</span>
|
|
71
|
+
<em>{row.client || "Workspace"} · {row.status || "draft"}</em>
|
|
72
|
+
</button>
|
|
73
|
+
))}
|
|
74
|
+
</div> : null}
|
|
75
|
+
</SettingsAccordionSection>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export {
|
|
79
|
+
CODEX_SITES_OBJECT_ID,
|
|
80
|
+
CodexSitesDataModelCard
|
|
81
|
+
};
|
|
@@ -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>
|
|
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
|
-
<
|
|
90
|
-
<
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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 };
|
|
@@ -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">
|
|
@@ -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
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthub/cli",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.9",
|
|
4
4
|
"description": "CLI control plane for Growthub Local and Agent Workspace as Code: export, fork, inspect, operate, sync, and optionally activate governed AI workspaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|