blokctl 0.2.11 → 0.4.0
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/create/project.js +63 -3
- package/dist/commands/create/utils/Examples.d.ts +3 -3
- package/dist/commands/create/utils/Examples.js +109 -13
- package/dist/commands/dev/index.js +50 -13
- package/dist/commands/generate/validators/WorkflowValidator.js +1 -1
- package/dist/commands/migrate/index.js +22 -0
- package/dist/commands/migrate/paths.d.ts +2 -0
- package/dist/commands/migrate/paths.js +267 -0
- package/dist/commands/migrate/workflows.d.ts +3 -0
- package/dist/commands/migrate/workflows.js +333 -0
- package/dist/commands/trace/index.js +6 -2
- package/dist/commands/trace/startStudio.d.ts +2 -0
- package/dist/commands/trace/startStudio.js +76 -11
- package/dist/index.js +1 -0
- package/dist/services/health-probe.d.ts +5 -0
- package/dist/services/health-probe.js +35 -0
- package/dist/services/runtime-detector.d.ts +3 -0
- package/dist/services/runtime-detector.js +21 -2
- package/dist/services/runtime-setup.d.ts +3 -0
- package/dist/services/runtime-setup.js +11 -2
- package/dist/studio-dist/assets/charts-Dh48HebV.js +68 -0
- package/dist/studio-dist/assets/graph-DWteCadQ.js +7 -0
- package/dist/studio-dist/assets/{icons-zP8LLgPh.js → icons-N5J4OhGx.js} +66 -51
- package/dist/studio-dist/assets/index-D6hOoOID.css +1 -0
- package/dist/studio-dist/assets/index-D_CdNmTc.js +42 -0
- package/dist/studio-dist/assets/react-vendor-l0sNRNKZ.js +1 -0
- package/dist/studio-dist/assets/tanstack-query-Day3Mt-4.js +17 -0
- package/dist/studio-dist/assets/tanstack-router-BB95iErN.js +25 -0
- package/dist/studio-dist/assets/{tanstack-table-DhwRvuH2.js → tanstack-table-Biem1hxK.js} +1 -1
- package/dist/studio-dist/favicon.svg +7 -4
- package/dist/studio-dist/index.html +19 -10
- package/package.json +15 -12
- package/dist/studio-dist/assets/charts-Dso0hPUR.js +0 -68
- package/dist/studio-dist/assets/graph-CsV2nWGn.js +0 -23
- package/dist/studio-dist/assets/index-CLyEkXMx.css +0 -1
- package/dist/studio-dist/assets/index-CNXFX_ar.js +0 -27
- package/dist/studio-dist/assets/react-vendor--Eh9ivFN.js +0 -17
- package/dist/studio-dist/assets/tanstack-query-CiM1U6F5.js +0 -1
- package/dist/studio-dist/assets/tanstack-router-Btjy0MKq.js +0 -25
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { promises as fsp } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import color from "picocolors";
|
|
4
|
+
export async function migrateWorkflows(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 v1 → v2 migrator"));
|
|
10
|
+
console.log(color.dim("Converts legacy JSON workflows to the canonical v2 shape.\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, { dryRun, writeBackup });
|
|
26
|
+
results.push(result);
|
|
27
|
+
printResult(result);
|
|
28
|
+
}
|
|
29
|
+
console.log("");
|
|
30
|
+
printSummary(results, dryRun, writeBackup);
|
|
31
|
+
}
|
|
32
|
+
async function migrateOne(file, opts) {
|
|
33
|
+
let raw;
|
|
34
|
+
let parsed;
|
|
35
|
+
try {
|
|
36
|
+
raw = await fsp.readFile(file, "utf8");
|
|
37
|
+
parsed = JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
return { kind: "error", file, error: err.message };
|
|
41
|
+
}
|
|
42
|
+
if (!isPlainObject(parsed)) {
|
|
43
|
+
return { kind: "error", file, error: "Workflow must be a JSON object" };
|
|
44
|
+
}
|
|
45
|
+
const wf = parsed;
|
|
46
|
+
const triggerKind = detectTriggerKind(wf);
|
|
47
|
+
const v2 = convertToV2(wf);
|
|
48
|
+
const serialized = `${JSON.stringify(v2, null, "\t")}\n`;
|
|
49
|
+
if (serialized.trimEnd() === raw.trimEnd()) {
|
|
50
|
+
return { kind: "already-v2", file };
|
|
51
|
+
}
|
|
52
|
+
if (opts.dryRun) {
|
|
53
|
+
return { kind: "migrated", file };
|
|
54
|
+
}
|
|
55
|
+
if (opts.writeBackup) {
|
|
56
|
+
try {
|
|
57
|
+
await fsp.writeFile(`${file}.bak`, raw);
|
|
58
|
+
}
|
|
59
|
+
catch (err) {
|
|
60
|
+
return { kind: "error", file, error: `Failed to write backup: ${err.message}` };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
await fsp.writeFile(file, serialized);
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { kind: "error", file, error: err.message };
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
kind: triggerKind === "http" ? "migrated" : "not-http",
|
|
71
|
+
file,
|
|
72
|
+
trigger: triggerKind,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function convertToV2(wf) {
|
|
76
|
+
const out = {};
|
|
77
|
+
if (typeof wf.name === "string")
|
|
78
|
+
out.name = wf.name;
|
|
79
|
+
if (typeof wf.version === "string")
|
|
80
|
+
out.version = wf.version;
|
|
81
|
+
if (typeof wf.description === "string")
|
|
82
|
+
out.description = wf.description;
|
|
83
|
+
out.trigger = convertTrigger(wf.trigger);
|
|
84
|
+
out.steps = convertSteps(Array.isArray(wf.steps) ? wf.steps : [], isPlainObject(wf.nodes) ? wf.nodes : {});
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
function convertTrigger(rawTrigger) {
|
|
88
|
+
if (!isPlainObject(rawTrigger))
|
|
89
|
+
return {};
|
|
90
|
+
const out = {};
|
|
91
|
+
for (const [kind, cfg] of Object.entries(rawTrigger)) {
|
|
92
|
+
if (kind === "http" && isPlainObject(cfg)) {
|
|
93
|
+
const httpCfg = { ...cfg };
|
|
94
|
+
if (httpCfg.method === "*")
|
|
95
|
+
httpCfg.method = "ANY";
|
|
96
|
+
out[kind] = httpCfg;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
out[kind] = cfg;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
function convertSteps(steps, nodes) {
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const rawStep of steps) {
|
|
107
|
+
if (!isPlainObject(rawStep))
|
|
108
|
+
continue;
|
|
109
|
+
const step = rawStep;
|
|
110
|
+
const id = pickString(step.name) ?? pickString(step.id);
|
|
111
|
+
if (!id)
|
|
112
|
+
continue;
|
|
113
|
+
if (isPlainObject(step.branch)) {
|
|
114
|
+
const rawBranch = step.branch;
|
|
115
|
+
const branchStep = {
|
|
116
|
+
id,
|
|
117
|
+
branch: {
|
|
118
|
+
when: rewriteLegacyExpressions(typeof rawBranch.when === "string" ? rawBranch.when : "true"),
|
|
119
|
+
then: convertSteps(Array.isArray(rawBranch.then) ? rawBranch.then : [], {}),
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
if (Array.isArray(rawBranch.else)) {
|
|
123
|
+
branchStep.branch.else = convertSteps(rawBranch.else, {});
|
|
124
|
+
}
|
|
125
|
+
if (step.active === false)
|
|
126
|
+
branchStep.active = false;
|
|
127
|
+
if (step.stop === true)
|
|
128
|
+
branchStep.stop = true;
|
|
129
|
+
out.push(branchStep);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const nodeRef = pickString(step.node) ?? pickString(step.use);
|
|
133
|
+
if (!nodeRef)
|
|
134
|
+
continue;
|
|
135
|
+
const v1NodeConfig = isPlainObject(nodes[id]) ? nodes[id] : null;
|
|
136
|
+
const inputs = (() => {
|
|
137
|
+
if (isPlainObject(step.inputs))
|
|
138
|
+
return step.inputs;
|
|
139
|
+
if (v1NodeConfig && isPlainObject(v1NodeConfig.inputs))
|
|
140
|
+
return v1NodeConfig.inputs;
|
|
141
|
+
return null;
|
|
142
|
+
})();
|
|
143
|
+
if (v1NodeConfig && Array.isArray(v1NodeConfig.conditions)) {
|
|
144
|
+
const conds = v1NodeConfig.conditions;
|
|
145
|
+
const ifCond = conds.find((c) => isPlainObject(c) && c.type === "if");
|
|
146
|
+
const elseCond = conds.find((c) => isPlainObject(c) && c.type === "else");
|
|
147
|
+
if (ifCond) {
|
|
148
|
+
const branchStep = {
|
|
149
|
+
id,
|
|
150
|
+
branch: {
|
|
151
|
+
when: rewriteLegacyExpressions(typeof ifCond.condition === "string" ? ifCond.condition : "true"),
|
|
152
|
+
then: convertSteps(Array.isArray(ifCond.steps) ? ifCond.steps : [], {}),
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
if (elseCond) {
|
|
156
|
+
branchStep.branch.else = convertSteps(Array.isArray(elseCond.steps) ? elseCond.steps : [], {});
|
|
157
|
+
}
|
|
158
|
+
if (step.active === false)
|
|
159
|
+
branchStep.active = false;
|
|
160
|
+
if (step.stop === true)
|
|
161
|
+
branchStep.stop = true;
|
|
162
|
+
out.push(branchStep);
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const v2Step = { id, use: nodeRef };
|
|
167
|
+
if (typeof step.type === "string")
|
|
168
|
+
v2Step.type = step.type;
|
|
169
|
+
if (inputs)
|
|
170
|
+
v2Step.inputs = rewriteLegacyExpressions(inputs);
|
|
171
|
+
if (step.set_var === false)
|
|
172
|
+
v2Step.ephemeral = true;
|
|
173
|
+
if (typeof step.as === "string")
|
|
174
|
+
v2Step.as = step.as;
|
|
175
|
+
if (step.spread === true)
|
|
176
|
+
v2Step.spread = true;
|
|
177
|
+
if (step.ephemeral === true)
|
|
178
|
+
v2Step.ephemeral = true;
|
|
179
|
+
if (step.active === false)
|
|
180
|
+
v2Step.active = false;
|
|
181
|
+
if (step.stop === true)
|
|
182
|
+
v2Step.stop = true;
|
|
183
|
+
if (typeof step.stream_logs === "boolean")
|
|
184
|
+
v2Step.stream_logs = step.stream_logs;
|
|
185
|
+
out.push(v2Step);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
export function rewriteLegacyExpressions(value) {
|
|
190
|
+
if (typeof value === "string") {
|
|
191
|
+
return rewriteOneString(value);
|
|
192
|
+
}
|
|
193
|
+
if (Array.isArray(value)) {
|
|
194
|
+
return value.map((v) => rewriteLegacyExpressions(v));
|
|
195
|
+
}
|
|
196
|
+
if (isPlainObject(value)) {
|
|
197
|
+
const out = {};
|
|
198
|
+
for (const [k, v] of Object.entries(value)) {
|
|
199
|
+
out[k] = rewriteLegacyExpressions(v);
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
function rewriteOneString(input) {
|
|
206
|
+
let s = input;
|
|
207
|
+
s = s.replace(/js\/ctx\.vars\b/g, "js/ctx.state");
|
|
208
|
+
s = s.replace(/js\/ctx\.response\.data\b/g, "js/ctx.prev.data");
|
|
209
|
+
s = s.replace(/\$\{ctx\.vars\b/g, "${ctx.state");
|
|
210
|
+
s = s.replace(/\$\{ctx\.response\.data\b/g, "${ctx.prev.data");
|
|
211
|
+
return s;
|
|
212
|
+
}
|
|
213
|
+
function detectTriggerKind(wf) {
|
|
214
|
+
if (!isPlainObject(wf.trigger))
|
|
215
|
+
return "unknown";
|
|
216
|
+
const keys = Object.keys(wf.trigger);
|
|
217
|
+
return keys[0] ?? "unknown";
|
|
218
|
+
}
|
|
219
|
+
async function resolveJsonRoot(cwd, explicit) {
|
|
220
|
+
if (explicit) {
|
|
221
|
+
const abs = path.isAbsolute(explicit) ? explicit : path.resolve(cwd, explicit);
|
|
222
|
+
if (await dirExists(abs))
|
|
223
|
+
return abs;
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
const candidates = [path.join(cwd, "workflows", "json"), path.join(cwd, "triggers", "http", "workflows", "json")];
|
|
227
|
+
for (const c of candidates) {
|
|
228
|
+
if (await dirExists(c))
|
|
229
|
+
return c;
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
async function dirExists(p) {
|
|
234
|
+
try {
|
|
235
|
+
const stat = await fsp.stat(p);
|
|
236
|
+
return stat.isDirectory();
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
async function collectJsonFiles(root) {
|
|
243
|
+
const out = [];
|
|
244
|
+
await walk(root, out);
|
|
245
|
+
out.sort();
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
async function walk(dir, out) {
|
|
249
|
+
let entries;
|
|
250
|
+
try {
|
|
251
|
+
entries = await fsp.readdir(dir, { withFileTypes: true });
|
|
252
|
+
}
|
|
253
|
+
catch {
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
for (const entry of entries) {
|
|
257
|
+
if (entry.name.startsWith(".") || entry.name.startsWith("_"))
|
|
258
|
+
continue;
|
|
259
|
+
const full = path.join(dir, entry.name);
|
|
260
|
+
if (entry.isDirectory()) {
|
|
261
|
+
await walk(full, out);
|
|
262
|
+
}
|
|
263
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".json")) {
|
|
264
|
+
out.push(full);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function isPlainObject(value) {
|
|
269
|
+
if (value === null || value === undefined)
|
|
270
|
+
return false;
|
|
271
|
+
if (typeof value !== "object")
|
|
272
|
+
return false;
|
|
273
|
+
if (Array.isArray(value))
|
|
274
|
+
return false;
|
|
275
|
+
const proto = Object.getPrototypeOf(value);
|
|
276
|
+
return proto === null || proto === Object.prototype;
|
|
277
|
+
}
|
|
278
|
+
function pickString(value) {
|
|
279
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
280
|
+
}
|
|
281
|
+
function printResult(result) {
|
|
282
|
+
const file = path.relative(process.cwd(), result.file);
|
|
283
|
+
switch (result.kind) {
|
|
284
|
+
case "migrated":
|
|
285
|
+
console.log(` ${color.green("✓")} ${color.cyan(file)}`);
|
|
286
|
+
break;
|
|
287
|
+
case "already-v2":
|
|
288
|
+
console.log(` ${color.dim("⊙")} ${color.dim(file)} ${color.dim("(already v2)")}`);
|
|
289
|
+
break;
|
|
290
|
+
case "not-http":
|
|
291
|
+
console.log(` ${color.green("✓")} ${color.cyan(file)} ${color.dim(`(${result.trigger} trigger; no URL preserved)`)}`);
|
|
292
|
+
break;
|
|
293
|
+
case "skipped":
|
|
294
|
+
console.log(` ${color.dim("→")} ${color.dim(file)} ${color.dim(`(${result.reason})`)}`);
|
|
295
|
+
break;
|
|
296
|
+
case "error":
|
|
297
|
+
console.log(` ${color.red("✗")} ${color.cyan(file)} ${color.red(`— ${result.error}`)}`);
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function printSummary(results, dryRun, writeBackup) {
|
|
302
|
+
const counts = {
|
|
303
|
+
migrated: 0,
|
|
304
|
+
alreadyV2: 0,
|
|
305
|
+
notHttp: 0,
|
|
306
|
+
skipped: 0,
|
|
307
|
+
error: 0,
|
|
308
|
+
};
|
|
309
|
+
for (const r of results) {
|
|
310
|
+
if (r.kind === "migrated")
|
|
311
|
+
counts.migrated++;
|
|
312
|
+
else if (r.kind === "already-v2")
|
|
313
|
+
counts.alreadyV2++;
|
|
314
|
+
else if (r.kind === "not-http")
|
|
315
|
+
counts.notHttp++;
|
|
316
|
+
else if (r.kind === "skipped")
|
|
317
|
+
counts.skipped++;
|
|
318
|
+
else if (r.kind === "error")
|
|
319
|
+
counts.error++;
|
|
320
|
+
}
|
|
321
|
+
const total = results.length;
|
|
322
|
+
const verb = dryRun ? "would migrate" : "migrated";
|
|
323
|
+
const summary = `Total: ${total} · ${color.green(`${verb}: ${counts.migrated + counts.notHttp}`)}${counts.alreadyV2 > 0 ? ` · ${color.dim(`already v2: ${counts.alreadyV2}`)}` : ""}${counts.error > 0 ? ` · ${color.red(`errors: ${counts.error}`)}` : ""}`;
|
|
324
|
+
console.log(summary);
|
|
325
|
+
if (dryRun) {
|
|
326
|
+
console.log(color.dim("\nDry run — no files written. Re-run without --dry-run to apply."));
|
|
327
|
+
}
|
|
328
|
+
else if (writeBackup && counts.migrated + counts.notHttp > 0) {
|
|
329
|
+
console.log(color.dim("\nBackups written next to each file as <name>.json.bak. Run with --no-backup to skip backups."));
|
|
330
|
+
}
|
|
331
|
+
if (counts.error > 0)
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
@@ -3,9 +3,11 @@ import { startStudio } from "./startStudio.js";
|
|
|
3
3
|
program
|
|
4
4
|
.command("trace")
|
|
5
5
|
.alias("studio")
|
|
6
|
-
.description("Open Blok Studio — real-time workflow trace UI")
|
|
6
|
+
.description("Open Blok Studio — real-time workflow trace UI (Prisma-Studio-style)")
|
|
7
7
|
.option("-p, --port <port>", "Studio UI port", "5555")
|
|
8
|
-
.option("-u, --url <url>", "Blok backend URL", "http://localhost:4000")
|
|
8
|
+
.option("-u, --url <url>", "Proxy mode: Blok backend URL to connect to", "http://localhost:4000")
|
|
9
|
+
.option("--db <path>", "Standalone mode: serve directly from a SQLite trace file (no trigger needed). Auto-detects .blok/trace.db when present.")
|
|
10
|
+
.option("--standalone", "Force standalone mode (mount /__blok/* on this server reading from .blok/trace.db)")
|
|
9
11
|
.option("--workflow <name>", "Open specific workflow")
|
|
10
12
|
.option("--run <id>", "Open specific run")
|
|
11
13
|
.option("--no-open", "Don't auto-open browser")
|
|
@@ -17,6 +19,8 @@ program
|
|
|
17
19
|
await startStudio({
|
|
18
20
|
port: Number.parseInt(options.port, 10),
|
|
19
21
|
url: options.url,
|
|
22
|
+
db: options.db,
|
|
23
|
+
standalone: options.standalone,
|
|
20
24
|
workflow: options.workflow,
|
|
21
25
|
run: options.run,
|
|
22
26
|
open: options.open,
|