@growthub/cli 0.9.8 → 0.9.10

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 (19) hide show
  1. package/README.md +23 -5
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +8 -2
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -8
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +9 -7
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/settings/integrations/route.js +2 -2
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +4 -4
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1264 -19
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +111 -77
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +1691 -138
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +8 -3
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +9 -7
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +10 -10
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +2 -2
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +62 -7
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +220 -2
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package-lock.json +10 -64
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +3 -0
  19. package/package.json +1 -1
@@ -1,7 +1,12 @@
1
1
  {
2
2
  "id": "workspace-builder-default",
3
- "name": "Workspace Builder",
4
- "description": "Default no-code composition for the official Growthub Custom Workspace Starter. The interface starts as a configurable dashboard-builder shell and can be customized by editing this file or by future governed UI controls.",
3
+ "name": "Growthub Workspace",
4
+ "description": "Default no-code composition for the official Growthub Custom Workspace Starter. The interface starts as a governed dashboard workspace and can be customized by editing this file or by future governed UI controls.",
5
+ "branding": {
6
+ "name": "Growthub Workspace",
7
+ "logoUrl": "",
8
+ "accent": "#3f68ff"
9
+ },
5
10
  "capabilities": [
6
11
  "dashboards",
7
12
  "canvas",
@@ -47,7 +52,7 @@
47
52
  },
48
53
  "provenance": {
49
54
  "createdBy": "cli",
50
- "mirrors": "growthub-agency-portal-starter-v1",
55
+ "mirrors": "growthub-custom-workspace-starter-v1",
51
56
  "note": "Shipped with growthub-custom-workspace-starter-v1; safe to edit inside a governed fork and validate through the starter export smoke path."
52
57
  }
53
58
  }
@@ -1,11 +1,11 @@
1
1
  function readAdapterConfig() {
2
2
  return {
3
3
  deployTarget: "vercel",
4
- dataAdapter: readEnum("AGENCY_PORTAL_DATA_ADAPTER", ["postgres", "qstash-kv", "provider-managed"], "provider-managed"),
5
- authAdapter: readEnum("AGENCY_PORTAL_AUTH_ADAPTER", ["oidc", "clerk", "authjs", "provider-managed"], "provider-managed"),
6
- paymentAdapter: readEnum("AGENCY_PORTAL_PAYMENT_ADAPTER", ["none", "stripe", "polar"], "none"),
7
- integrationAdapter: readEnum("AGENCY_PORTAL_INTEGRATION_ADAPTER", ["growthub-bridge", "byo-api-key", "static"], "static"),
8
- reportingAdapter: process.env.AGENCY_PORTAL_REPORTING_ADAPTER || void 0,
4
+ dataAdapter: readEnum(["GROWTHUB_WORKSPACE_DATA_ADAPTER", "AGENCY_PORTAL_DATA_ADAPTER"], ["postgres", "qstash-kv", "provider-managed"], "provider-managed"),
5
+ authAdapter: readEnum(["GROWTHUB_WORKSPACE_AUTH_ADAPTER", "AGENCY_PORTAL_AUTH_ADAPTER"], ["oidc", "clerk", "authjs", "provider-managed"], "provider-managed"),
6
+ paymentAdapter: readEnum(["GROWTHUB_WORKSPACE_PAYMENT_ADAPTER", "AGENCY_PORTAL_PAYMENT_ADAPTER"], ["none", "stripe", "polar"], "none"),
7
+ integrationAdapter: readEnum(["GROWTHUB_WORKSPACE_INTEGRATION_ADAPTER", "AGENCY_PORTAL_INTEGRATION_ADAPTER"], ["growthub-bridge", "byo-api-key", "static"], "static"),
8
+ reportingAdapter: process.env.GROWTHUB_WORKSPACE_REPORTING_ADAPTER || process.env.AGENCY_PORTAL_REPORTING_ADAPTER || void 0,
9
9
  growthubBridge: {
10
10
  baseUrl: process.env.GROWTHUB_BRIDGE_BASE_URL || void 0,
11
11
  integrationsPath: process.env.GROWTHUB_BRIDGE_INTEGRATIONS_PATH || "/api/mcp/accounts",
@@ -17,8 +17,10 @@ function readAdapterConfig() {
17
17
  }
18
18
  };
19
19
  }
20
- function readEnum(key, allowed, fallback) {
21
- const value = process.env[key];
20
+ function readEnum(keys, allowed, fallback) {
21
+ const keyList = Array.isArray(keys) ? keys : [keys];
22
+ const key = keyList.find((candidate) => process.env[candidate]);
23
+ const value = key ? process.env[key] : undefined;
22
24
  if (!value) return fallback;
23
25
  if (allowed.includes(value)) return value;
24
26
  throw new Error(`${key} must be one of: ${allowed.join(", ")}`);
@@ -1,6 +1,6 @@
1
1
  import { readAdapterConfig } from "@/lib/adapters/env";
2
2
  import {
3
- agencyPortalIntegrationCatalog
3
+ governedWorkspaceIntegrationCatalog
4
4
  } from "@/lib/domain/integrations";
5
5
  import {
6
6
  normalizeGrowthubBridgePayload
@@ -19,7 +19,7 @@ function describeIntegrationAdapter() {
19
19
  return {
20
20
  id: "byo-api-key",
21
21
  label: "Bring your own API key",
22
- requiredEnv: ["AGENCY_PORTAL_BYO_CONNECTIONS_JSON"],
22
+ requiredEnv: ["GROWTHUB_WORKSPACE_BYO_CONNECTIONS_JSON"],
23
23
  authority: "workspace-env"
24
24
  };
25
25
  }
@@ -30,16 +30,16 @@ function describeIntegrationAdapter() {
30
30
  authority: "local-catalog"
31
31
  };
32
32
  }
33
- async function listAgencyPortalIntegrations() {
33
+ async function listGovernedWorkspaceIntegrations() {
34
34
  const config = readAdapterConfig();
35
35
  if (config.integrationAdapter !== "growthub-bridge") {
36
36
  if (config.integrationAdapter === "byo-api-key") {
37
37
  return mergeBringYourOwnRows(readBringYourOwnRows());
38
38
  }
39
- return agencyPortalIntegrationCatalog;
39
+ return governedWorkspaceIntegrationCatalog;
40
40
  }
41
41
  if (!config.growthubBridge.baseUrl || !process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN) {
42
- return agencyPortalIntegrationCatalog;
42
+ return governedWorkspaceIntegrationCatalog;
43
43
  }
44
44
  const url = new URL(config.growthubBridge.integrationsPath, config.growthubBridge.baseUrl);
45
45
  const headers = {
@@ -54,7 +54,7 @@ async function listAgencyPortalIntegrations() {
54
54
  next: { revalidate: 30 }
55
55
  });
56
56
  if (!response.ok) {
57
- return agencyPortalIntegrationCatalog;
57
+ return governedWorkspaceIntegrationCatalog;
58
58
  }
59
59
  const payload = await response.json();
60
60
  const merged = mergeBridgeRows(normalizeGrowthubBridgePayload(payload));
@@ -80,7 +80,7 @@ function applyApiKeyOverlays(integrations, config) {
80
80
  });
81
81
  }
82
82
  function readBringYourOwnRows() {
83
- const raw = process.env.AGENCY_PORTAL_BYO_CONNECTIONS_JSON;
83
+ const raw = process.env.GROWTHUB_WORKSPACE_BYO_CONNECTIONS_JSON || process.env.AGENCY_PORTAL_BYO_CONNECTIONS_JSON;
84
84
  const rows = [];
85
85
  if (process.env.WINDSOR_API_KEY) {
86
86
  rows.push({
@@ -129,7 +129,7 @@ function mergeBringYourOwnRows(rows) {
129
129
  }
130
130
  function mergeBridgeRows(rows) {
131
131
  const seenProviders = /* @__PURE__ */ new Set();
132
- const merged = agencyPortalIntegrationCatalog.map((catalogItem) => {
132
+ const merged = governedWorkspaceIntegrationCatalog.map((catalogItem) => {
133
133
  const row = rows.find((item) => {
134
134
  const provider = item.provider || item.id;
135
135
  return provider === catalogItem.provider || item.id === catalogItem.id;
@@ -160,7 +160,7 @@ function mergeBridgeRows(rows) {
160
160
  const provider = row.provider || row.id;
161
161
  if (!provider) return false;
162
162
  if (seenProviders.has(provider)) return false;
163
- return !agencyPortalIntegrationCatalog.some((item) => item.provider === provider || item.id === row.id);
163
+ return !governedWorkspaceIntegrationCatalog.some((item) => item.provider === provider || item.id === row.id);
164
164
  });
165
165
  return [...merged, ...discoveredRows.map(toDiscoveredIntegration)];
166
166
  }
@@ -194,5 +194,5 @@ function toDiscoveredIntegration(row) {
194
194
  }
195
195
  export {
196
196
  describeIntegrationAdapter,
197
- listAgencyPortalIntegrations
197
+ listGovernedWorkspaceIntegrations
198
198
  };
@@ -172,7 +172,7 @@ const workspaceIntegrations = [
172
172
  setupMode: "hosted-authority"
173
173
  }
174
174
  ];
175
- const agencyPortalIntegrationCatalog = [...dataSources, ...workspaceIntegrations];
175
+ const governedWorkspaceIntegrationCatalog = [...dataSources, ...workspaceIntegrations];
176
176
  function groupIntegrationsByLane(integrations) {
177
177
  return {
178
178
  dataSources: integrations.filter((item) => item.lane === "data-source"),
@@ -180,6 +180,6 @@ function groupIntegrationsByLane(integrations) {
180
180
  };
181
181
  }
182
182
  export {
183
- agencyPortalIntegrationCatalog,
183
+ governedWorkspaceIntegrationCatalog,
184
184
  groupIntegrationsByLane
185
185
  };
@@ -1,3 +1,22 @@
1
+ /**
2
+ * Workspace persistence — thin adapter boundary.
3
+ *
4
+ * Modes (Workspace Builder Runtime V1 contract — `docs/WORKSPACE_BUILDER_RUNTIME_V1.md`):
5
+ *
6
+ * - `filesystem` Local Next.js dev or any runtime that opts in via
7
+ * `WORKSPACE_CONFIG_ALLOW_FS_WRITE=true`. Save writes
8
+ * `growthub.config.json` on disk.
9
+ * - `read-only` Vercel / Netlify-style runtimes where the bundle is
10
+ * immutable. `PATCH /api/workspace` returns 409 with the
11
+ * same `guidance` string the no-code Save UI surfaces.
12
+ * - `database` (future) Reserved adapter slot. Not implemented in V1 — the
13
+ * return shape is stable so a hosted adapter can be wired
14
+ * without changing UI or API contracts.
15
+ *
16
+ * `describePersistenceMode()` is the single source of truth the GET payload,
17
+ * the no-code Settings/Readiness panel, and the PATCH 409 path all read.
18
+ */
19
+
1
20
  import { promises as fs } from "node:fs";
2
21
  import path from "node:path";
3
22
  import { readAdapterConfig } from "@/lib/adapters/env";
@@ -8,6 +27,15 @@ import {
8
27
  validateWorkspaceConfig
9
28
  } from "@/lib/workspace-schema";
10
29
 
30
+ const PERSISTENCE_ADAPTERS = Object.freeze({
31
+ FILESYSTEM: "filesystem",
32
+ READ_ONLY: "read-only",
33
+ DATABASE: "database"
34
+ });
35
+
36
+ const READ_ONLY_GUIDANCE =
37
+ "Edit growthub.config.json locally, or set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true on a writable runtime.";
38
+
11
39
  function resolveWorkspaceConfigPath() {
12
40
  return path.resolve(/*turbopackIgnore: true*/ process.cwd(), "growthub.config.json");
13
41
  }
@@ -18,23 +46,47 @@ async function readWorkspaceConfig() {
18
46
  return JSON.parse(raw);
19
47
  }
20
48
 
49
+ /**
50
+ * `canSave` is a *logical* statement about adapter mode, not a *filesystem*
51
+ * guarantee. A `filesystem`-mode workspace whose `growthub.config.json` is
52
+ * actually read-only on disk (permission denied, RO mount) will still report
53
+ * `canSave: true`; the no-code Save UI surfaces the underlying fs error
54
+ * (workspace-builder.jsx#save → setConfigMessage) and PATCH returns 500 with
55
+ * the original error message. Read-only-mode 409 is the *contractual* not-save
56
+ * path and gets verbatim `guidance` instead.
57
+ */
21
58
  function describePersistenceMode() {
22
- const target = process.env.AGENCY_PORTAL_DEPLOY_TARGET || "vercel";
59
+ const target = process.env.GROWTHUB_WORKSPACE_DEPLOY_TARGET || process.env.AGENCY_PORTAL_DEPLOY_TARGET || "vercel";
23
60
  const isReadOnlyDeploy = target === "vercel" || target === "netlify";
24
61
  const allowFsWrite = process.env.WORKSPACE_CONFIG_ALLOW_FS_WRITE === "true";
62
+ const baseFilesystem = (reason) => ({
63
+ mode: PERSISTENCE_ADAPTERS.FILESYSTEM,
64
+ adapter: PERSISTENCE_ADAPTERS.FILESYSTEM,
65
+ canSave: true,
66
+ saveLabel: "Save writes growthub.config.json on disk.",
67
+ reason,
68
+ nextAction: null,
69
+ guidance: null
70
+ });
25
71
  if (allowFsWrite) {
26
- return { mode: "filesystem", reason: "WORKSPACE_CONFIG_ALLOW_FS_WRITE=true" };
72
+ return baseFilesystem("WORKSPACE_CONFIG_ALLOW_FS_WRITE=true");
27
73
  }
28
74
  if (process.env.NODE_ENV === "development") {
29
- return { mode: "filesystem", reason: "Local Next.js development" };
75
+ return baseFilesystem("Local Next.js development");
30
76
  }
31
77
  if (isReadOnlyDeploy) {
78
+ const reason = `Deploy target ${target} treats the bundle as read-only. Set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true on a writable runtime, or wire a hosted persistence adapter.`;
32
79
  return {
33
- mode: "read-only",
34
- reason: `Deploy target ${target} treats the bundle as read-only. Set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true on a writable runtime, or wire a hosted persistence adapter.`
80
+ mode: PERSISTENCE_ADAPTERS.READ_ONLY,
81
+ adapter: PERSISTENCE_ADAPTERS.READ_ONLY,
82
+ canSave: false,
83
+ saveLabel: "Save is disabled in this runtime.",
84
+ reason,
85
+ nextAction: "Set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true or connect a persistence adapter.",
86
+ guidance: READ_ONLY_GUIDANCE
35
87
  };
36
88
  }
37
- return { mode: "filesystem", reason: "Local development" };
89
+ return baseFilesystem("Local development");
38
90
  }
39
91
 
40
92
  function applyPatch(currentConfig, patch) {
@@ -76,10 +128,11 @@ function applyPatch(currentConfig, patch) {
76
128
  async function writeWorkspaceConfig(patch) {
77
129
  const persistence = describePersistenceMode();
78
130
  const adapter = readAdapterConfig();
79
- if (persistence.mode !== "filesystem") {
131
+ if (persistence.mode !== PERSISTENCE_ADAPTERS.FILESYSTEM || !persistence.canSave) {
80
132
  const error = new Error(persistence.reason);
81
133
  error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
82
134
  error.adapter = adapter.integrationAdapter;
135
+ error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
83
136
  throw error;
84
137
  }
85
138
  const current = await readWorkspaceConfig();
@@ -104,6 +157,8 @@ export {
104
157
  GRID_COLUMNS,
105
158
  GRID_ROWS,
106
159
  KNOWN_WIDGET_KINDS,
160
+ PERSISTENCE_ADAPTERS,
161
+ READ_ONLY_GUIDANCE,
107
162
  describePersistenceMode,
108
163
  readWorkspaceConfig,
109
164
  resolveWorkspaceConfigPath,
@@ -1,8 +1,51 @@
1
+ /**
2
+ * Workspace Config Contract V1 — local source of truth.
3
+ *
4
+ * Authoritative reference: `docs/WORKSPACE_CONFIG_CONTRACT_V1.md`.
5
+ * Companion runtime doc: `docs/WORKSPACE_BUILDER_RUNTIME_V1.md`.
6
+ *
7
+ * This file owns the workspace config validator. The persisted file lives at
8
+ * `<workspace>/growthub.config.json`. The PATCH allowlist on `/api/workspace`
9
+ * is permanently restricted to:
10
+ *
11
+ * - `dashboards` dashboard rows (id, name, status, tabs, activeTabId)
12
+ * - `widgetTypes` palette of allowed widget kinds (label/icon)
13
+ * - `canvas` active canvas: layout, single-tab `widgets[]`, or
14
+ * multi-tab `tabs[]` + `activeTabId`, plus `bindings`
15
+ *
16
+ * Other top-level fields (`id`, `name`, `description`, `capabilities`,
17
+ * `branding`, `pipelines`, `integrations`, `provenance`) are preserved
18
+ * round-trip but cannot be mutated through PATCH. The validator rejects
19
+ * unknown fields inside the three allowlisted sections.
20
+ *
21
+ * Canonical canvas shape (mutually exclusive — never both at once):
22
+ *
23
+ * single-tab → `canvas.widgets[]` (DO NOT also serialize tabs)
24
+ * multi-tab → `canvas.tabs[]` + `activeTabId` (DO NOT also serialize widgets)
25
+ *
26
+ * Import/export envelope: `{ version: 1, kind: "growthub-workspace-template",
27
+ * exportedAt, source, name, description, payload }`. Raw `{dashboards,
28
+ * widgetTypes, canvas}` payloads are also accepted for back-compat.
29
+ *
30
+ * Widget grid is a strict 12-column × 16-row fixed lattice with
31
+ * non-overlapping integer rectangles. Widget IDs are minted at clone time —
32
+ * template widgets intentionally omit `id`.
33
+ *
34
+ * Validation errors are surfaced as readable strings on the thrown error
35
+ * (`error.details: string[]`) so agents and the no-code Save UI can round-trip
36
+ * them without parsing a stack trace.
37
+ */
38
+
1
39
  const GRID_COLUMNS = 12;
2
40
  const GRID_ROWS = 16;
3
41
  const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
4
42
  const KNOWN_FIELDS = ["dashboards", "widgetTypes", "canvas"];
5
- const KNOWN_DATA_BINDING_MODES = ["manual", "json", "csv"];
43
+ const KNOWN_DATA_BINDING_MODES = ["manual", "json", "csv", "integration"];
44
+ const KNOWN_CHART_TYPES = ["bar-vertical", "bar-horizontal", "line", "pie", "sum", "gauge"];
45
+ const KNOWN_FILTER_OPERATORS = ["eq", "ne", "contains", "gt", "lt", "isEmpty", "isNotEmpty"];
46
+ const KNOWN_FILTER_CONJUNCTIONS = ["and", "or"];
47
+ const KNOWN_SORT_DIRECTIONS = ["asc", "desc"];
48
+ const KNOWN_AGGREGATIONS = ["sum", "avg", "count", "min", "max"];
6
49
  const WORKSPACE_TEMPLATE_KIND = "growthub-workspace-template";
7
50
  const WORKSPACE_TEMPLATE_VERSION = 1;
8
51
  const WORKSPACE_TEMPLATE_SOURCE = "growthub-custom-workspace-starter-v1";
@@ -23,7 +66,12 @@ const WIDGET_SCHEMA_CONTRACTS = {
23
66
  config: "kind-specific config object"
24
67
  },
25
68
  ChartWidgetConfig: {
26
- values: "number[]",
69
+ values: "number[] (legacy preserved)",
70
+ chartType: `${KNOWN_CHART_TYPES.join(" | ")} optional, defaults to bar-vertical`,
71
+ xAxis: "ChartAxisConfig optional",
72
+ yAxis: "ChartAxisConfig optional",
73
+ style: "ChartStyleConfig optional",
74
+ filter: "FilterConfig optional",
27
75
  binding: "StaticDataBinding optional"
28
76
  },
29
77
  ViewWidgetConfig: {
@@ -31,8 +79,42 @@ const WIDGET_SCHEMA_CONTRACTS = {
31
79
  layout: "Table",
32
80
  columns: "string[]",
33
81
  rows: "record[]",
82
+ fieldSettings: "FieldSettingsConfig optional (hidden[], order[])",
83
+ sort: "SortClause[] optional ({ fieldId, direction })",
84
+ filter: "FilterConfig optional ({ op, clauses[] })",
34
85
  binding: "StaticDataBinding optional"
35
86
  },
87
+ ChartAxisConfig: {
88
+ field: "string optional",
89
+ sort: "string optional (asc | desc | position)",
90
+ aggregation: `${KNOWN_AGGREGATIONS.join(" | ")} optional`,
91
+ groupBy: "string optional",
92
+ omitZero: "boolean optional",
93
+ min: "string | number optional",
94
+ max: "string | number optional"
95
+ },
96
+ ChartStyleConfig: {
97
+ colors: "string optional (auto | manual swatch label)",
98
+ axisName: "string optional",
99
+ dataLabels: "boolean optional"
100
+ },
101
+ FieldSettingsConfig: {
102
+ hidden: "string[] of column names hidden from preview",
103
+ order: "string[] of column names defining custom order"
104
+ },
105
+ SortClause: {
106
+ fieldId: "non-empty string (column name)",
107
+ direction: KNOWN_SORT_DIRECTIONS.join(" | ")
108
+ },
109
+ FilterConfig: {
110
+ op: KNOWN_FILTER_CONJUNCTIONS.join(" | "),
111
+ clauses: "FilterClause[]"
112
+ },
113
+ FilterClause: {
114
+ fieldId: "non-empty string (column name)",
115
+ operator: KNOWN_FILTER_OPERATORS.join(" | "),
116
+ value: "string | number | boolean optional"
117
+ },
36
118
  IframeWidgetConfig: {
37
119
  url: "string"
38
120
  },
@@ -295,6 +377,127 @@ function validateStaticDataBinding(binding, path, errors) {
295
377
  if (binding.csv !== undefined && typeof binding.csv !== "string") {
296
378
  errors.push(`${path}.csv must be a string`);
297
379
  }
380
+ if (binding.integrationId !== undefined && typeof binding.integrationId !== "string") {
381
+ errors.push(`${path}.integrationId must be a string`);
382
+ }
383
+ if (binding.lane !== undefined && typeof binding.lane !== "string") {
384
+ errors.push(`${path}.lane must be a string`);
385
+ }
386
+ }
387
+
388
+ function validateFieldSettings(fieldSettings, path, errors) {
389
+ if (fieldSettings === undefined) return;
390
+ if (!isPlainObject(fieldSettings)) {
391
+ errors.push(`${path} must be a plain object`);
392
+ return;
393
+ }
394
+ if (fieldSettings.hidden !== undefined) validateStringArray(fieldSettings.hidden, `${path}.hidden`, errors);
395
+ if (fieldSettings.order !== undefined) validateStringArray(fieldSettings.order, `${path}.order`, errors);
396
+ }
397
+
398
+ function validateSortClauses(sort, path, errors) {
399
+ if (sort === undefined) return;
400
+ if (!Array.isArray(sort)) {
401
+ errors.push(`${path} must be an array`);
402
+ return;
403
+ }
404
+ sort.forEach((clause, index) => {
405
+ const prefix = `${path}[${index}]`;
406
+ if (!isPlainObject(clause)) {
407
+ errors.push(`${prefix} must be a plain object`);
408
+ return;
409
+ }
410
+ if (typeof clause.fieldId !== "string" || !clause.fieldId) {
411
+ errors.push(`${prefix}.fieldId must be a non-empty string`);
412
+ }
413
+ if (clause.direction !== undefined && !KNOWN_SORT_DIRECTIONS.includes(clause.direction)) {
414
+ errors.push(`${prefix}.direction must be one of ${KNOWN_SORT_DIRECTIONS.join(", ")}`);
415
+ }
416
+ });
417
+ }
418
+
419
+ function validateFilterClauses(filter, path, errors) {
420
+ if (filter === undefined) return;
421
+ if (!isPlainObject(filter)) {
422
+ errors.push(`${path} must be a plain object`);
423
+ return;
424
+ }
425
+ if (filter.op !== undefined && !KNOWN_FILTER_CONJUNCTIONS.includes(filter.op)) {
426
+ errors.push(`${path}.op must be one of ${KNOWN_FILTER_CONJUNCTIONS.join(", ")}`);
427
+ }
428
+ if (filter.clauses !== undefined) {
429
+ if (!Array.isArray(filter.clauses)) {
430
+ errors.push(`${path}.clauses must be an array`);
431
+ } else {
432
+ filter.clauses.forEach((clause, index) => {
433
+ const prefix = `${path}.clauses[${index}]`;
434
+ if (!isPlainObject(clause)) {
435
+ errors.push(`${prefix} must be a plain object`);
436
+ return;
437
+ }
438
+ if (typeof clause.fieldId !== "string" || !clause.fieldId) {
439
+ errors.push(`${prefix}.fieldId must be a non-empty string`);
440
+ }
441
+ if (clause.operator !== undefined && !KNOWN_FILTER_OPERATORS.includes(clause.operator)) {
442
+ errors.push(`${prefix}.operator must be one of ${KNOWN_FILTER_OPERATORS.join(", ")}`);
443
+ }
444
+ if (
445
+ clause.value !== undefined &&
446
+ typeof clause.value !== "string" &&
447
+ typeof clause.value !== "number" &&
448
+ typeof clause.value !== "boolean"
449
+ ) {
450
+ errors.push(`${prefix}.value must be a string, number, or boolean`);
451
+ }
452
+ });
453
+ }
454
+ }
455
+ }
456
+
457
+ function validateChartAxis(axis, path, errors) {
458
+ if (axis === undefined) return;
459
+ if (!isPlainObject(axis)) {
460
+ errors.push(`${path} must be a plain object`);
461
+ return;
462
+ }
463
+ if (axis.field !== undefined && typeof axis.field !== "string") {
464
+ errors.push(`${path}.field must be a string`);
465
+ }
466
+ if (axis.sort !== undefined && typeof axis.sort !== "string") {
467
+ errors.push(`${path}.sort must be a string`);
468
+ }
469
+ if (axis.aggregation !== undefined && !KNOWN_AGGREGATIONS.includes(axis.aggregation)) {
470
+ errors.push(`${path}.aggregation must be one of ${KNOWN_AGGREGATIONS.join(", ")}`);
471
+ }
472
+ if (axis.groupBy !== undefined && typeof axis.groupBy !== "string") {
473
+ errors.push(`${path}.groupBy must be a string`);
474
+ }
475
+ if (axis.omitZero !== undefined && typeof axis.omitZero !== "boolean") {
476
+ errors.push(`${path}.omitZero must be a boolean`);
477
+ }
478
+ if (axis.min !== undefined && typeof axis.min !== "string" && typeof axis.min !== "number") {
479
+ errors.push(`${path}.min must be a string or number`);
480
+ }
481
+ if (axis.max !== undefined && typeof axis.max !== "string" && typeof axis.max !== "number") {
482
+ errors.push(`${path}.max must be a string or number`);
483
+ }
484
+ }
485
+
486
+ function validateChartStyle(style, path, errors) {
487
+ if (style === undefined) return;
488
+ if (!isPlainObject(style)) {
489
+ errors.push(`${path} must be a plain object`);
490
+ return;
491
+ }
492
+ if (style.colors !== undefined && typeof style.colors !== "string") {
493
+ errors.push(`${path}.colors must be a string`);
494
+ }
495
+ if (style.axisName !== undefined && typeof style.axisName !== "string") {
496
+ errors.push(`${path}.axisName must be a string`);
497
+ }
498
+ if (style.dataLabels !== undefined && typeof style.dataLabels !== "boolean") {
499
+ errors.push(`${path}.dataLabels must be a boolean`);
500
+ }
298
501
  }
299
502
 
300
503
  function validateWidgetConfig(kind, config, path, errors) {
@@ -315,6 +518,13 @@ function validateWidgetConfig(kind, config, path, errors) {
315
518
  });
316
519
  }
317
520
  }
521
+ if (config.chartType !== undefined && !KNOWN_CHART_TYPES.includes(config.chartType)) {
522
+ errors.push(`${path}.chartType must be one of ${KNOWN_CHART_TYPES.join(", ")}`);
523
+ }
524
+ validateChartAxis(config.xAxis, `${path}.xAxis`, errors);
525
+ validateChartAxis(config.yAxis, `${path}.yAxis`, errors);
526
+ validateChartStyle(config.style, `${path}.style`, errors);
527
+ validateFilterClauses(config.filter, `${path}.filter`, errors);
318
528
  validateStaticDataBinding(config.binding, `${path}.binding`, errors);
319
529
  }
320
530
  if (kind === "view") {
@@ -322,6 +532,9 @@ function validateWidgetConfig(kind, config, path, errors) {
322
532
  if (config.layout !== undefined && config.layout !== "Table") errors.push(`${path}.layout must be Table`);
323
533
  if (config.columns !== undefined) validateStringArray(config.columns, `${path}.columns`, errors);
324
534
  if (config.rows !== undefined && !Array.isArray(config.rows)) errors.push(`${path}.rows must be an array`);
535
+ validateFieldSettings(config.fieldSettings, `${path}.fieldSettings`, errors);
536
+ validateSortClauses(config.sort, `${path}.sort`, errors);
537
+ validateFilterClauses(config.filter, `${path}.filter`, errors);
325
538
  validateStaticDataBinding(config.binding, `${path}.binding`, errors);
326
539
  }
327
540
  if (kind === "iframe" && config.url !== undefined && typeof config.url !== "string") {
@@ -749,8 +962,13 @@ export {
749
962
  DASHBOARD_TEMPLATES,
750
963
  GRID_COLUMNS,
751
964
  GRID_ROWS,
965
+ KNOWN_AGGREGATIONS,
966
+ KNOWN_CHART_TYPES,
752
967
  KNOWN_DATA_BINDING_MODES,
753
968
  KNOWN_FIELDS,
969
+ KNOWN_FILTER_CONJUNCTIONS,
970
+ KNOWN_FILTER_OPERATORS,
971
+ KNOWN_SORT_DIRECTIONS,
754
972
  KNOWN_WIDGET_KINDS,
755
973
  SAMPLE_DATA_BINDINGS,
756
974
  SAMPLE_VIEW_ROWS,
@@ -8,15 +8,10 @@
8
8
  "name": "growthub-workspace-app",
9
9
  "version": "1.0.0",
10
10
  "dependencies": {
11
+ "lucide-react": "^0.468.0",
11
12
  "next": "16.2.4",
12
13
  "react": "19.2.4",
13
14
  "react-dom": "19.2.4"
14
- },
15
- "devDependencies": {
16
- "@types/node": "^20",
17
- "@types/react": "^19",
18
- "@types/react-dom": "^19",
19
- "typescript": "^5"
20
15
  }
21
16
  },
22
17
  "node_modules/@emnapi/runtime": {
@@ -638,36 +633,6 @@
638
633
  "tslib": "^2.8.0"
639
634
  }
640
635
  },
641
- "node_modules/@types/node": {
642
- "version": "20.19.39",
643
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz",
644
- "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==",
645
- "dev": true,
646
- "license": "MIT",
647
- "dependencies": {
648
- "undici-types": "~6.21.0"
649
- }
650
- },
651
- "node_modules/@types/react": {
652
- "version": "19.2.14",
653
- "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
654
- "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
655
- "dev": true,
656
- "license": "MIT",
657
- "dependencies": {
658
- "csstype": "^3.2.2"
659
- }
660
- },
661
- "node_modules/@types/react-dom": {
662
- "version": "19.2.3",
663
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
664
- "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
665
- "dev": true,
666
- "license": "MIT",
667
- "peerDependencies": {
668
- "@types/react": "^19.2.0"
669
- }
670
- },
671
636
  "node_modules/baseline-browser-mapping": {
672
637
  "version": "2.10.21",
673
638
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.21.tgz",
@@ -706,13 +671,6 @@
706
671
  "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
707
672
  "license": "MIT"
708
673
  },
709
- "node_modules/csstype": {
710
- "version": "3.2.3",
711
- "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
712
- "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
713
- "dev": true,
714
- "license": "MIT"
715
- },
716
674
  "node_modules/detect-libc": {
717
675
  "version": "2.1.2",
718
676
  "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -723,6 +681,15 @@
723
681
  "node": ">=8"
724
682
  }
725
683
  },
684
+ "node_modules/lucide-react": {
685
+ "version": "0.468.0",
686
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz",
687
+ "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==",
688
+ "license": "ISC",
689
+ "peerDependencies": {
690
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc"
691
+ }
692
+ },
726
693
  "node_modules/nanoid": {
727
694
  "version": "3.3.11",
728
695
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -950,27 +917,6 @@
950
917
  "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
951
918
  "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
952
919
  "license": "0BSD"
953
- },
954
- "node_modules/typescript": {
955
- "version": "5.9.3",
956
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
957
- "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
958
- "dev": true,
959
- "license": "Apache-2.0",
960
- "bin": {
961
- "tsc": "bin/tsc",
962
- "tsserver": "bin/tsserver"
963
- },
964
- "engines": {
965
- "node": ">=14.17"
966
- }
967
- },
968
- "node_modules/undici-types": {
969
- "version": "6.21.0",
970
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
971
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
972
- "dev": true,
973
- "license": "MIT"
974
920
  }
975
921
  }
976
922
  }
@@ -10,6 +10,7 @@
10
10
  "lint": "next lint"
11
11
  },
12
12
  "dependencies": {
13
+ "lucide-react": "^0.468.0",
13
14
  "next": "16.2.4",
14
15
  "react": "19.2.4",
15
16
  "react-dom": "19.2.4"
@@ -73,9 +73,12 @@
73
73
  "apps/workspace/app/layout.jsx",
74
74
  "apps/workspace/app/page.jsx",
75
75
  "apps/workspace/app/globals.css",
76
+ "apps/workspace/app/workspace-builder.jsx",
76
77
  "apps/workspace/app/settings/integrations/page.jsx",
77
78
  "apps/workspace/app/api/workspace/route.js",
78
79
  "apps/workspace/app/api/settings/integrations/route.js",
80
+ "apps/workspace/lib/workspace-schema.js",
81
+ "apps/workspace/lib/workspace-config.js",
79
82
  "apps/workspace/lib/domain/portal.js",
80
83
  "apps/workspace/lib/domain/integrations.js",
81
84
  "apps/workspace/lib/adapters/env.js",