@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.
Files changed (87) hide show
  1. package/.eslintrc.json +31 -0
  2. package/.prettierrc.json +10 -0
  3. package/README.md +3 -0
  4. package/build.js +104 -0
  5. package/lib/app.js +6 -0
  6. package/lib/commands/index.js +1 -0
  7. package/lib/commands/plugin/plugin-add.js +1 -0
  8. package/lib/commands/plugin/plugin-command.js +1 -0
  9. package/lib/commands/plugin/plugin-disable.js +1 -0
  10. package/lib/commands/plugin/plugin-enable.js +1 -0
  11. package/lib/commands/plugin/plugin-list.js +1 -0
  12. package/lib/commands/plugin/plugin-remove.js +1 -0
  13. package/lib/commands/plugin/plugin.js +1 -0
  14. package/lib/commands/project/project-add.js +1 -0
  15. package/lib/commands/project/project-command.js +1 -0
  16. package/lib/commands/project/project-list.js +1 -0
  17. package/lib/commands/project/project-remove.js +1 -0
  18. package/lib/commands/project/project-start.js +1 -0
  19. package/lib/commands/project/project-state-command.js +1 -0
  20. package/lib/commands/project/project-status.js +1 -0
  21. package/lib/commands/project/project-stop.js +1 -0
  22. package/lib/commands/project/project-validate.js +1 -0
  23. package/lib/commands/project/project.js +1 -0
  24. package/lib/commands/runium-command.js +1 -0
  25. package/lib/constants/error-code.js +1 -0
  26. package/lib/constants/index.js +1 -0
  27. package/lib/index.js +2 -0
  28. package/lib/macros/conditional.js +1 -0
  29. package/lib/macros/empty.js +1 -0
  30. package/lib/macros/env.js +1 -0
  31. package/lib/macros/index.js +1 -0
  32. package/lib/macros/path.js +1 -0
  33. package/lib/package.json +21 -0
  34. package/lib/services/config.js +1 -0
  35. package/lib/services/index.js +1 -0
  36. package/lib/services/output.js +3 -0
  37. package/lib/services/plugin-context.js +1 -0
  38. package/lib/services/plugin.js +1 -0
  39. package/lib/services/profile.js +1 -0
  40. package/lib/services/project.js +1 -0
  41. package/lib/services/shutdown.js +1 -0
  42. package/lib/utils/convert-path-to-valid-file-name.js +1 -0
  43. package/lib/utils/debounce.js +1 -0
  44. package/lib/utils/format-timestamp.js +1 -0
  45. package/lib/utils/index.js +1 -0
  46. package/package.json +44 -0
  47. package/src/app.ts +175 -0
  48. package/src/commands/index.ts +2 -0
  49. package/src/commands/plugin/plugin-add.ts +48 -0
  50. package/src/commands/plugin/plugin-command.ts +36 -0
  51. package/src/commands/plugin/plugin-disable.ts +46 -0
  52. package/src/commands/plugin/plugin-enable.ts +50 -0
  53. package/src/commands/plugin/plugin-list.ts +61 -0
  54. package/src/commands/plugin/plugin-remove.ts +42 -0
  55. package/src/commands/plugin/plugin.ts +41 -0
  56. package/src/commands/project/project-add.ts +46 -0
  57. package/src/commands/project/project-command.ts +36 -0
  58. package/src/commands/project/project-list.ts +32 -0
  59. package/src/commands/project/project-remove.ts +41 -0
  60. package/src/commands/project/project-start.ts +152 -0
  61. package/src/commands/project/project-state-command.ts +68 -0
  62. package/src/commands/project/project-status.ts +103 -0
  63. package/src/commands/project/project-stop.ts +55 -0
  64. package/src/commands/project/project-validate.ts +43 -0
  65. package/src/commands/project/project.ts +45 -0
  66. package/src/commands/runium-command.ts +50 -0
  67. package/src/constants/error-code.ts +15 -0
  68. package/src/constants/index.ts +1 -0
  69. package/src/index.ts +21 -0
  70. package/src/macros/conditional.ts +31 -0
  71. package/src/macros/empty.ts +6 -0
  72. package/src/macros/env.ts +8 -0
  73. package/src/macros/index.ts +12 -0
  74. package/src/macros/path.ts +9 -0
  75. package/src/services/config.ts +76 -0
  76. package/src/services/index.ts +7 -0
  77. package/src/services/output.ts +201 -0
  78. package/src/services/plugin-context.ts +81 -0
  79. package/src/services/plugin.ts +144 -0
  80. package/src/services/profile.ts +211 -0
  81. package/src/services/project.ts +114 -0
  82. package/src/services/shutdown.ts +130 -0
  83. package/src/utils/convert-path-to-valid-file-name.ts +39 -0
  84. package/src/utils/debounce.ts +23 -0
  85. package/src/utils/format-timestamp.ts +17 -0
  86. package/src/utils/index.ts +3 -0
  87. 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
+ }
@@ -0,0 +1,3 @@
1
+ export { convertPathToValidFileName } from './convert-path-to-valid-file-name.js';
2
+ export { debounce } from './debounce.js';
3
+ export { formatTimestamp } from './format-timestamp.js';
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
+ }