go-dev 0.4.2 → 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.
@@ -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
+ };
@@ -1,52 +1,64 @@
1
- class BaseService {
2
- /** @type {import("../process-manager")} */
3
- static _processManager = null;
4
-
5
- /**
6
- * Initializes the shared ProcessManager for all service types.
7
- * This should be called once at application startup.
8
- * @param {import("../process-manager")} processManagerInstance
9
- */
10
- static initialize(processManagerInstance) {
11
- if (BaseService._processManager) {
12
- console.warn('BaseService.initialize called multiple times. Skipping.');
13
- return;
14
- }
15
- BaseService._processManager = processManagerInstance;
16
- }
17
-
18
- /**
19
- * Static method for type-specific cleanup. To be overridden by subclasses.
20
- * @returns {Promise<void>}
21
- */
22
- static async cleanup() {
23
- console.log(`[${this.name}] No specific static cleanup defined.`);
24
- }
25
-
26
- /**
27
- * @param {string} name - The logical name of the service (e.g., 'api', 'frontend').
28
- * @param {string} mode - The resolved mode for this service (e.g., 'dev', 'docker', 'serve').
29
- * @param {object} config - The concrete configuration object for this service and mode.
30
- */
31
- constructor(name, mode, config, onExit, extraArgs) {
32
- this.name = name;
33
- this.mode = mode;
34
- this.config = config;
35
- this.onExit = onExit;
36
- this.extraArgs = extraArgs;
37
- }
38
-
39
- async start() {
40
- throw new Error(`Service type for '${this.name}' in mode '${this.mode}' must implement start()`);
41
- }
42
-
43
- async stop() {
44
- return;
45
- }
46
-
47
- async checkHealth() {
48
- return true;
49
- }
50
- }
51
-
1
+ const log = require('../logger');
2
+ const { buildColoredTag } = require('../service-colors');
3
+
4
+ class BaseService {
5
+ /** @type {import("../process-manager")} */
6
+ static _processManager = null;
7
+
8
+ /** @type {object | null} */
9
+ static _servicesMap = null;
10
+
11
+ /**
12
+ * Initializes shared state for all service types.
13
+ * This should be called once at application startup.
14
+ * @param {import("../process-manager")} processManagerInstance
15
+ * @param {object} [servicesMap] - The full `services` block from the loaded config, keyed by service name.
16
+ */
17
+ static initialize(processManagerInstance, servicesMap) {
18
+ if (BaseService._processManager) {
19
+ log.warn('BaseService.initialize called multiple times. Skipping.');
20
+ return;
21
+ }
22
+ BaseService._processManager = processManagerInstance;
23
+ BaseService._servicesMap = servicesMap ?? null;
24
+ }
25
+
26
+ /**
27
+ * Static method for type-specific cleanup. To be overridden by subclasses.
28
+ * @returns {Promise<void>}
29
+ */
30
+ static async cleanup() {
31
+ log.debug(`[${this.name}] No specific static cleanup defined.`);
32
+ }
33
+
34
+ /**
35
+ * @param {string} name - The logical name of the service (e.g., 'api', 'frontend').
36
+ * @param {string} mode - The resolved mode for this service (e.g., 'dev', 'docker', 'serve').
37
+ * @param {object} config - The concrete configuration object for this service and mode.
38
+ */
39
+ constructor(name, mode, config, onExit, extraArgs) {
40
+ this.name = name;
41
+ this.mode = mode;
42
+ this.config = config;
43
+ this.onExit = onExit;
44
+ this.extraArgs = extraArgs;
45
+ }
46
+
47
+ get coloredId() {
48
+ return buildColoredTag(this.name, this.mode);
49
+ }
50
+
51
+ async start() {
52
+ throw new Error(`Service type for '${this.name}' in mode '${this.mode}' must implement start()`);
53
+ }
54
+
55
+ async stop() {
56
+ return;
57
+ }
58
+
59
+ async checkHealth() {
60
+ return true;
61
+ }
62
+ }
63
+
52
64
  module.exports = { BaseService };
@@ -1,149 +1,238 @@
1
- const { BaseService } = require('./base');
2
-
3
- class CmdService extends BaseService {
4
- constructor(name, mode, config, onExit, extraArgs) {
5
- super(name, mode, config, onExit, extraArgs);
6
- this.prefix = `${name}:${mode}:`;
7
- this.processes = [];
8
- }
9
-
10
- async start() {
11
- console.log(`[${this.name}:${this.mode}] Starting cmd service...`);
12
-
13
- const { preCommands, commands } = this.config;
14
- if (!commands) {
15
- throw new Error(
16
- `[${this.name}:${this.mode}] Commands not found for service.`,
17
- );
18
- }
19
-
20
- if (preCommands && preCommands.length > 0) {
21
- console.log(`[${this.name}:${this.mode}] Running pre-commands...`);
22
- for (const command of preCommands) {
23
- const { cmdArgs, directory } = (Array.isArray(command) ?
24
- { cmdArgs: command } :
25
- { cmdArgs: command.command, directory: command.directory }
26
- );
27
- try {
28
- CmdService._processManager.runSync(cmdArgs[0], cmdArgs.slice(1), {
29
- cwd: directory,
30
- stdio: 'inherit',
31
- });
32
- } catch (error) {
33
- console.log({ cmdArgs });
34
- throw new Error(
35
- `[${this.name}:${this.mode}] Pre-command failed: ${cmdArgs.join(
36
- ' ',
37
- )}: ${error.message}`,
38
- );
39
- }
40
- }
41
- console.log(`[${this.name}:${this.mode}] Pre-commands completed.`);
42
- }
43
-
44
- const { cmdArgs, directory, restartOnError } = (Array.isArray(commands) && typeof commands[0] === 'string' ?
45
- { cmdArgs: [commands], directory: [undefined], restartOnError: [undefined] } :
46
- (Array.isArray(commands) ?
47
- {
48
- cmdArgs: commands.map(({ command }) => command),
49
- directory: commands.map(({ directory }) => directory),
50
- restartOnError: commands.map(({ restartOnError }) => restartOnError),
51
- } :
52
- {
53
- cmdArgs: [commands.command],
54
- directory: [commands.directory],
55
- restartOnError: [commands.restartOnError],
56
- }
57
- )
58
- );
59
-
60
- const useProcessIndex = cmdArgs.length > 1;
61
- const exitedProcess = Array.from({ length: cmdArgs.length });
62
- for (let index = 0; index < cmdArgs.length; index++) {
63
- const [command, ...args] = cmdArgs[index];
64
-
65
- const extraArgs = (this.extraArgs?.[index] ?? []).slice();
66
-
67
- const finalArgs = args.map(arg => {
68
- const regex = /(\\*)\$arg/g;
69
-
70
- let indexesToReplace = [];
71
- while (true) {
72
- const match = regex.exec(arg);
73
- if (match == null) {
74
- break;
75
- }
76
-
77
- const backslashes = match[1];
78
-
79
- const startIndex = match.index + backslashes.length;
80
-
81
- if (backslashes.length % 2 === 1) {
82
- indexesToReplace.unshift({
83
- startIndex: startIndex - 1,
84
- endIndex: startIndex + 5,
85
- replacement: '$arg',
86
- });
87
- continue;
88
- }
89
-
90
- const replacement = extraArgs.shift() ?? '';
91
- indexesToReplace.unshift({
92
- startIndex,
93
- endIndex: startIndex + 4,
94
- replacement,
95
- });
96
- }
97
-
98
- indexesToReplace.forEach(({ startIndex, endIndex, replacement }) => {
99
- console.log({ replacement, start: arg.slice(0, startIndex) });
100
- arg = arg.slice(0, startIndex) + replacement + arg.slice(endIndex);
101
- });
102
-
103
- return arg;
104
- }).concat(extraArgs);
105
-
106
- const process = CmdService._processManager.startManagedProcess(
107
- command,
108
- finalArgs,
109
- { cwd: directory[index] },
110
- (useProcessIndex ?
111
- `${this.prefix}${index}:` :
112
- this.prefix
113
- ),
114
- restartOnError[index],
115
- () => {
116
- exitedProcess[index] = true;
117
- if (exitedProcess.some(exited => !exited)) {
118
- return;
119
- }
120
-
121
- this.onExit?.();
122
- }
123
- );
124
-
125
- if (!process) {
126
- throw new Error(
127
- `[${this.name}:${this.mode}] Failed to spawn process: ${command.join(' ')}`,
128
- );
129
- }
130
-
131
- this.processes.push(process);
132
- console.log(
133
- `[${this.name}:${this.mode}] Process started (PID: ${process.process.pid}).`,
134
- );
135
- }
136
- }
137
-
138
- async stop() {
139
- const promises = this.processes.map(({ process }) => {
140
- console.log(`[${this.name}:${this.mode}] Stopping process (PID: ${process.pid}).`);
141
- return CmdService._processManager.killProcess(process);
142
- });
143
- this.processes.splice(0, this.processes.length);
144
-
145
- await Promise.all(promises);
146
- }
147
- }
148
-
1
+ const log = require('../logger');
2
+ const { BaseService } = require('./base');
3
+ const { buildColoredPrefix, buildColoredTag } = require('../service-colors');
4
+
5
+ class CmdService extends BaseService {
6
+ /**
7
+ * Per-process cache of in-flight or completed service-as-preCommand runs.
8
+ * Key: `${serviceName}:${resolvedMode}`. Value: Promise<void> for the run.
9
+ */
10
+ static _serviceCommandCache = new Map();
11
+
12
+ constructor(name, mode, config, onExit, extraArgs) {
13
+ super(name, mode, config, onExit, extraArgs);
14
+ this.processes = [];
15
+ }
16
+
17
+ static _normalizeCommands(commands) {
18
+ if (Array.isArray(commands) && typeof commands[0] === 'string') {
19
+ return [{ command: commands, directory: undefined }];
20
+ }
21
+ if (Array.isArray(commands)) {
22
+ return commands.map(c => (Array.isArray(c)
23
+ ? { command: c, directory: undefined }
24
+ : { command: c.command, directory: c.directory }
25
+ ));
26
+ }
27
+ return [{ command: commands.command, directory: commands.directory }];
28
+ }
29
+
30
+ static _resolveServiceMode(serviceName, requestedMode) {
31
+ const allServices = BaseService._servicesMap;
32
+ const service = allServices?.[serviceName];
33
+ if (!service) {
34
+ throw new Error(`Service '${serviceName}' referenced as preCommand not found in configuration.`);
35
+ }
36
+ const mode = (service.type === 'hybrid'
37
+ ? requestedMode ?? service.defaultMode ?? 'dev'
38
+ : requestedMode ?? 'dev');
39
+ const config = (service.type === 'hybrid'
40
+ ? service.modes?.[mode]
41
+ : (mode === 'dev' ? service : undefined));
42
+ if (config == null) {
43
+ throw new Error(`Mode '${mode}' not found in service '${serviceName}'.`);
44
+ }
45
+ if (config.type !== 'cmd') {
46
+ throw new Error(
47
+ `preCommand service '${serviceName}:${mode}' must be of type 'cmd', got '${config.type}'.`,
48
+ );
49
+ }
50
+ return { mode, config };
51
+ }
52
+
53
+ static _runServiceAsPreCommand(serviceName, requestedMode, fromContext) {
54
+ const { mode, config } = CmdService._resolveServiceMode(serviceName, requestedMode);
55
+ const key = `${serviceName}:${mode}`;
56
+ const existing = CmdService._serviceCommandCache.get(key);
57
+ if (existing) {
58
+ log.info(`[${fromContext}] preCommand service '${key}' already in flight or completed; awaiting.`);
59
+ return existing;
60
+ }
61
+
62
+ const promise = (async () => {
63
+ const ctx = buildColoredTag(serviceName, mode);
64
+ log.info(`[${ctx}] Running as preCommand service...`);
65
+
66
+ if (config.preCommands && config.preCommands.length > 0) {
67
+ for (const pre of config.preCommands) {
68
+ await CmdService._runPreCommand(pre, ctx);
69
+ }
70
+ }
71
+
72
+ const normalized = CmdService._normalizeCommands(config.commands);
73
+ await Promise.all(normalized.map(({ command, directory }) => {
74
+ const [cmd, ...args] = command;
75
+ return CmdService._processManager.runInherited(cmd, args, { cwd: directory });
76
+ }));
77
+
78
+ log.info(`[${ctx}] preCommand service completed.`);
79
+ })();
80
+
81
+ CmdService._serviceCommandCache.set(key, promise);
82
+ return promise;
83
+ }
84
+
85
+ static async _runPreCommand(pre, fromContext) {
86
+ if (!Array.isArray(pre) && pre != null && typeof pre === 'object' && pre.service != null) {
87
+ try {
88
+ await CmdService._runServiceAsPreCommand(pre.service, pre.mode, fromContext);
89
+ } catch (error) {
90
+ throw new Error(
91
+ `[${fromContext}] Pre-command service '${pre.service}${pre.mode ? `:${pre.mode}` : ''}' failed: ${error.message}`,
92
+ );
93
+ }
94
+ return;
95
+ }
96
+
97
+ const { cmdArgs, directory } = (Array.isArray(pre)
98
+ ? { cmdArgs: pre }
99
+ : { cmdArgs: pre.command, directory: pre.directory });
100
+ try {
101
+ CmdService._processManager.runSync(cmdArgs[0], cmdArgs.slice(1), {
102
+ cwd: directory,
103
+ stdio: 'inherit',
104
+ });
105
+ } catch (error) {
106
+ log.debug({ cmdArgs });
107
+ throw new Error(
108
+ `[${fromContext}] Pre-command failed: ${cmdArgs.join(' ')}: ${error.message}`,
109
+ );
110
+ }
111
+ }
112
+
113
+ async start() {
114
+ log.info(`[${this.coloredId}] Starting cmd service...`);
115
+
116
+ const { preCommands, commands } = this.config;
117
+ if (!commands) {
118
+ throw new Error(
119
+ `[${this.coloredId}] Commands not found for service.`,
120
+ );
121
+ }
122
+
123
+ if (preCommands && preCommands.length > 0) {
124
+ log.info(`[${this.coloredId}] Running pre-commands...`);
125
+ for (const pre of preCommands) {
126
+ await CmdService._runPreCommand(pre, this.coloredId);
127
+ }
128
+ log.info(`[${this.coloredId}] Pre-commands completed.`);
129
+ }
130
+
131
+ const { cmdArgs, directory, restartOnError } = (Array.isArray(commands) && typeof commands[0] === 'string' ?
132
+ { cmdArgs: [commands], directory: [undefined], restartOnError: [undefined] } :
133
+ (Array.isArray(commands) ?
134
+ {
135
+ cmdArgs: commands.map(({ command }) => command),
136
+ directory: commands.map(({ directory }) => directory),
137
+ restartOnError: commands.map(({ restartOnError }) => restartOnError),
138
+ } :
139
+ {
140
+ cmdArgs: [commands.command],
141
+ directory: [commands.directory],
142
+ restartOnError: [commands.restartOnError],
143
+ }
144
+ )
145
+ );
146
+
147
+ const useProcessIndex = cmdArgs.length > 1;
148
+ const exitedProcess = Array.from({ length: cmdArgs.length });
149
+ for (let index = 0; index < cmdArgs.length; index++) {
150
+ const [command, ...args] = cmdArgs[index];
151
+
152
+ const extraArgs = (this.extraArgs?.[index] ?? []).slice();
153
+
154
+ const finalArgs = args.map(arg => {
155
+ const regex = /(\\*)\$arg/g;
156
+
157
+ let indexesToReplace = [];
158
+ while (true) {
159
+ const match = regex.exec(arg);
160
+ if (match == null) {
161
+ break;
162
+ }
163
+
164
+ const backslashes = match[1];
165
+
166
+ const startIndex = match.index + backslashes.length;
167
+
168
+ if (backslashes.length % 2 === 1) {
169
+ indexesToReplace.unshift({
170
+ startIndex: startIndex - 1,
171
+ endIndex: startIndex + 5,
172
+ replacement: '$arg',
173
+ });
174
+ continue;
175
+ }
176
+
177
+ const replacement = extraArgs.shift() ?? '';
178
+ indexesToReplace.unshift({
179
+ startIndex,
180
+ endIndex: startIndex + 4,
181
+ replacement,
182
+ });
183
+ }
184
+
185
+ indexesToReplace.forEach(({ startIndex, endIndex, replacement }) => {
186
+ log.debug({ replacement, start: arg.slice(0, startIndex) });
187
+ arg = arg.slice(0, startIndex) + replacement + arg.slice(endIndex);
188
+ });
189
+
190
+ return arg;
191
+ }).concat(extraArgs);
192
+
193
+ const prefix = buildColoredPrefix(
194
+ this.name,
195
+ this.mode,
196
+ useProcessIndex ? index : null,
197
+ );
198
+ const process = CmdService._processManager.startManagedProcess(
199
+ command,
200
+ finalArgs,
201
+ { cwd: directory[index] },
202
+ prefix,
203
+ restartOnError[index],
204
+ () => {
205
+ exitedProcess[index] = true;
206
+ if (exitedProcess.some(exited => !exited)) {
207
+ return;
208
+ }
209
+
210
+ this.onExit?.();
211
+ }
212
+ );
213
+
214
+ if (!process) {
215
+ throw new Error(
216
+ `[${this.coloredId}] Failed to spawn process: ${command.join(' ')}`,
217
+ );
218
+ }
219
+
220
+ this.processes.push(process);
221
+ log.debug(
222
+ `[${this.coloredId}] Process started (PID: ${process.process.pid}).`,
223
+ );
224
+ }
225
+ }
226
+
227
+ async stop() {
228
+ const promises = this.processes.map(({ process }) => {
229
+ log.debug(`[${this.coloredId}] Stopping process (PID: ${process.pid}).`);
230
+ return CmdService._processManager.killProcess(process);
231
+ });
232
+ this.processes.splice(0, this.processes.length);
233
+
234
+ await Promise.all(promises);
235
+ }
236
+ }
237
+
149
238
  module.exports = { CmdService };