@growthub/cli 0.9.12 → 0.9.13
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/settings/apis-webhooks/route.js +59 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/settings/workspace/route.js +70 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +22 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/global-error.jsx +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +689 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apis-webhooks/apis-webhooks-form.jsx +208 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apis-webhooks/page.jsx +19 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/apps-list.jsx +43 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +109 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/general/general-settings-form.jsx +134 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/general/page.jsx +25 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +22 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/page.jsx +25 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +33 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +19 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +186 -1
- package/dist/index.js +3 -1
- package/package.json +1 -1
|
@@ -146,6 +146,16 @@ function generateId(prefix) {
|
|
|
146
146
|
return `${prefix}_${Math.random().toString(36).slice(2, 10)}_${Date.now().toString(36)}`;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
function textColorForAccent(accent) {
|
|
150
|
+
const hex = String(accent || "").replace("#", "");
|
|
151
|
+
if (!/^[0-9a-f]{6}$/i.test(hex)) return "#ffffff";
|
|
152
|
+
const red = parseInt(hex.slice(0, 2), 16);
|
|
153
|
+
const green = parseInt(hex.slice(2, 4), 16);
|
|
154
|
+
const blue = parseInt(hex.slice(4, 6), 16);
|
|
155
|
+
const luminance = (0.299 * red + 0.587 * green + 0.114 * blue) / 255;
|
|
156
|
+
return luminance > 0.62 ? "#252525" : "#ffffff";
|
|
157
|
+
}
|
|
158
|
+
|
|
149
159
|
function defaultTitleFor(kind) {
|
|
150
160
|
switch (kind) {
|
|
151
161
|
case "chart": return "Untitled chart";
|
|
@@ -3100,7 +3110,7 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3100
3110
|
});
|
|
3101
3111
|
list.push({
|
|
3102
3112
|
id: "workspace.settings", group: "Workspace", icon: Settings, label: "Go to Workspace Settings", shortcut: "G S",
|
|
3103
|
-
run: () =>
|
|
3113
|
+
run: () => { window.location.href = "/settings/general"; }
|
|
3104
3114
|
});
|
|
3105
3115
|
list.push({
|
|
3106
3116
|
id: "workspace.management", group: "Workspace", icon: Bolt, label: "Go to Management",
|
|
@@ -3137,15 +3147,20 @@ function WorkspaceBuilder({ initialConfig, adapterConfig, integrationAdapter, in
|
|
|
3137
3147
|
return <main className="workspace-builder" onPointerDownCapture={resetWidgetSelectionOnOutsidePointer} style={builderStyle}>
|
|
3138
3148
|
<aside className="workspace-rail" aria-label="Workspace navigation">
|
|
3139
3149
|
<div className="workspace-brand">
|
|
3140
|
-
<span className="workspace-mark"
|
|
3141
|
-
|
|
3150
|
+
<span className="workspace-mark" style={{
|
|
3151
|
+
background: branding.logoUrl ? undefined : branding.accent || undefined,
|
|
3152
|
+
color: branding.logoUrl ? undefined : textColorForAccent(branding.accent)
|
|
3153
|
+
}}>
|
|
3154
|
+
{branding.logoUrl ? <img src={branding.logoUrl} alt="" /> : (branding.name || config.name || "Growthub Workspace").slice(0, 1).toUpperCase()}
|
|
3155
|
+
</span>
|
|
3156
|
+
<span>{branding.name || config.name || "Growthub Workspace"}</span>
|
|
3142
3157
|
</div>
|
|
3143
3158
|
<nav className="workspace-nav">
|
|
3144
3159
|
<button type="button" className={workspaceView === "dashboards" ? "active workspace-nav-button" : "workspace-nav-button"} onClick={showDashboardHome}>Dashboards</button>
|
|
3145
3160
|
<Link href="/data-model">Data Model</Link>
|
|
3146
3161
|
<Link href="/settings/integrations">Integrations</Link>
|
|
3147
|
-
<button type="button" className="workspace-nav-button" onClick={() => setSettingsOpen(true)}>Workspace Settings</button>
|
|
3148
3162
|
<button type="button" className="workspace-nav-button" onClick={() => setManagementOpen(true)}>Management</button>
|
|
3163
|
+
<Link className="workspace-nav-bottom" href="/settings/general">Workspace Settings</Link>
|
|
3149
3164
|
</nav>
|
|
3150
3165
|
<div className="workspace-rail-status">
|
|
3151
3166
|
<span className="status-dot" />
|
|
@@ -155,6 +155,189 @@ async function writeWorkspaceConfig(patch) {
|
|
|
155
155
|
return next;
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
function normalizeWorkspaceIdentityPatch(patch) {
|
|
159
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch)) {
|
|
160
|
+
const error = new Error("settings patch must be a plain object");
|
|
161
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const allowed = new Set(["name", "branding"]);
|
|
166
|
+
const unknown = Object.keys(patch).filter((key) => !allowed.has(key));
|
|
167
|
+
if (unknown.length) {
|
|
168
|
+
const error = new Error("settings patch contains unknown fields");
|
|
169
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
170
|
+
error.details = unknown;
|
|
171
|
+
throw error;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const normalized = {};
|
|
175
|
+
if (Object.prototype.hasOwnProperty.call(patch, "name")) {
|
|
176
|
+
if (typeof patch.name !== "string") {
|
|
177
|
+
const error = new Error("name must be a string");
|
|
178
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
normalized.name = patch.name.trim() || "Growthub Workspace";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (Object.prototype.hasOwnProperty.call(patch, "branding")) {
|
|
185
|
+
if (!patch.branding || typeof patch.branding !== "object" || Array.isArray(patch.branding)) {
|
|
186
|
+
const error = new Error("branding must be a plain object");
|
|
187
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
const brandingAllowed = new Set(["name", "logoUrl", "accent"]);
|
|
191
|
+
const brandingUnknown = Object.keys(patch.branding).filter((key) => !brandingAllowed.has(key));
|
|
192
|
+
if (brandingUnknown.length) {
|
|
193
|
+
const error = new Error("branding patch contains unknown fields");
|
|
194
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
195
|
+
error.details = brandingUnknown.map((key) => `branding.${key}`);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
normalized.branding = {};
|
|
199
|
+
for (const key of brandingAllowed) {
|
|
200
|
+
if (Object.prototype.hasOwnProperty.call(patch.branding, key)) {
|
|
201
|
+
if (typeof patch.branding[key] !== "string") {
|
|
202
|
+
const error = new Error(`branding.${key} must be a string`);
|
|
203
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
normalized.branding[key] = patch.branding[key].trim();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return normalized;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function writeWorkspaceIdentitySettings(patch) {
|
|
215
|
+
const persistence = describePersistenceMode();
|
|
216
|
+
const adapter = readAdapterConfig();
|
|
217
|
+
if (persistence.mode !== PERSISTENCE_ADAPTERS.FILESYSTEM || !persistence.canSave) {
|
|
218
|
+
const error = new Error(persistence.reason);
|
|
219
|
+
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
220
|
+
error.adapter = adapter.integrationAdapter;
|
|
221
|
+
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const normalized = normalizeWorkspaceIdentityPatch(patch);
|
|
226
|
+
const current = await readWorkspaceConfig();
|
|
227
|
+
const next = { ...current };
|
|
228
|
+
if (normalized.name !== undefined) {
|
|
229
|
+
next.name = normalized.name;
|
|
230
|
+
}
|
|
231
|
+
if (normalized.branding) {
|
|
232
|
+
next.branding = {
|
|
233
|
+
...(current.branding && typeof current.branding === "object" && !Array.isArray(current.branding)
|
|
234
|
+
? current.branding
|
|
235
|
+
: {}),
|
|
236
|
+
...normalized.branding
|
|
237
|
+
};
|
|
238
|
+
if (!next.branding.name) {
|
|
239
|
+
next.branding.name = next.name || "Growthub Workspace";
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
validateWorkspaceConfig({
|
|
244
|
+
dashboards: next.dashboards,
|
|
245
|
+
widgetTypes: next.widgetTypes,
|
|
246
|
+
canvas: next.canvas,
|
|
247
|
+
dataModel: next.dataModel
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const configPath = resolveWorkspaceConfigPath();
|
|
251
|
+
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
252
|
+
if (path.dirname(configPath) !== expectedDir) {
|
|
253
|
+
const error = new Error(`refused to write outside workspace cwd: ${configPath}`);
|
|
254
|
+
error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
await fs.writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
258
|
+
return next;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeApiWebhookRefs(refs) {
|
|
262
|
+
if (!Array.isArray(refs)) {
|
|
263
|
+
const error = new Error("refs must be an array");
|
|
264
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
return refs
|
|
268
|
+
.map((item, index) => {
|
|
269
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
270
|
+
const error = new Error("each ref must be a plain object");
|
|
271
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
272
|
+
throw error;
|
|
273
|
+
}
|
|
274
|
+
const allowed = new Set(["id", "label", "kind", "endpointRef", "status", "hasSecret", "url"]);
|
|
275
|
+
const unknown = Object.keys(item).filter((key) => !allowed.has(key));
|
|
276
|
+
if (unknown.length) {
|
|
277
|
+
const error = new Error("ref contains unknown fields");
|
|
278
|
+
error.code = "INVALID_WORKSPACE_SETTINGS_PATCH";
|
|
279
|
+
error.details = unknown;
|
|
280
|
+
throw error;
|
|
281
|
+
}
|
|
282
|
+
const kind = item.kind === "webhook" ? "webhook" : "api";
|
|
283
|
+
const label = typeof item.label === "string" ? item.label.trim() : "";
|
|
284
|
+
const endpointRef = typeof item.endpointRef === "string" ? item.endpointRef.trim() : "";
|
|
285
|
+
const url = typeof item.url === "string" ? item.url.trim() : "";
|
|
286
|
+
if (!label && !endpointRef && !url && item.hasSecret !== true) return null;
|
|
287
|
+
return {
|
|
288
|
+
id: typeof item.id === "string" && item.id.trim() ? item.id.trim() : `custom-${kind}-${index + 1}`,
|
|
289
|
+
label: label || endpointRef,
|
|
290
|
+
kind,
|
|
291
|
+
sourceType: "custom-api-webhooks",
|
|
292
|
+
endpointRef,
|
|
293
|
+
url,
|
|
294
|
+
status: typeof item.status === "string" && item.status.trim() ? item.status.trim() : "configured",
|
|
295
|
+
hasSecret: item.hasSecret === true
|
|
296
|
+
};
|
|
297
|
+
})
|
|
298
|
+
.filter(Boolean);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function writeWorkspaceApiWebhookSettings(patch) {
|
|
302
|
+
const persistence = describePersistenceMode();
|
|
303
|
+
const adapter = readAdapterConfig();
|
|
304
|
+
if (persistence.mode !== PERSISTENCE_ADAPTERS.FILESYSTEM || !persistence.canSave) {
|
|
305
|
+
const error = new Error(persistence.reason);
|
|
306
|
+
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
307
|
+
error.adapter = adapter.integrationAdapter;
|
|
308
|
+
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
309
|
+
throw error;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const refs = normalizeApiWebhookRefs(patch?.refs);
|
|
313
|
+
const current = await readWorkspaceConfig();
|
|
314
|
+
const existing = Array.isArray(current.integrations) ? current.integrations : [];
|
|
315
|
+
const next = {
|
|
316
|
+
...current,
|
|
317
|
+
integrations: [
|
|
318
|
+
...existing.filter((item) => item?.sourceType !== "custom-api-webhooks"),
|
|
319
|
+
...refs
|
|
320
|
+
]
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
validateWorkspaceConfig({
|
|
324
|
+
dashboards: next.dashboards,
|
|
325
|
+
widgetTypes: next.widgetTypes,
|
|
326
|
+
canvas: next.canvas,
|
|
327
|
+
dataModel: next.dataModel
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const configPath = resolveWorkspaceConfigPath();
|
|
331
|
+
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
332
|
+
if (path.dirname(configPath) !== expectedDir) {
|
|
333
|
+
const error = new Error(`refused to write outside workspace cwd: ${configPath}`);
|
|
334
|
+
error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
|
|
335
|
+
throw error;
|
|
336
|
+
}
|
|
337
|
+
await fs.writeFile(configPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
338
|
+
return next.integrations.filter((item) => item?.sourceType === "custom-api-webhooks");
|
|
339
|
+
}
|
|
340
|
+
|
|
158
341
|
export {
|
|
159
342
|
GRID_COLUMNS,
|
|
160
343
|
GRID_ROWS,
|
|
@@ -165,5 +348,7 @@ export {
|
|
|
165
348
|
readWorkspaceConfig,
|
|
166
349
|
resolveWorkspaceConfigPath,
|
|
167
350
|
validateWorkspaceConfig,
|
|
168
|
-
writeWorkspaceConfig
|
|
351
|
+
writeWorkspaceConfig,
|
|
352
|
+
writeWorkspaceApiWebhookSettings,
|
|
353
|
+
writeWorkspaceIdentitySettings
|
|
169
354
|
};
|
package/dist/index.js
CHANGED
|
@@ -14199,7 +14199,9 @@ async function addAllowedHostname(host, opts) {
|
|
|
14199
14199
|
return;
|
|
14200
14200
|
}
|
|
14201
14201
|
const normalized = normalizeHostnameInput(host);
|
|
14202
|
-
const current = new Set(
|
|
14202
|
+
const current = new Set(
|
|
14203
|
+
(config.server.allowedHostnames ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
14204
|
+
);
|
|
14203
14205
|
const existed = current.has(normalized);
|
|
14204
14206
|
current.add(normalized);
|
|
14205
14207
|
config.server.allowedHostnames = Array.from(current).sort();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthub/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.13",
|
|
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": {
|