@runium/cli 0.0.1
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/.eslintrc.json +31 -0
- package/.prettierrc.json +10 -0
- package/README.md +3 -0
- package/build.js +104 -0
- package/lib/app.js +6 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/plugin/plugin-add.js +1 -0
- package/lib/commands/plugin/plugin-command.js +1 -0
- package/lib/commands/plugin/plugin-disable.js +1 -0
- package/lib/commands/plugin/plugin-enable.js +1 -0
- package/lib/commands/plugin/plugin-list.js +1 -0
- package/lib/commands/plugin/plugin-remove.js +1 -0
- package/lib/commands/plugin/plugin.js +1 -0
- package/lib/commands/project/project-add.js +1 -0
- package/lib/commands/project/project-command.js +1 -0
- package/lib/commands/project/project-list.js +1 -0
- package/lib/commands/project/project-remove.js +1 -0
- package/lib/commands/project/project-start.js +1 -0
- package/lib/commands/project/project-state-command.js +1 -0
- package/lib/commands/project/project-status.js +1 -0
- package/lib/commands/project/project-stop.js +1 -0
- package/lib/commands/project/project-validate.js +1 -0
- package/lib/commands/project/project.js +1 -0
- package/lib/commands/runium-command.js +1 -0
- package/lib/constants/error-code.js +1 -0
- package/lib/constants/index.js +1 -0
- package/lib/index.js +2 -0
- package/lib/macros/conditional.js +1 -0
- package/lib/macros/empty.js +1 -0
- package/lib/macros/env.js +1 -0
- package/lib/macros/index.js +1 -0
- package/lib/macros/path.js +1 -0
- package/lib/package.json +21 -0
- package/lib/services/config.js +1 -0
- package/lib/services/index.js +1 -0
- package/lib/services/output.js +3 -0
- package/lib/services/plugin-context.js +1 -0
- package/lib/services/plugin.js +1 -0
- package/lib/services/profile.js +1 -0
- package/lib/services/project.js +1 -0
- package/lib/services/shutdown.js +1 -0
- package/lib/utils/convert-path-to-valid-file-name.js +1 -0
- package/lib/utils/debounce.js +1 -0
- package/lib/utils/format-timestamp.js +1 -0
- package/lib/utils/index.js +1 -0
- package/package.json +44 -0
- package/src/app.ts +175 -0
- package/src/commands/index.ts +2 -0
- package/src/commands/plugin/plugin-add.ts +48 -0
- package/src/commands/plugin/plugin-command.ts +36 -0
- package/src/commands/plugin/plugin-disable.ts +46 -0
- package/src/commands/plugin/plugin-enable.ts +50 -0
- package/src/commands/plugin/plugin-list.ts +61 -0
- package/src/commands/plugin/plugin-remove.ts +42 -0
- package/src/commands/plugin/plugin.ts +41 -0
- package/src/commands/project/project-add.ts +46 -0
- package/src/commands/project/project-command.ts +36 -0
- package/src/commands/project/project-list.ts +32 -0
- package/src/commands/project/project-remove.ts +41 -0
- package/src/commands/project/project-start.ts +152 -0
- package/src/commands/project/project-state-command.ts +68 -0
- package/src/commands/project/project-status.ts +103 -0
- package/src/commands/project/project-stop.ts +55 -0
- package/src/commands/project/project-validate.ts +43 -0
- package/src/commands/project/project.ts +45 -0
- package/src/commands/runium-command.ts +50 -0
- package/src/constants/error-code.ts +15 -0
- package/src/constants/index.ts +1 -0
- package/src/index.ts +21 -0
- package/src/macros/conditional.ts +31 -0
- package/src/macros/empty.ts +6 -0
- package/src/macros/env.ts +8 -0
- package/src/macros/index.ts +12 -0
- package/src/macros/path.ts +9 -0
- package/src/services/config.ts +76 -0
- package/src/services/index.ts +7 -0
- package/src/services/output.ts +201 -0
- package/src/services/plugin-context.ts +81 -0
- package/src/services/plugin.ts +144 -0
- package/src/services/profile.ts +211 -0
- package/src/services/project.ts +114 -0
- package/src/services/shutdown.ts +130 -0
- package/src/utils/convert-path-to-valid-file-name.ts +39 -0
- package/src/utils/debounce.ts +23 -0
- package/src/utils/format-timestamp.ts +17 -0
- package/src/utils/index.ts +3 -0
- package/tsconfig.json +40 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { Inject, Service } from 'typedi';
|
|
5
|
+
import { applyMacros, Project, ProjectConfig, RuniumError } from '@runium/core';
|
|
6
|
+
import { ErrorCode } from '@constants';
|
|
7
|
+
import { macros } from '@macros';
|
|
8
|
+
import { PluginProjectDefinition, PluginService } from '@services';
|
|
9
|
+
|
|
10
|
+
@Service()
|
|
11
|
+
export class ProjectService {
|
|
12
|
+
constructor(@Inject() private pluginService: PluginService) {}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Init project
|
|
16
|
+
* @param path
|
|
17
|
+
*/
|
|
18
|
+
async initProject(path: string): Promise<Project> {
|
|
19
|
+
const content = await this.readFile(path);
|
|
20
|
+
const projectContent = this.applyMacros(content);
|
|
21
|
+
const projectData = this.parseProjectContent(projectContent, path);
|
|
22
|
+
|
|
23
|
+
const project = new Project(projectData);
|
|
24
|
+
return this.extendProjectWithPlugins(project);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Resolve path
|
|
29
|
+
* @param path
|
|
30
|
+
*/
|
|
31
|
+
resolvePath(path: string): string {
|
|
32
|
+
return resolve(path);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read project file
|
|
37
|
+
* @param path
|
|
38
|
+
*/
|
|
39
|
+
private async readFile(path: string): Promise<string> {
|
|
40
|
+
if (!existsSync(path)) {
|
|
41
|
+
throw new RuniumError(
|
|
42
|
+
`Project file "${path}" does not exist`,
|
|
43
|
+
ErrorCode.PROJECT_FILE_NOT_FOUND,
|
|
44
|
+
{ path }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return readFile(path, { encoding: 'utf-8' });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new RuniumError(
|
|
51
|
+
`Failed to read project file "${path}"`,
|
|
52
|
+
ErrorCode.PROJECT_FILE_CAN_NOT_READ,
|
|
53
|
+
{ path, original: error }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Apply macros to text
|
|
60
|
+
* @param text
|
|
61
|
+
*/
|
|
62
|
+
private applyMacros(text: string): string {
|
|
63
|
+
const plugins = this.pluginService.getAllPlugins();
|
|
64
|
+
const pluginMacros = plugins.reduce(
|
|
65
|
+
(acc, plugin) => ({ ...acc, ...(plugin.project?.macros || {}) }),
|
|
66
|
+
{}
|
|
67
|
+
);
|
|
68
|
+
return applyMacros(text, { ...pluginMacros, ...macros });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Parse project content
|
|
73
|
+
* @param content
|
|
74
|
+
* @param path
|
|
75
|
+
*/
|
|
76
|
+
private parseProjectContent(content: string, path: string): ProjectConfig {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(content);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
throw new RuniumError(
|
|
81
|
+
`Failed to parse project "${path}"`,
|
|
82
|
+
ErrorCode.PROJECT_JSON_PARSE_ERROR,
|
|
83
|
+
{ path, original: error }
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Extends the project with plugins
|
|
90
|
+
* @param project
|
|
91
|
+
*/
|
|
92
|
+
private extendProjectWithPlugins(project: Project): Project {
|
|
93
|
+
const plugins = this.pluginService.getAllPlugins();
|
|
94
|
+
|
|
95
|
+
plugins.forEach(plugin => {
|
|
96
|
+
const { tasks, actions, triggers, validationSchema } = (plugin.project ||
|
|
97
|
+
{}) as PluginProjectDefinition;
|
|
98
|
+
Object.entries(actions || {}).forEach(([type, processor]) => {
|
|
99
|
+
project.registerAction(type, processor);
|
|
100
|
+
});
|
|
101
|
+
Object.entries(tasks || {}).forEach(([type, processor]) => {
|
|
102
|
+
project.registerTask(type, processor);
|
|
103
|
+
});
|
|
104
|
+
Object.entries(triggers || {}).forEach(([type, processor]) => {
|
|
105
|
+
project.registerTrigger(type, processor);
|
|
106
|
+
});
|
|
107
|
+
if (validationSchema) {
|
|
108
|
+
project.extendValidationSchema(validationSchema);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
return project;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Inject, Service } from 'typedi';
|
|
2
|
+
import { OutputService } from '@services';
|
|
3
|
+
|
|
4
|
+
type ShutdownBlocker = () => Promise<void> | void;
|
|
5
|
+
|
|
6
|
+
const TIMEOUT = 30000;
|
|
7
|
+
const EXIT_DELAY = 250;
|
|
8
|
+
const SIGNALS: NodeJS.Signals[] = ['SIGHUP', 'SIGINT', 'SIGTERM', 'SIGQUIT'];
|
|
9
|
+
|
|
10
|
+
@Service()
|
|
11
|
+
export class ShutdownService {
|
|
12
|
+
private blockers: Set<ShutdownBlocker> = new Set();
|
|
13
|
+
private isShuttingDown = false;
|
|
14
|
+
|
|
15
|
+
constructor(@Inject() private outputService: OutputService) {}
|
|
16
|
+
/**
|
|
17
|
+
* Initialize shutdown handlers
|
|
18
|
+
*/
|
|
19
|
+
async init(): Promise<void> {
|
|
20
|
+
SIGNALS.forEach(signal => {
|
|
21
|
+
process.on(signal, () => {
|
|
22
|
+
this.shutdown(signal).catch(error => {
|
|
23
|
+
this.outputService.error('Error during shutdown: %s', error.message);
|
|
24
|
+
this.outputService.debug('Error details:', error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
process.on('uncaughtException', error => {
|
|
31
|
+
this.outputService.error('Uncaught exception: %s', error.message);
|
|
32
|
+
this.outputService.debug('Error details:', error);
|
|
33
|
+
this.shutdown('uncaughtException').catch(() => {
|
|
34
|
+
process.exit(1);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
39
|
+
this.outputService.error(
|
|
40
|
+
'Unhandled rejection at:',
|
|
41
|
+
promise,
|
|
42
|
+
'reason:',
|
|
43
|
+
reason
|
|
44
|
+
);
|
|
45
|
+
this.outputService.debug('Error details:', { reason, promise });
|
|
46
|
+
this.shutdown('unhandledRejection').catch(() => {
|
|
47
|
+
process.exit(1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
process.on('beforeExit', () => {
|
|
52
|
+
if (!this.isShuttingDown) {
|
|
53
|
+
this.shutdown('exit').catch(() => {
|
|
54
|
+
process.exit(1);
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add a shutdown blocker function
|
|
62
|
+
* @param blocker
|
|
63
|
+
*/
|
|
64
|
+
addBlocker(blocker: ShutdownBlocker): void {
|
|
65
|
+
this.blockers.add(blocker);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Remove a shutdown blocker
|
|
70
|
+
* @param blocker
|
|
71
|
+
*/
|
|
72
|
+
removeBlocker(blocker: ShutdownBlocker): boolean {
|
|
73
|
+
return this.blockers.delete(blocker);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute graceful shutdown
|
|
78
|
+
* @param reason
|
|
79
|
+
*/
|
|
80
|
+
async shutdown(reason?: string): Promise<void> {
|
|
81
|
+
if (this.isShuttingDown || !reason) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.isShuttingDown = true;
|
|
86
|
+
|
|
87
|
+
const exitProcess = (code: number): void => {
|
|
88
|
+
setTimeout(() => {
|
|
89
|
+
process.exit(code);
|
|
90
|
+
}, EXIT_DELAY);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
if (this.blockers.size === 0) {
|
|
94
|
+
exitProcess(0);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await this.executeBlockersWithTimeout();
|
|
100
|
+
exitProcess(0);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
exitProcess(1);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Execute all blockers with timeout
|
|
108
|
+
*/
|
|
109
|
+
private async executeBlockersWithTimeout(): Promise<void> {
|
|
110
|
+
const blockerPromises = Array.from(this.blockers).map(blocker =>
|
|
111
|
+
this.executeBlocker(blocker)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
115
|
+
setTimeout(() => {
|
|
116
|
+
reject(new Error(`Shutdown timeout after ${TIMEOUT}ms`));
|
|
117
|
+
}, TIMEOUT);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await Promise.race([Promise.allSettled(blockerPromises), timeoutPromise]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Execute a single blocker with error handling
|
|
125
|
+
* @param blocker
|
|
126
|
+
*/
|
|
127
|
+
private async executeBlocker(blocker: ShutdownBlocker): Promise<void> {
|
|
128
|
+
await blocker();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const MAX_FILENAME_LENGTH = 200;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Converts a file path to a valid filename by replacing invalid characters
|
|
5
|
+
* @param filePath
|
|
6
|
+
* @param replacement
|
|
7
|
+
*/
|
|
8
|
+
export function convertPathToValidFileName(
|
|
9
|
+
filePath: string,
|
|
10
|
+
replacement: string = '_'
|
|
11
|
+
): string {
|
|
12
|
+
let filename = filePath.trim();
|
|
13
|
+
|
|
14
|
+
// replace path separators (both forward and backward slashes)
|
|
15
|
+
filename = filename.replace(/^\/+/, '');
|
|
16
|
+
filename = filename.replace(/[/\\]/g, replacement);
|
|
17
|
+
|
|
18
|
+
// replace invalid filename characters
|
|
19
|
+
// Windows: < > : " | ? * and control characters (0-31)
|
|
20
|
+
// also replacing common problematic characters
|
|
21
|
+
// eslint-disable-next-line no-control-regex
|
|
22
|
+
filename = filename.replace(/[<>:"|?*\x00-\x1F]/g, replacement);
|
|
23
|
+
|
|
24
|
+
// remove leading/trailing dots and spaces (problematic on Windows)
|
|
25
|
+
filename = filename.replace(/^[.\s]+|[.\s]+$/g, '');
|
|
26
|
+
|
|
27
|
+
// replace multiple consecutive replacement characters with a single one
|
|
28
|
+
const replacementRegex = new RegExp(
|
|
29
|
+
`${replacement.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}+`,
|
|
30
|
+
'g'
|
|
31
|
+
);
|
|
32
|
+
filename = filename.replace(replacementRegex, replacement);
|
|
33
|
+
|
|
34
|
+
if (filename.length > MAX_FILENAME_LENGTH) {
|
|
35
|
+
filename = filename.substring(0, MAX_FILENAME_LENGTH);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return filename;
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a debounced function
|
|
3
|
+
*
|
|
4
|
+
* @param func
|
|
5
|
+
* @param wait
|
|
6
|
+
*/
|
|
7
|
+
export function debounce<T extends (...args: unknown[]) => unknown>(
|
|
8
|
+
func: T,
|
|
9
|
+
wait: number
|
|
10
|
+
): (...args: Parameters<T>) => void {
|
|
11
|
+
let timeoutId: NodeJS.Timeout | null = null;
|
|
12
|
+
|
|
13
|
+
return function debounced(...args: Parameters<T>): void {
|
|
14
|
+
if (timeoutId !== null) {
|
|
15
|
+
clearTimeout(timeoutId);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
timeoutId = setTimeout(() => {
|
|
19
|
+
func(...args);
|
|
20
|
+
timeoutId = null;
|
|
21
|
+
}, wait);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a timestamp to 'yyyy-mm-dd hh:mm:ss' format
|
|
3
|
+
* @param timestamp
|
|
4
|
+
*/
|
|
5
|
+
export function formatTimestamp(timestamp: number | Date): string {
|
|
6
|
+
const date = typeof timestamp === 'number' ? new Date(timestamp) : timestamp;
|
|
7
|
+
|
|
8
|
+
const year = date.getFullYear();
|
|
9
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
10
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
11
|
+
const hours = String(date.getHours()).padStart(2, '0');
|
|
12
|
+
const minutes = String(date.getMinutes()).padStart(2, '0');
|
|
13
|
+
const seconds = String(date.getSeconds()).padStart(2, '0');
|
|
14
|
+
const milliseconds = String(date.getMilliseconds()).padStart(3, '0');
|
|
15
|
+
|
|
16
|
+
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
|
|
17
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"target": "esnext",
|
|
5
|
+
"outDir": "./lib",
|
|
6
|
+
"rootDir": "src",
|
|
7
|
+
"strict": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"module": "esnext",
|
|
11
|
+
"moduleResolution": "bundler",
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": false,
|
|
14
|
+
"sourceMap": false,
|
|
15
|
+
"strictNullChecks": true,
|
|
16
|
+
"removeComments": true,
|
|
17
|
+
"emitDecoratorMetadata": true,
|
|
18
|
+
"experimentalDecorators": true,
|
|
19
|
+
"allowSyntheticDefaultImports": true,
|
|
20
|
+
"paths": {
|
|
21
|
+
"@commands/*": ["src/commands/*"],
|
|
22
|
+
"@commands": ["src/commands/index.js"],
|
|
23
|
+
"@constants/*": ["src/constants/*"],
|
|
24
|
+
"@constants": ["src/constants/index.js"],
|
|
25
|
+
"@macros/*": ["src/macros/*"],
|
|
26
|
+
"@macros": ["src/macros/index.js"],
|
|
27
|
+
"@services/*": ["src/services/*"],
|
|
28
|
+
"@services": ["src/services/index.js"],
|
|
29
|
+
"@utils/*": ["src/utils/*"],
|
|
30
|
+
"@utils": ["src/utils/index.js"]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"include": [
|
|
34
|
+
"src/**/*"
|
|
35
|
+
],
|
|
36
|
+
"exclude": [
|
|
37
|
+
"node_modules",
|
|
38
|
+
"lib/**/*"
|
|
39
|
+
]
|
|
40
|
+
}
|