@forklaunch/bunrun 0.0.1

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 forklaunch
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @forklaunch/bunrun
2
+
3
+ A Bun-native TypeScript workspace script runner with topological ordering.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g @forklaunch/bunrun
9
+ # or
10
+ bun add -g @forklaunch/bunrun
11
+ ```
12
+
13
+ **Requirements:** Bun >= 1.1.0
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ # Run build script in topological order
19
+ bunrun build
20
+
21
+ # Run with specific script
22
+ bunrun test
23
+
24
+ # Run with concurrency limit
25
+ bunrun build --jobs 4
26
+
27
+ # Run sequentially (one at a time)
28
+ bunrun build --sequential
29
+
30
+ # Filter packages
31
+ bunrun build --filter "@myorg/*"
32
+ bunrun build --exclude "**/legacy-*"
33
+
34
+ # Debug mode
35
+ bunrun build --debug
36
+
37
+ # Print plan without executing
38
+ bunrun build --print-only
39
+ ```
40
+
41
+ ## Features
42
+
43
+ - **Topological ordering**: Runs scripts in dependency order
44
+ - **Parallel execution**: Runs independent packages concurrently
45
+ - **Workspace discovery**: Automatically finds packages in monorepos
46
+ - **Flexible filtering**: Include/exclude packages by name or path
47
+ - **Dependency-aware**: Considers dependencies, devDependencies, and peerDependencies
48
+ - **Bun-native**: Direct TypeScript execution without compilation
49
+
50
+ ## Options
51
+
52
+ - `script` - Script to run (default: build)
53
+ - `-j, --jobs <n>` - Concurrency per tier (default: CPU count)
54
+ - `--sequential` - Run strictly one-by-one in topological order
55
+ - `--filter <glob>` - Include packages matching glob pattern (can repeat)
56
+ - `--exclude <glob>` - Exclude packages matching glob pattern (can repeat)
57
+ - `--no-dev` - Ignore devDependencies for ordering
58
+ - `--no-peer` - Ignore peerDependencies for ordering
59
+ - `--print-only` - Print execution plan without running
60
+ - `--debug` - Show diagnostic information
61
+
62
+ ## Examples
63
+
64
+ ```bash
65
+ # Build all packages in dependency order
66
+ bunrun build
67
+
68
+ # Run tests with limited concurrency
69
+ bunrun test --jobs 2
70
+
71
+ # Build only frontend packages
72
+ bunrun build --filter "**/frontend-*"
73
+
74
+ # Build everything except legacy packages
75
+ bunrun build --exclude "**/legacy-*"
76
+
77
+ # Sequential build for debugging
78
+ bunrun build --sequential --debug
79
+ ```
80
+
81
+ ## License
82
+
83
+ MIT
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+
2
+ export { }
package/dist/cli.js ADDED
@@ -0,0 +1,378 @@
1
+ #!/usr/bin/env bun
2
+
3
+ // src/index.ts
4
+ import { readFileSync, readdirSync, statSync } from "fs";
5
+ import { availableParallelism, cpus } from "os";
6
+ import { join, relative, resolve, sep } from "path";
7
+ var cwd = process.cwd();
8
+ function detectCPUCount() {
9
+ try {
10
+ return Math.max(1, availableParallelism());
11
+ } catch {
12
+ try {
13
+ return Math.max(1, cpus()?.length ?? 4);
14
+ } catch {
15
+ return 4;
16
+ }
17
+ }
18
+ }
19
+ function parseArgs() {
20
+ const args = process.argv.slice(2);
21
+ const opts = {
22
+ script: "build",
23
+ jobs: detectCPUCount(),
24
+ includeDev: true,
25
+ includePeer: true,
26
+ only: [],
27
+ exclude: [],
28
+ printOnly: false,
29
+ sequential: false,
30
+ debug: false
31
+ };
32
+ let startIndex = 0;
33
+ if (args.length > 0 && args[0] && !args[0].startsWith("-")) {
34
+ opts.script = args[0];
35
+ startIndex = 1;
36
+ }
37
+ for (let i = startIndex; i < args.length; i++) {
38
+ const a = args[i];
39
+ if (!a) continue;
40
+ if (a === "-j" || a === "--jobs") {
41
+ const nextArg = args[++i];
42
+ if (!nextArg) {
43
+ console.error(`Missing value for ${a}`);
44
+ process.exit(1);
45
+ }
46
+ const jobs = Number(nextArg);
47
+ if (isNaN(jobs) || jobs < 1) {
48
+ console.error(`Invalid jobs value: ${nextArg}`);
49
+ process.exit(1);
50
+ }
51
+ opts.jobs = jobs;
52
+ } else if (a === "--no-dev") {
53
+ opts.includeDev = false;
54
+ } else if (a === "--no-peer") {
55
+ opts.includePeer = false;
56
+ } else if (a === "--filter" || a === "--only") {
57
+ const nextArg = args[++i];
58
+ if (!nextArg) {
59
+ console.error(`Missing value for ${a}`);
60
+ process.exit(1);
61
+ }
62
+ opts.only.push(nextArg);
63
+ } else if (a === "--exclude") {
64
+ const nextArg = args[++i];
65
+ if (!nextArg) {
66
+ console.error(`Missing value for ${a}`);
67
+ process.exit(1);
68
+ }
69
+ opts.exclude.push(nextArg);
70
+ } else if (a === "--print-only") {
71
+ opts.printOnly = true;
72
+ } else if (a === "--sequential") {
73
+ opts.sequential = true;
74
+ } else if (a === "--debug") {
75
+ opts.debug = true;
76
+ } else {
77
+ console.error(`Unknown arg: ${a}`);
78
+ process.exit(1);
79
+ }
80
+ }
81
+ return opts;
82
+ }
83
+ function readJSON(p) {
84
+ try {
85
+ const content = readFileSync(p, "utf8");
86
+ const parsed = JSON.parse(content);
87
+ if (typeof parsed !== "object" || parsed === null) {
88
+ throw new Error(`Invalid JSON structure in ${p}`);
89
+ }
90
+ const pkg = parsed;
91
+ if (typeof pkg.name !== "string") {
92
+ throw new Error(`Missing or invalid name field in ${p}`);
93
+ }
94
+ return pkg;
95
+ } catch (error) {
96
+ if (error instanceof Error) {
97
+ throw new Error(`Failed to read package.json at ${p}: ${error.message}`);
98
+ }
99
+ throw error;
100
+ }
101
+ }
102
+ function isDir(p) {
103
+ try {
104
+ return statSync(p).isDirectory();
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+ function safeReadDir(d) {
110
+ try {
111
+ return readdirSync(d);
112
+ } catch {
113
+ return [];
114
+ }
115
+ }
116
+ function matchGlob(s, glob) {
117
+ const esc = glob.replace(/[-/\\^$+?.()|[\]{}]/g, "\\$&").replace(/\*\*/g, ".*").replace(/\*/g, "[^/]*");
118
+ const re = new RegExp("^" + esc + "$");
119
+ return re.test(s);
120
+ }
121
+ function expandGlob(root, pattern) {
122
+ const abs = resolve(root, pattern);
123
+ const parts = relative(root, abs).split(sep);
124
+ const out = /* @__PURE__ */ new Set();
125
+ function walk(dir, i) {
126
+ if (i === parts.length) {
127
+ out.add(dir);
128
+ return;
129
+ }
130
+ const seg = parts[i];
131
+ if (seg === "**") {
132
+ walk(dir, i + 1);
133
+ for (const e of safeReadDir(dir)) {
134
+ const p = join(dir, e);
135
+ if (isDir(p)) walk(p, i);
136
+ }
137
+ } else if (seg === "*") {
138
+ for (const e of safeReadDir(dir)) {
139
+ const p = join(dir, e);
140
+ if (isDir(p)) walk(p, i + 1);
141
+ }
142
+ } else {
143
+ const next = join(dir, seg);
144
+ if (isDir(next)) walk(next, i + 1);
145
+ }
146
+ }
147
+ walk(root, 0);
148
+ return [...out];
149
+ }
150
+ function getWorkspaceDirs(rootPkg) {
151
+ const ws = Array.isArray(rootPkg.workspaces) ? rootPkg.workspaces : rootPkg.workspaces?.packages || [];
152
+ const dirs = [];
153
+ for (const patt of ws) {
154
+ if (typeof patt !== "string") {
155
+ console.warn(`Skipping invalid workspace pattern: ${patt}`);
156
+ continue;
157
+ }
158
+ for (const d of expandGlob(cwd, patt)) {
159
+ try {
160
+ readJSON(join(d, "package.json"));
161
+ dirs.push(d);
162
+ } catch {
163
+ }
164
+ }
165
+ }
166
+ return [...new Set(dirs)];
167
+ }
168
+ function collectDeps(pkgJson, includeDev, includePeer) {
169
+ return {
170
+ ...pkgJson.dependencies || {},
171
+ ...pkgJson.optionalDependencies || {},
172
+ ...includeDev ? pkgJson.devDependencies || {} : {},
173
+ ...includePeer ? pkgJson.peerDependencies || {} : {}
174
+ };
175
+ }
176
+ function topoTiers(nodes, edges) {
177
+ const indeg = new Map(nodes.map((n) => [n, 0]));
178
+ for (const [, outs] of edges) {
179
+ for (const m of outs) {
180
+ indeg.set(m, (indeg.get(m) || 0) + 1);
181
+ }
182
+ }
183
+ const remain = new Set(nodes);
184
+ const tiers = [];
185
+ while (remain.size > 0) {
186
+ const zero = [];
187
+ for (const n of remain) {
188
+ if ((indeg.get(n) || 0) === 0) {
189
+ zero.push(n);
190
+ }
191
+ }
192
+ if (zero.length === 0) {
193
+ console.warn(
194
+ "Dependency cycle detected, placing remaining packages in final tier"
195
+ );
196
+ tiers.push([...remain]);
197
+ break;
198
+ }
199
+ tiers.push(zero);
200
+ for (const z of zero) {
201
+ remain.delete(z);
202
+ const outs = edges.get(z);
203
+ if (!outs) continue;
204
+ for (const m of outs) {
205
+ indeg.set(m, Math.max(0, (indeg.get(m) || 0) - 1));
206
+ }
207
+ }
208
+ }
209
+ return tiers;
210
+ }
211
+ async function runSequential(dirsInOrder, script) {
212
+ for (const dir of dirsInOrder) {
213
+ console.log(`\u25B6 ${dir}: bun run ${script}`);
214
+ const p = Bun.spawn(["bun", "run", script], {
215
+ cwd: dir,
216
+ stdout: "inherit",
217
+ stderr: "inherit"
218
+ });
219
+ const code = await p.exited;
220
+ if (code !== 0) {
221
+ throw new Error(`Failed in ${dir} with exit code ${code}`);
222
+ }
223
+ }
224
+ }
225
+ async function runTierParallel(dirs, script, concurrency) {
226
+ if (dirs.length === 0) return;
227
+ let i = 0;
228
+ const errs = [];
229
+ async function worker() {
230
+ while (true) {
231
+ const idx = i++;
232
+ if (idx >= dirs.length) break;
233
+ const dir = dirs[idx];
234
+ if (!dir) continue;
235
+ console.log(`\u25B6 ${dir}: bun run ${script}`);
236
+ const p = Bun.spawn(["bun", "run", script], {
237
+ cwd: dir,
238
+ stdout: "inherit",
239
+ stderr: "inherit"
240
+ });
241
+ const code = await p.exited;
242
+ if (code !== 0) {
243
+ errs.push(new Error(`Failed in ${dir} with exit code ${code}`));
244
+ }
245
+ }
246
+ }
247
+ const n = Math.max(1, Math.min(concurrency, dirs.length));
248
+ await Promise.all(Array.from({ length: n }, () => worker()));
249
+ if (errs.length > 0) {
250
+ throw errs[0];
251
+ }
252
+ }
253
+ async function main() {
254
+ const opts = parseArgs();
255
+ const rootPkg = readJSON(join(cwd, "package.json"));
256
+ const wsDirs = getWorkspaceDirs(rootPkg);
257
+ if (wsDirs.length === 0) {
258
+ console.error("No workspaces found in package.json");
259
+ process.exit(1);
260
+ }
261
+ const pkgs = wsDirs.map((dir) => {
262
+ try {
263
+ const json = readJSON(join(dir, "package.json"));
264
+ return {
265
+ name: json.name,
266
+ dir,
267
+ scripts: json.scripts ?? {},
268
+ json
269
+ };
270
+ } catch (error) {
271
+ console.warn(
272
+ `Failed to load package.json in ${dir}:`,
273
+ error instanceof Error ? error.message : error
274
+ );
275
+ return null;
276
+ }
277
+ }).filter((p) => p !== null && p.name.length > 0);
278
+ const selected = pkgs.filter((p) => {
279
+ const matchOnly = opts.only.length > 0 ? opts.only.some((g) => matchGlob(p.name, g) || matchGlob(p.dir, g)) : true;
280
+ const matchExclude = opts.exclude.length > 0 ? opts.exclude.some((g) => matchGlob(p.name, g) || matchGlob(p.dir, g)) : false;
281
+ return matchOnly && !matchExclude;
282
+ });
283
+ if (selected.length === 0) {
284
+ console.error("No packages matched selection.");
285
+ process.exit(1);
286
+ }
287
+ const names = new Set(selected.map((p) => p.name));
288
+ const edges = /* @__PURE__ */ new Map();
289
+ const ensure = (k) => {
290
+ const existing = edges.get(k);
291
+ if (existing) return existing;
292
+ const newSet = /* @__PURE__ */ new Set();
293
+ edges.set(k, newSet);
294
+ return newSet;
295
+ };
296
+ for (const p of selected) {
297
+ const all = collectDeps(p.json, opts.includeDev, opts.includePeer);
298
+ for (const depName of Object.keys(all)) {
299
+ if (!names.has(depName)) continue;
300
+ ensure(depName).add(p.name);
301
+ }
302
+ }
303
+ const nodes = selected.map((p) => p.name);
304
+ const tiers = topoTiers(nodes, edges);
305
+ const hasScript = (name) => {
306
+ const pkg = selected.find((p) => p.name === name);
307
+ if (!pkg) {
308
+ console.warn(`Package not found: ${name}`);
309
+ return false;
310
+ }
311
+ return Boolean(pkg.scripts[opts.script]);
312
+ };
313
+ if (opts.debug) {
314
+ console.log(`Script: ${opts.script}`);
315
+ console.log(
316
+ `Mode: ${opts.sequential ? "sequential" : `parallel by tier (jobs=${opts.jobs})`}`
317
+ );
318
+ console.log(
319
+ `Edges: deps + optional${opts.includeDev ? " + dev" : ""}${opts.includePeer ? " + peer" : ""}`
320
+ );
321
+ console.log("");
322
+ }
323
+ const tiersDirs = tiers.map(
324
+ (tier) => tier.filter(hasScript).map((name) => {
325
+ const pkg = selected.find((p) => p.name === name);
326
+ if (!pkg) {
327
+ throw new Error(`Package not found: ${name}`);
328
+ }
329
+ return pkg.dir;
330
+ })
331
+ );
332
+ const seqDirs = tiers.flat().filter(hasScript).map((name) => {
333
+ const pkg = selected.find((p) => p.name === name);
334
+ if (!pkg) {
335
+ throw new Error(`Package not found: ${name}`);
336
+ }
337
+ return pkg.dir;
338
+ });
339
+ if (opts.debug) {
340
+ if (opts.sequential) {
341
+ console.log("Plan (sequential order):");
342
+ seqDirs.forEach((d, i) => console.log(` ${i + 1}. ${d}`));
343
+ console.log("");
344
+ console.log("Command preview:");
345
+ console.log(
346
+ seqDirs.map((d) => `(cd ${JSON.stringify(d)} && bun run ${opts.script})`).join(" && ")
347
+ );
348
+ } else {
349
+ console.log("Plan (tiers):");
350
+ tiersDirs.forEach((dirs, i) => {
351
+ if (!dirs.length) return;
352
+ console.log(` Tier ${i + 1}:`);
353
+ dirs.forEach((d) => console.log(` - ${d}`));
354
+ });
355
+ console.log("");
356
+ console.log("Command preview (conceptual):");
357
+ console.log(
358
+ tiersDirs.filter((dirs) => dirs.length).map(
359
+ (dirs) => dirs.map(
360
+ (d) => `(cd ${JSON.stringify(d)} && bun run ${opts.script})`
361
+ ).join(" & ") + " && wait"
362
+ ).join(" && ")
363
+ );
364
+ }
365
+ }
366
+ if (opts.printOnly) return;
367
+ if (opts.sequential) {
368
+ await runSequential(seqDirs, opts.script);
369
+ } else {
370
+ for (const dirs of tiersDirs) {
371
+ if (dirs.length === 0) continue;
372
+ await runTierParallel(dirs, opts.script, opts.jobs);
373
+ }
374
+ }
375
+ }
376
+ main().catch((err) => {
377
+ throw err;
378
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@forklaunch/bunrun",
3
+ "version": "0.0.1",
4
+ "description": "Bun-native TypeScript workspace script runner with topological ordering",
5
+ "homepage": "https://github.com/forklaunch/forklaunch-js#readme",
6
+ "bugs": {
7
+ "url": "https://github.com/forklaunch/forklaunch-js/issues"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/forklaunch/forklaunch-js.git"
12
+ },
13
+ "license": "MIT",
14
+ "author": "Rohin Bhargava",
15
+ "type": "module",
16
+ "bin": {
17
+ "bunrun": "dist/index.mjs"
18
+ },
19
+ "engines": {
20
+ "bun": ">=1.1.0"
21
+ },
22
+ "files": [
23
+ "dist"
24
+ ],
25
+ "keywords": [
26
+ "bun",
27
+ "workspace",
28
+ "monorepo",
29
+ "build",
30
+ "topological",
31
+ "typescript",
32
+ "cli"
33
+ ],
34
+ "devDependencies": {
35
+ "@typescript/native-preview": "7.0.0-dev.20250911.1",
36
+ "tsup": "^8.0.0",
37
+ "typescript": "^5.6.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsgo --noEmit && tsup",
41
+ "check": "depcheck",
42
+ "clean": "rm -rf dist pnpm.lock.yaml node_modules",
43
+ "docs": "typedoc --out docs *",
44
+ "format": "prettier --ignore-path=.prettierignore --config .prettierrc '**/*.{ts,tsx,json}' --write",
45
+ "lint": "eslint . -c eslint.config.mjs",
46
+ "lint:fix": "eslint . -c eslint.config.mjs --fix",
47
+ "publish:package": "./publish-package.bash",
48
+ "test": "vitest --passWithNoTests"
49
+ }
50
+ }