@growthub/cli 0.9.4 → 0.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/route.js +64 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +201 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/page.jsx +11 -105
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +680 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +232 -0
- package/dist/index.js +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readAdapterConfig } from "@/lib/adapters/env";
|
|
4
|
+
|
|
5
|
+
const KNOWN_FIELDS = ["dashboards", "widgetTypes", "canvas"];
|
|
6
|
+
const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
|
|
7
|
+
const GRID_COLUMNS = 12;
|
|
8
|
+
const GRID_ROWS = 16;
|
|
9
|
+
|
|
10
|
+
function resolveWorkspaceConfigPath() {
|
|
11
|
+
return path.resolve(/*turbopackIgnore: true*/ process.cwd(), "growthub.config.json");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function readWorkspaceConfig() {
|
|
15
|
+
const configPath = resolveWorkspaceConfigPath();
|
|
16
|
+
const raw = await fs.readFile(configPath, "utf8");
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function describePersistenceMode() {
|
|
21
|
+
const target = process.env.AGENCY_PORTAL_DEPLOY_TARGET || "vercel";
|
|
22
|
+
const isReadOnlyDeploy = target === "vercel" || target === "netlify";
|
|
23
|
+
const allowFsWrite = process.env.WORKSPACE_CONFIG_ALLOW_FS_WRITE === "true";
|
|
24
|
+
if (allowFsWrite) {
|
|
25
|
+
return { mode: "filesystem", reason: "WORKSPACE_CONFIG_ALLOW_FS_WRITE=true" };
|
|
26
|
+
}
|
|
27
|
+
if (process.env.NODE_ENV === "development") {
|
|
28
|
+
return { mode: "filesystem", reason: "Local Next.js development" };
|
|
29
|
+
}
|
|
30
|
+
if (isReadOnlyDeploy) {
|
|
31
|
+
return {
|
|
32
|
+
mode: "read-only",
|
|
33
|
+
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.`
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return { mode: "filesystem", reason: "Local development" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isFiniteInt(value) {
|
|
40
|
+
return typeof value === "number" && Number.isFinite(value) && Math.floor(value) === value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function validateWidgetArray(widgets, contextPath, errors, seenIds) {
|
|
44
|
+
if (!Array.isArray(widgets)) {
|
|
45
|
+
errors.push(`${contextPath} must be an array`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const occupied = new Map();
|
|
49
|
+
widgets.forEach((widget, index) => {
|
|
50
|
+
const prefix = `${contextPath}[${index}]`;
|
|
51
|
+
if (!widget || typeof widget !== "object" || Array.isArray(widget)) {
|
|
52
|
+
errors.push(`${prefix} must be an object`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (typeof widget.id !== "string" || !widget.id) {
|
|
56
|
+
errors.push(`${prefix}.id must be a non-empty string`);
|
|
57
|
+
} else if (seenIds.has(widget.id)) {
|
|
58
|
+
errors.push(`${prefix}.id duplicates an earlier widget id`);
|
|
59
|
+
} else {
|
|
60
|
+
seenIds.add(widget.id);
|
|
61
|
+
}
|
|
62
|
+
if (!KNOWN_WIDGET_KINDS.includes(widget.kind)) {
|
|
63
|
+
errors.push(`${prefix}.kind must be one of ${KNOWN_WIDGET_KINDS.join(", ")}`);
|
|
64
|
+
}
|
|
65
|
+
if (!widget.position || typeof widget.position !== "object" || Array.isArray(widget.position)) {
|
|
66
|
+
errors.push(`${prefix}.position must be an object`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
for (const k of ["x", "y", "w", "h"]) {
|
|
70
|
+
if (!isFiniteInt(widget.position[k])) {
|
|
71
|
+
errors.push(`${prefix}.position.${k} must be a finite integer`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (
|
|
75
|
+
isFiniteInt(widget.position.x) &&
|
|
76
|
+
isFiniteInt(widget.position.w) &&
|
|
77
|
+
(widget.position.x < 0 || widget.position.w < 1 || widget.position.x + widget.position.w > GRID_COLUMNS)
|
|
78
|
+
) {
|
|
79
|
+
errors.push(`${prefix} x/w out of [0..${GRID_COLUMNS}] grid`);
|
|
80
|
+
}
|
|
81
|
+
if (
|
|
82
|
+
isFiniteInt(widget.position.y) &&
|
|
83
|
+
isFiniteInt(widget.position.h) &&
|
|
84
|
+
(widget.position.y < 0 || widget.position.h < 1 || widget.position.y + widget.position.h > GRID_ROWS)
|
|
85
|
+
) {
|
|
86
|
+
errors.push(`${prefix} y/h out of [0..${GRID_ROWS}] grid`);
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
isFiniteInt(widget.position.x) &&
|
|
90
|
+
isFiniteInt(widget.position.y) &&
|
|
91
|
+
isFiniteInt(widget.position.w) &&
|
|
92
|
+
isFiniteInt(widget.position.h)
|
|
93
|
+
) {
|
|
94
|
+
for (let dx = 0; dx < widget.position.w; dx += 1) {
|
|
95
|
+
for (let dy = 0; dy < widget.position.h; dy += 1) {
|
|
96
|
+
const cell = `${widget.position.x + dx}:${widget.position.y + dy}`;
|
|
97
|
+
const previous = occupied.get(cell);
|
|
98
|
+
if (previous) {
|
|
99
|
+
errors.push(`${prefix} overlaps ${previous} at grid cell ${cell}`);
|
|
100
|
+
} else {
|
|
101
|
+
occupied.set(cell, `${prefix}.position`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function validateWorkspaceConfig(nextConfig) {
|
|
110
|
+
if (!nextConfig || typeof nextConfig !== "object" || Array.isArray(nextConfig)) {
|
|
111
|
+
const error = new Error("workspace config must be a plain object");
|
|
112
|
+
error.code = "INVALID_WORKSPACE_CONFIG";
|
|
113
|
+
error.details = ["root must be a plain object"];
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
const errors = [];
|
|
117
|
+
for (const key of Object.keys(nextConfig)) {
|
|
118
|
+
if (!KNOWN_FIELDS.includes(key)) {
|
|
119
|
+
errors.push(`unknown top-level field: ${key}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (nextConfig.dashboards !== undefined && !Array.isArray(nextConfig.dashboards)) {
|
|
123
|
+
errors.push("dashboards must be an array");
|
|
124
|
+
}
|
|
125
|
+
if (nextConfig.widgetTypes !== undefined && !Array.isArray(nextConfig.widgetTypes)) {
|
|
126
|
+
errors.push("widgetTypes must be an array");
|
|
127
|
+
}
|
|
128
|
+
if (nextConfig.canvas !== undefined) {
|
|
129
|
+
if (typeof nextConfig.canvas !== "object" || Array.isArray(nextConfig.canvas) || nextConfig.canvas === null) {
|
|
130
|
+
errors.push("canvas must be a plain object");
|
|
131
|
+
} else {
|
|
132
|
+
const seenWidgetIds = new Set();
|
|
133
|
+
if (nextConfig.canvas.widgets !== undefined) {
|
|
134
|
+
validateWidgetArray(nextConfig.canvas.widgets, "canvas.widgets", errors, seenWidgetIds);
|
|
135
|
+
}
|
|
136
|
+
if (nextConfig.canvas.tabs !== undefined) {
|
|
137
|
+
if (!Array.isArray(nextConfig.canvas.tabs)) {
|
|
138
|
+
errors.push("canvas.tabs must be an array");
|
|
139
|
+
} else {
|
|
140
|
+
const seenTabIds = new Set();
|
|
141
|
+
nextConfig.canvas.tabs.forEach((tab, index) => {
|
|
142
|
+
const tabPrefix = `canvas.tabs[${index}]`;
|
|
143
|
+
if (!tab || typeof tab !== "object" || Array.isArray(tab)) {
|
|
144
|
+
errors.push(`${tabPrefix} must be an object`);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (typeof tab.id !== "string" || !tab.id) {
|
|
148
|
+
errors.push(`${tabPrefix}.id must be a non-empty string`);
|
|
149
|
+
} else if (seenTabIds.has(tab.id)) {
|
|
150
|
+
errors.push(`${tabPrefix}.id duplicates an earlier tab id`);
|
|
151
|
+
} else {
|
|
152
|
+
seenTabIds.add(tab.id);
|
|
153
|
+
}
|
|
154
|
+
if (typeof tab.name !== "string" || !tab.name) {
|
|
155
|
+
errors.push(`${tabPrefix}.name must be a non-empty string`);
|
|
156
|
+
}
|
|
157
|
+
if (tab.widgets !== undefined) {
|
|
158
|
+
validateWidgetArray(tab.widgets, `${tabPrefix}.widgets`, errors, seenWidgetIds);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (nextConfig.canvas.activeTabId !== undefined && typeof nextConfig.canvas.activeTabId !== "string") {
|
|
164
|
+
errors.push("canvas.activeTabId must be a string");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (errors.length) {
|
|
169
|
+
const error = new Error(`invalid workspace config: ${errors.join("; ")}`);
|
|
170
|
+
error.code = "INVALID_WORKSPACE_CONFIG";
|
|
171
|
+
error.details = errors;
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function applyPatch(currentConfig, patch) {
|
|
177
|
+
const next = { ...currentConfig };
|
|
178
|
+
if (patch.dashboards !== undefined) next.dashboards = patch.dashboards;
|
|
179
|
+
if (patch.widgetTypes !== undefined) next.widgetTypes = patch.widgetTypes;
|
|
180
|
+
if (patch.canvas !== undefined && patch.canvas !== null) {
|
|
181
|
+
next.canvas = {
|
|
182
|
+
...currentConfig.canvas,
|
|
183
|
+
...patch.canvas,
|
|
184
|
+
layout: { ...(currentConfig.canvas?.layout || {}), ...(patch.canvas.layout || {}) },
|
|
185
|
+
bindings: { ...(currentConfig.canvas?.bindings || {}), ...(patch.canvas.bindings || {}) }
|
|
186
|
+
};
|
|
187
|
+
for (const key of ["widgets", "tabs", "activeTabId", "name"]) {
|
|
188
|
+
if (Object.prototype.hasOwnProperty.call(patch.canvas, key) && patch.canvas[key] === null) {
|
|
189
|
+
delete next.canvas[key];
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return next;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function writeWorkspaceConfig(patch) {
|
|
197
|
+
const persistence = describePersistenceMode();
|
|
198
|
+
const adapter = readAdapterConfig();
|
|
199
|
+
if (persistence.mode !== "filesystem") {
|
|
200
|
+
const error = new Error(persistence.reason);
|
|
201
|
+
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
202
|
+
error.adapter = adapter.integrationAdapter;
|
|
203
|
+
throw error;
|
|
204
|
+
}
|
|
205
|
+
const current = await readWorkspaceConfig();
|
|
206
|
+
const next = applyPatch(current, patch);
|
|
207
|
+
validateWorkspaceConfig({
|
|
208
|
+
dashboards: next.dashboards,
|
|
209
|
+
widgetTypes: next.widgetTypes,
|
|
210
|
+
canvas: next.canvas
|
|
211
|
+
});
|
|
212
|
+
const configPath = resolveWorkspaceConfigPath();
|
|
213
|
+
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
214
|
+
if (path.dirname(configPath) !== expectedDir) {
|
|
215
|
+
const error = new Error(`refused to write outside workspace cwd: ${configPath}`);
|
|
216
|
+
error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
await fs.writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
220
|
+
return next;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
GRID_COLUMNS,
|
|
225
|
+
GRID_ROWS,
|
|
226
|
+
KNOWN_WIDGET_KINDS,
|
|
227
|
+
describePersistenceMode,
|
|
228
|
+
readWorkspaceConfig,
|
|
229
|
+
resolveWorkspaceConfigPath,
|
|
230
|
+
validateWorkspaceConfig,
|
|
231
|
+
writeWorkspaceConfig
|
|
232
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -10248,6 +10248,7 @@ var init_growthub_bridge_client = __esm({
|
|
|
10248
10248
|
constructor(session = readActiveSession()) {
|
|
10249
10249
|
this.session = session;
|
|
10250
10250
|
}
|
|
10251
|
+
session;
|
|
10251
10252
|
async listAssets(query = {}) {
|
|
10252
10253
|
const url = bridgeUrl(this.session, "/api/cli/profile", {
|
|
10253
10254
|
view: "gallery-assets",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthub/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.5",
|
|
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": {
|