@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.
- package/README.md +23 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/SKILL.md +8 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -8
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +9 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/settings/integrations/route.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +4 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +554 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +111 -77
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +485 -77
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json +8 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +9 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/index.js +10 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/domain/integrations.js +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +62 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +38 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +3 -0
- package/package.json +1 -1
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/growthub.config.json
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "workspace-builder-default",
|
|
3
|
-
"name": "Workspace
|
|
4
|
-
"description": "Default no-code composition for the official Growthub Custom Workspace Starter. The interface starts as a
|
|
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-
|
|
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
|
}
|
package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js
CHANGED
|
@@ -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(
|
|
21
|
-
const
|
|
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
|
-
|
|
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: ["
|
|
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
|
|
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
|
|
39
|
+
return governedWorkspaceIntegrationCatalog;
|
|
40
40
|
}
|
|
41
41
|
if (!config.growthubBridge.baseUrl || !process.env.GROWTHUB_BRIDGE_ACCESS_TOKEN) {
|
|
42
|
-
return
|
|
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
|
|
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 =
|
|
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 !
|
|
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
|
-
|
|
197
|
+
listGovernedWorkspaceIntegrations
|
|
198
198
|
};
|
|
@@ -172,7 +172,7 @@ const workspaceIntegrations = [
|
|
|
172
172
|
setupMode: "hosted-authority"
|
|
173
173
|
}
|
|
174
174
|
];
|
|
175
|
-
const
|
|
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
|
-
|
|
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
|
|
72
|
+
return baseFilesystem("WORKSPACE_CONFIG_ALLOW_FS_WRITE=true");
|
|
27
73
|
}
|
|
28
74
|
if (process.env.NODE_ENV === "development") {
|
|
29
|
-
return
|
|
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:
|
|
34
|
-
|
|
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
|
|
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 !==
|
|
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.
|
|
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": {
|