@growthub/cli 0.9.9 → 0.9.11

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 (30) hide show
  1. package/README.md +1 -1
  2. package/assets/worker-kits/creative-strategist-v1/kit.json +5 -2
  3. package/assets/worker-kits/growthub-agency-portal-starter-v1/kit.json +4 -1
  4. package/assets/worker-kits/growthub-ai-website-cloner-v1/kit.json +6 -3
  5. package/assets/worker-kits/growthub-creative-video-pipeline-v1/kit.json +4 -1
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -4
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integration-entities/route.js +50 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +980 -1
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +5 -2
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +4 -5
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +1686 -68
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/growthub-connection-normalizer.js +12 -16
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +61 -11
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +31 -1
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +236 -9
  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 +5 -2
  19. package/assets/worker-kits/growthub-email-marketing-v1/kit.json +5 -2
  20. package/assets/worker-kits/growthub-geo-seo-v1/kit.json +5 -2
  21. package/assets/worker-kits/growthub-hyperframes-studio-v1/kit.json +5 -2
  22. package/assets/worker-kits/growthub-marketing-skills-v1/kit.json +6 -3
  23. package/assets/worker-kits/growthub-open-higgsfield-studio-v1/kit.json +5 -2
  24. package/assets/worker-kits/growthub-open-montage-studio-v1/kit.json +6 -3
  25. package/assets/worker-kits/growthub-postiz-social-v1/kit.json +5 -2
  26. package/assets/worker-kits/growthub-twenty-crm-v1/kit.json +6 -3
  27. package/assets/worker-kits/growthub-video-use-studio-v1/kit.json +5 -2
  28. package/assets/worker-kits/growthub-zernio-social-v1/kit.json +5 -2
  29. package/dist/index.js +1750 -433
  30. package/package.json +1 -1
@@ -1,15 +1,12 @@
1
- const providerAliases = {
2
- ga4: "google-analytics",
3
- google_analytics: "google-analytics",
4
- google_drive: "google-drive",
5
- ghl: "go-high-level",
6
- gohighlevel: "go-high-level",
7
- meta: "meta-ads",
8
- meta_ads: "meta-ads"
9
- };
10
1
  function normalizeProviderId(provider) {
11
- const normalized = provider.trim().toLowerCase().replaceAll("_", "-");
12
- return providerAliases[normalized] || normalized;
2
+ return provider.trim().toLowerCase().replaceAll("_", "-");
3
+ }
4
+ function providerLabel(provider) {
5
+ return normalizeProviderId(provider)
6
+ .split("-")
7
+ .filter(Boolean)
8
+ .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))
9
+ .join(" ");
13
10
  }
14
11
  function isHostedRecord(row) {
15
12
  return "provider" in row && ("ready" in row || "connectedAt" in row || "scopes" in row || "handle" in row);
@@ -41,10 +38,10 @@ function normalizeMcpAccount(account) {
41
38
  const isVerified = account.isVerified === true;
42
39
  const isConnected = isActive;
43
40
  return {
44
- id: provider,
45
- provider,
46
- label: account.connectionName || void 0,
47
- name: account.connectionName || void 0,
41
+ id: provider,
42
+ provider,
43
+ label: providerLabel(provider),
44
+ name: providerLabel(provider),
48
45
  authType: normalizeConnectionType(account.connectionType),
49
46
  status: isConnected ? "connected" : "needs-connection",
50
47
  isConnected,
@@ -54,7 +51,6 @@ function normalizeMcpAccount(account) {
54
51
  setupMode: "hosted-authority",
55
52
  connectionMetadata: {
56
53
  source: "growthub-mcp-accounts",
57
- accountId: account.id,
58
54
  connectionName: account.connectionName,
59
55
  connectionType: account.connectionType,
60
56
  isVerified,
@@ -1,6 +1,7 @@
1
1
  import { readAdapterConfig } from "@/lib/adapters/env";
2
2
  import {
3
- governedWorkspaceIntegrationCatalog
3
+ governedWorkspaceIntegrationCatalog,
4
+ normalizeIntegrationEntities
4
5
  } from "@/lib/domain/integrations";
5
6
  import {
6
7
  normalizeGrowthubBridgePayload
@@ -136,10 +137,10 @@ function mergeBridgeRows(rows) {
136
137
  });
137
138
  if (!row) return catalogItem;
138
139
  seenProviders.add(row.provider || row.id || catalogItem.provider);
139
- return {
140
- ...catalogItem,
141
- label: row.label || row.name || catalogItem.label,
142
- name: row.name || row.label || catalogItem.name,
140
+ return {
141
+ ...catalogItem,
142
+ label: catalogItem.label,
143
+ name: catalogItem.name,
143
144
  icon: row.icon || catalogItem.icon,
144
145
  description: row.description || catalogItem.description,
145
146
  category: row.category || catalogItem.category,
@@ -150,7 +151,6 @@ function mergeBridgeRows(rows) {
150
151
  setupMode: row.setupMode || catalogItem.setupMode,
151
152
  status: row.status || (row.isConnected || row.isActive ? "connected" : catalogItem.status),
152
153
  connectionId: row.connectionId,
153
- accountId: row.accountId,
154
154
  secretEnvName: row.secretEnvName,
155
155
  connectionMetadata: row.connectionMetadata || row.metadata,
156
156
  metadata: row.metadata || row.connectionMetadata
@@ -167,8 +167,9 @@ function mergeBridgeRows(rows) {
167
167
  function toDiscoveredIntegration(row) {
168
168
  const provider = row.provider || row.id || "unknown-provider";
169
169
  const label = row.label || row.name || provider;
170
- const isDataPipeline = ["windsor-ai", "google-sheets", "google-analytics", "shopify", "meta-ads"].includes(provider);
171
170
  const isConnected = row.isConnected ?? row.status === "connected";
171
+ const lane = typeof row.lane === "string" && row.lane ? row.lane : "workspace-integration";
172
+ const objectType = typeof row.objectType === "string" && row.objectType ? row.objectType : "mcp-connection";
172
173
  return {
173
174
  id: row.id || provider,
174
175
  label,
@@ -180,19 +181,68 @@ function toDiscoveredIntegration(row) {
180
181
  authType: row.authType || "oauth_first_party",
181
182
  isConnected,
182
183
  isActive: row.isActive ?? isConnected,
183
- lane: isDataPipeline ? "data-source" : "workspace-integration",
184
- objectType: isDataPipeline ? "data-pipeline" : "mcp-connection",
184
+ lane,
185
+ objectType,
185
186
  status: row.status || (isConnected ? "connected" : "needs-connection"),
186
187
  authPath: row.authPath || "growthub-mcp-bridge",
187
188
  setupMode: row.setupMode || "hosted-authority",
188
189
  connectionId: row.connectionId,
189
- accountId: row.accountId,
190
190
  secretEnvName: row.secretEnvName,
191
191
  connectionMetadata: row.connectionMetadata || row.metadata,
192
192
  metadata: row.metadata || row.connectionMetadata
193
193
  };
194
194
  }
195
+ /**
196
+ * Governed Integration Reference Binding — entity metadata resolution.
197
+ *
198
+ * Returns NormalizedIntegrationEntity[] for the requested integration when a
199
+ * server-side object resolver is available. Bridge connection discovery alone
200
+ * does not fabricate provider objects.
201
+ *
202
+ * Authority invariant: this function runs server-side only (API route).
203
+ * The browser NEVER calls provider APIs, holds tokens, or resolves entities.
204
+ */
205
+ async function listEntityMetadataForIntegration(integrationId) {
206
+ if (!integrationId) return [];
207
+ const config = readAdapterConfig();
208
+
209
+ if (config.integrationAdapter === "growthub-bridge" &&
210
+ config.growthubBridge?.baseUrl &&
211
+ process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN) {
212
+ try {
213
+ const baseUrl = config.growthubBridge.baseUrl;
214
+ const entitiesPath = `/api/integrations/${encodeURIComponent(integrationId)}/entities`;
215
+ const url = new URL(entitiesPath, baseUrl);
216
+ const headers = {
217
+ accept: "application/json",
218
+ authorization: `Bearer ${process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN}`
219
+ };
220
+ if (config.growthubBridge.userId) {
221
+ headers["x-user-id"] = config.growthubBridge.userId;
222
+ }
223
+ const response = await fetch(url, {
224
+ headers,
225
+ next: { revalidate: 30 }
226
+ });
227
+ if (response.ok) {
228
+ const payload = await response.json();
229
+ const entities = Array.isArray(payload.entities) ? payload.entities :
230
+ Array.isArray(payload.objects) ? payload.objects :
231
+ Array.isArray(payload.data) ? payload.data :
232
+ Array.isArray(payload) ? payload : [];
233
+ const normalized = normalizeIntegrationEntities(entities);
234
+ if (normalized.length) return normalized;
235
+ }
236
+ } catch {
237
+ // No fallback object data. The UI must surface the missing resolver.
238
+ }
239
+ }
240
+
241
+ return [];
242
+ }
243
+
195
244
  export {
196
245
  describeIntegrationAdapter,
197
- listGovernedWorkspaceIntegrations
246
+ listGovernedWorkspaceIntegrations,
247
+ listEntityMetadataForIntegration
198
248
  };
@@ -173,13 +173,43 @@ const workspaceIntegrations = [
173
173
  }
174
174
  ];
175
175
  const governedWorkspaceIntegrationCatalog = [...dataSources, ...workspaceIntegrations];
176
+
176
177
  function groupIntegrationsByLane(integrations) {
177
178
  return {
178
179
  dataSources: integrations.filter((item) => item.lane === "data-source"),
179
180
  workspaceIntegrations: integrations.filter((item) => item.lane === "workspace-integration")
180
181
  };
181
182
  }
183
+
184
+ function normalizeIntegrationEntity(entity) {
185
+ if (!entity || typeof entity !== "object" || Array.isArray(entity)) return null;
186
+ const id = typeof entity.id === "string" ? entity.id.trim() : "";
187
+ const label = typeof entity.label === "string" && entity.label.trim()
188
+ ? entity.label.trim()
189
+ : id;
190
+ if (!id || !label) return null;
191
+ const normalized = {
192
+ id,
193
+ label,
194
+ secondaryLabel: typeof entity.secondaryLabel === "string" ? entity.secondaryLabel : id,
195
+ entityType: typeof entity.entityType === "string" ? entity.entityType : undefined,
196
+ provider: typeof entity.provider === "string" ? entity.provider : undefined,
197
+ lane: typeof entity.lane === "string" ? entity.lane : undefined,
198
+ status: typeof entity.status === "string" ? entity.status : undefined,
199
+ metadata: entity.metadata && typeof entity.metadata === "object" && !Array.isArray(entity.metadata)
200
+ ? entity.metadata
201
+ : undefined
202
+ };
203
+ return Object.fromEntries(Object.entries(normalized).filter(([, value]) => value !== undefined));
204
+ }
205
+
206
+ function normalizeIntegrationEntities(entities) {
207
+ if (!Array.isArray(entities)) return [];
208
+ return entities.map(normalizeIntegrationEntity).filter(Boolean);
209
+ }
210
+
182
211
  export {
183
212
  governedWorkspaceIntegrationCatalog,
184
- groupIntegrationsByLane
213
+ groupIntegrationsByLane,
214
+ normalizeIntegrationEntities
185
215
  };
@@ -40,7 +40,14 @@ const GRID_COLUMNS = 12;
40
40
  const GRID_ROWS = 16;
41
41
  const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
42
42
  const KNOWN_FIELDS = ["dashboards", "widgetTypes", "canvas"];
43
- 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"];
49
+
50
+ const NORMALIZED_OBJECT_FIELD_IDS = ["id", "label", "secondaryLabel", "entityType", "provider", "lane", "status"];
44
51
  const WORKSPACE_TEMPLATE_KIND = "growthub-workspace-template";
45
52
  const WORKSPACE_TEMPLATE_VERSION = 1;
46
53
  const WORKSPACE_TEMPLATE_SOURCE = "growthub-custom-workspace-starter-v1";
@@ -61,7 +68,12 @@ const WIDGET_SCHEMA_CONTRACTS = {
61
68
  config: "kind-specific config object"
62
69
  },
63
70
  ChartWidgetConfig: {
64
- values: "number[]",
71
+ values: "number[] (legacy preserved)",
72
+ chartType: `${KNOWN_CHART_TYPES.join(" | ")} optional, defaults to bar-vertical`,
73
+ xAxis: "ChartAxisConfig optional",
74
+ yAxis: "ChartAxisConfig optional",
75
+ style: "ChartStyleConfig optional",
76
+ filter: "FilterConfig optional",
65
77
  binding: "StaticDataBinding optional"
66
78
  },
67
79
  ViewWidgetConfig: {
@@ -69,8 +81,42 @@ const WIDGET_SCHEMA_CONTRACTS = {
69
81
  layout: "Table",
70
82
  columns: "string[]",
71
83
  rows: "record[]",
84
+ fieldSettings: "FieldSettingsConfig optional (hidden[], order[])",
85
+ sort: "SortClause[] optional ({ fieldId, direction })",
86
+ filter: "FilterConfig optional ({ op, clauses[] })",
72
87
  binding: "StaticDataBinding optional"
73
88
  },
89
+ ChartAxisConfig: {
90
+ field: "string optional",
91
+ sort: "string optional (asc | desc | position)",
92
+ aggregation: `${KNOWN_AGGREGATIONS.join(" | ")} optional`,
93
+ groupBy: "string optional",
94
+ omitZero: "boolean optional",
95
+ min: "string | number optional",
96
+ max: "string | number optional"
97
+ },
98
+ ChartStyleConfig: {
99
+ colors: "string optional (auto | manual swatch label)",
100
+ axisName: "string optional",
101
+ dataLabels: "boolean optional"
102
+ },
103
+ FieldSettingsConfig: {
104
+ hidden: "string[] of column names hidden from preview",
105
+ order: "string[] of column names defining custom order"
106
+ },
107
+ SortClause: {
108
+ fieldId: "non-empty string (column name)",
109
+ direction: KNOWN_SORT_DIRECTIONS.join(" | ")
110
+ },
111
+ FilterConfig: {
112
+ op: KNOWN_FILTER_CONJUNCTIONS.join(" | "),
113
+ clauses: "FilterClause[]"
114
+ },
115
+ FilterClause: {
116
+ fieldId: "non-empty string (column name)",
117
+ operator: KNOWN_FILTER_OPERATORS.join(" | "),
118
+ value: "string | number | boolean optional"
119
+ },
74
120
  IframeWidgetConfig: {
75
121
  url: "string"
76
122
  },
@@ -97,17 +143,35 @@ const WIDGET_SCHEMA_CONTRACTS = {
97
143
  source: "string",
98
144
  rows: "manual record[] optional",
99
145
  json: "JSON string optional",
100
- csv: "CSV string optional"
146
+ csv: "CSV string optional",
147
+ sourceType: "managed-integrations | custom-api-webhooks optional",
148
+ sourceAuthority: "string optional — adapter authority label, never a secret",
149
+ endpointRef: "string optional — stable custom API/webhook reference, never a token",
150
+ integrationId: "string optional (when mode === 'integration')",
151
+ lane: "string optional (when mode === 'integration')",
152
+ entityId: "string optional — stable source object ID (never a token or credential)",
153
+ entityType: "string optional — adapter-provided object type",
154
+ entityLabel: "string optional — display-only resolved label, not authoritative"
155
+ },
156
+ NormalizedIntegrationEntity: {
157
+ id: "non-empty string — stable source object ID",
158
+ label: "non-empty string — primary display name",
159
+ secondaryLabel: "string optional — muted subtitle (ID, domain, or type hint)",
160
+ entityType: "string optional — adapter-provided object type",
161
+ provider: "string optional — adapter/provider slug",
162
+ lane: "string optional — adapter-provided lane",
163
+ status: "string optional — adapter-provided status",
164
+ metadata: "record optional — additional adapter metadata"
101
165
  }
102
166
  };
103
167
 
104
168
  const SAMPLE_VIEW_ROWS = [
105
- { Name: "CMWL Direct", "Domain Name": "centerformedica" },
106
- { Name: "Medi-Weightloss", "Domain Name": "mediweightloss.com" },
107
- { Name: "Optima Tyler", "Domain Name": "optimatyler.com" },
108
- { Name: "Balanced Hormone He...", "Domain Name": "balancedhormor" },
109
- { Name: "Jolie Aesthetics RVA", "Domain Name": "jolie-aesthetics.c" },
110
- { Name: "Livea Centers", "Domain Name": "livea.com" }
169
+ { Name: "Example Company A", "Domain Name": "example-a.test" },
170
+ { Name: "Example Company B", "Domain Name": "example-b.test" },
171
+ { Name: "Example Company C", "Domain Name": "example-c.test" },
172
+ { Name: "Example Company D", "Domain Name": "example-d.test" },
173
+ { Name: "Example Company E", "Domain Name": "example-e.test" },
174
+ { Name: "Example Company F", "Domain Name": "example-f.test" }
111
175
  ];
112
176
 
113
177
  const SAMPLE_DATA_BINDINGS = {
@@ -321,6 +385,14 @@ function validateStaticDataBinding(binding, path, errors) {
321
385
  if (!KNOWN_DATA_BINDING_MODES.includes(binding.mode)) {
322
386
  errors.push(`${path}.mode must be one of ${KNOWN_DATA_BINDING_MODES.join(", ")}`);
323
387
  }
388
+ if (binding.mode === "integration") {
389
+ if (typeof binding.integrationId !== "string" || !binding.integrationId.trim()) {
390
+ errors.push(`${path}.integrationId is required when mode is integration`);
391
+ }
392
+ if (typeof binding.lane !== "string" || !binding.lane.trim()) {
393
+ errors.push(`${path}.lane is required when mode is integration`);
394
+ }
395
+ }
324
396
  if (binding.source !== undefined && typeof binding.source !== "string") {
325
397
  errors.push(`${path}.source must be a string`);
326
398
  }
@@ -333,6 +405,145 @@ function validateStaticDataBinding(binding, path, errors) {
333
405
  if (binding.csv !== undefined && typeof binding.csv !== "string") {
334
406
  errors.push(`${path}.csv must be a string`);
335
407
  }
408
+ if (binding.sourceType !== undefined && typeof binding.sourceType !== "string") {
409
+ errors.push(`${path}.sourceType must be a string`);
410
+ }
411
+ if (binding.sourceAuthority !== undefined && typeof binding.sourceAuthority !== "string") {
412
+ errors.push(`${path}.sourceAuthority must be a string`);
413
+ }
414
+ if (binding.endpointRef !== undefined && typeof binding.endpointRef !== "string") {
415
+ errors.push(`${path}.endpointRef must be a string`);
416
+ }
417
+ if (binding.integrationId !== undefined && typeof binding.integrationId !== "string") {
418
+ errors.push(`${path}.integrationId must be a string`);
419
+ }
420
+ if (binding.lane !== undefined && typeof binding.lane !== "string") {
421
+ errors.push(`${path}.lane must be a string`);
422
+ }
423
+ if (binding.entityId !== undefined && typeof binding.entityId !== "string") {
424
+ errors.push(`${path}.entityId must be a string`);
425
+ }
426
+ if (binding.entityType !== undefined && typeof binding.entityType !== "string") {
427
+ errors.push(`${path}.entityType must be a string`);
428
+ }
429
+ if (binding.entityLabel !== undefined && typeof binding.entityLabel !== "string") {
430
+ errors.push(`${path}.entityLabel must be a string`);
431
+ }
432
+ }
433
+
434
+ function validateFieldSettings(fieldSettings, path, errors) {
435
+ if (fieldSettings === undefined) return;
436
+ if (!isPlainObject(fieldSettings)) {
437
+ errors.push(`${path} must be a plain object`);
438
+ return;
439
+ }
440
+ if (fieldSettings.hidden !== undefined) validateStringArray(fieldSettings.hidden, `${path}.hidden`, errors);
441
+ if (fieldSettings.order !== undefined) validateStringArray(fieldSettings.order, `${path}.order`, errors);
442
+ }
443
+
444
+ function validateSortClauses(sort, path, errors) {
445
+ if (sort === undefined) return;
446
+ if (!Array.isArray(sort)) {
447
+ errors.push(`${path} must be an array`);
448
+ return;
449
+ }
450
+ sort.forEach((clause, index) => {
451
+ const prefix = `${path}[${index}]`;
452
+ if (!isPlainObject(clause)) {
453
+ errors.push(`${prefix} must be a plain object`);
454
+ return;
455
+ }
456
+ if (typeof clause.fieldId !== "string" || !clause.fieldId) {
457
+ errors.push(`${prefix}.fieldId must be a non-empty string`);
458
+ }
459
+ if (clause.direction !== undefined && !KNOWN_SORT_DIRECTIONS.includes(clause.direction)) {
460
+ errors.push(`${prefix}.direction must be one of ${KNOWN_SORT_DIRECTIONS.join(", ")}`);
461
+ }
462
+ });
463
+ }
464
+
465
+ function validateFilterClauses(filter, path, errors) {
466
+ if (filter === undefined) return;
467
+ if (!isPlainObject(filter)) {
468
+ errors.push(`${path} must be a plain object`);
469
+ return;
470
+ }
471
+ if (filter.op !== undefined && !KNOWN_FILTER_CONJUNCTIONS.includes(filter.op)) {
472
+ errors.push(`${path}.op must be one of ${KNOWN_FILTER_CONJUNCTIONS.join(", ")}`);
473
+ }
474
+ if (filter.clauses !== undefined) {
475
+ if (!Array.isArray(filter.clauses)) {
476
+ errors.push(`${path}.clauses must be an array`);
477
+ } else {
478
+ filter.clauses.forEach((clause, index) => {
479
+ const prefix = `${path}.clauses[${index}]`;
480
+ if (!isPlainObject(clause)) {
481
+ errors.push(`${prefix} must be a plain object`);
482
+ return;
483
+ }
484
+ if (typeof clause.fieldId !== "string" || !clause.fieldId) {
485
+ errors.push(`${prefix}.fieldId must be a non-empty string`);
486
+ }
487
+ if (clause.operator !== undefined && !KNOWN_FILTER_OPERATORS.includes(clause.operator)) {
488
+ errors.push(`${prefix}.operator must be one of ${KNOWN_FILTER_OPERATORS.join(", ")}`);
489
+ }
490
+ if (
491
+ clause.value !== undefined &&
492
+ typeof clause.value !== "string" &&
493
+ typeof clause.value !== "number" &&
494
+ typeof clause.value !== "boolean"
495
+ ) {
496
+ errors.push(`${prefix}.value must be a string, number, or boolean`);
497
+ }
498
+ });
499
+ }
500
+ }
501
+ }
502
+
503
+ function validateChartAxis(axis, path, errors) {
504
+ if (axis === undefined) return;
505
+ if (!isPlainObject(axis)) {
506
+ errors.push(`${path} must be a plain object`);
507
+ return;
508
+ }
509
+ if (axis.field !== undefined && typeof axis.field !== "string") {
510
+ errors.push(`${path}.field must be a string`);
511
+ }
512
+ if (axis.sort !== undefined && typeof axis.sort !== "string") {
513
+ errors.push(`${path}.sort must be a string`);
514
+ }
515
+ if (axis.aggregation !== undefined && !KNOWN_AGGREGATIONS.includes(axis.aggregation)) {
516
+ errors.push(`${path}.aggregation must be one of ${KNOWN_AGGREGATIONS.join(", ")}`);
517
+ }
518
+ if (axis.groupBy !== undefined && typeof axis.groupBy !== "string") {
519
+ errors.push(`${path}.groupBy must be a string`);
520
+ }
521
+ if (axis.omitZero !== undefined && typeof axis.omitZero !== "boolean") {
522
+ errors.push(`${path}.omitZero must be a boolean`);
523
+ }
524
+ if (axis.min !== undefined && typeof axis.min !== "string" && typeof axis.min !== "number") {
525
+ errors.push(`${path}.min must be a string or number`);
526
+ }
527
+ if (axis.max !== undefined && typeof axis.max !== "string" && typeof axis.max !== "number") {
528
+ errors.push(`${path}.max must be a string or number`);
529
+ }
530
+ }
531
+
532
+ function validateChartStyle(style, path, errors) {
533
+ if (style === undefined) return;
534
+ if (!isPlainObject(style)) {
535
+ errors.push(`${path} must be a plain object`);
536
+ return;
537
+ }
538
+ if (style.colors !== undefined && typeof style.colors !== "string") {
539
+ errors.push(`${path}.colors must be a string`);
540
+ }
541
+ if (style.axisName !== undefined && typeof style.axisName !== "string") {
542
+ errors.push(`${path}.axisName must be a string`);
543
+ }
544
+ if (style.dataLabels !== undefined && typeof style.dataLabels !== "boolean") {
545
+ errors.push(`${path}.dataLabels must be a boolean`);
546
+ }
336
547
  }
337
548
 
338
549
  function validateWidgetConfig(kind, config, path, errors) {
@@ -353,6 +564,13 @@ function validateWidgetConfig(kind, config, path, errors) {
353
564
  });
354
565
  }
355
566
  }
567
+ if (config.chartType !== undefined && !KNOWN_CHART_TYPES.includes(config.chartType)) {
568
+ errors.push(`${path}.chartType must be one of ${KNOWN_CHART_TYPES.join(", ")}`);
569
+ }
570
+ validateChartAxis(config.xAxis, `${path}.xAxis`, errors);
571
+ validateChartAxis(config.yAxis, `${path}.yAxis`, errors);
572
+ validateChartStyle(config.style, `${path}.style`, errors);
573
+ validateFilterClauses(config.filter, `${path}.filter`, errors);
356
574
  validateStaticDataBinding(config.binding, `${path}.binding`, errors);
357
575
  }
358
576
  if (kind === "view") {
@@ -360,6 +578,9 @@ function validateWidgetConfig(kind, config, path, errors) {
360
578
  if (config.layout !== undefined && config.layout !== "Table") errors.push(`${path}.layout must be Table`);
361
579
  if (config.columns !== undefined) validateStringArray(config.columns, `${path}.columns`, errors);
362
580
  if (config.rows !== undefined && !Array.isArray(config.rows)) errors.push(`${path}.rows must be an array`);
581
+ validateFieldSettings(config.fieldSettings, `${path}.fieldSettings`, errors);
582
+ validateSortClauses(config.sort, `${path}.sort`, errors);
583
+ validateFilterClauses(config.filter, `${path}.filter`, errors);
363
584
  validateStaticDataBinding(config.binding, `${path}.binding`, errors);
364
585
  }
365
586
  if (kind === "iframe" && config.url !== undefined && typeof config.url !== "string") {
@@ -787,9 +1008,15 @@ export {
787
1008
  DASHBOARD_TEMPLATES,
788
1009
  GRID_COLUMNS,
789
1010
  GRID_ROWS,
1011
+ KNOWN_AGGREGATIONS,
1012
+ KNOWN_CHART_TYPES,
790
1013
  KNOWN_DATA_BINDING_MODES,
791
1014
  KNOWN_FIELDS,
1015
+ KNOWN_FILTER_CONJUNCTIONS,
1016
+ KNOWN_FILTER_OPERATORS,
1017
+ KNOWN_SORT_DIRECTIONS,
792
1018
  KNOWN_WIDGET_KINDS,
1019
+ NORMALIZED_OBJECT_FIELD_IDS,
793
1020
  SAMPLE_DATA_BINDINGS,
794
1021
  SAMPLE_VIEW_ROWS,
795
1022
  WIDGET_SCHEMA_CONTRACTS,
@@ -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"
@@ -101,7 +101,7 @@
101
101
  },
102
102
  "outputStandard": {
103
103
  "type": "working-directory",
104
- "description": "Fork the starter kit directly with `growthub starter init` the CLI materializes this bundled asset tree to the user's chosen path, auto-registers the destination as a kit-fork (canonical state under `<forkPath>/.growthub-fork/fork.json`), optionally binds it to a GitHub remote, and hands off the forkId. Every downstream customization flows through the v1 Self-Healing Fork Sync Agent.",
104
+ "description": "Fork the starter kit directly with `growthub starter init` \u2014 the CLI materializes this bundled asset tree to the user's chosen path, auto-registers the destination as a kit-fork (canonical state under `<forkPath>/.growthub-fork/fork.json`), optionally binds it to a GitHub remote, and hands off the forkId. Every downstream customization flows through the v1 Self-Healing Fork Sync Agent.",
105
105
  "requiredPaths": [
106
106
  "QUICKSTART.md",
107
107
  "kit.json",
@@ -159,5 +159,8 @@
159
159
  "provenance": {
160
160
  "sourceRepo": "growthub-local",
161
161
  "frozenAt": "2026-04-16T00:00:00.000Z"
162
- }
162
+ },
163
+ "workspaceVisibility": "official",
164
+ "workspaceCompatible": true,
165
+ "workspaceCategory": "workspace-starter"
163
166
  }