@take-out/scripts 0.1.36 → 0.1.38-1772433507984

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/bin/run-group ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@take-out/scripts",
3
- "version": "0.1.36",
3
+ "version": "0.1.38-1772433507984",
4
4
  "type": "module",
5
5
  "main": "./src/run.ts",
6
6
  "sideEffects": false,
@@ -20,6 +20,7 @@
20
20
  }
21
21
  },
22
22
  "scripts": {
23
+ "postinstall": "cd src/run-group && (clang -O2 -o run-group run-group.c 2>/dev/null || gcc -O2 -o run-group run-group.c 2>/dev/null || true)",
23
24
  "lint": "oxlint src",
24
25
  "lint:fix": "oxfmt src && oxlint --fix --fix-suggestions src",
25
26
  "typecheck": "tko run typecheck"
@@ -30,7 +31,7 @@
30
31
  "dependencies": {
31
32
  "@clack/prompts": "^0.8.2",
32
33
  "@lydell/node-pty": "^1.2.0-beta.3",
33
- "@take-out/helpers": "0.1.36",
34
+ "@take-out/helpers": "0.1.38-1772433507984",
34
35
  "picocolors": "^1.1.1"
35
36
  },
36
37
  "peerDependencies": {
@@ -1,8 +1,39 @@
1
1
  import { spawn, type ChildProcess } from 'node:child_process'
2
+ import { existsSync } from 'node:fs'
2
3
  import { cpus } from 'node:os'
4
+ import { dirname, resolve } from 'node:path'
5
+ import { fileURLToPath } from 'node:url'
3
6
 
4
7
  import type { Timer } from '@take-out/helpers'
5
8
 
9
+ // find run-group binary for proper process group management
10
+ let _runGroupBin: string | null | undefined
11
+ function findRunGroupBinary(): string | null {
12
+ if (_runGroupBin !== undefined) return _runGroupBin
13
+
14
+ // check relative to this file (in packages/scripts/bin/)
15
+ try {
16
+ const __dirname = dirname(fileURLToPath(import.meta.url))
17
+ const localBin = resolve(__dirname, '..', 'bin', 'run-group')
18
+ if (existsSync(localBin)) {
19
+ _runGroupBin = localBin
20
+ return _runGroupBin
21
+ }
22
+ } catch {}
23
+
24
+ // check in node_modules
25
+ try {
26
+ const nmBin = resolve(process.cwd(), 'node_modules', '@take-out', 'scripts', 'bin', 'run-group')
27
+ if (existsSync(nmBin)) {
28
+ _runGroupBin = nmBin
29
+ return _runGroupBin
30
+ }
31
+ } catch {}
32
+
33
+ _runGroupBin = null
34
+ return null
35
+ }
36
+
6
37
  export type ProcessType = ChildProcess
7
38
  export type ProcessHandler = (process: ProcessType) => void
8
39
 
@@ -19,6 +50,39 @@ export function getIsExiting() {
19
50
 
20
51
  const processHandlers = new Set<ProcessHandler>()
21
52
 
53
+ // track all spawned child processes for cleanup on exit
54
+ const activeProcesses = new Set<ChildProcess>()
55
+
56
+ // install cleanup handlers once
57
+ let cleanupInstalled = false
58
+ function installCleanupHandlers() {
59
+ if (cleanupInstalled) return
60
+ cleanupInstalled = true
61
+
62
+ const cleanup = () => {
63
+ for (const proc of activeProcesses) {
64
+ try {
65
+ // kill the process group to ensure child processes are also killed
66
+ if (proc.pid) {
67
+ process.kill(-proc.pid, 'SIGTERM')
68
+ }
69
+ } catch {
70
+ // process may already be dead
71
+ }
72
+ }
73
+ }
74
+
75
+ process.on('exit', cleanup)
76
+ process.on('SIGINT', () => {
77
+ cleanup()
78
+ process.exit(130)
79
+ })
80
+ process.on('SIGTERM', () => {
81
+ cleanup()
82
+ process.exit(143)
83
+ })
84
+ }
85
+
22
86
  const colors = [
23
87
  '\x1b[36m', // cyan
24
88
  '\x1b[35m', // magenta
@@ -93,13 +157,22 @@ export async function run(
93
157
  let timeoutId: Timer | undefined
94
158
  let didTimeOut = false
95
159
 
160
+ // find run-group binary (handles process group cleanup properly)
161
+ const runGroupBin = findRunGroupBinary()
162
+
96
163
  try {
97
- const shell = spawn('bash', ['-c', command], {
98
- env: { ...process.env, ...env },
99
- cwd,
100
- stdio: ['ignore', 'pipe', 'pipe'],
101
- detached: detached ?? false,
102
- })
164
+ // use run-group to wrap the command - it handles process groups and cleanup
165
+ const shell = runGroupBin
166
+ ? spawn(runGroupBin, ['bash', '-c', command], {
167
+ env: { ...process.env, ...env },
168
+ cwd,
169
+ stdio: ['ignore', 'pipe', 'pipe'],
170
+ })
171
+ : spawn('bash', ['-c', command], {
172
+ env: { ...process.env, ...env },
173
+ cwd,
174
+ stdio: ['ignore', 'pipe', 'pipe'],
175
+ })
103
176
 
104
177
  if (detached) {
105
178
  shell.unref()
@@ -0,0 +1,24 @@
1
+ const std = @import("std");
2
+
3
+ pub fn build(b: *std.Build) void {
4
+ const target = b.standardTargetOptions(.{});
5
+ const optimize = b.standardOptimizeOption(.{});
6
+
7
+ const exe = b.addExecutable(.{
8
+ .name = "run-group",
9
+ .root_source_file = b.path("main.zig"),
10
+ .target = target,
11
+ .optimize = optimize,
12
+ });
13
+
14
+ b.installArtifact(exe);
15
+
16
+ const run_cmd = b.addRunArtifact(exe);
17
+ run_cmd.step.dependOn(b.getInstallStep());
18
+ if (b.args) |args| {
19
+ run_cmd.addArgs(args);
20
+ }
21
+
22
+ const run_step = b.step("run", "Run the app");
23
+ run_step.dependOn(&run_cmd.step);
24
+ }
@@ -0,0 +1,327 @@
1
+ const std = @import("std");
2
+ const posix = std.posix;
3
+ const Allocator = std.mem.Allocator;
4
+
5
+ // run-group: run commands with proper process group management
6
+ //
7
+ // usage:
8
+ // run-group <command> [args...] # single command
9
+ // run-group -p <cmd1> --- <cmd2> --- ... # parallel commands
10
+ //
11
+ // flags:
12
+ // -p, --parallel run commands in parallel (separated by ---)
13
+ // -k, --keep-going don't kill others on failure
14
+ // -q, --quiet suppress output
15
+ // -t, --timing show timing info
16
+ // --prefix <name> prefix output with [name]
17
+
18
+ const colors = [_][]const u8{
19
+ "\x1b[36m", // cyan
20
+ "\x1b[35m", // magenta
21
+ "\x1b[32m", // green
22
+ "\x1b[33m", // yellow
23
+ "\x1b[34m", // blue
24
+ "\x1b[31m", // red
25
+ };
26
+ const reset = "\x1b[0m";
27
+ const bold = "\x1b[1m";
28
+ const dim = "\x1b[2m";
29
+
30
+ const Child = struct {
31
+ pid: posix.pid_t,
32
+ name: []const u8,
33
+ color_idx: usize,
34
+ start_time: i64,
35
+ };
36
+
37
+ var children: std.ArrayList(Child) = undefined;
38
+ var keep_going = false;
39
+ var quiet = false;
40
+ var show_timing = false;
41
+ var any_failed = false;
42
+ var shutting_down = false;
43
+
44
+ fn killAllChildren() void {
45
+ for (children.items) |child| {
46
+ // kill process group
47
+ posix.kill(-child.pid, posix.SIG.TERM) catch {};
48
+ }
49
+ // brief wait then force kill
50
+ std.time.sleep(50 * std.time.ns_per_ms);
51
+ for (children.items) |child| {
52
+ posix.kill(-child.pid, posix.SIG.KILL) catch {};
53
+ }
54
+ }
55
+
56
+ fn signalHandler(sig: c_int) callconv(.C) void {
57
+ shutting_down = true;
58
+ killAllChildren();
59
+ // re-raise to get proper exit
60
+ const default_action = posix.Sigaction{
61
+ .handler = .{ .handler = posix.SIG.DFL },
62
+ .mask = posix.empty_sigset,
63
+ .flags = 0,
64
+ };
65
+ posix.sigaction(@intCast(sig), &default_action, null) catch {};
66
+ _ = posix.raise(@intCast(sig)) catch {};
67
+ }
68
+
69
+ fn getColor(idx: usize) []const u8 {
70
+ return colors[idx % colors.len];
71
+ }
72
+
73
+ fn formatDuration(ms: i64) [32]u8 {
74
+ var buf: [32]u8 = undefined;
75
+ const secs = @divFloor(ms, 1000);
76
+ const mins = @divFloor(secs, 60);
77
+ const rem_secs = @rem(secs, 60);
78
+
79
+ if (mins > 0) {
80
+ _ = std.fmt.bufPrint(&buf, "{}m {}s", .{ mins, rem_secs }) catch "?";
81
+ } else if (secs > 0) {
82
+ _ = std.fmt.bufPrint(&buf, "{}s", .{secs}) catch "?";
83
+ } else {
84
+ _ = std.fmt.bufPrint(&buf, "{}ms", .{ms}) catch "?";
85
+ }
86
+ return buf;
87
+ }
88
+
89
+ fn runSingle(allocator: Allocator, argv: []const []const u8, prefix: ?[]const u8, color_idx: usize) !u8 {
90
+ const start = std.time.milliTimestamp();
91
+
92
+ const pid = try posix.fork();
93
+
94
+ if (pid == 0) {
95
+ // child: create new process group
96
+ _ = posix.setpgid(0, 0) catch {};
97
+
98
+ // exec
99
+ const argv_z = try allocator.allocSentinel(?[*:0]const u8, argv.len, null);
100
+ for (argv, 0..) |arg, i| {
101
+ argv_z[i] = try allocator.dupeZ(u8, arg);
102
+ }
103
+
104
+ const env = @as([*:null]const ?[*:0]const u8, @ptrCast(std.c.environ));
105
+ const err = posix.execvpeZ(argv_z[0].?, argv_z, env);
106
+ _ = err;
107
+ std.posix.exit(127);
108
+ }
109
+
110
+ // parent: set pgid from parent side too
111
+ _ = posix.setpgid(pid, pid) catch {};
112
+
113
+ // wait for child
114
+ const result = posix.waitpid(pid, 0);
115
+
116
+ // cleanup any stragglers in the group
117
+ posix.kill(-pid, posix.SIG.TERM) catch {};
118
+
119
+ const duration = std.time.milliTimestamp() - start;
120
+ const status = result.status;
121
+
122
+ const exit_code: u8 = if (posix.W.IFEXITED(status))
123
+ posix.W.EXITSTATUS(status)
124
+ else if (posix.W.IFSIGNALED(status))
125
+ 128 + @as(u8, @intCast(posix.W.TERMSIG(status)))
126
+ else
127
+ 1;
128
+
129
+ if (show_timing and prefix != null) {
130
+ _ = color_idx;
131
+ const dur_buf = formatDuration(duration);
132
+ const dur_str = std.mem.sliceTo(&dur_buf, 0);
133
+ const stdout = std.io.getStdOut().writer();
134
+ if (exit_code == 0) {
135
+ stdout.print("{s}✓{s} {s}{s}{s} completed in {s}{s}{s}\n", .{
136
+ "\x1b[32m", reset, bold, prefix.?, reset, "\x1b[33m", dur_str, reset,
137
+ }) catch {};
138
+ } else {
139
+ stdout.print("{s}✗{s} {s}{s}{s} failed after {s}{s}{s}\n", .{
140
+ "\x1b[31m", reset, bold, prefix.?, reset, "\x1b[33m", dur_str, reset,
141
+ }) catch {};
142
+ }
143
+ }
144
+
145
+ return exit_code;
146
+ }
147
+
148
+ fn spawnChild(allocator: Allocator, argv: []const []const u8, name: []const u8, color_idx: usize) !void {
149
+ const start = std.time.milliTimestamp();
150
+ const pid = try posix.fork();
151
+
152
+ if (pid == 0) {
153
+ // child
154
+ _ = posix.setpgid(0, 0) catch {};
155
+
156
+ const argv_z = try allocator.allocSentinel(?[*:0]const u8, argv.len, null);
157
+ for (argv, 0..) |arg, i| {
158
+ argv_z[i] = try allocator.dupeZ(u8, arg);
159
+ }
160
+
161
+ const env = @as([*:null]const ?[*:0]const u8, @ptrCast(std.c.environ));
162
+ const err = posix.execvpeZ(argv_z[0].?, argv_z, env);
163
+ _ = err;
164
+ std.posix.exit(127);
165
+ }
166
+
167
+ // parent
168
+ _ = posix.setpgid(pid, pid) catch {};
169
+
170
+ try children.append(.{
171
+ .pid = pid,
172
+ .name = name,
173
+ .color_idx = color_idx,
174
+ .start_time = start,
175
+ });
176
+ }
177
+
178
+ fn waitForChildren() u8 {
179
+ var max_exit: u8 = 0;
180
+
181
+ while (children.items.len > 0) {
182
+ const result = posix.waitpid(-1, 0);
183
+ const pid = result.pid;
184
+
185
+ // find and remove child
186
+ for (children.items, 0..) |child, i| {
187
+ if (child.pid == pid) {
188
+ const duration = std.time.milliTimestamp() - child.start_time;
189
+ const status = result.status;
190
+
191
+ const exit_code: u8 = if (posix.W.IFEXITED(status))
192
+ posix.W.EXITSTATUS(status)
193
+ else if (posix.W.IFSIGNALED(status))
194
+ 128 + @as(u8, @intCast(posix.W.TERMSIG(status)))
195
+ else
196
+ 1;
197
+
198
+ if (exit_code > max_exit) max_exit = exit_code;
199
+
200
+ if (show_timing) {
201
+ const stdout = std.io.getStdOut().writer();
202
+ const dur_buf = formatDuration(duration);
203
+ const dur_str = std.mem.sliceTo(&dur_buf, 0);
204
+ if (exit_code == 0) {
205
+ stdout.print("{s}✓{s} {s}{s}{s} completed in {s}{s}{s}\n", .{
206
+ "\x1b[32m", reset, bold, child.name, reset, "\x1b[33m", dur_str, reset,
207
+ }) catch {};
208
+ } else {
209
+ stdout.print("{s}✗{s} {s}{s}{s} failed after {s}{s}{s}\n", .{
210
+ "\x1b[31m", reset, bold, child.name, reset, "\x1b[33m", dur_str, reset,
211
+ }) catch {};
212
+ any_failed = true;
213
+ if (!keep_going and !shutting_down) {
214
+ shutting_down = true;
215
+ killAllChildren();
216
+ }
217
+ }
218
+ }
219
+
220
+ // cleanup process group
221
+ posix.kill(-pid, posix.SIG.TERM) catch {};
222
+
223
+ _ = children.swapRemove(i);
224
+ break;
225
+ }
226
+ }
227
+ }
228
+
229
+ return max_exit;
230
+ }
231
+
232
+ pub fn main() !u8 {
233
+ var gpa = std.heap.GeneralPurposeAllocator(.{}){};
234
+ const allocator = gpa.allocator();
235
+
236
+ children = .{};
237
+ defer children.deinit(allocator);
238
+
239
+ const args = try std.process.argsAlloc(allocator);
240
+ defer std.process.argsFree(allocator, args);
241
+
242
+ if (args.len < 2) {
243
+ const stderr = std.io.getStdErr().writer();
244
+ try stderr.print(
245
+ \\usage: run-group [flags] <command> [args...]
246
+ \\ run-group -p <cmd1> --- <cmd2> --- ...
247
+ \\
248
+ \\flags:
249
+ \\ -p, --parallel run commands in parallel (separated by ---)
250
+ \\ -k, --keep-going don't kill others on failure
251
+ \\ -q, --quiet suppress output
252
+ \\ -t, --timing show timing info
253
+ \\
254
+ , .{});
255
+ return 1;
256
+ }
257
+
258
+ // set up signal handlers
259
+ const handler = posix.Sigaction{
260
+ .handler = .{ .handler = signalHandler },
261
+ .mask = posix.empty_sigset,
262
+ .flags = 0,
263
+ };
264
+ try posix.sigaction(posix.SIG.INT, &handler, null);
265
+ try posix.sigaction(posix.SIG.TERM, &handler, null);
266
+ try posix.sigaction(posix.SIG.HUP, &handler, null);
267
+
268
+ // parse flags
269
+ var parallel = false;
270
+ var cmd_start: usize = 1;
271
+
272
+ for (args[1..], 1..) |arg, i| {
273
+ if (std.mem.eql(u8, arg, "-p") or std.mem.eql(u8, arg, "--parallel")) {
274
+ parallel = true;
275
+ cmd_start = i + 1;
276
+ } else if (std.mem.eql(u8, arg, "-k") or std.mem.eql(u8, arg, "--keep-going")) {
277
+ keep_going = true;
278
+ cmd_start = i + 1;
279
+ } else if (std.mem.eql(u8, arg, "-q") or std.mem.eql(u8, arg, "--quiet")) {
280
+ quiet = true;
281
+ cmd_start = i + 1;
282
+ } else if (std.mem.eql(u8, arg, "-t") or std.mem.eql(u8, arg, "--timing")) {
283
+ show_timing = true;
284
+ cmd_start = i + 1;
285
+ } else if (arg[0] != '-') {
286
+ break;
287
+ }
288
+ }
289
+
290
+ if (cmd_start >= args.len) {
291
+ const stderr = std.io.getStdErr().writer();
292
+ try stderr.print("error: no command specified\n", .{});
293
+ return 1;
294
+ }
295
+
296
+ if (parallel) {
297
+ // split on --- and run in parallel
298
+ var cmds = std.ArrayList([]const []const u8).init(allocator);
299
+ var current_cmd = std.ArrayList([]const u8).init(allocator);
300
+
301
+ for (args[cmd_start..]) |arg| {
302
+ if (std.mem.eql(u8, arg, "---")) {
303
+ if (current_cmd.items.len > 0) {
304
+ try cmds.append(try current_cmd.toOwnedSlice());
305
+ current_cmd = std.ArrayList([]const u8).init(allocator);
306
+ }
307
+ } else {
308
+ try current_cmd.append(arg);
309
+ }
310
+ }
311
+ if (current_cmd.items.len > 0) {
312
+ try cmds.append(try current_cmd.toOwnedSlice());
313
+ }
314
+
315
+ // spawn all
316
+ for (cmds.items, 0..) |cmd, i| {
317
+ const name = cmd[0];
318
+ try spawnChild(allocator, cmd, name, i);
319
+ }
320
+
321
+ return waitForChildren();
322
+ } else {
323
+ // single command
324
+ const cmd = args[cmd_start..];
325
+ return try runSingle(allocator, cmd, cmd[0], 0);
326
+ }
327
+ }
Binary file