@growthub/cli 0.9.13 → 0.9.16
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 +17 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/README.md +27 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/integration-entities/route.js +41 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/list-entities/route.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-source/route.js +124 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/refresh-sources/route.js +127 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/register-resolver/route.js +119 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolvers/route.js +41 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-adapters/route.js +21 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +634 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-api-record/route.js +126 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/test-source/route.js +130 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/page.jsx +1349 -222
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +1048 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workspace-builder.jsx +1540 -433
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/data-sources-api-registry.md +141 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/docs/sandbox-environment-primitive.md +32 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +57 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolvers/README.md +133 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolvers/google-analytics.js +160 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/source-resolver-registry.js +85 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapter-loader.js +58 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/adapters/README.md +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +284 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-process.js +194 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/index.js +33 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js +113 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-config.js +79 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +211 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +126 -7
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/kit.json +16 -0
- package/dist/index.js +1764 -40677
- package/package.json +2 -2
|
@@ -338,6 +338,82 @@ async function writeWorkspaceApiWebhookSettings(patch) {
|
|
|
338
338
|
return next.integrations.filter((item) => item?.sourceType === "custom-api-webhooks");
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Source Records persistence — sidecar store for live-backed dataModel objects.
|
|
343
|
+
*
|
|
344
|
+
* Records are written by POST /api/workspace/refresh-sources when a resolver
|
|
345
|
+
* fetches live data for a source with `binding.sourceStorage: "workspace-source-records"`.
|
|
346
|
+
*
|
|
347
|
+
* Persistence is keyed by `sourceId` and stored in a JSON sidecar file
|
|
348
|
+
* (`growthub.source-records.json`) beside `growthub.config.json`. The same
|
|
349
|
+
* filesystem / read-only / database mode rules apply: in read-only mode writes
|
|
350
|
+
* are rejected gracefully so the refresh button surface is disabled.
|
|
351
|
+
*
|
|
352
|
+
* Shape: { [sourceId]: { records: Record[], integrationId: string, fetchedAt: string } }
|
|
353
|
+
*/
|
|
354
|
+
|
|
355
|
+
const SOURCE_RECORDS_FILENAME = "growthub.source-records.json";
|
|
356
|
+
|
|
357
|
+
function resolveSourceRecordsPath() {
|
|
358
|
+
return path.resolve(/*turbopackIgnore: true*/ process.cwd(), SOURCE_RECORDS_FILENAME);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function readWorkspaceSourceRecords(sourceId) {
|
|
362
|
+
const recordsPath = resolveSourceRecordsPath();
|
|
363
|
+
try {
|
|
364
|
+
const raw = await fs.readFile(recordsPath, "utf8");
|
|
365
|
+
const all = JSON.parse(raw);
|
|
366
|
+
if (sourceId) {
|
|
367
|
+
return all[sourceId] || null;
|
|
368
|
+
}
|
|
369
|
+
return all;
|
|
370
|
+
} catch {
|
|
371
|
+
return sourceId ? null : {};
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
async function writeWorkspaceSourceRecords(sourceId, records, metadata = {}) {
|
|
376
|
+
const persistence = describePersistenceMode();
|
|
377
|
+
if (persistence.mode !== PERSISTENCE_ADAPTERS.FILESYSTEM || !persistence.canSave) {
|
|
378
|
+
const error = new Error(persistence.reason);
|
|
379
|
+
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
380
|
+
error.guidance = persistence.guidance || READ_ONLY_GUIDANCE;
|
|
381
|
+
throw error;
|
|
382
|
+
}
|
|
383
|
+
if (typeof sourceId !== "string" || !sourceId.trim()) {
|
|
384
|
+
const error = new Error("sourceId must be a non-empty string");
|
|
385
|
+
error.code = "INVALID_SOURCE_RECORDS_WRITE";
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
if (!Array.isArray(records)) {
|
|
389
|
+
const error = new Error("records must be an array");
|
|
390
|
+
error.code = "INVALID_SOURCE_RECORDS_WRITE";
|
|
391
|
+
throw error;
|
|
392
|
+
}
|
|
393
|
+
const recordsPath = resolveSourceRecordsPath();
|
|
394
|
+
const expectedDir = path.resolve(/*turbopackIgnore: true*/ process.cwd());
|
|
395
|
+
if (path.dirname(recordsPath) !== expectedDir) {
|
|
396
|
+
const error = new Error(`refused to write outside workspace cwd: ${recordsPath}`);
|
|
397
|
+
error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
|
|
398
|
+
throw error;
|
|
399
|
+
}
|
|
400
|
+
let all = {};
|
|
401
|
+
try {
|
|
402
|
+
const raw = await fs.readFile(recordsPath, "utf8");
|
|
403
|
+
all = JSON.parse(raw);
|
|
404
|
+
} catch {
|
|
405
|
+
all = {};
|
|
406
|
+
}
|
|
407
|
+
all[sourceId.trim()] = {
|
|
408
|
+
records,
|
|
409
|
+
integrationId: metadata.integrationId || null,
|
|
410
|
+
fetchedAt: metadata.fetchedAt || new Date().toISOString(),
|
|
411
|
+
recordCount: records.length
|
|
412
|
+
};
|
|
413
|
+
await fs.writeFile(recordsPath, `${JSON.stringify(all, null, 2)}\n`, "utf8");
|
|
414
|
+
return all[sourceId.trim()];
|
|
415
|
+
}
|
|
416
|
+
|
|
341
417
|
export {
|
|
342
418
|
GRID_COLUMNS,
|
|
343
419
|
GRID_ROWS,
|
|
@@ -346,9 +422,11 @@ export {
|
|
|
346
422
|
READ_ONLY_GUIDANCE,
|
|
347
423
|
describePersistenceMode,
|
|
348
424
|
readWorkspaceConfig,
|
|
425
|
+
readWorkspaceSourceRecords,
|
|
349
426
|
resolveWorkspaceConfigPath,
|
|
350
427
|
validateWorkspaceConfig,
|
|
351
428
|
writeWorkspaceConfig,
|
|
352
429
|
writeWorkspaceApiWebhookSettings,
|
|
353
|
-
writeWorkspaceIdentitySettings
|
|
430
|
+
writeWorkspaceIdentitySettings,
|
|
431
|
+
writeWorkspaceSourceRecords
|
|
354
432
|
};
|
|
@@ -171,9 +171,12 @@ function deriveManualObjectTable(object) {
|
|
|
171
171
|
id: `manual-object:${object.id || source}`,
|
|
172
172
|
label: object.label || object.name || source,
|
|
173
173
|
source,
|
|
174
|
+
objectType: object.objectType || "custom",
|
|
175
|
+
icon: object.icon || null,
|
|
174
176
|
columns,
|
|
175
177
|
rows,
|
|
176
178
|
binding: object.binding || { mode: "manual", source: "Data Model" },
|
|
179
|
+
relations: Array.isArray(object.relations) ? object.relations : [],
|
|
177
180
|
mutable: true,
|
|
178
181
|
storage: "manual-object",
|
|
179
182
|
objectId: object.id,
|
|
@@ -313,6 +316,141 @@ function uniqueObjectId(workspaceConfig, name) {
|
|
|
313
316
|
return `${base}-${index}`;
|
|
314
317
|
}
|
|
315
318
|
|
|
319
|
+
/**
|
|
320
|
+
* Top-level object type presets.
|
|
321
|
+
* Each entry defines: label, icon (Lucide name), description, default columns, and
|
|
322
|
+
* any built-in relations. These are the five first-class types the UI offers when
|
|
323
|
+
* a user clicks "New object" — they act like schema templates, not hard constraints.
|
|
324
|
+
*
|
|
325
|
+
* Relation shape:
|
|
326
|
+
* {
|
|
327
|
+
* id: string, // stable slug within this object
|
|
328
|
+
* name: string, // display label
|
|
329
|
+
* field: string, // FK column on THIS object
|
|
330
|
+
* targetObjectType:string, // objectType of the referenced object
|
|
331
|
+
* type: "belongs-to" | "has-many",
|
|
332
|
+
* description: string
|
|
333
|
+
* }
|
|
334
|
+
*/
|
|
335
|
+
const OBJECT_TYPE_PRESETS = {
|
|
336
|
+
"data-source": {
|
|
337
|
+
label: "Data Source",
|
|
338
|
+
icon: "Globe",
|
|
339
|
+
description: "Custom API, webhook, or external feed. References an API Registry record while credentials stay in workspace settings.",
|
|
340
|
+
columns: ["Name", "registryId", "endpoint", "authRef", "baseUrl", "status", "lastTested", "lastResponse"],
|
|
341
|
+
relations: [
|
|
342
|
+
{
|
|
343
|
+
id: "resolver-binding",
|
|
344
|
+
name: "Resolver",
|
|
345
|
+
field: "registryId",
|
|
346
|
+
targetObjectType: "api-registry",
|
|
347
|
+
type: "belongs-to",
|
|
348
|
+
description: "The API Registry entry whose fetchRecords function resolves this source. Set registryId to match the resolver integrationId."
|
|
349
|
+
}
|
|
350
|
+
]
|
|
351
|
+
},
|
|
352
|
+
"api-registry": {
|
|
353
|
+
label: "API Registry",
|
|
354
|
+
icon: "Code2",
|
|
355
|
+
description: "HTTP API records with endpoint config, auth references, connection status, and stored test output.",
|
|
356
|
+
columns: ["integrationId", "authRef", "baseUrl", "endpoint", "method", "status", "lastTested", "lastResponse", "entityTypes", "description"],
|
|
357
|
+
relations: []
|
|
358
|
+
},
|
|
359
|
+
"people": {
|
|
360
|
+
label: "People",
|
|
361
|
+
icon: "Users",
|
|
362
|
+
description: "Contacts, leads, or team members with standard CRM fields.",
|
|
363
|
+
columns: ["Name", "Email", "Phone", "Company", "Status", "Role"],
|
|
364
|
+
relations: []
|
|
365
|
+
},
|
|
366
|
+
"tasks": {
|
|
367
|
+
label: "Tasks",
|
|
368
|
+
icon: "CheckSquare",
|
|
369
|
+
description: "Action items, to-dos, or work items.",
|
|
370
|
+
columns: ["Name", "Status", "DueDate", "Assignee", "Priority"],
|
|
371
|
+
relations: []
|
|
372
|
+
},
|
|
373
|
+
"sandbox-environment": {
|
|
374
|
+
label: "Sandbox Environment",
|
|
375
|
+
icon: "Terminal",
|
|
376
|
+
description: "Execution locality: local (process sandbox or Paperclip thin local agent-host CLI) or serverless (delegates to an API Registry HTTP target: Edge/QStash/cron webhook). Env refs resolve server-side; run history in growthub.source-records.json. Not a widget binding source.",
|
|
377
|
+
columns: [
|
|
378
|
+
"Name",
|
|
379
|
+
"lifecycleStatus",
|
|
380
|
+
"version",
|
|
381
|
+
"runLocality",
|
|
382
|
+
"schedulerRegistryId",
|
|
383
|
+
"runtime",
|
|
384
|
+
"adapter",
|
|
385
|
+
"agentHost",
|
|
386
|
+
"envRefs",
|
|
387
|
+
"networkAllow",
|
|
388
|
+
"allowList",
|
|
389
|
+
"instructions",
|
|
390
|
+
"command",
|
|
391
|
+
"timeoutMs",
|
|
392
|
+
"status",
|
|
393
|
+
"lastTested",
|
|
394
|
+
"lastRunId",
|
|
395
|
+
"lastSourceId",
|
|
396
|
+
"lastResponse"
|
|
397
|
+
],
|
|
398
|
+
relations: [
|
|
399
|
+
{
|
|
400
|
+
id: "scheduler-registry-binding",
|
|
401
|
+
name: "Scheduler (serverless)",
|
|
402
|
+
field: "schedulerRegistryId",
|
|
403
|
+
targetObjectType: "api-registry",
|
|
404
|
+
type: "belongs-to",
|
|
405
|
+
description: "When runLocality is serverless, POST /api/workspace/sandbox-run sends growthub-sandbox-run-v1 to this API Registry record (METHOD, baseUrl, endpoint, authRef resolved server-side). Use for Supabase Edge URL, QStash forwarder, Vercel-exposed webhook, cron targets, etc."
|
|
406
|
+
}
|
|
407
|
+
]
|
|
408
|
+
},
|
|
409
|
+
"custom": {
|
|
410
|
+
label: "Custom",
|
|
411
|
+
icon: "Plus",
|
|
412
|
+
description: "Start with a blank table — define your own fields and records.",
|
|
413
|
+
columns: ["Name"],
|
|
414
|
+
relations: []
|
|
415
|
+
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Create a typed business object from a preset template.
|
|
420
|
+
* Accepts objectType (one of the OBJECT_TYPE_PRESETS keys) and an optional icon override.
|
|
421
|
+
* The object is stored in dataModel.objects[] alongside manual objects.
|
|
422
|
+
*/
|
|
423
|
+
function createTypedBusinessObject(workspaceConfig, { name, objectType = "custom", icon } = {}) {
|
|
424
|
+
const label = String(name || "").trim();
|
|
425
|
+
if (!label) return workspaceConfig;
|
|
426
|
+
const preset = OBJECT_TYPE_PRESETS[objectType] || OBJECT_TYPE_PRESETS.custom;
|
|
427
|
+
const columns = [...preset.columns];
|
|
428
|
+
const dataModel =
|
|
429
|
+
workspaceConfig.dataModel && typeof workspaceConfig.dataModel === "object" && !Array.isArray(workspaceConfig.dataModel)
|
|
430
|
+
? workspaceConfig.dataModel
|
|
431
|
+
: {};
|
|
432
|
+
const id = uniqueObjectId(workspaceConfig, label);
|
|
433
|
+
const object = {
|
|
434
|
+
id,
|
|
435
|
+
label,
|
|
436
|
+
source: label,
|
|
437
|
+
objectType,
|
|
438
|
+
icon: icon || preset.icon,
|
|
439
|
+
columns,
|
|
440
|
+
rows: [],
|
|
441
|
+
binding: { mode: "manual", source: "Data Model" },
|
|
442
|
+
relations: preset.relations ? preset.relations.map((r) => ({ ...r })) : [],
|
|
443
|
+
fieldSettings: { hidden: [], order: columns }
|
|
444
|
+
};
|
|
445
|
+
return {
|
|
446
|
+
...workspaceConfig,
|
|
447
|
+
dataModel: {
|
|
448
|
+
...dataModel,
|
|
449
|
+
objects: [...normalizeManualObjects(workspaceConfig), object]
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
316
454
|
function createManualBusinessObject(workspaceConfig, { name, fields } = {}) {
|
|
317
455
|
const label = String(name || "").trim();
|
|
318
456
|
const columns = Array.from(new Set((Array.isArray(fields) ? fields : String(fields || "").split(","))
|
|
@@ -408,6 +546,73 @@ function describeBindingLane(binding) {
|
|
|
408
546
|
return "manual";
|
|
409
547
|
}
|
|
410
548
|
|
|
549
|
+
/**
|
|
550
|
+
* Saved env-key references — name-only projection of workspace integrations[].
|
|
551
|
+
*
|
|
552
|
+
* Used by the sandbox-environment drawer's env-ref multi-select. The browser
|
|
553
|
+
* receives the `endpointRef` slug only (never the secret value); the sandbox
|
|
554
|
+
* run route resolves the slug to a server-side env value using the same
|
|
555
|
+
* `envKeyCandidates(authRef)` pattern as `test-api-record/route.js`.
|
|
556
|
+
*
|
|
557
|
+
* Returns: [{ id, endpointRef, kind, hasSecret }]
|
|
558
|
+
*/
|
|
559
|
+
function listSavedEnvRefs(workspaceConfig) {
|
|
560
|
+
const integrations = Array.isArray(workspaceConfig?.integrations) ? workspaceConfig.integrations : [];
|
|
561
|
+
return integrations
|
|
562
|
+
.filter((entry) => entry?.sourceType === "custom-api-webhooks" && typeof entry.endpointRef === "string" && entry.endpointRef.trim())
|
|
563
|
+
.map((entry) => ({
|
|
564
|
+
id: entry.id || entry.endpointRef,
|
|
565
|
+
endpointRef: entry.endpointRef,
|
|
566
|
+
kind: entry.kind === "webhook" ? "webhook" : "api",
|
|
567
|
+
hasSecret: entry.hasSecret === true
|
|
568
|
+
}));
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Parse a sandbox-environment row's `envRefs` column into a clean string array.
|
|
573
|
+
* Stored as a comma-separated string in the row to keep the column flat under
|
|
574
|
+
* the existing governed Data Model schema; rendered as a multi-select chip
|
|
575
|
+
* group in the drawer. The server reads the same comma-separated form.
|
|
576
|
+
*/
|
|
577
|
+
function parseSandboxEnvRefs(value) {
|
|
578
|
+
if (Array.isArray(value)) {
|
|
579
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
580
|
+
}
|
|
581
|
+
if (typeof value !== "string") return [];
|
|
582
|
+
return value
|
|
583
|
+
.split(",")
|
|
584
|
+
.map((item) => item.trim())
|
|
585
|
+
.filter(Boolean);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Parse a sandbox-environment row's `allowList` column into a clean array of
|
|
590
|
+
* domain hostnames. Stored as comma-separated string for governed flatness;
|
|
591
|
+
* the run route enforces the list when `networkAllow` is truthy.
|
|
592
|
+
*/
|
|
593
|
+
function parseSandboxAllowList(value) {
|
|
594
|
+
if (Array.isArray(value)) {
|
|
595
|
+
return value.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean);
|
|
596
|
+
}
|
|
597
|
+
if (typeof value !== "string") return [];
|
|
598
|
+
return value
|
|
599
|
+
.split(",")
|
|
600
|
+
.map((item) => item.trim().toLowerCase())
|
|
601
|
+
.filter(Boolean);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Stable sourceId for a sandbox-environment row's run history sidecar.
|
|
606
|
+
* Keyed by object id + slugified Name so the key survives reorder of rows
|
|
607
|
+
* inside the same object. The sandbox-run route uses this id to read/write
|
|
608
|
+
* `growthub.source-records.json`.
|
|
609
|
+
*/
|
|
610
|
+
function sandboxRunSourceId(objectId, name) {
|
|
611
|
+
const slug = String(name || "").trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
612
|
+
if (!objectId || !slug) return null;
|
|
613
|
+
return `sandbox:${objectId}:${slug}`;
|
|
614
|
+
}
|
|
615
|
+
|
|
411
616
|
function describeBindingMode(binding) {
|
|
412
617
|
const lane = describeBindingLane(binding);
|
|
413
618
|
if (lane === "data-source") return { label: "Data source scope", description: "Integration reference selected in the existing widget source flow. Dynamic data resolves through the governed server-side integration path." };
|
|
@@ -417,17 +622,23 @@ function describeBindingMode(binding) {
|
|
|
417
622
|
}
|
|
418
623
|
|
|
419
624
|
export {
|
|
625
|
+
OBJECT_TYPE_PRESETS,
|
|
420
626
|
addTableField,
|
|
421
627
|
addTableRow,
|
|
422
628
|
appendRowsToTable,
|
|
423
629
|
createManualBusinessObject,
|
|
630
|
+
createTypedBusinessObject,
|
|
424
631
|
deleteTableRow,
|
|
425
632
|
describeBindingLane,
|
|
426
633
|
describeBindingMode,
|
|
427
634
|
duplicateTableRow,
|
|
428
635
|
exportTableAsCsv,
|
|
429
636
|
importTableFromCsv,
|
|
637
|
+
listSavedEnvRefs,
|
|
430
638
|
listWorkspaceDataModelTables,
|
|
639
|
+
parseSandboxAllowList,
|
|
640
|
+
parseSandboxEnvRefs,
|
|
431
641
|
replaceTableContent,
|
|
642
|
+
sandboxRunSourceId,
|
|
432
643
|
updateTableCell
|
|
433
644
|
};
|
|
@@ -47,6 +47,35 @@ const KNOWN_FILTER_OPERATORS = ["eq", "ne", "contains", "gt", "lt", "isEmpty", "
|
|
|
47
47
|
const KNOWN_FILTER_CONJUNCTIONS = ["and", "or"];
|
|
48
48
|
const KNOWN_SORT_DIRECTIONS = ["asc", "desc"];
|
|
49
49
|
const KNOWN_AGGREGATIONS = ["sum", "avg", "count", "min", "max"];
|
|
50
|
+
const KNOWN_SANDBOX_RUNTIMES = ["python", "node", "bash"];
|
|
51
|
+
/** Where execution is delegated: locally (process / agent-host CLI) or to a scheduler webhook (Supabase Edge, QStash, Vercel cron hitting your URL, etc.). */
|
|
52
|
+
const KNOWN_SANDBOX_RUN_LOCALITY = ["local", "serverless"];
|
|
53
|
+
const KNOWN_SANDBOX_LIFECYCLE_STATUSES = ["draft", "live"];
|
|
54
|
+
const DEFAULT_SANDBOX_RUN_LOCALITY = "local";
|
|
55
|
+
const DEFAULT_SANDBOX_ADAPTER = "local-process";
|
|
56
|
+
const SANDBOX_DEFAULT_TIMEOUT_MS = 60000;
|
|
57
|
+
const SANDBOX_MAX_TIMEOUT_MS = 600000;
|
|
58
|
+
/**
|
|
59
|
+
* Canonical Paperclip local agent-host slugs — mirrors the upstream
|
|
60
|
+
* `AGENT_ADAPTER_TYPES` enum in `packages/shared/src/constants.ts`. The
|
|
61
|
+
* sandbox-environment row's `agentHost` column accepts any of these values
|
|
62
|
+
* when `adapter === "local-agent-host"`. The standalone workspace starter
|
|
63
|
+
* does NOT import the @paperclipai/adapter-* packages directly — instead the
|
|
64
|
+
* default `local-agent-host` adapter spawns the host CLI binary the user has
|
|
65
|
+
* on PATH (cross-platform: macOS / Windows / Linux), keeping the workspace
|
|
66
|
+
* starter portable and thin.
|
|
67
|
+
*/
|
|
68
|
+
const KNOWN_SANDBOX_AGENT_HOSTS = [
|
|
69
|
+
"claude_local",
|
|
70
|
+
"codex_local",
|
|
71
|
+
"cursor",
|
|
72
|
+
"gemini_local",
|
|
73
|
+
"opencode_local",
|
|
74
|
+
"pi_local",
|
|
75
|
+
"qwen_local",
|
|
76
|
+
"openclaw_gateway",
|
|
77
|
+
"hermes_local"
|
|
78
|
+
];
|
|
50
79
|
|
|
51
80
|
const NORMALIZED_OBJECT_FIELD_IDS = ["id", "label", "secondaryLabel", "entityType", "provider", "lane", "status"];
|
|
52
81
|
const WORKSPACE_TEMPLATE_KIND = "growthub-workspace-template";
|
|
@@ -152,7 +181,9 @@ const WIDGET_SCHEMA_CONTRACTS = {
|
|
|
152
181
|
lane: "string optional (when mode === 'integration')",
|
|
153
182
|
entityId: "string optional — stable source object ID (never a token or credential)",
|
|
154
183
|
entityType: "string optional — adapter-provided object type",
|
|
155
|
-
entityLabel: "string optional — display-only resolved label, not authoritative"
|
|
184
|
+
entityLabel: "string optional — display-only resolved label, not authoritative",
|
|
185
|
+
sourceStorage: "'workspace-source-records' optional — marks this binding as live-backed; records are written by POST /api/workspace/refresh-sources and keyed by dataModel.objects[].sourceId",
|
|
186
|
+
sourceId: "string optional — stable key in growthub.source-records.json; required when sourceStorage === 'workspace-source-records'"
|
|
156
187
|
},
|
|
157
188
|
NormalizedIntegrationEntity: {
|
|
158
189
|
id: "non-empty string — stable source object ID",
|
|
@@ -200,19 +231,19 @@ const SAMPLE_DATA_BINDINGS = {
|
|
|
200
231
|
function defaultConfigFor(kind) {
|
|
201
232
|
switch (kind) {
|
|
202
233
|
case "chart":
|
|
203
|
-
return { values: [
|
|
234
|
+
return { values: [], binding: { mode: "manual", source: "", rows: [] } };
|
|
204
235
|
case "view":
|
|
205
236
|
return {
|
|
206
237
|
source: "",
|
|
207
238
|
layout: "Table",
|
|
208
239
|
columns: [],
|
|
209
240
|
rows: [],
|
|
210
|
-
binding: { mode: "manual", source: "
|
|
241
|
+
binding: { mode: "manual", source: "", rows: [] }
|
|
211
242
|
};
|
|
212
243
|
case "iframe":
|
|
213
244
|
return { url: "" };
|
|
214
245
|
case "rich-text":
|
|
215
|
-
return { text: "", binding: { mode: "manual", source: "
|
|
246
|
+
return { text: "", binding: { mode: "manual", source: "", rows: [] } };
|
|
216
247
|
default:
|
|
217
248
|
return {};
|
|
218
249
|
}
|
|
@@ -396,8 +427,11 @@ function validateStaticDataBinding(binding, path, errors) {
|
|
|
396
427
|
if (typeof binding.integrationId !== "string" || !binding.integrationId.trim()) {
|
|
397
428
|
errors.push(`${path}.integrationId is required when mode is integration`);
|
|
398
429
|
}
|
|
399
|
-
|
|
400
|
-
|
|
430
|
+
// lane is not required when sourceStorage delegates routing to a registry resolver
|
|
431
|
+
if (binding.sourceStorage !== "workspace-source-records") {
|
|
432
|
+
if (typeof binding.lane !== "string" || !binding.lane.trim()) {
|
|
433
|
+
errors.push(`${path}.lane is required when mode is integration`);
|
|
434
|
+
}
|
|
401
435
|
}
|
|
402
436
|
}
|
|
403
437
|
if (binding.source !== undefined && typeof binding.source !== "string") {
|
|
@@ -439,6 +473,14 @@ function validateStaticDataBinding(binding, path, errors) {
|
|
|
439
473
|
if (binding.entityLabel !== undefined && typeof binding.entityLabel !== "string") {
|
|
440
474
|
errors.push(`${path}.entityLabel must be a string`);
|
|
441
475
|
}
|
|
476
|
+
if (binding.sourceStorage !== undefined) {
|
|
477
|
+
if (binding.sourceStorage !== "workspace-source-records") {
|
|
478
|
+
errors.push(`${path}.sourceStorage must be "workspace-source-records" when present`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (binding.sourceId !== undefined && typeof binding.sourceId !== "string") {
|
|
482
|
+
errors.push(`${path}.sourceId must be a string`);
|
|
483
|
+
}
|
|
442
484
|
}
|
|
443
485
|
|
|
444
486
|
function validateFieldSettings(fieldSettings, path, errors) {
|
|
@@ -778,6 +820,65 @@ function validateCanvasConfig(canvas, errors) {
|
|
|
778
820
|
}
|
|
779
821
|
}
|
|
780
822
|
|
|
823
|
+
function validateSandboxEnvironmentRow(row, path, errors) {
|
|
824
|
+
if (!isPlainObject(row)) return;
|
|
825
|
+
const lifecycleStatus = String(row.lifecycleStatus || "").trim().toLowerCase();
|
|
826
|
+
if (row.lifecycleStatus !== undefined && row.lifecycleStatus !== "" && !KNOWN_SANDBOX_LIFECYCLE_STATUSES.includes(lifecycleStatus)) {
|
|
827
|
+
errors.push(`${path}.lifecycleStatus must be one of ${KNOWN_SANDBOX_LIFECYCLE_STATUSES.join(", ")}`);
|
|
828
|
+
}
|
|
829
|
+
if (row.version !== undefined && typeof row.version !== "string" && typeof row.version !== "number") {
|
|
830
|
+
errors.push(`${path}.version must be a string or number`);
|
|
831
|
+
}
|
|
832
|
+
const runLocalityNorm = String(row.runLocality || "").trim().toLowerCase();
|
|
833
|
+
if (row.runLocality !== undefined && row.runLocality !== "" && !KNOWN_SANDBOX_RUN_LOCALITY.includes(runLocalityNorm)) {
|
|
834
|
+
errors.push(`${path}.runLocality must be one of ${KNOWN_SANDBOX_RUN_LOCALITY.join(", ")}`);
|
|
835
|
+
}
|
|
836
|
+
if (runLocalityNorm === "serverless") {
|
|
837
|
+
if (typeof row.schedulerRegistryId !== "string" || !row.schedulerRegistryId.trim()) {
|
|
838
|
+
errors.push(`${path}.schedulerRegistryId must reference an API Registry integrationId when runLocality is serverless`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (runLocalityNorm === "local" && row.runtime !== undefined && row.runtime !== "" && !KNOWN_SANDBOX_RUNTIMES.includes(row.runtime)) {
|
|
842
|
+
errors.push(`${path}.runtime must be one of ${KNOWN_SANDBOX_RUNTIMES.join(", ")}`);
|
|
843
|
+
}
|
|
844
|
+
if (row.adapter !== undefined && typeof row.adapter !== "string") {
|
|
845
|
+
errors.push(`${path}.adapter must be a string`);
|
|
846
|
+
}
|
|
847
|
+
if (row.agentHost !== undefined && row.agentHost !== "" && !KNOWN_SANDBOX_AGENT_HOSTS.includes(row.agentHost)) {
|
|
848
|
+
errors.push(`${path}.agentHost must be one of ${KNOWN_SANDBOX_AGENT_HOSTS.join(", ")}`);
|
|
849
|
+
}
|
|
850
|
+
if (row.envRefs !== undefined && typeof row.envRefs !== "string" && !Array.isArray(row.envRefs)) {
|
|
851
|
+
errors.push(`${path}.envRefs must be a comma-separated string or array of env-ref slugs (never values)`);
|
|
852
|
+
}
|
|
853
|
+
if (row.networkAllow !== undefined) {
|
|
854
|
+
const value = String(row.networkAllow).trim().toLowerCase();
|
|
855
|
+
if (!["", "true", "false", "0", "1", "on", "off"].includes(value)) {
|
|
856
|
+
errors.push(`${path}.networkAllow must coerce to a boolean (true/false/on/off)`);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
if (row.allowList !== undefined && typeof row.allowList !== "string" && !Array.isArray(row.allowList)) {
|
|
860
|
+
errors.push(`${path}.allowList must be a comma-separated string or array of hostnames`);
|
|
861
|
+
}
|
|
862
|
+
if (row.instructions !== undefined && typeof row.instructions !== "string") {
|
|
863
|
+
errors.push(`${path}.instructions must be a string`);
|
|
864
|
+
}
|
|
865
|
+
if (row.command !== undefined && typeof row.command !== "string") {
|
|
866
|
+
errors.push(`${path}.command must be a string`);
|
|
867
|
+
}
|
|
868
|
+
if (row.lastRunId !== undefined && typeof row.lastRunId !== "string") {
|
|
869
|
+
errors.push(`${path}.lastRunId must be a string`);
|
|
870
|
+
}
|
|
871
|
+
if (row.lastSourceId !== undefined && typeof row.lastSourceId !== "string") {
|
|
872
|
+
errors.push(`${path}.lastSourceId must be a string`);
|
|
873
|
+
}
|
|
874
|
+
if (row.timeoutMs !== undefined && row.timeoutMs !== "") {
|
|
875
|
+
const ms = Number(row.timeoutMs);
|
|
876
|
+
if (!Number.isFinite(ms) || ms < 0 || ms > SANDBOX_MAX_TIMEOUT_MS) {
|
|
877
|
+
errors.push(`${path}.timeoutMs must be a finite number between 0 and ${SANDBOX_MAX_TIMEOUT_MS}`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
|
|
781
882
|
function validateDataModelConfig(dataModel, errors) {
|
|
782
883
|
if (dataModel === undefined) return;
|
|
783
884
|
if (!isPlainObject(dataModel)) {
|
|
@@ -805,15 +906,25 @@ function validateDataModelConfig(dataModel, errors) {
|
|
|
805
906
|
}
|
|
806
907
|
if (typeof object.label !== "string" || !object.label.trim()) errors.push(`${prefix}.label must be a non-empty string`);
|
|
807
908
|
if (object.source !== undefined && typeof object.source !== "string") errors.push(`${prefix}.source must be a string`);
|
|
909
|
+
if (object.sourceId !== undefined && typeof object.sourceId !== "string") errors.push(`${prefix}.sourceId must be a string`);
|
|
808
910
|
validateStringArray(object.columns, `${prefix}.columns`, errors);
|
|
809
911
|
if (!Array.isArray(object.rows)) {
|
|
810
912
|
errors.push(`${prefix}.rows must be an array`);
|
|
811
913
|
} else {
|
|
812
914
|
object.rows.forEach((row, rowIndex) => {
|
|
813
|
-
if (!isPlainObject(row))
|
|
915
|
+
if (!isPlainObject(row)) {
|
|
916
|
+
errors.push(`${prefix}.rows[${rowIndex}] must be a plain object`);
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (object.objectType === "sandbox-environment") {
|
|
920
|
+
validateSandboxEnvironmentRow(row, `${prefix}.rows[${rowIndex}]`, errors);
|
|
921
|
+
}
|
|
814
922
|
});
|
|
815
923
|
}
|
|
816
924
|
validateStaticDataBinding(object.binding, `${prefix}.binding`, errors);
|
|
925
|
+
if (object.binding?.sourceStorage === "workspace-source-records" && typeof object.sourceId !== "string") {
|
|
926
|
+
errors.push(`${prefix}.sourceId is required when binding.sourceStorage is "workspace-source-records"`);
|
|
927
|
+
}
|
|
817
928
|
validateFieldSettings(object.fieldSettings, `${prefix}.fieldSettings`, errors);
|
|
818
929
|
});
|
|
819
930
|
}
|
|
@@ -1062,11 +1173,19 @@ export {
|
|
|
1062
1173
|
KNOWN_AGGREGATIONS,
|
|
1063
1174
|
KNOWN_CHART_TYPES,
|
|
1064
1175
|
KNOWN_DATA_BINDING_MODES,
|
|
1176
|
+
DEFAULT_SANDBOX_RUN_LOCALITY,
|
|
1177
|
+
KNOWN_SANDBOX_LIFECYCLE_STATUSES,
|
|
1065
1178
|
KNOWN_FIELDS,
|
|
1066
1179
|
KNOWN_FILTER_CONJUNCTIONS,
|
|
1067
1180
|
KNOWN_FILTER_OPERATORS,
|
|
1181
|
+
KNOWN_SANDBOX_AGENT_HOSTS,
|
|
1182
|
+
KNOWN_SANDBOX_RUN_LOCALITY,
|
|
1183
|
+
KNOWN_SANDBOX_RUNTIMES,
|
|
1068
1184
|
KNOWN_SORT_DIRECTIONS,
|
|
1069
1185
|
KNOWN_WIDGET_KINDS,
|
|
1186
|
+
DEFAULT_SANDBOX_ADAPTER,
|
|
1187
|
+
SANDBOX_DEFAULT_TIMEOUT_MS,
|
|
1188
|
+
SANDBOX_MAX_TIMEOUT_MS,
|
|
1070
1189
|
NORMALIZED_OBJECT_FIELD_IDS,
|
|
1071
1190
|
SAMPLE_DATA_BINDINGS,
|
|
1072
1191
|
SAMPLE_VIEW_ROWS,
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"studio/src/main.jsx",
|
|
62
62
|
"studio/src/App.jsx",
|
|
63
63
|
"studio/src/app.css",
|
|
64
|
+
"apps/workspace/docs/sandbox-environment-primitive.md",
|
|
64
65
|
"apps/workspace/README.md",
|
|
65
66
|
"apps/workspace/.env.example",
|
|
66
67
|
"apps/workspace/package.json",
|
|
@@ -76,6 +77,12 @@
|
|
|
76
77
|
"apps/workspace/app/workspace-builder.jsx",
|
|
77
78
|
"apps/workspace/app/settings/integrations/page.jsx",
|
|
78
79
|
"apps/workspace/app/api/workspace/route.js",
|
|
80
|
+
"apps/workspace/app/api/workspace/refresh-sources/route.js",
|
|
81
|
+
"apps/workspace/app/api/workspace/test-source/route.js",
|
|
82
|
+
"apps/workspace/app/api/workspace/register-resolver/route.js",
|
|
83
|
+
"apps/workspace/app/api/workspace/resolvers/route.js",
|
|
84
|
+
"apps/workspace/app/api/workspace/sandbox-adapters/route.js",
|
|
85
|
+
"apps/workspace/app/api/workspace/sandbox-run/route.js",
|
|
79
86
|
"apps/workspace/app/api/settings/integrations/route.js",
|
|
80
87
|
"apps/workspace/lib/workspace-schema.js",
|
|
81
88
|
"apps/workspace/lib/workspace-config.js",
|
|
@@ -85,6 +92,15 @@
|
|
|
85
92
|
"apps/workspace/lib/adapters/auth/index.js",
|
|
86
93
|
"apps/workspace/lib/adapters/integrations/index.js",
|
|
87
94
|
"apps/workspace/lib/adapters/integrations/growthub-connection-normalizer.js",
|
|
95
|
+
"apps/workspace/lib/adapters/integrations/source-resolver-registry.js",
|
|
96
|
+
"apps/workspace/lib/adapters/integrations/resolver-loader.js",
|
|
97
|
+
"apps/workspace/lib/adapters/integrations/resolvers/README.md",
|
|
98
|
+
"apps/workspace/lib/adapters/sandboxes/adapter-loader.js",
|
|
99
|
+
"apps/workspace/lib/adapters/sandboxes/adapters/README.md",
|
|
100
|
+
"apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js",
|
|
101
|
+
"apps/workspace/lib/adapters/sandboxes/default-local-process.js",
|
|
102
|
+
"apps/workspace/lib/adapters/sandboxes/index.js",
|
|
103
|
+
"apps/workspace/lib/adapters/sandboxes/sandbox-adapter-registry.js",
|
|
88
104
|
"apps/workspace/lib/adapters/payments/index.js",
|
|
89
105
|
"apps/workspace/lib/adapters/persistence/index.js",
|
|
90
106
|
"apps/workspace/lib/adapters/persistence/postgres.js",
|