go-dev 0.4.1 → 0.5.0

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.
@@ -1,276 +1,309 @@
1
- const { spawn, spawnSync } = require('child_process');
2
- const prefixLines = require("./prefix-lines");
3
-
4
- class ProcessManager {
5
- constructor() {
6
- this.managedProcesses = new Set();
7
- this.cleanupInProgress = false;
8
- }
9
-
10
- /**
11
- * Runs a command synchronously (blocking).
12
- * Used for pre-commands, status checks, getting container names.
13
- * @param {string} command - The command to execute.
14
- * @param {string[]} args - Arguments for the command.
15
- * @param {object} [options={}] - Options for spawnSync (e.g., cwd, stdio).
16
- * @returns {string} The stdout of the command, trimmed.
17
- * @throws {Error} If the command fails.
18
- */
19
- runSync(command, args = [], options = {}) {
20
- if (this.cleanupInProgress) {
21
- console.warn(`[ProcessManager] Skipping synchronous command '${command}' during cleanup.`);
22
- return '';
23
- }
24
- console.log(`[ProcessManager] Running sync: ${command} ${args.join(' ')}`);
25
- try {
26
- const result = spawnSync(command, args, {
27
- shell: true,
28
- encoding: 'utf8',
29
- ...options,
30
- });
31
-
32
- if (result.error) {
33
- throw result.error;
34
- }
35
- if (result.status !== 0) {
36
- const stderrOutput = result.stderr ? result.stderr.trim() : 'No stderr output.';
37
- throw new Error(
38
- `Command failed with code ${result.status}: ${command} ${args.join(
39
- ' ',
40
- )}\n${stderrOutput}`,
41
- );
42
- }
43
- return result.stdout?.trim() ?? '';
44
- } catch (error) {
45
- throw new Error(`Failed to run sync command '${command}': ${error.message}`);
46
- }
47
- }
48
-
49
- /**
50
- * Runs a command asynchronously, inheriting stdio.
51
- * Used for initial 'docker compose up -d' where we want to see immediate output
52
- * and the process is not meant to be continually managed/restarted by the orchestrator.
53
- * @param {string} command - The command to execute.
54
- * @param {string[]} args - Arguments for the command.
55
- * @param {object} [options={}] - Options for spawn.
56
- * @returns {Promise<void>} A promise that resolves when the process exits successfully.
57
- */
58
- runInherited(command, args = [], options = {}) {
59
- return new Promise((resolve, reject) => {
60
- if (this.cleanupInProgress) {
61
- console.warn(`[ProcessManager] Skipping inherited command '${command}' during cleanup.`);
62
- return resolve();
63
- }
64
- console.log(`[ProcessManager] Running inherited: ${command} ${args.join(' ')}`);
65
- const proc = spawn(command, args, {
66
- shell: true,
67
- stdio: 'inherit',
68
- ...options,
69
- });
70
-
71
- proc.on('error', (err) => {
72
- console.error(`[ProcessManager] Failed to start inherited command '${command}':`, err);
73
- reject(err);
74
- });
75
-
76
- proc.on('exit', (code) => {
77
- if (code !== 0) {
78
- reject(
79
- new Error(`Inherited command '${command}' exited with code ${code}`),
80
- );
81
- } else {
82
- resolve();
83
- }
84
- });
85
- });
86
- }
87
-
88
- /**
89
- * Starts a long-running, managed process (like 'npx rollup -w').
90
- * Its output is prefixed, and it can be configured to restart on exit.
91
- * @param {string} command - The command to execute.
92
- * @param {string[]} args - Arguments for the command.
93
- * @param {object} options - Options for spawn (e.g., cwd).
94
- * @param {string} prefix - Prefix for stdout/stderr lines (e.g., 'frontend:').
95
- * @param {boolean} restartOnError - Whether to restart the process if it exits with non-zero code.
96
- * @param {Function} onExit
97
- * @param {ChildProcess[] | undefined} processReference - An array where to save the process to gain a reference to it.
98
- * @returns {ChildProcess} The spawned child process instance.
99
- */
100
- startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference) {
101
- if (this.cleanupInProgress) {
102
- console.warn(`[ProcessManager] Skipping managed process '${command}' during cleanup.`);
103
- return null;
104
- }
105
- console.log(
106
- `[ProcessManager] Starting managed process: ${command} ${args.join(
107
- ' ',
108
- )} (prefix: ${prefix})`,
109
- );
110
-
111
- if (processReference == null) {
112
- processReference = {}
113
- }
114
-
115
- const startedProcess = spawn(command, args, {
116
- shell: true,
117
- stdio: 'pipe',
118
- ...options,
119
- });
120
- processReference.process = startedProcess;
121
-
122
- let lastFormatting = '';
123
- startedProcess.stdout.on('data', (data) => {
124
- const result = prefixLines(data.toString(), prefix, lastFormatting);
125
- lastFormatting = result.lastFormatting;
126
- // process.stdout.write(JSON.stringify(data.toString()) + "\n");
127
- process.stdout.write(result.prefixedText);
128
- });
129
- startedProcess.stderr.on('data', (data) => {
130
- const result = prefixLines(data.toString(), prefix, lastFormatting);
131
- lastFormatting = result.lastFormatting;
132
- // process.stderr.write(JSON.stringify(data.toString()) + "\n");
133
- process.stderr.write(result.prefixedText);
134
- });
135
-
136
- this.managedProcesses.add(startedProcess);
137
-
138
- startedProcess.on('error', (err) => {
139
- console.error(
140
- `[ProcessManager] Error starting managed process '${command}': ${err.message}`,
141
- );
142
- this.managedProcesses.delete(startedProcess);
143
- });
144
-
145
- startedProcess.on('exit', (code) => {
146
- if (!this.managedProcesses.has(startedProcess)) {
147
- return;
148
- }
149
-
150
- this.managedProcesses.delete(startedProcess);
151
- if (this.cleanupInProgress) {
152
- console.log(
153
- `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited due to cleanup.`,
154
- );
155
- return;
156
- }
157
-
158
- if (code !== 0) {
159
- console.error(
160
- `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited with code ${code}.`,
161
- );
162
- if (restartOnError) {
163
- console.warn(
164
- `[ProcessManager] Restarting managed process '${command}'...`,
165
- );
166
- this.startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference);
167
- } else {
168
- onExit?.();
169
- }
170
- } else {
171
- console.log(
172
- `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited cleanly.`,
173
- );
174
- onExit?.();
175
- }
176
- });
177
-
178
- return processReference;
179
- }
180
-
181
- killProcess(process) {
182
- if (process.killed || process.exitCode != null) {
183
- return;
184
- }
185
-
186
- this.managedProcesses.delete(process);
187
-
188
- return new Promise((resolve, reject) => {
189
- let exited = false;
190
- const onExit = () => {
191
- exited = true;
192
- resolve();
193
- };
194
- process.on('exit', onExit);
195
-
196
- setTimeout(() => {
197
- if (exited) {
198
- return;
199
- }
200
-
201
- console.error(`[ProcessManager] Timeout reached for process interruption ${process.pid}`);
202
- try {
203
- process.kill('SIGKILL');
204
- } catch (e) {
205
- console.error(`[ProcessManager] Error force killing process ${process.pid}: ${e.message}`);
206
- reject(e);
207
- }
208
- }, 500);
209
-
210
- try {
211
- process.kill('SIGTERM');
212
- return;
213
- } catch (e) {
214
- console.error(`[ProcessManager] Error signaling process ${process.pid}: ${e.message}`);
215
- }
216
-
217
- try {
218
- process.kill('SIGKILL');
219
- } catch (e) {
220
- console.error(`[ProcessManager] Error force killing process ${process.pid}: ${e.message}`);
221
- reject(e);
222
- }
223
- });
224
- }
225
-
226
- /**
227
- * Kills all currently managed child processes.
228
- */
229
- async cleanupManagedProcesses() {
230
- if (this.cleanupInProgress) {
231
- console.log('[ProcessManager] Cleanup of managed processes already in progress, skipping.');
232
- return;
233
- }
234
- for (let index = this.managedProcesses.length - 1; index >= 0; index--) {
235
- const process = this.managedProcesses[index];
236
- if (process.killed || process.exitCode != null) {
237
- this.managedProcesses.splice(index, 1);
238
- }
239
- }
240
-
241
- this.cleanupInProgress = true;
242
-
243
- console.log('\n[ProcessManager] Initiating cleanup of managed processes...');
244
-
245
- for (const proc of [...this.managedProcesses]) {
246
- if (proc.killed || proc.exitCode != null) {
247
- continue;
248
- }
249
-
250
- console.log(`[ProcessManager] Killing managed process PID: ${proc.pid}`);
251
- try {
252
- proc.kill('SIGTERM');
253
- } catch (e) {
254
- console.error(`[ProcessManager] Error killing process ${proc.pid}: ${e.message}`);
255
- }
256
- }
257
- await new Promise(resolve => setTimeout(resolve, 500));
258
-
259
- for (const proc of [...this.managedProcesses]) {
260
- if (proc.killed || proc.exitCode != null) {
261
- continue;
262
- }
263
-
264
- console.warn(`[ProcessManager] Process ${proc.pid} did not exit gracefully, forcing kill.`);
265
- try {
266
- proc.kill('SIGKILL');
267
- } catch (e) {
268
- console.error(`[ProcessManager] Error forcing kill for process ${proc.pid}: ${e.message}`);
269
- }
270
- }
271
-
272
- console.log('[ProcessManager] Managed process cleanup complete.');
273
- }
274
- }
275
-
1
+ const { spawn, spawnSync, execSync } = require('child_process');
2
+ const log = require('./logger');
3
+ const prefixLines = require("./prefix-lines");
4
+
5
+ const isWindows = process.platform === 'win32';
6
+
7
+ class ProcessManager {
8
+ constructor() {
9
+ this.managedProcesses = new Set();
10
+ this.cleanupInProgress = false;
11
+ }
12
+
13
+ /**
14
+ * Runs a command synchronously (blocking).
15
+ * Used for pre-commands, status checks, getting container names.
16
+ * @param {string} command - The command to execute.
17
+ * @param {string[]} args - Arguments for the command.
18
+ * @param {object} [options={}] - Options for spawnSync (e.g., cwd, stdio).
19
+ * @returns {string} The stdout of the command, trimmed.
20
+ * @throws {Error} If the command fails.
21
+ */
22
+ runSync(command, args = [], options = {}) {
23
+ if (this.cleanupInProgress) {
24
+ log.warn(`[ProcessManager] Skipping synchronous command '${command}' during cleanup.`);
25
+ return '';
26
+ }
27
+ log.debug(`[ProcessManager] Running sync: ${command} ${args.join(' ')}`);
28
+ try {
29
+ const result = spawnSync(command, args, {
30
+ shell: true,
31
+ encoding: 'utf8',
32
+ ...options,
33
+ });
34
+
35
+ if (result.error) {
36
+ throw result.error;
37
+ }
38
+ if (result.status !== 0) {
39
+ const stderrOutput = result.stderr ? result.stderr.trim() : 'No stderr output.';
40
+ throw new Error(
41
+ `Command failed with code ${result.status}: ${command} ${args.join(
42
+ ' ',
43
+ )}\n${stderrOutput}`,
44
+ );
45
+ }
46
+ return result.stdout?.trim() ?? '';
47
+ } catch (error) {
48
+ throw new Error(`Failed to run sync command '${command}': ${error.message}`);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Runs a command asynchronously, inheriting stdio.
54
+ * Used for initial 'docker compose up -d' where we want to see immediate output
55
+ * and the process is not meant to be continually managed/restarted by the orchestrator.
56
+ * @param {string} command - The command to execute.
57
+ * @param {string[]} args - Arguments for the command.
58
+ * @param {object} [options={}] - Options for spawn.
59
+ * @returns {Promise<void>} A promise that resolves when the process exits successfully.
60
+ */
61
+ runInherited(command, args = [], options = {}) {
62
+ return new Promise((resolve, reject) => {
63
+ if (this.cleanupInProgress) {
64
+ log.warn(`[ProcessManager] Skipping inherited command '${command}' during cleanup.`);
65
+ return resolve();
66
+ }
67
+ log.debug(`[ProcessManager] Running inherited: ${command} ${args.join(' ')}`);
68
+ const proc = spawn(command, args, {
69
+ shell: true,
70
+ stdio: 'inherit',
71
+ ...options,
72
+ });
73
+
74
+ proc.on('error', (err) => {
75
+ log.error(`[ProcessManager] Failed to start inherited command '${command}':`, err);
76
+ reject(err);
77
+ });
78
+
79
+ proc.on('exit', (code) => {
80
+ if (code !== 0) {
81
+ reject(
82
+ new Error(`Inherited command '${command}' exited with code ${code}`),
83
+ );
84
+ } else {
85
+ resolve();
86
+ }
87
+ });
88
+ });
89
+ }
90
+
91
+ /**
92
+ * Starts a long-running, managed process (like 'npx rollup -w').
93
+ * Its output is prefixed, and it can be configured to restart on exit.
94
+ * @param {string} command - The command to execute.
95
+ * @param {string[]} args - Arguments for the command.
96
+ * @param {object} options - Options for spawn (e.g., cwd).
97
+ * @param {string} prefix - Prefix for stdout/stderr lines (e.g., 'frontend:').
98
+ * @param {boolean} restartOnError - Whether to restart the process if it exits with non-zero code.
99
+ * @param {Function} onExit
100
+ * @param {ChildProcess[] | undefined} processReference - An array where to save the process to gain a reference to it.
101
+ * @returns {ChildProcess} The spawned child process instance.
102
+ */
103
+ startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference) {
104
+ if (this.cleanupInProgress) {
105
+ log.warn(`[ProcessManager] Skipping managed process '${command}' during cleanup.`);
106
+ return null;
107
+ }
108
+ log.debug(
109
+ `[ProcessManager] Starting managed process: ${command} ${args.join(
110
+ ' ',
111
+ )} (prefix: ${prefix})`,
112
+ );
113
+
114
+ if (processReference == null) {
115
+ processReference = {}
116
+ }
117
+
118
+ const startedProcess = spawn(command, args, {
119
+ shell: true,
120
+ stdio: 'pipe',
121
+ ...options,
122
+ env: { FORCE_COLOR: '1', ...process.env, ...(options.env ?? {}) },
123
+ });
124
+ processReference.process = startedProcess;
125
+
126
+ let lastFormatting = '';
127
+ startedProcess.stdout.on('data', (data) => {
128
+ const result = prefixLines(data.toString(), prefix, lastFormatting);
129
+ lastFormatting = result.lastFormatting;
130
+ process.stdout.write(result.prefixedText);
131
+ });
132
+ startedProcess.stderr.on('data', (data) => {
133
+ const result = prefixLines(data.toString(), prefix, lastFormatting);
134
+ lastFormatting = result.lastFormatting;
135
+ process.stderr.write(result.prefixedText);
136
+ });
137
+
138
+ this.managedProcesses.add(startedProcess);
139
+
140
+ startedProcess.on('error', (err) => {
141
+ log.error(
142
+ `[ProcessManager] Error starting managed process '${command}': ${err.message}`,
143
+ );
144
+ this.managedProcesses.delete(startedProcess);
145
+ });
146
+
147
+ startedProcess.on('exit', (code) => {
148
+ if (!this.managedProcesses.has(startedProcess)) {
149
+ return;
150
+ }
151
+
152
+ this.managedProcesses.delete(startedProcess);
153
+ if (this.cleanupInProgress) {
154
+ log.debug(
155
+ `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited due to cleanup.`,
156
+ );
157
+ return;
158
+ }
159
+
160
+ if (code !== 0) {
161
+ log.error(
162
+ `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited with code ${code}.`,
163
+ );
164
+ if (restartOnError) {
165
+ log.warn(
166
+ `[ProcessManager] Restarting managed process '${command}'...`,
167
+ );
168
+ this.startManagedProcess(command, args, options, prefix, restartOnError, onExit, processReference);
169
+ } else {
170
+ onExit?.();
171
+ }
172
+ } else {
173
+ log.debug(
174
+ `[ProcessManager] Managed process '${command}' (PID: ${startedProcess.pid}) exited cleanly.`,
175
+ );
176
+ onExit?.();
177
+ }
178
+ });
179
+
180
+ return processReference;
181
+ }
182
+
183
+ /**
184
+ * Kills a single child process.
185
+ * On Windows, uses taskkill to kill the entire process tree.
186
+ * @param {ChildProcess} childProcess - The process to kill.
187
+ * @returns {Promise<void>}
188
+ */
189
+ killProcess(childProcess) {
190
+ if (childProcess.killed || childProcess.exitCode != null) {
191
+ return Promise.resolve();
192
+ }
193
+
194
+ this.managedProcesses.delete(childProcess);
195
+
196
+ if (isWindows) {
197
+ try {
198
+ execSync(`taskkill /T /F /PID ${childProcess.pid}`, { stdio: 'ignore' });
199
+ } catch {
200
+ // Process may have already been terminated by Ctrl+C signal propagation - this is OK
201
+ }
202
+ return Promise.resolve();
203
+ }
204
+
205
+ // On Unix, use SIGTERM with timeout fallback to SIGKILL
206
+ return new Promise((resolve) => {
207
+ let exited = false;
208
+ let timeoutId = null;
209
+
210
+ const onExit = () => {
211
+ exited = true;
212
+ if (timeoutId) {
213
+ clearTimeout(timeoutId);
214
+ timeoutId = null;
215
+ }
216
+ resolve();
217
+ };
218
+ childProcess.on('exit', onExit);
219
+
220
+ timeoutId = setTimeout(() => {
221
+ if (exited) {
222
+ return;
223
+ }
224
+
225
+ log.error(`[ProcessManager] Timeout reached for process interruption ${childProcess.pid}`);
226
+ try {
227
+ childProcess.kill('SIGKILL');
228
+ } catch (e) {
229
+ log.error(`[ProcessManager] Error force killing process ${childProcess.pid}: ${e.message}`);
230
+ }
231
+ resolve();
232
+ }, 500);
233
+
234
+ try {
235
+ childProcess.kill('SIGTERM');
236
+ } catch (e) {
237
+ log.error(`[ProcessManager] Error signaling process ${childProcess.pid}: ${e.message}`);
238
+ if (timeoutId) {
239
+ clearTimeout(timeoutId);
240
+ }
241
+ resolve();
242
+ }
243
+ });
244
+ }
245
+
246
+ /**
247
+ * Kills all currently managed child processes.
248
+ */
249
+ async cleanupManagedProcesses() {
250
+ if (this.cleanupInProgress) {
251
+ log.debug('[ProcessManager] Cleanup of managed processes already in progress, skipping.');
252
+ return;
253
+ }
254
+
255
+ this.cleanupInProgress = true;
256
+
257
+ log.debug('\n[ProcessManager] Initiating cleanup of managed processes...');
258
+
259
+ // Filter out already-dead processes
260
+ const processesToKill = [...this.managedProcesses].filter(proc => {
261
+ if (proc.killed || proc.exitCode != null) {
262
+ this.managedProcesses.delete(proc);
263
+ return false;
264
+ }
265
+ return true;
266
+ });
267
+
268
+ for (const proc of processesToKill) {
269
+ log.debug(`[ProcessManager] Killing managed process PID: ${proc.pid}`);
270
+ try {
271
+ if (isWindows) {
272
+ // On Windows, use taskkill to kill the entire process tree
273
+ try {
274
+ execSync(`taskkill /T /F /PID ${proc.pid}`, { stdio: 'ignore' });
275
+ } catch {
276
+ // Process may have already been terminated - this is OK
277
+ }
278
+ } else {
279
+ proc.kill('SIGTERM');
280
+ }
281
+ } catch (e) {
282
+ log.error(`[ProcessManager] Error killing process ${proc.pid}: ${e.message}`);
283
+ }
284
+ }
285
+
286
+ // On Unix, wait for processes to exit gracefully before using SIGKILL
287
+ if (processesToKill.length > 0 && !isWindows) {
288
+ await new Promise(resolve => setTimeout(resolve, 500));
289
+
290
+ for (const proc of [...this.managedProcesses]) {
291
+ if (proc.killed || proc.exitCode != null) {
292
+ this.managedProcesses.delete(proc);
293
+ continue;
294
+ }
295
+
296
+ log.warn(`[ProcessManager] Process ${proc.pid} did not exit gracefully, forcing kill.`);
297
+ try {
298
+ proc.kill('SIGKILL');
299
+ } catch (e) {
300
+ log.error(`[ProcessManager] Error forcing kill for process ${proc.pid}: ${e.message}`);
301
+ }
302
+ }
303
+ }
304
+
305
+ log.debug('[ProcessManager] Managed process cleanup complete.');
306
+ }
307
+ }
308
+
276
309
  module.exports = ProcessManager;
@@ -0,0 +1,58 @@
1
+ const PALETTE = [
2
+ 91, 92, 93, 94, 95, 96, // bright red, green, yellow, blue, magenta, cyan
3
+ 31, 32, 33, 34, 35, 36, // standard variants of the same
4
+ ];
5
+
6
+ function shuffle(input) {
7
+ const arr = input.slice();
8
+ for (let i = arr.length - 1; i > 0; i--) {
9
+ const j = Math.floor(Math.random() * (i + 1));
10
+ [arr[i], arr[j]] = [arr[j], arr[i]];
11
+ }
12
+ return arr;
13
+ }
14
+
15
+ const shuffledPalette = shuffle(PALETTE);
16
+ const colorByKey = new Map();
17
+
18
+ function keyFor(name, mode, taskIndex) {
19
+ if (taskIndex == null) return `${name}\0${mode}`;
20
+ return `${name}\0${mode}\0${taskIndex}`;
21
+ }
22
+
23
+ function colorForKey(key) {
24
+ let color = colorByKey.get(key);
25
+ if (color != null) return color;
26
+ color = shuffledPalette[colorByKey.size % shuffledPalette.length];
27
+ colorByKey.set(key, color);
28
+ return color;
29
+ }
30
+
31
+ function colorize(text, code) {
32
+ return `\x1b[${code}m${text}\x1b[0m`;
33
+ }
34
+
35
+ function buildColoredPrefix(name, mode, taskIndex) {
36
+ const color = colorForKey(keyFor(name, mode, taskIndex));
37
+ const taskSegment = taskIndex != null ? `${taskIndex}:` : '';
38
+ return colorize(`${name}:${mode}:${taskSegment}`, color);
39
+ }
40
+
41
+ function buildColoredTag(name, mode) {
42
+ return colorize(`${name}:${mode}`, colorForKey(keyFor(name, mode)));
43
+ }
44
+
45
+ function colorService(name, mode) {
46
+ return colorize(name, colorForKey(keyFor(name, mode)));
47
+ }
48
+
49
+ function colorMode(name, mode) {
50
+ return colorize(mode, colorForKey(keyFor(name, mode)));
51
+ }
52
+
53
+ module.exports = {
54
+ buildColoredPrefix,
55
+ buildColoredTag,
56
+ colorService,
57
+ colorMode,
58
+ };