blokctl 0.6.19 → 0.6.20

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.
@@ -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;
@@ -0,0 +1,67 @@
1
+ import { generateRuntimeEnvVars, generateSupervisordConfig, } from "./runtime-setup.js";
2
+ const RUNTIME_ENV_HEADER = "# Runtimes (auto-configured by blokctl)";
3
+ export function runtimeEnvKey(kind) {
4
+ return kind === "csharp" ? "CSHARP" : kind.toUpperCase();
5
+ }
6
+ export function withRuntime(config, rc) {
7
+ return { ...config, runtimes: { ...(config.runtimes ?? {}), [rc.kind]: rc } };
8
+ }
9
+ export function withoutRuntime(config, kind) {
10
+ if (!config.runtimes || !(kind in config.runtimes))
11
+ return config;
12
+ const runtimes = Object.fromEntries(Object.entries(config.runtimes).filter(([k]) => k !== kind));
13
+ return { ...config, runtimes: Object.keys(runtimes).length === 0 ? undefined : runtimes };
14
+ }
15
+ export function rewriteRuntimeEnvBlock(envContent, runtimes) {
16
+ const isManaged = (line) => {
17
+ const t = line.trimStart();
18
+ return (line.trim() === RUNTIME_ENV_HEADER ||
19
+ /^RUNTIME_[A-Z0-9]+_(HOST|PORT|GRPC_PORT)=/.test(t) ||
20
+ /^BLOK_TRANSPORT=/.test(t));
21
+ };
22
+ const kept = envContent
23
+ .split("\n")
24
+ .filter((line) => !isManaged(line))
25
+ .join("\n")
26
+ .replace(/\n{3,}/g, "\n\n")
27
+ .replace(/\n+$/, "");
28
+ const block = generateRuntimeEnvVars(runtimes);
29
+ if (!block)
30
+ return kept.length > 0 ? `${kept}\n` : "";
31
+ return `${kept}\n${block}\n`;
32
+ }
33
+ export function rewriteSupervisordRuntimes(supervisordContent, runtimes) {
34
+ const out = [];
35
+ let skipping = false;
36
+ for (const line of supervisordContent.split("\n")) {
37
+ const t = line.trimStart();
38
+ if (/^\[program:[\w-]+_runtime\]/.test(t)) {
39
+ skipping = true;
40
+ continue;
41
+ }
42
+ if (/^\[/.test(t))
43
+ skipping = false;
44
+ if (!skipping)
45
+ out.push(line);
46
+ }
47
+ const kept = out.join("\n").replace(/\n+$/, "");
48
+ const block = generateSupervisordConfig(runtimes);
49
+ return block ? `${kept}\n${block}\n` : `${kept}\n`;
50
+ }
51
+ const RUNTIME_GITIGNORE_GLOBS = [
52
+ ".blok/runtimes/**/bin/",
53
+ ".blok/runtimes/**/obj/",
54
+ ".blok/runtimes/**/target/",
55
+ ".blok/runtimes/**/__pycache__/",
56
+ ".blok/runtimes/**/python3_runtime/",
57
+ ".blok/runtimes/**/vendor/",
58
+ ];
59
+ export function ensureRuntimeGitignore(gitignoreContent) {
60
+ if (/^\.blok\/\s*$/m.test(gitignoreContent))
61
+ return gitignoreContent;
62
+ const missing = RUNTIME_GITIGNORE_GLOBS.filter((glob) => !gitignoreContent.includes(glob));
63
+ if (missing.length === 0)
64
+ return gitignoreContent;
65
+ const base = gitignoreContent.replace(/\n+$/, "");
66
+ return `${base}\n\n# Blok runtime build artifacts (managed by blokctl)\n${missing.join("\n")}\n`;
67
+ }