@growthub/cli 0.9.8 → 0.9.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.
Files changed (17) 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 +554 -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 +485 -77
  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 +38 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +3 -0
  17. 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,3 +1,41 @@
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"];
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@growthub/cli",
3
- "version": "0.9.8",
3
+ "version": "0.9.9",
4
4
  "description": "Growthub Local is a control plane for forked worker kits. The CLI is the executor, the hosted app is the identity authority, the worker kit is the unit of portable agent infrastructure, and the fork is the operator's personal branch of that infrastructure — policy-governed, trace-backed, and self-healing.",
5
5
  "type": "module",
6
6
  "bin": {