@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.
Files changed (18) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/settings/apis-webhooks/route.js +59 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/settings/workspace/route.js +70 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +22 -5
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/global-error.jsx +21 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +689 -6
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apis-webhooks/apis-webhooks-form.jsx +208 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apis-webhooks/page.jsx +19 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/apps-list.jsx +43 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/apps/page.jsx +109 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/general/general-settings-form.jsx +134 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/general/page.jsx +25 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +22 -3
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/page.jsx +25 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/settings-shell.jsx +33 -0
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +19 -4
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +186 -1
  17. package/dist/index.js +3 -1
  18. 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: () => setSettingsOpen(true)
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">G</span>
3141
- <span>Growthub Workspace</span>
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((config.server.allowedHostnames ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean));
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.12",
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": {