blokctl 0.4.0 → 0.6.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.
@@ -2,6 +2,8 @@ import child_process from "node:child_process";
2
2
  import path from "node:path";
3
3
  import util from "node:util";
4
4
  import fsExtra from "fs-extra";
5
+ import { detectRuntimeVersion } from "./runtime-detector.js";
6
+ import { computeDefaultConstraint, formatVersionMismatch, formatVersionSuccess, satisfiesConstraint, } from "./semver-utils.js";
5
7
  const exec = util.promisify(child_process.exec);
6
8
  export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner) {
7
9
  const sdkSourcePath = path.join(githubRepoLocal, "sdks", runtime.sdkDir);
@@ -49,6 +51,8 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
49
51
  cwd: path.relative(projectDir, blokctlRuntimeDir),
50
52
  kind: runtime.kind,
51
53
  label: runtime.label,
54
+ version: runtime.version,
55
+ requiredVersion: runtime.version ? computeDefaultConstraint(runtime.version) : undefined,
52
56
  transport: "grpc",
53
57
  };
54
58
  }
@@ -194,6 +198,30 @@ stdout_logfile=/var/log/${rc.kind}.out.log
194
198
  }
195
199
  return config;
196
200
  }
201
+ export async function validateProjectRuntimes(projectDir) {
202
+ const config = readProjectConfig(projectDir);
203
+ if (!config?.runtimes)
204
+ return [];
205
+ const results = [];
206
+ for (const [kind, rc] of Object.entries(config.runtimes)) {
207
+ if (!rc.requiredVersion)
208
+ continue;
209
+ const currentVersion = await detectRuntimeVersion(kind);
210
+ const satisfied = currentVersion ? satisfiesConstraint(currentVersion, rc.requiredVersion) : false;
211
+ const message = satisfied
212
+ ? formatVersionSuccess(rc.label, currentVersion, rc.requiredVersion)
213
+ : formatVersionMismatch(rc.label, currentVersion, rc.requiredVersion);
214
+ results.push({
215
+ kind,
216
+ label: rc.label,
217
+ required: rc.requiredVersion,
218
+ found: currentVersion,
219
+ satisfied,
220
+ message,
221
+ });
222
+ }
223
+ return results;
224
+ }
197
225
  const TRIGGER_PORTS = {
198
226
  http: 4000,
199
227
  sse: 4001,
@@ -0,0 +1,18 @@
1
+ export interface SemverParts {
2
+ major: number;
3
+ minor: number;
4
+ patch: number;
5
+ }
6
+ export interface ParsedConstraint {
7
+ operator: ">=" | "^" | "~" | "=" | "";
8
+ version: string;
9
+ parts: SemverParts;
10
+ }
11
+ export declare function parseSemver(version: string): SemverParts;
12
+ export declare function compareSemver(a: string, b: string): number;
13
+ export declare function semverGte(a: SemverParts, b: SemverParts): boolean;
14
+ export declare function parseConstraint(constraint: string): ParsedConstraint;
15
+ export declare function satisfiesConstraint(version: string, constraint: string): boolean;
16
+ export declare function computeDefaultConstraint(version: string): string;
17
+ export declare function formatVersionMismatch(runtime: string, found: string | undefined, required: string, installHint?: string): string;
18
+ export declare function formatVersionSuccess(runtime: string, found: string, required: string): string;
@@ -0,0 +1,82 @@
1
+ export function parseSemver(version) {
2
+ const cleaned = version.trim();
3
+ const parts = cleaned.split(".").map(Number);
4
+ return {
5
+ major: parts[0] || 0,
6
+ minor: parts[1] || 0,
7
+ patch: parts[2] || 0,
8
+ };
9
+ }
10
+ export function compareSemver(a, b) {
11
+ const pa = parseSemver(a);
12
+ const pb = parseSemver(b);
13
+ if (pa.major !== pb.major)
14
+ return pa.major - pb.major;
15
+ if (pa.minor !== pb.minor)
16
+ return pa.minor - pb.minor;
17
+ return pa.patch - pb.patch;
18
+ }
19
+ export function semverGte(a, b) {
20
+ if (a.major !== b.major)
21
+ return a.major > b.major;
22
+ if (a.minor !== b.minor)
23
+ return a.minor > b.minor;
24
+ return a.patch >= b.patch;
25
+ }
26
+ export function parseConstraint(constraint) {
27
+ const trimmed = constraint.trim();
28
+ if (trimmed.startsWith(">=")) {
29
+ const ver = trimmed.slice(2);
30
+ return { operator: ">=", version: ver, parts: parseSemver(ver) };
31
+ }
32
+ if (trimmed.startsWith("^")) {
33
+ const ver = trimmed.slice(1);
34
+ return { operator: "^", version: ver, parts: parseSemver(ver) };
35
+ }
36
+ if (trimmed.startsWith("~")) {
37
+ const ver = trimmed.slice(1);
38
+ return { operator: "~", version: ver, parts: parseSemver(ver) };
39
+ }
40
+ const ver = trimmed.startsWith("=") ? trimmed.slice(1) : trimmed;
41
+ return { operator: trimmed.startsWith("=") ? "=" : "", version: ver, parts: parseSemver(ver) };
42
+ }
43
+ export function satisfiesConstraint(version, constraint) {
44
+ const actual = parseSemver(version);
45
+ const parsed = parseConstraint(constraint);
46
+ const base = parsed.parts;
47
+ switch (parsed.operator) {
48
+ case ">=":
49
+ return semverGte(actual, base);
50
+ case "^": {
51
+ if (base.major !== 0) {
52
+ return actual.major === base.major && semverGte(actual, base);
53
+ }
54
+ if (base.minor !== 0) {
55
+ return actual.major === 0 && actual.minor === base.minor && actual.patch >= base.patch;
56
+ }
57
+ return actual.major === 0 && actual.minor === 0 && actual.patch === base.patch;
58
+ }
59
+ case "~":
60
+ return actual.major === base.major && actual.minor === base.minor && actual.patch >= base.patch;
61
+ case "=":
62
+ case "":
63
+ return actual.major === base.major && actual.minor === base.minor && actual.patch === base.patch;
64
+ default:
65
+ return false;
66
+ }
67
+ }
68
+ export function computeDefaultConstraint(version) {
69
+ const parts = parseSemver(version);
70
+ return `>=${parts.major}.${parts.minor}.0`;
71
+ }
72
+ export function formatVersionMismatch(runtime, found, required, installHint) {
73
+ const lines = [` x ${runtime}`, ` Required: ${required}`, ` Found: ${found || "not installed"}`];
74
+ if (installHint) {
75
+ lines.push(` Fix: ${installHint}`);
76
+ }
77
+ lines.push(" Or update constraint in .blok/config.json");
78
+ return lines.join("\n");
79
+ }
80
+ export function formatVersionSuccess(runtime, found, required) {
81
+ return ` ✓ ${runtime} ${found} (requires ${required})`;
82
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,233 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { compareSemver, computeDefaultConstraint, formatVersionMismatch, formatVersionSuccess, parseConstraint, parseSemver, satisfiesConstraint, semverGte, } from "./semver-utils.js";
3
+ describe("parseSemver", () => {
4
+ it("parses a full semver string", () => {
5
+ expect(parseSemver("3.12.0")).toEqual({ major: 3, minor: 12, patch: 0 });
6
+ });
7
+ it("parses a two-part version", () => {
8
+ expect(parseSemver("1.22")).toEqual({ major: 1, minor: 22, patch: 0 });
9
+ });
10
+ it("parses a single-part version", () => {
11
+ expect(parseSemver("17")).toEqual({ major: 17, minor: 0, patch: 0 });
12
+ });
13
+ it("handles 0.x versions", () => {
14
+ expect(parseSemver("0.2.3")).toEqual({ major: 0, minor: 2, patch: 3 });
15
+ expect(parseSemver("0.0.3")).toEqual({ major: 0, minor: 0, patch: 3 });
16
+ });
17
+ it("handles large version numbers", () => {
18
+ expect(parseSemver("17.0.11")).toEqual({ major: 17, minor: 0, patch: 11 });
19
+ });
20
+ it("trims whitespace", () => {
21
+ expect(parseSemver(" 3.12.0 ")).toEqual({ major: 3, minor: 12, patch: 0 });
22
+ });
23
+ });
24
+ describe("compareSemver", () => {
25
+ it("returns 0 for equal versions", () => {
26
+ expect(compareSemver("1.2.3", "1.2.3")).toBe(0);
27
+ });
28
+ it("returns positive when a > b (major)", () => {
29
+ expect(compareSemver("2.0.0", "1.9.9")).toBeGreaterThan(0);
30
+ });
31
+ it("returns positive when a > b (minor)", () => {
32
+ expect(compareSemver("1.3.0", "1.2.9")).toBeGreaterThan(0);
33
+ });
34
+ it("returns positive when a > b (patch)", () => {
35
+ expect(compareSemver("1.2.4", "1.2.3")).toBeGreaterThan(0);
36
+ });
37
+ it("returns negative when a < b", () => {
38
+ expect(compareSemver("1.2.3", "1.2.4")).toBeLessThan(0);
39
+ });
40
+ it("treats missing patch as 0", () => {
41
+ expect(compareSemver("1.22", "1.22.0")).toBe(0);
42
+ });
43
+ });
44
+ describe("semverGte", () => {
45
+ it("returns true for equal versions", () => {
46
+ expect(semverGte({ major: 3, minor: 10, patch: 0 }, { major: 3, minor: 10, patch: 0 })).toBe(true);
47
+ });
48
+ it("returns true when major is greater", () => {
49
+ expect(semverGte({ major: 4, minor: 0, patch: 0 }, { major: 3, minor: 10, patch: 0 })).toBe(true);
50
+ });
51
+ it("returns true when minor is greater", () => {
52
+ expect(semverGte({ major: 3, minor: 11, patch: 0 }, { major: 3, minor: 10, patch: 0 })).toBe(true);
53
+ });
54
+ it("returns true when patch is greater", () => {
55
+ expect(semverGte({ major: 3, minor: 10, patch: 1 }, { major: 3, minor: 10, patch: 0 })).toBe(true);
56
+ });
57
+ it("returns false when less than", () => {
58
+ expect(semverGte({ major: 3, minor: 9, patch: 7 }, { major: 3, minor: 10, patch: 0 })).toBe(false);
59
+ });
60
+ });
61
+ describe("parseConstraint", () => {
62
+ it("parses >= operator", () => {
63
+ const result = parseConstraint(">=3.10.0");
64
+ expect(result.operator).toBe(">=");
65
+ expect(result.version).toBe("3.10.0");
66
+ expect(result.parts).toEqual({ major: 3, minor: 10, patch: 0 });
67
+ });
68
+ it("parses ^ operator", () => {
69
+ const result = parseConstraint("^1.22.0");
70
+ expect(result.operator).toBe("^");
71
+ expect(result.version).toBe("1.22.0");
72
+ });
73
+ it("parses ~ operator", () => {
74
+ const result = parseConstraint("~1.22.0");
75
+ expect(result.operator).toBe("~");
76
+ expect(result.version).toBe("1.22.0");
77
+ });
78
+ it("parses exact version (no operator)", () => {
79
+ const result = parseConstraint("3.12.0");
80
+ expect(result.operator).toBe("");
81
+ expect(result.version).toBe("3.12.0");
82
+ });
83
+ it("parses = operator", () => {
84
+ const result = parseConstraint("=3.12.0");
85
+ expect(result.operator).toBe("=");
86
+ expect(result.version).toBe("3.12.0");
87
+ });
88
+ it("trims whitespace", () => {
89
+ const result = parseConstraint(" >=3.10.0 ");
90
+ expect(result.operator).toBe(">=");
91
+ expect(result.version).toBe("3.10.0");
92
+ });
93
+ });
94
+ describe("satisfiesConstraint", () => {
95
+ describe(">= operator", () => {
96
+ it("satisfies when equal", () => {
97
+ expect(satisfiesConstraint("3.10.0", ">=3.10.0")).toBe(true);
98
+ });
99
+ it("satisfies when greater (patch)", () => {
100
+ expect(satisfiesConstraint("3.10.1", ">=3.10.0")).toBe(true);
101
+ });
102
+ it("satisfies when greater (minor)", () => {
103
+ expect(satisfiesConstraint("3.11.0", ">=3.10.0")).toBe(true);
104
+ });
105
+ it("satisfies when greater (major)", () => {
106
+ expect(satisfiesConstraint("4.0.0", ">=3.10.0")).toBe(true);
107
+ });
108
+ it("fails when less than (minor)", () => {
109
+ expect(satisfiesConstraint("3.9.7", ">=3.10.0")).toBe(false);
110
+ });
111
+ it("fails when less than (major)", () => {
112
+ expect(satisfiesConstraint("2.99.99", ">=3.10.0")).toBe(false);
113
+ });
114
+ it("Python 3.12.0 satisfies >=3.10.0", () => {
115
+ expect(satisfiesConstraint("3.12.0", ">=3.10.0")).toBe(true);
116
+ });
117
+ it("Python 3.9.7 does not satisfy >=3.10.0", () => {
118
+ expect(satisfiesConstraint("3.9.7", ">=3.10.0")).toBe(false);
119
+ });
120
+ it("Go 1.22.5 satisfies >=1.21.0", () => {
121
+ expect(satisfiesConstraint("1.22.5", ">=1.21.0")).toBe(true);
122
+ });
123
+ it("Java 17.0.11 satisfies >=17.0.0", () => {
124
+ expect(satisfiesConstraint("17.0.11", ">=17.0.0")).toBe(true);
125
+ });
126
+ it("Java 11.0.2 does not satisfy >=17.0.0", () => {
127
+ expect(satisfiesConstraint("11.0.2", ">=17.0.0")).toBe(false);
128
+ });
129
+ });
130
+ describe("^ (caret) operator", () => {
131
+ it("satisfies when equal", () => {
132
+ expect(satisfiesConstraint("1.22.0", "^1.22.0")).toBe(true);
133
+ });
134
+ it("satisfies when patch is higher", () => {
135
+ expect(satisfiesConstraint("1.22.5", "^1.22.0")).toBe(true);
136
+ });
137
+ it("satisfies when minor is higher", () => {
138
+ expect(satisfiesConstraint("1.23.0", "^1.22.0")).toBe(true);
139
+ });
140
+ it("fails when major is different", () => {
141
+ expect(satisfiesConstraint("2.0.0", "^1.22.0")).toBe(false);
142
+ });
143
+ it("fails when version is lower", () => {
144
+ expect(satisfiesConstraint("1.21.9", "^1.22.0")).toBe(false);
145
+ });
146
+ it("^0.2.3 requires minor match", () => {
147
+ expect(satisfiesConstraint("0.2.3", "^0.2.3")).toBe(true);
148
+ expect(satisfiesConstraint("0.2.4", "^0.2.3")).toBe(true);
149
+ expect(satisfiesConstraint("0.3.0", "^0.2.3")).toBe(false);
150
+ });
151
+ it("^0.0.3 requires exact patch match", () => {
152
+ expect(satisfiesConstraint("0.0.3", "^0.0.3")).toBe(true);
153
+ expect(satisfiesConstraint("0.0.4", "^0.0.3")).toBe(false);
154
+ });
155
+ });
156
+ describe("~ (tilde) operator", () => {
157
+ it("satisfies when equal", () => {
158
+ expect(satisfiesConstraint("1.22.0", "~1.22.0")).toBe(true);
159
+ });
160
+ it("satisfies when patch is higher", () => {
161
+ expect(satisfiesConstraint("1.22.5", "~1.22.0")).toBe(true);
162
+ });
163
+ it("fails when minor is different", () => {
164
+ expect(satisfiesConstraint("1.23.0", "~1.22.0")).toBe(false);
165
+ });
166
+ it("fails when major is different", () => {
167
+ expect(satisfiesConstraint("2.22.0", "~1.22.0")).toBe(false);
168
+ });
169
+ it("fails when patch is lower", () => {
170
+ expect(satisfiesConstraint("1.22.0", "~1.22.3")).toBe(false);
171
+ });
172
+ });
173
+ describe("exact version", () => {
174
+ it("satisfies when exact match", () => {
175
+ expect(satisfiesConstraint("3.12.0", "3.12.0")).toBe(true);
176
+ });
177
+ it("fails when patch differs", () => {
178
+ expect(satisfiesConstraint("3.12.1", "3.12.0")).toBe(false);
179
+ });
180
+ it("fails when minor differs", () => {
181
+ expect(satisfiesConstraint("3.13.0", "3.12.0")).toBe(false);
182
+ });
183
+ it("works with = prefix", () => {
184
+ expect(satisfiesConstraint("3.12.0", "=3.12.0")).toBe(true);
185
+ expect(satisfiesConstraint("3.12.1", "=3.12.0")).toBe(false);
186
+ });
187
+ });
188
+ });
189
+ describe("computeDefaultConstraint", () => {
190
+ it("computes >=major.minor.0 from full version", () => {
191
+ expect(computeDefaultConstraint("3.12.4")).toBe(">=3.12.0");
192
+ });
193
+ it("computes >=major.minor.0 when patch is already 0", () => {
194
+ expect(computeDefaultConstraint("1.22.0")).toBe(">=1.22.0");
195
+ });
196
+ it("handles two-part version", () => {
197
+ expect(computeDefaultConstraint("1.22")).toBe(">=1.22.0");
198
+ });
199
+ it("handles large major version (Java)", () => {
200
+ expect(computeDefaultConstraint("17.0.11")).toBe(">=17.0.0");
201
+ });
202
+ it("handles 0.x versions", () => {
203
+ expect(computeDefaultConstraint("0.2.3")).toBe(">=0.2.0");
204
+ });
205
+ });
206
+ describe("formatVersionMismatch", () => {
207
+ it("formats a mismatch with install hint", () => {
208
+ const result = formatVersionMismatch("Python 3", "3.9.7", ">=3.10.0", "Install Python 3.10+: https://python.org/downloads/");
209
+ expect(result).toContain("x Python 3");
210
+ expect(result).toContain("Required: >=3.10.0");
211
+ expect(result).toContain("Found: 3.9.7");
212
+ expect(result).toContain("Fix: Install Python 3.10+");
213
+ expect(result).toContain("Or update constraint in .blok/config.json");
214
+ });
215
+ it("formats a mismatch when not installed", () => {
216
+ const result = formatVersionMismatch("Go", undefined, ">=1.22.0", "Install Go: https://go.dev/dl/");
217
+ expect(result).toContain("Found: not installed");
218
+ });
219
+ it("formats without install hint", () => {
220
+ const result = formatVersionMismatch("Rust", "1.70.0", ">=1.75.0");
221
+ expect(result).not.toContain("Fix:");
222
+ expect(result).toContain("Or update constraint");
223
+ });
224
+ });
225
+ describe("formatVersionSuccess", () => {
226
+ it("formats a success message", () => {
227
+ const result = formatVersionSuccess("Python 3", "3.12.0", ">=3.10.0");
228
+ expect(result).toContain("✓");
229
+ expect(result).toContain("Python 3");
230
+ expect(result).toContain("3.12.0");
231
+ expect(result).toContain("requires >=3.10.0");
232
+ });
233
+ });