@cargo-ai/cdk 1.0.11 → 1.0.12
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/build/src/cli/commands/deploy.d.ts.map +1 -1
- package/build/src/cli/commands/deploy.js +105 -1
- package/build/src/deploy/executors.live.d.ts.map +1 -1
- package/build/src/deploy/executors.live.js +56 -2
- package/build/src/deploy/import.d.ts +12 -0
- package/build/src/deploy/import.d.ts.map +1 -1
- package/build/src/deploy/import.js +79 -9
- package/build/src/deploy/index.d.ts +2 -0
- package/build/src/deploy/index.d.ts.map +1 -1
- package/build/src/deploy/index.js +1 -0
- package/build/src/deploy/pull/enumerate.d.ts +8 -0
- package/build/src/deploy/pull/enumerate.d.ts.map +1 -0
- package/build/src/deploy/pull/enumerate.js +258 -0
- package/build/src/deploy/pull/index.d.ts +28 -0
- package/build/src/deploy/pull/index.d.ts.map +1 -0
- package/build/src/deploy/pull/index.js +152 -0
- package/build/src/deploy/pull/ir.d.ts +45 -0
- package/build/src/deploy/pull/ir.d.ts.map +1 -0
- package/build/src/deploy/pull/ir.js +72 -0
- package/build/src/deploy/pull/mappers.d.ts +10 -0
- package/build/src/deploy/pull/mappers.d.ts.map +1 -0
- package/build/src/deploy/pull/mappers.js +689 -0
- package/build/src/deploy/pull/print.d.ts +9 -0
- package/build/src/deploy/pull/print.d.ts.map +1 -0
- package/build/src/deploy/pull/print.js +59 -0
- package/build/src/deploy/pull/resolve.d.ts +24 -0
- package/build/src/deploy/pull/resolve.d.ts.map +1 -0
- package/build/src/deploy/pull/resolve.js +113 -0
- package/build/src/deploy/pull/untar.d.ts +6 -0
- package/build/src/deploy/pull/untar.d.ts.map +1 -0
- package/build/src/deploy/pull/untar.js +84 -0
- package/build/src/deploy/readers.live.d.ts +3 -0
- package/build/src/deploy/readers.live.d.ts.map +1 -1
- package/build/src/deploy/readers.live.js +12 -6
- package/build/src/index.d.ts +3 -3
- package/build/src/index.d.ts.map +1 -1
- package/build/src/index.js +1 -1
- package/build/src/refs.d.ts +7 -1
- package/build/src/refs.d.ts.map +1 -1
- package/build/src/refs.js +10 -0
- package/build/src/resources/customIntegration.d.ts +3 -2
- package/build/src/resources/customIntegration.d.ts.map +1 -1
- package/build/src/resources/model.d.ts +56 -8
- package/build/src/resources/model.d.ts.map +1 -1
- package/build/src/resources/model.js +48 -1
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// Workspace enumeration (the only IO in the pull pipeline besides file writes).
|
|
2
|
+
// Calls the list/all endpoint for each requested kind and returns raw rows for
|
|
3
|
+
// the pure mappers. Kept thin and kind-keyed so mappers stay fixture-testable.
|
|
4
|
+
//
|
|
5
|
+
// Kinds are fetched concurrently, and per-row secondary fetches (workflow
|
|
6
|
+
// releases, file bytes, source archives) fan out per kind — only the output
|
|
7
|
+
// ORDER is fixed, since dedupeSlugs disambiguates colliding slugs in input
|
|
8
|
+
// order and generated file paths depend on it.
|
|
9
|
+
import { isNotFound } from "../readers.live.js";
|
|
10
|
+
import { UNIFY_EXTRACTOR_SLUGS } from "./mappers.js";
|
|
11
|
+
import { untarGz } from "./untar.js";
|
|
12
|
+
// The fixed output order (independent of the requested `kinds` order).
|
|
13
|
+
const ENUMERATION_ORDER = [
|
|
14
|
+
"folder",
|
|
15
|
+
"connector",
|
|
16
|
+
"customIntegration",
|
|
17
|
+
"model",
|
|
18
|
+
"relationship",
|
|
19
|
+
"segment",
|
|
20
|
+
"tool",
|
|
21
|
+
"play",
|
|
22
|
+
"mcpServer",
|
|
23
|
+
"agent",
|
|
24
|
+
"capacity",
|
|
25
|
+
"territory",
|
|
26
|
+
"file",
|
|
27
|
+
"worker",
|
|
28
|
+
"app",
|
|
29
|
+
"context",
|
|
30
|
+
];
|
|
31
|
+
// Deterministic per-row conditions that skip the row rather than abort the
|
|
32
|
+
// pull: the target is gone, or its content is beyond the API's read limits
|
|
33
|
+
// (readBytes/runtime.read reject >10MiB with a `tooLarge` reason). Anything
|
|
34
|
+
// else — auth, network, server errors — propagates: a blip must not be
|
|
35
|
+
// misread as "nothing there" and silently shrink the pulled workspace.
|
|
36
|
+
function isSkippableRead(error) {
|
|
37
|
+
if (isNotFound(error))
|
|
38
|
+
return true;
|
|
39
|
+
const reason = error.reason;
|
|
40
|
+
if (typeof reason === "string" && /toolarge/i.test(reason))
|
|
41
|
+
return true;
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
return /too large/i.test(message);
|
|
44
|
+
}
|
|
45
|
+
export async function enumerateWorkspace(api, kinds) {
|
|
46
|
+
// Shared by the model and relationship collectors — fetched once.
|
|
47
|
+
const cache = {};
|
|
48
|
+
function allModels() {
|
|
49
|
+
if (cache.models === undefined) {
|
|
50
|
+
cache.models = api.storage.model.all().then(({ models }) => models);
|
|
51
|
+
}
|
|
52
|
+
return cache.models;
|
|
53
|
+
}
|
|
54
|
+
// A play/tool's body lives in its deployed workflow release. Not-found
|
|
55
|
+
// (never deployed) → null, so the mapper can decide (skip a tool, or emit a
|
|
56
|
+
// play without a workflow); any other error propagates — a network blip must
|
|
57
|
+
// not read as "never deployed" and silently skip the resource.
|
|
58
|
+
async function deployedWorkflow(workflowUuid) {
|
|
59
|
+
try {
|
|
60
|
+
const { release } = await api.orchestration.release.getDeployed({
|
|
61
|
+
workflowUuid,
|
|
62
|
+
});
|
|
63
|
+
return release;
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (isNotFound(error))
|
|
67
|
+
return null;
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function fileBytes(uuid) {
|
|
72
|
+
try {
|
|
73
|
+
const blob = await api.content.file.readBytes(uuid);
|
|
74
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (isSkippableRead(error))
|
|
78
|
+
return undefined;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Download + extract the promoted deployment's source tree (the pre-build
|
|
83
|
+
// project the user uploaded). Null when never promoted or not exportable.
|
|
84
|
+
async function deploymentSource(promotedDeployment) {
|
|
85
|
+
if (promotedDeployment === null || promotedDeployment === undefined) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
const uuid = promotedDeployment["uuid"];
|
|
89
|
+
if (typeof uuid !== "string")
|
|
90
|
+
return null;
|
|
91
|
+
let url;
|
|
92
|
+
try {
|
|
93
|
+
({ url } = await api.hosting.deployment.getSourceArchiveUrl({ uuid }));
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (isNotFound(error))
|
|
97
|
+
return null;
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const response = await fetch(url);
|
|
101
|
+
// 403/404: the archive object is gone or not exportable — skip the row
|
|
102
|
+
// (S3 answers 403 AccessDenied for missing keys). Server errors propagate.
|
|
103
|
+
if (response.status === 403 || response.status === 404)
|
|
104
|
+
return null;
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
throw new Error(`failed to download deployment source ${uuid}: HTTP ${String(response.status)}`);
|
|
107
|
+
}
|
|
108
|
+
return untarGz(new Uint8Array(await response.arrayBuffer()));
|
|
109
|
+
}
|
|
110
|
+
// The workspace's single context repo: its uuid + every markdown file. Null
|
|
111
|
+
// when the workspace has no repo.
|
|
112
|
+
async function contextRow() {
|
|
113
|
+
let repositoryUuid;
|
|
114
|
+
try {
|
|
115
|
+
const { repository } = await api.context.repository.get();
|
|
116
|
+
repositoryUuid = repository["uuid"];
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (isNotFound(error))
|
|
120
|
+
return null;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const { entries } = await api.context.runtime.browse({ depth: -1 });
|
|
124
|
+
const markdown = entries.filter((entry) => entry.kind === "file" && /\.mdx?$/i.test(entry.path));
|
|
125
|
+
// An unreadable file (gone, oversized) skips that file, not the pull.
|
|
126
|
+
const contents = await Promise.all(markdown.map(async (entry) => {
|
|
127
|
+
try {
|
|
128
|
+
return (await api.context.runtime.read({ path: entry.path })).content;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (isSkippableRead(error))
|
|
132
|
+
return undefined;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}));
|
|
136
|
+
const files = {};
|
|
137
|
+
markdown.forEach((entry, index) => {
|
|
138
|
+
const content = contents[index];
|
|
139
|
+
if (content !== undefined)
|
|
140
|
+
files[entry.path] = content;
|
|
141
|
+
});
|
|
142
|
+
return { uuid: repositoryUuid, __files: files };
|
|
143
|
+
}
|
|
144
|
+
const rows = (kind, raws) => raws.map((raw) => ({ kind, row: raw }));
|
|
145
|
+
const collectors = {
|
|
146
|
+
folder: async () => rows("folder", (await api.workspaceManagement.folder.all()).folders),
|
|
147
|
+
connector: async () => rows("connector", (await api.connection.connector.list({})).connectors),
|
|
148
|
+
customIntegration: async () => rows("customIntegration", (await api.connection.customIntegration.list()).customIntegrations),
|
|
149
|
+
model: async () => rows("model", await allModels()),
|
|
150
|
+
relationship: async () => {
|
|
151
|
+
// Relationships touching a unify model are auto-generated alongside it —
|
|
152
|
+
// flag them here (the mapper is pure and can't see the model rows) so
|
|
153
|
+
// they are skipped rather than authored.
|
|
154
|
+
const [{ relationships }, models] = await Promise.all([
|
|
155
|
+
api.storage.relationship.all(),
|
|
156
|
+
allModels(),
|
|
157
|
+
]);
|
|
158
|
+
const unifyModelUuids = new Set(models
|
|
159
|
+
.filter((m) => UNIFY_EXTRACTOR_SLUGS.has(m.extractorSlug))
|
|
160
|
+
.map((m) => m.uuid));
|
|
161
|
+
return relationships.map((row) => ({
|
|
162
|
+
kind: "relationship",
|
|
163
|
+
row: {
|
|
164
|
+
...row,
|
|
165
|
+
__touchesUnifyModel: unifyModelUuids.has(row.fromModelUuid) ||
|
|
166
|
+
unifyModelUuids.has(row.toModelUuid),
|
|
167
|
+
},
|
|
168
|
+
}));
|
|
169
|
+
},
|
|
170
|
+
segment: async () => rows("segment", (await api.segmentation.segment.all()).segments),
|
|
171
|
+
tool: async () => {
|
|
172
|
+
const { tools } = await api.orchestration.tool.all();
|
|
173
|
+
return Promise.all(tools.map(async (row) => ({
|
|
174
|
+
kind: "tool",
|
|
175
|
+
row: {
|
|
176
|
+
...row,
|
|
177
|
+
__workflow: await deployedWorkflow(row.workflowUuid),
|
|
178
|
+
},
|
|
179
|
+
})));
|
|
180
|
+
},
|
|
181
|
+
play: async () => {
|
|
182
|
+
const { plays } = await api.orchestration.play.list({});
|
|
183
|
+
// A dangling backing segment (deleted out of band) skips those fields,
|
|
184
|
+
// not the play — the mapper treats a null __segment as "no overrides".
|
|
185
|
+
const backingSegment = async (uuid) => {
|
|
186
|
+
try {
|
|
187
|
+
const { segment } = await api.segmentation.segment.get(uuid);
|
|
188
|
+
return segment;
|
|
189
|
+
}
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (isNotFound(error))
|
|
192
|
+
return null;
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
return Promise.all(plays.map(async (row) => {
|
|
197
|
+
// filter/sort/limit/trackingColumnSlugs live on the backing segment.
|
|
198
|
+
const [workflow, segment] = await Promise.all([
|
|
199
|
+
deployedWorkflow(row.workflowUuid),
|
|
200
|
+
backingSegment(row.segmentUuid),
|
|
201
|
+
]);
|
|
202
|
+
return {
|
|
203
|
+
kind: "play",
|
|
204
|
+
row: {
|
|
205
|
+
...row,
|
|
206
|
+
__workflow: workflow,
|
|
207
|
+
__segment: segment,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}));
|
|
211
|
+
},
|
|
212
|
+
mcpServer: async () => rows("mcpServer", (await api.ai.mcpServer.all()).mcpServers),
|
|
213
|
+
agent: async () => rows("agent", (await api.ai.agent.all()).agents),
|
|
214
|
+
capacity: async () => rows("capacity", (await api.revenueOrganization.capacity.all()).capacities),
|
|
215
|
+
territory: async () => rows("territory", (await api.revenueOrganization.territory.all()).territories),
|
|
216
|
+
file: async () => {
|
|
217
|
+
const { files } = await api.content.file.all();
|
|
218
|
+
return Promise.all(files.map(async (row) => ({
|
|
219
|
+
kind: "file",
|
|
220
|
+
// Unreadable content → __bytes undefined and the mapper skips it.
|
|
221
|
+
row: { ...row, __bytes: await fileBytes(row.uuid) },
|
|
222
|
+
})));
|
|
223
|
+
},
|
|
224
|
+
worker: async () => {
|
|
225
|
+
const { workers } = await api.hosting.worker.list();
|
|
226
|
+
return Promise.all(workers.map(async (row) => ({
|
|
227
|
+
kind: "worker",
|
|
228
|
+
row: {
|
|
229
|
+
...row,
|
|
230
|
+
__sourceFiles: await deploymentSource(row["promotedDeployment"]),
|
|
231
|
+
},
|
|
232
|
+
})));
|
|
233
|
+
},
|
|
234
|
+
app: async () => {
|
|
235
|
+
const { apps } = await api.hosting.app.list();
|
|
236
|
+
return Promise.all(apps.map(async (row) => ({
|
|
237
|
+
kind: "app",
|
|
238
|
+
row: {
|
|
239
|
+
...row,
|
|
240
|
+
__sourceFiles: await deploymentSource(row["promotedDeployment"]),
|
|
241
|
+
},
|
|
242
|
+
})));
|
|
243
|
+
},
|
|
244
|
+
context: async () => {
|
|
245
|
+
const row = await contextRow();
|
|
246
|
+
return row === null ? [] : [{ kind: "context", row }];
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
// A requested kind missing from `collectors` or ENUMERATION_ORDER would
|
|
250
|
+
// otherwise be silently never enumerated — the registries must stay in sync.
|
|
251
|
+
const unenumerable = kinds.filter((kind) => collectors[kind] === undefined || !ENUMERATION_ORDER.includes(kind));
|
|
252
|
+
if (unenumerable.length > 0) {
|
|
253
|
+
throw new Error(`cdk pull: no enumerator for kind(s) ${unenumerable.join(", ")} — add them to collectors and ENUMERATION_ORDER.`);
|
|
254
|
+
}
|
|
255
|
+
const wanted = ENUMERATION_ORDER.filter((kind) => kinds.includes(kind));
|
|
256
|
+
const chunks = await Promise.all(wanted.map((kind) => collectors[kind]()));
|
|
257
|
+
return chunks.flat();
|
|
258
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Api } from "@cargo-ai/api";
|
|
2
|
+
import type { ResourceKind } from "../../core.js";
|
|
3
|
+
import { type IRResource } from "./ir.js";
|
|
4
|
+
import { type GeneratedFile } from "./print.js";
|
|
5
|
+
export { SUPPORTED_KINDS } from "./ir.js";
|
|
6
|
+
export declare const SKIP_REASONS: Partial<Record<ResourceKind, string>>;
|
|
7
|
+
export type PullOptions = {
|
|
8
|
+
readonly kinds?: readonly ResourceKind[];
|
|
9
|
+
readonly dryRun?: boolean;
|
|
10
|
+
readonly noAdopt?: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type PullResult = {
|
|
13
|
+
readonly files: readonly GeneratedFile[];
|
|
14
|
+
readonly counts: Readonly<Record<string, number>>;
|
|
15
|
+
readonly skipped: Readonly<Record<string, number>>;
|
|
16
|
+
readonly envNames: readonly string[];
|
|
17
|
+
readonly adopted: number;
|
|
18
|
+
};
|
|
19
|
+
export declare function generateFiles(rows: {
|
|
20
|
+
kind: ResourceKind;
|
|
21
|
+
row: Record<string, unknown>;
|
|
22
|
+
}[]): {
|
|
23
|
+
files: GeneratedFile[];
|
|
24
|
+
resources: IRResource[];
|
|
25
|
+
skipped: Record<string, number>;
|
|
26
|
+
};
|
|
27
|
+
export declare function pull(root: string, api: Api, workspaceUuid: string, options?: PullOptions): Promise<PullResult>;
|
|
28
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/deploy/pull/index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAEzC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAIlD,OAAO,EAAE,KAAK,UAAU,EAAmC,MAAM,SAAS,CAAC;AAE3E,OAAO,EAAE,KAAK,aAAa,EAAiB,MAAM,YAAY,CAAC;AAG/D,OAAO,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAI1C,eAAO,MAAM,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAY9D,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IAExB,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAEzC,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAG1B,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IAEzC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAGlD,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAGnD,QAAQ,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AA8DF,wBAAgB,aAAa,CAC3B,IAAI,EAAE;IACJ,IAAI,EAAE,YAAY,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B,EAAE,GACF;IACD,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB,SAAS,EAAE,UAAU,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACjC,CAoBA;AAED,wBAAsB,IAAI,CACxB,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,GAAG,EACR,aAAa,EAAE,MAAM,EACrB,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,UAAU,CAAC,CAoDrB"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// `cdk pull` — generate define* source from a live workspace.
|
|
2
|
+
//
|
|
3
|
+
// Pipeline: enumerate (IO) → map (pure) → resolve refs (pure) → print (pure) →
|
|
4
|
+
// write files → adopt into cargo.state.json (reusing importResource, so the very
|
|
5
|
+
// next `cdk deploy` is a no-op instead of duplicate-creating what's already live).
|
|
6
|
+
//
|
|
7
|
+
// Adoption is the default and the safety property: without it, the generated
|
|
8
|
+
// code has no state, so a deploy would re-create resources that already exist.
|
|
9
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
10
|
+
import { dirname, join } from "node:path";
|
|
11
|
+
import { adoptResources } from "../import.js";
|
|
12
|
+
import { loadResources } from "../load.js";
|
|
13
|
+
import { enumerateWorkspace } from "./enumerate.js";
|
|
14
|
+
import { isSecretMarker, SUPPORTED_KINDS } from "./ir.js";
|
|
15
|
+
import { fileAssetPath, mapRow } from "./mappers.js";
|
|
16
|
+
import { printResource } from "./print.js";
|
|
17
|
+
import { resolveResources } from "./resolve.js";
|
|
18
|
+
export { SUPPORTED_KINDS } from "./ir.js";
|
|
19
|
+
// Why a kind's resources may be skipped — static per-kind, surfaced by the CLI
|
|
20
|
+
// next to the skip counts so a gap is never silent.
|
|
21
|
+
export const SKIP_REASONS = {
|
|
22
|
+
model: "unify models are auto-generated",
|
|
23
|
+
relationship: "auto-generated by unification",
|
|
24
|
+
segment: "play-backing segments are managed by their play",
|
|
25
|
+
customIntegration: "external (baseUrl) integrations have no builder",
|
|
26
|
+
tool: "read-only or never deployed",
|
|
27
|
+
mcpServer: "template-installed",
|
|
28
|
+
agent: "never deployed, or no dedicated LLM connector",
|
|
29
|
+
file: "content not readable",
|
|
30
|
+
worker: "never promoted",
|
|
31
|
+
app: "never promoted",
|
|
32
|
+
context: "empty repository",
|
|
33
|
+
};
|
|
34
|
+
// Every masked secret's env name, in file order (deduped + sorted).
|
|
35
|
+
function collectEnvNames(resources) {
|
|
36
|
+
const names = new Set();
|
|
37
|
+
const walk = (value) => {
|
|
38
|
+
if (isSecretMarker(value)) {
|
|
39
|
+
names.add(value.envName);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(value)) {
|
|
43
|
+
for (const item of value)
|
|
44
|
+
walk(item);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (typeof value === "object" && value !== null) {
|
|
48
|
+
for (const item of Object.values(value))
|
|
49
|
+
walk(item);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
for (const resource of resources)
|
|
53
|
+
walk(resource.spec);
|
|
54
|
+
return [...names].sort();
|
|
55
|
+
}
|
|
56
|
+
// Disambiguate resources that map to the same `<kind>:<slug>` (e.g. two models
|
|
57
|
+
// named "contacts", or folders whose names slugify alike) so files and export
|
|
58
|
+
// identifiers never collide + silently overwrite. Deterministic: append _2, _3…
|
|
59
|
+
// in input order. Refs still resolve by uuid, so renaming a slug is safe.
|
|
60
|
+
function dedupeSlugs(resources) {
|
|
61
|
+
const used = new Set();
|
|
62
|
+
return resources.map((resource) => {
|
|
63
|
+
let slug = resource.slug;
|
|
64
|
+
let n = 1;
|
|
65
|
+
while (used.has(`${resource.kind}:${slug}`)) {
|
|
66
|
+
n += 1;
|
|
67
|
+
slug = `${resource.slug}_${String(n)}`;
|
|
68
|
+
}
|
|
69
|
+
used.add(`${resource.kind}:${slug}`);
|
|
70
|
+
return slug === resource.slug ? resource : { ...resource, slug };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// After dedupeSlugs may have renamed a file resource, re-derive its single
|
|
74
|
+
// asset path from the final slug so two same-named files don't overwrite each
|
|
75
|
+
// other's content (spec.path references the asset, so both move together).
|
|
76
|
+
// Worker/app assets need no fixup: their slugs are server-unique, so dedupe
|
|
77
|
+
// never renames them.
|
|
78
|
+
function fixupFileAsset(resource) {
|
|
79
|
+
if (resource.kind !== "file" || resource.assets === undefined) {
|
|
80
|
+
return resource;
|
|
81
|
+
}
|
|
82
|
+
const asset = resource.assets[0];
|
|
83
|
+
if (asset === undefined)
|
|
84
|
+
return resource;
|
|
85
|
+
const path = fileAssetPath(resource.slug, asset.path);
|
|
86
|
+
if (path === asset.path)
|
|
87
|
+
return resource;
|
|
88
|
+
return {
|
|
89
|
+
...resource,
|
|
90
|
+
spec: { ...resource.spec, path },
|
|
91
|
+
assets: [{ ...asset, path }],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
// Turn a live workspace into generated files (+ resolved cross-refs), purely.
|
|
95
|
+
// `skipped` counts resources a mapper declined (returned null) per kind.
|
|
96
|
+
export function generateFiles(rows) {
|
|
97
|
+
const skipped = {};
|
|
98
|
+
const mapped = [];
|
|
99
|
+
for (const entry of rows) {
|
|
100
|
+
const ir = mapRow(entry.kind, entry.row);
|
|
101
|
+
if (ir === null) {
|
|
102
|
+
skipped[entry.kind] = (skipped[entry.kind] ?? 0) + 1;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
mapped.push(ir);
|
|
106
|
+
}
|
|
107
|
+
const resources = dedupeSlugs(mapped).map(fixupFileAsset);
|
|
108
|
+
const resolved = resolveResources(resources);
|
|
109
|
+
const files = [
|
|
110
|
+
...resolved.map(printResource),
|
|
111
|
+
...resources.flatMap((resource) => resource.assets === undefined ? [] : resource.assets),
|
|
112
|
+
];
|
|
113
|
+
return { files, resources, skipped };
|
|
114
|
+
}
|
|
115
|
+
export async function pull(root, api, workspaceUuid, options = {}) {
|
|
116
|
+
const kinds = options.kinds ?? SUPPORTED_KINDS;
|
|
117
|
+
const rows = await enumerateWorkspace(api, kinds);
|
|
118
|
+
const { files, resources, skipped } = generateFiles(rows);
|
|
119
|
+
const envNames = collectEnvNames(resources);
|
|
120
|
+
const counts = {};
|
|
121
|
+
for (const resource of resources) {
|
|
122
|
+
counts[resource.kind] = (counts[resource.kind] ?? 0) + 1;
|
|
123
|
+
}
|
|
124
|
+
if (options.dryRun === true) {
|
|
125
|
+
return { files, counts, skipped, envNames, adopted: 0 };
|
|
126
|
+
}
|
|
127
|
+
for (const file of files) {
|
|
128
|
+
const abs = join(root, file.path);
|
|
129
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
130
|
+
if ("source" in file)
|
|
131
|
+
await writeFile(abs, file.source, "utf8");
|
|
132
|
+
else
|
|
133
|
+
await writeFile(abs, file.bytes);
|
|
134
|
+
}
|
|
135
|
+
// The env vars the generated code expects (masked secrets) — an example env
|
|
136
|
+
// file so the repo is fillable before its first fresh deploy.
|
|
137
|
+
if (envNames.length > 0) {
|
|
138
|
+
await writeFile(join(root, ".env.example"), `# Secrets masked by cdk pull — fill in before deploying to a fresh workspace.\n${envNames
|
|
139
|
+
.map((name) => `${name}=`)
|
|
140
|
+
.join("\n")}\n`, "utf8");
|
|
141
|
+
}
|
|
142
|
+
// Load the just-written project once, then batch-adopt every resource so its
|
|
143
|
+
// state entry binds code → live uuid and a later deploy sees it as
|
|
144
|
+
// up-to-date. Atomic: a mid-batch failure writes no state at all.
|
|
145
|
+
const adopted = options.noAdopt === true
|
|
146
|
+
? 0
|
|
147
|
+
: await adoptResources(root, api, workspaceUuid, resources.map((resource) => ({
|
|
148
|
+
id: `${resource.kind}:${resource.slug}`,
|
|
149
|
+
uuid: resource.uuid,
|
|
150
|
+
})), await loadResources(root));
|
|
151
|
+
return { files, counts, skipped, envNames, adopted };
|
|
152
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ResourceKind } from "../../core.js";
|
|
2
|
+
export type RefMarker = {
|
|
3
|
+
readonly __ref: true;
|
|
4
|
+
readonly uuid: string;
|
|
5
|
+
readonly accessor?: string;
|
|
6
|
+
readonly fallback: unknown;
|
|
7
|
+
};
|
|
8
|
+
export type SecretMarker = {
|
|
9
|
+
readonly __secret: true;
|
|
10
|
+
readonly envName: string;
|
|
11
|
+
};
|
|
12
|
+
export type GeneratedAsset = {
|
|
13
|
+
readonly path: string;
|
|
14
|
+
readonly bytes: Uint8Array;
|
|
15
|
+
};
|
|
16
|
+
export type IRResource = {
|
|
17
|
+
readonly kind: ResourceKind;
|
|
18
|
+
readonly slug: string;
|
|
19
|
+
readonly uuid: string;
|
|
20
|
+
readonly spec: Record<string, unknown>;
|
|
21
|
+
readonly assets?: readonly GeneratedAsset[];
|
|
22
|
+
};
|
|
23
|
+
export declare function isRefMarker(value: unknown): value is RefMarker;
|
|
24
|
+
export declare function isSecretMarker(value: unknown): value is SecretMarker;
|
|
25
|
+
export type CallMarker = {
|
|
26
|
+
readonly __call: true;
|
|
27
|
+
readonly fn: string;
|
|
28
|
+
readonly args: readonly unknown[];
|
|
29
|
+
};
|
|
30
|
+
export declare function isCallMarker(value: unknown): value is CallMarker;
|
|
31
|
+
export declare function call(fn: string, args: readonly unknown[]): CallMarker;
|
|
32
|
+
export declare function ref(uuid: string, fallbackFn: string, options?: {
|
|
33
|
+
fallbackUuid?: string;
|
|
34
|
+
accessor?: string;
|
|
35
|
+
fallbackExpr?: unknown;
|
|
36
|
+
}): RefMarker;
|
|
37
|
+
export type KindMeta = {
|
|
38
|
+
readonly dir: string;
|
|
39
|
+
readonly defineFn: string;
|
|
40
|
+
readonly singleton?: boolean;
|
|
41
|
+
};
|
|
42
|
+
export declare const KIND_META: Partial<Record<ResourceKind, KindMeta>>;
|
|
43
|
+
export declare const SUPPORTED_KINDS: ResourceKind[];
|
|
44
|
+
export declare function toIdentifier(slug: string): string;
|
|
45
|
+
//# sourceMappingURL=ir.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ir.d.ts","sourceRoot":"","sources":["../../../../src/deploy/pull/ir.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IAErB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAGtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAI3B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC;IACxB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B,CAAC;AAKF,MAAM,MAAM,cAAc,GAAG;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;CAC7C,CAAC;AAEF,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAM9D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,YAAY,CAMpE;AAKD,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,SAAS,OAAO,EAAE,CAAC;CACnC,CAAC;AAEF,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAMhE;AAED,wBAAgB,IAAI,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,OAAO,EAAE,GAAG,UAAU,CAErE;AAED,wBAAgB,GAAG,CACjB,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAIP,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,YAAY,CAAC,EAAE,OAAO,CAAC;CACnB,GACL,SAAS,CAQX;AAID,MAAM,MAAM,QAAQ,GAAG;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAG1B,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,SAAS,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,CAoB7D,CAAC;AAEF,eAAO,MAAM,eAAe,gBAA2C,CAAC;AAKxE,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQjD"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// Intermediate representation for `cdk pull` (live workspace → define* source).
|
|
2
|
+
//
|
|
3
|
+
// A live resource is first mapped to an `IRResource`: its define*-spec fields
|
|
4
|
+
// (create-spec, not the drift-reader's updatable subset), with two kinds of
|
|
5
|
+
// markers embedded wherever a raw value can't be printed as plain JSON:
|
|
6
|
+
// - `RefMarker` — a uuid pointing at another resource. `resolve` later turns
|
|
7
|
+
// it into a code handle (if that resource is also being pulled) or a literal
|
|
8
|
+
// `xxRef("uuid")`.
|
|
9
|
+
// - `SecretMarker` — an encrypted credential field. Printed as
|
|
10
|
+
// `secret("NAME")` (the env-var name, read at apply time); the real value
|
|
11
|
+
// is never recoverable from the API.
|
|
12
|
+
//
|
|
13
|
+
// map/resolve/print are pure so the whole pipeline is unit-testable from raw
|
|
14
|
+
// API-row fixtures, with no network.
|
|
15
|
+
export function isRefMarker(value) {
|
|
16
|
+
return (typeof value === "object" &&
|
|
17
|
+
value !== null &&
|
|
18
|
+
value.__ref === true);
|
|
19
|
+
}
|
|
20
|
+
export function isSecretMarker(value) {
|
|
21
|
+
return (typeof value === "object" &&
|
|
22
|
+
value !== null &&
|
|
23
|
+
value.__secret === true);
|
|
24
|
+
}
|
|
25
|
+
export function isCallMarker(value) {
|
|
26
|
+
return (typeof value === "object" &&
|
|
27
|
+
value !== null &&
|
|
28
|
+
value.__call === true);
|
|
29
|
+
}
|
|
30
|
+
export function call(fn, args) {
|
|
31
|
+
return { __call: true, fn, args };
|
|
32
|
+
}
|
|
33
|
+
export function ref(uuid, fallbackFn, options = {}) {
|
|
34
|
+
const fallback = options.fallbackExpr === undefined
|
|
35
|
+
? call(fallbackFn, [
|
|
36
|
+
options.fallbackUuid === undefined ? uuid : options.fallbackUuid,
|
|
37
|
+
])
|
|
38
|
+
: options.fallbackExpr;
|
|
39
|
+
return { __ref: true, uuid, accessor: options.accessor, fallback };
|
|
40
|
+
}
|
|
41
|
+
export const KIND_META = {
|
|
42
|
+
connector: { dir: "connectors", defineFn: "defineConnector" },
|
|
43
|
+
customIntegration: {
|
|
44
|
+
dir: "custom-integrations",
|
|
45
|
+
defineFn: "defineCustomIntegration",
|
|
46
|
+
},
|
|
47
|
+
model: { dir: "models", defineFn: "defineModel" },
|
|
48
|
+
relationship: { dir: "relationships", defineFn: "defineRelationship" },
|
|
49
|
+
segment: { dir: "segments", defineFn: "defineSegment" },
|
|
50
|
+
folder: { dir: "folders", defineFn: "defineFolder" },
|
|
51
|
+
capacity: { dir: "capacities", defineFn: "defineCapacity" },
|
|
52
|
+
territory: { dir: "territories", defineFn: "defineTerritory" },
|
|
53
|
+
tool: { dir: "tools", defineFn: "defineTool" },
|
|
54
|
+
play: { dir: "plays", defineFn: "definePlay" },
|
|
55
|
+
mcpServer: { dir: "mcp-servers", defineFn: "defineMcpServer" },
|
|
56
|
+
agent: { dir: "agents", defineFn: "defineAgent" },
|
|
57
|
+
file: { dir: "files", defineFn: "defineFile" },
|
|
58
|
+
worker: { dir: "workers", defineFn: "defineWorker" },
|
|
59
|
+
app: { dir: "apps", defineFn: "defineApp" },
|
|
60
|
+
context: { dir: "context", defineFn: "defineContext", singleton: true },
|
|
61
|
+
};
|
|
62
|
+
export const SUPPORTED_KINDS = Object.keys(KIND_META);
|
|
63
|
+
// A live slug (snake_case / kebab) → a valid JS identifier for `export const`.
|
|
64
|
+
// camelCases across `-`/`_`, strips other invalid chars, and prefixes a leading
|
|
65
|
+
// digit. Deterministic so imports and exports agree across files.
|
|
66
|
+
export function toIdentifier(slug) {
|
|
67
|
+
const camel = slug
|
|
68
|
+
.replace(/[^A-Za-z0-9]+(.)?/g, (_m, c) => c === undefined ? "" : c.toUpperCase())
|
|
69
|
+
.replace(/[^A-Za-z0-9]/g, "");
|
|
70
|
+
const safe = camel.length === 0 ? "resource" : camel;
|
|
71
|
+
return /^[0-9]/.test(safe) ? `r${safe}` : safe;
|
|
72
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ResourceKind } from "../../core.js";
|
|
2
|
+
import { type IRResource } from "./ir.js";
|
|
3
|
+
type Row = Record<string, unknown>;
|
|
4
|
+
export declare function fileAssetPath(slug: string, sourceName: string): string;
|
|
5
|
+
type Mapper = (row: Row) => IRResource | null;
|
|
6
|
+
export declare const UNIFY_EXTRACTOR_SLUGS: Set<string>;
|
|
7
|
+
export declare const MAPPERS: Partial<Record<ResourceKind, Mapper>>;
|
|
8
|
+
export declare function mapRow(kind: ResourceKind, row: Row): IRResource | null;
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=mappers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mappers.d.ts","sourceRoot":"","sources":["../../../../src/deploy/pull/mappers.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,OAAO,EAEL,KAAK,UAAU,EAMhB,MAAM,SAAS,CAAC;AAEjB,KAAK,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AAoHnC,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,CAGtE;AAGD,KAAK,MAAM,GAAG,CAAC,GAAG,EAAE,GAAG,KAAK,UAAU,GAAG,IAAI,CAAC;AAkB9C,eAAO,MAAM,qBAAqB,aAKhC,CAAC;AAmiBH,eAAO,MAAM,OAAO,EAAE,OAAO,CAAC,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,CAiBzD,CAAC;AAEF,wBAAgB,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,GAAG,EAAE,GAAG,GAAG,UAAU,GAAG,IAAI,CAQtE"}
|