@runium/cli 0.0.2 → 0.0.3

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 (134) hide show
  1. package/.eslintrc.json +31 -0
  2. package/.prettierrc.json +10 -0
  3. package/README.md +3 -0
  4. package/build.js +125 -0
  5. package/lib/app.js +6 -0
  6. package/{commands → lib/commands}/index.js +0 -0
  7. package/lib/commands/plugin/plugin-add.js +1 -0
  8. package/lib/commands/plugin/plugin-disable.js +1 -0
  9. package/lib/commands/plugin/plugin-enable.js +1 -0
  10. package/lib/commands/plugin/plugin-remove.js +1 -0
  11. package/lib/commands/plugin/plugin.js +1 -0
  12. package/lib/commands/project/project-add.js +1 -0
  13. package/lib/commands/project/project-command.js +1 -0
  14. package/lib/commands/project/project-start.js +1 -0
  15. package/lib/commands/project/project-state-command.js +1 -0
  16. package/lib/commands/project/project-status.js +1 -0
  17. package/lib/commands/project/project-stop.js +1 -0
  18. package/lib/commands/project/project-validate.js +4 -0
  19. package/lib/commands/project/project.js +1 -0
  20. package/lib/commands/runium-command.js +1 -0
  21. package/lib/constants/error-code.js +1 -0
  22. package/{constants → lib/constants}/index.js +0 -0
  23. package/lib/index.js +2 -0
  24. package/lib/macros/date.js +1 -0
  25. package/lib/macros/index.js +1 -0
  26. package/lib/macros/path.js +1 -0
  27. package/lib/package.json +22 -0
  28. package/lib/services/command.js +1 -0
  29. package/lib/services/config.js +1 -0
  30. package/lib/services/file.js +1 -0
  31. package/lib/services/index.js +1 -0
  32. package/lib/services/output.js +3 -0
  33. package/lib/services/plugin-context.js +1 -0
  34. package/lib/services/plugin.js +1 -0
  35. package/lib/services/profile.js +1 -0
  36. package/lib/services/project.js +1 -0
  37. package/lib/services/shutdown.js +1 -0
  38. package/lib/utils/get-version.js +1 -0
  39. package/lib/utils/index.js +1 -0
  40. package/lib/validation/create-validator.js +1 -0
  41. package/lib/validation/get-config-schema.js +1 -0
  42. package/lib/validation/get-error-messages.js +1 -0
  43. package/lib/validation/get-plugin-schema.js +1 -0
  44. package/lib/validation/index.js +1 -0
  45. package/package.json +33 -7
  46. package/src/app.ts +190 -0
  47. package/src/commands/index.ts +2 -0
  48. package/src/commands/plugin/plugin-add.ts +48 -0
  49. package/src/commands/plugin/plugin-command.ts +36 -0
  50. package/src/commands/plugin/plugin-disable.ts +46 -0
  51. package/src/commands/plugin/plugin-enable.ts +50 -0
  52. package/src/commands/plugin/plugin-list.ts +61 -0
  53. package/src/commands/plugin/plugin-remove.ts +42 -0
  54. package/src/commands/plugin/plugin.ts +36 -0
  55. package/src/commands/project/project-add.ts +64 -0
  56. package/src/commands/project/project-command.ts +43 -0
  57. package/src/commands/project/project-list.ts +32 -0
  58. package/src/commands/project/project-remove.ts +41 -0
  59. package/src/commands/project/project-start.ts +158 -0
  60. package/src/commands/project/project-state-command.ts +53 -0
  61. package/src/commands/project/project-status.ts +116 -0
  62. package/src/commands/project/project-stop.ts +59 -0
  63. package/src/commands/project/project-validate.ts +56 -0
  64. package/src/commands/project/project.ts +40 -0
  65. package/src/commands/runium-command.ts +52 -0
  66. package/src/constants/error-code.ts +28 -0
  67. package/src/constants/index.ts +1 -0
  68. package/src/global.d.ts +6 -0
  69. package/src/index.ts +24 -0
  70. package/src/macros/conditional.ts +31 -0
  71. package/src/macros/date.ts +15 -0
  72. package/src/macros/empty.ts +6 -0
  73. package/src/macros/env.ts +8 -0
  74. package/src/macros/index.ts +17 -0
  75. package/src/macros/path.ts +24 -0
  76. package/src/services/command.ts +171 -0
  77. package/src/services/config.ts +119 -0
  78. package/src/services/file.ts +272 -0
  79. package/src/services/index.ts +9 -0
  80. package/src/services/output.ts +205 -0
  81. package/src/services/plugin-context.ts +140 -0
  82. package/src/services/plugin.ts +248 -0
  83. package/src/services/profile.ts +199 -0
  84. package/src/services/project.ts +142 -0
  85. package/src/services/shutdown.ts +147 -0
  86. package/src/utils/convert-path-to-valid-file-name.ts +39 -0
  87. package/src/utils/debounce.ts +23 -0
  88. package/src/utils/format-timestamp.ts +17 -0
  89. package/src/utils/get-version.ts +13 -0
  90. package/src/utils/index.ts +4 -0
  91. package/src/validation/create-validator.ts +27 -0
  92. package/src/validation/get-config-schema.ts +59 -0
  93. package/src/validation/get-error-messages.ts +35 -0
  94. package/src/validation/get-plugin-schema.ts +137 -0
  95. package/src/validation/index.ts +4 -0
  96. package/tsconfig.json +38 -0
  97. package/app.js +0 -6
  98. package/commands/plugin/plugin-add.js +0 -1
  99. package/commands/plugin/plugin-disable.js +0 -1
  100. package/commands/plugin/plugin-enable.js +0 -1
  101. package/commands/plugin/plugin-remove.js +0 -1
  102. package/commands/plugin/plugin.js +0 -1
  103. package/commands/project/project-add.js +0 -1
  104. package/commands/project/project-command.js +0 -1
  105. package/commands/project/project-start.js +0 -1
  106. package/commands/project/project-state-command.js +0 -1
  107. package/commands/project/project-status.js +0 -1
  108. package/commands/project/project-stop.js +0 -1
  109. package/commands/project/project-validate.js +0 -1
  110. package/commands/project/project.js +0 -1
  111. package/commands/runium-command.js +0 -1
  112. package/constants/error-code.js +0 -1
  113. package/index.js +0 -2
  114. package/macros/index.js +0 -1
  115. package/macros/path.js +0 -1
  116. package/services/config.js +0 -1
  117. package/services/index.js +0 -1
  118. package/services/output.js +0 -3
  119. package/services/plugin-context.js +0 -1
  120. package/services/plugin.js +0 -1
  121. package/services/profile.js +0 -1
  122. package/services/project.js +0 -1
  123. package/services/shutdown.js +0 -1
  124. package/utils/index.js +0 -1
  125. /package/{commands → lib/commands}/plugin/plugin-command.js +0 -0
  126. /package/{commands → lib/commands}/plugin/plugin-list.js +0 -0
  127. /package/{commands → lib/commands}/project/project-list.js +0 -0
  128. /package/{commands → lib/commands}/project/project-remove.js +0 -0
  129. /package/{macros → lib/macros}/conditional.js +0 -0
  130. /package/{macros → lib/macros}/empty.js +0 -0
  131. /package/{macros → lib/macros}/env.js +0 -0
  132. /package/{utils → lib/utils}/convert-path-to-valid-file-name.js +0 -0
  133. /package/{utils → lib/utils}/debounce.js +0 -0
  134. /package/{utils → lib/utils}/format-timestamp.js +0 -0
@@ -0,0 +1,199 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { Inject, Service } from 'typedi';
5
+ import { ConfigService, FileService } from '@services';
6
+
7
+ export interface ProfilePlugin {
8
+ name: string;
9
+ path: string;
10
+ file?: boolean;
11
+ disabled?: boolean;
12
+ options?: Record<string, unknown>;
13
+ }
14
+
15
+ export interface ProfileProject {
16
+ name: string;
17
+ path: string;
18
+ }
19
+
20
+ const PLUGINS_FILE_NAME = 'plugins.json';
21
+ const PROJECTS_FILE_NAME = 'projects.json';
22
+
23
+ @Service()
24
+ export class ProfileService {
25
+ private path: string = process.cwd();
26
+
27
+ private plugins: ProfilePlugin[] = [];
28
+
29
+ private projects: ProfileProject[] = [];
30
+
31
+ constructor(
32
+ @Inject() private configService: ConfigService,
33
+ @Inject() private fileService: FileService
34
+ ) {}
35
+
36
+ /**
37
+ * Initialize the profile service
38
+ */
39
+ async init(): Promise<void> {
40
+ this.path = this.configService.get('profile').path;
41
+ if (!existsSync(this.path)) {
42
+ await mkdir(this.path, { recursive: true });
43
+ }
44
+
45
+ await this.readPlugins();
46
+ await this.patchPlugins();
47
+ await this.readProjects();
48
+ }
49
+
50
+ /**
51
+ * Get plugins
52
+ */
53
+ getPlugins(): ProfilePlugin[] {
54
+ return this.plugins;
55
+ }
56
+
57
+ /**
58
+ * Get plugin by name
59
+ * @param name
60
+ */
61
+ getPluginByName(name: string): ProfilePlugin | undefined {
62
+ return this.plugins.find(p => p.name === name);
63
+ }
64
+
65
+ /**
66
+ * Add plugin
67
+ * @param plugin
68
+ */
69
+ async addPlugin(plugin: ProfilePlugin): Promise<void> {
70
+ this.plugins = this.plugins
71
+ .filter(p => p.name !== plugin.name)
72
+ .concat(plugin);
73
+ await this.writePlugins();
74
+ }
75
+
76
+ /**
77
+ * Update plugin
78
+ * @param name
79
+ */
80
+ async removePlugin(name: string): Promise<void> {
81
+ this.plugins = this.plugins.filter(plugin => plugin.name !== name);
82
+ await this.writePlugins();
83
+ }
84
+
85
+ /**
86
+ * Update plugin
87
+ * @param name
88
+ * @param data
89
+ */
90
+ async updatePlugin(
91
+ name: string,
92
+ data: Partial<Omit<ProfilePlugin, 'name'>>
93
+ ): Promise<void> {
94
+ const index = this.plugins.findIndex(plugin => plugin.name === name);
95
+ if (index !== -1) {
96
+ this.plugins[index] = {
97
+ ...this.plugins[index],
98
+ ...data,
99
+ };
100
+ await this.writePlugins();
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get projects
106
+ */
107
+ getProjects(): ProfileProject[] {
108
+ return this.projects;
109
+ }
110
+
111
+ /**
112
+ * Get project by name
113
+ * @param name
114
+ */
115
+ getProjectByName(name: string): ProfileProject | undefined {
116
+ return this.projects.find(p => p.name === name);
117
+ }
118
+
119
+ /**
120
+ * Add project
121
+ * @param project
122
+ */
123
+ async addProject(project: ProfileProject): Promise<void> {
124
+ this.projects = this.projects
125
+ .filter(p => p.name !== project.name)
126
+ .concat(project);
127
+ await this.writeProjects();
128
+ }
129
+
130
+ /**
131
+ * Remove project
132
+ * @param name
133
+ */
134
+ async removeProject(name: string): Promise<void> {
135
+ this.projects = this.projects.filter(p => p.name !== name);
136
+ await this.writeProjects();
137
+ }
138
+
139
+ /**
140
+ * Get path for a file
141
+ * @param parts
142
+ */
143
+ getPath(...parts: string[]): string {
144
+ return join(this.path, ...parts);
145
+ }
146
+
147
+ /**
148
+ * Read plugins from file
149
+ */
150
+ private async readPlugins(): Promise<void> {
151
+ this.plugins =
152
+ ((await this.fileService
153
+ .readJson(this.getPath(PLUGINS_FILE_NAME))
154
+ .catch(() => [])) as ProfilePlugin[]) || this.plugins;
155
+ }
156
+
157
+ /**
158
+ * Write plugins to file
159
+ */
160
+ private async writePlugins(): Promise<void> {
161
+ await this.fileService.writeJson(
162
+ this.getPath(PLUGINS_FILE_NAME),
163
+ this.plugins
164
+ );
165
+ }
166
+
167
+ /**
168
+ * Patch plugins
169
+ */
170
+ private async patchPlugins(): Promise<void> {
171
+ const pluginsConfig = this.configService.get('plugins');
172
+ for (const plugin of this.plugins) {
173
+ const config = pluginsConfig[plugin.name];
174
+ if (config) {
175
+ Object.assign(plugin, config);
176
+ }
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Read projects from file
182
+ */
183
+ private async readProjects(): Promise<void> {
184
+ this.projects =
185
+ ((await this.fileService
186
+ .readJson(this.getPath(PROJECTS_FILE_NAME))
187
+ .catch(() => [])) as ProfileProject[]) || this.projects;
188
+ }
189
+
190
+ /**
191
+ * Write projects to file
192
+ */
193
+ private async writeProjects(): Promise<void> {
194
+ await this.fileService.writeJson(
195
+ this.getPath(PROJECTS_FILE_NAME),
196
+ this.projects
197
+ );
198
+ }
199
+ }
@@ -0,0 +1,142 @@
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
+ await this.pluginService.runHook('project.beforeConfigRead', path);
20
+
21
+ let content = await this.readFile(path);
22
+ content = await this.pluginService.runHook(
23
+ 'project.afterConfigRead',
24
+ content,
25
+ { mutable: true }
26
+ );
27
+ content = this.applyMacros(content);
28
+ content = await this.pluginService.runHook(
29
+ 'project.afterConfigMacrosApply',
30
+ content,
31
+ { mutable: true }
32
+ );
33
+
34
+ let projectData = this.parseProjectContent(content, path);
35
+ projectData = await this.pluginService.runHook(
36
+ 'project.afterConfigParse',
37
+ projectData,
38
+ { mutable: true }
39
+ );
40
+
41
+ const project = new Project(projectData);
42
+ return this.extendProjectWithPlugins(project);
43
+ }
44
+
45
+ /**
46
+ * Resolve path
47
+ * @param path
48
+ */
49
+ resolvePath(path: string): string {
50
+ return resolve(path);
51
+ }
52
+
53
+ /**
54
+ * Run hook
55
+ * @param args
56
+ */
57
+ runHook(
58
+ ...args: Parameters<PluginService['runHook']>
59
+ ): ReturnType<PluginService['runHook']> {
60
+ return this.pluginService.runHook(...args);
61
+ }
62
+
63
+ /**
64
+ * Read project file
65
+ * @param path
66
+ */
67
+ private async readFile(path: string): Promise<string> {
68
+ if (!existsSync(path)) {
69
+ throw new RuniumError(
70
+ `Project file "${path}" does not exist`,
71
+ ErrorCode.PROJECT_FILE_NOT_FOUND,
72
+ { path }
73
+ );
74
+ }
75
+ try {
76
+ return readFile(path, { encoding: 'utf-8' });
77
+ } catch (error) {
78
+ throw new RuniumError(
79
+ `Failed to read project file "${path}"`,
80
+ ErrorCode.PROJECT_FILE_CAN_NOT_READ,
81
+ { path, original: error }
82
+ );
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Apply macros to text
88
+ * @param text
89
+ */
90
+ private applyMacros(text: string): string {
91
+ const plugins = this.pluginService.getAllPlugins();
92
+ const pluginMacros = plugins.reduce(
93
+ (acc, plugin) => ({ ...acc, ...(plugin.project?.macros || {}) }),
94
+ {}
95
+ );
96
+ return applyMacros(text, { ...pluginMacros, ...macros });
97
+ }
98
+
99
+ /**
100
+ * Parse project content
101
+ * @param content
102
+ * @param path
103
+ */
104
+ private parseProjectContent(content: string, path: string): ProjectConfig {
105
+ try {
106
+ return JSON.parse(content);
107
+ } catch (error) {
108
+ throw new RuniumError(
109
+ `Failed to parse project "${path}"`,
110
+ ErrorCode.PROJECT_JSON_PARSE_ERROR,
111
+ { path, original: error }
112
+ );
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Extends the project with plugins
118
+ * @param project
119
+ */
120
+ private extendProjectWithPlugins(project: Project): Project {
121
+ const plugins = this.pluginService.getAllPlugins();
122
+
123
+ plugins.forEach(plugin => {
124
+ const { tasks, actions, triggers, validationSchema } = (plugin.project ||
125
+ {}) as PluginProjectDefinition;
126
+ Object.entries(actions || {}).forEach(([type, processor]) => {
127
+ project.registerAction(type, processor);
128
+ });
129
+ Object.entries(tasks || {}).forEach(([type, processor]) => {
130
+ project.registerTask(type, processor);
131
+ });
132
+ Object.entries(triggers || {}).forEach(([type, processor]) => {
133
+ project.registerTrigger(type, processor);
134
+ });
135
+ if (validationSchema) {
136
+ project.extendValidationSchema(validationSchema);
137
+ }
138
+ });
139
+
140
+ return project;
141
+ }
142
+ }
@@ -0,0 +1,147 @@
1
+ import { Inject, Service } from 'typedi';
2
+ import { OutputService, PluginService } from '@services';
3
+
4
+ type ShutdownBlocker = (reason?: string) => 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(
16
+ @Inject() private outputService: OutputService,
17
+ @Inject() private pluginService: PluginService
18
+ ) {}
19
+ /**
20
+ * Initialize shutdown handlers
21
+ */
22
+ async init(): Promise<void> {
23
+ SIGNALS.forEach(signal => {
24
+ process.on(signal, () => {
25
+ this.shutdown(signal).catch(error => {
26
+ this.outputService.error('Error during shutdown: %s', error.message);
27
+ this.outputService.debug('Error details:', error);
28
+ process.exit(1);
29
+ });
30
+ });
31
+ });
32
+
33
+ process.on('uncaughtException', error => {
34
+ this.outputService.error('Uncaught exception: %s', error.message);
35
+ this.outputService.debug('Error details:', error);
36
+ this.shutdown('uncaughtException').catch(() => {
37
+ process.exit(1);
38
+ });
39
+ });
40
+
41
+ process.on('unhandledRejection', (reason, promise) => {
42
+ this.outputService.error(
43
+ 'Unhandled rejection at:',
44
+ promise,
45
+ 'reason:',
46
+ reason
47
+ );
48
+ this.outputService.debug('Error details:', { reason, promise });
49
+ this.shutdown('unhandledRejection').catch(() => {
50
+ process.exit(1);
51
+ });
52
+ });
53
+
54
+ process.on('beforeExit', () => {
55
+ if (!this.isShuttingDown) {
56
+ this.shutdown('exit').catch(() => {
57
+ process.exit(1);
58
+ });
59
+ }
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Add a shutdown blocker function
65
+ * @param blocker
66
+ */
67
+ addBlocker(blocker: ShutdownBlocker): void {
68
+ this.blockers.add(blocker);
69
+ }
70
+
71
+ /**
72
+ * Remove a shutdown blocker
73
+ * @param blocker
74
+ */
75
+ removeBlocker(blocker: ShutdownBlocker): boolean {
76
+ return this.blockers.delete(blocker);
77
+ }
78
+
79
+ /**
80
+ * Execute graceful shutdown
81
+ * @param reason
82
+ */
83
+ async shutdown(reason?: string): Promise<void> {
84
+ if (this.isShuttingDown || !reason) {
85
+ return;
86
+ }
87
+
88
+ // add plugin beforeExit hooks as shutdown blockers
89
+ const plugins = this.pluginService.getAllPlugins();
90
+ for (const plugin of plugins) {
91
+ const hook = plugin.hooks?.app?.beforeExit;
92
+ if (hook) {
93
+ this.addBlocker(hook.bind(plugin, reason));
94
+ }
95
+ }
96
+
97
+ this.isShuttingDown = true;
98
+
99
+ const exitProcess = (code: number): void => {
100
+ setTimeout(() => {
101
+ process.exit(code);
102
+ }, EXIT_DELAY);
103
+ };
104
+
105
+ if (this.blockers.size === 0) {
106
+ exitProcess(0);
107
+ return;
108
+ }
109
+
110
+ try {
111
+ await this.executeBlockersWithTimeout(reason);
112
+ exitProcess(0);
113
+ } catch (error) {
114
+ exitProcess(1);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Execute all blockers with timeout
120
+ * @param reason
121
+ */
122
+ private async executeBlockersWithTimeout(reason: string): Promise<void> {
123
+ const blockerPromises = Array.from(this.blockers).map(blocker =>
124
+ this.executeBlocker(blocker, reason)
125
+ );
126
+
127
+ const timeoutPromise = new Promise<never>((_, reject) => {
128
+ setTimeout(() => {
129
+ reject(new Error(`Shutdown timeout after ${TIMEOUT}ms`));
130
+ }, TIMEOUT);
131
+ });
132
+
133
+ await Promise.race([Promise.allSettled(blockerPromises), timeoutPromise]);
134
+ }
135
+
136
+ /**
137
+ * Execute a single blocker with error handling
138
+ * @param blocker
139
+ * @param reason
140
+ */
141
+ private async executeBlocker(
142
+ blocker: ShutdownBlocker,
143
+ reason: string
144
+ ): Promise<void> {
145
+ await blocker(reason);
146
+ }
147
+ }
@@ -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,13 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+
4
+ let version: string | null = null;
5
+
6
+ export function getVersion(): string {
7
+ if (!version) {
8
+ const packageJsonPath = resolve(import.meta.dirname, '..', 'package.json');
9
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
10
+ version = packageJson.version;
11
+ }
12
+ return version as string;
13
+ }
@@ -0,0 +1,4 @@
1
+ export { convertPathToValidFileName } from './convert-path-to-valid-file-name.js';
2
+ export { debounce } from './debounce.js';
3
+ export { formatTimestamp } from './format-timestamp.js';
4
+ export { getVersion } from './get-version.js';
@@ -0,0 +1,27 @@
1
+ import Ajv, { ValidateFunction } from 'ajv/dist/2020.js';
2
+
3
+ const classes = { Function: Function };
4
+
5
+ export type Validator<T = unknown> = ValidateFunction<T>;
6
+
7
+ /**
8
+ * Create validator
9
+ * @param schema
10
+ */
11
+ export function createValidator<T = unknown>(schema: object): Validator<T> {
12
+ const ajv = new Ajv({
13
+ allowUnionTypes: true,
14
+ allErrors: true,
15
+ verbose: true,
16
+ });
17
+
18
+ ajv.addKeyword({
19
+ keyword: 'instanceof',
20
+ schemaType: 'string',
21
+ validate: (schema: string, data: unknown) => {
22
+ return data instanceof classes[schema as keyof typeof classes];
23
+ },
24
+ });
25
+
26
+ return ajv.compile<T>(schema);
27
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Get config schema
3
+ */
4
+ export function getConfigSchema(): object {
5
+ return {
6
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
7
+ $id: 'https://runium.dev/schemas/config.json',
8
+ title: 'Runium Config',
9
+ type: 'object',
10
+ properties: {
11
+ env: {
12
+ type: 'object',
13
+ properties: {
14
+ path: {
15
+ type: 'array',
16
+ items: {
17
+ type: 'string',
18
+ },
19
+ },
20
+ },
21
+ additionalProperties: false,
22
+ },
23
+ output: {
24
+ type: 'object',
25
+ properties: {
26
+ debug: {
27
+ type: 'boolean',
28
+ },
29
+ },
30
+ additionalProperties: false,
31
+ },
32
+ profile: {
33
+ type: 'object',
34
+ properties: {
35
+ path: {
36
+ type: 'string',
37
+ },
38
+ },
39
+ additionalProperties: false,
40
+ },
41
+ plugins: {
42
+ type: 'object',
43
+ additionalProperties: {
44
+ type: 'object',
45
+ properties: {
46
+ disabled: {
47
+ type: 'boolean',
48
+ },
49
+ options: {
50
+ type: 'object',
51
+ },
52
+ },
53
+ additionalProperties: false,
54
+ },
55
+ },
56
+ },
57
+ additionalProperties: false,
58
+ };
59
+ }