@growthub/cli 0.13.9 → 0.14.0
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/workspace/env-status/route.js +31 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +130 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +5 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +396 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +75 -55
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +100 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +176 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +63 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +7 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Resolver Proposal V1 — the governed resolver-file lane for the
|
|
3
|
+
* helper, aligned to AWaC topology + the Causation-ITT loop.
|
|
4
|
+
*
|
|
5
|
+
* AWaC boundary: a resolver is a SERVER FILE under
|
|
6
|
+
* lib/adapters/integrations/resolvers/, NOT workspace config. It therefore must
|
|
7
|
+
* NOT travel through the PATCH allowlist (dashboards|widgetTypes|canvas|
|
|
8
|
+
* dataModel). It is its own proposal lane with affectedField "server-file",
|
|
9
|
+
* gated by the same filesystem/read-only persistence rule, and it emits a
|
|
10
|
+
* receipt — so "state → eligibility → guidance → action → evidence" closes.
|
|
11
|
+
*
|
|
12
|
+
* This module is PURE (build/validate/generate). The actual confined fs write
|
|
13
|
+
* lives in lib/server-resolver-write.js (server-only). Secret-safe: the
|
|
14
|
+
* generated resolver reads its secret from the server env at run time via the
|
|
15
|
+
* authRef candidate keys — never an inlined value.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const RESOLVER_PROPOSAL_TYPE = "resolver.create";
|
|
19
|
+
const RESOLVER_AFFECTED_FIELD = "server-file";
|
|
20
|
+
const RESOLVER_DIR = "lib/adapters/integrations/resolvers";
|
|
21
|
+
|
|
22
|
+
function clean(value) {
|
|
23
|
+
return String(value == null ? "" : value).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slugify(value, fallback = "resolver") {
|
|
27
|
+
const slug = clean(value)
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/\.js$/, "")
|
|
30
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
31
|
+
.replace(/-+/g, "-")
|
|
32
|
+
.replace(/^-|-$/g, "")
|
|
33
|
+
.slice(0, 64);
|
|
34
|
+
return slug || fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function envCandidates(ref) {
|
|
38
|
+
const token = clean(ref).replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
39
|
+
if (!token) return [];
|
|
40
|
+
return Array.from(new Set([token, `${token}_API_KEY`, `${token}_TOKEN`]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Confine a resolver filename to the resolver dir. Mirrors the register-resolver
|
|
45
|
+
* route's traversal guard so the studio previews exactly what the write accepts.
|
|
46
|
+
*/
|
|
47
|
+
function resolveResolverFilePath(name) {
|
|
48
|
+
const base = slugify(name, "resolver");
|
|
49
|
+
if (base.includes("/") || base.includes("..")) {
|
|
50
|
+
return { ok: false, path: null, filename: null, dir: RESOLVER_DIR, error: "resolver filename must not contain path segments" };
|
|
51
|
+
}
|
|
52
|
+
const filename = `${base}.js`;
|
|
53
|
+
return { ok: true, path: `${RESOLVER_DIR}/${filename}`, filename, dir: RESOLVER_DIR, error: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeJsString(value) {
|
|
57
|
+
return JSON.stringify(clean(value));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate a concrete, runnable resolver module for a specific API. baseUrl /
|
|
62
|
+
* endpoint / rootPath / idField are baked in from the registry row + the
|
|
63
|
+
* response profile, so the file works for THIS API. The secret is read from the
|
|
64
|
+
* server env via the authRef candidate keys at run time — never inlined.
|
|
65
|
+
*/
|
|
66
|
+
function generateResolverCode({ integrationId, baseUrl, endpoint, method, authRef, headerName, prefix, rootPath, idField, entityType }) {
|
|
67
|
+
const id = slugify(integrationId, "integration");
|
|
68
|
+
const url = `${clean(baseUrl).replace(/\/+$/, "")}/${clean(endpoint).replace(/^\/+/, "")}`.replace(/\/$/, "") || clean(baseUrl) || clean(endpoint);
|
|
69
|
+
const m = (clean(method).toUpperCase() || "GET");
|
|
70
|
+
const candidates = envCandidates(authRef);
|
|
71
|
+
const header = clean(headerName) || "authorization";
|
|
72
|
+
const pfx = clean(prefix);
|
|
73
|
+
const root = clean(rootPath);
|
|
74
|
+
const idf = clean(idField) || "id";
|
|
75
|
+
const ent = clean(entityType) || "records";
|
|
76
|
+
|
|
77
|
+
return `// Resolver for "${id}" — generated by the governed helper resolver studio.
|
|
78
|
+
// Server file. Reads its secret from the server env at run time (candidates:
|
|
79
|
+
// ${candidates.join(", ") || "none"}); never hard-code a secret here.
|
|
80
|
+
import { registerSourceResolver } from "../source-resolver-registry.js";
|
|
81
|
+
|
|
82
|
+
const ENV_CANDIDATES = ${JSON.stringify(candidates)};
|
|
83
|
+
function readSecret() {
|
|
84
|
+
for (const key of ENV_CANDIDATES) {
|
|
85
|
+
if (process.env[key]) return process.env[key];
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
function valueAtPath(obj, path) {
|
|
90
|
+
if (!path) return obj;
|
|
91
|
+
return String(path).split(".").filter(Boolean).reduce((acc, part) => (acc == null ? acc : acc[part]), obj);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
registerSourceResolver({
|
|
95
|
+
integrationId: ${safeJsString(id)},
|
|
96
|
+
label: ${safeJsString(`${id} (${ent})`)},
|
|
97
|
+
// fetchRecords runs server-side. Secret stays in the server env.
|
|
98
|
+
fetchRecords: async () => {
|
|
99
|
+
const secret = readSecret();
|
|
100
|
+
const res = await fetch(${safeJsString(url)}, {
|
|
101
|
+
method: ${safeJsString(m)},
|
|
102
|
+
headers: {
|
|
103
|
+
accept: "application/json",
|
|
104
|
+
...(secret ? { ${safeJsString(header)}: ${pfx ? `\`${pfx} \${secret}\`` : "secret"} } : {}),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const raw = res.headers.get("content-type")?.includes("application/json") ? await res.json() : await res.text();
|
|
108
|
+
const extracted = valueAtPath(raw, ${safeJsString(root)});
|
|
109
|
+
const items = Array.isArray(extracted) ? extracted : (Array.isArray(raw) ? raw : []);
|
|
110
|
+
return items.map((item, index) => ({
|
|
111
|
+
id: item && item[${safeJsString(idf)}] != null ? String(item[${safeJsString(idf)}]) : String(index),
|
|
112
|
+
...item,
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build an inert resolver.create proposal. Carries the target path, the
|
|
121
|
+
* generated code, security notes, and the resolver's input/output contract.
|
|
122
|
+
* Nothing is written until the apply lane writes the file (gated).
|
|
123
|
+
*/
|
|
124
|
+
function buildResolverProposal(input = {}) {
|
|
125
|
+
const integrationId = slugify(input.integrationId, "integration");
|
|
126
|
+
const target = resolveResolverFilePath(integrationId);
|
|
127
|
+
const code = generateResolverCode({
|
|
128
|
+
integrationId,
|
|
129
|
+
baseUrl: input.baseUrl,
|
|
130
|
+
endpoint: input.endpoint,
|
|
131
|
+
method: input.method,
|
|
132
|
+
authRef: input.authRef,
|
|
133
|
+
headerName: input.headerName,
|
|
134
|
+
prefix: input.prefix,
|
|
135
|
+
rootPath: input.rootPath,
|
|
136
|
+
idField: input.idField,
|
|
137
|
+
entityType: input.entityType,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
type: RESOLVER_PROPOSAL_TYPE,
|
|
141
|
+
affectedField: RESOLVER_AFFECTED_FIELD,
|
|
142
|
+
rationale: `Generate a server resolver that shapes ${integrationId}'s response${clean(input.rootPath) ? ` at "${clean(input.rootPath)}"` : ""} into governed rows.`,
|
|
143
|
+
confidence: 0.85,
|
|
144
|
+
target,
|
|
145
|
+
language: "javascript",
|
|
146
|
+
code,
|
|
147
|
+
security: [
|
|
148
|
+
"Server file — only written in filesystem mode (read-only runtimes get a 409 + guidance).",
|
|
149
|
+
"Reads its secret from the server env at run time; never inlines a value.",
|
|
150
|
+
`Path confined to ${RESOLVER_DIR}.`,
|
|
151
|
+
],
|
|
152
|
+
contract: {
|
|
153
|
+
inputs: ["registry baseUrl/endpoint", `authRef env (${envCandidates(input.authRef).join(", ") || "none"})`],
|
|
154
|
+
output: `Record[] with a stable id (from "${clean(input.idField) || "id"}").`,
|
|
155
|
+
},
|
|
156
|
+
payload: {
|
|
157
|
+
integrationId,
|
|
158
|
+
rootPath: clean(input.rootPath),
|
|
159
|
+
entityType: clean(input.entityType) || "records",
|
|
160
|
+
filename: target.filename,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validate a resolver.create proposal before the apply lane writes it.
|
|
167
|
+
* Returns { ok, error }.
|
|
168
|
+
*/
|
|
169
|
+
function validateResolverProposal(proposal) {
|
|
170
|
+
if (!proposal || proposal.type !== RESOLVER_PROPOSAL_TYPE) {
|
|
171
|
+
return { ok: false, error: "not a resolver.create proposal" };
|
|
172
|
+
}
|
|
173
|
+
if (proposal.affectedField !== RESOLVER_AFFECTED_FIELD) {
|
|
174
|
+
return { ok: false, error: `resolver.create affectedField must be "${RESOLVER_AFFECTED_FIELD}"` };
|
|
175
|
+
}
|
|
176
|
+
const target = proposal.target || resolveResolverFilePath(proposal.payload?.integrationId);
|
|
177
|
+
if (!target || !target.ok) {
|
|
178
|
+
return { ok: false, error: target?.error || "invalid resolver target path" };
|
|
179
|
+
}
|
|
180
|
+
const code = clean(proposal.code);
|
|
181
|
+
if (!code) return { ok: false, error: "resolver code is empty" };
|
|
182
|
+
if (!code.includes("registerSourceResolver")) {
|
|
183
|
+
return { ok: false, error: "resolver code must call registerSourceResolver()" };
|
|
184
|
+
}
|
|
185
|
+
if (!target.filename.endsWith(".js")) {
|
|
186
|
+
return { ok: false, error: "resolver file must have a .js extension" };
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, error: null, target };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
RESOLVER_PROPOSAL_TYPE,
|
|
193
|
+
RESOLVER_AFFECTED_FIELD,
|
|
194
|
+
RESOLVER_DIR,
|
|
195
|
+
resolveResolverFilePath,
|
|
196
|
+
generateResolverCode,
|
|
197
|
+
buildResolverProposal,
|
|
198
|
+
validateResolverProposal,
|
|
199
|
+
envCandidates,
|
|
200
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growthub/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "CLI control plane for Growthub Local and Agent Workspace as Code: export, fork, inspect, operate, sync, and optionally activate governed AI workspaces.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|