@sverka/sdk 0.0.0 → 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/dist/index.d.mts +398 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2216 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +38 -10
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2216 @@
|
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import path, { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { AgentStep, ComponentStep, CompositionError, CoreError, Pipeline, PipelineCallStep, PlanningError, ShellStep, canonicalStringify, computeGraphId, computePlanId, computePlanId as computePlanId$1, computeRunPlanId, expandPipelineCalls, matrix, parallel, pipeline, run, validateGraph, validatePlan, validatePlan as validatePlan$1, when, workflow, workflow as workflow$1 } from "@sverka/workflow";
|
|
6
|
+
import { CheckError, DEFAULT_POLICY, DEFAULT_POLICY as DEFAULT_POLICY$1, computeFingerprint, createBaseline, createBuiltinResolver, createBuiltinResolver as createBuiltinResolver$1, createPolicy, createPolicy as createPolicy$1, evaluatePolicy, evaluatePolicy as evaluatePolicy$1, extractFindings, extractFindings as extractFindings$1, filterOnlyNew, filterOnlyNew as filterOnlyNew$1, loadBaseline, loadBaseline as loadBaseline$1, normalizeSarif, saveBaseline, updateBaseline } from "@sverka/verification";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { rm } from "node:fs/promises";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
//#region src/planner/internal/git-cli.ts
|
|
11
|
+
/**
|
|
12
|
+
* Resolve the absolute path to the `git` executable by searching `PATH`
|
|
13
|
+
* at this trusted parent level. The spawned child receives a minimal
|
|
14
|
+
* environment without `PATH`, mitigating PATH-injection hotspots.
|
|
15
|
+
*/
|
|
16
|
+
function resolveGitBinary() {
|
|
17
|
+
const pathEnv = process.env.PATH ?? "";
|
|
18
|
+
for (const dir of pathEnv.split(path.delimiter)) {
|
|
19
|
+
if (!dir) continue;
|
|
20
|
+
const candidate = path.join(dir, "git");
|
|
21
|
+
if (existsSync(candidate)) return candidate;
|
|
22
|
+
}
|
|
23
|
+
return "git";
|
|
24
|
+
}
|
|
25
|
+
const GIT_BINARY = resolveGitBinary();
|
|
26
|
+
/**
|
|
27
|
+
* Default `GitCli` implementation backed by `node:child_process.spawn`.
|
|
28
|
+
*/
|
|
29
|
+
function createGitCli() {
|
|
30
|
+
return { run(args, cwd) {
|
|
31
|
+
return new Promise((resolvePromise, reject) => {
|
|
32
|
+
const env = {
|
|
33
|
+
HOME: process.env.HOME,
|
|
34
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
35
|
+
};
|
|
36
|
+
const child = spawn(GIT_BINARY, [...args], {
|
|
37
|
+
cwd,
|
|
38
|
+
env,
|
|
39
|
+
stdio: [
|
|
40
|
+
"ignore",
|
|
41
|
+
"pipe",
|
|
42
|
+
"pipe"
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
let stdout = "";
|
|
46
|
+
let stderr = "";
|
|
47
|
+
child.stdout?.on("data", (data) => {
|
|
48
|
+
stdout += data.toString();
|
|
49
|
+
});
|
|
50
|
+
child.stderr?.on("data", (data) => {
|
|
51
|
+
stderr += data.toString();
|
|
52
|
+
});
|
|
53
|
+
child.on("error", (err) => {
|
|
54
|
+
reject(err);
|
|
55
|
+
});
|
|
56
|
+
child.on("close", (code) => {
|
|
57
|
+
if (code === 0) resolvePromise(stdout);
|
|
58
|
+
else reject(new Error(`git ${args.join(" ")} exited ${code ?? -1}`, stderr.trim() ? { cause: stderr.trim() } : void 0));
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
} };
|
|
62
|
+
}
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/planner/detect.ts
|
|
65
|
+
const MANIFEST_FILES = /* @__PURE__ */ new Set([
|
|
66
|
+
"package.json",
|
|
67
|
+
"pyproject.toml",
|
|
68
|
+
"Cargo.toml",
|
|
69
|
+
"go.mod",
|
|
70
|
+
"pom.xml",
|
|
71
|
+
"build.gradle",
|
|
72
|
+
"composer.json"
|
|
73
|
+
]);
|
|
74
|
+
const LOCKFILE_MAP = {
|
|
75
|
+
"bun.lock": "bun",
|
|
76
|
+
"bun.lockb": "bun",
|
|
77
|
+
"package-lock.json": "npm",
|
|
78
|
+
"yarn.lock": "yarn",
|
|
79
|
+
"pnpm-lock.yaml": "pnpm",
|
|
80
|
+
"poetry.lock": "poetry",
|
|
81
|
+
"Cargo.lock": "cargo",
|
|
82
|
+
"go.sum": "go"
|
|
83
|
+
};
|
|
84
|
+
const MONOREPO_MARKER_FILES = {
|
|
85
|
+
"nx.json": "nx",
|
|
86
|
+
"turbo.json": "turborepo",
|
|
87
|
+
"lerna.json": "lerna",
|
|
88
|
+
"pnpm-workspace.yaml": "pnpm-workspace"
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* Detect local signals from a file list. Pure: no I/O.
|
|
92
|
+
*/
|
|
93
|
+
function detectSignals(files) {
|
|
94
|
+
const signals = [];
|
|
95
|
+
for (const file of files) {
|
|
96
|
+
const base = basename$1(file);
|
|
97
|
+
if (MANIFEST_FILES.has(base)) {
|
|
98
|
+
signals.push({
|
|
99
|
+
type: "manifest",
|
|
100
|
+
path: file,
|
|
101
|
+
detail: null,
|
|
102
|
+
confidence: 1
|
|
103
|
+
});
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (Object.hasOwn(LOCKFILE_MAP, base)) {
|
|
107
|
+
signals.push({
|
|
108
|
+
type: "lockfile",
|
|
109
|
+
path: file,
|
|
110
|
+
detail: null,
|
|
111
|
+
confidence: 1
|
|
112
|
+
});
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (base === "Dockerfile" || base.endsWith(".Dockerfile")) {
|
|
116
|
+
signals.push({
|
|
117
|
+
type: "dockerfile",
|
|
118
|
+
path: file,
|
|
119
|
+
detail: null,
|
|
120
|
+
confidence: 1
|
|
121
|
+
});
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (base === "docker-compose.yml" || base === "docker-compose.yaml") {
|
|
125
|
+
signals.push({
|
|
126
|
+
type: "docker-compose",
|
|
127
|
+
path: file,
|
|
128
|
+
detail: null,
|
|
129
|
+
confidence: 1
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (isCiDefinition(file, base)) {
|
|
134
|
+
signals.push({
|
|
135
|
+
type: "ci-definition",
|
|
136
|
+
path: file,
|
|
137
|
+
detail: null,
|
|
138
|
+
confidence: 1
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (Object.hasOwn(MONOREPO_MARKER_FILES, base)) signals.push({
|
|
143
|
+
type: "monorepo-marker",
|
|
144
|
+
path: file,
|
|
145
|
+
detail: null,
|
|
146
|
+
confidence: 1
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return signals;
|
|
150
|
+
}
|
|
151
|
+
function isCiDefinition(file, base) {
|
|
152
|
+
if (base === ".gitlab-ci.yml" || base === "azure-pipelines.yml" || base === "Jenkinsfile") return true;
|
|
153
|
+
if (file.startsWith(".github/workflows/") && (base.endsWith(".yml") || base.endsWith(".yaml"))) return true;
|
|
154
|
+
if (file.startsWith(".circleci/")) return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
const EXTENSION_MAP = {
|
|
158
|
+
".ts": "TypeScript",
|
|
159
|
+
".tsx": "TypeScript",
|
|
160
|
+
".js": "JavaScript",
|
|
161
|
+
".jsx": "JavaScript",
|
|
162
|
+
".mjs": "JavaScript",
|
|
163
|
+
".cjs": "JavaScript",
|
|
164
|
+
".py": "Python",
|
|
165
|
+
".rs": "Rust",
|
|
166
|
+
".go": "Go",
|
|
167
|
+
".java": "Java",
|
|
168
|
+
".kt": "Kotlin",
|
|
169
|
+
".rb": "Ruby",
|
|
170
|
+
".php": "PHP",
|
|
171
|
+
".cs": "C#",
|
|
172
|
+
".cpp": "C++",
|
|
173
|
+
".cc": "C++",
|
|
174
|
+
".cxx": "C++",
|
|
175
|
+
".c": "C",
|
|
176
|
+
".h": "C",
|
|
177
|
+
".hpp": "C++",
|
|
178
|
+
".swift": "Swift",
|
|
179
|
+
".scala": "Scala",
|
|
180
|
+
".sh": "Shell",
|
|
181
|
+
".bash": "Shell"
|
|
182
|
+
};
|
|
183
|
+
function detectLanguages(files) {
|
|
184
|
+
const counts = /* @__PURE__ */ new Map();
|
|
185
|
+
for (const file of files) {
|
|
186
|
+
const ext = extname(file);
|
|
187
|
+
const lang = EXTENSION_MAP[ext];
|
|
188
|
+
if (!lang) continue;
|
|
189
|
+
const entry = counts.get(lang);
|
|
190
|
+
if (entry) entry.push(ext);
|
|
191
|
+
else counts.set(lang, [ext]);
|
|
192
|
+
}
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const [name, exts] of counts) {
|
|
195
|
+
const fileCount = exts.length;
|
|
196
|
+
const confidence = Math.min(1, fileCount / 10);
|
|
197
|
+
out.push({
|
|
198
|
+
name,
|
|
199
|
+
confidence,
|
|
200
|
+
evidence: [...new Set(exts)],
|
|
201
|
+
fileCount
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
out.sort((a, b) => b.fileCount - a.fileCount || a.name.localeCompare(b.name));
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
const MANIFEST_PACKAGE_MANAGER = {
|
|
208
|
+
"go.mod": "go",
|
|
209
|
+
"Cargo.toml": "cargo"
|
|
210
|
+
};
|
|
211
|
+
function detectPackageManagers(signals, rootPkgJson) {
|
|
212
|
+
const byName = /* @__PURE__ */ new Map();
|
|
213
|
+
collectLockfilePackageManagers(signals, byName);
|
|
214
|
+
collectManifestPackageManagers(signals, byName);
|
|
215
|
+
applyPackageManagerField(rootPkgJson, byName);
|
|
216
|
+
return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
|
|
217
|
+
}
|
|
218
|
+
/** Populate `byName` from lockfile signals. */
|
|
219
|
+
function collectLockfilePackageManagers(signals, byName) {
|
|
220
|
+
for (const sig of signals) {
|
|
221
|
+
if (sig.type !== "lockfile") continue;
|
|
222
|
+
const base = basename$1(sig.path);
|
|
223
|
+
const name = LOCKFILE_MAP[base];
|
|
224
|
+
if (!name) {
|
|
225
|
+
const other = byName.get("other");
|
|
226
|
+
if (other) other.evidence.push(sig.path);
|
|
227
|
+
else byName.set("other", {
|
|
228
|
+
name: "other",
|
|
229
|
+
version: null,
|
|
230
|
+
lockfile: sig.path,
|
|
231
|
+
evidence: [sig.path]
|
|
232
|
+
});
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const existing = byName.get(name);
|
|
236
|
+
if (existing) {
|
|
237
|
+
if (!existing.evidence.includes(sig.path)) existing.evidence.push(sig.path);
|
|
238
|
+
} else byName.set(name, {
|
|
239
|
+
name,
|
|
240
|
+
version: null,
|
|
241
|
+
lockfile: sig.path,
|
|
242
|
+
evidence: [sig.path]
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** Populate `byName` from manifest signals for ecosystems without a lockfile or when the manifest is the primary signal. */
|
|
247
|
+
function collectManifestPackageManagers(signals, byName) {
|
|
248
|
+
for (const sig of signals) {
|
|
249
|
+
if (sig.type !== "manifest") continue;
|
|
250
|
+
const base = basename$1(sig.path);
|
|
251
|
+
const name = MANIFEST_PACKAGE_MANAGER[base];
|
|
252
|
+
if (!name || byName.has(name)) continue;
|
|
253
|
+
byName.set(name, {
|
|
254
|
+
name,
|
|
255
|
+
version: null,
|
|
256
|
+
lockfile: null,
|
|
257
|
+
evidence: [sig.path]
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/** Apply the `packageManager` field override from the root package.json. */
|
|
262
|
+
function applyPackageManagerField(rootPkgJson, byName) {
|
|
263
|
+
if (!rootPkgJson) return;
|
|
264
|
+
const pm = rootPkgJson["packageManager"];
|
|
265
|
+
if (typeof pm !== "string") return;
|
|
266
|
+
const { tool, version } = parsePackageManager(pm);
|
|
267
|
+
if (!tool) return;
|
|
268
|
+
const existing = byName.get(tool);
|
|
269
|
+
if (existing) {
|
|
270
|
+
existing.version = version;
|
|
271
|
+
if (!existing.evidence.includes("package.json#packageManager")) existing.evidence.push("package.json#packageManager");
|
|
272
|
+
} else byName.set(tool, {
|
|
273
|
+
name: tool,
|
|
274
|
+
version,
|
|
275
|
+
lockfile: null,
|
|
276
|
+
evidence: ["package.json#packageManager"]
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
function parsePackageManager(pm) {
|
|
280
|
+
const atIdx = pm.indexOf("@");
|
|
281
|
+
if (atIdx <= 0) return {
|
|
282
|
+
tool: null,
|
|
283
|
+
version: null
|
|
284
|
+
};
|
|
285
|
+
const toolName = pm.slice(0, atIdx);
|
|
286
|
+
const version = pm.slice(atIdx + 1) || null;
|
|
287
|
+
if (![
|
|
288
|
+
"npm",
|
|
289
|
+
"yarn",
|
|
290
|
+
"pnpm",
|
|
291
|
+
"bun",
|
|
292
|
+
"pip",
|
|
293
|
+
"poetry",
|
|
294
|
+
"uv",
|
|
295
|
+
"pipenv",
|
|
296
|
+
"cargo",
|
|
297
|
+
"go",
|
|
298
|
+
"maven",
|
|
299
|
+
"gradle",
|
|
300
|
+
"composer",
|
|
301
|
+
"other"
|
|
302
|
+
].includes(toolName)) return {
|
|
303
|
+
tool: null,
|
|
304
|
+
version
|
|
305
|
+
};
|
|
306
|
+
return {
|
|
307
|
+
tool: toolName,
|
|
308
|
+
version
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function detectMonorepo(signals, rootPkgJson) {
|
|
312
|
+
const globs = rootPkgJson ? extractWorkspaceGlobs(rootPkgJson["workspaces"]) : [];
|
|
313
|
+
const workspaces = resolveWorkspaceDirs(signals, globs);
|
|
314
|
+
for (const sig of signals) {
|
|
315
|
+
if (sig.type !== "monorepo-marker") continue;
|
|
316
|
+
const base = basename$1(sig.path);
|
|
317
|
+
const tool = MONOREPO_MARKER_FILES[base];
|
|
318
|
+
if (tool) return {
|
|
319
|
+
tool,
|
|
320
|
+
workspaces,
|
|
321
|
+
evidence: globs.length > 0 ? [sig.path, "package.json#workspaces"] : [sig.path]
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
if (globs.length > 0 && rootPkgJson) return {
|
|
325
|
+
tool: isBunWorkspace(rootPkgJson) ? "bun-workspace" : "custom",
|
|
326
|
+
workspaces,
|
|
327
|
+
evidence: ["package.json#workspaces"]
|
|
328
|
+
};
|
|
329
|
+
return null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Resolve workspace globs to concrete workspace dirs using manifest signals.
|
|
333
|
+
* A dir is a workspace when `<dir>/package.json` appears as a manifest and
|
|
334
|
+
* `<dir>` matches a workspace glob (`*` = one path segment, `**` = any depth).
|
|
335
|
+
*/
|
|
336
|
+
function resolveWorkspaceDirs(signals, globs) {
|
|
337
|
+
const positive = globs.filter((g) => !g.startsWith("!"));
|
|
338
|
+
const negative = globs.filter((g) => g.startsWith("!")).map((g) => g.slice(1));
|
|
339
|
+
const dirs = /* @__PURE__ */ new Set();
|
|
340
|
+
for (const sig of signals) {
|
|
341
|
+
if (sig.type !== "manifest" || !sig.path.endsWith("/package.json")) continue;
|
|
342
|
+
const dir = sig.path.slice(0, sig.path.length - 13);
|
|
343
|
+
if (positive.some((g) => matchesWorkspaceGlob(dir, g)) && !negative.some((g) => matchesWorkspaceGlob(dir, g))) dirs.add(dir);
|
|
344
|
+
}
|
|
345
|
+
return [...dirs].sort((a, b) => a.localeCompare(b));
|
|
346
|
+
}
|
|
347
|
+
function matchesWorkspaceGlob(dir, glob) {
|
|
348
|
+
const d = dir.split("/");
|
|
349
|
+
const g = glob.split("/");
|
|
350
|
+
const memo = /* @__PURE__ */ new Map();
|
|
351
|
+
const match = (di, gi) => {
|
|
352
|
+
const key = `${di}:${gi}`;
|
|
353
|
+
const cached = memo.get(key);
|
|
354
|
+
if (cached !== void 0) return cached;
|
|
355
|
+
let result;
|
|
356
|
+
if (gi === g.length) result = di === d.length;
|
|
357
|
+
else if (g[gi] === "**") {
|
|
358
|
+
result = false;
|
|
359
|
+
for (let k = di; k <= d.length && !result; k++) result = match(k, gi + 1);
|
|
360
|
+
} else result = di < d.length && matchesGlobSegment(d[di], g[gi]) && match(di + 1, gi + 1);
|
|
361
|
+
memo.set(key, result);
|
|
362
|
+
return result;
|
|
363
|
+
};
|
|
364
|
+
return match(0, 0);
|
|
365
|
+
}
|
|
366
|
+
function matchesGlobSegment(segment, pattern) {
|
|
367
|
+
let s = 0;
|
|
368
|
+
let p = 0;
|
|
369
|
+
let starP = -1;
|
|
370
|
+
let starS = -1;
|
|
371
|
+
while (s < segment.length) if (p < pattern.length && (pattern[p] === "?" || pattern[p] === segment[s])) {
|
|
372
|
+
s++;
|
|
373
|
+
p++;
|
|
374
|
+
} else if (p < pattern.length && pattern[p] === "*") {
|
|
375
|
+
starP = p++;
|
|
376
|
+
starS = s;
|
|
377
|
+
} else if (starP !== -1) {
|
|
378
|
+
p = starP + 1;
|
|
379
|
+
s = ++starS;
|
|
380
|
+
} else return false;
|
|
381
|
+
while (p < pattern.length && pattern[p] === "*") p++;
|
|
382
|
+
return p === pattern.length;
|
|
383
|
+
}
|
|
384
|
+
function extractWorkspaceGlobs(ws) {
|
|
385
|
+
if (Array.isArray(ws)) return ws.filter((g) => typeof g === "string");
|
|
386
|
+
if (typeof ws === "object" && ws !== null) {
|
|
387
|
+
const packages = ws["packages"];
|
|
388
|
+
if (Array.isArray(packages)) return packages.filter((g) => typeof g === "string");
|
|
389
|
+
}
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
392
|
+
function isBunWorkspace(pkgJson) {
|
|
393
|
+
const pm = pkgJson["packageManager"];
|
|
394
|
+
return typeof pm === "string" && pm.startsWith("bun");
|
|
395
|
+
}
|
|
396
|
+
function basename$1(path) {
|
|
397
|
+
const idx = path.lastIndexOf("/");
|
|
398
|
+
return idx >= 0 ? path.slice(idx + 1) : path;
|
|
399
|
+
}
|
|
400
|
+
function extname(path) {
|
|
401
|
+
const base = basename$1(path);
|
|
402
|
+
const idx = base.lastIndexOf(".");
|
|
403
|
+
return idx >= 0 ? base.slice(idx) : "";
|
|
404
|
+
}
|
|
405
|
+
//#endregion
|
|
406
|
+
//#region src/planner/explain.ts
|
|
407
|
+
const SIGNAL_TYPES = [
|
|
408
|
+
"manifest",
|
|
409
|
+
"lockfile",
|
|
410
|
+
"dockerfile",
|
|
411
|
+
"docker-compose",
|
|
412
|
+
"ci-definition",
|
|
413
|
+
"monorepo-marker",
|
|
414
|
+
"git-metadata"
|
|
415
|
+
];
|
|
416
|
+
/**
|
|
417
|
+
* Build a `DiscoveryExplanation` from the collected signals. Pure.
|
|
418
|
+
*/
|
|
419
|
+
function buildExplanation(signals, opts) {
|
|
420
|
+
const counts = {};
|
|
421
|
+
for (const t of SIGNAL_TYPES) counts[t] = 0;
|
|
422
|
+
for (const sig of signals) counts[sig.type] = (counts[sig.type] ?? 0) + 1;
|
|
423
|
+
const parts = [];
|
|
424
|
+
const total = signals.length;
|
|
425
|
+
parts.push(`${total} signal${total === 1 ? "" : "s"}`);
|
|
426
|
+
const nonzero = SIGNAL_TYPES.filter((t) => counts[t] > 0);
|
|
427
|
+
if (nonzero.length) parts.push(`(${formatSignalCounts(nonzero, counts)})`);
|
|
428
|
+
if (opts.dirty) parts.push("working tree dirty");
|
|
429
|
+
else parts.push("working tree clean");
|
|
430
|
+
if (!opts.hasBaseRef) parts.push("no baseRef provided");
|
|
431
|
+
return {
|
|
432
|
+
summary: parts.join(" "),
|
|
433
|
+
signalCounts: counts
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
/** Format non-zero signal counts as "type=count, type=count". */
|
|
437
|
+
function formatSignalCounts(types, counts) {
|
|
438
|
+
return types.map((t) => t + "=" + counts[t]).join(", ");
|
|
439
|
+
}
|
|
440
|
+
//#endregion
|
|
441
|
+
//#region src/planner/errors.ts
|
|
442
|
+
/**
|
|
443
|
+
* Base error class for the planner package. All discovery failures throw a
|
|
444
|
+
* `DiscoveryError` with one of the four `DiscoveryErrorCode` values.
|
|
445
|
+
*/
|
|
446
|
+
var DiscoveryError = class extends Error {
|
|
447
|
+
code;
|
|
448
|
+
cause;
|
|
449
|
+
constructor(message, code, cause) {
|
|
450
|
+
super(message);
|
|
451
|
+
this.name = "DiscoveryError";
|
|
452
|
+
this.code = code;
|
|
453
|
+
this.cause = cause;
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
/**
|
|
457
|
+
* Planner error for Run Plan binding failures. Spec 13 — Error handling.
|
|
458
|
+
*/
|
|
459
|
+
var PlannerError = class extends Error {
|
|
460
|
+
code;
|
|
461
|
+
cause;
|
|
462
|
+
constructor(message, code, cause) {
|
|
463
|
+
super(message);
|
|
464
|
+
this.name = "PlannerError";
|
|
465
|
+
this.code = code;
|
|
466
|
+
this.cause = cause;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
//#endregion
|
|
470
|
+
//#region src/planner/bind.ts
|
|
471
|
+
const INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
472
|
+
"string",
|
|
473
|
+
"number",
|
|
474
|
+
"boolean"
|
|
475
|
+
]);
|
|
476
|
+
/**
|
|
477
|
+
* Bind a Definition Graph's Entry + user inputs into a concrete RunPlan
|
|
478
|
+
* that the native engine can execute.
|
|
479
|
+
*/
|
|
480
|
+
function bindRunPlan(options) {
|
|
481
|
+
const { graph, entryId, inputs } = options;
|
|
482
|
+
validateGraphShape(graph);
|
|
483
|
+
if (graph.project.pipelines.length === 0) throw new PlannerError("graph has no pipelines", "INVALID_GRAPH");
|
|
484
|
+
const { pipeline, entry } = findEntry(graph, entryId);
|
|
485
|
+
validateRoots(pipeline, entry);
|
|
486
|
+
validatePipeline(graph, pipeline);
|
|
487
|
+
const reachableSteps = computeReachableSteps(pipeline.steps, entry.roots);
|
|
488
|
+
const expandedSteps = expandPipelineCalls(graph, reachableSteps);
|
|
489
|
+
const boundInputs = bindInputs(pipeline.inputs, inputs);
|
|
490
|
+
const graphId = computeGraphId(graph);
|
|
491
|
+
const boundEntry = {
|
|
492
|
+
id: entry.id,
|
|
493
|
+
trigger: entry.trigger
|
|
494
|
+
};
|
|
495
|
+
return {
|
|
496
|
+
apiVersion: "sverka.dev/v1run",
|
|
497
|
+
id: computeRunPlanId({
|
|
498
|
+
apiVersion: "sverka.dev/v1run",
|
|
499
|
+
graphId,
|
|
500
|
+
entry: boundEntry,
|
|
501
|
+
inputs: boundInputs,
|
|
502
|
+
steps: expandedSteps
|
|
503
|
+
}),
|
|
504
|
+
graphId,
|
|
505
|
+
entry: boundEntry,
|
|
506
|
+
inputs: boundInputs,
|
|
507
|
+
steps: expandedSteps,
|
|
508
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Validate that the runtime value has the shape of a DefinitionGraph before
|
|
513
|
+
* any field is dereferenced. Malformed values become PlannerError(INVALID_GRAPH)
|
|
514
|
+
* instead of TypeError.
|
|
515
|
+
*/
|
|
516
|
+
function validateGraphShape(value) {
|
|
517
|
+
const graph = value;
|
|
518
|
+
if (typeof graph !== "object" || graph === null) throw new PlannerError("graph must be an object", "INVALID_GRAPH");
|
|
519
|
+
const project = graph.project;
|
|
520
|
+
if (typeof project !== "object" || project === null) throw new PlannerError("graph.project must be an object", "INVALID_GRAPH");
|
|
521
|
+
const pipelines = project.pipelines;
|
|
522
|
+
if (!Array.isArray(pipelines)) throw new PlannerError("graph.project.pipelines must be an array", "INVALID_GRAPH");
|
|
523
|
+
for (let i = 0; i < pipelines.length; i++) {
|
|
524
|
+
const pipeline = pipelines[i];
|
|
525
|
+
validatePipelineShape(pipeline, i);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function validatePipelineShape(value, index) {
|
|
529
|
+
const pipeline = value;
|
|
530
|
+
if (typeof pipeline !== "object" || pipeline === null) throw new PlannerError(`graph.project.pipelines[${index}] must be an object`, "INVALID_GRAPH");
|
|
531
|
+
validatePipelineFields(pipeline, index);
|
|
532
|
+
validatePipelineChildren(pipeline, index);
|
|
533
|
+
}
|
|
534
|
+
function validatePipelineFields(pipeline, index) {
|
|
535
|
+
if (typeof pipeline.id !== "string") throw new PlannerError(`graph.project.pipelines[${index}].id must be a string`, "INVALID_GRAPH");
|
|
536
|
+
if (!Array.isArray(pipeline.entries)) throw new PlannerError(`graph.project.pipelines[${index}].entries must be an array`, "INVALID_GRAPH");
|
|
537
|
+
if (!Array.isArray(pipeline.steps)) throw new PlannerError(`graph.project.pipelines[${index}].steps must be an array`, "INVALID_GRAPH");
|
|
538
|
+
if (typeof pipeline.inputs !== "object" || pipeline.inputs === null || Array.isArray(pipeline.inputs)) throw new PlannerError(`graph.project.pipelines[${index}].inputs must be an object`, "INVALID_GRAPH");
|
|
539
|
+
}
|
|
540
|
+
function validatePipelineChildren(pipeline, index) {
|
|
541
|
+
const inputs = pipeline.inputs;
|
|
542
|
+
for (const [name, descriptor] of Object.entries(inputs)) validateInputDescriptor(descriptor, index, name);
|
|
543
|
+
const entries = pipeline.entries;
|
|
544
|
+
if (!Array.isArray(entries)) return;
|
|
545
|
+
const steps = pipeline.steps;
|
|
546
|
+
if (!Array.isArray(steps)) return;
|
|
547
|
+
for (let j = 0; j < entries.length; j++) validateEntryShape(entries[j], index, j);
|
|
548
|
+
for (let j = 0; j < steps.length; j++) validateStepShape(steps[j], index, j);
|
|
549
|
+
}
|
|
550
|
+
function validateInputDescriptor(value, pipelineIndex, name) {
|
|
551
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].inputs.${name} must be an object`, "INVALID_GRAPH");
|
|
552
|
+
const type = value.type;
|
|
553
|
+
if (type !== void 0 && !INPUT_TYPES.has(type)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].inputs.${name} has invalid type`, "INVALID_GRAPH");
|
|
554
|
+
}
|
|
555
|
+
function validateEntryShape(value, pipelineIndex, index) {
|
|
556
|
+
const entry = value;
|
|
557
|
+
if (typeof entry !== "object" || entry === null) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].entries[${index}] must be an object`, "INVALID_GRAPH");
|
|
558
|
+
if (typeof entry.id !== "string") throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].entries[${index}].id must be a string`, "INVALID_GRAPH");
|
|
559
|
+
if (!Array.isArray(entry.roots)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].entries[${index}].roots must be an array`, "INVALID_GRAPH");
|
|
560
|
+
}
|
|
561
|
+
function validateStepShape(value, pipelineIndex, index) {
|
|
562
|
+
const step = value;
|
|
563
|
+
if (typeof step !== "object" || step === null) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].steps[${index}] must be an object`, "INVALID_GRAPH");
|
|
564
|
+
if (typeof step.id !== "string") throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].steps[${index}].id must be a string`, "INVALID_GRAPH");
|
|
565
|
+
if (!Array.isArray(step.dependencies)) throw new PlannerError(`graph.project.pipelines[${pipelineIndex}].steps[${index}].dependencies must be an array`, "INVALID_GRAPH");
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
568
|
+
* Compute the transitive closure of reachable steps from the given roots.
|
|
569
|
+
* A step is reachable if it is connected to a root by following
|
|
570
|
+
* `dependencies[].producer` edges backward (i.e. the root's prerequisites).
|
|
571
|
+
* Returns steps in the original graph order, filtered to reachable steps.
|
|
572
|
+
*/
|
|
573
|
+
function computeReachableSteps(steps, roots) {
|
|
574
|
+
const producerMap = buildProducerMap(steps);
|
|
575
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
576
|
+
const queue = [];
|
|
577
|
+
for (const id of roots) if (!reachable.has(id)) {
|
|
578
|
+
reachable.add(id);
|
|
579
|
+
queue.push(id);
|
|
580
|
+
}
|
|
581
|
+
let head = 0;
|
|
582
|
+
while (head < queue.length) {
|
|
583
|
+
const id = queue[head];
|
|
584
|
+
head++;
|
|
585
|
+
for (const producer of producerMap.get(id) ?? []) if (!reachable.has(producer)) {
|
|
586
|
+
reachable.add(producer);
|
|
587
|
+
queue.push(producer);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return steps.filter((s) => reachable.has(s.id));
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Build a map from each step id to the list of producer step ids it depends on.
|
|
594
|
+
*/
|
|
595
|
+
function buildProducerMap(steps) {
|
|
596
|
+
const map = /* @__PURE__ */ new Map();
|
|
597
|
+
for (const step of steps) {
|
|
598
|
+
const producers = [];
|
|
599
|
+
map.set(step.id, producers);
|
|
600
|
+
for (const dep of step.dependencies) producers.push(dep.producer);
|
|
601
|
+
}
|
|
602
|
+
return map;
|
|
603
|
+
}
|
|
604
|
+
function findEntry(graph, entryId) {
|
|
605
|
+
for (const pipeline of graph.project.pipelines) {
|
|
606
|
+
const entry = pipeline.entries.find((e) => e.id === entryId);
|
|
607
|
+
if (entry) return {
|
|
608
|
+
pipeline,
|
|
609
|
+
entry
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
throw new PlannerError(`entry "${entryId}" not found`, "ENTRY_NOT_FOUND");
|
|
613
|
+
}
|
|
614
|
+
function validateRoots(pipeline, entry) {
|
|
615
|
+
const stepIds = new Set(pipeline.steps.map((s) => s.id));
|
|
616
|
+
for (const root of entry.roots) if (!stepIds.has(root)) throw new PlannerError(`entry root "${root}" not found in pipeline steps`, "ROOT_NOT_FOUND");
|
|
617
|
+
}
|
|
618
|
+
function validatePipeline(graph, pipeline) {
|
|
619
|
+
try {
|
|
620
|
+
validateGraph({ project: {
|
|
621
|
+
id: graph.project.id,
|
|
622
|
+
pipelines: [pipeline]
|
|
623
|
+
} });
|
|
624
|
+
} catch (err) {
|
|
625
|
+
throw new PlannerError(err instanceof Error ? err.message : String(err), "INVALID_GRAPH", err);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function bindInputs(pipelineInputs, userInputs) {
|
|
629
|
+
const result = {};
|
|
630
|
+
for (const [name, input] of Object.entries(pipelineInputs)) {
|
|
631
|
+
const type = input.type;
|
|
632
|
+
validateInputType(name, type);
|
|
633
|
+
if (input.secret) continue;
|
|
634
|
+
const userValue = userInputs?.[name];
|
|
635
|
+
if (userValue !== void 0) {
|
|
636
|
+
assertInputValue(userValue, type, name);
|
|
637
|
+
result[name] = userValue;
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if (input.default !== void 0) {
|
|
641
|
+
assertInputValue(input.default, type, name);
|
|
642
|
+
result[name] = input.default;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (input.required ?? true) throw new PlannerError(`required input "${name}" has no value and no default`, "MISSING_INPUT");
|
|
646
|
+
}
|
|
647
|
+
return result;
|
|
648
|
+
}
|
|
649
|
+
function validateInputType(name, type) {
|
|
650
|
+
if (type === void 0 || !INPUT_TYPES.has(type)) throw new PlannerError(`input "${name}" has no valid declared type`, "INVALID_GRAPH");
|
|
651
|
+
}
|
|
652
|
+
function assertInputValue(value, type, name) {
|
|
653
|
+
if (type === "array") {
|
|
654
|
+
if (!Array.isArray(value) || !value.every((item) => typeof item === "string")) throw new PlannerError(`input "${name}" value ${JSON.stringify(value)} does not match declared type "array" (expected string array)`, "INVALID_INPUT");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
if (typeof value !== (type === "choice" ? "string" : type)) throw new PlannerError(`input "${name}" value ${JSON.stringify(value)} does not match declared type "${type}"`, "INVALID_INPUT");
|
|
658
|
+
}
|
|
659
|
+
//#endregion
|
|
660
|
+
//#region src/planner/planner.ts
|
|
661
|
+
function createPlanner() {
|
|
662
|
+
return new PlannerImpl();
|
|
663
|
+
}
|
|
664
|
+
const DEFAULT_MAX_DEPTH = 10;
|
|
665
|
+
var PlannerImpl = class {
|
|
666
|
+
git;
|
|
667
|
+
constructor() {
|
|
668
|
+
this.git = createGitCli();
|
|
669
|
+
}
|
|
670
|
+
async discover(options) {
|
|
671
|
+
const root = options.root;
|
|
672
|
+
if (!existsSync(root)) throw new DiscoveryError(`root directory not found: ${root}`, "ROOT_NOT_FOUND");
|
|
673
|
+
await assertGitAvailable(this.git, root);
|
|
674
|
+
const toplevel = await resolveToplevel(this.git, root);
|
|
675
|
+
const { tracked, untracked, porcelain } = await collectGitFiles(this.git, toplevel);
|
|
676
|
+
const allFiles = [.../* @__PURE__ */ new Set([...tracked, ...untracked])];
|
|
677
|
+
const scoped = options.scopeToRoot === true ? scopeFilesToRoot(allFiles, toplevel, root) : null;
|
|
678
|
+
const projectRoot = scoped?.dir ?? toplevel;
|
|
679
|
+
const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
680
|
+
const filtered = applyMaxDepth(scoped?.files ?? allFiles, maxDepth);
|
|
681
|
+
const signals = detectSignals(filtered);
|
|
682
|
+
const languages = detectLanguages(filtered);
|
|
683
|
+
const rootPkgJson = readRootPackageJson(projectRoot);
|
|
684
|
+
const packageManagers = detectPackageManagers(signals, rootPkgJson);
|
|
685
|
+
const monorepo = detectMonorepo(signals, rootPkgJson);
|
|
686
|
+
const hasContainerBuild = signals.some((s) => s.type === "dockerfile" || s.type === "docker-compose");
|
|
687
|
+
const hasCiDefinition = signals.some((s) => s.type === "ci-definition");
|
|
688
|
+
const commit = await resolveHeadCommit(this.git, toplevel);
|
|
689
|
+
const dirty = porcelain.length > 0;
|
|
690
|
+
const changedFiles = await collectChangedFiles(this.git, toplevel, options.baseRef);
|
|
691
|
+
const gitSignal = {
|
|
692
|
+
type: "git-metadata",
|
|
693
|
+
path: ".git",
|
|
694
|
+
detail: commit ? `HEAD=${commit.slice(0, 12)}` : "no commits",
|
|
695
|
+
confidence: 1
|
|
696
|
+
};
|
|
697
|
+
const allSignals = [...signals, gitSignal];
|
|
698
|
+
return {
|
|
699
|
+
root: projectRoot,
|
|
700
|
+
commit,
|
|
701
|
+
dirty,
|
|
702
|
+
changedFiles,
|
|
703
|
+
languages,
|
|
704
|
+
packageManagers,
|
|
705
|
+
hasContainerBuild,
|
|
706
|
+
hasCiDefinition,
|
|
707
|
+
monorepo,
|
|
708
|
+
localSignals: allSignals,
|
|
709
|
+
explanation: buildExplanation(allSignals, {
|
|
710
|
+
hasBaseRef: Boolean(options.baseRef),
|
|
711
|
+
dirty
|
|
712
|
+
})
|
|
713
|
+
};
|
|
714
|
+
}
|
|
715
|
+
async plan(context) {
|
|
716
|
+
return synthesizePlan(context);
|
|
717
|
+
}
|
|
718
|
+
bindRunPlan(options) {
|
|
719
|
+
return bindRunPlan(options);
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
function applyMaxDepth(files, maxDepth) {
|
|
723
|
+
return files.filter((f) => f.split("/").length <= maxDepth);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Restrict repo-relative `files` to the subtree under `root`, re-rooting
|
|
727
|
+
* paths relative to `root`. Returns null when `root` is not inside the
|
|
728
|
+
* repository (should not happen — toplevel is resolved from `root`).
|
|
729
|
+
*/
|
|
730
|
+
function scopeFilesToRoot(files, toplevel, root) {
|
|
731
|
+
const dir = realpathSync(resolve(root));
|
|
732
|
+
const prefix = relative(toplevel, dir).split(sep).join("/");
|
|
733
|
+
if (prefix.startsWith("..") || isAbsolute(prefix)) return null;
|
|
734
|
+
if (prefix === "") return {
|
|
735
|
+
dir,
|
|
736
|
+
files: [...files]
|
|
737
|
+
};
|
|
738
|
+
return {
|
|
739
|
+
dir,
|
|
740
|
+
files: files.filter((f) => f.startsWith(prefix + "/")).map((f) => f.slice(prefix.length + 1))
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
/** Throw `GIT_UNAVAILABLE` if git is not on PATH. */
|
|
744
|
+
async function assertGitAvailable(git, root) {
|
|
745
|
+
try {
|
|
746
|
+
await git.run(["--version"], root);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
throw new DiscoveryError("git is not installed or not on PATH", "GIT_UNAVAILABLE", err);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
/** Resolve the repository toplevel, throwing `GIT_NOT_A_REPO` on failure. */
|
|
752
|
+
async function resolveToplevel(git, root) {
|
|
753
|
+
try {
|
|
754
|
+
return (await git.run(["rev-parse", "--show-toplevel"], root)).trim();
|
|
755
|
+
} catch (err) {
|
|
756
|
+
throw new DiscoveryError(`not a git repository: ${root}`, "GIT_NOT_A_REPO", err);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
/** Collect tracked files, untracked files, and porcelain lines from git. */
|
|
760
|
+
async function collectGitFiles(git, toplevel) {
|
|
761
|
+
let trackedRaw;
|
|
762
|
+
let porcelainRaw;
|
|
763
|
+
try {
|
|
764
|
+
trackedRaw = await git.run(["ls-files"], toplevel);
|
|
765
|
+
porcelainRaw = await git.run([
|
|
766
|
+
"status",
|
|
767
|
+
"--porcelain",
|
|
768
|
+
"-uall"
|
|
769
|
+
], toplevel);
|
|
770
|
+
} catch (err) {
|
|
771
|
+
throw new DiscoveryError("filesystem traversal failed", "TRAVERSAL_FAILED", err);
|
|
772
|
+
}
|
|
773
|
+
const tracked = trackedRaw.split("\n").filter(Boolean);
|
|
774
|
+
const porcelain = porcelainRaw.split("\n").filter(Boolean);
|
|
775
|
+
return {
|
|
776
|
+
tracked,
|
|
777
|
+
untracked: extractUntracked(porcelain),
|
|
778
|
+
porcelain
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
/** Extract untracked file paths from porcelain status lines. */
|
|
782
|
+
function extractUntracked(porcelain) {
|
|
783
|
+
const untracked = [];
|
|
784
|
+
for (const line of porcelain) {
|
|
785
|
+
const status = line.slice(0, 2);
|
|
786
|
+
const path = line.slice(3).trim();
|
|
787
|
+
if (status.includes("??")) untracked.push(path);
|
|
788
|
+
}
|
|
789
|
+
return untracked;
|
|
790
|
+
}
|
|
791
|
+
/** Resolve the HEAD commit SHA, returning "" on failure. */
|
|
792
|
+
async function resolveHeadCommit(git, toplevel) {
|
|
793
|
+
try {
|
|
794
|
+
return (await git.run(["rev-parse", "HEAD"], toplevel)).trim();
|
|
795
|
+
} catch {
|
|
796
|
+
return "";
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
const REF_RE = /^[A-Za-z0-9_./~^\-@{}]+$/;
|
|
800
|
+
function validateBaseRef(baseRef) {
|
|
801
|
+
if (baseRef.startsWith("-") || !REF_RE.test(baseRef)) throw new DiscoveryError(`invalid baseRef: ${baseRef}`, "INVALID_BASE_REF");
|
|
802
|
+
}
|
|
803
|
+
/** Collect changed files against `baseRef` when provided. */
|
|
804
|
+
async function collectChangedFiles(git, toplevel, baseRef) {
|
|
805
|
+
if (!baseRef) return [];
|
|
806
|
+
validateBaseRef(baseRef);
|
|
807
|
+
try {
|
|
808
|
+
return parseDiffNameStatus(await git.run([
|
|
809
|
+
"diff",
|
|
810
|
+
"--name-status",
|
|
811
|
+
`${baseRef}...HEAD`,
|
|
812
|
+
"--"
|
|
813
|
+
], toplevel));
|
|
814
|
+
} catch (err) {
|
|
815
|
+
throw new DiscoveryError(`failed to diff against baseRef "${baseRef}"`, "TRAVERSAL_FAILED", err);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
function readRootPackageJson(root) {
|
|
819
|
+
try {
|
|
820
|
+
const raw = readFileSync(`${root}/package.json`, "utf8");
|
|
821
|
+
return JSON.parse(raw);
|
|
822
|
+
} catch {
|
|
823
|
+
return null;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function parseDiffNameStatus(diff) {
|
|
827
|
+
const out = [];
|
|
828
|
+
for (const line of diff.split("\n").filter(Boolean)) {
|
|
829
|
+
const parts = line.split(" ");
|
|
830
|
+
const code = parts[0] ?? "";
|
|
831
|
+
const path = code.startsWith("R") ? parts[2] ?? "" : parts[1] ?? "";
|
|
832
|
+
if (!path) continue;
|
|
833
|
+
out.push({
|
|
834
|
+
path,
|
|
835
|
+
status: diffStatusCode(code)
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return out;
|
|
839
|
+
}
|
|
840
|
+
/** Map a git diff --name-status code to a `ChangedFile` status. */
|
|
841
|
+
function diffStatusCode(code) {
|
|
842
|
+
if (code.startsWith("A")) return "added";
|
|
843
|
+
if (code.startsWith("M")) return "modified";
|
|
844
|
+
if (code.startsWith("D")) return "deleted";
|
|
845
|
+
if (code.startsWith("R")) return "renamed";
|
|
846
|
+
return "modified";
|
|
847
|
+
}
|
|
848
|
+
const PLAN_DRIVERS = [
|
|
849
|
+
{
|
|
850
|
+
checkId: "typecheck",
|
|
851
|
+
reason: "Node project defaults",
|
|
852
|
+
languages: ["TypeScript", "JavaScript"],
|
|
853
|
+
packageManagers: [
|
|
854
|
+
"npm",
|
|
855
|
+
"yarn",
|
|
856
|
+
"pnpm",
|
|
857
|
+
"bun"
|
|
858
|
+
]
|
|
859
|
+
},
|
|
860
|
+
{
|
|
861
|
+
checkId: "lint",
|
|
862
|
+
reason: "Node project defaults",
|
|
863
|
+
languages: ["TypeScript", "JavaScript"],
|
|
864
|
+
packageManagers: [
|
|
865
|
+
"npm",
|
|
866
|
+
"yarn",
|
|
867
|
+
"pnpm",
|
|
868
|
+
"bun"
|
|
869
|
+
]
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
checkId: "test",
|
|
873
|
+
reason: "Node project defaults",
|
|
874
|
+
languages: ["TypeScript", "JavaScript"],
|
|
875
|
+
packageManagers: [
|
|
876
|
+
"npm",
|
|
877
|
+
"yarn",
|
|
878
|
+
"pnpm",
|
|
879
|
+
"bun"
|
|
880
|
+
]
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
checkId: "lint",
|
|
884
|
+
reason: "Python project defaults",
|
|
885
|
+
languages: ["Python"],
|
|
886
|
+
packageManagers: [
|
|
887
|
+
"pip",
|
|
888
|
+
"poetry",
|
|
889
|
+
"uv",
|
|
890
|
+
"pipenv"
|
|
891
|
+
]
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
checkId: "test",
|
|
895
|
+
reason: "Python project defaults",
|
|
896
|
+
languages: ["Python"],
|
|
897
|
+
packageManagers: [
|
|
898
|
+
"pip",
|
|
899
|
+
"poetry",
|
|
900
|
+
"uv",
|
|
901
|
+
"pipenv"
|
|
902
|
+
]
|
|
903
|
+
},
|
|
904
|
+
{
|
|
905
|
+
checkId: "fmt-check",
|
|
906
|
+
reason: "Rust project defaults",
|
|
907
|
+
languages: ["Rust"],
|
|
908
|
+
packageManagers: ["cargo"]
|
|
909
|
+
},
|
|
910
|
+
{
|
|
911
|
+
checkId: "clippy",
|
|
912
|
+
reason: "Rust project defaults",
|
|
913
|
+
languages: ["Rust"],
|
|
914
|
+
packageManagers: ["cargo"]
|
|
915
|
+
},
|
|
916
|
+
{
|
|
917
|
+
checkId: "test",
|
|
918
|
+
reason: "Rust project defaults",
|
|
919
|
+
languages: ["Rust"],
|
|
920
|
+
packageManagers: ["cargo"]
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
checkId: "vet",
|
|
924
|
+
reason: "Go project defaults",
|
|
925
|
+
languages: ["Go"],
|
|
926
|
+
packageManagers: ["go"]
|
|
927
|
+
},
|
|
928
|
+
{
|
|
929
|
+
checkId: "test",
|
|
930
|
+
reason: "Go project defaults",
|
|
931
|
+
languages: ["Go"],
|
|
932
|
+
packageManagers: ["go"]
|
|
933
|
+
}
|
|
934
|
+
];
|
|
935
|
+
function synthesizePlan(context) {
|
|
936
|
+
const langNames = context.languages.map((l) => l.name);
|
|
937
|
+
const pmNames = context.packageManagers.map((p) => p.name);
|
|
938
|
+
const signalRef = resolveSignalRef(context.localSignals);
|
|
939
|
+
const checks = collectDefaultChecks(PLAN_DRIVERS, langNames, pmNames, signalRef);
|
|
940
|
+
return {
|
|
941
|
+
context,
|
|
942
|
+
checks,
|
|
943
|
+
workflowPath: null,
|
|
944
|
+
notes: buildPlanNotes(checks, langNames, pmNames)
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
/** Resolve the signal reference for the first manifest/lockfile signal. */
|
|
948
|
+
function resolveSignalRef(signals) {
|
|
949
|
+
const manifestSignal = signals.find((s) => s.type === "manifest" || s.type === "lockfile") ?? null;
|
|
950
|
+
return manifestSignal ? `${manifestSignal.type}:${manifestSignal.path}` : null;
|
|
951
|
+
}
|
|
952
|
+
/** Collect deduplicated default checks that match the detected languages/package managers. */
|
|
953
|
+
function collectDefaultChecks(drivers, langNames, pmNames, signalRef) {
|
|
954
|
+
const checks = [];
|
|
955
|
+
const seen = /* @__PURE__ */ new Set();
|
|
956
|
+
for (const driver of drivers) {
|
|
957
|
+
const langMatch = driver.languages.some((l) => langNames.includes(l));
|
|
958
|
+
const pmMatch = driver.packageManagers.some((p) => pmNames.includes(p));
|
|
959
|
+
if (!langMatch || !pmMatch) continue;
|
|
960
|
+
const key = `${driver.checkId}:${driver.reason}`;
|
|
961
|
+
if (seen.has(key)) continue;
|
|
962
|
+
seen.add(key);
|
|
963
|
+
const id = makeCheckId(driver.checkId, driver.reason);
|
|
964
|
+
checks.push({
|
|
965
|
+
id,
|
|
966
|
+
checkId: driver.checkId,
|
|
967
|
+
reason: driver.reason,
|
|
968
|
+
signalRef,
|
|
969
|
+
priority: 2
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
return checks;
|
|
973
|
+
}
|
|
974
|
+
/** Build a stable proposed-check id from its checkId and reason. */
|
|
975
|
+
function makeCheckId(checkId, reason) {
|
|
976
|
+
return "prop-" + createHash("sha256").update(checkId + reason).digest("hex").slice(0, 16);
|
|
977
|
+
}
|
|
978
|
+
/** Build human-readable notes describing the selected checks. */
|
|
979
|
+
function buildPlanNotes(checks, langNames, pmNames) {
|
|
980
|
+
if (checks.length === 0) return ["No default checks applied: no recognized languages or package managers detected."];
|
|
981
|
+
const drivers = [];
|
|
982
|
+
if (langNames.length) drivers.push(`languages=[${langNames.join(",")}]`);
|
|
983
|
+
if (pmNames.length) drivers.push(`packageManagers=[${pmNames.join(",")}]`);
|
|
984
|
+
return [`Selected ${checks.length} default checks from ${drivers.join(" ")}.`];
|
|
985
|
+
}
|
|
986
|
+
//#endregion
|
|
987
|
+
//#region src/planner/matrix.ts
|
|
988
|
+
/**
|
|
989
|
+
* Expand all matrix steps in a step list into concrete step instances.
|
|
990
|
+
* Non-matrix steps pass through unchanged.
|
|
991
|
+
*
|
|
992
|
+
* - Each matrix step becomes N steps, one per combination (cross-product of
|
|
993
|
+
* dimensions, minus exclude, plus include).
|
|
994
|
+
* - Each expanded step gets `matrixValues` (the concrete bindings) and
|
|
995
|
+
* `matrix: undefined` (the spec is consumed).
|
|
996
|
+
* - Expanded step IDs: `<originalId>[key=val,key=val]` (keys sorted alphabetically).
|
|
997
|
+
* - Dependencies are rewired: a dependency on a matrix step maps to ALL
|
|
998
|
+
* expanded instances (all-to-all, no correlation matching in M1).
|
|
999
|
+
*/
|
|
1000
|
+
function expandMatrixSteps(steps) {
|
|
1001
|
+
const expansionMap = /* @__PURE__ */ new Map();
|
|
1002
|
+
let hasMatrix = false;
|
|
1003
|
+
for (const step of steps) if (step.matrix) {
|
|
1004
|
+
hasMatrix = true;
|
|
1005
|
+
expansionMap.set(step.id, expandStep(step));
|
|
1006
|
+
} else expansionMap.set(step.id, [step]);
|
|
1007
|
+
if (!hasMatrix) return steps;
|
|
1008
|
+
const result = [];
|
|
1009
|
+
for (const step of steps) {
|
|
1010
|
+
const expanded = expansionMap.get(step.id);
|
|
1011
|
+
for (const expStep of expanded) result.push(rewireDependencies(expStep, expansionMap));
|
|
1012
|
+
}
|
|
1013
|
+
return result;
|
|
1014
|
+
}
|
|
1015
|
+
function expandStep(step) {
|
|
1016
|
+
const spec = step.matrix;
|
|
1017
|
+
if (spec.maxParallel !== void 0) {
|
|
1018
|
+
if (!Number.isInteger(spec.maxParallel) || spec.maxParallel < 1) throw new PlannerError(`matrix on step '${step.id}' has invalid maxParallel: must be a positive integer (got ${spec.maxParallel})`, "INVALID_MATRIX");
|
|
1019
|
+
}
|
|
1020
|
+
const combinations = computeCombinations(spec);
|
|
1021
|
+
if (combinations.length === 0) throw new PlannerError(`matrix on step '${step.id}' produced no combinations (check exclude rules)`, "INVALID_MATRIX");
|
|
1022
|
+
return combinations.map((combo) => {
|
|
1023
|
+
const { matrix: _consumed, ...rest } = step;
|
|
1024
|
+
return {
|
|
1025
|
+
...rest,
|
|
1026
|
+
id: formatExpandedId(step.id, combo),
|
|
1027
|
+
matrixValues: combo,
|
|
1028
|
+
dependencies: step.dependencies,
|
|
1029
|
+
...spec.failFast !== void 0 ? { matrixFailFast: spec.failFast } : {},
|
|
1030
|
+
...spec.maxParallel !== void 0 ? { matrixMaxParallel: spec.maxParallel } : {}
|
|
1031
|
+
};
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
function computeCombinations(spec) {
|
|
1035
|
+
const keys = Object.keys(spec.dimensions);
|
|
1036
|
+
if (keys.length === 0) return (spec.include ?? []).map((e) => ({ ...e }));
|
|
1037
|
+
return mergeIncludes(applyExcludes(crossProduct(spec.dimensions, keys), spec.exclude ?? []), spec.include ?? []);
|
|
1038
|
+
}
|
|
1039
|
+
/**
|
|
1040
|
+
* Compute the cross-product of all matrix dimensions.
|
|
1041
|
+
* Throws INVALID_MATRIX if any dimension has no values.
|
|
1042
|
+
*/
|
|
1043
|
+
function crossProduct(dimensions, keys) {
|
|
1044
|
+
let combinations = [{}];
|
|
1045
|
+
for (const key of keys) {
|
|
1046
|
+
const values = dimensions[key];
|
|
1047
|
+
if (!values || values.length === 0) throw new PlannerError(`matrix dimension '${key}' has no values`, "INVALID_MATRIX");
|
|
1048
|
+
const next = [];
|
|
1049
|
+
for (const combo of combinations) for (const v of values) next.push({
|
|
1050
|
+
...combo,
|
|
1051
|
+
[key]: v
|
|
1052
|
+
});
|
|
1053
|
+
combinations = next;
|
|
1054
|
+
}
|
|
1055
|
+
return combinations;
|
|
1056
|
+
}
|
|
1057
|
+
/** Filter out combinations matching any exclude rule (partial match). */
|
|
1058
|
+
function applyExcludes(combinations, exclude) {
|
|
1059
|
+
if (exclude.length === 0) return [...combinations];
|
|
1060
|
+
return combinations.filter((c) => !matchesAnyRule(c, exclude));
|
|
1061
|
+
}
|
|
1062
|
+
/** Merge include entries, deduplicating against existing combinations. */
|
|
1063
|
+
function mergeIncludes(combinations, include) {
|
|
1064
|
+
const includeEntries = include.map((e) => ({ ...e }));
|
|
1065
|
+
const existingKeys = new Set(combinations.map(comboKey));
|
|
1066
|
+
const uniqueIncludes = includeEntries.filter((e) => !existingKeys.has(comboKey(e)));
|
|
1067
|
+
return [...combinations, ...uniqueIncludes];
|
|
1068
|
+
}
|
|
1069
|
+
function matchesAnyRule(combo, rules) {
|
|
1070
|
+
for (const rule of rules) {
|
|
1071
|
+
const entries = Object.entries(rule);
|
|
1072
|
+
if (entries.length === 0) continue;
|
|
1073
|
+
if (entries.every(([k, v]) => combo[k] === v)) return true;
|
|
1074
|
+
}
|
|
1075
|
+
return false;
|
|
1076
|
+
}
|
|
1077
|
+
/** Produce a deterministic string key for a combination (sorted key=value pairs). */
|
|
1078
|
+
function comboKey(combo) {
|
|
1079
|
+
return Object.keys(combo).sort((a, b) => a.localeCompare(b)).map((k) => `${k}=${String(combo[k])}`).join(",");
|
|
1080
|
+
}
|
|
1081
|
+
function formatExpandedId(id, combo) {
|
|
1082
|
+
return `${id}[${Object.keys(combo).sort((a, b) => a.localeCompare(b)).map((k) => `${k}=${combo[k]}`).join(",")}]`;
|
|
1083
|
+
}
|
|
1084
|
+
function rewireDependencies(step, expansionMap) {
|
|
1085
|
+
const hasDeps = step.dependencies.length > 0;
|
|
1086
|
+
const hasInputs = step.inputs.length > 0;
|
|
1087
|
+
if (!hasDeps && !hasInputs) return step;
|
|
1088
|
+
const newDeps = hasDeps ? rewireDeps(step.dependencies, expansionMap) : step.dependencies;
|
|
1089
|
+
const newInputs = hasInputs ? rewireInputs(step.inputs, expansionMap) : step.inputs;
|
|
1090
|
+
return {
|
|
1091
|
+
...step,
|
|
1092
|
+
dependencies: newDeps,
|
|
1093
|
+
inputs: newInputs
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
function rewireDeps(deps, expansionMap) {
|
|
1097
|
+
const newDeps = [];
|
|
1098
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1099
|
+
for (const dep of deps) {
|
|
1100
|
+
const expanded = expansionMap.get(dep.producer);
|
|
1101
|
+
if (!expanded) {
|
|
1102
|
+
addDep(newDeps, seen, dep);
|
|
1103
|
+
continue;
|
|
1104
|
+
}
|
|
1105
|
+
for (const expStep of expanded) addDep(newDeps, seen, {
|
|
1106
|
+
...dep,
|
|
1107
|
+
producer: expStep.id
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
return newDeps;
|
|
1111
|
+
}
|
|
1112
|
+
function rewireInputs(inputs, expansionMap) {
|
|
1113
|
+
const newInputs = [];
|
|
1114
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1115
|
+
for (const ref of inputs) {
|
|
1116
|
+
if (ref.kind !== "step") {
|
|
1117
|
+
newInputs.push(ref);
|
|
1118
|
+
continue;
|
|
1119
|
+
}
|
|
1120
|
+
const expanded = expansionMap.get(ref.step);
|
|
1121
|
+
if (!expanded) {
|
|
1122
|
+
newInputs.push(ref);
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
for (const expStep of expanded) {
|
|
1126
|
+
const rewired = {
|
|
1127
|
+
...ref,
|
|
1128
|
+
step: expStep.id
|
|
1129
|
+
};
|
|
1130
|
+
const key = `${rewired.step}:${rewired.output}`;
|
|
1131
|
+
if (seen.has(key)) continue;
|
|
1132
|
+
seen.add(key);
|
|
1133
|
+
newInputs.push(rewired);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return newInputs;
|
|
1137
|
+
}
|
|
1138
|
+
function addDep(deps, seen, dep) {
|
|
1139
|
+
const key = dep.kind === "control" ? `control:${dep.producer}` : `${dep.kind}:${dep.producer}:${dep.output}`;
|
|
1140
|
+
if (seen.has(key)) return;
|
|
1141
|
+
seen.add(key);
|
|
1142
|
+
deps.push(dep);
|
|
1143
|
+
}
|
|
1144
|
+
//#endregion
|
|
1145
|
+
//#region src/errors.ts
|
|
1146
|
+
var SdkError = class extends Error {
|
|
1147
|
+
cause;
|
|
1148
|
+
code;
|
|
1149
|
+
constructor(message, code, cause) {
|
|
1150
|
+
super(message);
|
|
1151
|
+
this.name = "SdkError";
|
|
1152
|
+
this.code = code;
|
|
1153
|
+
if (cause !== void 0) this.cause = cause;
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
//#endregion
|
|
1157
|
+
//#region src/compat/config.ts
|
|
1158
|
+
const CONFIG_NAMES = ["sverka.config.ts", "sverka.config.js"];
|
|
1159
|
+
const MAX_PARENTS = 5;
|
|
1160
|
+
/**
|
|
1161
|
+
* Search upward from `root` for sverka.config.ts (then .js). Returns the
|
|
1162
|
+
* path or null. Searches up to 5 parent directories.
|
|
1163
|
+
*/
|
|
1164
|
+
async function findConfig(root) {
|
|
1165
|
+
let dir = resolve(root);
|
|
1166
|
+
for (let i = 0; i <= MAX_PARENTS; i++) {
|
|
1167
|
+
for (const name of CONFIG_NAMES) {
|
|
1168
|
+
const candidate = resolve(dir, name);
|
|
1169
|
+
if (existsSync(candidate)) return candidate;
|
|
1170
|
+
}
|
|
1171
|
+
const parent = dirname(dir);
|
|
1172
|
+
if (parent === dir) break;
|
|
1173
|
+
dir = parent;
|
|
1174
|
+
}
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
/**
|
|
1178
|
+
* Dynamically import a sverka.config.ts file and return its default
|
|
1179
|
+
* export as a WorkflowDefinition. Validates the shape.
|
|
1180
|
+
*
|
|
1181
|
+
* Under Bun, .ts files import natively. Under Node, use the .js fallback
|
|
1182
|
+
* (findConfig searches .ts first, then .js).
|
|
1183
|
+
*/
|
|
1184
|
+
async function loadWorkflow(configPath, root) {
|
|
1185
|
+
const resolved = isAbsolute(configPath) ? resolve(configPath) : resolve(root ?? process.cwd(), configPath);
|
|
1186
|
+
if (!existsSync(resolved)) throw new SdkError(`config file not found: ${configPath}`, "CONFIG_NOT_FOUND");
|
|
1187
|
+
if (root !== void 0) {
|
|
1188
|
+
const rel = relative(resolve(root), resolved);
|
|
1189
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) throw new SdkError(`config path escapes root: ${configPath}`, "CONFIG_PATH_ESCAPE");
|
|
1190
|
+
}
|
|
1191
|
+
let mod;
|
|
1192
|
+
try {
|
|
1193
|
+
mod = await import(pathToFileURL(resolved).href);
|
|
1194
|
+
} catch (e) {
|
|
1195
|
+
throw new SdkError(`failed to load config: ${basename(configPath)}`, "CONFIG_LOAD_FAILED", e);
|
|
1196
|
+
}
|
|
1197
|
+
const def = mod.default;
|
|
1198
|
+
validateDefaultExport(def);
|
|
1199
|
+
validateWorkflowShape(def.workflow);
|
|
1200
|
+
return def;
|
|
1201
|
+
}
|
|
1202
|
+
function validateDefaultExport(def) {
|
|
1203
|
+
if (!isPlainObject(def)) throw new SdkError("config default export must be an object", "CONFIG_INVALID");
|
|
1204
|
+
if (typeof def.name !== "string" || def.name.length === 0) throw new SdkError("config default export must have a non-empty 'name' string", "CONFIG_INVALID");
|
|
1205
|
+
if (!isPlainObject(def.workflow)) throw new SdkError("config default export must have a 'workflow' object", "CONFIG_INVALID");
|
|
1206
|
+
}
|
|
1207
|
+
function validateWorkflowShape(wf) {
|
|
1208
|
+
const isWorkflow = Array.isArray(wf.roots) && typeof wf.plan === "function";
|
|
1209
|
+
const isOperation = typeof wf.kind === "string" && isPlainObject(wf.spec);
|
|
1210
|
+
if (!isWorkflow && !isOperation) throw new SdkError("config 'workflow' must be a Workflow (with 'roots' and 'plan') or an Operation (with 'kind' and 'spec')", "CONFIG_INVALID");
|
|
1211
|
+
}
|
|
1212
|
+
function isPlainObject(v) {
|
|
1213
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1214
|
+
}
|
|
1215
|
+
//#endregion
|
|
1216
|
+
//#region src/compat/convert.ts
|
|
1217
|
+
const SVERKA_VERSION = (() => {
|
|
1218
|
+
let raw = "";
|
|
1219
|
+
try {
|
|
1220
|
+
raw = readFileSync(new URL("../package.json", import.meta.url), "utf-8");
|
|
1221
|
+
} catch {
|
|
1222
|
+
try {
|
|
1223
|
+
raw = readFileSync(new URL("../../package.json", import.meta.url), "utf-8");
|
|
1224
|
+
} catch {
|
|
1225
|
+
return "0.0.0";
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const parsed = JSON.parse(raw);
|
|
1229
|
+
if (typeof parsed === "object" && parsed !== null && "version" in parsed && typeof parsed.version === "string") return parsed.version;
|
|
1230
|
+
return "0.0.0";
|
|
1231
|
+
})();
|
|
1232
|
+
/**
|
|
1233
|
+
* Convert an array of OperationSpecs into a validated IR Plan with all
|
|
1234
|
+
* defaults filled per the spec's conversion table.
|
|
1235
|
+
*/
|
|
1236
|
+
function convertToPlan(operations, opts) {
|
|
1237
|
+
const planOps = operations.map((spec) => convertOperation(spec, opts));
|
|
1238
|
+
const sourceContextHash = computeSourceContextHash(opts.context);
|
|
1239
|
+
const metadata = {
|
|
1240
|
+
sverkaVersion: SVERKA_VERSION,
|
|
1241
|
+
generatedBy: "manual"
|
|
1242
|
+
};
|
|
1243
|
+
const body = {
|
|
1244
|
+
apiVersion: "sverka.dev/v1",
|
|
1245
|
+
name: opts.name,
|
|
1246
|
+
sourceContextHash,
|
|
1247
|
+
operations: planOps,
|
|
1248
|
+
metadata
|
|
1249
|
+
};
|
|
1250
|
+
const id = computePlanId$1(body);
|
|
1251
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1252
|
+
return {
|
|
1253
|
+
...body,
|
|
1254
|
+
id,
|
|
1255
|
+
createdAt
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
function convertOperation(spec, opts) {
|
|
1259
|
+
if (!spec.id || spec.id.length === 0) throw new SdkError(`operation has empty id (kind=${spec.kind}, name=${spec.name})`, "EXECUTION_FAILED");
|
|
1260
|
+
return buildPlanOperation(spec, buildExecutor(spec, opts), buildResources(spec), buildRetry(spec), (spec.artifacts ?? []).map(convertArtifact), spec.cache ? convertCache(spec.cache) : void 0);
|
|
1261
|
+
}
|
|
1262
|
+
function buildExecutor(spec, opts) {
|
|
1263
|
+
return {
|
|
1264
|
+
type: opts.executor,
|
|
1265
|
+
...spec.image !== void 0 ? { image: spec.image } : {},
|
|
1266
|
+
...spec.imageDigest !== void 0 ? { imageDigest: spec.imageDigest } : {}
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
function buildResources(spec) {
|
|
1270
|
+
return {
|
|
1271
|
+
cpu: spec.cpuLimit ?? "1",
|
|
1272
|
+
memory: spec.memoryLimit ?? "512Mi"
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
function buildRetry(spec) {
|
|
1276
|
+
const retries = spec.retries ?? 0;
|
|
1277
|
+
return {
|
|
1278
|
+
maxAttempts: Math.max(1, retries + 1),
|
|
1279
|
+
backoffSeconds: 0,
|
|
1280
|
+
retryOn: ["failure", "timeout"]
|
|
1281
|
+
};
|
|
1282
|
+
}
|
|
1283
|
+
function buildPlanOperation(spec, executor, resources, retry, artifacts, cache) {
|
|
1284
|
+
return {
|
|
1285
|
+
id: spec.id,
|
|
1286
|
+
kind: spec.kind,
|
|
1287
|
+
name: spec.name,
|
|
1288
|
+
...optionalFields(spec),
|
|
1289
|
+
...resolveDefaults(spec),
|
|
1290
|
+
executor,
|
|
1291
|
+
resources,
|
|
1292
|
+
...cache !== void 0 ? { cache } : {},
|
|
1293
|
+
artifacts,
|
|
1294
|
+
retry
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
function valueOrDefault(value, defaultValue) {
|
|
1298
|
+
return value === void 0 ? defaultValue : value;
|
|
1299
|
+
}
|
|
1300
|
+
/** Resolve fields with default values to reduce buildPlanOperation complexity. */
|
|
1301
|
+
function resolveDefaults(spec) {
|
|
1302
|
+
return {
|
|
1303
|
+
dependsOn: valueOrDefault(spec.dependsOn, []),
|
|
1304
|
+
network: valueOrDefault(spec.network, "deny"),
|
|
1305
|
+
credentials: valueOrDefault(spec.credentials, []),
|
|
1306
|
+
timeoutSeconds: valueOrDefault(spec.timeoutSeconds, 300),
|
|
1307
|
+
continueOnError: valueOrDefault(spec.continueOnError, false)
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
function optionalFields(spec) {
|
|
1311
|
+
return {
|
|
1312
|
+
...spec.description !== void 0 ? { description: spec.description } : {},
|
|
1313
|
+
...spec.command !== void 0 ? { command: spec.command } : {},
|
|
1314
|
+
...spec.args !== void 0 ? { args: spec.args } : {},
|
|
1315
|
+
...spec.env !== void 0 ? { env: spec.env } : {},
|
|
1316
|
+
...spec.workingDir !== void 0 ? { workingDir: spec.workingDir } : {},
|
|
1317
|
+
...spec.condition !== void 0 ? { condition: spec.condition } : {}
|
|
1318
|
+
};
|
|
1319
|
+
}
|
|
1320
|
+
function convertArtifact(a) {
|
|
1321
|
+
return {
|
|
1322
|
+
path: a.path,
|
|
1323
|
+
...a.name !== void 0 ? { name: a.name } : {},
|
|
1324
|
+
retain: a.retain ?? false
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
function convertCache(c) {
|
|
1328
|
+
return {
|
|
1329
|
+
inputs: c.inputs,
|
|
1330
|
+
outputs: c.outputs ?? [],
|
|
1331
|
+
key: c.key ?? computeCacheKey(c.inputs)
|
|
1332
|
+
};
|
|
1333
|
+
}
|
|
1334
|
+
function computeCacheKey(inputs) {
|
|
1335
|
+
return createHash("sha256").update(inputs.join(",")).digest("hex").slice(0, 32);
|
|
1336
|
+
}
|
|
1337
|
+
function computeSourceContextHash(context) {
|
|
1338
|
+
const changedFiles = context ? [...context.changedFiles.map((f) => f.path)].sort((a, b) => a.localeCompare(b)) : [];
|
|
1339
|
+
const value = {
|
|
1340
|
+
commit: context?.commit ?? "",
|
|
1341
|
+
dirty: context?.dirty ?? false,
|
|
1342
|
+
changedFiles
|
|
1343
|
+
};
|
|
1344
|
+
return createHash("sha256").update(canonicalStringify(value)).digest("hex");
|
|
1345
|
+
}
|
|
1346
|
+
//#endregion
|
|
1347
|
+
//#region src/compat/internal/plan-runtime.ts
|
|
1348
|
+
/**
|
|
1349
|
+
* A plan-mode Runtime that records operations without side effects.
|
|
1350
|
+
* Used by the SDK to evaluate a workflow graph into an OperationSpec[]
|
|
1351
|
+
* without executing any commands.
|
|
1352
|
+
*/
|
|
1353
|
+
var PlanRuntime = class {
|
|
1354
|
+
mode = "plan";
|
|
1355
|
+
recorded = [];
|
|
1356
|
+
start;
|
|
1357
|
+
constructor() {
|
|
1358
|
+
this.start = Date.now();
|
|
1359
|
+
}
|
|
1360
|
+
async evaluate(operation) {
|
|
1361
|
+
this.recorded.push(operation);
|
|
1362
|
+
return {
|
|
1363
|
+
operationId: operation.id,
|
|
1364
|
+
status: "planned",
|
|
1365
|
+
durationMs: 0
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
async finalize() {
|
|
1369
|
+
return {
|
|
1370
|
+
mode: "plan",
|
|
1371
|
+
operations: this.recorded,
|
|
1372
|
+
outcomes: [],
|
|
1373
|
+
durationMs: Date.now() - this.start
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
};
|
|
1377
|
+
//#endregion
|
|
1378
|
+
//#region src/sverka.ts
|
|
1379
|
+
/**
|
|
1380
|
+
* Create a Sverka instance with default options pre-applied. Per-call
|
|
1381
|
+
* options override defaults.
|
|
1382
|
+
*/
|
|
1383
|
+
function createSverka(defaultOptions) {
|
|
1384
|
+
return {
|
|
1385
|
+
async plan(options) {
|
|
1386
|
+
return doPlan(mergeOptions(defaultOptions, options));
|
|
1387
|
+
},
|
|
1388
|
+
async toPlan(options) {
|
|
1389
|
+
return buildPlan(mergeOptions(defaultOptions, options));
|
|
1390
|
+
},
|
|
1391
|
+
async execute(options) {
|
|
1392
|
+
return doExecute(mergeOptions(defaultOptions, options));
|
|
1393
|
+
}
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
/** Top-level plan convenience function. */
|
|
1397
|
+
async function plan(options) {
|
|
1398
|
+
return doPlan(options ?? {});
|
|
1399
|
+
}
|
|
1400
|
+
/** Top-level toPlan convenience function. Returns the canonical Plan IR. */
|
|
1401
|
+
async function toPlan(options) {
|
|
1402
|
+
return buildPlan(options ?? {});
|
|
1403
|
+
}
|
|
1404
|
+
/** Top-level execute convenience function. */
|
|
1405
|
+
async function execute(options) {
|
|
1406
|
+
return doExecute(options ?? {});
|
|
1407
|
+
}
|
|
1408
|
+
async function doPlan(options) {
|
|
1409
|
+
const root = options.root ?? process.cwd();
|
|
1410
|
+
const planner = createPlanner();
|
|
1411
|
+
const context = await planner.discover({
|
|
1412
|
+
root,
|
|
1413
|
+
...options.baseRef !== void 0 ? { baseRef: options.baseRef } : {}
|
|
1414
|
+
});
|
|
1415
|
+
const configPath = await resolveConfigPath(options, root);
|
|
1416
|
+
if (configPath !== null) {
|
|
1417
|
+
const def = await loadWorkflow(configPath, root);
|
|
1418
|
+
const operations = await evaluateWorkflow(def);
|
|
1419
|
+
return {
|
|
1420
|
+
context,
|
|
1421
|
+
operations,
|
|
1422
|
+
proposal: null,
|
|
1423
|
+
plan: buildPlanFromOps(operations, def.name, context)
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
const proposal = await planner.plan(context);
|
|
1427
|
+
const resolver = options.resolver ?? createBuiltinResolver$1();
|
|
1428
|
+
const operations = [];
|
|
1429
|
+
for (const check of proposal.checks) try {
|
|
1430
|
+
const r = resolver.resolve(check, context);
|
|
1431
|
+
if (r !== null) operations.push(r.operation);
|
|
1432
|
+
} catch {}
|
|
1433
|
+
return {
|
|
1434
|
+
context,
|
|
1435
|
+
operations,
|
|
1436
|
+
proposal,
|
|
1437
|
+
plan: buildPlanFromOps(operations, "sverka-plan", context)
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
/** Build a canonical Plan IR from resolved operations. */
|
|
1441
|
+
async function buildPlan(options) {
|
|
1442
|
+
return (await doPlan(options)).plan;
|
|
1443
|
+
}
|
|
1444
|
+
/** Convert operations into a validated Plan with host executor defaults. */
|
|
1445
|
+
function buildPlanFromOps(operations, name, context) {
|
|
1446
|
+
const plan = convertToPlan(operations, {
|
|
1447
|
+
name,
|
|
1448
|
+
executor: "host",
|
|
1449
|
+
context
|
|
1450
|
+
});
|
|
1451
|
+
if (operations.length > 0) {
|
|
1452
|
+
const validation = validatePlan$1(plan);
|
|
1453
|
+
if (!validation.valid) throw new SdkError(`plan validation failed: ${validation.errors.map((e) => e.message).join("; ")}`, "EXECUTION_FAILED");
|
|
1454
|
+
}
|
|
1455
|
+
return plan;
|
|
1456
|
+
}
|
|
1457
|
+
async function doExecute(options) {
|
|
1458
|
+
const root = options.root ?? process.cwd();
|
|
1459
|
+
const executorType = options.executor ?? "host";
|
|
1460
|
+
const context = await createPlanner().discover({
|
|
1461
|
+
root,
|
|
1462
|
+
...options.baseRef !== void 0 ? { baseRef: options.baseRef } : {}
|
|
1463
|
+
});
|
|
1464
|
+
const { def, operations, resolvedChecks, planName } = await resolveOperations(options, root, context, executorType);
|
|
1465
|
+
const { runtimeResult, findings } = await runPlan(buildAndValidatePlan(operations, planName, executorType, context), resolvedChecks, root, executorType);
|
|
1466
|
+
return buildExecutionResult(runtimeResult, findings, options, def);
|
|
1467
|
+
}
|
|
1468
|
+
/** Load or auto-discover the operations to execute. */
|
|
1469
|
+
async function resolveOperations(options, root, context, executorType) {
|
|
1470
|
+
const configPath = await resolveConfigPath(options, root);
|
|
1471
|
+
if (configPath !== null) {
|
|
1472
|
+
const def = await loadWorkflow(configPath, root);
|
|
1473
|
+
return {
|
|
1474
|
+
def,
|
|
1475
|
+
operations: await evaluateWorkflow(def),
|
|
1476
|
+
resolvedChecks: [],
|
|
1477
|
+
planName: def.name
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const proposal = await createPlanner().plan(context);
|
|
1481
|
+
const resolver = options.resolver ?? createBuiltinResolver$1();
|
|
1482
|
+
const tmpChecks = [];
|
|
1483
|
+
for (const check of proposal.checks) try {
|
|
1484
|
+
const r = resolver.resolve(check, context);
|
|
1485
|
+
if (r !== null) tmpChecks.push(r);
|
|
1486
|
+
} catch {}
|
|
1487
|
+
if (executorType === "docker" && tmpChecks.some((r) => r.operation.image === void 0)) throw new SdkError("docker executor requires container images; every operation must declare an image", "EXECUTION_FAILED");
|
|
1488
|
+
const operations = tmpChecks.map((r) => buildOperationWithOutputs(r));
|
|
1489
|
+
if (operations.length === 0) throw new SdkError("no config found and auto-discovery produced no resolvable checks", "CONFIG_NOT_FOUND");
|
|
1490
|
+
return {
|
|
1491
|
+
def: null,
|
|
1492
|
+
operations,
|
|
1493
|
+
resolvedChecks: tmpChecks,
|
|
1494
|
+
planName: "sverka-plan"
|
|
1495
|
+
};
|
|
1496
|
+
}
|
|
1497
|
+
/** Convert operations into a Plan and validate it. */
|
|
1498
|
+
function buildAndValidatePlan(operations, planName, executorType, context) {
|
|
1499
|
+
const plan = convertToPlan(operations, {
|
|
1500
|
+
name: planName,
|
|
1501
|
+
executor: executorType,
|
|
1502
|
+
context
|
|
1503
|
+
});
|
|
1504
|
+
if (operations.length > 0) {
|
|
1505
|
+
const validation = validatePlan$1(plan);
|
|
1506
|
+
if (!validation.valid) throw new SdkError(`plan validation failed: ${validation.errors.map((e) => e.message).join("; ")}`, "EXECUTION_FAILED");
|
|
1507
|
+
}
|
|
1508
|
+
return plan;
|
|
1509
|
+
}
|
|
1510
|
+
/** Execute a Plan and extract findings from resolved checks. */
|
|
1511
|
+
async function runPlan(plan, resolvedChecks, root, executorType) {
|
|
1512
|
+
const artifactDir = mkdtempSync(join(tmpdir(), "sverka-artifacts-"));
|
|
1513
|
+
const cacheDir = mkdtempSync(join(tmpdir(), "sverka-cache-"));
|
|
1514
|
+
try {
|
|
1515
|
+
const { Scheduler } = await import("@sverka/runtime");
|
|
1516
|
+
const scheduler = new Scheduler({
|
|
1517
|
+
executors: [await createExecutor(executorType, cacheDir, plan.operations)],
|
|
1518
|
+
maxConcurrent: 4,
|
|
1519
|
+
workspace: root,
|
|
1520
|
+
artifactDir,
|
|
1521
|
+
cacheDir,
|
|
1522
|
+
credentials: {},
|
|
1523
|
+
resume: false
|
|
1524
|
+
});
|
|
1525
|
+
try {
|
|
1526
|
+
return {
|
|
1527
|
+
runtimeResult: await scheduler.execute(plan),
|
|
1528
|
+
findings: await extractResolvedFindings(resolvedChecks, artifactDir)
|
|
1529
|
+
};
|
|
1530
|
+
} catch (e) {
|
|
1531
|
+
throw new SdkError(`execution failed: ${e instanceof Error ? e.message : String(e)}`, "EXECUTION_FAILED", e);
|
|
1532
|
+
} finally {
|
|
1533
|
+
await scheduler.dispose().catch(() => {});
|
|
1534
|
+
}
|
|
1535
|
+
} finally {
|
|
1536
|
+
await Promise.all([rm(artifactDir, {
|
|
1537
|
+
recursive: true,
|
|
1538
|
+
force: true
|
|
1539
|
+
}).catch(() => {}), rm(cacheDir, {
|
|
1540
|
+
recursive: true,
|
|
1541
|
+
force: true
|
|
1542
|
+
}).catch(() => {})]);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
/** Extract findings from resolved checks' SARIF output artifacts. */
|
|
1546
|
+
async function extractResolvedFindings(resolvedChecks, artifactDir) {
|
|
1547
|
+
const all = [];
|
|
1548
|
+
for (const r of resolvedChecks) {
|
|
1549
|
+
if (r.outputs.length === 0) continue;
|
|
1550
|
+
const extracted = await extractFindings$1(r.outputs, artifactDir, r.checkId);
|
|
1551
|
+
all.push(...extracted);
|
|
1552
|
+
}
|
|
1553
|
+
return all;
|
|
1554
|
+
}
|
|
1555
|
+
/** Build the final ExecutionResult from runtime output and user options. */
|
|
1556
|
+
async function buildExecutionResult(runtimeResult, findings, options, def) {
|
|
1557
|
+
let baselineFingerprints = [];
|
|
1558
|
+
let filteredFindings = findings;
|
|
1559
|
+
if (options.baselinePath) {
|
|
1560
|
+
const baseline = await loadBaseline$1(options.baselinePath);
|
|
1561
|
+
baselineFingerprints = baseline.fingerprints;
|
|
1562
|
+
filteredFindings = options.onlyNew ? filterOnlyNew$1(findings, baseline) : findings;
|
|
1563
|
+
}
|
|
1564
|
+
const policy = def?.policy ? createPolicy$1(def.policy) : DEFAULT_POLICY$1;
|
|
1565
|
+
const policyResult = evaluatePolicy$1(filteredFindings, policy, baselineFingerprints);
|
|
1566
|
+
const verdict = runtimeResult.status === "success" ? policyResult.verdict : "fail";
|
|
1567
|
+
return {
|
|
1568
|
+
findings: filteredFindings,
|
|
1569
|
+
policyResult,
|
|
1570
|
+
verdict,
|
|
1571
|
+
status: runtimeResult.status,
|
|
1572
|
+
...runtimeResult.runtimeFailure ? { runtimeFailure: runtimeResult.runtimeFailure } : {},
|
|
1573
|
+
outcomes: runtimeResult.outcomes,
|
|
1574
|
+
durationMs: runtimeResult.durationMs
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
function mergeOptions(defaults, callOpts) {
|
|
1578
|
+
return {
|
|
1579
|
+
...defaults,
|
|
1580
|
+
...callOpts
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
async function resolveConfigPath(options, root) {
|
|
1584
|
+
if (options.configPath) return options.configPath;
|
|
1585
|
+
return findConfig(root);
|
|
1586
|
+
}
|
|
1587
|
+
async function evaluateWorkflow(def) {
|
|
1588
|
+
const wf = normalizeWorkflow(def.name, def.workflow);
|
|
1589
|
+
const runtime = new PlanRuntime();
|
|
1590
|
+
return (await wf.plan(runtime)).operations;
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Normalize a WorkflowDefinition's workflow field to a Workflow.
|
|
1594
|
+
* If it's already a Workflow (has `roots` and `plan`), return as-is.
|
|
1595
|
+
* If it's a bare Operation, wrap it with `workflow(name, op)`.
|
|
1596
|
+
*/
|
|
1597
|
+
function normalizeWorkflow(name, wf) {
|
|
1598
|
+
if (isWorkflow(wf)) return wf;
|
|
1599
|
+
return workflow$1(name, wf);
|
|
1600
|
+
}
|
|
1601
|
+
function isWorkflow(wf) {
|
|
1602
|
+
return "roots" in wf && "plan" in wf;
|
|
1603
|
+
}
|
|
1604
|
+
async function createExecutor(type, cacheDir, operations) {
|
|
1605
|
+
if (type === "docker") {
|
|
1606
|
+
const { DockerExecutor } = await import("@sverka/runtime");
|
|
1607
|
+
return new DockerExecutor({
|
|
1608
|
+
runAs: `${process.getuid?.() ?? 1e3}:${process.getgid?.() ?? 1e3}`,
|
|
1609
|
+
cacheDir
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
const { HostExecutor, createAllowlist } = await import("@sverka/runtime");
|
|
1613
|
+
return new HostExecutor({
|
|
1614
|
+
enabled: true,
|
|
1615
|
+
allowlist: createAllowlist(operations.map((op) => op.command).filter((c) => typeof c === "string" && c.length > 0)),
|
|
1616
|
+
envAllowlist: ["PATH"]
|
|
1617
|
+
});
|
|
1618
|
+
}
|
|
1619
|
+
/** Merge resolved-check outputs into the operation's artifact declarations. */
|
|
1620
|
+
function buildOperationWithOutputs(r) {
|
|
1621
|
+
if (r.outputs.length === 0) return r.operation;
|
|
1622
|
+
const artifacts = [...r.operation.artifacts ?? [], ...r.outputs.map((o) => ({
|
|
1623
|
+
path: o.path,
|
|
1624
|
+
retain: false
|
|
1625
|
+
}))];
|
|
1626
|
+
return {
|
|
1627
|
+
...r.operation,
|
|
1628
|
+
artifacts
|
|
1629
|
+
};
|
|
1630
|
+
}
|
|
1631
|
+
//#endregion
|
|
1632
|
+
//#region src/compat/helpers.ts
|
|
1633
|
+
/** Name an operation. Sugar for `op.named(name)`. */
|
|
1634
|
+
function task(name, op) {
|
|
1635
|
+
return op.named(name);
|
|
1636
|
+
}
|
|
1637
|
+
/** Type-safe helper for sverka.config.ts. Identity function. */
|
|
1638
|
+
function defineWorkflow(definition) {
|
|
1639
|
+
return definition;
|
|
1640
|
+
}
|
|
1641
|
+
//#endregion
|
|
1642
|
+
//#region src/internal/is-reference.ts
|
|
1643
|
+
const OUTPUT_TYPES = /* @__PURE__ */ new Set([
|
|
1644
|
+
"string",
|
|
1645
|
+
"number",
|
|
1646
|
+
"boolean",
|
|
1647
|
+
"artifact"
|
|
1648
|
+
]);
|
|
1649
|
+
const CONTEXT_NAMESPACES = /* @__PURE__ */ new Set([
|
|
1650
|
+
"env",
|
|
1651
|
+
"secrets",
|
|
1652
|
+
"git",
|
|
1653
|
+
"change",
|
|
1654
|
+
"event",
|
|
1655
|
+
"run",
|
|
1656
|
+
"inputs",
|
|
1657
|
+
"matrix"
|
|
1658
|
+
]);
|
|
1659
|
+
/**
|
|
1660
|
+
* Type guard for Reference values.
|
|
1661
|
+
* Shared between expr.ts and sh.ts to avoid duplication.
|
|
1662
|
+
* Validates that type and namespace values are within their finite unions.
|
|
1663
|
+
*/
|
|
1664
|
+
function isReference(value) {
|
|
1665
|
+
if (typeof value !== "object" || value === null || !("kind" in value)) return false;
|
|
1666
|
+
const ref = value;
|
|
1667
|
+
if (ref.kind === "step") return isStepRef(value);
|
|
1668
|
+
if (ref.kind === "context") return isContextRef(value);
|
|
1669
|
+
return false;
|
|
1670
|
+
}
|
|
1671
|
+
function isStepRef(value) {
|
|
1672
|
+
const s = value;
|
|
1673
|
+
return typeof s.step === "string" && typeof s.output === "string" && typeof s.type === "string" && OUTPUT_TYPES.has(s.type);
|
|
1674
|
+
}
|
|
1675
|
+
function isContextRef(value) {
|
|
1676
|
+
const c = value;
|
|
1677
|
+
return typeof c.namespace === "string" && typeof c.field === "string" && CONTEXT_NAMESPACES.has(c.namespace);
|
|
1678
|
+
}
|
|
1679
|
+
//#endregion
|
|
1680
|
+
//#region src/dollar.ts
|
|
1681
|
+
function createBuilder(state) {
|
|
1682
|
+
const builder = {
|
|
1683
|
+
outputs(outputs) {
|
|
1684
|
+
state.outputs = outputs;
|
|
1685
|
+
return builder;
|
|
1686
|
+
},
|
|
1687
|
+
inputs(inputs) {
|
|
1688
|
+
state.inputs = inputs;
|
|
1689
|
+
return builder;
|
|
1690
|
+
},
|
|
1691
|
+
dependsOn(steps) {
|
|
1692
|
+
state.dependsOn = steps;
|
|
1693
|
+
return builder;
|
|
1694
|
+
},
|
|
1695
|
+
runtime(runtime) {
|
|
1696
|
+
state.runtime = runtime;
|
|
1697
|
+
return builder;
|
|
1698
|
+
},
|
|
1699
|
+
timeout(ms) {
|
|
1700
|
+
state.timeout = ms;
|
|
1701
|
+
return builder;
|
|
1702
|
+
},
|
|
1703
|
+
condition(ref) {
|
|
1704
|
+
state.condition = ref;
|
|
1705
|
+
return builder;
|
|
1706
|
+
},
|
|
1707
|
+
matrix(spec) {
|
|
1708
|
+
state.matrix = spec;
|
|
1709
|
+
return builder;
|
|
1710
|
+
},
|
|
1711
|
+
interruptible(value) {
|
|
1712
|
+
state.interruptible = value ?? true;
|
|
1713
|
+
return builder;
|
|
1714
|
+
},
|
|
1715
|
+
compensate(command) {
|
|
1716
|
+
state.compensation = {
|
|
1717
|
+
kind: "shell",
|
|
1718
|
+
command
|
|
1719
|
+
};
|
|
1720
|
+
return builder;
|
|
1721
|
+
},
|
|
1722
|
+
build(pipeline, id) {
|
|
1723
|
+
const allInputs = [...state.inputs ? [...state.inputs] : [], ...state.collectedInputs];
|
|
1724
|
+
return new ShellStep(pipeline, id, {
|
|
1725
|
+
command: state.command,
|
|
1726
|
+
...state.outputs ? { outputs: state.outputs } : {},
|
|
1727
|
+
...allInputs.length > 0 ? { inputs: allInputs } : {},
|
|
1728
|
+
...state.dependsOn ? { dependsOn: state.dependsOn } : {},
|
|
1729
|
+
...state.runtime ? { runtime: state.runtime } : {},
|
|
1730
|
+
...state.timeout !== void 0 ? { timeout: state.timeout } : {},
|
|
1731
|
+
...state.condition !== void 0 ? { condition: state.condition } : {},
|
|
1732
|
+
...state.matrix !== void 0 ? { matrix: state.matrix } : {},
|
|
1733
|
+
...state.interruptible !== void 0 ? { interruptible: state.interruptible } : {},
|
|
1734
|
+
...state.compensation !== void 0 ? { compensation: state.compensation } : {}
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
return builder;
|
|
1739
|
+
}
|
|
1740
|
+
/**
|
|
1741
|
+
* Create a shell command step builder from a tagged template.
|
|
1742
|
+
* References interpolated in the template are collected and added to the
|
|
1743
|
+
* step's inputs automatically.
|
|
1744
|
+
*/
|
|
1745
|
+
function $(strings, ...values) {
|
|
1746
|
+
let command = "";
|
|
1747
|
+
const collectedInputs = [];
|
|
1748
|
+
for (let i = 0; i < strings.length; i++) {
|
|
1749
|
+
command += strings[i];
|
|
1750
|
+
if (i < values.length) {
|
|
1751
|
+
const v = values[i];
|
|
1752
|
+
if (typeof v === "string") command += v;
|
|
1753
|
+
else if (isReference(v)) {
|
|
1754
|
+
const ref = v;
|
|
1755
|
+
collectedInputs.push(ref);
|
|
1756
|
+
if (ref.kind === "step") command += `\${${ref.step}.${ref.output}}`;
|
|
1757
|
+
else command += `\${${ref.namespace}.${ref.field}}`;
|
|
1758
|
+
} else throw new SdkError(`invalid interpolation value at position ${i}: expected string or Reference`, "INVALID_INTERPOLATION");
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
return createBuilder({
|
|
1762
|
+
command,
|
|
1763
|
+
collectedInputs
|
|
1764
|
+
});
|
|
1765
|
+
}
|
|
1766
|
+
//#endregion
|
|
1767
|
+
//#region src/shell.ts
|
|
1768
|
+
/**
|
|
1769
|
+
* Create a ShellProxy bound to an optional command prefix and shell interpreter.
|
|
1770
|
+
* - prefix: accumulated property-access chain (e.g. "git" for shell.git)
|
|
1771
|
+
* - shellOpt: interpreter string from shell("bash"), applied as runtime.shell
|
|
1772
|
+
*/
|
|
1773
|
+
function createShellProxy(prefix, shellOpt) {
|
|
1774
|
+
const proxy = function(stringsOrInterpreter, ...values) {
|
|
1775
|
+
if (typeof stringsOrInterpreter === "string") return createShellProxy(prefix, stringsOrInterpreter);
|
|
1776
|
+
const builder = $(stringsOrInterpreter, ...values);
|
|
1777
|
+
if (prefix) return wrapBuilder(builder, (step) => applyPrefixAndShell(step, prefix, shellOpt));
|
|
1778
|
+
if (shellOpt) return wrapBuilder(builder, (step) => applyShell(step, shellOpt));
|
|
1779
|
+
return builder;
|
|
1780
|
+
};
|
|
1781
|
+
return new Proxy(proxy, { get(_target, prop) {
|
|
1782
|
+
if (typeof prop !== "string") return void 0;
|
|
1783
|
+
if (prop === "then" || prop === "toJSON" || prop === "toString") return void 0;
|
|
1784
|
+
return createShellProxy(prefix ? `${prefix} ${prop}` : prop, shellOpt);
|
|
1785
|
+
} });
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Apply a command prefix and optional shell interpreter to a built step.
|
|
1789
|
+
* Mutates the step in-place to preserve pipeline registration.
|
|
1790
|
+
*/
|
|
1791
|
+
function applyPrefixAndShell(step, prefix, shellOpt) {
|
|
1792
|
+
const runtime = shellOpt ? {
|
|
1793
|
+
...step.runtime,
|
|
1794
|
+
shell: shellOpt
|
|
1795
|
+
} : step.runtime;
|
|
1796
|
+
Object.assign(step, {
|
|
1797
|
+
command: `${prefix} ${step.command}`,
|
|
1798
|
+
runtime
|
|
1799
|
+
});
|
|
1800
|
+
return step;
|
|
1801
|
+
}
|
|
1802
|
+
/**
|
|
1803
|
+
* Apply a shell interpreter to a built step (no prefix).
|
|
1804
|
+
* Mutates the step in-place to preserve pipeline registration.
|
|
1805
|
+
*/
|
|
1806
|
+
function applyShell(step, interpreter) {
|
|
1807
|
+
const runtime = {
|
|
1808
|
+
...step.runtime,
|
|
1809
|
+
shell: interpreter
|
|
1810
|
+
};
|
|
1811
|
+
Object.assign(step, { runtime });
|
|
1812
|
+
return step;
|
|
1813
|
+
}
|
|
1814
|
+
/**
|
|
1815
|
+
* Wrap a StepBuilder so `build()` applies a transform to the final step.
|
|
1816
|
+
* Chain methods delegate to the original builder and return the wrapper.
|
|
1817
|
+
*/
|
|
1818
|
+
function wrapBuilder(builder, transform) {
|
|
1819
|
+
const origBuild = builder.build.bind(builder);
|
|
1820
|
+
const wrapped = {
|
|
1821
|
+
outputs(o) {
|
|
1822
|
+
builder.outputs(o);
|
|
1823
|
+
return wrapped;
|
|
1824
|
+
},
|
|
1825
|
+
inputs(i) {
|
|
1826
|
+
builder.inputs(i);
|
|
1827
|
+
return wrapped;
|
|
1828
|
+
},
|
|
1829
|
+
dependsOn(s) {
|
|
1830
|
+
builder.dependsOn(s);
|
|
1831
|
+
return wrapped;
|
|
1832
|
+
},
|
|
1833
|
+
runtime(r) {
|
|
1834
|
+
builder.runtime(r);
|
|
1835
|
+
return wrapped;
|
|
1836
|
+
},
|
|
1837
|
+
timeout(ms) {
|
|
1838
|
+
builder.timeout(ms);
|
|
1839
|
+
return wrapped;
|
|
1840
|
+
},
|
|
1841
|
+
condition(ref) {
|
|
1842
|
+
builder.condition(ref);
|
|
1843
|
+
return wrapped;
|
|
1844
|
+
},
|
|
1845
|
+
matrix(spec) {
|
|
1846
|
+
builder.matrix(spec);
|
|
1847
|
+
return wrapped;
|
|
1848
|
+
},
|
|
1849
|
+
interruptible(value) {
|
|
1850
|
+
builder.interruptible(value);
|
|
1851
|
+
return wrapped;
|
|
1852
|
+
},
|
|
1853
|
+
compensate(command) {
|
|
1854
|
+
builder.compensate(command);
|
|
1855
|
+
return wrapped;
|
|
1856
|
+
},
|
|
1857
|
+
build(pipeline, id) {
|
|
1858
|
+
return transform(origBuild(pipeline, id));
|
|
1859
|
+
}
|
|
1860
|
+
};
|
|
1861
|
+
return wrapped;
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Shell proxy export. Provides command-prefix and shell-selector DX.
|
|
1865
|
+
*
|
|
1866
|
+
* @example
|
|
1867
|
+
* const { git, npm, bun, make, docker } = shell;
|
|
1868
|
+
* git`push origin main`.build(p, "push") // → "git push origin main"
|
|
1869
|
+
* bun`run build`.build(p, "build") // → "bun run build"
|
|
1870
|
+
*
|
|
1871
|
+
* @example
|
|
1872
|
+
* const bash = shell("bash");
|
|
1873
|
+
* bash.git`push`.build(p, "push") // → "git push" + runtime.shell="bash"
|
|
1874
|
+
* shell("pwsh")`Write-Host hello`.build(p, "greet") // → "Write-Host hello" + runtime.shell="pwsh"
|
|
1875
|
+
*/
|
|
1876
|
+
const shell = createShellProxy("");
|
|
1877
|
+
//#endregion
|
|
1878
|
+
//#region src/artifact.ts
|
|
1879
|
+
/** Create an artifact output declaration with the given path. */
|
|
1880
|
+
function artifact(path) {
|
|
1881
|
+
return {
|
|
1882
|
+
type: "artifact",
|
|
1883
|
+
path
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
//#endregion
|
|
1887
|
+
//#region src/pipeline.ts
|
|
1888
|
+
/** Create a Pipeline, run step functions, then entry functions. */
|
|
1889
|
+
function pipeline$1(project, id, config) {
|
|
1890
|
+
const p = new Pipeline(project, id, { ...config.inputs ? { inputs: config.inputs } : {} });
|
|
1891
|
+
if (config.steps) for (const stepFn of config.steps) stepFn(p);
|
|
1892
|
+
if (config.entries) for (const entryFn of config.entries) entryFn(p);
|
|
1893
|
+
return p;
|
|
1894
|
+
}
|
|
1895
|
+
//#endregion
|
|
1896
|
+
//#region src/call-pipeline.ts
|
|
1897
|
+
function createCallBuilder(state) {
|
|
1898
|
+
const builder = {
|
|
1899
|
+
outputs(outputs) {
|
|
1900
|
+
state.outputs = outputs;
|
|
1901
|
+
return builder;
|
|
1902
|
+
},
|
|
1903
|
+
dependsOn(steps) {
|
|
1904
|
+
state.dependsOn = steps;
|
|
1905
|
+
return builder;
|
|
1906
|
+
},
|
|
1907
|
+
runtime(runtime) {
|
|
1908
|
+
state.runtime = runtime;
|
|
1909
|
+
return builder;
|
|
1910
|
+
},
|
|
1911
|
+
timeout(ms) {
|
|
1912
|
+
state.timeout = ms;
|
|
1913
|
+
return builder;
|
|
1914
|
+
},
|
|
1915
|
+
condition(ref) {
|
|
1916
|
+
state.condition = ref;
|
|
1917
|
+
return builder;
|
|
1918
|
+
},
|
|
1919
|
+
interruptible(value) {
|
|
1920
|
+
state.interruptible = value;
|
|
1921
|
+
return builder;
|
|
1922
|
+
},
|
|
1923
|
+
build(pipeline, id) {
|
|
1924
|
+
return new PipelineCallStep(pipeline, id, {
|
|
1925
|
+
callee: state.callee,
|
|
1926
|
+
callInputs: state.callInputs,
|
|
1927
|
+
...state.outputs ? { outputs: state.outputs } : {},
|
|
1928
|
+
...state.dependsOn ? { dependsOn: state.dependsOn } : {},
|
|
1929
|
+
...state.runtime ? { runtime: state.runtime } : {},
|
|
1930
|
+
...state.timeout !== void 0 ? { timeout: state.timeout } : {},
|
|
1931
|
+
...state.condition !== void 0 ? { condition: state.condition } : {},
|
|
1932
|
+
...state.interruptible !== void 0 ? { interruptible: state.interruptible } : {}
|
|
1933
|
+
});
|
|
1934
|
+
}
|
|
1935
|
+
};
|
|
1936
|
+
return builder;
|
|
1937
|
+
}
|
|
1938
|
+
/**
|
|
1939
|
+
* Create a pipeline-call step builder.
|
|
1940
|
+
* @param callee The callee pipeline id (within the same Project).
|
|
1941
|
+
* @param callInputs Bound inputs for the callee pipeline.
|
|
1942
|
+
*/
|
|
1943
|
+
function callPipeline(callee, callInputs) {
|
|
1944
|
+
return createCallBuilder({
|
|
1945
|
+
callee,
|
|
1946
|
+
callInputs: callInputs ? { ...callInputs } : {}
|
|
1947
|
+
});
|
|
1948
|
+
}
|
|
1949
|
+
//#endregion
|
|
1950
|
+
//#region src/component.ts
|
|
1951
|
+
function createComponentBuilder(state) {
|
|
1952
|
+
const builder = {
|
|
1953
|
+
outputs(outputs) {
|
|
1954
|
+
state.outputs = outputs;
|
|
1955
|
+
return builder;
|
|
1956
|
+
},
|
|
1957
|
+
dependsOn(steps) {
|
|
1958
|
+
state.dependsOn = steps;
|
|
1959
|
+
return builder;
|
|
1960
|
+
},
|
|
1961
|
+
runtime(runtime) {
|
|
1962
|
+
state.runtime = runtime;
|
|
1963
|
+
return builder;
|
|
1964
|
+
},
|
|
1965
|
+
timeout(ms) {
|
|
1966
|
+
state.timeout = ms;
|
|
1967
|
+
return builder;
|
|
1968
|
+
},
|
|
1969
|
+
condition(ref) {
|
|
1970
|
+
state.condition = ref;
|
|
1971
|
+
return builder;
|
|
1972
|
+
},
|
|
1973
|
+
interruptible(value) {
|
|
1974
|
+
state.interruptible = value;
|
|
1975
|
+
return builder;
|
|
1976
|
+
},
|
|
1977
|
+
build(pipeline, id) {
|
|
1978
|
+
return new ComponentStep(pipeline, id, {
|
|
1979
|
+
component: state.component,
|
|
1980
|
+
...state.outputs ? { outputs: state.outputs } : {},
|
|
1981
|
+
...state.dependsOn ? { dependsOn: state.dependsOn } : {},
|
|
1982
|
+
...state.runtime ? { runtime: state.runtime } : {},
|
|
1983
|
+
...state.timeout !== void 0 ? { timeout: state.timeout } : {},
|
|
1984
|
+
...state.condition !== void 0 ? { condition: state.condition } : {},
|
|
1985
|
+
...state.interruptible !== void 0 ? { interruptible: state.interruptible } : {}
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
};
|
|
1989
|
+
return builder;
|
|
1990
|
+
}
|
|
1991
|
+
/**
|
|
1992
|
+
* Create a component step builder.
|
|
1993
|
+
* @param name The component name (e.g. "deploy").
|
|
1994
|
+
* @param version The component version (e.g. "1.0.0").
|
|
1995
|
+
* @param inputs Bound inputs for the component.
|
|
1996
|
+
*/
|
|
1997
|
+
function component(name, version, inputs) {
|
|
1998
|
+
return createComponentBuilder({ component: {
|
|
1999
|
+
name,
|
|
2000
|
+
version,
|
|
2001
|
+
inputs: inputs ? { ...inputs } : {}
|
|
2002
|
+
} });
|
|
2003
|
+
}
|
|
2004
|
+
//#endregion
|
|
2005
|
+
//#region src/when.ts
|
|
2006
|
+
/** Mark a reference, expression, or status condition as a step condition. Returns it unchanged. */
|
|
2007
|
+
function when$1(cond) {
|
|
2008
|
+
return cond;
|
|
2009
|
+
}
|
|
2010
|
+
//#endregion
|
|
2011
|
+
//#region src/expr.ts
|
|
2012
|
+
/**
|
|
2013
|
+
* Create a symbolic expression from a tagged template.
|
|
2014
|
+
* Context references produce `${namespace.field}` placeholders while step
|
|
2015
|
+
* references produce `${step.output}` placeholders; both are collected into
|
|
2016
|
+
* `refs` for dependency inference.
|
|
2017
|
+
* String, number, and boolean values are inlined into the template.
|
|
2018
|
+
*/
|
|
2019
|
+
function expr(strings, ...values) {
|
|
2020
|
+
let template = "";
|
|
2021
|
+
const refs = [];
|
|
2022
|
+
for (let i = 0; i < strings.length; i++) {
|
|
2023
|
+
template += strings[i];
|
|
2024
|
+
if (i < values.length) {
|
|
2025
|
+
const v = values[i];
|
|
2026
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") template += String(v);
|
|
2027
|
+
else if (isReference(v)) {
|
|
2028
|
+
refs.push(v);
|
|
2029
|
+
if (v.kind === "step") template += `\${${v.step}.${v.output}}`;
|
|
2030
|
+
else template += `\${${v.namespace}.${v.field}}`;
|
|
2031
|
+
} else throw new SdkError(`invalid interpolation value at position ${i}: expected string, number, boolean, or Reference`, "INVALID_INTERPOLATION");
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
return {
|
|
2035
|
+
kind: "expression",
|
|
2036
|
+
template,
|
|
2037
|
+
refs
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
//#endregion
|
|
2041
|
+
//#region src/status.ts
|
|
2042
|
+
/**
|
|
2043
|
+
* Create a status-based condition.
|
|
2044
|
+
* @example
|
|
2045
|
+
* sh`notify-slack`.condition(status("failure"))
|
|
2046
|
+
*/
|
|
2047
|
+
function status(s) {
|
|
2048
|
+
return {
|
|
2049
|
+
kind: "status",
|
|
2050
|
+
status: s
|
|
2051
|
+
};
|
|
2052
|
+
}
|
|
2053
|
+
//#endregion
|
|
2054
|
+
//#region src/internal/proxy-props.ts
|
|
2055
|
+
const PROTECTED_PROXY_PROPS = /* @__PURE__ */ new Set([
|
|
2056
|
+
"then",
|
|
2057
|
+
"toJSON",
|
|
2058
|
+
"inspect",
|
|
2059
|
+
"constructor",
|
|
2060
|
+
"valueOf",
|
|
2061
|
+
Symbol.toPrimitive,
|
|
2062
|
+
Symbol.iterator,
|
|
2063
|
+
Symbol.toStringTag
|
|
2064
|
+
]);
|
|
2065
|
+
/**
|
|
2066
|
+
* Create a dynamic Proxy that delegates protected/symbol properties to
|
|
2067
|
+
* Reflect.get and calls `resolve(prop)` for everything else.
|
|
2068
|
+
*
|
|
2069
|
+
* This shared factory eliminates duplication between context namespace
|
|
2070
|
+
* proxies and image reference proxies.
|
|
2071
|
+
*/
|
|
2072
|
+
function createDynamicProxy(resolve) {
|
|
2073
|
+
return new Proxy({}, { get(target, prop) {
|
|
2074
|
+
if (typeof prop === "symbol" || PROTECTED_PROXY_PROPS.has(prop)) return Reflect.get(target, prop);
|
|
2075
|
+
return resolve(prop);
|
|
2076
|
+
} });
|
|
2077
|
+
}
|
|
2078
|
+
//#endregion
|
|
2079
|
+
//#region src/images.ts
|
|
2080
|
+
/** Create an ImageRef from a raw OCI reference string. */
|
|
2081
|
+
function image(ref) {
|
|
2082
|
+
if (typeof ref !== "string") throw new SdkError("image reference must be a string", "INVALID_IMAGE");
|
|
2083
|
+
if (ref.length === 0) throw new SdkError("image reference must be a non-empty string", "INVALID_IMAGE");
|
|
2084
|
+
return { ref };
|
|
2085
|
+
}
|
|
2086
|
+
/** Create a Proxy that maps property access to `image(prefix:prop)`. */
|
|
2087
|
+
function createImageProxy(prefix) {
|
|
2088
|
+
return createDynamicProxy((prop) => image(`${prefix}:${prop}`));
|
|
2089
|
+
}
|
|
2090
|
+
const images = {
|
|
2091
|
+
node: createImageProxy("node"),
|
|
2092
|
+
ubuntu: createImageProxy("ubuntu")
|
|
2093
|
+
};
|
|
2094
|
+
//#endregion
|
|
2095
|
+
//#region src/context.ts
|
|
2096
|
+
/** Create a ContextRef for a namespace + field. */
|
|
2097
|
+
function ctx(namespace, field) {
|
|
2098
|
+
return {
|
|
2099
|
+
kind: "context",
|
|
2100
|
+
namespace,
|
|
2101
|
+
field
|
|
2102
|
+
};
|
|
2103
|
+
}
|
|
2104
|
+
/** Dynamic namespace proxy — any property access returns a ContextRef. */
|
|
2105
|
+
function dynamicNamespace(namespace) {
|
|
2106
|
+
return createDynamicProxy((prop) => ctx(namespace, prop));
|
|
2107
|
+
}
|
|
2108
|
+
/** Environment variable references: env.CI_TRACE, env.MY_VAR. */
|
|
2109
|
+
const env = dynamicNamespace("env");
|
|
2110
|
+
/** Secret references: secrets.NPM_TOKEN. */
|
|
2111
|
+
const secrets = dynamicNamespace("secrets");
|
|
2112
|
+
/** Git metadata references: git.sha, git.branch, git.tag. */
|
|
2113
|
+
const git = {
|
|
2114
|
+
sha: ctx("git", "sha"),
|
|
2115
|
+
branch: ctx("git", "branch"),
|
|
2116
|
+
tag: ctx("git", "tag")
|
|
2117
|
+
};
|
|
2118
|
+
/** Change request metadata: change.id, change.source, change.target, change.draft. */
|
|
2119
|
+
const change = {
|
|
2120
|
+
id: ctx("change", "id"),
|
|
2121
|
+
source: ctx("change", "source"),
|
|
2122
|
+
target: ctx("change", "target"),
|
|
2123
|
+
draft: ctx("change", "draft")
|
|
2124
|
+
};
|
|
2125
|
+
/** Event metadata: event.type. */
|
|
2126
|
+
const event = { type: ctx("event", "type") };
|
|
2127
|
+
/** Run metadata: run.id, run.attempt. */
|
|
2128
|
+
const run$1 = {
|
|
2129
|
+
id: ctx("run", "id"),
|
|
2130
|
+
attempt: ctx("run", "attempt")
|
|
2131
|
+
};
|
|
2132
|
+
/** Pipeline input references: inputs.environment. */
|
|
2133
|
+
const inputs = dynamicNamespace("inputs");
|
|
2134
|
+
/** Matrix variable references: matrix.node, matrix.os. */
|
|
2135
|
+
const matrix$1 = dynamicNamespace("matrix");
|
|
2136
|
+
//#endregion
|
|
2137
|
+
//#region src/agent.ts
|
|
2138
|
+
function createAgentBuilder(state) {
|
|
2139
|
+
const builder = {
|
|
2140
|
+
outputs(outputs) {
|
|
2141
|
+
state.outputs = outputs;
|
|
2142
|
+
return builder;
|
|
2143
|
+
},
|
|
2144
|
+
inputs(inputs) {
|
|
2145
|
+
state.inputs = inputs;
|
|
2146
|
+
return builder;
|
|
2147
|
+
},
|
|
2148
|
+
dependsOn(steps) {
|
|
2149
|
+
state.dependsOn = steps;
|
|
2150
|
+
return builder;
|
|
2151
|
+
},
|
|
2152
|
+
tools(...tools) {
|
|
2153
|
+
state.tools = [...state.tools, ...tools];
|
|
2154
|
+
return builder;
|
|
2155
|
+
},
|
|
2156
|
+
engine(engine) {
|
|
2157
|
+
state.engine = engine;
|
|
2158
|
+
return builder;
|
|
2159
|
+
},
|
|
2160
|
+
model(model) {
|
|
2161
|
+
state.model = model;
|
|
2162
|
+
return builder;
|
|
2163
|
+
},
|
|
2164
|
+
maxTokens(n) {
|
|
2165
|
+
state.maxTokens = n;
|
|
2166
|
+
return builder;
|
|
2167
|
+
},
|
|
2168
|
+
build(pipeline, id) {
|
|
2169
|
+
const allInputs = [...state.inputs ? [...state.inputs] : [], ...state.collectedInputs];
|
|
2170
|
+
return new AgentStep(pipeline, id, {
|
|
2171
|
+
engine: state.engine,
|
|
2172
|
+
prompt: state.prompt,
|
|
2173
|
+
...state.model !== void 0 ? { model: state.model } : {},
|
|
2174
|
+
...state.tools.length > 0 ? { tools: state.tools } : {},
|
|
2175
|
+
...state.maxTokens !== void 0 ? { maxTokens: state.maxTokens } : {},
|
|
2176
|
+
...state.outputs ? { outputs: state.outputs } : {},
|
|
2177
|
+
...allInputs.length > 0 ? { inputs: allInputs } : {},
|
|
2178
|
+
...state.dependsOn ? { dependsOn: state.dependsOn } : {}
|
|
2179
|
+
});
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
return builder;
|
|
2183
|
+
}
|
|
2184
|
+
/**
|
|
2185
|
+
* Create an AI agent step builder from a tagged template.
|
|
2186
|
+
* `agent\`Build and test\`` creates a builder with `engine: "default"`.
|
|
2187
|
+
* References interpolated in the template are collected and added to the
|
|
2188
|
+
* step's inputs automatically (same as `$`).
|
|
2189
|
+
*/
|
|
2190
|
+
function agent(strings, ...values) {
|
|
2191
|
+
let prompt = "";
|
|
2192
|
+
const collectedInputs = [];
|
|
2193
|
+
for (let i = 0; i < strings.length; i++) {
|
|
2194
|
+
prompt += strings[i];
|
|
2195
|
+
if (i < values.length) {
|
|
2196
|
+
const v = values[i];
|
|
2197
|
+
if (typeof v === "string") prompt += v;
|
|
2198
|
+
else if (isReference(v)) {
|
|
2199
|
+
const ref = v;
|
|
2200
|
+
collectedInputs.push(ref);
|
|
2201
|
+
if (ref.kind === "step") prompt += `\${${ref.step}.${ref.output}}`;
|
|
2202
|
+
else prompt += `\${${ref.namespace}.${ref.field}}`;
|
|
2203
|
+
} else throw new SdkError(`invalid interpolation value at position ${i}: expected string or Reference`, "INVALID_INTERPOLATION");
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
return createAgentBuilder({
|
|
2207
|
+
prompt,
|
|
2208
|
+
engine: "default",
|
|
2209
|
+
tools: [],
|
|
2210
|
+
collectedInputs
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
//#endregion
|
|
2214
|
+
export { $, CheckError, CompositionError, CoreError, DEFAULT_POLICY, DiscoveryError, PlannerError, PlanningError, SdkError, agent, artifact, bindRunPlan, callPipeline, change, component, computeFingerprint, computePlanId, computeReachableSteps, createBaseline, createBuiltinResolver, createPlanner, createPolicy, createSverka, defineWorkflow, env, evaluatePolicy, event, execute, expandMatrixSteps, expr, extractFindings, filterOnlyNew, findConfig, git, image, images, inputs, loadBaseline, loadWorkflow, matrix, matrix$1 as matrixContext, normalizeSarif, parallel, pipeline, pipeline$1 as pipelineV0, plan, run, run$1 as runContext, saveBaseline, secrets, shell, status, task, toPlan, updateBaseline, validatePlan, when, when$1 as whenV0, workflow };
|
|
2215
|
+
|
|
2216
|
+
//# sourceMappingURL=index.mjs.map
|