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.
Files changed (39) hide show
  1. package/dist/commands/create/project.js +63 -3
  2. package/dist/commands/create/utils/Examples.d.ts +3 -3
  3. package/dist/commands/create/utils/Examples.js +109 -13
  4. package/dist/commands/dev/index.js +50 -13
  5. package/dist/commands/generate/validators/WorkflowValidator.js +1 -1
  6. package/dist/commands/migrate/index.js +22 -0
  7. package/dist/commands/migrate/paths.d.ts +2 -0
  8. package/dist/commands/migrate/paths.js +267 -0
  9. package/dist/commands/migrate/workflows.d.ts +3 -0
  10. package/dist/commands/migrate/workflows.js +333 -0
  11. package/dist/commands/trace/index.js +6 -2
  12. package/dist/commands/trace/startStudio.d.ts +2 -0
  13. package/dist/commands/trace/startStudio.js +76 -11
  14. package/dist/index.js +1 -0
  15. package/dist/services/health-probe.d.ts +5 -0
  16. package/dist/services/health-probe.js +35 -0
  17. package/dist/services/runtime-detector.d.ts +3 -0
  18. package/dist/services/runtime-detector.js +21 -2
  19. package/dist/services/runtime-setup.d.ts +3 -0
  20. package/dist/services/runtime-setup.js +11 -2
  21. package/dist/studio-dist/assets/charts-Dh48HebV.js +68 -0
  22. package/dist/studio-dist/assets/graph-DWteCadQ.js +7 -0
  23. package/dist/studio-dist/assets/{icons-zP8LLgPh.js → icons-N5J4OhGx.js} +66 -51
  24. package/dist/studio-dist/assets/index-D6hOoOID.css +1 -0
  25. package/dist/studio-dist/assets/index-D_CdNmTc.js +42 -0
  26. package/dist/studio-dist/assets/react-vendor-l0sNRNKZ.js +1 -0
  27. package/dist/studio-dist/assets/tanstack-query-Day3Mt-4.js +17 -0
  28. package/dist/studio-dist/assets/tanstack-router-BB95iErN.js +25 -0
  29. package/dist/studio-dist/assets/{tanstack-table-DhwRvuH2.js → tanstack-table-Biem1hxK.js} +1 -1
  30. package/dist/studio-dist/favicon.svg +7 -4
  31. package/dist/studio-dist/index.html +19 -10
  32. package/package.json +15 -12
  33. package/dist/studio-dist/assets/charts-Dso0hPUR.js +0 -68
  34. package/dist/studio-dist/assets/graph-CsV2nWGn.js +0 -23
  35. package/dist/studio-dist/assets/index-CLyEkXMx.css +0 -1
  36. package/dist/studio-dist/assets/index-CNXFX_ar.js +0 -27
  37. package/dist/studio-dist/assets/react-vendor--Eh9ivFN.js +0 -17
  38. package/dist/studio-dist/assets/tanstack-query-CiM1U6F5.js +0 -1
  39. package/dist/studio-dist/assets/tanstack-router-Btjy0MKq.js +0 -25
@@ -4,6 +4,7 @@ import https from "node:https";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import * as p from "@clack/prompts";
7
+ import express from "express";
7
8
  import open from "open";
8
9
  import color from "picocolors";
9
10
  import serveHandler from "serve-handler";
@@ -18,6 +19,10 @@ function resolveStaticPath() {
18
19
  return workspace;
19
20
  return null;
20
21
  }
22
+ function autoDetectDbPath() {
23
+ const candidate = path.resolve(process.cwd(), ".blok", "trace.db");
24
+ return fs.existsSync(candidate) ? candidate : null;
25
+ }
21
26
  async function checkBackendHealth(backendUrl) {
22
27
  return new Promise((resolve) => {
23
28
  const url = new URL("/__blok/health", backendUrl);
@@ -33,7 +38,7 @@ async function checkBackendHealth(backendUrl) {
33
38
  });
34
39
  }
35
40
  function proxyRequest(req, res, backendUrl) {
36
- const targetUrl = new URL(req.url, backendUrl);
41
+ const targetUrl = new URL(req.url ?? "/", backendUrl);
37
42
  const client = targetUrl.protocol === "https:" ? https : http;
38
43
  const proxyReq = client.request(targetUrl, {
39
44
  method: req.method,
@@ -56,28 +61,83 @@ function proxyRequest(req, res, backendUrl) {
56
61
  });
57
62
  req.pipe(proxyReq, { end: true });
58
63
  }
64
+ async function buildStandaloneApp(dbPath) {
65
+ const runner = (await import("@blokjs/runner"));
66
+ const { RunTracker, createStore, registerTraceRoutes } = runner;
67
+ const absoluteDb = path.resolve(dbPath);
68
+ const dir = path.dirname(absoluteDb);
69
+ if (dir && !fs.existsSync(dir)) {
70
+ fs.mkdirSync(dir, { recursive: true });
71
+ }
72
+ const store = createStore({ type: "sqlite", sqlitePath: absoluteDb });
73
+ const tracker = new RunTracker(undefined, store);
74
+ RunTracker.instance = tracker;
75
+ const app = express();
76
+ app.use(express.json({ limit: "10mb" }));
77
+ const traceRouter = express.Router();
78
+ registerTraceRoutes(traceRouter, tracker);
79
+ app.use("/__blok", traceRouter);
80
+ return app;
81
+ }
59
82
  export async function startStudio(options) {
60
83
  const { port, url: backendUrl, open: shouldOpen } = options;
61
84
  p.intro(color.bgCyan(color.black(" Blok Studio ")));
62
85
  const staticPath = resolveStaticPath();
63
86
  if (!staticPath) {
64
- p.log.error(`Studio assets not found.\n` + ` Build them first: ${color.cyan("pnpm --filter @blokjs/studio build")}`);
87
+ p.log.error(`Studio assets not found.\n Build them first: ${color.cyan("bun run --filter @blokjs/studio build")}`);
65
88
  process.exit(1);
66
89
  }
67
- const s = p.spinner();
68
- s.start("Checking backend health...");
69
- const healthy = await checkBackendHealth(backendUrl);
70
- if (!healthy) {
71
- s.stop(color.yellow("Backend not reachable"));
72
- p.log.warn(`Blok backend not found at ${color.cyan(backendUrl)}\n` + ` Start it first: ${color.cyan("blokctl dev")}`);
73
- p.log.info("Starting Studio anyway — it will connect when the backend is up.");
90
+ let mode = "proxy";
91
+ let dbPath = null;
92
+ if (options.db) {
93
+ mode = "standalone";
94
+ dbPath = path.resolve(options.db);
95
+ }
96
+ else if (options.standalone) {
97
+ mode = "standalone";
98
+ dbPath = autoDetectDbPath() ?? path.resolve(process.cwd(), ".blok", "trace.db");
74
99
  }
75
100
  else {
76
- s.stop(color.green("Backend healthy"));
101
+ const s = p.spinner();
102
+ s.start("Checking backend health...");
103
+ const healthy = await checkBackendHealth(backendUrl);
104
+ if (healthy) {
105
+ s.stop(color.green("Backend healthy"));
106
+ }
107
+ else {
108
+ const auto = autoDetectDbPath();
109
+ if (auto) {
110
+ s.stop(color.green(`Backend not reachable; serving from ${color.cyan(path.relative(process.cwd(), auto))}`));
111
+ mode = "standalone";
112
+ dbPath = auto;
113
+ }
114
+ else {
115
+ s.stop(color.yellow("Backend not reachable"));
116
+ p.log.warn(`Blok backend not found at ${color.cyan(backendUrl)}\n` +
117
+ ` Start it first: ${color.cyan("blokctl dev")}\n` +
118
+ ` Or open a trace file directly: ${color.cyan("blokctl studio --db <path>")}`);
119
+ p.log.info("Starting Studio anyway — it will connect when the backend is up.");
120
+ }
121
+ }
122
+ }
123
+ let standaloneApp = null;
124
+ if (mode === "standalone" && dbPath) {
125
+ try {
126
+ standaloneApp = await buildStandaloneApp(dbPath);
127
+ p.log.success(`Standalone mode · reading ${color.cyan(path.relative(process.cwd(), dbPath))}`);
128
+ }
129
+ catch (e) {
130
+ p.log.error(`Failed to open trace file ${color.cyan(dbPath)}\n ${e.message}\n` +
131
+ ` Make sure better-sqlite3 is available: ${color.cyan("npm install better-sqlite3")}`);
132
+ process.exit(1);
133
+ }
77
134
  }
78
135
  const server = http.createServer((req, res) => {
79
136
  const url = req.url || "/";
80
137
  if (url.startsWith("/__blok")) {
138
+ if (standaloneApp) {
139
+ return standaloneApp(req, res);
140
+ }
81
141
  return proxyRequest(req, res, backendUrl);
82
142
  }
83
143
  serveHandler(req, res, {
@@ -95,7 +155,12 @@ export async function startStudio(options) {
95
155
  studioUrl += `/runs/${encodeURIComponent(options.run)}`;
96
156
  }
97
157
  p.log.success(`Studio running at ${color.cyan(studioUrl)}`);
98
- p.log.info(`Connected to backend at ${color.dim(backendUrl)}`);
158
+ if (mode === "standalone" && dbPath) {
159
+ p.log.info(`Standalone · ${color.dim(`reading ${path.relative(process.cwd(), dbPath)}`)}`);
160
+ }
161
+ else {
162
+ p.log.info(`Proxying to backend at ${color.dim(backendUrl)}`);
163
+ }
99
164
  console.log(color.dim(" Press Ctrl+C to stop\n"));
100
165
  if (shouldOpen) {
101
166
  await open(studioUrl);
package/dist/index.js CHANGED
@@ -168,6 +168,7 @@ async function main() {
168
168
  program
169
169
  .command("dev")
170
170
  .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.")
171
172
  .action(async (options) => {
172
173
  await analytics.trackCommandExecution({
173
174
  command: "dev",
@@ -0,0 +1,5 @@
1
+ export declare function tryConnect(host: string, port: number, timeoutMs: number): Promise<boolean>;
2
+ export declare function waitForGrpcPort(port: number, timeoutMs: number, proc?: {
3
+ exitCode: number | null;
4
+ on(event: "exit", listener: () => void): void;
5
+ }): Promise<boolean>;
@@ -0,0 +1,35 @@
1
+ import { Socket } from "node:net";
2
+ export function tryConnect(host, port, timeoutMs) {
3
+ return new Promise((resolve) => {
4
+ const sock = new Socket();
5
+ const done = (ok) => {
6
+ sock.destroy();
7
+ resolve(ok);
8
+ };
9
+ sock.setTimeout(timeoutMs);
10
+ sock
11
+ .once("connect", () => done(true))
12
+ .once("timeout", () => done(false))
13
+ .once("error", () => done(false));
14
+ sock.connect(port, host);
15
+ });
16
+ }
17
+ export async function waitForGrpcPort(port, timeoutMs, proc) {
18
+ if (proc && proc.exitCode !== null)
19
+ return false;
20
+ const start = Date.now();
21
+ let exited = false;
22
+ proc?.on("exit", () => {
23
+ exited = true;
24
+ });
25
+ while (Date.now() - start < timeoutMs) {
26
+ if (exited)
27
+ return false;
28
+ if (await tryConnect("127.0.0.1", port, 500))
29
+ return true;
30
+ if (await tryConnect("::1", port, 500))
31
+ return true;
32
+ await new Promise((r) => setTimeout(r, 200));
33
+ }
34
+ return false;
35
+ }
@@ -5,10 +5,12 @@ export interface RuntimeInfo {
5
5
  version?: string;
6
6
  installHint: string;
7
7
  defaultPort: number;
8
+ defaultGrpcPort: number;
8
9
  commands: string[];
9
10
  toolchain: string;
10
11
  installDeps: string;
11
12
  startCmd: string;
13
+ grpcStartCmd?: string;
12
14
  sdkDir: string;
13
15
  secondaryTool?: {
14
16
  name: string;
@@ -18,6 +20,7 @@ export interface RuntimeInfo {
18
20
  installHint: string;
19
21
  };
20
22
  }
23
+ export declare function detectRr(): string | null;
21
24
  export declare function detectRuntimes(): Promise<RuntimeInfo[]>;
22
25
  export declare function getRuntimeDefinition(kind: string): Omit<RuntimeInfo, "available" | "version"> | undefined;
23
26
  export declare function getAllRuntimeDefinitions(): Omit<RuntimeInfo, "available" | "version">[];
@@ -1,12 +1,24 @@
1
1
  import child_process from "node:child_process";
2
2
  import util from "node:util";
3
3
  const exec = util.promisify(child_process.exec);
4
+ export function detectRr() {
5
+ for (const bin of ["/opt/homebrew/bin/rr", "rr"]) {
6
+ try {
7
+ child_process.execSync(`${bin} --version`, { stdio: "ignore" });
8
+ return bin;
9
+ }
10
+ catch {
11
+ }
12
+ }
13
+ return null;
14
+ }
4
15
  const RUNTIME_DEFINITIONS = [
5
16
  {
6
17
  kind: "python3",
7
18
  label: "Python 3",
8
19
  installHint: "Install Python: https://python.org/downloads/",
9
20
  defaultPort: 9007,
21
+ defaultGrpcPort: 10007,
10
22
  commands: ["python3 --version"],
11
23
  toolchain: "python3",
12
24
  installDeps: "pip3 install -r requirements.txt",
@@ -18,6 +30,7 @@ const RUNTIME_DEFINITIONS = [
18
30
  label: "Go",
19
31
  installHint: "Install Go: https://go.dev/dl/",
20
32
  defaultPort: 9001,
33
+ defaultGrpcPort: 10001,
21
34
  commands: ["go version"],
22
35
  toolchain: "go",
23
36
  installDeps: "go mod download",
@@ -29,6 +42,7 @@ const RUNTIME_DEFINITIONS = [
29
42
  label: "Rust",
30
43
  installHint: "Install Rust: https://rustup.rs/",
31
44
  defaultPort: 9002,
45
+ defaultGrpcPort: 10002,
32
46
  commands: ["rustc --version"],
33
47
  toolchain: "rustc + cargo",
34
48
  installDeps: "cargo build --release",
@@ -40,6 +54,7 @@ const RUNTIME_DEFINITIONS = [
40
54
  label: "Java",
41
55
  installHint: "Install JDK 17+: https://adoptium.net/",
42
56
  defaultPort: 9003,
57
+ defaultGrpcPort: 10003,
43
58
  commands: [
44
59
  "java --version",
45
60
  "/opt/homebrew/opt/openjdk/bin/java --version",
@@ -60,6 +75,7 @@ const RUNTIME_DEFINITIONS = [
60
75
  label: "C# / .NET",
61
76
  installHint: "Install .NET SDK: https://dotnet.microsoft.com/download",
62
77
  defaultPort: 9004,
78
+ defaultGrpcPort: 10004,
63
79
  commands: ["dotnet --version"],
64
80
  toolchain: "dotnet",
65
81
  installDeps: "dotnet restore",
@@ -69,12 +85,14 @@ const RUNTIME_DEFINITIONS = [
69
85
  {
70
86
  kind: "php",
71
87
  label: "PHP",
72
- installHint: "Install PHP 8.2+: https://php.net/downloads",
88
+ installHint: "Install PHP 8.2+ + RoadRunner: https://php.net/downloads (rr: brew install roadrunner)",
73
89
  defaultPort: 9005,
90
+ defaultGrpcPort: 10005,
74
91
  commands: ["php --version"],
75
- toolchain: "php + composer",
92
+ toolchain: "php + composer + rr",
76
93
  installDeps: "composer install",
77
94
  startCmd: "php bin/serve.php",
95
+ grpcStartCmd: "rr serve -c .rr.yaml --override grpc.listen=tcp://127.0.0.1:10005",
78
96
  sdkDir: "php",
79
97
  secondaryTool: {
80
98
  name: "Composer",
@@ -87,6 +105,7 @@ const RUNTIME_DEFINITIONS = [
87
105
  label: "Ruby",
88
106
  installHint: "Install Ruby 3.2+: https://ruby-lang.org/en/downloads/",
89
107
  defaultPort: 9006,
108
+ defaultGrpcPort: 10006,
90
109
  commands: ["ruby --version", "/opt/homebrew/opt/ruby/bin/ruby --version"],
91
110
  toolchain: "ruby + bundler",
92
111
  installDeps: "bundle install",
@@ -6,10 +6,13 @@ type SpinnerHandler = {
6
6
  };
7
7
  export interface RuntimeConfig {
8
8
  port: number;
9
+ grpcPort?: number;
9
10
  startCmd: string;
11
+ grpcStartCmd?: string;
10
12
  cwd: string;
11
13
  kind: string;
12
14
  label: string;
15
+ transport?: "grpc" | "http";
13
16
  }
14
17
  export interface TriggerConfig {
15
18
  kind: string;
@@ -43,10 +43,13 @@ export async function setupRuntime(runtime, githubRepoLocal, projectDir, spinner
43
43
  spinner.message(`${runtime.label} runtime setup complete.`);
44
44
  return {
45
45
  port: runtime.defaultPort,
46
+ grpcPort: runtime.defaultGrpcPort,
46
47
  startCmd: startCmdOverride || runtime.startCmd,
48
+ grpcStartCmd: runtime.grpcStartCmd,
47
49
  cwd: path.relative(projectDir, blokctlRuntimeDir),
48
50
  kind: runtime.kind,
49
51
  label: runtime.label,
52
+ transport: "grpc",
50
53
  };
51
54
  }
52
55
  async function setupPython3(sdkDir, projectRuntimeDir, spinner) {
@@ -166,17 +169,23 @@ export function generateRuntimeEnvVars(runtimeConfigs) {
166
169
  const envKey = rc.kind === "csharp" ? "CSHARP" : rc.kind.toUpperCase();
167
170
  lines.push(`RUNTIME_${envKey}_HOST=localhost`);
168
171
  lines.push(`RUNTIME_${envKey}_PORT=${rc.port}`);
172
+ if (rc.grpcPort !== undefined) {
173
+ lines.push(`RUNTIME_${envKey}_GRPC_PORT=${rc.grpcPort}`);
174
+ }
169
175
  }
176
+ lines.push("BLOK_TRANSPORT=grpc");
170
177
  return lines.join("\n");
171
178
  }
172
179
  export function generateSupervisordConfig(runtimeConfigs) {
173
180
  let config = "";
174
181
  for (const rc of runtimeConfigs) {
182
+ const cmd = rc.grpcStartCmd ?? rc.startCmd;
183
+ const grpcPortLine = rc.grpcPort !== undefined ? `,GRPC_PORT="${rc.grpcPort}"` : "";
175
184
  config += `
176
185
  [program:${rc.kind}_runtime]
177
- command=${rc.startCmd}
186
+ command=${cmd}
178
187
  directory=/app/${rc.cwd}
179
- environment=PORT="${rc.port}",HOST="0.0.0.0"
188
+ environment=PORT="${rc.port}"${grpcPortLine},HOST="0.0.0.0",BLOK_TRANSPORT="grpc"
180
189
  autostart=true
181
190
  autorestart=true
182
191
  stderr_logfile=/var/log/${rc.kind}.err.log