blokctl 0.4.0 → 0.6.2

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.js CHANGED
@@ -5,6 +5,7 @@ import util from "node:util";
5
5
  import * as p from "@clack/prompts";
6
6
  import fsExtra from "fs-extra";
7
7
  import color from "picocolors";
8
+ import { checkProject } from "./commands/check/index.js";
8
9
  import { createNode } from "./commands/create/node.js";
9
10
  import { createProject } from "./commands/create/project.js";
10
11
  import { createWorkflow } from "./commands/create/workflow.js";
@@ -165,10 +166,22 @@ async function main() {
165
166
  create.addCommand(node);
166
167
  create.addCommand(workflow);
167
168
  program.addCommand(create);
169
+ program
170
+ .command("check")
171
+ .description("Validate runtime version requirements")
172
+ .action(async (options) => {
173
+ await analytics.trackCommandExecution({
174
+ command: "check",
175
+ args: options,
176
+ execution: async () => {
177
+ await checkProject(options);
178
+ },
179
+ });
180
+ });
168
181
  program
169
182
  .command("dev")
170
183
  .description("Start the development server")
171
- .option("--with-http-fallback", "Spawn SDKs on HTTP transport instead of gRPC. Use only when one of your SDKs lacks a working gRPC listener (e.g. PHP without RoadRunner). Removed in v0.4.0.")
184
+ .option("--skip-version-check", "Skip runtime version validation")
172
185
  .action(async (options) => {
173
186
  await analytics.trackCommandExecution({
174
187
  command: "dev",
@@ -13,7 +13,7 @@ class LocalTokenManager {
13
13
  ensureStorage() {
14
14
  try {
15
15
  if (!fs.existsSync(this.storageDir)) {
16
- fs.mkdirSync(this.storageDir, { mode: 0o700 });
16
+ fs.mkdirSync(this.storageDir, { mode: 0o700, recursive: true });
17
17
  }
18
18
  else {
19
19
  fs.chmodSync(this.storageDir, 0o700);
@@ -22,5 +22,6 @@ export interface RuntimeInfo {
22
22
  }
23
23
  export declare function detectRr(): string | null;
24
24
  export declare function detectRuntimes(): Promise<RuntimeInfo[]>;
25
+ export declare function detectRuntimeVersion(kind: string): Promise<string | undefined>;
25
26
  export declare function getRuntimeDefinition(kind: string): Omit<RuntimeInfo, "available" | "version"> | undefined;
26
27
  export declare function getAllRuntimeDefinitions(): Omit<RuntimeInfo, "available" | "version">[];
@@ -192,6 +192,18 @@ export async function detectRuntimes() {
192
192
  }
193
193
  return results;
194
194
  }
195
+ export async function detectRuntimeVersion(kind) {
196
+ const def = RUNTIME_DEFINITIONS.find((r) => r.kind === kind);
197
+ if (!def)
198
+ return undefined;
199
+ for (const cmd of def.commands) {
200
+ const output = await tryExec(cmd);
201
+ if (output) {
202
+ return parseVersion(output, def.kind);
203
+ }
204
+ }
205
+ return undefined;
206
+ }
195
207
  export function getRuntimeDefinition(kind) {
196
208
  return RUNTIME_DEFINITIONS.find((r) => r.kind === kind);
197
209
  }
@@ -12,7 +12,9 @@ export interface RuntimeConfig {
12
12
  cwd: string;
13
13
  kind: string;
14
14
  label: string;
15
- transport?: "grpc" | "http";
15
+ version?: string;
16
+ requiredVersion?: string;
17
+ transport?: "grpc";
16
18
  }
17
19
  export interface TriggerConfig {
18
20
  kind: string;
@@ -31,6 +33,15 @@ export declare function writeProjectConfig(projectDir: string, runtimeConfigs: R
31
33
  export declare function readProjectConfig(projectDir: string): ProjectConfig | null;
32
34
  export declare function generateRuntimeEnvVars(runtimeConfigs: RuntimeConfig[]): string;
33
35
  export declare function generateSupervisordConfig(runtimeConfigs: RuntimeConfig[]): string;
36
+ export interface RuntimeValidationResult {
37
+ kind: string;
38
+ label: string;
39
+ required: string;
40
+ found: string | undefined;
41
+ satisfied: boolean;
42
+ message: string;
43
+ }
44
+ export declare function validateProjectRuntimes(projectDir: string): Promise<RuntimeValidationResult[]>;
34
45
  export declare function getTriggerPort(triggerKind: string): number;
35
46
  export declare function getTriggerLabel(triggerKind: string): string;
36
47
  export declare function createTriggerConfig(triggerKind: string): TriggerConfig;
@@ -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
+ });