blokctl 0.3.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.
- package/dist/commands/check/index.d.ts +2 -0
- package/dist/commands/check/index.js +39 -0
- package/dist/commands/create/project.js +20 -5
- package/dist/commands/create/utils/Examples.d.ts +2 -2
- package/dist/commands/create/utils/Examples.js +28 -31
- package/dist/commands/dev/index.js +40 -57
- package/dist/commands/generate/GenerationAnalytics.js +2 -1
- package/dist/commands/generate/e2e/NodeGenerator.e2e.test.js +1 -1
- package/dist/commands/generate/e2e/TriggerGenerator.e2e.test.js +1 -1
- package/dist/commands/generate/e2e/WorkflowGenerator.e2e.test.js +1 -1
- package/dist/commands/migrate/index.js +11 -0
- package/dist/commands/migrate/paths.d.ts +2 -0
- package/dist/commands/migrate/paths.js +267 -0
- package/dist/index.js +14 -1
- package/dist/services/local-token-manager.js +1 -1
- package/dist/services/runtime-detector.d.ts +1 -0
- package/dist/services/runtime-detector.js +12 -0
- package/dist/services/runtime-setup.d.ts +12 -1
- package/dist/services/runtime-setup.js +28 -0
- package/dist/services/semver-utils.d.ts +18 -0
- package/dist/services/semver-utils.js +82 -0
- package/dist/services/semver-utils.test.d.ts +1 -0
- package/dist/services/semver-utils.test.js +233 -0
- package/dist/studio-dist/assets/{icons-N5J4OhGx.js ā icons-D-BBts99.js} +110 -65
- package/dist/studio-dist/assets/index-BD8_9YPN.js +42 -0
- package/dist/studio-dist/assets/index-D4Bc9-mb.css +1 -0
- package/dist/studio-dist/index.html +3 -3
- package/package.json +2 -2
- package/dist/studio-dist/assets/index-D6JA5F-X.js +0 -42
- package/dist/studio-dist/assets/index-mdQkg9ul.css +0 -1
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import color from "picocolors";
|
|
4
|
+
export async function migratePaths(opts) {
|
|
5
|
+
const cwd = process.cwd();
|
|
6
|
+
const explicitDir = opts.dir ?? null;
|
|
7
|
+
const dryRun = opts.dryRun === true;
|
|
8
|
+
const writeBackup = opts.backup !== false;
|
|
9
|
+
console.log(color.cyan("\nš£ Workflow path migrator"));
|
|
10
|
+
console.log(color.dim("Adds explicit `trigger.http.path` to every JSON HTTP workflow.\n"));
|
|
11
|
+
const root = await resolveJsonRoot(cwd, explicitDir);
|
|
12
|
+
if (!root) {
|
|
13
|
+
console.log(color.red("ā Could not find a JSON workflows directory. Looked in: " +
|
|
14
|
+
"workflows/json/, triggers/http/workflows/json/. Pass --dir <path> to override."));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
console.log(color.dim(`Scanning ${color.cyan(root)} (recursive)\n`));
|
|
18
|
+
const files = await collectJsonFiles(root);
|
|
19
|
+
if (files.length === 0) {
|
|
20
|
+
console.log(color.yellow("No JSON workflow files found."));
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const results = [];
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
const result = await migrateOne(file, root, { dryRun, writeBackup });
|
|
26
|
+
results.push(result);
|
|
27
|
+
printResult(result);
|
|
28
|
+
}
|
|
29
|
+
console.log("");
|
|
30
|
+
printSummary(results, dryRun, writeBackup);
|
|
31
|
+
const tsRoots = [path.join(cwd, "triggers", "http", "src", "workflows"), path.join(cwd, "src", "workflows")];
|
|
32
|
+
for (const tsRoot of tsRoots) {
|
|
33
|
+
if (await dirExists(tsRoot)) {
|
|
34
|
+
const tsFiles = await collectTsFiles(tsRoot);
|
|
35
|
+
if (tsFiles.length > 0) {
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log(color.yellow("ā TS workflows detected ā these are NOT migrated by this codemod."));
|
|
38
|
+
console.log(color.dim(` Found at: ${tsRoot}`));
|
|
39
|
+
console.log(color.dim(" Migrate them manually: ensure each workflow's `trigger.http.path` is set explicitly."));
|
|
40
|
+
console.log(color.dim(` Files: ${tsFiles.length}`));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function migrateOne(file, root, opts) {
|
|
46
|
+
let raw;
|
|
47
|
+
let parsed;
|
|
48
|
+
try {
|
|
49
|
+
raw = await fsp.readFile(file, "utf8");
|
|
50
|
+
parsed = JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
return { kind: "error", file, error: err.message };
|
|
54
|
+
}
|
|
55
|
+
if (!isPlainObject(parsed)) {
|
|
56
|
+
return { kind: "error", file, error: "Workflow must be a JSON object" };
|
|
57
|
+
}
|
|
58
|
+
const wf = parsed;
|
|
59
|
+
const trigger = wf.trigger;
|
|
60
|
+
if (!isPlainObject(trigger)) {
|
|
61
|
+
return { kind: "no-trigger", file };
|
|
62
|
+
}
|
|
63
|
+
const httpCfg = trigger.http;
|
|
64
|
+
if (!isPlainObject(httpCfg)) {
|
|
65
|
+
return { kind: "not-http", file };
|
|
66
|
+
}
|
|
67
|
+
const http = httpCfg;
|
|
68
|
+
const existingPath = typeof http.path === "string" ? http.path : undefined;
|
|
69
|
+
const relative = path.relative(root, file);
|
|
70
|
+
const derivedUrl = deriveUrlFromFilePath(relative);
|
|
71
|
+
if (existingPath === undefined) {
|
|
72
|
+
http.path = derivedUrl;
|
|
73
|
+
}
|
|
74
|
+
else if (existingPath === "/" && derivedUrl !== "/") {
|
|
75
|
+
http.path = derivedUrl;
|
|
76
|
+
const serialized = `${JSON.stringify(wf, null, "\t")}\n`;
|
|
77
|
+
if (serialized.trimEnd() === raw.trimEnd()) {
|
|
78
|
+
return { kind: "already-explicit", file, path: derivedUrl };
|
|
79
|
+
}
|
|
80
|
+
const writeResult = await maybeWrite(file, raw, serialized, opts);
|
|
81
|
+
if (writeResult)
|
|
82
|
+
return writeResult;
|
|
83
|
+
return { kind: "rewrote-root", file, from: "/", to: derivedUrl };
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
return { kind: "already-explicit", file, path: existingPath };
|
|
87
|
+
}
|
|
88
|
+
const serialized = `${JSON.stringify(wf, null, "\t")}\n`;
|
|
89
|
+
if (serialized.trimEnd() === raw.trimEnd()) {
|
|
90
|
+
return { kind: "already-explicit", file, path: existingPath ?? derivedUrl };
|
|
91
|
+
}
|
|
92
|
+
const writeResult = await maybeWrite(file, raw, serialized, opts);
|
|
93
|
+
if (writeResult)
|
|
94
|
+
return writeResult;
|
|
95
|
+
return { kind: "added", file, path: http.path };
|
|
96
|
+
}
|
|
97
|
+
async function maybeWrite(file, raw, serialized, opts) {
|
|
98
|
+
if (opts.dryRun)
|
|
99
|
+
return null;
|
|
100
|
+
if (opts.writeBackup) {
|
|
101
|
+
try {
|
|
102
|
+
await fsp.writeFile(`${file}.bak`, raw);
|
|
103
|
+
}
|
|
104
|
+
catch (err) {
|
|
105
|
+
return { kind: "error", file, error: `Failed to write backup: ${err.message}` };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
await fsp.writeFile(file, serialized);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
return { kind: "error", file, error: err.message };
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
function deriveUrlFromFilePath(relativePath) {
|
|
117
|
+
const noExt = relativePath.replace(/\.json$/i, "");
|
|
118
|
+
const segments = noExt.split(path.sep).filter((s) => s.length > 0);
|
|
119
|
+
if (segments.length === 0)
|
|
120
|
+
return "/";
|
|
121
|
+
if (segments[segments.length - 1] === "index")
|
|
122
|
+
segments.pop();
|
|
123
|
+
if (segments.length === 0)
|
|
124
|
+
return "/";
|
|
125
|
+
const converted = segments.map((seg) => {
|
|
126
|
+
const match = seg.match(/^\[(\.{3})?([A-Za-z_][A-Za-z0-9_]*)\]$/);
|
|
127
|
+
if (!match)
|
|
128
|
+
return seg;
|
|
129
|
+
return `:${match[2]}`;
|
|
130
|
+
});
|
|
131
|
+
return `/${converted.join("/")}`;
|
|
132
|
+
}
|
|
133
|
+
function printResult(result) {
|
|
134
|
+
const rel = path.relative(process.cwd(), result.file);
|
|
135
|
+
switch (result.kind) {
|
|
136
|
+
case "added":
|
|
137
|
+
console.log(` ${color.green("ā")} ${rel} ${color.dim("ā")} ${color.cyan(`path: "${result.path}"`)}`);
|
|
138
|
+
break;
|
|
139
|
+
case "rewrote-root":
|
|
140
|
+
console.log(` ${color.green("ā")} ${rel} ${color.dim("ā")} ${color.cyan(`"${result.from}"`)} ā ${color.cyan(`"${result.to}"`)}`);
|
|
141
|
+
break;
|
|
142
|
+
case "already-explicit":
|
|
143
|
+
console.log(` ${color.dim("Ā·")} ${rel} ${color.dim(`(already explicit: "${result.path}")`)}`);
|
|
144
|
+
break;
|
|
145
|
+
case "no-trigger":
|
|
146
|
+
console.log(` ${color.dim("Ā·")} ${rel} ${color.dim("(no trigger)")}`);
|
|
147
|
+
break;
|
|
148
|
+
case "not-http":
|
|
149
|
+
console.log(` ${color.dim("Ā·")} ${rel} ${color.dim("(non-HTTP trigger)")}`);
|
|
150
|
+
break;
|
|
151
|
+
case "error":
|
|
152
|
+
console.log(` ${color.red("ā")} ${rel} ${color.red(`error: ${result.error}`)}`);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function printSummary(results, dryRun, writeBackup) {
|
|
157
|
+
const counts = {
|
|
158
|
+
added: results.filter((r) => r.kind === "added").length,
|
|
159
|
+
rewrote: results.filter((r) => r.kind === "rewrote-root").length,
|
|
160
|
+
already: results.filter((r) => r.kind === "already-explicit").length,
|
|
161
|
+
skipped: results.filter((r) => r.kind === "no-trigger" || r.kind === "not-http").length,
|
|
162
|
+
errors: results.filter((r) => r.kind === "error").length,
|
|
163
|
+
};
|
|
164
|
+
const action = dryRun ? "would be" : "were";
|
|
165
|
+
console.log(color.bold("Summary:"));
|
|
166
|
+
console.log(` ${color.green(`${counts.added} ${action} updated`)} (added explicit path)`);
|
|
167
|
+
if (counts.rewrote > 0)
|
|
168
|
+
console.log(` ${color.green(`${counts.rewrote} ${action} updated`)} (rewrote "/" ā file-derived)`);
|
|
169
|
+
console.log(` ${color.dim(`${counts.already} already explicit`)}`);
|
|
170
|
+
if (counts.skipped > 0)
|
|
171
|
+
console.log(` ${color.dim(`${counts.skipped} skipped (non-HTTP / no trigger)`)}`);
|
|
172
|
+
if (counts.errors > 0)
|
|
173
|
+
console.log(` ${color.red(`${counts.errors} errors`)}`);
|
|
174
|
+
if (!dryRun && (counts.added > 0 || counts.rewrote > 0) && writeBackup) {
|
|
175
|
+
console.log("");
|
|
176
|
+
console.log(color.dim("Backups written as <name>.json.bak. Delete them once verified."));
|
|
177
|
+
}
|
|
178
|
+
if (dryRun && (counts.added > 0 || counts.rewrote > 0)) {
|
|
179
|
+
console.log("");
|
|
180
|
+
console.log(color.cyan("Re-run without --dry-run to apply."));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function resolveJsonRoot(cwd, explicit) {
|
|
184
|
+
if (explicit) {
|
|
185
|
+
const abs = path.isAbsolute(explicit) ? explicit : path.resolve(cwd, explicit);
|
|
186
|
+
if (await dirExists(abs))
|
|
187
|
+
return abs;
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const candidates = [path.join(cwd, "workflows", "json"), path.join(cwd, "triggers", "http", "workflows", "json")];
|
|
191
|
+
for (const c of candidates) {
|
|
192
|
+
if (await dirExists(c))
|
|
193
|
+
return c;
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
async function dirExists(p) {
|
|
198
|
+
try {
|
|
199
|
+
const stat = await fsp.stat(p);
|
|
200
|
+
return stat.isDirectory();
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
async function collectJsonFiles(root) {
|
|
207
|
+
const out = [];
|
|
208
|
+
await walkJson(root, out);
|
|
209
|
+
out.sort();
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
async function collectTsFiles(root) {
|
|
213
|
+
const out = [];
|
|
214
|
+
await walkTs(root, out);
|
|
215
|
+
out.sort();
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
async function walkJson(dir, out) {
|
|
219
|
+
let entries;
|
|
220
|
+
try {
|
|
221
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
228
|
+
continue;
|
|
229
|
+
const full = path.join(dir, entry.name);
|
|
230
|
+
if (entry.isDirectory()) {
|
|
231
|
+
await walkJson(full, out);
|
|
232
|
+
}
|
|
233
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".json")) {
|
|
234
|
+
out.push(full);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
async function walkTs(dir, out) {
|
|
239
|
+
let entries;
|
|
240
|
+
try {
|
|
241
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
for (const entry of entries) {
|
|
247
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_") || entry.name === "node_modules")
|
|
248
|
+
continue;
|
|
249
|
+
const full = path.join(dir, entry.name);
|
|
250
|
+
if (entry.isDirectory()) {
|
|
251
|
+
await walkTs(full, out);
|
|
252
|
+
}
|
|
253
|
+
else if (entry.isFile() &&
|
|
254
|
+
(entry.name.toLowerCase().endsWith(".ts") || entry.name.toLowerCase().endsWith(".js")) &&
|
|
255
|
+
entry.name !== "index.ts" &&
|
|
256
|
+
entry.name !== "index.js") {
|
|
257
|
+
out.push(full);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
function isPlainObject(value) {
|
|
262
|
+
if (value === null || value === undefined)
|
|
263
|
+
return false;
|
|
264
|
+
if (Array.isArray(value))
|
|
265
|
+
return false;
|
|
266
|
+
return typeof value === "object";
|
|
267
|
+
}
|
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("--
|
|
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
|
-
|
|
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 {};
|