swallowkit 2.0.0-alpha.5 → 2.0.0-alpha.7
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/cli/commands/provision.d.ts +4 -1
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +233 -69
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +14 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/core/operations/provision-deployment.d.ts +45 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +151 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +39 -5
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +609 -103
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +8 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +11 -3
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/state.d.ts +7 -0
- package/dist/core/project/state.d.ts.map +1 -1
- package/dist/core/project/state.js +86 -0
- package/dist/core/project/state.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +30 -2
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +17 -11
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +2 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +58 -12
- package/dist/mcp/index.js.map +1 -1
- package/dist/utils/azure-cli.d.ts +16 -4
- package/dist/utils/azure-cli.d.ts.map +1 -1
- package/dist/utils/azure-cli.js +166 -4
- package/dist/utils/azure-cli.js.map +1 -1
- package/dist/utils/azure-diagnostics.d.ts +16 -0
- package/dist/utils/azure-diagnostics.d.ts.map +1 -0
- package/dist/utils/azure-diagnostics.js +68 -0
- package/dist/utils/azure-diagnostics.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/agent-instructions.test.ts +5 -0
- package/src/__tests__/agent-loop-phase34.test.ts +19 -8
- package/src/__tests__/azure-cli.test.ts +5 -2
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +138 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/mcp.test.ts +56 -0
- package/src/__tests__/provision-runtime.test.ts +636 -0
- package/src/cli/commands/provision.ts +243 -70
- package/src/cli/index.ts +12 -2
- package/src/core/operations/provision-deployment.ts +167 -0
- package/src/core/operations/provision-operations.ts +597 -145
- package/src/core/project/agent-templates/AGENTS.md +22 -0
- package/src/core/project/agent-templates/CLAUDE.md +7 -0
- package/src/core/project/agent-templates/copilot-instructions.md +8 -0
- package/src/core/project/capabilities.ts +19 -3
- package/src/core/project/state.ts +70 -0
- package/src/core/project/workflows.ts +30 -2
- package/src/machine/index.ts +20 -14
- package/src/mcp/index.ts +50 -15
- package/src/utils/azure-cli.ts +167 -6
- package/src/utils/azure-diagnostics.ts +75 -0
package/src/utils/azure-cli.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* run through cmd.exe with every argument quoted.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { spawnSync, SpawnSyncOptions } from "child_process";
|
|
10
|
+
import { spawn, spawnSync, SpawnSyncOptions } from "child_process";
|
|
11
11
|
import * as fs from "fs";
|
|
12
12
|
import * as path from "path";
|
|
13
13
|
|
|
@@ -26,6 +26,20 @@ export interface AzCliResult {
|
|
|
26
26
|
stdout: string;
|
|
27
27
|
stderr: string;
|
|
28
28
|
error?: Error;
|
|
29
|
+
signal: NodeJS.Signals | null;
|
|
30
|
+
elapsedMs: number;
|
|
31
|
+
failureKind?: "timeout" | "buffer" | "cancelled" | "spawn" | "signal" | "communication" | "exit";
|
|
32
|
+
errorCode?: string;
|
|
33
|
+
stdoutTruncated: boolean;
|
|
34
|
+
stderrTruncated: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface AzCliRunOptions {
|
|
38
|
+
timeout?: number;
|
|
39
|
+
invocation?: AzCliInvocation | null;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
/** Capture is bounded independently of the child's output volume. */
|
|
42
|
+
captureBytes?: number;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
export type AzCliAvailability =
|
|
@@ -79,10 +93,10 @@ export function resolveAzCliInvocation(
|
|
|
79
93
|
export function buildBatchCommandLine(script: string, args: string[]): string {
|
|
80
94
|
for (const value of [script, ...args]) {
|
|
81
95
|
if (/["%\r\n]/.test(value)) {
|
|
82
|
-
throw new Error(`Azure CLI argument cannot be passed safely through ${path.basename(script)}
|
|
96
|
+
throw new Error(`Azure CLI argument cannot be passed safely through ${path.basename(script)} (quotes, percent signs, or newlines).`);
|
|
83
97
|
}
|
|
84
98
|
}
|
|
85
|
-
return `"${[script, ...args].map((value) => `"${value}"`).join(" ")}"`;
|
|
99
|
+
return `"${[script, ...args].map((value) => `"${value.replace(/(\\+)$/, "$1$1")}"`).join(" ")}"`;
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
let cachedInvocation: AzCliInvocation | null | undefined;
|
|
@@ -103,16 +117,18 @@ export function setAzCliInvocationForTesting(invocation: AzCliInvocation | null
|
|
|
103
117
|
|
|
104
118
|
export function runAzCli(
|
|
105
119
|
args: string[],
|
|
106
|
-
options:
|
|
120
|
+
options: AzCliRunOptions = {}
|
|
107
121
|
): AzCliResult {
|
|
122
|
+
const startedAt = Date.now();
|
|
108
123
|
const invocation = options.invocation === undefined ? getAzCliInvocation() : options.invocation;
|
|
109
124
|
if (!invocation) {
|
|
110
|
-
return
|
|
125
|
+
return missingCliResult();
|
|
111
126
|
}
|
|
112
127
|
const spawnOptions: SpawnSyncOptions = {
|
|
113
128
|
shell: false,
|
|
114
129
|
encoding: "utf-8",
|
|
115
130
|
timeout: options.timeout ?? 300_000,
|
|
131
|
+
maxBuffer: options.captureBytes ?? 1_048_576,
|
|
116
132
|
env: invocation.env ? { ...process.env, ...invocation.env } : process.env,
|
|
117
133
|
};
|
|
118
134
|
let result;
|
|
@@ -124,16 +140,161 @@ export function runAzCli(
|
|
|
124
140
|
})
|
|
125
141
|
: spawnSync(invocation.command, [...invocation.prefixArgs, ...args], spawnOptions);
|
|
126
142
|
} catch (error) {
|
|
127
|
-
return {
|
|
143
|
+
return {
|
|
144
|
+
...missingCliResult(),
|
|
145
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
146
|
+
errorCode: (error as NodeJS.ErrnoException)?.code,
|
|
147
|
+
elapsedMs: Date.now() - startedAt,
|
|
148
|
+
};
|
|
128
149
|
}
|
|
150
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
129
151
|
return {
|
|
130
152
|
status: result.status,
|
|
131
153
|
stdout: String(result.stdout ?? ""),
|
|
132
154
|
stderr: String(result.stderr ?? ""),
|
|
155
|
+
signal: result.signal ?? null,
|
|
156
|
+
elapsedMs: Date.now() - startedAt,
|
|
157
|
+
stdoutTruncated: errorCode === "ENOBUFS",
|
|
158
|
+
stderrTruncated: errorCode === "ENOBUFS",
|
|
159
|
+
...(errorCode ? { errorCode } : {}),
|
|
160
|
+
...(errorCode === "ETIMEDOUT" ? { failureKind: "timeout" as const }
|
|
161
|
+
: errorCode === "ENOBUFS" ? { failureKind: "buffer" as const }
|
|
162
|
+
: result.error ? { failureKind: "spawn" as const }
|
|
163
|
+
: result.signal ? { failureKind: "signal" as const }
|
|
164
|
+
: result.status !== 0 ? { failureKind: "exit" as const } : {}),
|
|
133
165
|
...(result.error ? { error: result.error } : {}),
|
|
134
166
|
};
|
|
135
167
|
}
|
|
136
168
|
|
|
169
|
+
function missingCliResult(): AzCliResult {
|
|
170
|
+
return {
|
|
171
|
+
status: null, stdout: "", stderr: "", signal: null, elapsedMs: 0,
|
|
172
|
+
stdoutTruncated: false, stderrTruncated: false,
|
|
173
|
+
error: Object.assign(new Error("az was not found on PATH"), { code: "ENOENT" }),
|
|
174
|
+
errorCode: "ENOENT", failureKind: "spawn",
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Unlike spawnSync/maxBuffer, discarding excess output never kills the Azure CLI. */
|
|
179
|
+
export async function runAzCliAsync(args: string[], options: AzCliRunOptions = {}): Promise<AzCliResult> {
|
|
180
|
+
const invocation = options.invocation === undefined ? getAzCliInvocation() : options.invocation;
|
|
181
|
+
if (!invocation) return missingCliResult();
|
|
182
|
+
const timeout = options.timeout ?? 60_000;
|
|
183
|
+
const captureBytes = options.captureBytes ?? 65_536;
|
|
184
|
+
if (!Number.isFinite(timeout) || timeout <= 0 || !Number.isInteger(captureBytes) || captureBytes < 1) {
|
|
185
|
+
throw new Error("Azure CLI timeout and captureBytes must be finite positive values.");
|
|
186
|
+
}
|
|
187
|
+
const startedAt = Date.now();
|
|
188
|
+
const result: AzCliResult = {
|
|
189
|
+
status: null, stdout: "", stderr: "", signal: null, elapsedMs: 0,
|
|
190
|
+
stdoutTruncated: false, stderrTruncated: false,
|
|
191
|
+
};
|
|
192
|
+
if (options.signal?.aborted) return { ...result, failureKind: "cancelled" };
|
|
193
|
+
let commandArgs: string[];
|
|
194
|
+
try {
|
|
195
|
+
commandArgs = invocation.batchScript
|
|
196
|
+
? ["/d", "/s", "/v:off", "/c", buildBatchCommandLine(invocation.batchScript, args)]
|
|
197
|
+
: [...invocation.prefixArgs, ...args];
|
|
198
|
+
} catch (error) {
|
|
199
|
+
return { ...result, error: error instanceof Error ? error : new Error(String(error)), failureKind: "spawn" };
|
|
200
|
+
}
|
|
201
|
+
return new Promise<AzCliResult>((resolve) => {
|
|
202
|
+
let stdout = Buffer.alloc(0);
|
|
203
|
+
let stderr = Buffer.alloc(0);
|
|
204
|
+
let finished = false;
|
|
205
|
+
let drainTimer: NodeJS.Timeout | undefined;
|
|
206
|
+
let child: ReturnType<typeof spawn>;
|
|
207
|
+
const finish = () => {
|
|
208
|
+
if (finished) return;
|
|
209
|
+
finished = true;
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
if (drainTimer) clearTimeout(drainTimer);
|
|
212
|
+
options.signal?.removeEventListener("abort", abort);
|
|
213
|
+
result.stdout = stdout.toString("utf-8");
|
|
214
|
+
result.stderr = stderr.toString("utf-8");
|
|
215
|
+
result.elapsedMs = Date.now() - startedAt;
|
|
216
|
+
if (!result.failureKind && result.status !== 0) {
|
|
217
|
+
result.failureKind = result.signal ? "signal"
|
|
218
|
+
: /ConnectionReset|ConnectionError|connection.*(?:closed|reset|aborted)|ReadTimeout|RemoteDisconnected|ETIMEDOUT|ECONNRESET/i.test(result.stderr)
|
|
219
|
+
? "communication" : "exit";
|
|
220
|
+
}
|
|
221
|
+
resolve(result);
|
|
222
|
+
};
|
|
223
|
+
const stop = (kind: "timeout" | "cancelled") => {
|
|
224
|
+
if (finished || result.failureKind === "timeout" || result.failureKind === "cancelled") return;
|
|
225
|
+
result.failureKind = kind;
|
|
226
|
+
if (kind === "timeout") result.errorCode = "ETIMEDOUT";
|
|
227
|
+
child?.kill("SIGTERM");
|
|
228
|
+
// Descendants can inherit pipes; do not wait indefinitely for their close events.
|
|
229
|
+
drainTimer = setTimeout(() => {
|
|
230
|
+
child?.kill("SIGKILL");
|
|
231
|
+
child?.stdout?.destroy();
|
|
232
|
+
child?.stderr?.destroy();
|
|
233
|
+
result.stdoutTruncated = true;
|
|
234
|
+
result.stderrTruncated = true;
|
|
235
|
+
finish();
|
|
236
|
+
}, 1_000);
|
|
237
|
+
};
|
|
238
|
+
const abort = () => stop("cancelled");
|
|
239
|
+
const timer = setTimeout(() => stop("timeout"), timeout);
|
|
240
|
+
try {
|
|
241
|
+
child = spawn(invocation.command, commandArgs, {
|
|
242
|
+
shell: false,
|
|
243
|
+
windowsHide: true,
|
|
244
|
+
...(invocation.batchScript ? { windowsVerbatimArguments: true } : {}),
|
|
245
|
+
env: invocation.env ? { ...process.env, ...invocation.env } : process.env,
|
|
246
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
result.error = error instanceof Error ? error : new Error(String(error));
|
|
250
|
+
result.errorCode = (error as NodeJS.ErrnoException)?.code;
|
|
251
|
+
result.failureKind = "spawn";
|
|
252
|
+
finish();
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const capture = (stream: "stdout" | "stderr", chunk: Buffer) => {
|
|
256
|
+
const buffer = stream === "stdout" ? stdout : stderr;
|
|
257
|
+
const remaining = captureBytes - buffer.length;
|
|
258
|
+
if (chunk.length > remaining) result[`${stream}Truncated`] = true;
|
|
259
|
+
if (remaining <= 0) return;
|
|
260
|
+
const combined = Buffer.concat([buffer, chunk.subarray(0, Math.max(remaining, 0))]);
|
|
261
|
+
if (stream === "stdout") stdout = combined;
|
|
262
|
+
else stderr = combined;
|
|
263
|
+
};
|
|
264
|
+
child.stdout?.on("data", (chunk: Buffer) => capture("stdout", chunk));
|
|
265
|
+
child.stderr?.on("data", (chunk: Buffer) => capture("stderr", chunk));
|
|
266
|
+
child.on("error", (error: NodeJS.ErrnoException) => {
|
|
267
|
+
if (finished) return;
|
|
268
|
+
result.error = error;
|
|
269
|
+
result.errorCode = error.code;
|
|
270
|
+
result.failureKind ??= "spawn";
|
|
271
|
+
finish();
|
|
272
|
+
});
|
|
273
|
+
child.on("exit", (code, signal) => {
|
|
274
|
+
if (finished) return;
|
|
275
|
+
result.status = code;
|
|
276
|
+
result.signal = signal;
|
|
277
|
+
if (!drainTimer) {
|
|
278
|
+
drainTimer = setTimeout(() => {
|
|
279
|
+
child.stdout?.destroy();
|
|
280
|
+
child.stderr?.destroy();
|
|
281
|
+
result.stdoutTruncated = true;
|
|
282
|
+
result.stderrTruncated = true;
|
|
283
|
+
finish();
|
|
284
|
+
}, 1_000);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
child.on("close", (code, signal) => {
|
|
288
|
+
if (finished) return;
|
|
289
|
+
result.status = code;
|
|
290
|
+
result.signal = signal;
|
|
291
|
+
finish();
|
|
292
|
+
});
|
|
293
|
+
options.signal?.addEventListener("abort", abort, { once: true });
|
|
294
|
+
if (options.signal?.aborted) abort();
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
137
298
|
export function checkAzCliAvailability(): AzCliAvailability {
|
|
138
299
|
const invocation = getAzCliInvocation();
|
|
139
300
|
if (!invocation) {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { AzCliResult } from "./azure-cli";
|
|
2
|
+
|
|
3
|
+
const SENSITIVE_KEY = /outputs|parameters|password|secret|token|authorization|connection.?string|accountkey|api.?key|functions?.?key|client.?secret|access.?key|primary.?key|secondary.?key/i;
|
|
4
|
+
|
|
5
|
+
export function redactAzureText(text: string, secretValues: readonly string[] = []): string {
|
|
6
|
+
const redactValues = (value: string) => {
|
|
7
|
+
for (const secret of secretValues) {
|
|
8
|
+
if (secret) value = value.split(secret).join("[REDACTED]").split(JSON.stringify(secret).slice(1, -1)).join("[REDACTED]");
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
};
|
|
12
|
+
text = redactValues(text);
|
|
13
|
+
let value = text;
|
|
14
|
+
try {
|
|
15
|
+
const redact = (entry: unknown): unknown => {
|
|
16
|
+
if (typeof entry === "string") return redactPlainText(redactValues(entry));
|
|
17
|
+
if (Array.isArray(entry)) return entry.map(redact);
|
|
18
|
+
if (entry && typeof entry === "object") {
|
|
19
|
+
return Object.fromEntries(Object.entries(entry).map(([key, item]) => [key, SENSITIVE_KEY.test(key) ? "[REDACTED]" : redact(item)]));
|
|
20
|
+
}
|
|
21
|
+
return entry;
|
|
22
|
+
};
|
|
23
|
+
value = JSON.stringify(redact(JSON.parse(text)));
|
|
24
|
+
} catch {
|
|
25
|
+
// An incomplete deployment payload cannot be safely projected/redacted.
|
|
26
|
+
if (/"(?:outputs|parameters)"\s*:/i.test(text)) return "[Azure payload omitted: may contain secrets]";
|
|
27
|
+
value = redactPlainText(text);
|
|
28
|
+
}
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function redactPlainText(text: string): string {
|
|
33
|
+
return text
|
|
34
|
+
.replace(/\bBearer\s+[^\s"',;]+/gi, "Bearer [REDACTED]")
|
|
35
|
+
.replace(/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, "[REDACTED]")
|
|
36
|
+
.replace(/(?:AccountEndpoint|DefaultEndpointsProtocol)=[^\r\n"']+/gi, "[connection string REDACTED]")
|
|
37
|
+
.replace(/((?:api[-_ ]?key|functions?[-_ ]?key|accountkey|password|client[-_ ]?secret|secret|token|access[-_ ]?key|primary[-_ ]?key|secondary[-_ ]?key|authorization)\s*["']?\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;}\]]+)/gi, "$1[REDACTED]")
|
|
38
|
+
.replace(/([?&](?:code|sig|token|key|secret|password)=)[^&\s"'<>]+/gi, "$1[REDACTED]");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AzureCliDiagnostic {
|
|
42
|
+
phase: string;
|
|
43
|
+
exitCode: number | null;
|
|
44
|
+
signal: NodeJS.Signals | null;
|
|
45
|
+
elapsedMs: number;
|
|
46
|
+
failureKind?: AzCliResult["failureKind"];
|
|
47
|
+
errorCode?: string;
|
|
48
|
+
stdout: string;
|
|
49
|
+
stderr: string;
|
|
50
|
+
outputTruncated: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function azureCliDiagnostic(result: AzCliResult, phase: string, secretValues: readonly string[] = []): AzureCliDiagnostic {
|
|
54
|
+
const stdout = result.stdoutTruncated ? "[stdout omitted: capture limit reached]" : redactAzureText(result.stdout, secretValues);
|
|
55
|
+
const stderr = result.stderrTruncated ? "[stderr omitted: capture limit reached]" : redactAzureText(result.stderr, secretValues);
|
|
56
|
+
return {
|
|
57
|
+
phase,
|
|
58
|
+
exitCode: result.status,
|
|
59
|
+
signal: result.signal ?? null,
|
|
60
|
+
elapsedMs: result.elapsedMs,
|
|
61
|
+
...(result.failureKind ? { failureKind: result.failureKind } : {}),
|
|
62
|
+
...(result.errorCode ? { errorCode: result.errorCode } : {}),
|
|
63
|
+
stdout: stdout.slice(0, 4_000),
|
|
64
|
+
stderr: stderr.slice(0, 4_000),
|
|
65
|
+
outputTruncated: result.stdoutTruncated || result.stderrTruncated || stdout.length > 4_000 || stderr.length > 4_000,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatAzureDiagnostic(diagnostic: AzureCliDiagnostic): string {
|
|
70
|
+
return [
|
|
71
|
+
`az ${diagnostic.phase}: exit=${diagnostic.exitCode ?? "unknown"}, signal=${diagnostic.signal ?? "none"}, kind=${diagnostic.failureKind ?? "none"}, elapsed=${diagnostic.elapsedMs}ms${diagnostic.errorCode ? `, error=${diagnostic.errorCode}` : ""}${diagnostic.outputTruncated ? ", output omitted/truncated" : ""}`,
|
|
72
|
+
...(diagnostic.stdout ? [`stdout: ${diagnostic.stdout}`] : []),
|
|
73
|
+
...(diagnostic.stderr ? [`stderr: ${diagnostic.stderr}`] : []),
|
|
74
|
+
].join("\n");
|
|
75
|
+
}
|