@growthub/cli 0.14.3 → 0.14.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/resolvers/[integrationId]/route.js +157 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +5 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +33 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/resolvers/route.js +86 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +30 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryReviewModal.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/CeoCockpit.jsx +532 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +400 -188
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +36 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphEmptyCanvas.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationRunTracePanel.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxOrchestrationEditorPanel.jsx +1 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +9 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +14 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +24 -19
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-agent-teams.js +211 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-bootstrap-console.js +325 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/ceo-cockpit-console.js +206 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +7 -82
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/resolver-constructor.js +217 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-registry.js +99 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/unified-resolver-registry.js +545 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-patch-policy.js +2 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +30 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-schema.js +69 -0
- package/package.json +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryActionCard.jsx +0 -141
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolConfirmModal.jsx +0 -64
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SandboxToolDraftPanel.jsx +0 -376
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolver Constructor V1 (CMS SDK v1.5.1) — closes the no-code "too many open
|
|
3
|
+
* fields" gap. Instead of asking a non-technical user to hand-author a
|
|
4
|
+
* resolver's rootPath / idField / entityType / auth header, this constructs the
|
|
5
|
+
* governed resolver from facts ALREADY computed: the tested response profile
|
|
6
|
+
* (`profileApiResponse`) and the resolver recommendation (`recommendResolver`),
|
|
7
|
+
* plus the row's own auth config (mirrored from how test-api-record sent it, so
|
|
8
|
+
* the resolver behaves exactly like the test that just passed).
|
|
9
|
+
*
|
|
10
|
+
* Agnostic across the normalized governance taxonomy (http | custom | tool | mcp
|
|
11
|
+
* | chrome | nango), via a single builder dispatch (`getResolverBuilder`), the
|
|
12
|
+
* Nango precedent generalized:
|
|
13
|
+
* - HTTP-shaped kinds (http / custom / and any non-reserved value, including a
|
|
14
|
+
* literal "webhook" an operator may type) materialize a server file built
|
|
15
|
+
* from the response shape;
|
|
16
|
+
* - nango is config-driven (no file) with honest readiness;
|
|
17
|
+
* - reserved kinds (mcp | chrome | tool) cannot be auto-constructed from an
|
|
18
|
+
* HTTP response — advertised truthfully with a concrete next action, never
|
|
19
|
+
* left blank or mislabeled.
|
|
20
|
+
*
|
|
21
|
+
* `connectorKind` is operator-editable text and is honored verbatim — it is
|
|
22
|
+
* never silently normalized.
|
|
23
|
+
*
|
|
24
|
+
* Pure: no fetch, no secrets, never throws. The returned `proposal` (file mode)
|
|
25
|
+
* flows ONLY through the governed apply lane (helper/apply → writeResolverProposalFile)
|
|
26
|
+
* and the no-code cockpit — never a hand edit.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { buildResolverProposal } from "./workspace-resolver-proposal.js";
|
|
30
|
+
import { slugifyIntegrationId, RESOLVER_ENDPOINT_BASE } from "./unified-resolver-registry.js";
|
|
31
|
+
|
|
32
|
+
function clean(value) {
|
|
33
|
+
return String(value == null ? "" : value).trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The canonical governed endpoint a row will be exposed at once registered. */
|
|
37
|
+
function endpointFor(integrationId) {
|
|
38
|
+
const slug = slugifyIntegrationId(integrationId, "");
|
|
39
|
+
return slug ? `${RESOLVER_ENDPOINT_BASE}/${slug}` : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A plain-language "what the system detected" summary + a confidence band, so the
|
|
44
|
+
* review panel can SHOW understanding ("I found 42 records under data.items…")
|
|
45
|
+
* instead of a developer form. Pure; derived from the tested response profile.
|
|
46
|
+
* high — top-level/clean records: direct apply is safe.
|
|
47
|
+
* medium — records nested under a container: review the mapping, then apply.
|
|
48
|
+
* low — pagination or no record array: needs human review before trust.
|
|
49
|
+
*/
|
|
50
|
+
function detectShape(profile, recommendation) {
|
|
51
|
+
if (!profile || !profile.parsed) return null;
|
|
52
|
+
const level = clean(recommendation?.level);
|
|
53
|
+
let confidence = "high";
|
|
54
|
+
if (profile.hasPagination || !profile.usable) confidence = "low";
|
|
55
|
+
else if (level === "recommended") confidence = "medium";
|
|
56
|
+
else if (level === "required") confidence = "low";
|
|
57
|
+
const recordPath = clean(profile.arrayPath);
|
|
58
|
+
const entityType = clean(profile.suggestedEntityType) || "records";
|
|
59
|
+
const idField = clean(profile.candidates?.id) || "id";
|
|
60
|
+
return {
|
|
61
|
+
confidence,
|
|
62
|
+
recordCount: Number.isFinite(profile.recordCount) ? profile.recordCount : 0,
|
|
63
|
+
recordPath,
|
|
64
|
+
idField,
|
|
65
|
+
entityType,
|
|
66
|
+
hasPagination: Boolean(profile.hasPagination),
|
|
67
|
+
// One human sentence the panel can show verbatim.
|
|
68
|
+
sentence: profile.usable
|
|
69
|
+
? `Found ${profile.recordCount} ${entityType}${recordPath ? ` under "${recordPath}"` : " (top-level)"}, keyed by "${idField}"${profile.hasPagination ? " — paginated, so a resolver is required to fetch every page" : ""}.`
|
|
70
|
+
: "No record array detected — review the response before activating.",
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* The auth header/prefix the resolver must send — mirrored from how
|
|
76
|
+
* test-api-record built its request (authHeaderName || authHeader || x-api-key;
|
|
77
|
+
* authPrefix), so a constructed resolver matches the test that just succeeded.
|
|
78
|
+
*/
|
|
79
|
+
function deriveAuthHeader(row) {
|
|
80
|
+
const headerName = clean(row?.authHeaderName) || clean(row?.authHeader) || "x-api-key";
|
|
81
|
+
const prefix = clean(row?.authPrefix);
|
|
82
|
+
return { headerName, prefix };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function constructCustomHttpProposal({ row, profile, recommendation, recordRef }) {
|
|
86
|
+
const { headerName, prefix } = deriveAuthHeader(row);
|
|
87
|
+
const rootPath = clean(profile?.arrayPath) || clean(recommendation?.rootPath);
|
|
88
|
+
const idField = clean(profile?.candidates?.id) || "id";
|
|
89
|
+
const entityType = clean(profile?.suggestedEntityType) || clean(row?.entityTypes) || "records";
|
|
90
|
+
|
|
91
|
+
const blanks = [];
|
|
92
|
+
if (!clean(row?.integrationId)) blanks.push("integrationId");
|
|
93
|
+
if (!clean(row?.baseUrl) && !clean(row?.endpoint)) blanks.push("target (baseUrl or endpoint)");
|
|
94
|
+
|
|
95
|
+
const proposal = buildResolverProposal({
|
|
96
|
+
integrationId: row?.integrationId,
|
|
97
|
+
baseUrl: row?.baseUrl,
|
|
98
|
+
endpoint: row?.endpoint,
|
|
99
|
+
method: row?.method,
|
|
100
|
+
authRef: row?.authRef,
|
|
101
|
+
headerName,
|
|
102
|
+
prefix,
|
|
103
|
+
rootPath,
|
|
104
|
+
idField,
|
|
105
|
+
entityType,
|
|
106
|
+
recordRef,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const detected = detectShape(profile, recommendation);
|
|
110
|
+
// Respect the row's declared governance kind (http / custom / webhook / …);
|
|
111
|
+
// default to http when unset. Only the resolver IMPLEMENTATION is HTTP here.
|
|
112
|
+
const declaredKind = clean(row?.connectorKind).toLowerCase() || "http";
|
|
113
|
+
return {
|
|
114
|
+
ok: blanks.length === 0,
|
|
115
|
+
mode: "file",
|
|
116
|
+
connectorKind: declaredKind,
|
|
117
|
+
endpoint: endpointFor(row?.integrationId),
|
|
118
|
+
proposal,
|
|
119
|
+
prefill: { rootPath, idField, entityType, headerName, prefix },
|
|
120
|
+
detected,
|
|
121
|
+
confidence: detected ? detected.confidence : "low",
|
|
122
|
+
authRef: clean(row?.authRef),
|
|
123
|
+
blanks,
|
|
124
|
+
reason: blanks.length
|
|
125
|
+
? `Fill ${blanks.join(", ")} on the row before constructing a resolver.`
|
|
126
|
+
: (recommendation?.reason || "Resolver constructed from the tested response shape."),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Honest readiness for a config-driven (Nango) row. A resolver registers from
|
|
132
|
+
* the row automatically — but only if the row actually carries the minimum
|
|
133
|
+
* binding. We never report "nothing to apply / done" when the endpoint would not
|
|
134
|
+
* be usable; missing config returns an actionable next step instead.
|
|
135
|
+
*/
|
|
136
|
+
function constructNangoReadiness({ row }) {
|
|
137
|
+
const blanks = [];
|
|
138
|
+
const providerKey = clean(row?.providerConfigKey) || clean(row?.integrationId);
|
|
139
|
+
if (!providerKey) blanks.push("providerConfigKey (or integrationId)");
|
|
140
|
+
const hasConnection =
|
|
141
|
+
clean(row?.connectionIds) || clean(row?.connectionId) || clean(row?.nangoConnectionId);
|
|
142
|
+
if (!hasConnection) blanks.push("connectionIds");
|
|
143
|
+
if (!clean(row?.endpoint)) blanks.push("endpoint (Nango proxy path)");
|
|
144
|
+
const ready = blanks.length === 0;
|
|
145
|
+
return {
|
|
146
|
+
ok: ready,
|
|
147
|
+
mode: "config-driven",
|
|
148
|
+
connectorKind: "nango",
|
|
149
|
+
endpoint: endpointFor(row?.integrationId),
|
|
150
|
+
proposal: null,
|
|
151
|
+
prefill: null,
|
|
152
|
+
detected: null,
|
|
153
|
+
blanks,
|
|
154
|
+
state: ready ? "config-driven-ready" : "config-driven-missing-config",
|
|
155
|
+
reason: ready
|
|
156
|
+
? "Config-driven via Nango — the resolver registers from this row automatically once it loads; no file to write. Confirm it appears as registered in the registry."
|
|
157
|
+
: `Config-driven via Nango, but the row is missing ${blanks.join(", ")}. Add these so the resolver can register and the endpoint becomes usable.`,
|
|
158
|
+
nextAction: ready ? null : { id: "edit", label: `Add ${blanks.join(", ")} to the row` },
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve a builder for a connector kind. Each builder emits the same
|
|
164
|
+
* `{ ok, mode, connectorKind, proposal, prefill, blanks, reason }` contract.
|
|
165
|
+
* - custom-http (default) → file mode (materialized resolver)
|
|
166
|
+
* - nango → config-driven (no file; built from the row)
|
|
167
|
+
* - mcp/webhook/chrome → not yet supported (truthful, not blank)
|
|
168
|
+
*/
|
|
169
|
+
function getResolverBuilder(connectorKind) {
|
|
170
|
+
const kind = clean(connectorKind).toLowerCase();
|
|
171
|
+
if (kind === "nango") {
|
|
172
|
+
return (args) => constructNangoReadiness(args);
|
|
173
|
+
}
|
|
174
|
+
// Reserved for auto-construction — these need their own resolver implementation
|
|
175
|
+
// and cannot be derived from an HTTP response shape (taxonomy: mcp|chrome|tool).
|
|
176
|
+
if (["mcp", "chrome", "tool"].includes(kind)) {
|
|
177
|
+
return (args) => ({
|
|
178
|
+
ok: false,
|
|
179
|
+
mode: "unsupported",
|
|
180
|
+
reserved: true,
|
|
181
|
+
connectorKind: kind,
|
|
182
|
+
endpoint: endpointFor(args?.row?.integrationId),
|
|
183
|
+
proposal: null,
|
|
184
|
+
prefill: null,
|
|
185
|
+
detected: null,
|
|
186
|
+
blanks: [],
|
|
187
|
+
// Reserved should build confidence, not feel like a dead end: say what is
|
|
188
|
+
// reserved, what works now, and the concrete next move.
|
|
189
|
+
reason: `Auto-construction for "${kind}" connectors is reserved for a future release. What works today: set this row's connector to custom-http and construct a resolver, or ask the governed helper to propose a plan. Your record stays governed either way.`,
|
|
190
|
+
nextAction: { id: "use-custom-http", label: "Switch to a custom-http resolver" },
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return (args) => constructCustomHttpProposal(args);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Construct a governed resolver for one API Registry row from its tested shape.
|
|
198
|
+
*
|
|
199
|
+
* @param {object} input
|
|
200
|
+
* @param {object} input.row the api-registry row (drawer draft)
|
|
201
|
+
* @param {object} [input.profile] profileApiResponse(row.lastResponse)
|
|
202
|
+
* @param {object} [input.recommendation] recommendResolver(profile)
|
|
203
|
+
* @param {object} [input.recordRef] { objectId, rowName } of the governed record
|
|
204
|
+
* @returns {{ ok, mode, connectorKind, proposal, prefill, blanks, reason }}
|
|
205
|
+
*/
|
|
206
|
+
function constructResolverProposal(input = {}) {
|
|
207
|
+
const row = input.row && typeof input.row === "object" ? input.row : {};
|
|
208
|
+
const builder = getResolverBuilder(row.connectorKind);
|
|
209
|
+
return builder({
|
|
210
|
+
row,
|
|
211
|
+
profile: input.profile || null,
|
|
212
|
+
recommendation: input.recommendation || null,
|
|
213
|
+
recordRef: input.recordRef || null,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export { constructResolverProposal, getResolverBuilder };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Resolver Registry IO V1 — the confined, server-only bridge between the
|
|
3
|
+
* pure unified-resolver-registry deriver and the filesystem.
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities (server-only; the browser never imports this):
|
|
6
|
+
* - read the provenance header off each resolver file (to tag
|
|
7
|
+
* helper-generated vs static-file provenance)
|
|
8
|
+
* - persist the externalized, agent-readable index artifact and the endpoint
|
|
9
|
+
* manifest (gated by persistence mode — read-only runtimes skip silently
|
|
10
|
+
* and the live derivation is still returned over the API)
|
|
11
|
+
*
|
|
12
|
+
* The artifacts are PROJECTIONS of the governed records — do-not-edit, and kept
|
|
13
|
+
* in sync by this write-through. Never logs file contents. Contract:
|
|
14
|
+
* `@growthub/api-contract/resolver-registry`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { promises as fs } from "node:fs";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { describePersistenceMode } from "@/lib/workspace-config";
|
|
20
|
+
import {
|
|
21
|
+
RESOLVER_REGISTRY_DIR,
|
|
22
|
+
RESOLVER_REGISTRY_INDEX_FILE,
|
|
23
|
+
RESOLVER_ENDPOINT_MANIFEST_FILE,
|
|
24
|
+
parseResolverFileHeader,
|
|
25
|
+
slugifyIntegrationId,
|
|
26
|
+
buildEndpointManifest,
|
|
27
|
+
} from "@/lib/unified-resolver-registry";
|
|
28
|
+
|
|
29
|
+
const HEADER_BYTES = 600;
|
|
30
|
+
|
|
31
|
+
function resolversDirAbs() {
|
|
32
|
+
return path.resolve(/*turbopackIgnore: true*/ process.cwd(), RESOLVER_REGISTRY_DIR);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read the provenance header of every resolver file.
|
|
37
|
+
* Returns { [slug]: { generated, integrationId, record } }. Never throws.
|
|
38
|
+
*/
|
|
39
|
+
async function readResolverFileMeta() {
|
|
40
|
+
const dir = resolversDirAbs();
|
|
41
|
+
const meta = {};
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = await fs.readdir(dir);
|
|
45
|
+
} catch {
|
|
46
|
+
return meta;
|
|
47
|
+
}
|
|
48
|
+
const jsFiles = entries.filter((f) => f.endsWith(".js") && !f.startsWith("_") && !f.startsWith("."));
|
|
49
|
+
await Promise.all(
|
|
50
|
+
jsFiles.map(async (file) => {
|
|
51
|
+
const slug = slugifyIntegrationId(file, "");
|
|
52
|
+
if (!slug) return;
|
|
53
|
+
try {
|
|
54
|
+
const handle = await fs.open(path.join(dir, file), "r");
|
|
55
|
+
try {
|
|
56
|
+
const buf = Buffer.alloc(HEADER_BYTES);
|
|
57
|
+
const { bytesRead } = await handle.read(buf, 0, HEADER_BYTES, 0);
|
|
58
|
+
meta[slug] = parseResolverFileHeader(buf.toString("utf8", 0, bytesRead));
|
|
59
|
+
} finally {
|
|
60
|
+
await handle.close();
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Unreadable file — leave it out of meta; the deriver still sees the
|
|
64
|
+
// filename via `files` and classifies it as static-file.
|
|
65
|
+
}
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
return meta;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Persist the index + endpoint manifest artifacts (gated). Returns
|
|
73
|
+
* { written: boolean, reason?: string }. Read-only runtimes are not an error —
|
|
74
|
+
* the live API derivation remains the source of truth at request time.
|
|
75
|
+
*/
|
|
76
|
+
async function persistResolverRegistryArtifacts(index) {
|
|
77
|
+
const persistence = describePersistenceMode();
|
|
78
|
+
if (!persistence.canSave) {
|
|
79
|
+
return { written: false, reason: persistence.reason || "read-only runtime" };
|
|
80
|
+
}
|
|
81
|
+
const dir = resolversDirAbs();
|
|
82
|
+
try {
|
|
83
|
+
await fs.mkdir(dir, { recursive: true });
|
|
84
|
+
const indexPath = path.resolve(/*turbopackIgnore: true*/ process.cwd(), RESOLVER_REGISTRY_INDEX_FILE);
|
|
85
|
+
const manifestPath = path.resolve(/*turbopackIgnore: true*/ process.cwd(), RESOLVER_ENDPOINT_MANIFEST_FILE);
|
|
86
|
+
// Confinement — both artifacts live directly in the resolvers dir.
|
|
87
|
+
if (path.dirname(indexPath) !== dir || path.dirname(manifestPath) !== dir) {
|
|
88
|
+
return { written: false, reason: "artifact path escaped the resolvers dir" };
|
|
89
|
+
}
|
|
90
|
+
const manifest = buildEndpointManifest(index, index.generatedAt);
|
|
91
|
+
await fs.writeFile(indexPath, `${JSON.stringify(index, null, 2)}\n`, "utf8");
|
|
92
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
93
|
+
return { written: true };
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return { written: false, reason: err?.message || "artifact write failed" };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export { readResolverFileMeta, persistResolverRegistryArtifacts };
|