@growthub/cli 0.13.5 → 0.13.7

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 (39) hide show
  1. package/assets/worker-kits/growthub-custom-workspace-starter-v1/QUICKSTART.md +19 -0
  2. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/.env.example +8 -0
  3. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +4 -0
  4. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/action/execute/route.js +60 -0
  5. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/actions/route.js +50 -0
  6. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connect-session/route.js +68 -0
  7. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/connection-status/route.js +56 -0
  8. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/proxy/route.js +67 -0
  9. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integrations/nango/status/route.js +50 -0
  10. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/metadata-graph/route.js +1 -0
  11. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +172 -0
  12. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +161 -50
  13. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/NangoConnectionPanel.jsx +531 -0
  14. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +274 -18
  15. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/nango/page.jsx +167 -0
  16. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/settings/integrations/page.jsx +1 -0
  17. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +62 -7
  18. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +554 -48
  19. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-rail.jsx +24 -14
  20. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/env.js +7 -0
  21. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/index.js +38 -0
  22. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-adapter.js +552 -0
  23. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-config-loader.js +202 -0
  24. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/nango/nango-schema.js +303 -0
  25. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +49 -10
  26. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +1 -1
  27. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/nango.js +49 -0
  28. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/templates/template-registry.js +4 -2
  29. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +2 -2
  30. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +534 -0
  31. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +3 -1
  32. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +82 -0
  33. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +102 -3
  34. package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
  35. package/assets/worker-kits/growthub-custom-workspace-starter-v1/bundles/growthub-custom-workspace-starter-v1.json +1 -0
  36. package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +5 -0
  37. package/assets/worker-kits/growthub-custom-workspace-starter-v1/templates/seeded-configs/project-management.config.json +276 -0
  38. package/dist/index.js +127 -44
  39. package/package.json +1 -1
@@ -305,7 +305,7 @@ function findSandboxRowsForRegistry(workspaceConfig, integrationId) {
305
305
  if (!sandboxObject) return [];
306
306
  const rows = Array.isArray(sandboxObject.rows) ? sandboxObject.rows : [];
307
307
  return rows.filter((row) => {
308
- const graph = parseOrchestrationGraph(row?.orchestrationGraph);
308
+ const graph = parseOrchestrationGraph(row?.orchestrationConfig || row?.orchestrationGraph);
309
309
  if (!graph?.nodes) return String(row?.schedulerRegistryId || "").trim() === id;
310
310
  return graph.nodes.some(
311
311
  (node) => node?.type === "api-registry-call"
@@ -359,7 +359,7 @@ function buildSandboxRowFromApiRegistry(workspaceConfig, registryRow, options =
359
359
  lastRunId: "",
360
360
  lastSourceId: "",
361
361
  lastResponse: "",
362
- orchestrationGraph: serializeOrchestrationGraph(graph),
362
+ orchestrationConfig: serializeOrchestrationGraph(graph),
363
363
  description: String(options.description || registryRow?.description || "").trim()
364
364
  };
365
365
  }
@@ -0,0 +1,534 @@
1
+ /**
2
+ * Growthub Workspace Customer Activation Layer V1 — derivation helpers.
3
+ *
4
+ * Reads the existing governed artifacts (workspaceConfig + sidecar
5
+ * source records + workspace metadata graph store) and projects a typed,
6
+ * read-only activation checklist:
7
+ *
8
+ * - what template the workspace was scaffolded from (provenance)
9
+ * - which setup steps are still pending
10
+ * - what the next obvious action is
11
+ *
12
+ * Authority order:
13
+ *
14
+ * 1. growthub.config.json (governed workspace artifact)
15
+ * 2. growthub.source-records.json (sidecar source/run state)
16
+ * 3. derived metadata store (workspace-metadata-store.js)
17
+ *
18
+ * Invariants:
19
+ *
20
+ * - Pure derivation. No React. No fetch. No mutation of inputs.
21
+ * - Never includes secrets, OAuth tokens, provider credentials, or
22
+ * access/refresh tokens in the output. Connection IDs are surfaced
23
+ * as booleans only (`connectionConfigured: true/false`).
24
+ * - Activation state is NOT persisted — every checklist field is
25
+ * recomputable from inputs on every page load. Don't add hidden
26
+ * storage. Don't add localStorage. Don't add a separate sidecar.
27
+ * - Backwards-compatible: a workspace with no `provenance` block still
28
+ * produces a valid (generic) activation state.
29
+ *
30
+ * The output shape:
31
+ *
32
+ * {
33
+ * kind: "growthub-workspace-activation-state-v1"
34
+ * version: 1
35
+ * template: "blank" | "project-management" | <slug>
36
+ * templateName:string
37
+ * headline: string // user-facing "what is this workspace for"
38
+ * subheadline: string // user-facing "what's next"
39
+ * complete: boolean
40
+ * completedCount: number
41
+ * totalCount: number
42
+ * nextStepId: string | null
43
+ * steps: [
44
+ * {
45
+ * id: string // stable; safe to use as key
46
+ * label: string // short title
47
+ * description:string // one-line user-facing copy
48
+ * status: "complete"|"pending"|"blocked"|"optional"
49
+ * href: string // deep link into existing surface
50
+ * hint?: string // why it's blocked (no secrets)
51
+ * cta?: string // button label override
52
+ * }
53
+ * ]
54
+ * }
55
+ */
56
+
57
+ const ACTIVATION_KIND = "growthub-workspace-activation-state-v1";
58
+ const ACTIVATION_VERSION = 1;
59
+
60
+ const TEMPLATE_PROJECT_MANAGEMENT = "project-management";
61
+
62
+ function isPlainObject(value) {
63
+ return value !== null && typeof value === "object" && !Array.isArray(value);
64
+ }
65
+
66
+ function safeString(value) {
67
+ if (value == null) return "";
68
+ return typeof value === "string" ? value : String(value);
69
+ }
70
+
71
+ function findDataModelObject(workspaceConfig, predicate) {
72
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
73
+ ? workspaceConfig.dataModel.objects
74
+ : [];
75
+ for (const object of objects) {
76
+ if (!isPlainObject(object)) continue;
77
+ if (predicate(object)) return object;
78
+ }
79
+ return null;
80
+ }
81
+
82
+ function listObjectRows(object) {
83
+ return Array.isArray(object?.rows) ? object.rows : [];
84
+ }
85
+
86
+ function safeBoolean(value) {
87
+ return Boolean(value);
88
+ }
89
+
90
+ function deriveProvenance(workspaceConfig) {
91
+ const provenance = isPlainObject(workspaceConfig?.provenance) ? workspaceConfig.provenance : null;
92
+ const template = safeString(provenance?.template).trim().toLowerCase();
93
+ const templateKind = safeString(provenance?.templateKind).trim();
94
+ const privacy = safeString(provenance?.privacy).trim();
95
+ const note = safeString(provenance?.note).trim();
96
+ const templateName = safeString(workspaceConfig?.name || provenance?.label || "").trim();
97
+ return {
98
+ hasProvenance: Boolean(provenance),
99
+ template: template || "blank",
100
+ templateKind,
101
+ privacy,
102
+ note,
103
+ templateName,
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Detect whether a string-or-list-of-strings field on an api-registry row
109
+ * contains at least one configured connection ID. We surface this as a
110
+ * boolean only — the connection identifier itself is never echoed into the
111
+ * activation state (it can be PII-adjacent).
112
+ */
113
+ function hasConnectionId(row) {
114
+ if (!isPlainObject(row)) return false;
115
+ const raw = row.connectionIds ?? row.connectionId;
116
+ if (Array.isArray(raw)) {
117
+ return raw.some((entry) => safeString(entry).trim().length > 0);
118
+ }
119
+ return safeString(raw).trim().length > 0;
120
+ }
121
+
122
+ /**
123
+ * Detect whether the workspace source-records sidecar has any rows for
124
+ * the given source id. Reads `recordCount` first (cheap) and falls back to
125
+ * `records.length`. Never inspects the underlying records.
126
+ */
127
+ function hasSourceRecords(workspaceSourceRecords, sourceId) {
128
+ if (!isPlainObject(workspaceSourceRecords)) return false;
129
+ const key = safeString(sourceId).trim();
130
+ if (!key) return false;
131
+ const sidecar = workspaceSourceRecords[key];
132
+ if (!isPlainObject(sidecar)) return false;
133
+ if (Number.isFinite(sidecar.recordCount) && sidecar.recordCount > 0) return true;
134
+ if (Array.isArray(sidecar.records) && sidecar.records.length > 0) return true;
135
+ return false;
136
+ }
137
+
138
+ /**
139
+ * Pull the latest workflow + sandbox row status from the typed metadata
140
+ * store when it's available, falling back to inspecting workspaceConfig
141
+ * directly. The store path is preferred because it dedupes runs and tags
142
+ * pipeline health, but neither path is required.
143
+ */
144
+ function findWorkflowRow(workspaceConfig, predicate) {
145
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
146
+ ? workspaceConfig.dataModel.objects
147
+ : [];
148
+ for (const object of objects) {
149
+ if (!isPlainObject(object) || object.objectType !== "sandbox-environment") continue;
150
+ const rows = listObjectRows(object);
151
+ for (const row of rows) {
152
+ if (!isPlainObject(row)) continue;
153
+ if (predicate(row, object)) return { object, row };
154
+ }
155
+ }
156
+ return null;
157
+ }
158
+
159
+ function parseSafe(value) {
160
+ if (isPlainObject(value)) return value;
161
+ if (typeof value !== "string") return null;
162
+ const trimmed = value.trim();
163
+ if (!trimmed) return null;
164
+ try {
165
+ const parsed = JSON.parse(trimmed);
166
+ return isPlainObject(parsed) ? parsed : null;
167
+ } catch {
168
+ return null;
169
+ }
170
+ }
171
+
172
+ function deriveLatestRunStatus(workflowRow) {
173
+ if (!isPlainObject(workflowRow)) return { status: "never", ok: false };
174
+ const status = safeString(workflowRow.status).trim().toLowerCase();
175
+ const lastResponse = parseSafe(workflowRow.lastResponse);
176
+ if (!lastResponse) {
177
+ if (status === "tested" || status === "ok" || status === "success") {
178
+ return { status: "ok", ok: true };
179
+ }
180
+ if (status === "failed" || status === "error") {
181
+ return { status: "failed", ok: false };
182
+ }
183
+ return { status: "never", ok: false };
184
+ }
185
+ const exitCode = Number.isFinite(lastResponse.exitCode) ? Number(lastResponse.exitCode) : null;
186
+ const ok = exitCode === 0 && !safeString(lastResponse.error).trim();
187
+ return { status: ok ? "ok" : "failed", ok };
188
+ }
189
+
190
+ function findDashboardByIdOrName(workspaceConfig, predicate) {
191
+ const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
192
+ for (const dashboard of dashboards) {
193
+ if (!isPlainObject(dashboard)) continue;
194
+ if (predicate(dashboard)) return dashboard;
195
+ }
196
+ return null;
197
+ }
198
+
199
+ // ───────────────────────────────────────────────────────────────────────────
200
+ // Project Management Workspace Template — adapter
201
+ // ───────────────────────────────────────────────────────────────────────────
202
+
203
+ /**
204
+ * Derive the Project Management activation checklist.
205
+ *
206
+ * The seed config (`templates/seeded-configs/project-management.config.json`)
207
+ * ships a Nango-backed api-registry row, a Project Task Source, a sandbox
208
+ * workflow row, and a dashboard. This helper checks each one and builds
209
+ * the deep-link checklist.
210
+ */
211
+ function deriveProjectManagementActivationState({ workspaceConfig, workspaceSourceRecords, metadataGraph }) {
212
+ const provenance = deriveProvenance(workspaceConfig);
213
+ const registry = findDataModelObject(workspaceConfig, (o) => o.objectType === "api-registry");
214
+ const registryRow = registry
215
+ ? listObjectRows(registry).find((row) => safeString(row?.connectorKind).trim().toLowerCase() === "nango")
216
+ || listObjectRows(registry)[0]
217
+ : null;
218
+ const providerConfigKey = safeString(registryRow?.providerConfigKey).trim()
219
+ || safeString(registryRow?.integrationId).trim();
220
+ const connectionConfigured = hasConnectionId(registryRow);
221
+
222
+ const sourceObject = findDataModelObject(workspaceConfig, (o) => o.id === "project-task-source"
223
+ || o.objectType === "data-source");
224
+ const sourceRow = listObjectRows(sourceObject)[0] || null;
225
+ const sourceId = safeString(sourceRow?.sourceId).trim();
226
+ const sourceHasRecords = hasSourceRecords(workspaceSourceRecords, sourceId);
227
+
228
+ const workflowMatch = findWorkflowRow(workspaceConfig, (row) => {
229
+ const name = safeString(row?.Name || row?.name).trim();
230
+ return name === "project-active-tasks-workflow"
231
+ || name.toLowerCase().includes("project-active-tasks");
232
+ });
233
+ const workflowRow = workflowMatch?.row || null;
234
+ const workflowObjectId = workflowMatch?.object?.id || "sandbox-environments";
235
+ const workflowRowName = safeString(workflowRow?.Name || workflowRow?.name).trim();
236
+ const workflowRun = deriveLatestRunStatus(workflowRow);
237
+
238
+ const dashboard = findDashboardByIdOrName(workspaceConfig,
239
+ (d) => d.id === "project-management-template"
240
+ || safeString(d.name).trim().toLowerCase() === "project management");
241
+
242
+ // NANGO_SECRET_KEY presence is impossible to detect in pure derivation
243
+ // (it's an env var the browser can't read). The metadata graph exposes a
244
+ // safe boolean via authority/runtime status if the server has surfaced it;
245
+ // otherwise the step always asks the user to confirm.
246
+ const integrationsRuntime = isPlainObject(metadataGraph?.runtime) ? metadataGraph.runtime : null;
247
+ const nangoConfiguredHint = integrationsRuntime?.nangoConfigured;
248
+ // If the runtime explicitly says configured, mark the env step as
249
+ // complete. If it explicitly says not-configured OR we have no signal at
250
+ // all, we don't claim it's complete — the user still needs to confirm.
251
+ const nangoEnvComplete = nangoConfiguredHint === true;
252
+
253
+ const steps = [];
254
+
255
+ steps.push({
256
+ id: "provider-env",
257
+ label: "Configure provider auth",
258
+ description: "Set NANGO_SECRET_KEY in your runtime environment so the workspace can talk to Nango.",
259
+ status: nangoEnvComplete ? "complete" : "pending",
260
+ href: "/settings/integrations",
261
+ hint: nangoEnvComplete
262
+ ? ""
263
+ : "Set NANGO_SECRET_KEY in .env.local (or your hosted runtime), then restart the workspace server.",
264
+ cta: nangoEnvComplete ? "Review integrations" : "Open integration settings",
265
+ });
266
+
267
+ const nangoStepHref = registry && registryRow
268
+ ? `/data-model?object=${encodeURIComponent(registry.id)}&row=${encodeURIComponent(safeString(registryRow.integrationId).trim() || "asana-active-tasks")}`
269
+ : "/data-model";
270
+
271
+ steps.push({
272
+ id: "nango-connection",
273
+ label: "Connect provider through Nango",
274
+ description: connectionConfigured
275
+ ? `Connection configured for providerConfigKey "${providerConfigKey || "asana"}".`
276
+ : "Open the API Registry row and run Create Connect Session, then paste the connectionId Nango returns.",
277
+ status: connectionConfigured ? "complete" : (nangoEnvComplete ? "pending" : "blocked"),
278
+ href: nangoStepHref,
279
+ hint: connectionConfigured
280
+ ? ""
281
+ : nangoEnvComplete
282
+ ? "Use the Nango panel in the API Registry row — your workspace never sees provider tokens."
283
+ : "Finish the previous step first — Nango needs NANGO_SECRET_KEY to mint a Connect Session.",
284
+ cta: connectionConfigured ? "Manage connection" : "Open Nango panel",
285
+ });
286
+
287
+ const workflowHref = workflowRowName
288
+ ? `/workflows?object=${encodeURIComponent(workflowObjectId)}&row=${encodeURIComponent(workflowRowName)}&field=orchestrationConfig`
289
+ : "/workflows";
290
+
291
+ const workflowComplete = workflowRun.ok && sourceHasRecords;
292
+
293
+ steps.push({
294
+ id: "workflow-run",
295
+ label: "Run the Active Tasks workflow",
296
+ description: workflowComplete
297
+ ? "Workflow has executed successfully and refreshed the Project Task Source."
298
+ : workflowRun.status === "failed"
299
+ ? "The last run failed — open the run trace to see what went wrong."
300
+ : "Open the seeded workflow and click Test to pull active tasks from your project.",
301
+ status: workflowComplete
302
+ ? "complete"
303
+ : connectionConfigured
304
+ ? (workflowRun.status === "failed" ? "blocked" : "pending")
305
+ : "blocked",
306
+ href: workflowHref,
307
+ hint: workflowComplete
308
+ ? ""
309
+ : connectionConfigured
310
+ ? workflowRun.status === "failed"
311
+ ? "Open the workflow's See Runs trace and check the failing node's response."
312
+ : "Fill projectGid + workspaceGid in the input node, then click Test."
313
+ : "Finish the Nango connection step first.",
314
+ cta: workflowComplete ? "Open workflow" : workflowRun.status === "failed" ? "Open run trace" : "Open workflow",
315
+ });
316
+
317
+ const dashboardHref = dashboard
318
+ ? `/?dashboard=${encodeURIComponent(dashboard.id)}`
319
+ : "/";
320
+
321
+ steps.push({
322
+ id: "dashboard-view",
323
+ label: "View the Active Tasks dashboard",
324
+ description: workflowComplete
325
+ ? "Open the dashboard to see the latest active project tasks."
326
+ : "After the workflow runs successfully, hydrated tasks will appear in this dashboard.",
327
+ status: workflowComplete ? "complete" : "pending",
328
+ href: dashboardHref,
329
+ cta: "Open dashboard",
330
+ });
331
+
332
+ steps.push({
333
+ id: "customize",
334
+ label: "Customize this workspace",
335
+ description: "Duplicate the dashboard, add objects, schedule the workflow, or wire another provider through Nango.",
336
+ status: "optional",
337
+ href: "/data-model",
338
+ cta: "Customize",
339
+ });
340
+
341
+ const completedCount = steps.filter((step) => step.status === "complete").length;
342
+ const requiredCount = steps.filter((step) => step.status !== "optional").length;
343
+ const complete = completedCount >= requiredCount;
344
+ const nextStep = steps.find((step) => step.status === "pending" || step.status === "blocked");
345
+
346
+ return {
347
+ kind: ACTIVATION_KIND,
348
+ version: ACTIVATION_VERSION,
349
+ template: TEMPLATE_PROJECT_MANAGEMENT,
350
+ templateName: provenance.templateName || "Project Management Workspace",
351
+ headline: complete
352
+ ? "Your Project Management workspace is live."
353
+ : `You're ${requiredCount - completedCount} step${requiredCount - completedCount === 1 ? "" : "s"} from your first task dashboard.`,
354
+ subheadline: complete
355
+ ? "Inspect runs, customize objects, or wire another provider."
356
+ : nextStep
357
+ ? `Next: ${nextStep.label}.`
358
+ : "Open the dashboard to see your latest tasks.",
359
+ complete,
360
+ completedCount,
361
+ totalCount: requiredCount,
362
+ nextStepId: nextStep ? nextStep.id : null,
363
+ steps,
364
+ };
365
+ }
366
+
367
+ // ───────────────────────────────────────────────────────────────────────────
368
+ // Blank governed workspace — adapter
369
+ // ───────────────────────────────────────────────────────────────────────────
370
+
371
+ /**
372
+ * Generic activation checklist for blank governed workspaces. Drives the
373
+ * user toward the first object → first dashboard → first widget → first
374
+ * workflow → first run, using only routes that already exist.
375
+ */
376
+ function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceRecords, metadataGraph }) {
377
+ const provenance = deriveProvenance(workspaceConfig);
378
+ const objects = Array.isArray(workspaceConfig?.dataModel?.objects)
379
+ ? workspaceConfig.dataModel.objects
380
+ : [];
381
+
382
+ // Hidden helper objects don't count as "user-created".
383
+ const HIDDEN_OBJECT_IDS = new Set([
384
+ "workspace-helper-sandbox",
385
+ "nav-folders",
386
+ "helper-threads",
387
+ "sandbox-environments",
388
+ "workflow-api-registry",
389
+ "workspace-ui-cache",
390
+ ]);
391
+ const userObjects = objects.filter((o) => isPlainObject(o)
392
+ && safeString(o.id).trim()
393
+ && !HIDDEN_OBJECT_IDS.has(o.id));
394
+ const objectCreated = userObjects.length > 0;
395
+
396
+ const dashboards = Array.isArray(workspaceConfig?.dashboards) ? workspaceConfig.dashboards : [];
397
+ const dashboardCreated = dashboards.length > 0;
398
+ const widgetCount = dashboards.reduce((acc, dashboard) => {
399
+ const tabs = Array.isArray(dashboard?.tabs) ? dashboard.tabs : [];
400
+ for (const tab of tabs) {
401
+ const widgets = Array.isArray(tab?.widgets) ? tab.widgets : [];
402
+ acc += widgets.length;
403
+ }
404
+ return acc;
405
+ }, 0);
406
+ const widgetAdded = widgetCount > 0;
407
+
408
+ const workflowMatch = findWorkflowRow(workspaceConfig, () => true);
409
+ const workflowCreated = Boolean(workflowMatch?.row);
410
+ const workflowRun = deriveLatestRunStatus(workflowMatch?.row);
411
+
412
+ const steps = [
413
+ {
414
+ id: "create-object",
415
+ label: "Create your first object",
416
+ description: objectCreated
417
+ ? `${userObjects.length} object${userObjects.length === 1 ? "" : "s"} in your Data Model.`
418
+ : "Start by adding a custom object or connecting a data source in Management.",
419
+ status: objectCreated ? "complete" : "pending",
420
+ href: "/data-model",
421
+ cta: objectCreated ? "Open Data Model" : "Create object",
422
+ },
423
+ {
424
+ id: "create-dashboard",
425
+ label: "Create a dashboard",
426
+ description: dashboardCreated
427
+ ? `${dashboards.length} dashboard${dashboards.length === 1 ? "" : "s"} ready.`
428
+ : "Add a dashboard from the Builder to visualize your data.",
429
+ status: dashboardCreated ? "complete" : (objectCreated ? "pending" : "blocked"),
430
+ href: "/",
431
+ hint: dashboardCreated || objectCreated ? "" : "Add an object first so dashboards have data to bind to.",
432
+ cta: dashboardCreated ? "Open Builder" : "New dashboard",
433
+ },
434
+ {
435
+ id: "add-widget",
436
+ label: "Add a widget",
437
+ description: widgetAdded
438
+ ? `${widgetCount} widget${widgetCount === 1 ? "" : "s"} placed.`
439
+ : "Bind a chart or view widget to one of your objects.",
440
+ status: widgetAdded ? "complete" : (dashboardCreated ? "pending" : "blocked"),
441
+ href: "/",
442
+ hint: widgetAdded || dashboardCreated ? "" : "Add a dashboard first.",
443
+ cta: widgetAdded ? "Open Builder" : "Add widget",
444
+ },
445
+ {
446
+ id: "create-workflow",
447
+ label: "Create a workflow",
448
+ description: workflowCreated
449
+ ? "Sandbox workflow scaffolded."
450
+ : "Open Workflows to assemble your first automation.",
451
+ status: workflowCreated ? "complete" : "pending",
452
+ href: "/workflows",
453
+ cta: workflowCreated ? "Open Workflows" : "New workflow",
454
+ },
455
+ {
456
+ id: "run-workflow",
457
+ label: "Run your workflow",
458
+ description: workflowRun.ok
459
+ ? "Workflow has run successfully at least once."
460
+ : workflowRun.status === "failed"
461
+ ? "Last run failed — open the trace and fix the failing node."
462
+ : "Click Test inside the workflow to do a first run.",
463
+ status: workflowRun.ok ? "complete" : (workflowCreated ? "pending" : "blocked"),
464
+ href: "/workflows",
465
+ hint: workflowRun.ok || workflowCreated ? "" : "Create a workflow first.",
466
+ cta: workflowRun.ok ? "View runs" : "Open workflow",
467
+ },
468
+ ];
469
+
470
+ const completedCount = steps.filter((step) => step.status === "complete").length;
471
+ const requiredCount = steps.length;
472
+ const complete = completedCount >= requiredCount;
473
+ const nextStep = steps.find((step) => step.status === "pending" || step.status === "blocked");
474
+
475
+ return {
476
+ kind: ACTIVATION_KIND,
477
+ version: ACTIVATION_VERSION,
478
+ template: "blank",
479
+ templateName: provenance.templateName || "Governed Workspace",
480
+ headline: complete
481
+ ? "Your workspace is set up."
482
+ : "Get started with your governed workspace.",
483
+ subheadline: complete
484
+ ? "Add more objects, widgets, or workflows as your needs grow."
485
+ : nextStep
486
+ ? `Next: ${nextStep.label}.`
487
+ : "Pick the next step that fits what you want to build.",
488
+ complete,
489
+ completedCount,
490
+ totalCount: requiredCount,
491
+ nextStepId: nextStep ? nextStep.id : null,
492
+ steps,
493
+ // Silence unused-arg warnings — sidecar inputs are accepted by every
494
+ // adapter so callers can pass the same envelope through.
495
+ _sourceRecords: workspaceSourceRecords ? true : false,
496
+ _metadata: metadataGraph ? true : false,
497
+ };
498
+ }
499
+
500
+ // ───────────────────────────────────────────────────────────────────────────
501
+ // Public entry — template router
502
+ // ───────────────────────────────────────────────────────────────────────────
503
+
504
+ /**
505
+ * Resolve the activation state for the current workspace.
506
+ *
507
+ * Templates are routed by `workspaceConfig.provenance.template`. Unknown
508
+ * templates and workspaces without provenance fall back to the blank
509
+ * adapter so existing workspaces never lose their UX.
510
+ */
511
+ function deriveWorkspaceActivationState(input = {}) {
512
+ const safeInput = {
513
+ workspaceConfig: isPlainObject(input.workspaceConfig) ? input.workspaceConfig : {},
514
+ workspaceSourceRecords: isPlainObject(input.workspaceSourceRecords) ? input.workspaceSourceRecords : {},
515
+ metadataGraph: isPlainObject(input.metadataGraph) ? input.metadataGraph : null,
516
+ };
517
+ const provenance = deriveProvenance(safeInput.workspaceConfig);
518
+ if (provenance.template === TEMPLATE_PROJECT_MANAGEMENT) {
519
+ return deriveProjectManagementActivationState(safeInput);
520
+ }
521
+ return deriveBlankWorkspaceActivationState(safeInput);
522
+ }
523
+
524
+ export {
525
+ ACTIVATION_KIND,
526
+ ACTIVATION_VERSION,
527
+ TEMPLATE_PROJECT_MANAGEMENT,
528
+ deriveWorkspaceActivationState,
529
+ deriveProjectManagementActivationState,
530
+ deriveBlankWorkspaceActivationState,
531
+ deriveProvenance,
532
+ hasConnectionId,
533
+ hasSourceRecords,
534
+ };
@@ -417,6 +417,7 @@ function deriveManualObjectTable(object, options = {}) {
417
417
  source,
418
418
  objectType: object.objectType || "custom",
419
419
  icon: object.icon || null,
420
+ locked: object.locked !== undefined ? Boolean(object.locked) : Boolean(object.objectType && object.objectType !== "custom"),
420
421
  pickerHidden: Boolean(object.pickerHidden),
421
422
  columns: hydratedColumns,
422
423
  rows: hydratedRows,
@@ -447,6 +448,7 @@ function deriveManualObjectTable(object, options = {}) {
447
448
  const HIDDEN_HELPER_OBJECT_IDS = new Set([
448
449
  "workspace-helper-sandbox",
449
450
  "nav-folders",
451
+ "workspace-ui-cache",
450
452
  ]);
451
453
 
452
454
  /**
@@ -888,7 +890,7 @@ const OBJECT_TYPE_PRESETS = {
888
890
  "lastRunId",
889
891
  "lastSourceId",
890
892
  "lastResponse",
891
- "orchestrationGraph",
893
+ "orchestrationConfig",
892
894
  "description",
893
895
  "resolverTemplateId",
894
896
  "connectorKind",
@@ -941,6 +941,83 @@ function deriveWorkspaceWorkflowActionMetadataItems(workflowNodeItems) {
941
941
  return { items, warnings: [] };
942
942
  }
943
943
 
944
+ /**
945
+ * Workspace PROVENANCE metadata.
946
+ *
947
+ * Surfaces the `provenance` block of a seeded workspace template (e.g.
948
+ * the Project Management seed) as safe booleans + strings so the customer
949
+ * activation layer can derive setup state without re-parsing the config.
950
+ * The block intentionally only echoes non-secret descriptors:
951
+ *
952
+ * - template slug (e.g. "project-management")
953
+ * - template kind (e.g. "workspace-template")
954
+ * - privacy descriptor (e.g. "sanitized-no-secrets-no-provider-data")
955
+ * - booleans: has provider api-registry row, has any configured
956
+ * connectionId, has any persisted source records, has at least one
957
+ * seeded workflow row, has at least one seeded dashboard.
958
+ *
959
+ * Returns a single-item list so the metadata store can stay shape-stable
960
+ * even when no provenance block exists.
961
+ */
962
+ function deriveWorkspaceProvenanceMetadataItems(workspaceConfig, workspaceSourceRecords) {
963
+ const safeConfig = isPlainObject(workspaceConfig) ? workspaceConfig : {};
964
+ const provenance = isPlainObject(safeConfig.provenance) ? safeConfig.provenance : null;
965
+ const objects = Array.isArray(safeConfig.dataModel?.objects) ? safeConfig.dataModel.objects : [];
966
+ let apiRegistryRows = 0;
967
+ let nangoRows = 0;
968
+ let connectionsConfigured = 0;
969
+ let sandboxRows = 0;
970
+ for (const object of objects) {
971
+ if (!isPlainObject(object)) continue;
972
+ const rows = Array.isArray(object.rows) ? object.rows : [];
973
+ if (object.objectType === "api-registry") {
974
+ apiRegistryRows += rows.length;
975
+ for (const row of rows) {
976
+ if (!isPlainObject(row)) continue;
977
+ if (safeString(row.connectorKind).trim().toLowerCase() === "nango") nangoRows += 1;
978
+ const raw = row.connectionIds ?? row.connectionId;
979
+ if (Array.isArray(raw)) {
980
+ if (raw.some((entry) => safeString(entry).trim())) connectionsConfigured += 1;
981
+ } else if (safeString(raw).trim()) {
982
+ connectionsConfigured += 1;
983
+ }
984
+ }
985
+ }
986
+ if (object.objectType === "sandbox-environment") {
987
+ sandboxRows += rows.length;
988
+ }
989
+ }
990
+ let sourceRecordKeys = 0;
991
+ if (isPlainObject(workspaceSourceRecords)) {
992
+ for (const value of Object.values(workspaceSourceRecords)) {
993
+ if (!isPlainObject(value)) continue;
994
+ const count = Number.isFinite(value.recordCount)
995
+ ? Number(value.recordCount)
996
+ : Array.isArray(value.records) ? value.records.length : 0;
997
+ if (count > 0) sourceRecordKeys += 1;
998
+ }
999
+ }
1000
+ return {
1001
+ items: [{
1002
+ kind: "workspaceProvenance",
1003
+ id: safeString(provenance?.template).trim() || "blank",
1004
+ metadataId: stableId("provenance", safeString(provenance?.template).trim() || "blank"),
1005
+ template: safeString(provenance?.template).trim() || "blank",
1006
+ templateKind: safeString(provenance?.templateKind).trim(),
1007
+ privacy: safeString(provenance?.privacy).trim(),
1008
+ mirrors: safeString(provenance?.mirrors).trim(),
1009
+ hasProvenance: Boolean(provenance),
1010
+ apiRegistryRows,
1011
+ nangoRows,
1012
+ connectionsConfigured,
1013
+ sandboxRows,
1014
+ hydratedSourceRecordKeys: sourceRecordKeys,
1015
+ hasSeededDashboard: Array.isArray(safeConfig.dashboards) && safeConfig.dashboards.length > 0
1016
+ }],
1017
+ warnings: []
1018
+ };
1019
+ }
1020
+
944
1021
  /**
945
1022
  * Worker kit metadata.
946
1023
  *
@@ -1073,6 +1150,9 @@ function buildWorkspaceMetadataStore({
1073
1150
  const workerKits = deriveWorkspaceWorkerKitMetadataItems(safeConfig);
1074
1151
  warnings.push(...workerKits.warnings);
1075
1152
 
1153
+ const provenance = deriveWorkspaceProvenanceMetadataItems(safeConfig, safeSourceRecords);
1154
+ warnings.push(...provenance.warnings);
1155
+
1076
1156
  const pipelineHealth = deriveWorkspacePipelineHealthMetadataItems(sandboxes.items, runs.items);
1077
1157
  warnings.push(...pipelineHealth.warnings);
1078
1158
 
@@ -1098,6 +1178,7 @@ function buildWorkspaceMetadataStore({
1098
1178
  runs: runs.items,
1099
1179
  outputArtifacts: runs.outputArtifacts,
1100
1180
  workerKits: workerKits.items,
1181
+ provenance: provenance.items,
1101
1182
  pipelineHealth: pipelineHealth.items,
1102
1183
  warnings
1103
1184
  };
@@ -1181,6 +1262,7 @@ export {
1181
1262
  deriveWorkspaceRunRecordMetadataItems,
1182
1263
  deriveWorkspaceRunMetadataItems,
1183
1264
  deriveWorkspaceWorkerKitMetadataItems,
1265
+ deriveWorkspaceProvenanceMetadataItems,
1184
1266
  deriveWorkspacePipelineHealthMetadataItems,
1185
1267
  isSecretKey
1186
1268
  };