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.
- package/dist/commands/create/project.js +52 -13
- package/dist/commands/create/utils/Examples.d.ts +3 -3
- package/dist/commands/create/utils/Examples.js +987 -328
- package/dist/commands/gen/appTypes.js +40 -1
- package/dist/commands/runtime/add.d.ts +2 -0
- package/dist/commands/runtime/add.js +143 -0
- package/dist/commands/runtime/index.d.ts +1 -0
- package/dist/commands/runtime/index.js +43 -0
- package/dist/commands/runtime/list.d.ts +2 -0
- package/dist/commands/runtime/list.js +60 -0
- package/dist/commands/runtime/remove.d.ts +2 -0
- package/dist/commands/runtime/remove.js +114 -0
- package/dist/commands/runtime/shared.d.ts +22 -0
- package/dist/commands/runtime/shared.js +164 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/services/runtime-mutations.d.ts +7 -0
- package/dist/services/runtime-mutations.js +67 -0
- package/package.json +2 -2
- package/dist/commands/marketplace/runtime.d.ts +0 -54
- package/dist/commands/marketplace/runtime.js +0 -350
|
@@ -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
|
-
|
|
232
|
+
warnJsonSkipped();
|
|
233
|
+
console.log(color.dim('Next: `import type { BlokApp } from "<out>"` and `createBlokClient<BlokApp>({ baseUrl })`.\n'));
|
|
195
234
|
}
|
|
@@ -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,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,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;
|