go-dev 0.1.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.
- package/package.json +15 -0
- package/src/config.js +117 -0
- package/src/dependency-resolver.js +102 -0
- package/src/index.js +4 -0
- package/src/orchestrator.js +106 -0
- package/src/process-manager.js +213 -0
- package/src/services/base.js +50 -0
- package/src/services/cmd.js +81 -0
- package/src/services/docker.js +172 -0
- package/test.js +136 -0
package/package.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "go-dev",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"main": "src/index.js",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
7
|
+
},
|
|
8
|
+
"author": "Giuliano Collacchioni",
|
|
9
|
+
"license": "MIT",
|
|
10
|
+
"description": "",
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"joi": "^17.13.3",
|
|
13
|
+
"js-yaml": "^4.1.0"
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/config.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
const Joi = require('joi');
|
|
2
|
+
const yaml = require('js-yaml');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const dependencyEntrySchema = Joi.alternatives().try(
|
|
7
|
+
Joi.string(),
|
|
8
|
+
Joi.object({
|
|
9
|
+
service: Joi.string().required(),
|
|
10
|
+
mode: Joi.string().required()
|
|
11
|
+
})
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
const commandSchema = Joi.array().items(Joi.string().min(1)).min(1);
|
|
15
|
+
const commandConfigSchema = Joi.alternatives().try(
|
|
16
|
+
commandSchema,
|
|
17
|
+
Joi.object({
|
|
18
|
+
command: commandSchema,
|
|
19
|
+
directory: Joi.string().min(1),
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const cmdServiceConfigSchema = Joi.object({
|
|
24
|
+
type: Joi.string().valid('cmd').required(),
|
|
25
|
+
preCommands: Joi.array().items(commandConfigSchema).default([]),
|
|
26
|
+
commands: Joi.object().pattern(
|
|
27
|
+
Joi.string(),
|
|
28
|
+
commandConfigSchema
|
|
29
|
+
).min(1).required(),
|
|
30
|
+
defaultCommand: Joi.string().default('start'),
|
|
31
|
+
directory: Joi.string(),
|
|
32
|
+
dependencies: Joi.array().items(dependencyEntrySchema).default([]),
|
|
33
|
+
healthCheck: Joi.boolean().default(false)
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const dockerServiceConfigSchema = Joi.object({
|
|
37
|
+
type: Joi.string().valid('docker').required(),
|
|
38
|
+
service: Joi.string().required(),
|
|
39
|
+
composeFile: Joi.string().default('docker-compose.yml'),
|
|
40
|
+
dependencies: Joi.array().items(dependencyEntrySchema).default([]),
|
|
41
|
+
healthCheck: Joi.boolean().default(true)
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const serviceSchema = Joi.alternatives().try(
|
|
45
|
+
cmdServiceConfigSchema,
|
|
46
|
+
dockerServiceConfigSchema,
|
|
47
|
+
Joi.object({
|
|
48
|
+
type: Joi.string().valid('hybrid').required(),
|
|
49
|
+
defaultMode: Joi.string(),
|
|
50
|
+
modes: Joi.object().pattern(
|
|
51
|
+
Joi.string(),
|
|
52
|
+
Joi.alternatives().try(
|
|
53
|
+
cmdServiceConfigSchema,
|
|
54
|
+
dockerServiceConfigSchema
|
|
55
|
+
)
|
|
56
|
+
).min(2).required()
|
|
57
|
+
})
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const configSchema = Joi.object({
|
|
61
|
+
services: Joi.object().pattern(Joi.string(), serviceSchema).required(),
|
|
62
|
+
presets: Joi.object().pattern(
|
|
63
|
+
Joi.string(),
|
|
64
|
+
Joi.object({
|
|
65
|
+
services: Joi.array().items(Joi.string()).required(),
|
|
66
|
+
modes: Joi.object().pattern(Joi.string(), Joi.string()).default({})
|
|
67
|
+
})
|
|
68
|
+
).default({})
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
function findConfigFile() {
|
|
72
|
+
const possibleNames = [
|
|
73
|
+
'dev-orchestrator',
|
|
74
|
+
'.dev-orchestrator'
|
|
75
|
+
].flatMap(baseName => {
|
|
76
|
+
return [
|
|
77
|
+
baseName,
|
|
78
|
+
`${baseName}.config`
|
|
79
|
+
];
|
|
80
|
+
}).flatMap(baseName => {
|
|
81
|
+
return [
|
|
82
|
+
`${baseName}.yml`,
|
|
83
|
+
`${baseName}.yaml`
|
|
84
|
+
];
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
for (const name of possibleNames) {
|
|
88
|
+
if (fs.existsSync(name)) {
|
|
89
|
+
return name;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
throw new Error(`No config file found. Expected one of: ${possibleNames.join(', ')}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function loadConfig(configPath) {
|
|
97
|
+
if (!configPath) {
|
|
98
|
+
configPath = findConfigFile();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!fs.existsSync(configPath)) {
|
|
102
|
+
throw new Error(`Config file not found: ${configPath}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
106
|
+
const config = yaml.load(configContent);
|
|
107
|
+
|
|
108
|
+
const { error, value } = configSchema.validate(config);
|
|
109
|
+
|
|
110
|
+
if (error) {
|
|
111
|
+
throw new Error(`Invalid config: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
module.exports = { loadConfig };
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
function resolveServiceExecutionGraph(config, presetName) {
|
|
2
|
+
const preset = config.presets[presetName];
|
|
3
|
+
if (!preset) {
|
|
4
|
+
throw new Error(`Preset '${presetName}' not found in configuration.`);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const services = [];
|
|
8
|
+
const dependencies = [];
|
|
9
|
+
|
|
10
|
+
for (const serviceName of preset.services) {
|
|
11
|
+
addService(
|
|
12
|
+
serviceName,
|
|
13
|
+
preset.modes[serviceName],
|
|
14
|
+
null,
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return { dependencies, services };
|
|
19
|
+
|
|
20
|
+
function addService(serviceName, mode, dependentService) {
|
|
21
|
+
const service = config.services[serviceName];
|
|
22
|
+
if (service == null) {
|
|
23
|
+
throw new Error(`Service named '${serviceName}' not found in configuration.`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (dependentService != null) {
|
|
27
|
+
const existingService = services.find(({ name }) => {
|
|
28
|
+
return name === serviceName;
|
|
29
|
+
});
|
|
30
|
+
if (existingService != null) {
|
|
31
|
+
console.warn(
|
|
32
|
+
`Ignoring dependency '${serviceName}' for '${dependentService}' because it is flagged to be run as service in mode '${existingService.mode}'.`
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
const existingDependencyIndex = dependencies.findIndex(({ name }) => {
|
|
38
|
+
return name === serviceName;
|
|
39
|
+
});
|
|
40
|
+
if (existingDependencyIndex >= 0) {
|
|
41
|
+
console.warn(
|
|
42
|
+
`Removing service '${serviceName}' from dependencies because it is flagged to be run as service in mode '${dependencies[existingDependencyIndex].mode}'.`
|
|
43
|
+
);
|
|
44
|
+
dependencies.splice(existingDependencyIndex, 1);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
mode = (service.type === 'hybrid' ?
|
|
49
|
+
mode ?? service.defaultMode ?? 'dev' :
|
|
50
|
+
mode ?? 'dev'
|
|
51
|
+
);
|
|
52
|
+
const serviceConfig = (service.type === 'hybrid' ?
|
|
53
|
+
service.modes[mode] :
|
|
54
|
+
(mode === 'dev' ? service : undefined)
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
if (serviceConfig == null) {
|
|
58
|
+
throw new Error(`Mode named '${mode}' not found in service '${serviceName}'.`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (dependentService == null) {
|
|
62
|
+
services.push({
|
|
63
|
+
name: serviceName,
|
|
64
|
+
mode,
|
|
65
|
+
config: serviceConfig,
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
dependencies.unshift({
|
|
69
|
+
name: serviceName,
|
|
70
|
+
mode,
|
|
71
|
+
config: serviceConfig,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (let index = serviceConfig.dependencies.length - 1; index >= 0; index--) {
|
|
76
|
+
const dependency = serviceConfig.dependencies[index];
|
|
77
|
+
const {
|
|
78
|
+
service: dependencyName,
|
|
79
|
+
mode: dependencyMode,
|
|
80
|
+
} = (typeof dependency === 'string' ?
|
|
81
|
+
{ service: dependency, mode: 'dev' } :
|
|
82
|
+
dependency
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const existingDependencyIndex = dependencies.find(({ name }) => {
|
|
86
|
+
return name === dependencyName;
|
|
87
|
+
});
|
|
88
|
+
if (existingDependencyIndex != null) {
|
|
89
|
+
console.warn(`Skipping dependency '${dependencyName}' for '${serviceName}' because it's already present in dependencies list.`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
addService(
|
|
94
|
+
dependencyName,
|
|
95
|
+
dependencyMode,
|
|
96
|
+
serviceName,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { resolveServiceExecutionGraph };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const { loadConfig } = require('./config');
|
|
2
|
+
const { resolveServiceExecutionGraph } = require('./dependency-resolver');
|
|
3
|
+
const ProcessManager = require('./process-manager');
|
|
4
|
+
const { BaseService } = require('./services/base');
|
|
5
|
+
const { CmdService } = require('./services/cmd');
|
|
6
|
+
const { DockerService } = require('./services/docker');
|
|
7
|
+
|
|
8
|
+
const serviceTypeMap = {
|
|
9
|
+
cmd: CmdService,
|
|
10
|
+
docker: DockerService,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
class Orchestrator {
|
|
14
|
+
constructor(configPath) {
|
|
15
|
+
this.config = loadConfig(configPath);
|
|
16
|
+
|
|
17
|
+
this.processManager = new ProcessManager();
|
|
18
|
+
this.activeServiceInstances = new Map();
|
|
19
|
+
|
|
20
|
+
BaseService.initialize(this.processManager);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async start(presetName) {
|
|
24
|
+
try {
|
|
25
|
+
const { dependencies, services: primaryServices } = resolveServiceExecutionGraph(
|
|
26
|
+
this.config,
|
|
27
|
+
presetName,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
console.log(`Starting development environment for preset: ${presetName}`);
|
|
31
|
+
console.log('--- Resolved Dependencies to Start First ---');
|
|
32
|
+
dependencies.forEach(s => console.log(` - ${s.name} (mode: ${s.mode})`));
|
|
33
|
+
console.log('--- Resolved Primary Services to Run ---');
|
|
34
|
+
primaryServices.forEach(s => console.log(` - ${s.name} (mode: ${s.mode})`));
|
|
35
|
+
|
|
36
|
+
console.log('\n--- Starting Dependencies ---');
|
|
37
|
+
for (const { name, mode, config } of dependencies) {
|
|
38
|
+
if (this.activeServiceInstances.has(name)) {
|
|
39
|
+
console.log(`[${name}:${mode}] Already active, skipping start.`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const ServiceClass = serviceTypeMap[config.type];
|
|
43
|
+
if (!ServiceClass) {
|
|
44
|
+
throw new Error(`Unknown service type '${config.type}' for service '${name}'.`);
|
|
45
|
+
}
|
|
46
|
+
const serviceInstance = new ServiceClass(name, mode, config);
|
|
47
|
+
this.activeServiceInstances.set(name, serviceInstance);
|
|
48
|
+
await serviceInstance.start();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('\n--- Starting Primary Services ---');
|
|
52
|
+
const primaryServicePromises = [];
|
|
53
|
+
for (const { name, mode, config } of primaryServices) {
|
|
54
|
+
if (this.activeServiceInstances.has(name)) {
|
|
55
|
+
console.log(`[${name}:${mode}] Already active, skipping start.`);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const ServiceClass = serviceTypeMap[config.type];
|
|
59
|
+
if (!ServiceClass) {
|
|
60
|
+
throw new Error(`Unknown service type '${config.type}' for service '${name}'.`);
|
|
61
|
+
}
|
|
62
|
+
const serviceInstance = new ServiceClass(name, mode, config);
|
|
63
|
+
this.activeServiceInstances.set(name, serviceInstance);
|
|
64
|
+
primaryServicePromises.push(serviceInstance.start());
|
|
65
|
+
}
|
|
66
|
+
await Promise.all(primaryServicePromises);
|
|
67
|
+
|
|
68
|
+
console.log('\n--- All services initiated. Press Ctrl+C to stop. ---');
|
|
69
|
+
|
|
70
|
+
process.once('SIGINT', this.cleanup.bind(this));
|
|
71
|
+
process.once('SIGTERM', this.cleanup.bind(this));
|
|
72
|
+
process.stdin.resume();
|
|
73
|
+
|
|
74
|
+
} catch (error) {
|
|
75
|
+
console.error('\n❌ Orchestrator failed to start:', error.message);
|
|
76
|
+
await this.cleanup(true);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async cleanup() {
|
|
82
|
+
if (this.processManager.cleanupInProgress) {
|
|
83
|
+
console.log('[Orchestrator] Cleanup already in progress.');
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
console.log('\n[Orchestrator] Initiating graceful cleanup...');
|
|
88
|
+
|
|
89
|
+
for (const [name, instance] of this.activeServiceInstances.entries()) {
|
|
90
|
+
try {
|
|
91
|
+
console.log(`[Orchestrator] Requesting instance stop for ${name}:${instance.mode}`);
|
|
92
|
+
await instance.stop();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`[Orchestrator] Error stopping instance ${name}:${instance.mode}: ${error.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
await DockerService.cleanup();
|
|
99
|
+
await this.processManager.cleanupManagedProcesses();
|
|
100
|
+
|
|
101
|
+
console.log('[Orchestrator] Cleanup complete.');
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
module.exports = Orchestrator;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// src/process-manager.js
|
|
2
|
+
const { spawn, spawnSync } = require('child_process');
|
|
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} restartOnExit - Whether to restart the process if it exits with non-zero code.
|
|
96
|
+
* @returns {ChildProcess} The spawned child process instance.
|
|
97
|
+
*/
|
|
98
|
+
startManagedProcess(command, args, options, prefix, restartOnExit) {
|
|
99
|
+
if (this.cleanupInProgress) {
|
|
100
|
+
console.warn(`[ProcessManager] Skipping managed process '${command}' during cleanup.`);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
console.log(
|
|
104
|
+
`[ProcessManager] Starting managed process: ${command} ${args.join(
|
|
105
|
+
' ',
|
|
106
|
+
)} (prefix: ${prefix})`,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const proc = spawn(command, args, {
|
|
110
|
+
shell: true,
|
|
111
|
+
stdio: 'pipe',
|
|
112
|
+
...options,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
proc.stdout.on('data', (data) => {
|
|
116
|
+
let prefixedData = data.toString().split('\n').map(line => `${prefix} ${line}`).join('\n');
|
|
117
|
+
if (prefixedData.endsWith(`${prefix} `)) {
|
|
118
|
+
prefixedData = prefixedData.slice(0, -(`${prefix} `).length);
|
|
119
|
+
}
|
|
120
|
+
if (!prefixedData.endsWith('\n')) {
|
|
121
|
+
prefixedData = prefixedData + '\n';
|
|
122
|
+
}
|
|
123
|
+
process.stdout.write(prefixedData);
|
|
124
|
+
});
|
|
125
|
+
proc.stderr.on('data', (data) => {
|
|
126
|
+
let prefixedData = data.toString().split('\n').map(line => `${prefix} ${line}`).join('\n');
|
|
127
|
+
if (prefixedData.endsWith(`${prefix} `)) {
|
|
128
|
+
prefixedData = prefixedData.slice(0, -(`${prefix} `).length);
|
|
129
|
+
}
|
|
130
|
+
if (!prefixedData.endsWith('\n')) {
|
|
131
|
+
prefixedData = prefixedData + '\n';
|
|
132
|
+
}
|
|
133
|
+
process.stderr.write(prefixedData);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.managedProcesses.add(proc);
|
|
137
|
+
|
|
138
|
+
proc.on('error', (err) => {
|
|
139
|
+
console.error(
|
|
140
|
+
`[ProcessManager] Error starting managed process '${command}': ${err.message}`,
|
|
141
|
+
);
|
|
142
|
+
this.managedProcesses.delete(proc);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
proc.on('exit', (code, signal) => {
|
|
146
|
+
this.managedProcesses.delete(proc);
|
|
147
|
+
if (this.cleanupInProgress) {
|
|
148
|
+
console.log(
|
|
149
|
+
`[ProcessManager] Managed process '${command}' (PID: ${proc.pid}) exited due to cleanup.`,
|
|
150
|
+
);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (code !== 0) {
|
|
155
|
+
console.error(
|
|
156
|
+
`[ProcessManager] Managed process '${command}' (PID: ${proc.pid}) exited with code ${code}.`,
|
|
157
|
+
);
|
|
158
|
+
if (restartOnExit) {
|
|
159
|
+
console.warn(
|
|
160
|
+
`[ProcessManager] Restarting managed process '${command}'...`,
|
|
161
|
+
);
|
|
162
|
+
this.startManagedProcess(command, args, options, prefix, restartOnExit);
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
console.log(
|
|
166
|
+
`[ProcessManager] Managed process '${command}' (PID: ${proc.pid}) exited cleanly.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return proc;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Kills all currently managed child processes.
|
|
176
|
+
*/
|
|
177
|
+
async cleanupManagedProcesses() {
|
|
178
|
+
if (this.cleanupInProgress) {
|
|
179
|
+
console.log('[ProcessManager] Cleanup of managed processes already in progress, skipping.');
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
this.cleanupInProgress = true;
|
|
183
|
+
|
|
184
|
+
console.log('\n[ProcessManager] Initiating cleanup of managed processes...');
|
|
185
|
+
|
|
186
|
+
for (const proc of [...this.managedProcesses]) {
|
|
187
|
+
console.log(`[ProcessManager] Killing managed process PID: ${proc.pid}`);
|
|
188
|
+
try {
|
|
189
|
+
proc.kill('SIGTERM');
|
|
190
|
+
} catch (e) {
|
|
191
|
+
console.error(`[ProcessManager] Error killing process ${proc.pid}: ${e.message}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
195
|
+
|
|
196
|
+
for (const proc of [...this.managedProcesses]) {
|
|
197
|
+
if (proc.killed) {
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.warn(`[ProcessManager] Process ${proc.pid} did not exit gracefully, forcing kill.`);
|
|
202
|
+
try {
|
|
203
|
+
proc.kill('SIGKILL');
|
|
204
|
+
} catch (e) {
|
|
205
|
+
console.error(`[ProcessManager] Error forcing kill for process ${proc.pid}: ${e.message}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
console.log('[ProcessManager] Managed process cleanup complete.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = ProcessManager;
|
|
@@ -0,0 +1,50 @@
|
|
|
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) {
|
|
32
|
+
this.name = name;
|
|
33
|
+
this.mode = mode;
|
|
34
|
+
this.config = config;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async start() {
|
|
38
|
+
throw new Error(`Service type for '${this.name}' in mode '${this.mode}' must implement start()`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async stop() {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async checkHealth() {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { BaseService };
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const { BaseService } = require('./base');
|
|
2
|
+
|
|
3
|
+
class CmdService extends BaseService {
|
|
4
|
+
constructor(name, mode, config) {
|
|
5
|
+
super(name, mode, config);
|
|
6
|
+
this.prefix = `${name}:${mode}:`;
|
|
7
|
+
this.process = null;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async start() {
|
|
11
|
+
console.log(`[${this.name}:${this.mode}] Starting cmd service...`);
|
|
12
|
+
|
|
13
|
+
const { preCommands, commands, defaultCommand } = this.config;
|
|
14
|
+
|
|
15
|
+
if (preCommands && preCommands.length > 0) {
|
|
16
|
+
console.log(`[${this.name}:${this.mode}] Running pre-commands...`);
|
|
17
|
+
for (const command of preCommands) {
|
|
18
|
+
const { cmdArgs, directory } = (Array.isArray(command) ?
|
|
19
|
+
{ cmdArgs: command } :
|
|
20
|
+
{ cmdArgs: command.command, cmdArgs: command.directory }
|
|
21
|
+
);
|
|
22
|
+
try {
|
|
23
|
+
CmdService._processManager.runSync(cmdArgs[0], cmdArgs.slice(1), {
|
|
24
|
+
cwd: directory,
|
|
25
|
+
stdio: 'inherit',
|
|
26
|
+
});
|
|
27
|
+
} catch (error) {
|
|
28
|
+
throw new Error(
|
|
29
|
+
`[${this.name}:${this.mode}] Pre-command failed: ${cmdArgs.join(
|
|
30
|
+
' ',
|
|
31
|
+
)}: ${error.message}`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
console.log(`[${this.name}:${this.mode}] Pre-commands completed.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const mainCommand = commands[defaultCommand];
|
|
39
|
+
if (!mainCommand) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`[${this.name}:${this.mode}] Default command '${defaultCommand}' not found for service.`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { cmdArgs, directory } = (Array.isArray(mainCommand) ?
|
|
46
|
+
{ cmdArgs: mainCommand } :
|
|
47
|
+
{ cmdArgs: mainCommand.command, directory: mainCommand.directory }
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
this.process = CmdService._processManager.startManagedProcess(
|
|
51
|
+
cmdArgs[0],
|
|
52
|
+
cmdArgs.slice(1),
|
|
53
|
+
{ cwd: directory },
|
|
54
|
+
this.prefix,
|
|
55
|
+
true,
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!this.process) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`[${this.name}:${this.mode}] Failed to spawn main process: ${cmdArgs.join(' ')}`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
console.log(
|
|
64
|
+
`[${this.name}:${this.mode}] Main process started (PID: ${this.process.pid}).`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async stop() {
|
|
69
|
+
if (this.process) {
|
|
70
|
+
console.log(`[${this.name}:${this.mode}] Stopping main process (PID: ${this.process.pid}).`);
|
|
71
|
+
try {
|
|
72
|
+
this.process.kill('SIGTERM');
|
|
73
|
+
} catch (e) {
|
|
74
|
+
console.error(`[${this.name}:${this.mode}] Error signaling process ${this.process.pid}: ${e.message}`);
|
|
75
|
+
}
|
|
76
|
+
this.process = null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { CmdService };
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
const { BaseService } = require('./base');
|
|
2
|
+
|
|
3
|
+
class DockerService extends BaseService {
|
|
4
|
+
/** @type {Set<string>} */
|
|
5
|
+
static _composeFilesToStop = new Set();
|
|
6
|
+
|
|
7
|
+
static async cleanup() {
|
|
8
|
+
if (!DockerService._processManager) {
|
|
9
|
+
console.warn('[DockerService] ProcessManager not initialized, skipping static cleanup.');
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (DockerService._composeFilesToStop.size > 0) {
|
|
14
|
+
console.log('[DockerService] Stopping all docker services from used compose files...');
|
|
15
|
+
for (const composeFile of DockerService._composeFilesToStop) {
|
|
16
|
+
try {
|
|
17
|
+
console.log(`[DockerService] Running 'docker compose -f ${composeFile} stop'`);
|
|
18
|
+
DockerService._processManager.runSync(
|
|
19
|
+
'docker',
|
|
20
|
+
['compose', '-f', composeFile, 'stop'],
|
|
21
|
+
{ stdio: 'inherit' },
|
|
22
|
+
);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(
|
|
25
|
+
`[DockerService] Failed to execute 'docker compose -f ${composeFile} stop': ${error.message}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
DockerService._composeFilesToStop.clear();
|
|
30
|
+
} else {
|
|
31
|
+
console.log('[DockerService] No docker services were started by orchestrator, skipping global stop.');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
constructor(name, mode, config) {
|
|
36
|
+
super(name, mode, config);
|
|
37
|
+
this.dockerServiceName = config.service;
|
|
38
|
+
this.dockerComposeFile = config.composeFile;
|
|
39
|
+
this.containerName = null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async start() {
|
|
43
|
+
console.log(`[${this.name}:${this.mode}] Starting docker service '${this.dockerServiceName}' (using ${this.dockerComposeFile})...`);
|
|
44
|
+
|
|
45
|
+
let status = this._getContainerStatus();
|
|
46
|
+
if (status === 'running') {
|
|
47
|
+
console.log(
|
|
48
|
+
`[${this.name}:${this.mode}] Docker container for '${this.dockerServiceName}' is already running.`,
|
|
49
|
+
);
|
|
50
|
+
DockerService._composeFilesToStop.add(this.dockerComposeFile);
|
|
51
|
+
} else {
|
|
52
|
+
console.log(`[${this.name}:${this.mode}] Bringing up docker service '${this.dockerServiceName}'...`);
|
|
53
|
+
try {
|
|
54
|
+
await DockerService._processManager.runInherited(
|
|
55
|
+
'docker',
|
|
56
|
+
['compose', '-f', this.dockerComposeFile, 'up', this.dockerServiceName, '-d'],
|
|
57
|
+
);
|
|
58
|
+
DockerService._composeFilesToStop.add(this.dockerComposeFile);
|
|
59
|
+
console.log(`[${this.name}:${this.mode}] Docker service '${this.dockerServiceName}' brought up.`);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
throw new Error(
|
|
62
|
+
`[${this.name}:${this.mode}] Failed to bring up docker service '${this.dockerServiceName}': ${error.message}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this.config.healthCheck) {
|
|
68
|
+
console.log(`[${this.name}:${this.mode}] Checking healthiness for '${this.dockerServiceName}'...`);
|
|
69
|
+
try {
|
|
70
|
+
await this._checkServiceHealthiness();
|
|
71
|
+
console.log(`[${this.name}:${this.mode}] Docker service '${this.dockerServiceName}' is healthy.`);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
`[${this.name}:${this.mode}] Health check failed for '${this.dockerServiceName}': ${error.message}`,
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
console.log(`[${this.name}:${this.mode}] Skipping health check for '${this.dockerServiceName}'.`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async stop() {
|
|
83
|
+
// The static cleanup method will handle the actual docker compose stop.
|
|
84
|
+
console.log(`[${this.name}:${this.mode}] Relying on orchestrator's static docker compose stop for '${this.dockerServiceName}'.`);
|
|
85
|
+
this.containerName = null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async checkHealth() {
|
|
89
|
+
return await this._checkServiceHealthiness();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_getContainerName() {
|
|
93
|
+
if (this.containerName) {
|
|
94
|
+
return this.containerName;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
// Access processManager statically
|
|
98
|
+
const name = DockerService._processManager.runSync(
|
|
99
|
+
'docker',
|
|
100
|
+
['compose', '-f', this.dockerComposeFile, 'ps', '-a', '-q', this.dockerServiceName],
|
|
101
|
+
);
|
|
102
|
+
if (name) {
|
|
103
|
+
this.containerName = name;
|
|
104
|
+
return name;
|
|
105
|
+
}
|
|
106
|
+
} catch (error) {
|
|
107
|
+
// console.warn(...);
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
_getContainerStatus() {
|
|
113
|
+
const containerName = this._getContainerName();
|
|
114
|
+
if (!containerName) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
// Access processManager statically
|
|
119
|
+
return DockerService._processManager.runSync(
|
|
120
|
+
'docker',
|
|
121
|
+
['container', 'inspect', '-f', '{{.State.Status}}', containerName],
|
|
122
|
+
);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
// console.warn(...);
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async _checkServiceHealthiness(maxAttempts = 30, delayMs = 1000) {
|
|
130
|
+
const containerName = this._getContainerName();
|
|
131
|
+
if (!containerName) {
|
|
132
|
+
throw new Error(`[${this.name}:${this.mode}] Cannot check health: Container for '${this.dockerServiceName}' not found.`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
process.stdout.write(`[${this.name}:${this.mode}] Checking healthiness for '${containerName}'`);
|
|
136
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
137
|
+
if (attempt > 1) {
|
|
138
|
+
process.stdout.write(".");
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
// Access processManager statically
|
|
144
|
+
const healthStatus = DockerService._processManager.runSync(
|
|
145
|
+
'docker',
|
|
146
|
+
['container', 'inspect', '-f', '{{.State.Health.Status}}', containerName],
|
|
147
|
+
{ stdio: 'pipe' }
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
if (healthStatus === 'healthy' || healthStatus === 'none') {
|
|
151
|
+
process.stdout.write(" Healthy!\n");
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (healthStatus === 'starting' || healthStatus === 'unhealthy') {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
process.stdout.write(` Status: ${healthStatus}\n`);
|
|
160
|
+
throw new Error(`[${this.name}:${this.mode}] Container '${containerName}' is in unexpected health state: ${healthStatus}`);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
process.stdout.write(` Error: ${error.message}\n`);
|
|
163
|
+
throw new Error(`[${this.name}:${this.mode}] Failed to check health for '${containerName}': ${error.message}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
process.stdout.write("\n");
|
|
168
|
+
throw new Error(`[${this.name}:${this.mode}] Service '${this.dockerServiceName}' wasn't healthy in time after ${maxAttempts} attempts.`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = { DockerService };
|
package/test.js
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
const { loadConfig } = require('./src/config');
|
|
2
|
+
const { resolveServiceExecutionGraph } = require('./src/dependency-resolver');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const testConfigContent = `
|
|
6
|
+
composeFile: infrastructure/docker-compose.yml
|
|
7
|
+
|
|
8
|
+
services:
|
|
9
|
+
postgres:
|
|
10
|
+
type: docker
|
|
11
|
+
service: postgres
|
|
12
|
+
healthCheck: true
|
|
13
|
+
|
|
14
|
+
api:
|
|
15
|
+
type: hybrid
|
|
16
|
+
defaultMode: dev
|
|
17
|
+
modes:
|
|
18
|
+
dev:
|
|
19
|
+
type: cmd
|
|
20
|
+
commands:
|
|
21
|
+
start: [npx, rollup, -c, -w]
|
|
22
|
+
directory: ./api
|
|
23
|
+
dependencies:
|
|
24
|
+
- postgres
|
|
25
|
+
- { service: frontend, mode: serve }
|
|
26
|
+
|
|
27
|
+
docker:
|
|
28
|
+
type: docker
|
|
29
|
+
service: api
|
|
30
|
+
healthCheck: true
|
|
31
|
+
dependencies:
|
|
32
|
+
- postgres
|
|
33
|
+
|
|
34
|
+
frontend:
|
|
35
|
+
type: hybrid
|
|
36
|
+
defaultMode: dev
|
|
37
|
+
modes:
|
|
38
|
+
dev:
|
|
39
|
+
type: cmd
|
|
40
|
+
commands:
|
|
41
|
+
start: [npx, rollup, -c, -w]
|
|
42
|
+
directory: ./frontend
|
|
43
|
+
dependencies:
|
|
44
|
+
- { service: api, mode: docker }
|
|
45
|
+
|
|
46
|
+
serve:
|
|
47
|
+
type: cmd
|
|
48
|
+
preCommands:
|
|
49
|
+
- [npm, --prefix, frontend, run, build]
|
|
50
|
+
commands:
|
|
51
|
+
start: [node, ./localserver.mjs]
|
|
52
|
+
directory: ./frontend
|
|
53
|
+
dependencies:
|
|
54
|
+
- api
|
|
55
|
+
|
|
56
|
+
presets:
|
|
57
|
+
api:
|
|
58
|
+
services: [api]
|
|
59
|
+
|
|
60
|
+
frontend:
|
|
61
|
+
services: [frontend]
|
|
62
|
+
|
|
63
|
+
all:
|
|
64
|
+
services: [api, frontend]
|
|
65
|
+
|
|
66
|
+
# Example of a preset that would cause a cycle if dependencies were set up naively
|
|
67
|
+
# (and without the \`dev\` modes being peers)
|
|
68
|
+
# cycle-test:
|
|
69
|
+
# services: [api, frontend]
|
|
70
|
+
# modes:
|
|
71
|
+
# api: dev
|
|
72
|
+
# frontend: cycle-dev # Imagine 'cycle-dev' depends on 'api:dev'
|
|
73
|
+
# # To trigger a cycle, you'd need api:dev -> frontend:dev and frontend:dev -> api:dev
|
|
74
|
+
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
const testConfigPath = 'temp-dev-orchestrator.yml';
|
|
78
|
+
fs.writeFileSync(testConfigPath, testConfigContent);
|
|
79
|
+
|
|
80
|
+
console.log('--- Testing Dependency Resolver ---');
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const config = loadConfig(testConfigPath);
|
|
84
|
+
console.log('✅ Config loaded successfully');
|
|
85
|
+
|
|
86
|
+
console.log('\n--- Resolving "api" preset ---');
|
|
87
|
+
const apiExecutionGraph = resolveServiceExecutionGraph(config, 'api');
|
|
88
|
+
console.log('✅ "api" preset resolved successfully.');
|
|
89
|
+
console.log('Dependencies:');
|
|
90
|
+
apiExecutionGraph.dependencies.forEach((node) => {
|
|
91
|
+
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
92
|
+
});
|
|
93
|
+
console.log('Services:');
|
|
94
|
+
apiExecutionGraph.services.forEach((node) => {
|
|
95
|
+
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
console.log('\n--- Resolving "frontend" preset ---');
|
|
99
|
+
const frontendExecutionGraph = resolveServiceExecutionGraph(config, 'frontend');
|
|
100
|
+
console.log('✅ "frontend" preset resolved successfully.');
|
|
101
|
+
console.log('Dependencies:');
|
|
102
|
+
frontendExecutionGraph.dependencies.forEach((node) => {
|
|
103
|
+
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
104
|
+
});
|
|
105
|
+
console.log('Services:');
|
|
106
|
+
frontendExecutionGraph.services.forEach((node) => {
|
|
107
|
+
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
console.log('\n--- Resolving "all" preset ---');
|
|
111
|
+
const allExecutionGraph = resolveServiceExecutionGraph(config, 'all');
|
|
112
|
+
console.log('✅ "all" preset resolved successfully.');
|
|
113
|
+
console.log('Dependencies:');
|
|
114
|
+
allExecutionGraph.dependencies.forEach((node) => {
|
|
115
|
+
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
116
|
+
});
|
|
117
|
+
console.log('Services:');
|
|
118
|
+
allExecutionGraph.services.forEach((node) => {
|
|
119
|
+
console.log(` - ${node.name} (mode: ${node.mode})`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
console.log('\n--- Testing non-existent preset ---');
|
|
123
|
+
try {
|
|
124
|
+
resolveServiceExecutionGraph(config, 'non-existent');
|
|
125
|
+
} catch (error) {
|
|
126
|
+
console.log(`✅ Caught expected error: ${error.message}`);
|
|
127
|
+
}
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('❌ An unexpected error occurred during testing:', error);
|
|
130
|
+
} finally {
|
|
131
|
+
// Clean up the temporary config file
|
|
132
|
+
if (fs.existsSync(testConfigPath)) {
|
|
133
|
+
fs.unlinkSync(testConfigPath);
|
|
134
|
+
console.log(`\nCleaned up temporary config file: ${testConfigPath}`);
|
|
135
|
+
}
|
|
136
|
+
}
|