swallowkit 1.1.2 → 2.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +61 -7
- package/README.md +61 -7
- package/dist/cli/commands/add-auth.d.ts.map +1 -1
- package/dist/cli/commands/add-auth.js +103 -27
- package/dist/cli/commands/add-auth.js.map +1 -1
- package/dist/cli/commands/configure-mcp.d.ts +12 -0
- package/dist/cli/commands/configure-mcp.d.ts.map +1 -0
- package/dist/cli/commands/configure-mcp.js +42 -0
- package/dist/cli/commands/configure-mcp.js.map +1 -0
- package/dist/cli/commands/dev.d.ts +5 -2
- package/dist/cli/commands/dev.d.ts.map +1 -1
- package/dist/cli/commands/dev.js +143 -67
- package/dist/cli/commands/dev.js.map +1 -1
- package/dist/cli/commands/init.d.ts +14 -7
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +149 -170
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/migrations.d.ts.map +1 -1
- package/dist/cli/commands/migrations.js +25 -0
- package/dist/cli/commands/migrations.js.map +1 -1
- package/dist/cli/commands/provision.d.ts +5 -0
- package/dist/cli/commands/provision.d.ts.map +1 -1
- package/dist/cli/commands/provision.js +278 -55
- package/dist/cli/commands/provision.js.map +1 -1
- package/dist/cli/commands/scaffold.d.ts +5 -1
- package/dist/cli/commands/scaffold.d.ts.map +1 -1
- package/dist/cli/commands/scaffold.js +123 -18
- package/dist/cli/commands/scaffold.js.map +1 -1
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +49 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/core/config.d.ts +2 -1
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +19 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/operations/create-model.d.ts +2 -0
- package/dist/core/operations/create-model.d.ts.map +1 -1
- package/dist/core/operations/create-model.js +60 -8
- package/dist/core/operations/create-model.js.map +1 -1
- package/dist/core/operations/migration-generator.d.ts.map +1 -1
- package/dist/core/operations/migration-generator.js +12 -1
- package/dist/core/operations/migration-generator.js.map +1 -1
- package/dist/core/operations/model-migration.d.ts +22 -0
- package/dist/core/operations/model-migration.d.ts.map +1 -0
- package/dist/core/operations/model-migration.js +36 -0
- package/dist/core/operations/model-migration.js.map +1 -0
- package/dist/core/operations/provision-deployment.d.ts +52 -0
- package/dist/core/operations/provision-deployment.d.ts.map +1 -0
- package/dist/core/operations/provision-deployment.js +164 -0
- package/dist/core/operations/provision-deployment.js.map +1 -0
- package/dist/core/operations/provision-operations.d.ts +62 -6
- package/dist/core/operations/provision-operations.d.ts.map +1 -1
- package/dist/core/operations/provision-operations.js +653 -115
- package/dist/core/operations/provision-operations.js.map +1 -1
- package/dist/core/operations/scaffold-apply.d.ts +1 -0
- package/dist/core/operations/scaffold-apply.d.ts.map +1 -1
- package/dist/core/operations/scaffold-apply.js +1 -0
- package/dist/core/operations/scaffold-apply.js.map +1 -1
- package/dist/core/operations/scaffold-machine.d.ts +1 -0
- package/dist/core/operations/scaffold-machine.d.ts.map +1 -1
- package/dist/core/operations/scaffold-machine.js.map +1 -1
- package/dist/core/operations/scaffold-plan.d.ts +2 -0
- package/dist/core/operations/scaffold-plan.d.ts.map +1 -1
- package/dist/core/operations/scaffold-plan.js +3 -1
- package/dist/core/operations/scaffold-plan.js.map +1 -1
- package/dist/core/project/agent-instructions.d.ts +2 -1
- package/dist/core/project/agent-instructions.d.ts.map +1 -1
- package/dist/core/project/agent-instructions.js +15 -0
- package/dist/core/project/agent-instructions.js.map +1 -1
- package/dist/core/project/boundaries.d.ts +5 -2
- package/dist/core/project/boundaries.d.ts.map +1 -1
- package/dist/core/project/boundaries.js +51 -20
- package/dist/core/project/boundaries.js.map +1 -1
- package/dist/core/project/capabilities.d.ts +17 -0
- package/dist/core/project/capabilities.d.ts.map +1 -1
- package/dist/core/project/capabilities.js +45 -6
- package/dist/core/project/capabilities.js.map +1 -1
- package/dist/core/project/home-page.d.ts +4 -0
- package/dist/core/project/home-page.d.ts.map +1 -0
- package/dist/core/project/home-page.js +113 -0
- package/dist/core/project/home-page.js.map +1 -0
- package/dist/core/project/manifest.d.ts +3 -1
- package/dist/core/project/manifest.d.ts.map +1 -1
- package/dist/core/project/manifest.js +47 -25
- package/dist/core/project/manifest.js.map +1 -1
- package/dist/core/project/mcp-config.d.ts +26 -0
- package/dist/core/project/mcp-config.d.ts.map +1 -0
- package/dist/core/project/mcp-config.js +117 -0
- package/dist/core/project/mcp-config.js.map +1 -0
- package/dist/core/project/react-project.d.ts +26 -0
- package/dist/core/project/react-project.d.ts.map +1 -0
- package/dist/core/project/react-project.js +443 -0
- package/dist/core/project/react-project.js.map +1 -0
- 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/validation.d.ts.map +1 -1
- package/dist/core/project/validation.js +32 -10
- package/dist/core/project/validation.js.map +1 -1
- package/dist/core/project/workflows.d.ts.map +1 -1
- package/dist/core/project/workflows.js +53 -7
- package/dist/core/project/workflows.js.map +1 -1
- package/dist/core/scaffold/auth-generator.d.ts.map +1 -1
- package/dist/core/scaffold/auth-generator.js +26 -27
- package/dist/core/scaffold/auth-generator.js.map +1 -1
- package/dist/core/scaffold/model-parser.d.ts +23 -0
- package/dist/core/scaffold/model-parser.d.ts.map +1 -1
- package/dist/core/scaffold/model-parser.js +407 -137
- package/dist/core/scaffold/model-parser.js.map +1 -1
- package/dist/core/scaffold/native-schema-generator.d.ts +13 -0
- package/dist/core/scaffold/native-schema-generator.d.ts.map +1 -1
- package/dist/core/scaffold/native-schema-generator.js +294 -91
- package/dist/core/scaffold/native-schema-generator.js.map +1 -1
- package/dist/core/scaffold/openapi-generator.d.ts +5 -0
- package/dist/core/scaffold/openapi-generator.d.ts.map +1 -1
- package/dist/core/scaffold/openapi-generator.js +53 -3
- package/dist/core/scaffold/openapi-generator.js.map +1 -1
- package/dist/core/scaffold/react-bff-generator.d.ts +10 -0
- package/dist/core/scaffold/react-bff-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-bff-generator.js +252 -0
- package/dist/core/scaffold/react-bff-generator.js.map +1 -0
- package/dist/core/scaffold/react-ui-generator.d.ts +6 -0
- package/dist/core/scaffold/react-ui-generator.d.ts.map +1 -0
- package/dist/core/scaffold/react-ui-generator.js +41 -0
- package/dist/core/scaffold/react-ui-generator.js.map +1 -0
- package/dist/core/scaffold/ui-generator.d.ts +0 -3
- package/dist/core/scaffold/ui-generator.d.ts.map +1 -1
- package/dist/core/scaffold/ui-generator.js +68 -7
- package/dist/core/scaffold/ui-generator.js.map +1 -1
- package/dist/core/verify/index.d.ts +4 -3
- package/dist/core/verify/index.d.ts.map +1 -1
- package/dist/core/verify/index.js +122 -5
- package/dist/core/verify/index.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/machine/contracts.d.ts +1 -0
- package/dist/machine/contracts.d.ts.map +1 -1
- package/dist/machine/errors.d.ts +3 -2
- package/dist/machine/errors.d.ts.map +1 -1
- package/dist/machine/errors.js +2 -1
- package/dist/machine/errors.js.map +1 -1
- package/dist/machine/index.d.ts.map +1 -1
- package/dist/machine/index.js +61 -30
- package/dist/machine/index.js.map +1 -1
- package/dist/mcp/index.d.ts +6 -4
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +210 -25
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/version-selection.d.ts +55 -0
- package/dist/mcp/version-selection.d.ts.map +1 -0
- package/dist/mcp/version-selection.js +152 -0
- package/dist/mcp/version-selection.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/utils/azure-cli.d.ts +56 -0
- package/dist/utils/azure-cli.d.ts.map +1 -0
- package/dist/utils/azure-cli.js +326 -0
- package/dist/utils/azure-cli.js.map +1 -0
- 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/dist/utils/package-manager.d.ts +2 -2
- package/dist/utils/package-manager.d.ts.map +1 -1
- package/dist/utils/package-manager.js +6 -3
- package/dist/utils/package-manager.js.map +1 -1
- package/dist/utils/python-uv.d.ts +5 -7
- package/dist/utils/python-uv.d.ts.map +1 -1
- package/dist/utils/python-uv.js +50 -24
- package/dist/utils/python-uv.js.map +1 -1
- package/dist/utils/react-dev.d.ts +4 -0
- package/dist/utils/react-dev.d.ts.map +1 -0
- package/dist/utils/react-dev.js +35 -0
- package/dist/utils/react-dev.js.map +1 -0
- package/dist/utils/semver.d.ts +14 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +61 -0
- package/dist/utils/semver.js.map +1 -0
- package/package.json +7 -4
- package/src/__tests__/__snapshots__/boundaries.test.ts.snap +154 -0
- package/src/__tests__/agent-instructions.test.ts +6 -1
- package/src/__tests__/agent-loop-phase34.test.ts +143 -8
- package/src/__tests__/auth.test.ts +9 -0
- package/src/__tests__/azure-cli.test.ts +202 -0
- package/src/__tests__/boundaries.test.ts +42 -0
- package/src/__tests__/csharp-schema.test.ts +261 -0
- package/src/__tests__/dev.test.ts +13 -6
- package/src/__tests__/fixtures/az wrapper.cmd +2 -0
- package/src/__tests__/fixtures/azure-cli.cjs +153 -0
- package/src/__tests__/fixtures/provision-runner.cjs +28 -0
- package/src/__tests__/generated-project-matrix.test.ts +789 -0
- package/src/__tests__/home-page.test.ts +50 -0
- package/src/__tests__/init.test.ts +145 -5
- package/src/__tests__/mcp-version.test.ts +262 -0
- package/src/__tests__/mcp.test.ts +120 -3
- package/src/__tests__/multi-auth.test.ts +115 -4
- package/src/__tests__/package-manager.test.ts +17 -2
- package/src/__tests__/plan-apply.test.ts +179 -0
- package/src/__tests__/provision-runtime.test.ts +722 -0
- package/src/__tests__/python-uv.test.ts +50 -14
- package/src/__tests__/python-venv.test.ts +48 -0
- package/src/__tests__/react-dev.test.ts +72 -0
- package/src/__tests__/react.test.ts +838 -0
- package/src/__tests__/release-metadata.test.ts +121 -0
- package/src/__tests__/schema-analysis.test.ts +329 -0
- package/src/cli/commands/add-auth.ts +103 -29
- package/src/cli/commands/configure-mcp.ts +54 -0
- package/src/cli/commands/dev.ts +145 -73
- package/src/cli/commands/init.ts +147 -172
- package/src/cli/commands/migrations.ts +22 -0
- package/src/cli/commands/provision.ts +294 -56
- package/src/cli/commands/scaffold.ts +145 -22
- package/src/cli/index.ts +46 -10
- package/src/core/config.ts +21 -1
- package/src/core/operations/create-model.ts +51 -13
- package/src/core/operations/migration-generator.ts +14 -1
- package/src/core/operations/model-migration.ts +52 -0
- package/src/core/operations/provision-deployment.ts +183 -0
- package/src/core/operations/provision-operations.ts +662 -154
- package/src/core/operations/scaffold-apply.ts +2 -0
- package/src/core/operations/scaffold-machine.ts +1 -0
- package/src/core/operations/scaffold-plan.ts +5 -1
- package/src/core/project/agent-instructions.ts +17 -1
- package/src/core/project/agent-templates/AGENTS.md +113 -15
- package/src/core/project/agent-templates/CLAUDE.md +14 -1
- package/src/core/project/agent-templates/bff-routes.instructions.md +5 -3
- package/src/core/project/agent-templates/copilot-instructions.md +15 -1
- package/src/core/project/agent-templates/shared-models.instructions.md +11 -3
- package/src/core/project/boundaries.ts +62 -20
- package/src/core/project/capabilities.ts +63 -6
- package/src/core/project/home-page.ts +114 -0
- package/src/core/project/manifest.ts +52 -27
- package/src/core/project/mcp-config.ts +84 -0
- package/src/core/project/react-project.ts +529 -0
- package/src/core/project/state.ts +70 -0
- package/src/core/project/validation.ts +32 -10
- package/src/core/project/workflows.ts +53 -7
- package/src/core/scaffold/auth-generator.ts +26 -27
- package/src/core/scaffold/model-parser.ts +413 -129
- package/src/core/scaffold/native-schema-generator.ts +319 -103
- package/src/core/scaffold/openapi-generator.ts +63 -5
- package/src/core/scaffold/react-bff-generator.ts +292 -0
- package/src/core/scaffold/react-ui-generator.ts +51 -0
- package/src/core/scaffold/ui-generator.ts +70 -8
- package/src/core/verify/index.ts +118 -6
- package/src/index.ts +3 -1
- package/src/machine/contracts.ts +1 -0
- package/src/machine/errors.ts +4 -2
- package/src/machine/index.ts +67 -36
- package/src/mcp/index.ts +221 -31
- package/src/mcp/version-selection.ts +163 -0
- package/src/types/index.ts +4 -0
- package/src/utils/azure-cli.ts +315 -0
- package/src/utils/azure-diagnostics.ts +75 -0
- package/src/utils/package-manager.ts +7 -4
- package/src/utils/python-uv.ts +53 -33
- package/src/utils/react-dev.ts +31 -0
- package/src/utils/semver.ts +59 -0
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Azure CLI invocation that works without a shell on every platform.
|
|
3
|
+
*
|
|
4
|
+
* On Windows `az` is a batch wrapper (az.cmd). Node cannot spawn it with `shell: false`
|
|
5
|
+
* (ENOENT for "az", EINVAL for "az.cmd"), so the wrapper is resolved explicitly:
|
|
6
|
+
* the MSI layout runs its bundled Python exactly like az.cmd does, and other wrappers
|
|
7
|
+
* run through cmd.exe with every argument quoted.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn, spawnSync, SpawnSyncOptions } from "child_process";
|
|
11
|
+
import * as fs from "fs";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
|
|
14
|
+
export interface AzCliInvocation {
|
|
15
|
+
command: string;
|
|
16
|
+
prefixArgs: string[];
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
/** Batch wrapper that must be started through cmd.exe. */
|
|
19
|
+
batchScript?: string;
|
|
20
|
+
/** Resolved executable or wrapper, for diagnostics. */
|
|
21
|
+
source: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface AzCliResult {
|
|
25
|
+
status: number | null;
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
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;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type AzCliAvailability =
|
|
46
|
+
| { available: true; invocation: AzCliInvocation }
|
|
47
|
+
| { available: false; reason: "not-found" | "not-runnable"; detail: string };
|
|
48
|
+
|
|
49
|
+
function findOnWindowsPath(name: string, env: NodeJS.ProcessEnv): string | null {
|
|
50
|
+
const pathValue = env.PATH ?? env.Path ?? "";
|
|
51
|
+
const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean).map((ext) => ext.toLowerCase());
|
|
52
|
+
for (const directory of pathValue.split(";").filter(Boolean)) {
|
|
53
|
+
for (const extension of extensions) {
|
|
54
|
+
const candidate = path.join(directory.replace(/^"|"$/g, ""), `${name}${extension}`);
|
|
55
|
+
try {
|
|
56
|
+
if (fs.statSync(candidate).isFile()) return candidate;
|
|
57
|
+
} catch {
|
|
58
|
+
// Keep searching PATH.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function resolveAzCliInvocation(
|
|
66
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
67
|
+
platform: NodeJS.Platform = process.platform
|
|
68
|
+
): AzCliInvocation | null {
|
|
69
|
+
if (platform !== "win32") return { command: "az", prefixArgs: [], source: "az" };
|
|
70
|
+
|
|
71
|
+
const resolved = findOnWindowsPath("az", env);
|
|
72
|
+
if (!resolved) return null;
|
|
73
|
+
const extension = path.extname(resolved).toLowerCase();
|
|
74
|
+
if (extension === ".exe" || extension === ".com") {
|
|
75
|
+
return { command: resolved, prefixArgs: [], source: resolved };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// MSI install: wbin\az.cmd runs "..\python.exe -IBm azure.cli %*" with AZ_INSTALLER=MSI.
|
|
79
|
+
const bundledPython = path.join(path.dirname(resolved), "..", "python.exe");
|
|
80
|
+
if (path.basename(path.dirname(resolved)).toLowerCase() === "wbin" && fs.existsSync(bundledPython)) {
|
|
81
|
+
return {
|
|
82
|
+
command: path.resolve(bundledPython),
|
|
83
|
+
prefixArgs: ["-IBm", "azure.cli"],
|
|
84
|
+
env: { AZ_INSTALLER: "MSI" },
|
|
85
|
+
source: resolved,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { command: env.ComSpec ?? "cmd.exe", prefixArgs: [], batchScript: resolved, source: resolved };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Builds the cmd.exe /s /c payload. Rejects characters cmd.exe would reinterpret even inside quotes. */
|
|
93
|
+
export function buildBatchCommandLine(script: string, args: string[]): string {
|
|
94
|
+
for (const value of [script, ...args]) {
|
|
95
|
+
if (/["%\r\n]/.test(value)) {
|
|
96
|
+
throw new Error(`Azure CLI argument cannot be passed safely through ${path.basename(script)} (quotes, percent signs, or newlines).`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return `"${[script, ...args].map((value) => `"${value.replace(/(\\+)$/, "$1$1")}"`).join(" ")}"`;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let cachedInvocation: AzCliInvocation | null | undefined;
|
|
103
|
+
|
|
104
|
+
export function getAzCliInvocation(): AzCliInvocation | null {
|
|
105
|
+
if (cachedInvocation === undefined) cachedInvocation = resolveAzCliInvocation();
|
|
106
|
+
return cachedInvocation;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function resetAzCliInvocationCache(): void {
|
|
110
|
+
cachedInvocation = undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Test hook: pin the resolved invocation (null = not found). */
|
|
114
|
+
export function setAzCliInvocationForTesting(invocation: AzCliInvocation | null | undefined): void {
|
|
115
|
+
cachedInvocation = invocation;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function runAzCli(
|
|
119
|
+
args: string[],
|
|
120
|
+
options: AzCliRunOptions = {}
|
|
121
|
+
): AzCliResult {
|
|
122
|
+
const startedAt = Date.now();
|
|
123
|
+
const invocation = options.invocation === undefined ? getAzCliInvocation() : options.invocation;
|
|
124
|
+
if (!invocation) {
|
|
125
|
+
return missingCliResult();
|
|
126
|
+
}
|
|
127
|
+
const spawnOptions: SpawnSyncOptions = {
|
|
128
|
+
shell: false,
|
|
129
|
+
encoding: "utf-8",
|
|
130
|
+
timeout: options.timeout ?? 300_000,
|
|
131
|
+
maxBuffer: options.captureBytes ?? 1_048_576,
|
|
132
|
+
env: invocation.env ? { ...process.env, ...invocation.env } : process.env,
|
|
133
|
+
};
|
|
134
|
+
let result;
|
|
135
|
+
try {
|
|
136
|
+
result = invocation.batchScript
|
|
137
|
+
? spawnSync(invocation.command, ["/d", "/s", "/v:off", "/c", buildBatchCommandLine(invocation.batchScript, args)], {
|
|
138
|
+
...spawnOptions,
|
|
139
|
+
windowsVerbatimArguments: true,
|
|
140
|
+
})
|
|
141
|
+
: spawnSync(invocation.command, [...invocation.prefixArgs, ...args], spawnOptions);
|
|
142
|
+
} catch (error) {
|
|
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
|
+
};
|
|
149
|
+
}
|
|
150
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
151
|
+
return {
|
|
152
|
+
status: result.status,
|
|
153
|
+
stdout: String(result.stdout ?? ""),
|
|
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 } : {}),
|
|
165
|
+
...(result.error ? { error: result.error } : {}),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
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
|
+
|
|
298
|
+
export function checkAzCliAvailability(): AzCliAvailability {
|
|
299
|
+
const invocation = getAzCliInvocation();
|
|
300
|
+
if (!invocation) {
|
|
301
|
+
return { available: false, reason: "not-found", detail: "az was not found on PATH." };
|
|
302
|
+
}
|
|
303
|
+
const result = runAzCli(["--version"], { timeout: 60_000, invocation });
|
|
304
|
+
if (result.status === 0) return { available: true, invocation };
|
|
305
|
+
const errorCode = (result.error as NodeJS.ErrnoException | undefined)?.code;
|
|
306
|
+
if (errorCode === "ENOENT") {
|
|
307
|
+
return { available: false, reason: "not-found", detail: "az was not found on PATH." };
|
|
308
|
+
}
|
|
309
|
+
const output = `${result.stderr || result.stdout}`.trim().split(/\r?\n/).slice(-3).join(" ");
|
|
310
|
+
return {
|
|
311
|
+
available: false,
|
|
312
|
+
reason: "not-runnable",
|
|
313
|
+
detail: `${invocation.source} could not run "az --version" (${result.error?.message ?? `exit ${result.status}`})${output ? `: ${output}` : ""}`,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as fs from "fs";
|
|
2
2
|
import * as path from "path";
|
|
3
3
|
import { execSync } from "child_process";
|
|
4
|
-
import { BackendLanguage } from "../types";
|
|
4
|
+
import { BackendLanguage, FrontendFramework } from "../types";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Supported package managers
|
|
@@ -156,7 +156,7 @@ export function getWorkspaceConfig(pm: PackageManager, workspaces: string[]) {
|
|
|
156
156
|
/** pnpm uses pnpm-workspace.yaml */
|
|
157
157
|
type: "file" as const,
|
|
158
158
|
filename: "pnpm-workspace.yaml",
|
|
159
|
-
content: `packages:\n${workspaces.map((w) => ` - ${w}`).join("\n")}\nallowBuilds:\n protobufjs: false\n unrs-resolver: false\n`,
|
|
159
|
+
content: `packages:\n${workspaces.map((w) => ` - ${w}`).join("\n")}\nallowBuilds:\n keytar: false\n protobufjs: false\n unrs-resolver: false\n`,
|
|
160
160
|
};
|
|
161
161
|
}
|
|
162
162
|
|
|
@@ -214,7 +214,10 @@ export function getAzurePipelinesSetup(pm: PackageManager): string {
|
|
|
214
214
|
/**
|
|
215
215
|
* Build script for generated package.json (depends on workspace command syntax)
|
|
216
216
|
*/
|
|
217
|
-
export function getBuildScript(pm: PackageManager): string {
|
|
217
|
+
export function getBuildScript(pm: PackageManager, frontend: FrontendFramework = "nextjs"): string {
|
|
218
|
+
if (frontend === "react") {
|
|
219
|
+
return `${getCommands(pm).runFilter("shared")} build && tsc --noEmit && vite build && node -e "require('fs').copyFileSync('staticwebapp.config.json','dist/staticwebapp.config.json')" && ${getCommands(pm).runFilter("bff")} build`;
|
|
220
|
+
}
|
|
218
221
|
const copyStandaloneAssets = `node -e "const fs=require('fs');fs.mkdirSync('.next/standalone/.next',{recursive:true});if(fs.existsSync('.next/static'))fs.cpSync('.next/static','.next/standalone/.next/static',{recursive:true});if(fs.existsSync('public'))fs.cpSync('public','.next/standalone/public',{recursive:true});"`;
|
|
219
222
|
|
|
220
223
|
if (pm === "pnpm") {
|
|
@@ -247,5 +250,5 @@ export function getFunctionsStartScript(
|
|
|
247
250
|
return "cd functions && npm start";
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
return "cd functions && func start";
|
|
253
|
+
return backendLanguage === "csharp" ? "cd functions && dotnet run" : "cd functions && func start";
|
|
251
254
|
}
|
package/src/utils/python-uv.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import * as path from "path";
|
|
2
|
+
import * as fs from "fs";
|
|
3
|
+
import { execFile } from "child_process";
|
|
4
|
+
import { promisify } from "util";
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
export const DEFAULT_AZURE_PYTHON_VERSION = "3.14";
|
|
4
8
|
|
|
5
9
|
export interface ProjectLocalUvPaths {
|
|
6
10
|
stateDir: string;
|
|
7
|
-
cacheDir: string;
|
|
8
|
-
pythonInstallDir: string;
|
|
9
|
-
toolDir: string;
|
|
10
|
-
toolBinDir: string;
|
|
11
11
|
localUvInstallDir: string;
|
|
12
12
|
localUvExecutable: string;
|
|
13
13
|
}
|
|
@@ -22,10 +22,6 @@ export function getProjectLocalUvPaths(projectRoot: string): ProjectLocalUvPaths
|
|
|
22
22
|
|
|
23
23
|
return {
|
|
24
24
|
stateDir,
|
|
25
|
-
cacheDir: path.join(stateDir, "cache"),
|
|
26
|
-
pythonInstallDir: path.join(stateDir, "python"),
|
|
27
|
-
toolDir: path.join(stateDir, "tools"),
|
|
28
|
-
toolBinDir: path.join(stateDir, "tools", "bin"),
|
|
29
25
|
localUvInstallDir,
|
|
30
26
|
localUvExecutable: process.platform === "win32"
|
|
31
27
|
? path.join(localUvInstallDir, "uv.exe")
|
|
@@ -33,40 +29,26 @@ export function getProjectLocalUvPaths(projectRoot: string): ProjectLocalUvPaths
|
|
|
33
29
|
};
|
|
34
30
|
}
|
|
35
31
|
|
|
36
|
-
export function
|
|
32
|
+
export function buildProjectLocalUvInstallerEnv(
|
|
37
33
|
baseEnv: NodeJS.ProcessEnv,
|
|
38
34
|
projectRoot: string
|
|
39
35
|
): NodeJS.ProcessEnv {
|
|
40
36
|
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
37
|
+
|
|
41
38
|
const env: NodeJS.ProcessEnv = {
|
|
42
39
|
...baseEnv,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
UV_TOOL_DIR: uvPaths.toolDir,
|
|
46
|
-
UV_TOOL_BIN_DIR: uvPaths.toolBinDir,
|
|
47
|
-
UV_PYTHON_PREFERENCE: "only-managed",
|
|
40
|
+
UV_UNMANAGED_INSTALL: uvPaths.localUvInstallDir,
|
|
41
|
+
UV_NO_MODIFY_PATH: "1",
|
|
48
42
|
};
|
|
49
|
-
|
|
43
|
+
// Windows PowerShell must discover its own modules, not inherit PowerShell 7's.
|
|
50
44
|
if (process.platform === "win32") {
|
|
51
|
-
env
|
|
45
|
+
for (const key of Object.keys(env)) {
|
|
46
|
+
if (key.toLowerCase() === "psmodulepath") delete env[key];
|
|
47
|
+
}
|
|
52
48
|
}
|
|
53
|
-
|
|
54
49
|
return env;
|
|
55
50
|
}
|
|
56
51
|
|
|
57
|
-
export function buildProjectLocalUvInstallerEnv(
|
|
58
|
-
baseEnv: NodeJS.ProcessEnv,
|
|
59
|
-
projectRoot: string
|
|
60
|
-
): NodeJS.ProcessEnv {
|
|
61
|
-
const uvPaths = getProjectLocalUvPaths(projectRoot);
|
|
62
|
-
|
|
63
|
-
return {
|
|
64
|
-
...buildProjectLocalUvEnv(baseEnv, projectRoot),
|
|
65
|
-
UV_UNMANAGED_INSTALL: uvPaths.localUvInstallDir,
|
|
66
|
-
UV_NO_MODIFY_PATH: "1",
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
52
|
export function getProjectLocalUvInstallerCommand(): { command: string; args: string[] } {
|
|
71
53
|
if (process.platform === "win32") {
|
|
72
54
|
return {
|
|
@@ -87,10 +69,48 @@ export function getProjectLocalUvInstallerCommand(): { command: string; args: st
|
|
|
87
69
|
};
|
|
88
70
|
}
|
|
89
71
|
|
|
90
|
-
export function buildUvVenvArgs(venvDir: string
|
|
91
|
-
return ["venv", venvDir
|
|
72
|
+
export function buildUvVenvArgs(venvDir: string): string[] {
|
|
73
|
+
return ["venv", venvDir];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getVirtualEnvPythonPath(venvDir: string): string {
|
|
77
|
+
return process.platform === "win32"
|
|
78
|
+
? path.join(venvDir, "Scripts", "python.exe")
|
|
79
|
+
: path.join(venvDir, "bin", "python");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function ensureUvVirtualEnvironment(
|
|
83
|
+
uvCommand: string,
|
|
84
|
+
venvDir: string,
|
|
85
|
+
cwd: string,
|
|
86
|
+
env: NodeJS.ProcessEnv
|
|
87
|
+
): Promise<string> {
|
|
88
|
+
const pythonExecutable = getVirtualEnvPythonPath(venvDir);
|
|
89
|
+
if (!fs.existsSync(venvDir)) {
|
|
90
|
+
const { stdout, stderr } = await execFileAsync(uvCommand, buildUvVenvArgs(venvDir), { cwd, env });
|
|
91
|
+
if (env.SWALLOWKIT_MACHINE_OUTPUT !== "1") {
|
|
92
|
+
if (stdout) process.stdout.write(stdout);
|
|
93
|
+
if (stderr) process.stderr.write(stderr);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await execFileAsync(pythonExecutable, ["--version"], { cwd, env });
|
|
98
|
+
} catch (error) {
|
|
99
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Cannot execute Python at ${pythonExecutable}: ${reason}\n` +
|
|
102
|
+
"The virtual environment was preserved. Check the interpreter and OS application-control policy. " +
|
|
103
|
+
"To switch Python, explicitly recreate this environment with uv and a trusted interpreter; SwallowKit will not clear it automatically."
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return pythonExecutable;
|
|
92
107
|
}
|
|
93
108
|
|
|
94
109
|
export function buildUvPipInstallArgs(pythonExecutable: string, requirementsPath: string): string[] {
|
|
95
110
|
return ["pip", "install", "--python", pythonExecutable, "-r", requirementsPath];
|
|
96
111
|
}
|
|
112
|
+
|
|
113
|
+
export function buildPythonCodegenInstallEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
|
114
|
+
// Codegen reads JSON; PyYAML's optional accelerator must not require a C compiler.
|
|
115
|
+
return { ...env, PYYAML_FORCE_LIBYAML: env.PYYAML_FORCE_LIBYAML ?? "0" };
|
|
116
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const REACT_DEV_WATCH_DIRECTORIES = [
|
|
2
|
+
"functions",
|
|
3
|
+
"bff",
|
|
4
|
+
".uv",
|
|
5
|
+
".swallowkit",
|
|
6
|
+
"shared/dist",
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export function buildReactDevWatchIgnoredSource(rootExpression: string): string {
|
|
10
|
+
return `${JSON.stringify(REACT_DEV_WATCH_DIRECTORIES)}.map(directory => \`\${${rootExpression}.replace(/\\\\/g, '/').replace(/\\/$/, '')}/\${directory}/**\`)`;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function buildReactDevConfigSource(): string {
|
|
14
|
+
return `import { defineConfig, loadConfigFromFile, mergeConfig } from 'vite';
|
|
15
|
+
|
|
16
|
+
export default defineConfig(async env => {
|
|
17
|
+
const loaded = await loadConfigFromFile(env, undefined, process.cwd());
|
|
18
|
+
const config = loaded?.config ?? {};
|
|
19
|
+
if (config.server?.watch === null) return config;
|
|
20
|
+
return mergeConfig(config, {
|
|
21
|
+
plugins: [{
|
|
22
|
+
name: 'swallowkit-dev-watch-boundaries',
|
|
23
|
+
configResolved(resolved) {
|
|
24
|
+
resolved.configFileDependencies.push(...(loaded?.dependencies ?? []));
|
|
25
|
+
},
|
|
26
|
+
}],
|
|
27
|
+
server: { watch: { ignored: ${buildReactDevWatchIgnoredSource("process.cwd()")} } },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
`;
|
|
31
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface ParsedSemver {
|
|
2
|
+
major: number;
|
|
3
|
+
minor: number;
|
|
4
|
+
patch: number;
|
|
5
|
+
prerelease: Array<string | number>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const SEMVER_PATTERN =
|
|
9
|
+
/^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*)(?:\.(?:0|[1-9]\d*|\d*[A-Za-z-][0-9A-Za-z-]*))*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
|
|
10
|
+
|
|
11
|
+
export function parseSemver(value: string | null | undefined): ParsedSemver | null {
|
|
12
|
+
const match = typeof value === "string" ? SEMVER_PATTERN.exec(value.trim()) : null;
|
|
13
|
+
if (!match) return null;
|
|
14
|
+
return {
|
|
15
|
+
major: Number(match[1]),
|
|
16
|
+
minor: Number(match[2]),
|
|
17
|
+
patch: Number(match[3]),
|
|
18
|
+
prerelease: match[4]
|
|
19
|
+
? match[4].split(".").map((identifier) => (/^\d+$/.test(identifier) ? Number(identifier) : identifier))
|
|
20
|
+
: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function isPrerelease(value: string): boolean {
|
|
25
|
+
return (parseSemver(value)?.prerelease.length ?? 0) > 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Compares versions by SemVer 2.0 precedence. Build metadata is ignored.
|
|
30
|
+
* Returns null when either value is not a valid version.
|
|
31
|
+
*/
|
|
32
|
+
export function compareSemver(left: string, right: string): number | null {
|
|
33
|
+
const a = parseSemver(left);
|
|
34
|
+
const b = parseSemver(right);
|
|
35
|
+
if (!a || !b) return null;
|
|
36
|
+
|
|
37
|
+
for (const key of ["major", "minor", "patch"] as const) {
|
|
38
|
+
if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (a.prerelease.length === 0 || b.prerelease.length === 0) {
|
|
42
|
+
if (a.prerelease.length === b.prerelease.length) return 0;
|
|
43
|
+
return a.prerelease.length === 0 ? 1 : -1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const length = Math.max(a.prerelease.length, b.prerelease.length);
|
|
47
|
+
for (let index = 0; index < length; index++) {
|
|
48
|
+
const leftId = a.prerelease[index];
|
|
49
|
+
const rightId = b.prerelease[index];
|
|
50
|
+
if (leftId === undefined) return -1;
|
|
51
|
+
if (rightId === undefined) return 1;
|
|
52
|
+
if (leftId === rightId) continue;
|
|
53
|
+
if (typeof leftId === "number" && typeof rightId === "number") return leftId < rightId ? -1 : 1;
|
|
54
|
+
if (typeof leftId === "number") return -1;
|
|
55
|
+
if (typeof rightId === "number") return 1;
|
|
56
|
+
return leftId < rightId ? -1 : 1;
|
|
57
|
+
}
|
|
58
|
+
return 0;
|
|
59
|
+
}
|