@cargo-ai/cdk 0.1.1
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 +187 -0
- package/build/src/core.d.ts +83 -0
- package/build/src/core.d.ts.map +1 -0
- package/build/src/core.js +205 -0
- package/build/src/deploy/apply.d.ts +55 -0
- package/build/src/deploy/apply.d.ts.map +1 -0
- package/build/src/deploy/apply.js +219 -0
- package/build/src/deploy/audit.d.ts +14 -0
- package/build/src/deploy/audit.d.ts.map +1 -0
- package/build/src/deploy/audit.js +25 -0
- package/build/src/deploy/compile.d.ts +36 -0
- package/build/src/deploy/compile.d.ts.map +1 -0
- package/build/src/deploy/compile.js +99 -0
- package/build/src/deploy/destroy.d.ts +20 -0
- package/build/src/deploy/destroy.d.ts.map +1 -0
- package/build/src/deploy/destroy.js +153 -0
- package/build/src/deploy/executors.live.d.ts +4 -0
- package/build/src/deploy/executors.live.d.ts.map +1 -0
- package/build/src/deploy/executors.live.js +623 -0
- package/build/src/deploy/import.d.ts +13 -0
- package/build/src/deploy/import.d.ts.map +1 -0
- package/build/src/deploy/import.js +87 -0
- package/build/src/deploy/index.d.ts +39 -0
- package/build/src/deploy/index.d.ts.map +1 -0
- package/build/src/deploy/index.js +51 -0
- package/build/src/deploy/load.d.ts +7 -0
- package/build/src/deploy/load.d.ts.map +1 -0
- package/build/src/deploy/load.js +58 -0
- package/build/src/deploy/lock.d.ts +10 -0
- package/build/src/deploy/lock.d.ts.map +1 -0
- package/build/src/deploy/lock.js +101 -0
- package/build/src/deploy/plan.d.ts +3 -0
- package/build/src/deploy/plan.d.ts.map +1 -0
- package/build/src/deploy/plan.js +39 -0
- package/build/src/deploy/readers.live.d.ts +4 -0
- package/build/src/deploy/readers.live.d.ts.map +1 -0
- package/build/src/deploy/readers.live.js +273 -0
- package/build/src/deploy/refresh.d.ts +32 -0
- package/build/src/deploy/refresh.d.ts.map +1 -0
- package/build/src/deploy/refresh.js +76 -0
- package/build/src/deploy/state.d.ts +23 -0
- package/build/src/deploy/state.d.ts.map +1 -0
- package/build/src/deploy/state.js +107 -0
- package/build/src/index.d.ts +30 -0
- package/build/src/index.d.ts.map +1 -0
- package/build/src/index.js +30 -0
- package/build/src/refs.d.ts +28 -0
- package/build/src/refs.d.ts.map +1 -0
- package/build/src/refs.js +34 -0
- package/build/src/resources/actions.d.ts +31 -0
- package/build/src/resources/actions.d.ts.map +1 -0
- package/build/src/resources/actions.js +44 -0
- package/build/src/resources/agent.d.ts +71 -0
- package/build/src/resources/agent.d.ts.map +1 -0
- package/build/src/resources/agent.js +96 -0
- package/build/src/resources/app.d.ts +18 -0
- package/build/src/resources/app.d.ts.map +1 -0
- package/build/src/resources/app.js +33 -0
- package/build/src/resources/bundle.d.ts +15 -0
- package/build/src/resources/bundle.d.ts.map +1 -0
- package/build/src/resources/bundle.js +61 -0
- package/build/src/resources/connector.d.ts +30 -0
- package/build/src/resources/connector.d.ts.map +1 -0
- package/build/src/resources/connector.js +41 -0
- package/build/src/resources/context.d.ts +15 -0
- package/build/src/resources/context.d.ts.map +1 -0
- package/build/src/resources/context.js +58 -0
- package/build/src/resources/file.d.ts +15 -0
- package/build/src/resources/file.d.ts.map +1 -0
- package/build/src/resources/file.js +35 -0
- package/build/src/resources/folder.d.ts +20 -0
- package/build/src/resources/folder.d.ts.map +1 -0
- package/build/src/resources/folder.js +28 -0
- package/build/src/resources/mcpServer.d.ts +25 -0
- package/build/src/resources/mcpServer.d.ts.map +1 -0
- package/build/src/resources/mcpServer.js +34 -0
- package/build/src/resources/model.d.ts +34 -0
- package/build/src/resources/model.d.ts.map +1 -0
- package/build/src/resources/model.js +43 -0
- package/build/src/resources/play.d.ts +35 -0
- package/build/src/resources/play.d.ts.map +1 -0
- package/build/src/resources/play.js +49 -0
- package/build/src/resources/tool.d.ts +40 -0
- package/build/src/resources/tool.d.ts.map +1 -0
- package/build/src/resources/tool.js +36 -0
- package/build/src/resources/worker.d.ts +18 -0
- package/build/src/resources/worker.d.ts.map +1 -0
- package/build/src/resources/worker.js +40 -0
- package/build/tsconfig.tsbuildinfo +1 -0
- package/package.json +71 -0
- package/templates/blank/README.md +24 -0
- package/templates/blank/cargo.ts +44 -0
- package/templates/blank/package.json +17 -0
- package/templates/blank/tsconfig.json +14 -0
- package/templates/full/README.md +69 -0
- package/templates/full/agents/enricher.ts +13 -0
- package/templates/full/agents/sdr.ts +28 -0
- package/templates/full/apps/dashboard/index.html +12 -0
- package/templates/full/apps/dashboard/package-lock.json +1080 -0
- package/templates/full/apps/dashboard/package.json +15 -0
- package/templates/full/apps/dashboard/src/main.tsx +12 -0
- package/templates/full/apps/dashboard/tsconfig.json +13 -0
- package/templates/full/apps/dashboard.ts +9 -0
- package/templates/full/connectors/hubspot.ts +12 -0
- package/templates/full/connectors/openai.ts +9 -0
- package/templates/full/context/context.ts +7 -0
- package/templates/full/context/icp.md +22 -0
- package/templates/full/context/personas/head-of-growth.md +26 -0
- package/templates/full/files/playbook.md +19 -0
- package/templates/full/files/playbook.ts +8 -0
- package/templates/full/folders/crm.ts +14 -0
- package/templates/full/mcp/crm.ts +15 -0
- package/templates/full/models/contacts.ts +15 -0
- package/templates/full/package.json +17 -0
- package/templates/full/plays/onboarding.ts +41 -0
- package/templates/full/tools/enrich.ts +34 -0
- package/templates/full/tsconfig.json +17 -0
- package/templates/full/workers/webhook/index.js +12 -0
- package/templates/full/workers/webhook/manifest.json +1 -0
- package/templates/full/workers/webhook/package-lock.json +12 -0
- package/templates/full/workers/webhook/package.json +6 -0
- package/templates/full/workers/webhook.ts +13 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// The apply engine — the live half. It compiles, then walks the plan in
|
|
2
|
+
// topological order: for each drifted resource it resolves the tokens in its
|
|
3
|
+
// spec to the real outputs its dependencies produced, calls the matching
|
|
4
|
+
// executor (passing the prior state entry for idempotency), records the new
|
|
5
|
+
// outputs, and persists state AFTER EACH resource so a mid-deploy crash leaves a
|
|
6
|
+
// recoverable cargo.state.json. Returns the next State.
|
|
7
|
+
//
|
|
8
|
+
// Side effects are injected as `Executors` (one per kind), so this stays pure
|
|
9
|
+
// and testable. A host binds them to @cargo-ai/api; tests bind mocks.
|
|
10
|
+
import { dependenciesOf, resolveSecrets, resolveTokens, } from "../core.js";
|
|
11
|
+
import { compile, } from "./compile.js";
|
|
12
|
+
const PLACEHOLDER = /\$\{[A-Za-z0-9_]+\}/;
|
|
13
|
+
// Refuse to send an unresolved `env("X")` placeholder (a missing secret/config
|
|
14
|
+
// value) to a live create. Walks the resolved spec for `${NAME}` strings.
|
|
15
|
+
function assertNoPlaceholders(id, value) {
|
|
16
|
+
if (typeof value === "string") {
|
|
17
|
+
if (PLACEHOLDER.test(value)) {
|
|
18
|
+
throw new Error(`${id}: unresolved placeholder "${value}" — set the environment variable before deploying.`);
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
for (const item of value)
|
|
24
|
+
assertNoPlaceholders(id, item);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (value !== null && typeof value === "object") {
|
|
28
|
+
for (const item of Object.values(value))
|
|
29
|
+
assertNoPlaceholders(id, item);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function apply(input, executors, persist, readers, onProgress) {
|
|
33
|
+
const { plan, errors } = compile(input);
|
|
34
|
+
if (errors.length > 0) {
|
|
35
|
+
const detail = errors.map((e) => `${e.id}: ${e.message}`).join("; ");
|
|
36
|
+
throw new Error(`apply aborted — fix plan errors first: ${detail}`);
|
|
37
|
+
}
|
|
38
|
+
const byId = new Map(input.nodes.map((n) => [n.id, n]));
|
|
39
|
+
// Resolved outputs by resource id, seeded from prior state so unchanged
|
|
40
|
+
// ("noop") dependencies still resolve for their dependents.
|
|
41
|
+
const outputs = new Map();
|
|
42
|
+
for (const [id, entry] of Object.entries(input.state?.resources ?? {})) {
|
|
43
|
+
outputs.set(id, entry.outputs);
|
|
44
|
+
}
|
|
45
|
+
const state = {
|
|
46
|
+
version: input.state?.version ?? 1,
|
|
47
|
+
workspaceUuid: input.state?.workspaceUuid ?? "",
|
|
48
|
+
resources: { ...(input.state?.resources ?? {}) },
|
|
49
|
+
};
|
|
50
|
+
const applied = [];
|
|
51
|
+
const skipped = [];
|
|
52
|
+
// Resources whose produced outputs changed identity this run (e.g. a file
|
|
53
|
+
// whose content changed got a new uuid). The plan is in topological order, so
|
|
54
|
+
// a dependency is recorded here before any dependent is processed.
|
|
55
|
+
const changedOutputs = new Set();
|
|
56
|
+
// How many resources will actually be applied — drives the (index/total)
|
|
57
|
+
// counter. Promoted noops (stale-dependency re-applies) bump it as they occur.
|
|
58
|
+
let total = plan.filter((e) => e.change !== "noop").length;
|
|
59
|
+
let index = 0;
|
|
60
|
+
for (const entry of plan) {
|
|
61
|
+
const node = byId.get(entry.id);
|
|
62
|
+
if (node === undefined)
|
|
63
|
+
continue; // delete entries — handled by destroy
|
|
64
|
+
let change = entry.change;
|
|
65
|
+
if (change === "noop") {
|
|
66
|
+
// A noop's own spec hash is unchanged, but if one of its dependencies
|
|
67
|
+
// produced a NEW output identity this run, the tokens in this spec now
|
|
68
|
+
// resolve to different values than its live config holds (e.g. it still
|
|
69
|
+
// points at a file uuid that was just deleted). Re-apply it so it
|
|
70
|
+
// re-points at the new value; otherwise it silently dangles.
|
|
71
|
+
const deps = dependenciesOf(node.spec);
|
|
72
|
+
const stale = Array.from(deps).some((d) => changedOutputs.has(d));
|
|
73
|
+
if (!stale) {
|
|
74
|
+
skipped.push(entry);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
change = "update";
|
|
78
|
+
total += 1; // a noop promoted to a re-apply — count it
|
|
79
|
+
}
|
|
80
|
+
index += 1;
|
|
81
|
+
// `change` is create|update here (noop was skipped/promoted above; delete
|
|
82
|
+
// entries have no node and were `continue`d). `index`/`total` are fixed for
|
|
83
|
+
// this resource, so the start/done events share one payload.
|
|
84
|
+
const progress = {
|
|
85
|
+
id: node.id,
|
|
86
|
+
change: change,
|
|
87
|
+
index,
|
|
88
|
+
total,
|
|
89
|
+
};
|
|
90
|
+
onProgress?.({ phase: "start", ...progress });
|
|
91
|
+
// tokens (deferred outputs) then secrets (deferred env reads) — both resolve
|
|
92
|
+
// to real values here, at apply, just before the executor runs.
|
|
93
|
+
const resolved = resolveSecrets(resolveTokens(node.spec, outputs));
|
|
94
|
+
assertNoPlaceholders(node.id, resolved);
|
|
95
|
+
const prior = input.state?.resources[node.id];
|
|
96
|
+
let produced;
|
|
97
|
+
try {
|
|
98
|
+
produced = await runExecutor(node, resolved, prior, executors);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Tag the failure with the resource that failed and surface the API's
|
|
102
|
+
// structured detail — executors throw `@cargo-ai/api` FetcherErrors whose
|
|
103
|
+
// bare `message` is often a terse "Invalid configuration", with the
|
|
104
|
+
// actionable bits (reason, status, which field) on `reason`/`status`/`data`.
|
|
105
|
+
throw new Error(`${node.id}: ${describeExecutorError(error)}`, {
|
|
106
|
+
cause: error,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Carry the "adopted" marker forward across updates so destroy keeps
|
|
110
|
+
// releasing (not deleting) a resource the CDK adopted or imported, even for
|
|
111
|
+
// kinds whose executor doesn't echo it back.
|
|
112
|
+
if (prior?.outputs["adopted"] === "true" &&
|
|
113
|
+
produced["adopted"] === undefined) {
|
|
114
|
+
produced["adopted"] = "true";
|
|
115
|
+
}
|
|
116
|
+
if (outputsChanged(prior?.outputs, produced)) {
|
|
117
|
+
changedOutputs.add(node.id);
|
|
118
|
+
}
|
|
119
|
+
outputs.set(node.id, produced);
|
|
120
|
+
const newEntry = {
|
|
121
|
+
hash: entry.hash,
|
|
122
|
+
uuid: produced.uuid,
|
|
123
|
+
outputs: produced,
|
|
124
|
+
deps: Array.from(dependenciesOf(node.spec)),
|
|
125
|
+
};
|
|
126
|
+
state.resources[entry.id] = newEntry;
|
|
127
|
+
// Capture a baseline fingerprint of the just-applied live resource so a
|
|
128
|
+
// later `refresh` can tell whether it was changed out-of-band. Best-effort:
|
|
129
|
+
// a read failure must not fail the deploy.
|
|
130
|
+
if (readers !== undefined) {
|
|
131
|
+
try {
|
|
132
|
+
const live = await readers[node.kind](newEntry);
|
|
133
|
+
if (live.fingerprint !== undefined)
|
|
134
|
+
newEntry.live = live.fingerprint;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// leave newEntry.live unset — refresh will report "unknown"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (persist !== undefined)
|
|
141
|
+
persist(state);
|
|
142
|
+
onProgress?.({ phase: "done", ...progress });
|
|
143
|
+
applied.push(change === entry.change ? entry : { ...entry, change });
|
|
144
|
+
}
|
|
145
|
+
return { state, applied, skipped };
|
|
146
|
+
}
|
|
147
|
+
// Flatten an executor error into the most actionable one-line string we can.
|
|
148
|
+
// `@cargo-ai/api` FetcherErrors put a terse summary on `.message` (e.g.
|
|
149
|
+
// "Invalid configuration") and the useful detail on `.reason` (a code),
|
|
150
|
+
// `.status` (HTTP), and `.data` (the response body, often naming the bad
|
|
151
|
+
// field). We duck-type those so the engine stays free of an `@cargo-ai/api`
|
|
152
|
+
// import.
|
|
153
|
+
function describeExecutorError(error) {
|
|
154
|
+
if (!(error instanceof Error))
|
|
155
|
+
return String(error);
|
|
156
|
+
const e = error;
|
|
157
|
+
const parts = [e.message];
|
|
158
|
+
if (typeof e.reason === "string" &&
|
|
159
|
+
e.reason.length > 0 &&
|
|
160
|
+
e.reason !== e.message) {
|
|
161
|
+
parts.push(`reason=${e.reason}`);
|
|
162
|
+
}
|
|
163
|
+
if (typeof e.status === "number")
|
|
164
|
+
parts.push(`status=${String(e.status)}`);
|
|
165
|
+
if (e.data !== null && typeof e.data === "object") {
|
|
166
|
+
const extra = Object.entries(e.data)
|
|
167
|
+
.filter(([k, v]) => k !== "errorMessage" &&
|
|
168
|
+
k !== "reason" &&
|
|
169
|
+
v !== undefined &&
|
|
170
|
+
v !== null)
|
|
171
|
+
.map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`);
|
|
172
|
+
if (extra.length > 0)
|
|
173
|
+
parts.push(extra.join(" "));
|
|
174
|
+
}
|
|
175
|
+
return parts.join(" | ");
|
|
176
|
+
}
|
|
177
|
+
// True when a resource's produced outputs differ from its prior outputs in any
|
|
178
|
+
// token-resolvable field — i.e. its identity (uuid / datasetUuid / url / …)
|
|
179
|
+
// changed, so dependents referencing it must re-apply. A brand-new resource
|
|
180
|
+
// (no prior) always counts as changed. The bookkeeping-only "adopted" marker is
|
|
181
|
+
// ignored so carrying it forward doesn't spuriously invalidate dependents.
|
|
182
|
+
function outputsChanged(prior, produced) {
|
|
183
|
+
if (prior === undefined)
|
|
184
|
+
return true;
|
|
185
|
+
const keys = new Set([...Object.keys(prior), ...Object.keys(produced)]);
|
|
186
|
+
for (const key of keys) {
|
|
187
|
+
if (key === "adopted")
|
|
188
|
+
continue;
|
|
189
|
+
if (prior[key] !== produced[key])
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
function runExecutor(node, resolved, prior, executors) {
|
|
195
|
+
switch (node.kind) {
|
|
196
|
+
case "connector":
|
|
197
|
+
return executors.connector(resolved, prior);
|
|
198
|
+
case "model":
|
|
199
|
+
return executors.model(resolved, prior);
|
|
200
|
+
case "agent":
|
|
201
|
+
return executors.agent(resolved, prior);
|
|
202
|
+
case "play":
|
|
203
|
+
return executors.play(resolved, prior);
|
|
204
|
+
case "tool":
|
|
205
|
+
return executors.tool(resolved, prior);
|
|
206
|
+
case "mcpServer":
|
|
207
|
+
return executors.mcpServer(resolved, prior);
|
|
208
|
+
case "folder":
|
|
209
|
+
return executors.folder(resolved, prior);
|
|
210
|
+
case "file":
|
|
211
|
+
return executors.file(resolved, prior);
|
|
212
|
+
case "worker":
|
|
213
|
+
return executors.worker(resolved, prior);
|
|
214
|
+
case "app":
|
|
215
|
+
return executors.app(resolved, prior);
|
|
216
|
+
case "context":
|
|
217
|
+
return executors.context(resolved, prior);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AuditEntry = {
|
|
2
|
+
runId: string;
|
|
3
|
+
at: string;
|
|
4
|
+
command: string;
|
|
5
|
+
workspaceUuid: string;
|
|
6
|
+
ok: boolean;
|
|
7
|
+
summary?: Record<string, unknown>;
|
|
8
|
+
error?: string;
|
|
9
|
+
};
|
|
10
|
+
/** A short, unique id for one CDK run, surfaced to the user and the audit log. */
|
|
11
|
+
export declare function newRunId(): string;
|
|
12
|
+
/** Append one audit record. Best-effort — never throws into the caller. */
|
|
13
|
+
export declare function appendAudit(root: string, entry: AuditEntry): void;
|
|
14
|
+
//# sourceMappingURL=audit.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"audit.d.ts","sourceRoot":"","sources":["../../../src/deploy/audit.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,UAAU,GAAG;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,EAAE,EAAE,OAAO,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,kFAAkF;AAClF,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAOD,2EAA2E;AAC3E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAMjE"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// A local, append-only audit trail of every mutating CDK run (deploy / destroy /
|
|
2
|
+
// import / prune). Each line of `cargo.state.audit.jsonl` records who ran what,
|
|
3
|
+
// against which workspace, and the outcome — so a workspace's change history is
|
|
4
|
+
// reconstructable without a backend. It's a local operational log; git-ignore it.
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { appendFileSync } from "node:fs";
|
|
7
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
8
|
+
const AUDIT_FILE = "cargo.state.audit.jsonl";
|
|
9
|
+
/** A short, unique id for one CDK run, surfaced to the user and the audit log. */
|
|
10
|
+
export function newRunId() {
|
|
11
|
+
return randomUUID().slice(0, 8);
|
|
12
|
+
}
|
|
13
|
+
function auditPath(root) {
|
|
14
|
+
const dir = isAbsolute(root) ? root : resolve(process.cwd(), root);
|
|
15
|
+
return join(dir, AUDIT_FILE);
|
|
16
|
+
}
|
|
17
|
+
/** Append one audit record. Best-effort — never throws into the caller. */
|
|
18
|
+
export function appendAudit(root, entry) {
|
|
19
|
+
try {
|
|
20
|
+
appendFileSync(auditPath(root), `${JSON.stringify(entry)}\n`);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// a failed audit write must not fail the command
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type ResourceKind, type ResourceNode } from "../core.js";
|
|
2
|
+
export type ChangeType = "create" | "update" | "noop" | "delete";
|
|
3
|
+
export type PlanEntry = {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: ResourceKind;
|
|
6
|
+
slug: string;
|
|
7
|
+
change: ChangeType;
|
|
8
|
+
hash: string;
|
|
9
|
+
};
|
|
10
|
+
export type ValidationError = {
|
|
11
|
+
id: string;
|
|
12
|
+
message: string;
|
|
13
|
+
};
|
|
14
|
+
export type StateEntry = {
|
|
15
|
+
hash: string;
|
|
16
|
+
uuid: string;
|
|
17
|
+
outputs: Record<string, string>;
|
|
18
|
+
deps?: string[];
|
|
19
|
+
live?: string;
|
|
20
|
+
};
|
|
21
|
+
export type State = {
|
|
22
|
+
version: number;
|
|
23
|
+
workspaceUuid: string;
|
|
24
|
+
resources: Record<string, StateEntry>;
|
|
25
|
+
};
|
|
26
|
+
export type CompileInput = {
|
|
27
|
+
nodes: readonly ResourceNode[];
|
|
28
|
+
state?: State;
|
|
29
|
+
};
|
|
30
|
+
export type CompileResult = {
|
|
31
|
+
order: string[];
|
|
32
|
+
plan: PlanEntry[];
|
|
33
|
+
errors: ValidationError[];
|
|
34
|
+
};
|
|
35
|
+
export declare function compile(input: CompileInput): CompileResult;
|
|
36
|
+
//# sourceMappingURL=compile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compile.d.ts","sourceRoot":"","sources":["../../../src/deploy/compile.ts"],"names":[],"mappings":"AAUA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,YAAY,EAElB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEjE,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAE9D,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC;IAIb,IAAI,EAAE,MAAM,CAAC;IAGb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAIhC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAKhB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAC;AACF,MAAM,MAAM,KAAK,GAAG;IAClB,OAAO,EAAE,MAAM,CAAC;IAGhB,aAAa,EAAE,MAAM,CAAC;IACtB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,SAAS,YAAY,EAAE,CAAC;IAC/B,KAAK,CAAC,EAAE,KAAK,CAAC;CACf,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,MAAM,EAAE,eAAe,EAAE,CAAC;CAC3B,CAAC;AAmEF,wBAAgB,OAAO,CAAC,KAAK,EAAE,YAAY,GAAG,aAAa,CAuC1D"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// The offline reconcile engine. Pure + testable — no IO, no network. It takes
|
|
2
|
+
// the resources declared via define* (the registry's nodes), validates the
|
|
3
|
+
// token graph, topologically orders it, content-hashes each resource, and diffs
|
|
4
|
+
// against the prior cargo.state.json to produce create/update/noop.
|
|
5
|
+
//
|
|
6
|
+
// Dependencies are derived from tokens embedded in each resource's spec, so the
|
|
7
|
+
// graph is whatever the handle wiring expressed — no string slugs to resolve.
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
9
|
+
import { dependenciesOf, stableStringify, } from "../core.js";
|
|
10
|
+
function hashOf(value) {
|
|
11
|
+
return ("sha256:" +
|
|
12
|
+
createHash("sha256")
|
|
13
|
+
.update(stableStringify(value))
|
|
14
|
+
.digest("hex")
|
|
15
|
+
.slice(0, 16));
|
|
16
|
+
}
|
|
17
|
+
function validate(nodes, edges) {
|
|
18
|
+
const errors = [];
|
|
19
|
+
const ids = new Set(nodes.map((n) => n.id));
|
|
20
|
+
for (const node of nodes) {
|
|
21
|
+
for (const dep of edges.get(node.id) ?? new Set()) {
|
|
22
|
+
if (!ids.has(dep)) {
|
|
23
|
+
errors.push({
|
|
24
|
+
id: node.id,
|
|
25
|
+
message: `references unknown resource "${dep}"`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return errors;
|
|
31
|
+
}
|
|
32
|
+
function topoSort(nodes, edges) {
|
|
33
|
+
const visited = new Map();
|
|
34
|
+
const order = [];
|
|
35
|
+
const cycle = [];
|
|
36
|
+
function visit(id) {
|
|
37
|
+
const state = visited.get(id);
|
|
38
|
+
if (state === "done")
|
|
39
|
+
return true;
|
|
40
|
+
if (state === "visiting") {
|
|
41
|
+
cycle.push(id);
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
visited.set(id, "visiting");
|
|
45
|
+
for (const dep of edges.get(id) ?? new Set()) {
|
|
46
|
+
if (!edges.has(dep))
|
|
47
|
+
continue; // unknown dep — reported by validate()
|
|
48
|
+
if (!visit(dep)) {
|
|
49
|
+
cycle.push(id);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
visited.set(id, "done");
|
|
54
|
+
order.push(id);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const ids = nodes.map((n) => n.id).sort();
|
|
58
|
+
for (const id of ids) {
|
|
59
|
+
if (cycle.length > 0)
|
|
60
|
+
break;
|
|
61
|
+
visit(id);
|
|
62
|
+
}
|
|
63
|
+
return { order, cycle };
|
|
64
|
+
}
|
|
65
|
+
export function compile(input) {
|
|
66
|
+
const { nodes } = input;
|
|
67
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
68
|
+
const edges = new Map();
|
|
69
|
+
for (const node of nodes) {
|
|
70
|
+
edges.set(node.id, dependenciesOf(node.spec));
|
|
71
|
+
}
|
|
72
|
+
const errors = validate(nodes, edges);
|
|
73
|
+
const { order, cycle } = topoSort(nodes, edges);
|
|
74
|
+
if (cycle.length > 0) {
|
|
75
|
+
errors.push({
|
|
76
|
+
id: cycle[0] ?? "(graph)",
|
|
77
|
+
message: `cyclic dependency: ${cycle.join(" → ")}`,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const plan = order.map((id) => {
|
|
81
|
+
const node = byId.get(id);
|
|
82
|
+
const hash = hashOf(node.spec);
|
|
83
|
+
const prior = input.state?.resources[id];
|
|
84
|
+
const change = prior === undefined ? "create" : prior.hash === hash ? "noop" : "update";
|
|
85
|
+
return { id, kind: node.kind, slug: node.slug, change, hash };
|
|
86
|
+
});
|
|
87
|
+
// Resources in state but no longer in code — candidates to prune. Appended
|
|
88
|
+
// after the apply order; the actual removal runs in dependency-safe order.
|
|
89
|
+
const deletes = [];
|
|
90
|
+
for (const [id, entry] of Object.entries(input.state?.resources ?? {})) {
|
|
91
|
+
if (byId.has(id))
|
|
92
|
+
continue;
|
|
93
|
+
const kind = id.slice(0, id.indexOf(":"));
|
|
94
|
+
const slug = id.slice(id.indexOf(":") + 1);
|
|
95
|
+
deletes.push({ id, kind, slug, change: "delete", hash: entry.hash });
|
|
96
|
+
}
|
|
97
|
+
deletes.sort((a, b) => a.id.localeCompare(b.id));
|
|
98
|
+
return { order, plan: [...plan, ...deletes], errors };
|
|
99
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Api } from "@cargo-ai/api";
|
|
2
|
+
export type DestroyResult = {
|
|
3
|
+
removed: string[];
|
|
4
|
+
released: string[];
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Remove the given state ids in dependency-safe order (dependents first),
|
|
8
|
+
* releasing adopted resources instead of deleting them, and persisting state
|
|
9
|
+
* after each. Shared by `destroy` (tear down all) and deploy's `--prune` (remove
|
|
10
|
+
* resources dropped from code).
|
|
11
|
+
*/
|
|
12
|
+
export declare function removeResources(root: string, api: Api, ids: string[]): Promise<DestroyResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Tear down state-tracked resources. With `target`, removes just that id;
|
|
15
|
+
* otherwise removes everything in state.
|
|
16
|
+
*/
|
|
17
|
+
export declare function destroy(root: string, api: Api, opts?: {
|
|
18
|
+
target?: string;
|
|
19
|
+
}): Promise<DestroyResult>;
|
|
20
|
+
//# sourceMappingURL=destroy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destroy.d.ts","sourceRoot":"","sources":["../../../src/deploy/destroy.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAMzC,MAAM,MAAM,aAAa,GAAG;IAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAkGtE;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,MAAM,EAAE,GACZ,OAAO,CAAC,aAAa,CAAC,CA4BxB;AAED;;;GAGG;AACH,wBAAsB,OAAO,CAC3B,IAAI,EAAE,MAAM,EACZ,GAAG,EAAE,GAAG,EACR,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAO,GAC7B,OAAO,CAAC,aAAa,CAAC,CA4BxB"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// destroy — tear down resources recorded in cargo.state.json (everything, or a
|
|
2
|
+
// single target id), deleting by uuid in dependency-safe order (dependents
|
|
3
|
+
// before their dependencies) and persisting state after each removal.
|
|
4
|
+
//
|
|
5
|
+
// Deletion is always by the stored uuid: play and agent have no slug, so the
|
|
6
|
+
// state entry is the only handle on them.
|
|
7
|
+
import { readState, writeState } from "./state.js";
|
|
8
|
+
// Coarse fallback order (dependents first) used as the tiebreak between
|
|
9
|
+
// resources with no recorded dependency between them, and for state written
|
|
10
|
+
// before per-resource deps were tracked.
|
|
11
|
+
const REMOVE_ORDER = [
|
|
12
|
+
"context",
|
|
13
|
+
"play",
|
|
14
|
+
"agent",
|
|
15
|
+
"mcpServer",
|
|
16
|
+
"tool",
|
|
17
|
+
"worker",
|
|
18
|
+
"app",
|
|
19
|
+
"file",
|
|
20
|
+
"model",
|
|
21
|
+
"connector",
|
|
22
|
+
"folder",
|
|
23
|
+
];
|
|
24
|
+
const KNOWN_KINDS = new Set(REMOVE_ORDER);
|
|
25
|
+
function kindOf(id) {
|
|
26
|
+
const colon = id.indexOf(":");
|
|
27
|
+
const kind = colon === -1 ? "" : id.slice(0, colon);
|
|
28
|
+
if (!KNOWN_KINDS.has(kind)) {
|
|
29
|
+
// A malformed or future-scheme state id would otherwise reach `remove`'s
|
|
30
|
+
// switch, match no case, no-op, and still get deleted from state — silently
|
|
31
|
+
// orphaning the live resource. Refuse loudly instead.
|
|
32
|
+
throw new Error(`cannot destroy "${id}": unrecognized resource kind "${kind}". Refusing ` +
|
|
33
|
+
`to drop state for an entry it can't delete.`);
|
|
34
|
+
}
|
|
35
|
+
return kind;
|
|
36
|
+
}
|
|
37
|
+
// Order the ids so a resource is removed before anything it depends on
|
|
38
|
+
// (dependents first), using the deps recorded in state. Within an independent
|
|
39
|
+
// group, REMOVE_ORDER breaks the tie. Cycles can't occur (the apply graph is a
|
|
40
|
+
// DAG); a stray self/missing edge is simply ignored.
|
|
41
|
+
function removalOrder(ids, state) {
|
|
42
|
+
const inSet = new Set(ids);
|
|
43
|
+
// We DFS post-order (dependencies-first) then reverse for dependents-first, so
|
|
44
|
+
// seed + recurse in REVERSE REMOVE_ORDER → ties come out in REMOVE_ORDER.
|
|
45
|
+
const reverseKind = (a, b) => REMOVE_ORDER.indexOf(kindOf(b)) - REMOVE_ORDER.indexOf(kindOf(a));
|
|
46
|
+
const seeds = [...ids].sort(reverseKind);
|
|
47
|
+
const visited = new Set();
|
|
48
|
+
const postorder = [];
|
|
49
|
+
const visit = (id) => {
|
|
50
|
+
if (visited.has(id))
|
|
51
|
+
return;
|
|
52
|
+
visited.add(id);
|
|
53
|
+
const deps = (state.resources[id]?.deps ?? [])
|
|
54
|
+
.filter((dep) => inSet.has(dep) && dep !== id)
|
|
55
|
+
.sort(reverseKind);
|
|
56
|
+
for (const dep of deps)
|
|
57
|
+
visit(dep);
|
|
58
|
+
postorder.push(id);
|
|
59
|
+
};
|
|
60
|
+
for (const id of seeds)
|
|
61
|
+
visit(id);
|
|
62
|
+
return postorder.reverse();
|
|
63
|
+
}
|
|
64
|
+
async function remove(api, kind, uuid) {
|
|
65
|
+
switch (kind) {
|
|
66
|
+
case "connector":
|
|
67
|
+
return api.connection.connector.remove(uuid);
|
|
68
|
+
case "model":
|
|
69
|
+
await api.storage.model.remove(uuid);
|
|
70
|
+
return;
|
|
71
|
+
case "play":
|
|
72
|
+
return api.orchestration.play.remove(uuid);
|
|
73
|
+
case "agent":
|
|
74
|
+
return api.ai.agent.remove(uuid);
|
|
75
|
+
case "tool":
|
|
76
|
+
return api.orchestration.tool.remove(uuid);
|
|
77
|
+
case "mcpServer":
|
|
78
|
+
return api.ai.mcpServer.remove(uuid);
|
|
79
|
+
case "folder":
|
|
80
|
+
return api.workspaceManagement.folder.remove(uuid);
|
|
81
|
+
case "file":
|
|
82
|
+
return api.content.file.remove(uuid);
|
|
83
|
+
case "worker":
|
|
84
|
+
return api.hosting.worker.remove(uuid);
|
|
85
|
+
case "app":
|
|
86
|
+
return api.hosting.app.remove(uuid);
|
|
87
|
+
case "context":
|
|
88
|
+
// The context repository is a per-workspace singleton with no delete API
|
|
89
|
+
// (and `defineContext` is additive) — destroy just drops it from state,
|
|
90
|
+
// never deleting repo files.
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Remove the given state ids in dependency-safe order (dependents first),
|
|
96
|
+
* releasing adopted resources instead of deleting them, and persisting state
|
|
97
|
+
* after each. Shared by `destroy` (tear down all) and deploy's `--prune` (remove
|
|
98
|
+
* resources dropped from code).
|
|
99
|
+
*/
|
|
100
|
+
export async function removeResources(root, api, ids) {
|
|
101
|
+
const state = readState(root);
|
|
102
|
+
if (state === undefined)
|
|
103
|
+
return { removed: [], released: [] };
|
|
104
|
+
const present = removalOrder(ids.filter((id) => id in state.resources), state);
|
|
105
|
+
const removed = [];
|
|
106
|
+
const released = [];
|
|
107
|
+
for (const id of present) {
|
|
108
|
+
const entry = state.resources[id];
|
|
109
|
+
if (entry === undefined)
|
|
110
|
+
continue;
|
|
111
|
+
if (entry.outputs["adopted"] === "true") {
|
|
112
|
+
// Adopted: the CDK linked an existing resource it didn't create. Release
|
|
113
|
+
// it (drop from state) without deleting the underlying Cargo resource.
|
|
114
|
+
delete state.resources[id];
|
|
115
|
+
writeState(root, state);
|
|
116
|
+
released.push(id);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
await remove(api, kindOf(id), entry.uuid);
|
|
120
|
+
delete state.resources[id];
|
|
121
|
+
writeState(root, state);
|
|
122
|
+
removed.push(id);
|
|
123
|
+
}
|
|
124
|
+
return { removed, released };
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Tear down state-tracked resources. With `target`, removes just that id;
|
|
128
|
+
* otherwise removes everything in state.
|
|
129
|
+
*/
|
|
130
|
+
export async function destroy(root, api, opts = {}) {
|
|
131
|
+
const state = readState(root);
|
|
132
|
+
if (state === undefined)
|
|
133
|
+
return { removed: [], released: [] };
|
|
134
|
+
// Targeting one resource must not leave dependents pointing at a deleted uuid.
|
|
135
|
+
// Refuse if any other state entry still depends on the target.
|
|
136
|
+
if (opts.target !== undefined && opts.target in state.resources) {
|
|
137
|
+
const target = opts.target;
|
|
138
|
+
const dependents = Object.entries(state.resources)
|
|
139
|
+
.filter(([id, e]) => id !== target && (e.deps ?? []).includes(target))
|
|
140
|
+
.map(([id]) => id);
|
|
141
|
+
if (dependents.length > 0) {
|
|
142
|
+
throw new Error(`cannot destroy "${target}": ${String(dependents.length)} resource(s) ` +
|
|
143
|
+
`still depend on it (${dependents.join(", ")}). Remove those first, ` +
|
|
144
|
+
`or destroy without --target.`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const ids = opts.target !== undefined
|
|
148
|
+
? opts.target in state.resources
|
|
149
|
+
? [opts.target]
|
|
150
|
+
: []
|
|
151
|
+
: Object.keys(state.resources);
|
|
152
|
+
return removeResources(root, api, ids);
|
|
153
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"executors.live.d.ts","sourceRoot":"","sources":["../../../src/deploy/executors.live.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAIzC,OAAO,KAAK,EAAE,SAAS,EAAW,MAAM,YAAY,CAAC;AA4BrD,wBAAgB,aAAa,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS,CAyjBjD"}
|