scripts-orchestrator 1.2.3 → 2.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/README.md CHANGED
@@ -39,13 +39,14 @@ Create a configuration file (default: `scripts-orchestrator.config.js`) that def
39
39
 
40
40
  ```javascript
41
41
  {
42
- command: 'command_name', // The npm script to run
43
- description: 'Description', // Optional description
44
- status: 'enabled', // 'enabled' or 'disabled'
45
- attempts: 1, // Number of retry attempts
46
- dependencies: [], // Array of dependent commands
47
- background: false, // Whether to run in background
48
- health_check: { // Health check configuration
42
+ command: 'command_name', // The npm script to run
43
+ description: 'Description', // Optional description
44
+ status: 'enabled', // 'enabled' or 'disabled'
45
+ attempts: 1, // Number of retry attempts
46
+ dependencies: [], // Array of dependent commands
47
+ background: false, // Whether to run in background
48
+ kill_command: 'kill_storybook', // Optional kill command to kill the process
49
+ health_check: { // Health check configuration
49
50
  url: 'http://localhost:port',
50
51
  max_attempts: 20,
51
52
  interval: 2000
@@ -88,112 +89,48 @@ export default [
88
89
  ];
89
90
  ```
90
91
 
91
- ### Storybook Testing with Background Process
92
+ ### Basic Build and Test Pipeline with Phases
92
93
  ```javascript
93
- export default [
94
- {
95
- command: 'test-storybook',
96
- description: 'Run Storybook tests',
97
- status: 'enabled',
98
- attempts: 2,
99
- dependencies: [
100
- {
101
- command: 'storybook_silent',
102
- background: true,
103
- wait: 5000,
104
- health_check: {
105
- url: 'http://localhost:6006',
106
- max_attempts: 20,
107
- interval: 2000
94
+ export default {
95
+ phases: [
96
+ {
97
+ name: 'build',
98
+ parallel: [
99
+ {
100
+ command: 'build',
101
+ description: 'Build the project',
102
+ status: 'enabled',
103
+ attempts: 1
108
104
  }
109
- }
110
- ]
111
- }
112
- ];
113
- ```
114
-
115
- ### Playwright Testing with Development Server
116
- ```javascript
117
- export default [
118
- {
119
- command: 'playwright_ci',
120
- description: 'Run Playwright tests',
121
- status: 'enabled',
122
- attempts: 1,
123
- dependencies: [
124
- {
125
- command: 'dev',
126
- background: true,
127
- health_check: {
128
- url: 'http://localhost:5173',
129
- max_attempts: 20,
130
- interval: 2000
105
+ ]
106
+ },
107
+ {
108
+ name: 'test',
109
+ parallel: [
110
+ {
111
+ command: 'test',
112
+ description: 'Run unit tests',
113
+ status: 'enabled',
114
+ attempts: 2,
115
+ should_retry: (output) => {
116
+ // Only retry if there are actual test failures
117
+ const testSummaryMatch = output.match(/Test Suites:.*?(\d+) failed/);
118
+ return testSummaryMatch && parseInt(testSummaryMatch[1]) > 0;
119
+ }
120
+ },
121
+ {
122
+ command: 'lint',
123
+ description: 'Run lint checks',
124
+ status: 'enabled'
131
125
  }
132
- }
133
- ]
134
- }
135
- ];
136
- ```
137
-
138
- ### Full CI Pipeline with Multiple Checks
139
- ```javascript
140
- export default [
141
- {
142
- command: 'build',
143
- description: 'Build the project',
144
- status: 'enabled',
145
- attempts: 1
146
- },
147
- {
148
- command: 'test-ci',
149
- description: 'Run unit tests',
150
- status: 'enabled',
151
- attempts: 2,
152
- should_retry: (output) => {
153
- const testSummaryMatch = output.match(/Test Suites:.*?(\d+) failed/);
154
- const hasTestFailures = testSummaryMatch && parseInt(testSummaryMatch[1]) > 0;
155
- const hasCoverageFailures = output.match(/Jest: "global" coverage threshold/);
156
-
157
- // Only retry for actual test failures, not coverage issues
158
- return hasTestFailures;
126
+ ]
159
127
  }
160
- },
161
- {
162
- command: 'test-storybook',
163
- description: 'Run Storybook tests',
164
- status: 'enabled',
165
- attempts: 2,
166
- dependencies: [
167
- {
168
- command: 'storybook_silent',
169
- background: true,
170
- wait: 5000,
171
- health_check: {
172
- url: 'http://localhost:6006',
173
- max_attempts: 20,
174
- interval: 2000
175
- }
176
- }
177
- ]
178
- },
179
- {
180
- command: 'stylelint',
181
- description: 'Run stylelint checks',
182
- status: 'enabled'
183
- },
184
- {
185
- command: 'lint',
186
- description: 'Run lint checks',
187
- status: 'enabled'
188
- },
189
- {
190
- command: 'jscpd',
191
- description: 'Run code duplication checks',
192
- status: 'enabled'
193
- }
194
- ];
128
+ ]
129
+ };
195
130
  ```
196
131
 
132
+ See more examples [here](./docs/samples.md)
133
+
197
134
  ## Command Types
198
135
 
199
136
  The orchestrator is completely agnostic to what commands it runs. It can execute any npm scripts. Common use cases include:
@@ -248,6 +185,17 @@ The orchestrator doesn't care what the commands do - it just ensures they run (i
248
185
  - `0`: All commands executed successfully
249
186
  - `1`: One or more commands failed or were skipped
250
187
 
188
+
189
+ ## History
190
+ See [versions](./docs/versions.md)
191
+
192
+ ## Roadmap
193
+ - Better UX to indicate what is happening
194
+ - Tests to avoid regression
195
+ - Retry should append to the log file
196
+ - Run any shell command rather than assume the command is specified in package.json (? tentative)
197
+
198
+
251
199
  ## Disclaimer
252
200
 
253
201
  This software is provided "as is", without warranty of any kind, express or implied. The author(s) shall not be liable for any claims, damages, or other liabilities arising from the use of this software. Users are responsible for testing and verifying the software in their own environment before using it in production.
@@ -258,3 +206,5 @@ Contributions are welcome! Please feel free to submit a Pull Request.
258
206
 
259
207
  ## License
260
208
  This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
209
+
210
+
@@ -11,6 +11,22 @@ export class Orchestrator {
11
11
  this.failedCommands = [];
12
12
  this.skippedCommands = [];
13
13
  this.commandTimings = new Map();
14
+
15
+ // Flatten commands for easier tracking
16
+ this.allCommands = this.flattenCommands(config);
17
+ }
18
+
19
+ flattenCommands(config) {
20
+ // Handle both old array format and new phases format
21
+ if (Array.isArray(config)) {
22
+ return config;
23
+ }
24
+
25
+ if (config.phases) {
26
+ return config.phases.flatMap(phase => phase.parallel || []);
27
+ }
28
+
29
+ return [];
14
30
  }
15
31
 
16
32
  formatDuration(ms) {
@@ -36,6 +52,7 @@ export class Orchestrator {
36
52
  should_retry,
37
53
  process_tracking = false,
38
54
  health_check,
55
+ kill_command,
39
56
  } = commandConfig;
40
57
 
41
58
  const startTime = Date.now();
@@ -71,6 +88,7 @@ export class Orchestrator {
71
88
  url: checkUrl,
72
89
  startedByScript: false,
73
90
  process_tracking,
91
+ kill_command,
74
92
  });
75
93
  this.commandTimings.set(command, Date.now() - startTime);
76
94
  visited.delete(command);
@@ -128,12 +146,14 @@ export class Orchestrator {
128
146
  await new Promise((resolve) => setTimeout(resolve, 1000));
129
147
  }
130
148
 
131
- const { success, output } = await this.processManager.runCommand(
132
- attempt === 1 ? command : retry_command || command,
149
+ const { success, output } = await this.processManager.runCommand({
150
+ cmd: attempt === 1 ? command : retry_command || command,
133
151
  logFile,
134
152
  background,
135
- health_check,
136
- );
153
+ healthCheck: health_check,
154
+ kill_command,
155
+ isRetry: attempt > 1,
156
+ });
137
157
  commandOutput = output;
138
158
  result = success;
139
159
 
@@ -170,7 +190,7 @@ export class Orchestrator {
170
190
  this.logger.info('\nCommand Summary:');
171
191
  let hasFailures = false;
172
192
 
173
- this.config.forEach(({ command }) => {
193
+ this.allCommands.forEach(({ command }) => {
174
194
  const duration = this.commandTimings.get(command);
175
195
  const durationStr = duration ? ` (${this.formatDuration(duration)})` : '';
176
196
 
@@ -194,18 +214,42 @@ export class Orchestrator {
194
214
 
195
215
  async run() {
196
216
  try {
197
- // Run top-level commands in parallel
198
- const tasks = this.config.map((commandConfig) =>
199
- this.executeCommand(commandConfig),
200
- );
217
+ let hasFailures = false;
201
218
 
202
- // Wait for all top-level commands to complete
203
- const results = await Promise.all(tasks);
204
-
205
- // Check if any command failed
206
- const hasFailures = results.some(result => !result) ||
207
- this.failedCommands.length > 0 ||
208
- this.skippedCommands.length > 0;
219
+ // Handle both old array format and new phases format
220
+ if (Array.isArray(this.config)) {
221
+ // Legacy: Run all commands in parallel
222
+ const tasks = this.config.map((commandConfig) =>
223
+ this.executeCommand(commandConfig),
224
+ );
225
+ const results = await Promise.all(tasks);
226
+ hasFailures = results.some(result => !result);
227
+ } else if (this.config.phases) {
228
+ // New: Run phases sequentially, commands within phases in parallel
229
+ for (const phase of this.config.phases) {
230
+ this.logger.info(`\n🔄 Starting phase: ${phase.name}`);
231
+
232
+ const tasks = phase.parallel.map((commandConfig) =>
233
+ this.executeCommand(commandConfig),
234
+ );
235
+
236
+ const results = await Promise.all(tasks);
237
+ const phaseHasFailures = results.some(result => !result);
238
+
239
+ if (phaseHasFailures) {
240
+ hasFailures = true;
241
+ this.logger.error(`❌ Phase "${phase.name}" completed with failures`);
242
+ break; // Stop executing remaining phases on failure
243
+ } else {
244
+ this.logger.success(`✅ Phase "${phase.name}" completed successfully`);
245
+ }
246
+ }
247
+ }
248
+
249
+ // Check final status
250
+ hasFailures = hasFailures ||
251
+ this.failedCommands.length > 0 ||
252
+ this.skippedCommands.length > 0;
209
253
 
210
254
  // Add a small delay to ensure all processes have finished
211
255
  await new Promise((resolve) => setTimeout(resolve, 1000));
@@ -11,17 +11,18 @@ export class ProcessManager {
11
11
  this.backgroundProcessesDetails = [];
12
12
  }
13
13
 
14
- addBackgroundProcess({ command, url, startedByScript, process_tracking }) {
14
+ addBackgroundProcess({ command, url, startedByScript, process_tracking, kill_command }) {
15
15
  this.logger.verbose(`Adding background process: ${command} (${url})`);
16
16
  this.backgroundProcessesDetails.push({
17
17
  command,
18
18
  url,
19
19
  startedByScript,
20
20
  process_tracking,
21
+ kill_command,
21
22
  });
22
23
  }
23
24
 
24
- async runCommand(cmd, logFile, background = false, healthCheck = null) {
25
+ async runCommand({ cmd, logFile, background = false, healthCheck = null, kill_command = null, isRetry = false }) {
25
26
  const LOGS_DIR = path.resolve(process.cwd(), 'scripts-orchestrator-logs');
26
27
  const LOG_FILE = logFile || path.join(LOGS_DIR, `${cmd}.log`);
27
28
 
@@ -31,8 +32,12 @@ export class ProcessManager {
31
32
  fs.mkdirSync(LOGS_DIR, { recursive: true });
32
33
  }
33
34
 
34
- this.logger.verbose(`Clearing log file at ${LOG_FILE}`);
35
- fs.writeFileSync(LOG_FILE, ''); // Clear the log file
35
+ if (!isRetry) {
36
+ this.logger.verbose(`Clearing log file at ${LOG_FILE}`);
37
+ fs.writeFileSync(LOG_FILE, ''); // Clear the log file
38
+ } else {
39
+ this.logger.verbose(`Appending to existing log file at ${LOG_FILE} (retry attempt)`);
40
+ }
36
41
  } catch (error) {
37
42
  this.logger.error(`Failed to setup log file: ${error.message}`);
38
43
  return Promise.resolve({ success: false, output: '' });
@@ -40,15 +45,16 @@ export class ProcessManager {
40
45
 
41
46
  return new Promise((resolve) => {
42
47
  this.logger.info(`Running: npm run ${cmd}`);
48
+
49
+ // Create isolated environment for each process
50
+ const isolatedEnv = this.createIsolatedEnvironment({ command: cmd });
51
+
43
52
  const options = {
44
53
  shell: true,
45
54
  detached: background,
46
55
  stdio: ['ignore', 'pipe', 'pipe'],
47
56
  cwd: process.cwd(),
48
- env: {
49
- ...process.env,
50
- NODE_ENV: process.env.NODE_ENV || 'development',
51
- },
57
+ env: isolatedEnv,
52
58
  windowsHide: true,
53
59
  ...(background ? { processGroup: true } : {}),
54
60
  };
@@ -102,6 +108,7 @@ export class ProcessManager {
102
108
  startTime: Date.now(),
103
109
  url: healthCheck?.url,
104
110
  startedByScript: true,
111
+ kill_command,
105
112
  });
106
113
 
107
114
  this.logger.verbose(`Unreferencing process ${processGroupId}`);
@@ -171,10 +178,46 @@ export class ProcessManager {
171
178
  });
172
179
  }
173
180
 
181
+ createIsolatedEnvironment({ command }) {
182
+ // Create a deep copy to avoid any reference sharing
183
+ const baseEnv = JSON.parse(JSON.stringify(process.env));
184
+
185
+ // Set standard environment variables
186
+ const isolatedEnv = {
187
+ ...baseEnv,
188
+ NODE_ENV: process.env.NODE_ENV || 'development',
189
+ // Add command-specific environment isolation
190
+ SCRIPTS_ORCHESTRATOR_COMMAND: command,
191
+ SCRIPTS_ORCHESTRATOR_PID: process.pid.toString(),
192
+ // Force fresh PATH to avoid any dynamic modifications
193
+ PATH: process.env.PATH,
194
+ // Ensure npm/node paths are isolated
195
+ npm_config_cache: path.join(process.cwd(), 'node_modules/.cache/npm'),
196
+ // Prevent npm from sharing config between parallel processes
197
+ npm_config_progress: 'false',
198
+ npm_config_loglevel: 'error',
199
+ };
200
+
201
+ // Remove any potentially problematic environment variables
202
+ delete isolatedEnv.npm_lifecycle_event;
203
+ delete isolatedEnv.npm_lifecycle_script;
204
+
205
+ return isolatedEnv;
206
+ }
207
+
174
208
  async cleanup() {
175
209
  this.logger.info('\nCleaning up background processes...');
210
+
211
+ // Debug: Log the number of processes we're tracking
212
+ this.logger.info(`- Found ${this.backgroundProcessesDetails.length} background processes to clean up`);
213
+
214
+ // Debug: Log each process details
215
+ this.backgroundProcessesDetails.forEach(({ command, pgid, url, startedByScript, kill_command }, index) => {
216
+ this.logger.verbose(`- Process ${index + 1}: command=${command}, pgid=${pgid}, url=${url}, startedByScript=${startedByScript}, kill_command=${kill_command}`);
217
+ });
218
+
176
219
  const killPromises = this.backgroundProcessesDetails.map(
177
- async ({ command, pgid, url, startedByScript }) => {
220
+ async ({ command, pgid, url, startedByScript, kill_command }) => {
178
221
  if (!startedByScript) {
179
222
  this.logger.verbose(
180
223
  `- Skipping cleanup for ${command} (${url}) as it was not started by this script`,
@@ -182,6 +225,26 @@ export class ProcessManager {
182
225
  return;
183
226
  }
184
227
 
228
+ this.logger.verbose(`- Processing cleanup for ${command} (kill_command: ${kill_command})`);
229
+
230
+ // Try custom kill command first if specified
231
+ if (kill_command) {
232
+ try {
233
+ this.logger.verbose(`- Using custom kill command: npm run ${kill_command}`);
234
+ const result = await this.runCommand({ cmd: kill_command, logFile: null, background: false });
235
+ if (result.success) {
236
+ this.logger.verbose(`- Successfully killed ${command} using custom command`);
237
+ return;
238
+ } else {
239
+ this.logger.verbose('- Custom kill command failed, falling back to process signals');
240
+ }
241
+ } catch (error) {
242
+ this.logger.verbose(`- Custom kill command error: ${error.message}, falling back`);
243
+ }
244
+ } else {
245
+ this.logger.verbose(`- No kill_command specified for ${command}, using process signals`);
246
+ }
247
+
185
248
  try {
186
249
  // First try to kill the process group
187
250
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scripts-orchestrator",
3
- "version": "1.2.3",
3
+ "version": "2.1.0",
4
4
  "description": "A powerful script orchestrator for running parallel commands with dependency management, background processes, and health checks",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",
@@ -8,7 +8,8 @@
8
8
  "scripts-orchestrator": "index.js"
9
9
  },
10
10
  "scripts": {
11
- "test": "echo \"Error: no test specified\" && exit 1",
11
+ "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
12
+ "test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage --detectOpenHandles --runInBand",
12
13
  "start": "node index.js",
13
14
  "lint": "eslint .",
14
15
  "prepare": "npm run lint"
@@ -29,7 +30,7 @@
29
30
  "license": "MIT",
30
31
  "repository": {
31
32
  "type": "git",
32
- "url": "git+https://github.com/Pratishthan/scripts-orchestrator"
33
+ "url": "git+https://github.com/Pratishthan/scripts-orchestrator.git"
33
34
  },
34
35
  "bugs": {
35
36
  "url": "https://github.com/Pratishthan/scripts-orchestrator/issues"
@@ -39,10 +40,14 @@
39
40
  "node": ">=14.0.0"
40
41
  },
41
42
  "dependencies": {
42
- "chalk": "^4.1.2"
43
+ "chalk": "^4.1.2",
44
+ "yargs": "^17.7.2"
43
45
  },
44
46
  "devDependencies": {
45
- "eslint": "^8.0.0"
47
+ "@types/jest": "^29.5.14",
48
+ "eslint": "^8.0.0",
49
+ "jest": "^29.7.0",
50
+ "ts-jest": "^29.3.3"
46
51
  },
47
52
  "files": [
48
53
  "lib/",
@@ -1,83 +1,115 @@
1
- export default [
2
- {
3
- command: 'build',
4
- description: 'Build the project',
5
- status: 'enabled',
6
- attempts: 1,
7
- },
8
- {
9
- command: 'test-ci',
10
- description: 'Run unit tests',
11
- status: 'enabled',
12
- attempts: 2,
13
- should_retry: (output) => {
14
- // Check for actual test failures in the summary
15
- const testSummaryMatch = output.match(/Test Suites:.*?(\d+) failed/);
16
- const hasTestFailures =
17
- testSummaryMatch && parseInt(testSummaryMatch[1]) > 0;
1
+ export default {
2
+ phases: [
3
+ {
4
+ name: 'build',
5
+ parallel: [
6
+ {
7
+ command: 'build',
8
+ description: 'Build the project',
9
+ status: 'enabled',
10
+ attempts: 1,
11
+ },
12
+ {
13
+ command: 'stylelint',
14
+ description: 'Run stylelint checks',
15
+ status: 'enabled',
16
+ },
17
+ { command: 'lint', description: 'Run lint checks', status: 'enabled' },
18
+ {
19
+ command: 'jscpd',
20
+ description: 'Run code duplication checks',
21
+ status: 'enabled',
22
+ },
23
+ ],
24
+ },
25
+ {
26
+ name: 'storybook tests',
27
+ parallel: [
28
+ {
29
+ command: 'test-storybook',
30
+ description: 'Run Storybook tests',
31
+ status: 'enabled',
32
+ attempts: 2,
33
+ dependencies: [
34
+ {
35
+ command: 'storybook_silent',
36
+ background: true,
37
+ wait: 5000,
38
+ kill_command: 'kill_storybook',
39
+ dependencies: [],
40
+ // Add process tracking
41
+ process_tracking: true,
42
+ // Add health check
43
+ health_check: {
44
+ url: 'http://localhost:6006',
45
+ max_attempts: 20,
46
+ interval: 2000,
47
+ },
48
+ },
49
+ ],
50
+ },
51
+ ],
52
+ },
53
+ {
54
+ name: 'unit tests',
55
+ parallel: [
56
+ {
57
+ command: 'test-ci',
58
+ description: 'Run unit tests',
59
+ status: 'enabled',
60
+ attempts: 2,
61
+ should_retry: (output) => {
62
+ // Check for test failures in both formats
63
+ const testSuiteFailureMatch = output.match(
64
+ /Test Suites:.*?\(\d+\) failed/,
65
+ );
66
+ const individualTestFailureMatch =
67
+ output.match(/✘\s*(\d+)\s*failing/);
68
+
69
+ const hasTestSuiteFailures =
70
+ testSuiteFailureMatch && parseInt(testSuiteFailureMatch[1]) > 0;
71
+ const hasIndividualTestFailures =
72
+ individualTestFailureMatch &&
73
+ parseInt(individualTestFailureMatch[1]) > 0;
74
+
75
+ const hasTestFailures =
76
+ hasTestSuiteFailures || hasIndividualTestFailures;
18
77
 
19
- // Check for coverage failures
20
- const coverageSummaryMatch = output.match(
21
- /Jest: "global" coverage threshold/,
22
- );
23
- const hasCoverageFailures = coverageSummaryMatch !== null;
78
+ // Check for "Test suite failed to run" in logs
79
+ if (output.includes('Test suite failed to run')) {
80
+ console.error('Certain tests could not be run');
81
+ return false; // Don't retry if certain tests could not be run
82
+ }
24
83
 
25
- if (!hasTestFailures && hasCoverageFailures) {
26
- console.log(
27
- 'Tests have passed but coverage thresholds have not been met',
28
- );
29
- return false; // Don't retry if only coverage failed
30
- }
84
+ if (!hasTestFailures) {
85
+ console.log(
86
+ 'Tests have passed but coverage thresholds have not been met',
87
+ );
88
+ return false; // Don't retry if only coverage failed
89
+ }
31
90
 
32
- return hasTestFailures; // Only retry if there are actual test failures
91
+ return hasTestFailures; // Only retry if there are actual test failures
92
+ },
93
+ },
94
+ ],
33
95
  },
34
- },
35
- {
36
- command: 'test-storybook',
37
- description: 'Run Storybook tests',
38
- status: 'enabled',
39
- attempts: 2,
40
- dependencies: [
41
- {
42
- command: 'storybook_silent',
43
- background: true,
44
- wait: 5000,
45
- kill_script: 'kill_storybook',
46
- dependencies: [],
47
- // Add process tracking
48
- process_tracking: true,
49
- // Add health check
50
- health_check: {
51
- url: 'http://localhost:6006',
52
- max_attempts: 20,
53
- interval: 2000,
96
+ {
97
+ name: 'playwright',
98
+ parallel: [
99
+ {
100
+ command: 'playwright_ci',
101
+ description: 'Run Playwright tests',
102
+ status: 'enabled',
103
+ attempts: 1, //Playwright internally retries in CI mode
104
+ dependencies: [
105
+ {
106
+ command: 'dev',
107
+ background: true,
108
+ url: 'http://localhost:5173',
109
+ },
110
+ ],
54
111
  },
55
- },
56
- ],
57
- },
58
- {
59
- command: 'stylelint',
60
- description: 'Run stylelint checks',
61
- status: 'enabled',
62
- },
63
- { command: 'lint', description: 'Run lint checks', status: 'enabled' },
64
- {
65
- command: 'jscpd',
66
- description: 'Run code duplication checks',
67
- status: 'enabled',
68
- },
69
- {
70
- command: 'playwright_ci',
71
- description: 'Run Playwright tests',
72
- status: 'enabled',
73
- attempts: 1, //Playwright internally retries in CI mode
74
- dependencies: [
75
- {
76
- command: 'dev',
77
- background: true,
78
- url: 'http://localhost:5173',
79
- kill: 'application_end',
80
- },
81
- ],
82
- },
83
- ];
112
+ ],
113
+ },
114
+ ],
115
+ };