blokctl 0.6.19 → 0.6.21

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.
@@ -126,6 +126,35 @@ async function collectTsFiles(dir) {
126
126
  }
127
127
  return out;
128
128
  }
129
+ async function collectJsonWorkflowNames(dir) {
130
+ const names = [];
131
+ let dirents;
132
+ try {
133
+ dirents = await fsp.readdir(dir, { withFileTypes: true });
134
+ }
135
+ catch {
136
+ return names;
137
+ }
138
+ for (const d of dirents) {
139
+ if (d.name.startsWith("_") || d.name.startsWith("."))
140
+ continue;
141
+ const full = path.join(dir, d.name);
142
+ if (d.isDirectory()) {
143
+ names.push(...(await collectJsonWorkflowNames(full)));
144
+ }
145
+ else if (d.name.endsWith(".json")) {
146
+ try {
147
+ const parsed = JSON.parse(await fsp.readFile(full, "utf8"));
148
+ if (typeof parsed.name === "string" && (parsed.trigger !== undefined || parsed.steps !== undefined)) {
149
+ names.push(parsed.name);
150
+ }
151
+ }
152
+ catch {
153
+ }
154
+ }
155
+ }
156
+ return names;
157
+ }
129
158
  async function resolveWorkflowsDir(cwd, explicit) {
130
159
  const candidates = explicit
131
160
  ? [explicit]
@@ -159,6 +188,14 @@ export async function generateAppTypes(opts) {
159
188
  : path.join(cwd, opts.out ?? "blok-app.d.ts");
160
189
  console.log(color.dim(`Scanning ${color.cyan(dir)} (recursive)\n`));
161
190
  const files = await collectTsFiles(dir);
191
+ const jsonScanDirs = [dir, path.join(cwd, "workflows/json"), path.join(cwd, "triggers/http/workflows/json")];
192
+ const jsonNames = [...new Set((await Promise.all(jsonScanDirs.map((d) => collectJsonWorkflowNames(d)))).flat())];
193
+ const warnJsonSkipped = () => {
194
+ if (jsonNames.length === 0)
195
+ return;
196
+ console.log(color.yellow(`ℹ️ ${jsonNames.length} JSON workflow(s) are NOT in app-types (JSON has no TS type to import): ${jsonNames.join(", ")}.`));
197
+ console.log(color.dim(" Convert them to TS workflows to type them, or call them by string name on the client.\n"));
198
+ };
162
199
  const entries = [];
163
200
  const skipped = [];
164
201
  for (const file of files) {
@@ -173,6 +210,7 @@ export async function generateAppTypes(opts) {
173
210
  console.log(color.yellow("No TS workflows with a literal `name:` found — nothing to generate."));
174
211
  if (skipped.length > 0)
175
212
  console.log(color.dim(`Skipped (no literal name): ${skipped.join(", ")}`));
213
+ warnJsonSkipped();
176
214
  return;
177
215
  }
178
216
  const { source, collisions } = buildAppTypeSource(entries, outFile);
@@ -191,5 +229,6 @@ export async function generateAppTypes(opts) {
191
229
  if (skipped.length > 0) {
192
230
  console.log(color.dim(`ℹ️ Skipped ${skipped.length} file(s) without a literal workflow name (dynamic name or not a workflow): ${skipped.join(", ")}`));
193
231
  }
194
- console.log(color.dim('\nNext: `import type { BlokApp } from "<out>"` and `createBlokClient<BlokApp>({ baseUrl })`.\n'));
232
+ warnJsonSkipped();
233
+ console.log(color.dim('Next: `import type { BlokApp } from "<out>"` and `createBlokClient<BlokApp>({ baseUrl })`.\n'));
195
234
  }
@@ -0,0 +1,2 @@
1
+ import type { OptionValues } from "../../services/commander.js";
2
+ export declare function runtimeAdd(kindArg: string | undefined, options: OptionValues): Promise<void>;
@@ -0,0 +1,143 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import * as p from "@clack/prompts";
4
+ import color from "picocolors";
5
+ import { isNonInteractive } from "../../services/non-interactive.js";
6
+ import { detectRuntimes, getRuntimeDefinition } from "../../services/runtime-detector.js";
7
+ import { ensureRuntimeGitignore, rewriteRuntimeEnvBlock, rewriteSupervisordRuntimes, runtimeEnvKey, withRuntime, } from "../../services/runtime-mutations.js";
8
+ import { setupRuntime } from "../../services/runtime-setup.js";
9
+ import { RuntimeCommandError, assertGrpcPortFree, assertSidecarKind, readConfigSafe, reportRuntimeError, resolveProjectRoot, resolveSdkSource, } from "./shared.js";
10
+ export async function runtimeAdd(kindArg, options) {
11
+ try {
12
+ let grpcPortOverride;
13
+ if (options.grpcPort !== undefined) {
14
+ const parsed = Number(options.grpcPort);
15
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) {
16
+ throw new RuntimeCommandError(`--grpc-port must be an integer 1-65535 (got "${options.grpcPort}").`);
17
+ }
18
+ grpcPortOverride = parsed;
19
+ }
20
+ const root = resolveProjectRoot(options.directory);
21
+ const config = readConfigSafe(root);
22
+ const installedKinds = Object.keys(config.runtimes ?? {});
23
+ const nonInteractive = isNonInteractive() || options.yes === true;
24
+ let detected;
25
+ let kind = kindArg?.trim().toLowerCase();
26
+ if (!kind) {
27
+ if (nonInteractive) {
28
+ throw new RuntimeCommandError("Specify a runtime: blokctl runtime add <go|rust|java|csharp|php|ruby|python3>");
29
+ }
30
+ detected = await detectRuntimes();
31
+ const choices = detected
32
+ .filter((d) => !installedKinds.includes(d.kind))
33
+ .map((d) => ({
34
+ value: d.kind,
35
+ label: d.label,
36
+ hint: d.available ? `${d.toolchain} ready` : `needs ${d.toolchain}`,
37
+ }));
38
+ if (choices.length === 0) {
39
+ p.intro(color.inverse(" Add runtime "));
40
+ p.outro(color.dim("All supported runtimes are already installed."));
41
+ return;
42
+ }
43
+ const picked = await p.select({ message: "Which runtime do you want to add?", options: choices });
44
+ if (p.isCancel(picked)) {
45
+ p.cancel("Cancelled.");
46
+ return;
47
+ }
48
+ kind = picked;
49
+ }
50
+ assertSidecarKind(kind);
51
+ const def = getRuntimeDefinition(kind);
52
+ if (!def)
53
+ throw new RuntimeCommandError(`Unknown runtime "${kind}".`);
54
+ const sdkDir = path.join(root, ".blok", "runtimes", kind);
55
+ const alreadyInstalled = Boolean(config.runtimes?.[kind]) || fs.existsSync(sdkDir);
56
+ p.intro(color.inverse(` Add ${def.label} runtime `));
57
+ if (alreadyInstalled && options.force !== true) {
58
+ if (nonInteractive) {
59
+ p.outro(color.dim(`${def.label} is already installed. Re-run with --force to reinstall.`));
60
+ return;
61
+ }
62
+ const reinstall = await p.confirm({
63
+ message: `${def.label} is already installed. Reinstall it?`,
64
+ initialValue: false,
65
+ });
66
+ if (p.isCancel(reinstall) || !reinstall) {
67
+ p.outro(color.dim("Left unchanged."));
68
+ return;
69
+ }
70
+ }
71
+ const rt = (detected ?? (await detectRuntimes())).find((d) => d.kind === kind);
72
+ if (!rt)
73
+ throw new RuntimeCommandError(`Unknown runtime "${kind}".`);
74
+ if (!rt.available && options.skipToolchainCheck !== true) {
75
+ let missing = rt.toolchain;
76
+ let hint = rt.installHint;
77
+ if (rt.secondaryTool && rt.secondaryTool.available === false) {
78
+ missing = rt.secondaryTool.name;
79
+ hint = rt.secondaryTool.installHint;
80
+ }
81
+ throw new RuntimeCommandError(`${def.label} toolchain not detected (need ${color.bold(missing)}). ${hint}\n Already have it? Re-run with --skip-toolchain-check.`);
82
+ }
83
+ const grpcPort = grpcPortOverride ?? rt.defaultGrpcPort;
84
+ const clash = Object.values(config.runtimes ?? {}).find((rc) => rc.kind !== kind && rc.grpcPort === grpcPort);
85
+ if (clash) {
86
+ throw new RuntimeCommandError(`gRPC port ${grpcPort} is already used by the ${clash.label} runtime. Pass --grpc-port <n> to pick another.`);
87
+ }
88
+ if (!alreadyInstalled)
89
+ await assertGrpcPortFree(grpcPort);
90
+ const s = p.spinner();
91
+ s.start("Resolving SDK source…");
92
+ const source = await resolveSdkSource(root, options.local, (msg) => s.message(msg));
93
+ if (alreadyInstalled)
94
+ fs.rmSync(sdkDir, { recursive: true, force: true });
95
+ let rc;
96
+ try {
97
+ rc = await setupRuntime(rt, source, root, s);
98
+ }
99
+ catch (err) {
100
+ fs.rmSync(sdkDir, { recursive: true, force: true });
101
+ s.stop(color.red(`${def.label} setup failed`));
102
+ throw new RuntimeCommandError(`${def.label} setup failed: ${err.message.split("\n")[0]}`);
103
+ }
104
+ if (grpcPortOverride !== undefined) {
105
+ rc.grpcPort = grpcPortOverride;
106
+ if (rc.grpcStartCmd)
107
+ rc.grpcStartCmd = rc.grpcStartCmd.split(String(rt.defaultGrpcPort)).join(String(grpcPortOverride));
108
+ }
109
+ s.stop(`${def.label} runtime ready`);
110
+ const nextConfig = withRuntime(config, rc);
111
+ const remaining = Object.values(nextConfig.runtimes ?? {});
112
+ fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
113
+ fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
114
+ const envPath = path.join(root, ".env.local");
115
+ const envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
116
+ fs.writeFileSync(envPath, rewriteRuntimeEnvBlock(envContent, remaining));
117
+ const supervisordPath = path.join(root, "supervisord.conf");
118
+ if (fs.existsSync(supervisordPath)) {
119
+ fs.writeFileSync(supervisordPath, rewriteSupervisordRuntimes(fs.readFileSync(supervisordPath, "utf8"), remaining));
120
+ }
121
+ const gitignorePath = path.join(root, ".gitignore");
122
+ if (fs.existsSync(gitignorePath)) {
123
+ const before = fs.readFileSync(gitignorePath, "utf8");
124
+ const after = ensureRuntimeGitignore(before);
125
+ if (after !== before)
126
+ fs.writeFileSync(gitignorePath, after);
127
+ }
128
+ p.note([
129
+ `${color.green("✓")} .blok/config.json ${color.dim(`runtimes.${kind}`)}`,
130
+ `${color.green("✓")} .env.local ${color.dim(`RUNTIME_${runtimeEnvKey(kind)}_GRPC_PORT=${rc.grpcPort}`)}`,
131
+ fs.existsSync(supervisordPath)
132
+ ? `${color.green("✓")} supervisord.conf ${color.dim(`[program:${kind}_runtime]`)}`
133
+ : "",
134
+ `${color.green("✓")} runtimes/${kind}/nodes/ ${color.dim("(your runtime nodes go here)")}`,
135
+ ]
136
+ .filter(Boolean)
137
+ .join("\n"), `${def.label} added`);
138
+ p.outro(`Run ${color.cyan("blokctl dev")} to start it, then add ${color.cyan(`type: "runtime.${kind}"`)} steps to your workflows.`);
139
+ }
140
+ catch (err) {
141
+ reportRuntimeError(err);
142
+ }
143
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { Command } from "commander";
2
+ import { program } from "../../services/commander.js";
3
+ import { runtimeAdd } from "./add.js";
4
+ import { runtimeList } from "./list.js";
5
+ import { runtimeRemove } from "./remove.js";
6
+ const runtime = new Command("runtime").description("Add, remove, or list language runtimes in an existing project");
7
+ runtime.action(() => {
8
+ runtime.help();
9
+ });
10
+ runtime
11
+ .command("add")
12
+ .description("Add a language runtime (go, rust, java, csharp, php, ruby, python3) to this project")
13
+ .argument("[runtime]", "Runtime to add (omit for an interactive picker)")
14
+ .option("-d, --directory <path>", "Project directory (default: current directory)")
15
+ .option("--local <path>", "Use a local blok repo for SDK source instead of fetching by version")
16
+ .option("--grpc-port <port>", "Override the gRPC port for this runtime")
17
+ .option("--force", "Reinstall if the runtime is already present")
18
+ .option("--skip-toolchain-check", "Add even if the language toolchain isn't detected")
19
+ .option("-y, --yes", "Skip prompts (non-interactive)")
20
+ .action(async (runtimeArg, options) => {
21
+ await runtimeAdd(runtimeArg, options);
22
+ });
23
+ runtime
24
+ .command("remove")
25
+ .alias("rm")
26
+ .description("Remove a language runtime from this project")
27
+ .argument("<runtime>", "Runtime to remove")
28
+ .option("-d, --directory <path>", "Project directory (default: current directory)")
29
+ .option("--purge-nodes", "Also delete your custom nodes in runtimes/<runtime>/nodes/")
30
+ .option("-y, --yes", "Skip prompts (keeps your custom nodes)")
31
+ .action(async (runtimeArg, options) => {
32
+ await runtimeRemove(runtimeArg.toLowerCase(), options);
33
+ });
34
+ runtime
35
+ .command("list")
36
+ .alias("ls")
37
+ .description("List installed runtimes and which are available to add")
38
+ .option("-d, --directory <path>", "Project directory (default: current directory)")
39
+ .option("--json", "Output as JSON")
40
+ .action(async (options) => {
41
+ await runtimeList(options);
42
+ });
43
+ program.addCommand(runtime);
@@ -0,0 +1,2 @@
1
+ import type { OptionValues } from "../../services/commander.js";
2
+ export declare function runtimeList(options: OptionValues): Promise<void>;
@@ -0,0 +1,60 @@
1
+ import * as p from "@clack/prompts";
2
+ import color from "picocolors";
3
+ import { detectRuntimes } from "../../services/runtime-detector.js";
4
+ import { readConfigSafe, reportRuntimeError, resolveProjectRoot } from "./shared.js";
5
+ export async function runtimeList(options) {
6
+ try {
7
+ const root = resolveProjectRoot(options.directory);
8
+ const installed = readConfigSafe(root).runtimes ?? {};
9
+ const detected = await detectRuntimes();
10
+ const detectedByKind = new Map(detected.map((d) => [d.kind, d]));
11
+ const installedKinds = Object.keys(installed);
12
+ if (options.json) {
13
+ console.log(JSON.stringify({
14
+ installed: installedKinds.map((kind) => ({
15
+ kind,
16
+ label: installed[kind].label,
17
+ grpcPort: installed[kind].grpcPort,
18
+ version: installed[kind].version,
19
+ requiredVersion: installed[kind].requiredVersion,
20
+ toolchainAvailable: detectedByKind.get(kind)?.available ?? false,
21
+ })),
22
+ available: detected
23
+ .filter((d) => !(d.kind in installed))
24
+ .map((d) => ({ kind: d.kind, label: d.label, toolchainAvailable: d.available })),
25
+ }, null, 2));
26
+ return;
27
+ }
28
+ p.intro(color.inverse(" Blok runtimes "));
29
+ if (installedKinds.length === 0) {
30
+ p.log.info(color.dim("No sidecar runtimes installed yet."));
31
+ }
32
+ else {
33
+ const rows = installedKinds.map((kind) => {
34
+ const rc = installed[kind];
35
+ const d = detectedByKind.get(kind);
36
+ const ready = d?.available ?? false;
37
+ const mark = ready ? color.green("✓") : color.yellow("!");
38
+ const port = color.dim(`gRPC :${rc.grpcPort ?? "?"}`.padEnd(14));
39
+ const tool = ready
40
+ ? color.dim(`${d?.toolchain} ${d?.version ?? ""}`.trim())
41
+ : color.yellow("toolchain not detected");
42
+ return `${mark} ${color.bold(rc.label.padEnd(12))} ${port} ${tool}`;
43
+ });
44
+ p.note(rows.join("\n"), `Installed (${installedKinds.length})`);
45
+ }
46
+ const available = detected.filter((d) => !(d.kind in installed));
47
+ if (available.length > 0) {
48
+ const rows = available.map((d) => {
49
+ const statusText = (d.available ? `${d.toolchain} ready` : `needs ${d.toolchain}`).padEnd(20);
50
+ const status = d.available ? color.green(statusText) : color.dim(statusText);
51
+ return `${color.bold(d.label.padEnd(12))} ${status} ${color.dim(`blokctl runtime add ${d.kind}`)}`;
52
+ });
53
+ p.note(rows.join("\n"), `Available to add (${available.length})`);
54
+ }
55
+ p.outro(color.dim("Node / TypeScript runs in-process — always available, nothing to install."));
56
+ }
57
+ catch (err) {
58
+ reportRuntimeError(err);
59
+ }
60
+ }
@@ -0,0 +1,2 @@
1
+ import type { OptionValues } from "../../services/commander.js";
2
+ export declare function runtimeRemove(kind: string, options: OptionValues): Promise<void>;
@@ -0,0 +1,114 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import * as p from "@clack/prompts";
4
+ import color from "picocolors";
5
+ import { isNonInteractive } from "../../services/non-interactive.js";
6
+ import { getRuntimeDefinition } from "../../services/runtime-detector.js";
7
+ import { rewriteRuntimeEnvBlock, rewriteSupervisordRuntimes, runtimeEnvKey, withoutRuntime, } from "../../services/runtime-mutations.js";
8
+ import { assertSidecarKind, listUserNodes, readConfigSafe, reportRuntimeError, resolveProjectRoot, scanWorkflowsForRuntime, } from "./shared.js";
9
+ export async function runtimeRemove(kind, options) {
10
+ try {
11
+ assertSidecarKind(kind);
12
+ const root = resolveProjectRoot(options.directory);
13
+ const nonInteractive = isNonInteractive() || options.yes === true;
14
+ const label = getRuntimeDefinition(kind)?.label ?? kind;
15
+ const config = readConfigSafe(root);
16
+ const sdkDir = path.join(root, ".blok", "runtimes", kind);
17
+ const inConfig = Boolean(config.runtimes?.[kind]);
18
+ const onDisk = fs.existsSync(sdkDir);
19
+ if (!inConfig && !onDisk) {
20
+ p.intro(color.inverse(` Remove ${label} runtime `));
21
+ p.outro(color.dim(`${label} isn't installed in this project — nothing to remove.`));
22
+ return;
23
+ }
24
+ p.intro(color.inverse(` Remove ${label} runtime `));
25
+ const hits = scanWorkflowsForRuntime(root, kind);
26
+ if (hits.length > 0) {
27
+ const list = hits
28
+ .slice(0, 10)
29
+ .map((h) => ` ${color.yellow("•")} ${h.file}${h.count > 1 ? color.dim(` (${h.count} refs)`) : ""}`)
30
+ .join("\n");
31
+ const more = hits.length > 10 ? `\n ${color.dim(`…and ${hits.length - 10} more`)}` : "";
32
+ p.log.warn(`${color.yellow(`${hits.length} workflow file(s)`)} reference ${color.bold(`runtime.${kind}`)} — those steps will fail at run time after removal:\n${list}${more}`);
33
+ }
34
+ const userNodes = listUserNodes(root, kind);
35
+ let deleteNodes = false;
36
+ if (!userNodes.isSymlink && userNodes.files.length > 0) {
37
+ if (options.purgeNodes === true) {
38
+ deleteNodes = true;
39
+ }
40
+ else if (nonInteractive) {
41
+ deleteNodes = false;
42
+ p.log.info(color.dim(`Keeping your ${userNodes.files.length} node file(s) in runtimes/${kind}/nodes/ (use --purge-nodes to delete).`));
43
+ }
44
+ else {
45
+ const answer = await p.confirm({
46
+ message: `Also delete your ${color.bold(`${userNodes.files.length} custom node file(s)`)} in runtimes/${kind}/nodes/?`,
47
+ initialValue: false,
48
+ });
49
+ deleteNodes = !p.isCancel(answer) && answer === true;
50
+ }
51
+ }
52
+ if (!nonInteractive) {
53
+ const ok = await p.confirm({ message: `Remove the ${label} runtime?`, initialValue: false });
54
+ if (p.isCancel(ok) || !ok) {
55
+ p.outro(color.dim("Left unchanged."));
56
+ return;
57
+ }
58
+ }
59
+ const s = p.spinner();
60
+ s.start(`Removing ${label} runtime…`);
61
+ const nextConfig = withoutRuntime(config, kind);
62
+ fs.mkdirSync(path.join(root, ".blok"), { recursive: true });
63
+ fs.writeFileSync(path.join(root, ".blok", "config.json"), `${JSON.stringify(nextConfig, null, 2)}\n`);
64
+ const remaining = Object.values(nextConfig.runtimes ?? {});
65
+ const envPath = path.join(root, ".env.local");
66
+ if (fs.existsSync(envPath)) {
67
+ fs.writeFileSync(envPath, rewriteRuntimeEnvBlock(fs.readFileSync(envPath, "utf8"), remaining));
68
+ }
69
+ const supervisordPath = path.join(root, "supervisord.conf");
70
+ if (fs.existsSync(supervisordPath)) {
71
+ fs.writeFileSync(supervisordPath, rewriteSupervisordRuntimes(fs.readFileSync(supervisordPath, "utf8"), remaining));
72
+ }
73
+ fs.rmSync(sdkDir, { recursive: true, force: true });
74
+ const projRuntimeDir = path.join(root, "runtimes", kind);
75
+ const tryLstat = (target) => {
76
+ try {
77
+ return fs.lstatSync(target);
78
+ }
79
+ catch {
80
+ return null;
81
+ }
82
+ };
83
+ for (const name of ["nodes", "core"]) {
84
+ const link = path.join(projRuntimeDir, name);
85
+ if (tryLstat(link)?.isSymbolicLink())
86
+ fs.unlinkSync(link);
87
+ }
88
+ if (deleteNodes)
89
+ fs.rmSync(path.join(projRuntimeDir, "nodes"), { recursive: true, force: true });
90
+ if (fs.existsSync(projRuntimeDir) && fs.readdirSync(projRuntimeDir).length === 0)
91
+ fs.rmdirSync(projRuntimeDir);
92
+ s.stop(`${label} runtime removed`);
93
+ const keptNodes = !deleteNodes && !userNodes.isSymlink && userNodes.files.length > 0;
94
+ p.note([
95
+ `${color.red("−")} .blok/config.json ${color.dim(`runtimes.${kind}`)}`,
96
+ `${color.red("−")} .env.local ${color.dim(`RUNTIME_${runtimeEnvKey(kind)}_*`)}`,
97
+ fs.existsSync(supervisordPath)
98
+ ? `${color.red("−")} supervisord.conf ${color.dim(`[program:${kind}_runtime]`)}`
99
+ : "",
100
+ `${color.red("−")} .blok/runtimes/${kind}/ ${color.dim("(SDK source + build output)")}`,
101
+ keptNodes
102
+ ? `${color.green("✓")} runtimes/${kind}/nodes/ ${color.dim(`kept your ${userNodes.files.length} node file(s)`)}`
103
+ : "",
104
+ ]
105
+ .filter(Boolean)
106
+ .join("\n"), `${label} removed`);
107
+ p.outro(hits.length > 0
108
+ ? color.yellow(`Update the ${hits.length} workflow(s) that referenced runtime.${kind}. Re-add anytime: blokctl runtime add ${kind}.`)
109
+ : color.dim(`Done. Re-add anytime: blokctl runtime add ${kind}.`));
110
+ }
111
+ catch (err) {
112
+ reportRuntimeError(err);
113
+ }
114
+ }
@@ -0,0 +1,22 @@
1
+ import { type ProjectConfig } from "../../services/runtime-setup.js";
2
+ export declare const NON_SIDECAR_KINDS: Set<string>;
3
+ export declare class RuntimeCommandError extends Error {
4
+ }
5
+ export declare function readConfigSafe(projectRoot: string): ProjectConfig;
6
+ export declare function assertGrpcPortFree(grpcPort: number): Promise<void>;
7
+ export declare function resolveProjectRoot(directory?: string): string;
8
+ export declare function readFrameworkTag(projectRoot: string): string | null;
9
+ export declare function resolveSdkSource(projectRoot: string, localOverride: string | undefined, onProgress?: (msg: string) => void): Promise<string>;
10
+ export interface WorkflowRuntimeHit {
11
+ file: string;
12
+ count: number;
13
+ }
14
+ export declare function scanWorkflowsForRuntime(projectRoot: string, kind: string): WorkflowRuntimeHit[];
15
+ export interface UserNodesInfo {
16
+ dir: string;
17
+ isSymlink: boolean;
18
+ files: string[];
19
+ }
20
+ export declare function listUserNodes(projectRoot: string, kind: string): UserNodesInfo;
21
+ export declare function reportRuntimeError(err: unknown): void;
22
+ export declare function assertSidecarKind(kind: string): void;
@@ -0,0 +1,164 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import * as p from "@clack/prompts";
5
+ import color from "picocolors";
6
+ import simpleGit from "simple-git";
7
+ import { tryConnect } from "../../services/health-probe.js";
8
+ import { getRuntimeDefinition } from "../../services/runtime-detector.js";
9
+ import { readProjectConfig } from "../../services/runtime-setup.js";
10
+ const HOME_DIR = `${os.homedir()}/.blok`;
11
+ const GITHUB_REPO_REMOTE = "https://github.com/well-prado/blok.git";
12
+ export const NON_SIDECAR_KINDS = new Set(["node", "nodejs", "typescript", "ts", "bun", "docker", "wasm"]);
13
+ export class RuntimeCommandError extends Error {
14
+ }
15
+ export function readConfigSafe(projectRoot) {
16
+ try {
17
+ return readProjectConfig(projectRoot) ?? {};
18
+ }
19
+ catch {
20
+ throw new RuntimeCommandError(`Could not parse ${path.join(projectRoot, ".blok", "config.json")} — fix or delete it and retry.`);
21
+ }
22
+ }
23
+ export async function assertGrpcPortFree(grpcPort) {
24
+ if (await tryConnect("127.0.0.1", grpcPort, 400)) {
25
+ throw new RuntimeCommandError(`gRPC port ${grpcPort} is already in use by a live process. Stop it, or pass --grpc-port <n> to use another port.`);
26
+ }
27
+ }
28
+ export function resolveProjectRoot(directory) {
29
+ const root = path.resolve(directory ?? process.cwd());
30
+ const pkgPath = path.join(root, "package.json");
31
+ if (!fs.existsSync(pkgPath)) {
32
+ throw new RuntimeCommandError(`No package.json found at ${root}. Run this inside a Blok project, or pass --directory <path>.`);
33
+ }
34
+ let pkg;
35
+ try {
36
+ pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
37
+ }
38
+ catch {
39
+ throw new RuntimeCommandError(`Could not parse ${pkgPath}.`);
40
+ }
41
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
42
+ const looksLikeBlok = Object.keys(deps).some((d) => d.startsWith("@blokjs/")) || fs.existsSync(path.join(root, ".blok"));
43
+ if (!looksLikeBlok) {
44
+ throw new RuntimeCommandError(`${root} doesn't look like a Blok project (no @blokjs/* dependency, no .blok/). Pass --directory <path> to the project root.`);
45
+ }
46
+ return root;
47
+ }
48
+ export function readFrameworkTag(projectRoot) {
49
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectRoot, "package.json"), "utf8"));
50
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
51
+ const range = deps["@blokjs/runner"] ?? deps["@blokjs/shared"] ?? deps.blokctl;
52
+ if (!range)
53
+ return null;
54
+ const m = range.match(/(\d+\.\d+\.\d+)/);
55
+ return m ? `v${m[1]}` : null;
56
+ }
57
+ export async function resolveSdkSource(projectRoot, localOverride, onProgress) {
58
+ if (localOverride) {
59
+ const resolved = path.resolve(localOverride);
60
+ if (!fs.existsSync(path.join(resolved, "sdks"))) {
61
+ throw new RuntimeCommandError(`--local path ${resolved} has no sdks/ directory.`);
62
+ }
63
+ return resolved;
64
+ }
65
+ const tag = readFrameworkTag(projectRoot);
66
+ if (!tag) {
67
+ throw new RuntimeCommandError("Couldn't determine the project's Blok version from package.json. Pass --local <path-to-blok-repo> to point at a matching SDK source.");
68
+ }
69
+ const cacheDir = path.join(HOME_DIR, "sdk-src", tag);
70
+ if (fs.existsSync(path.join(cacheDir, "sdks")))
71
+ return cacheDir;
72
+ onProgress?.(`Fetching SDK source (blok ${tag})…`);
73
+ fs.mkdirSync(path.join(HOME_DIR, "sdk-src"), { recursive: true });
74
+ const git = simpleGit({ baseDir: path.join(HOME_DIR, "sdk-src") });
75
+ try {
76
+ await git.clone(GITHUB_REPO_REMOTE, cacheDir, ["--branch", tag, "--depth", "1"]);
77
+ }
78
+ catch (err) {
79
+ fs.rmSync(cacheDir, { recursive: true, force: true });
80
+ throw new RuntimeCommandError(`Couldn't fetch the SDK source for ${tag} (${err.message.split("\n")[0]}). If this version isn't on GitHub, pass --local <path-to-blok-repo>.`);
81
+ }
82
+ if (!fs.existsSync(path.join(cacheDir, "sdks"))) {
83
+ throw new RuntimeCommandError(`Fetched ${tag} but it has no sdks/ directory.`);
84
+ }
85
+ return cacheDir;
86
+ }
87
+ const SCAN_SKIP_DIRS = new Set(["node_modules", ".blok", "dist", ".git", "coverage", "runtimes", ".nx"]);
88
+ export function scanWorkflowsForRuntime(projectRoot, kind) {
89
+ const ref = new RegExp(`runtime\\.${kind}\\b`, "g");
90
+ const hits = [];
91
+ const walk = (dir) => {
92
+ let entries;
93
+ try {
94
+ entries = fs.readdirSync(dir, { withFileTypes: true });
95
+ }
96
+ catch {
97
+ return;
98
+ }
99
+ for (const entry of entries) {
100
+ if (entry.isSymbolicLink())
101
+ continue;
102
+ const full = path.join(dir, entry.name);
103
+ if (entry.isDirectory()) {
104
+ if (!SCAN_SKIP_DIRS.has(entry.name))
105
+ walk(full);
106
+ continue;
107
+ }
108
+ if (!/\.(ts|tsx|js|mjs|json)$/.test(entry.name))
109
+ continue;
110
+ let content;
111
+ try {
112
+ content = fs.readFileSync(full, "utf8");
113
+ }
114
+ catch {
115
+ continue;
116
+ }
117
+ const matches = content.match(ref);
118
+ if (matches && matches.length > 0) {
119
+ hits.push({ file: path.relative(projectRoot, full), count: matches.length });
120
+ }
121
+ }
122
+ };
123
+ walk(projectRoot);
124
+ return hits.sort((a, b) => a.file.localeCompare(b.file));
125
+ }
126
+ export function listUserNodes(projectRoot, kind) {
127
+ const dir = path.join(projectRoot, "runtimes", kind, "nodes");
128
+ if (!fs.existsSync(dir))
129
+ return { dir, isSymlink: false, files: [] };
130
+ const stat = fs.lstatSync(dir);
131
+ if (stat.isSymbolicLink())
132
+ return { dir, isSymlink: true, files: [] };
133
+ const files = [];
134
+ const walk = (d) => {
135
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
136
+ if (entry.isSymbolicLink())
137
+ continue;
138
+ const full = path.join(d, entry.name);
139
+ if (entry.isDirectory())
140
+ walk(full);
141
+ else
142
+ files.push(path.relative(dir, full));
143
+ }
144
+ };
145
+ walk(dir);
146
+ return { dir, isSymlink: false, files: files.sort() };
147
+ }
148
+ export function reportRuntimeError(err) {
149
+ if (err instanceof RuntimeCommandError) {
150
+ p.cancel(err.message);
151
+ }
152
+ else {
153
+ p.cancel(color.red(`Unexpected error: ${err?.message ?? String(err)}`));
154
+ }
155
+ process.exitCode = 1;
156
+ }
157
+ export function assertSidecarKind(kind) {
158
+ if (NON_SIDECAR_KINDS.has(kind)) {
159
+ throw new RuntimeCommandError(`"${kind}" runs in-process and is always available — there's no sidecar to add or remove. Sidecar runtimes: go, rust, java, csharp, php, ruby, python3.`);
160
+ }
161
+ if (!getRuntimeDefinition(kind)) {
162
+ throw new RuntimeCommandError(`Unknown runtime "${kind}". Supported: go, rust, java, csharp, php, ruby, python3.`);
163
+ }
164
+ }
package/dist/index.d.ts CHANGED
@@ -12,6 +12,7 @@ import "./commands/gen/index.js";
12
12
  import "./commands/nodes/index.js";
13
13
  import "./commands/config/index.js";
14
14
  import "./commands/migrate/index.js";
15
+ import "./commands/runtime/index.js";
15
16
  import "./commands/graph/index.js";
16
17
  import "./commands/profile/index.js";
17
18
  import "./commands/cost/index.js";
package/dist/index.js CHANGED
@@ -27,6 +27,7 @@ import "./commands/gen/index.js";
27
27
  import "./commands/nodes/index.js";
28
28
  import "./commands/config/index.js";
29
29
  import "./commands/migrate/index.js";
30
+ import "./commands/runtime/index.js";
30
31
  import "./commands/graph/index.js";
31
32
  import "./commands/profile/index.js";
32
33
  import "./commands/cost/index.js";
@@ -0,0 +1,7 @@
1
+ import { type ProjectConfig, type RuntimeConfig } from "./runtime-setup.js";
2
+ export declare function runtimeEnvKey(kind: string): string;
3
+ export declare function withRuntime(config: ProjectConfig, rc: RuntimeConfig): ProjectConfig;
4
+ export declare function withoutRuntime(config: ProjectConfig, kind: string): ProjectConfig;
5
+ export declare function rewriteRuntimeEnvBlock(envContent: string, runtimes: RuntimeConfig[]): string;
6
+ export declare function rewriteSupervisordRuntimes(supervisordContent: string, runtimes: RuntimeConfig[]): string;
7
+ export declare function ensureRuntimeGitignore(gitignoreContent: string): string;