@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,201 @@
1
+ import { Console } from 'node:console';
2
+ import { Transform } from 'node:stream';
3
+
4
+ import { Service } from 'typedi';
5
+
6
+ export enum OutputLevel {
7
+ TRACE = 0,
8
+ DEBUG = 1,
9
+ INFO = 2,
10
+ WARN = 3,
11
+ ERROR = 4,
12
+ SILENT = 5,
13
+ }
14
+
15
+ /**
16
+ * Console dumper
17
+ * wrapper around node console with a transform stream
18
+ */
19
+ class ConsoleDumper extends Console {
20
+ private readonly transform: Transform;
21
+
22
+ constructor() {
23
+ const transform = new Transform({
24
+ transform: (chunk, _, cb) => cb(null, chunk),
25
+ });
26
+ super({
27
+ stdout: transform,
28
+ stderr: transform,
29
+ colorMode: false,
30
+ });
31
+ this.transform = transform;
32
+ }
33
+
34
+ /**
35
+ * Get a table output with index column removed
36
+ * @param data
37
+ * @param columns
38
+ */
39
+ getPatchedTable(data: unknown[], columns?: string[]): string {
40
+ this.table(data, columns);
41
+
42
+ const original = (this.transform.read() || '').toString();
43
+
44
+ // Tables should all start with roughly:
45
+ // ┌─────────┬──────
46
+ // │ (index) │
47
+ // ├─────────┼
48
+ const columnWidth = original.indexOf('┬') + 1;
49
+
50
+ return original
51
+ .split('\n')
52
+ .map((line: string) => line.charAt(0) + line.slice(columnWidth))
53
+ .join('\n');
54
+ }
55
+ }
56
+
57
+ const dumper = new ConsoleDumper();
58
+
59
+ @Service()
60
+ export class OutputService {
61
+ private outputLevel: OutputLevel = OutputLevel.INFO;
62
+
63
+ /**
64
+ * Set output level
65
+ * @param level
66
+ */
67
+ setLevel(level: OutputLevel): void {
68
+ this.outputLevel = level;
69
+ }
70
+
71
+ /**
72
+ * Get output level
73
+ */
74
+ getLevel(): OutputLevel {
75
+ return this.outputLevel;
76
+ }
77
+
78
+ /**
79
+ * Log a trace message
80
+ * @param message
81
+ * @param args
82
+ */
83
+ trace(message: string, ...args: unknown[]): void {
84
+ if (this.outputLevel <= OutputLevel.TRACE) {
85
+ // eslint-disable-next-line no-console
86
+ console.log(message, ...args);
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Log a debug message
92
+ * @param message
93
+ * @param args
94
+ */
95
+ debug(message: string, ...args: unknown[]): void {
96
+ if (this.outputLevel <= OutputLevel.DEBUG) {
97
+ // eslint-disable-next-line no-console
98
+ console.log(message, ...args);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Log an info message
104
+ * @param message
105
+ * @param args
106
+ */
107
+ info(message: string, ...args: unknown[]): void {
108
+ if (this.outputLevel <= OutputLevel.INFO) {
109
+ // eslint-disable-next-line no-console
110
+ console.log(message, ...args);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Log a success message
116
+ * @param message
117
+ * @param args
118
+ */
119
+ success(message: string, ...args: unknown[]): void {
120
+ if (this.outputLevel <= OutputLevel.INFO) {
121
+ // eslint-disable-next-line no-console
122
+ console.log(message, ...args);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Log a warning message
128
+ * @param message
129
+ * @param args
130
+ */
131
+ warn(message: string, ...args: unknown[]): void {
132
+ if (this.outputLevel <= OutputLevel.WARN) {
133
+ // eslint-disable-next-line no-console
134
+ console.warn(message, ...args);
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Log an error message
140
+ * @param message
141
+ * @param args
142
+ */
143
+ error(message: string, ...args: unknown[]): void {
144
+ if (this.outputLevel <= OutputLevel.ERROR) {
145
+ // eslint-disable-next-line no-console
146
+ console.error(message, ...args);
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Log a message without level
152
+ * @param message
153
+ * @param args
154
+ */
155
+ log(message: string, ...args: unknown[]): void {
156
+ if (this.outputLevel < OutputLevel.SILENT) {
157
+ // eslint-disable-next-line no-console
158
+ console.log(message, ...args);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Output a table
164
+ * @param data
165
+ * @param columns
166
+ */
167
+ table(data: unknown[], columns?: string[]): void {
168
+ if (this.outputLevel < OutputLevel.SILENT) {
169
+ const patchedData = data.map((item, index) => ({
170
+ ...(item as object),
171
+ '#': index + 1,
172
+ }));
173
+ const patchedOutput = dumper.getPatchedTable(
174
+ patchedData,
175
+ columns ? ['#', ...columns] : undefined
176
+ );
177
+ // eslint-disable-next-line no-console
178
+ console.log(patchedOutput);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Output a blank line
184
+ */
185
+ newLine(): void {
186
+ if (this.outputLevel < OutputLevel.SILENT) {
187
+ // eslint-disable-next-line no-console
188
+ console.log('');
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Clear output
194
+ */
195
+ clear(): void {
196
+ if (this.outputLevel < OutputLevel.SILENT) {
197
+ // eslint-disable-next-line no-console
198
+ console.clear();
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,81 @@
1
+ import { Inject, Service } from 'typedi';
2
+
3
+ import {
4
+ RuniumError,
5
+ isRuniumError,
6
+ RuniumTask,
7
+ RuniumTrigger,
8
+ readJsonFile,
9
+ writeJsonFile,
10
+ applyMacros,
11
+ TaskStatus,
12
+ ProjectEvent,
13
+ ProjectStatus,
14
+ } from '@runium/core';
15
+ import { RuniumCommand } from '@commands/runium-command.js';
16
+ import { OutputLevel, OutputService, ShutdownService } from '@services';
17
+
18
+ // @ts-expect-error global object access
19
+ global.runium = null;
20
+
21
+ @Service()
22
+ export class PluginContextService {
23
+ constructor(
24
+ @Inject() private outputService: OutputService,
25
+ @Inject() private shutdownService: ShutdownService
26
+ ) {}
27
+
28
+ /**
29
+ * Initialize the plugin context service
30
+ */
31
+ async init(): Promise<void> {
32
+ const output = this.outputService;
33
+ const shutdown = this.shutdownService;
34
+
35
+ const runium = {
36
+ class: {
37
+ RuniumCommand,
38
+ RuniumError,
39
+ RuniumTask,
40
+ RuniumTrigger,
41
+ },
42
+ enum: {
43
+ OutputLevel: Object.keys(OutputLevel)
44
+ .filter(key => isNaN(Number(key)))
45
+ .reduce(
46
+ (acc, key) => {
47
+ acc[key] = OutputLevel[key as keyof typeof OutputLevel];
48
+ return acc;
49
+ },
50
+ {} as Record<string, number>
51
+ ),
52
+ ProjectEvent,
53
+ ProjectStatus,
54
+ TaskStatus,
55
+ },
56
+ utils: {
57
+ applyMacros,
58
+ isRuniumError,
59
+ readJsonFile,
60
+ writeJsonFile,
61
+ },
62
+ output: {
63
+ getLevel: output.getLevel.bind(output),
64
+ setLevel: output.setLevel.bind(output),
65
+ trace: output.trace.bind(output),
66
+ debug: output.debug.bind(output),
67
+ info: output.info.bind(output),
68
+ warn: output.warn.bind(output),
69
+ error: output.error.bind(output),
70
+ table: output.table.bind(output),
71
+ log: output.log.bind(output),
72
+ },
73
+ shutdown: {
74
+ addBlocker: shutdown.addBlocker.bind(shutdown),
75
+ removeBlocker: shutdown.removeBlocker.bind(shutdown),
76
+ },
77
+ };
78
+ // @ts-expect-error global object access
79
+ global.runium = Object.freeze(runium);
80
+ }
81
+ }
@@ -0,0 +1,144 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { Service } from 'typedi';
4
+ import {
5
+ isRuniumError,
6
+ Macro,
7
+ ProjectSchemaExtension,
8
+ RuniumError,
9
+ RuniumTaskConstructor,
10
+ RuniumTriggerConstructor,
11
+ RuniumTriggerOptions,
12
+ } from '@runium/core';
13
+ import { ErrorCode } from '@constants';
14
+
15
+ type PluginOptions = Record<string, unknown>;
16
+
17
+ type PluginModule = { default: (options?: PluginOptions) => Plugin };
18
+
19
+ export interface PluginProjectDefinition {
20
+ macros?: Record<string, Macro>;
21
+ tasks?: Record<string, RuniumTaskConstructor>;
22
+ actions?: Record<string, (payload: unknown) => void>;
23
+ triggers?: Record<string, RuniumTriggerConstructor<RuniumTriggerOptions>>;
24
+ validationSchema?: ProjectSchemaExtension;
25
+ }
26
+
27
+ export interface PluginOptionsDefinition {
28
+ value?: PluginOptions;
29
+ validate?(options: PluginOptions): boolean;
30
+ }
31
+
32
+ export interface PluginAppDefinition {
33
+ commands?: Record<string, unknown>;
34
+ }
35
+
36
+ export interface Plugin {
37
+ name: string;
38
+ project?: PluginProjectDefinition;
39
+ options?: PluginOptionsDefinition;
40
+ app?: PluginAppDefinition;
41
+ }
42
+
43
+ @Service()
44
+ export class PluginService {
45
+ private plugins: Map<string, Plugin> = new Map();
46
+
47
+ /**
48
+ * Get all plugins
49
+ */
50
+ getAllPlugins(): Plugin[] {
51
+ return Array.from(this.plugins.values());
52
+ }
53
+
54
+ /**
55
+ * Get plugin by name
56
+ * @param name
57
+ */
58
+ getPluginByName(name: string): Plugin | undefined {
59
+ return this.plugins.get(name);
60
+ }
61
+
62
+ /**
63
+ * Load plugin
64
+ * @param path
65
+ */
66
+ async loadPlugin(path: string): Promise<string> {
67
+ if (!path || !existsSync(path)) {
68
+ throw new RuniumError(
69
+ `Plugin file "${path}" does not exist`,
70
+ ErrorCode.PLUGIN_FILE_NOT_FOUND,
71
+ { path }
72
+ );
73
+ }
74
+
75
+ try {
76
+ const pluginModule = (await import(path)) as PluginModule;
77
+ const { default: getPlugin } = pluginModule;
78
+ if (!getPlugin || typeof getPlugin !== 'function') {
79
+ throw new RuniumError(
80
+ 'Plugin module must have a default function',
81
+ ErrorCode.PLUGIN_INCORRECT_MODULE,
82
+ { path }
83
+ );
84
+ }
85
+ const plugin = getPlugin();
86
+
87
+ this.validate(plugin);
88
+
89
+ this.plugins.set(plugin.name, plugin);
90
+
91
+ return plugin.name;
92
+ } catch (error) {
93
+ if (isRuniumError(error)) {
94
+ throw error;
95
+ }
96
+ throw new RuniumError(
97
+ `Failed to load plugin "${path}"`,
98
+ ErrorCode.PLUGIN_INCORRECT_PLUGIN,
99
+ { path, original: error }
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Unload plugin
106
+ * @param name
107
+ */
108
+ async unloadPlugin(name: string): Promise<boolean> {
109
+ return this.plugins.delete(name);
110
+ }
111
+
112
+ /**
113
+ * Resolve path
114
+ * @param path
115
+ * @param isFile
116
+ */
117
+ resolvePath(path: string, isFile: boolean = false): string {
118
+ try {
119
+ const resolvedPath = isFile ? resolve(path) : import.meta.resolve(path);
120
+ return resolvedPath.replace('file://', '');
121
+ } catch (error) {
122
+ throw new RuniumError(
123
+ `Failed to resolve plugin path "${path}"`,
124
+ ErrorCode.PLUGIN_PATH_RESOLVE_ERROR,
125
+ { path, original: error }
126
+ );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Validate plugin
132
+ * @param plugin
133
+ */
134
+ private validate(plugin: Plugin): void {
135
+ // TODO add plugin validation
136
+ if (!plugin || !plugin?.name) {
137
+ throw new RuniumError(
138
+ 'Incorrect plugin format',
139
+ ErrorCode.PLUGIN_INCORRECT_PLUGIN,
140
+ { name: plugin.name }
141
+ );
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,211 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir } from 'node:fs/promises';
3
+ import { dirname, join } from 'node:path';
4
+ import { Inject, Service } from 'typedi';
5
+ import {
6
+ JSONObject,
7
+ readJsonFile,
8
+ RuniumError,
9
+ writeJsonFile,
10
+ } from '@runium/core';
11
+ import { ConfigService } from '@services';
12
+ import { ErrorCode } from '@constants';
13
+
14
+ export interface ProfilePlugin {
15
+ name: string;
16
+ path: string;
17
+ file?: boolean;
18
+ disabled?: boolean;
19
+ options?: Record<string, unknown>;
20
+ }
21
+
22
+ export interface ProfileProject {
23
+ name: string;
24
+ path: string;
25
+ }
26
+
27
+ const PLUGINS_FILE_NAME = 'plugins.json';
28
+ const PROJECTS_FILE_NAME = 'projects.json';
29
+
30
+ @Service()
31
+ export class ProfileService {
32
+ private path: string = process.cwd();
33
+
34
+ private plugins: ProfilePlugin[] = [];
35
+
36
+ private projects: ProfileProject[] = [];
37
+
38
+ constructor(@Inject() private configService: ConfigService) {}
39
+
40
+ /**
41
+ * Initialize the profile service
42
+ */
43
+ async init(): Promise<void> {
44
+ this.path = this.configService.get('profile').path;
45
+ if (!existsSync(this.path)) {
46
+ await mkdir(this.path, { recursive: true });
47
+ }
48
+
49
+ await this.readPlugins();
50
+ await this.readProjects();
51
+ }
52
+
53
+ /**
54
+ * Get plugins
55
+ */
56
+ getPlugins(): ProfilePlugin[] {
57
+ return this.plugins;
58
+ }
59
+
60
+ /**
61
+ * Get plugin by name
62
+ * @param name
63
+ */
64
+ getPluginByName(name: string): ProfilePlugin | undefined {
65
+ return this.plugins.find(p => p.name === name);
66
+ }
67
+
68
+ /**
69
+ * Add plugin
70
+ * @param plugin
71
+ */
72
+ async addPlugin(plugin: ProfilePlugin): Promise<void> {
73
+ this.plugins = this.plugins
74
+ .filter(p => p.name !== plugin.name)
75
+ .concat(plugin);
76
+ await this.writePlugins();
77
+ }
78
+
79
+ /**
80
+ * Update plugin
81
+ * @param name
82
+ */
83
+ async removePlugin(name: string): Promise<void> {
84
+ this.plugins = this.plugins.filter(plugin => plugin.name !== name);
85
+ await this.writePlugins();
86
+ }
87
+
88
+ /**
89
+ * Update plugin
90
+ * @param name
91
+ * @param data
92
+ */
93
+ async updatePlugin(
94
+ name: string,
95
+ data: Partial<Omit<ProfilePlugin, 'name'>>
96
+ ): Promise<void> {
97
+ const index = this.plugins.findIndex(plugin => plugin.name === name);
98
+ if (index !== -1) {
99
+ this.plugins[index] = {
100
+ ...this.plugins[index],
101
+ ...data,
102
+ };
103
+ await this.writePlugins();
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Get projects
109
+ */
110
+ getProjects(): ProfileProject[] {
111
+ return this.projects;
112
+ }
113
+
114
+ /**
115
+ * Get project by name
116
+ * @param name
117
+ */
118
+ getProjectByName(name: string): ProfileProject | undefined {
119
+ return this.projects.find(p => p.name === name);
120
+ }
121
+
122
+ /**
123
+ * Add project
124
+ * @param project
125
+ */
126
+ async addProject(project: ProfileProject): Promise<void> {
127
+ this.projects = this.projects
128
+ .filter(p => p.name !== project.name)
129
+ .concat(project);
130
+ await this.writeProjects();
131
+ }
132
+
133
+ /**
134
+ * Remove project
135
+ * @param name
136
+ */
137
+ async removeProject(name: string): Promise<void> {
138
+ this.projects = this.projects.filter(p => p.name !== name);
139
+ await this.writeProjects();
140
+ }
141
+
142
+ /**
143
+ * Read JSON file
144
+ * @param pathParts
145
+ */
146
+ async readJsonFile(...pathParts: string[]): Promise<JSONObject> {
147
+ return readJsonFile(this.getPath(...pathParts));
148
+ }
149
+
150
+ /**
151
+ * Write JSON file
152
+ * @param data
153
+ * @param parts
154
+ */
155
+ async writeJsonFile(data: unknown, ...parts: string[]): Promise<void> {
156
+ const path = this.getPath(...parts);
157
+ try {
158
+ const dirName = dirname(path);
159
+ if (!existsSync(dirName)) {
160
+ await mkdir(dirName, { recursive: true });
161
+ }
162
+ await writeJsonFile(path, data);
163
+ } catch (error) {
164
+ throw new RuniumError(
165
+ `Failed to write JSON file`,
166
+ ErrorCode.PROFILE_JSON_WRITE_ERROR,
167
+ { path, data, original: error }
168
+ );
169
+ }
170
+ }
171
+ /**
172
+ * Read JSON file
173
+ * @param parts
174
+ */
175
+ private getPath(...parts: string[]): string {
176
+ return join(this.path, ...parts);
177
+ }
178
+
179
+ /**
180
+ * Read plugins from file
181
+ */
182
+ private async readPlugins(): Promise<void> {
183
+ this.plugins =
184
+ ((await readJsonFile(this.getPath(PLUGINS_FILE_NAME)).catch(
185
+ () => []
186
+ )) as ProfilePlugin[]) || this.plugins;
187
+ }
188
+
189
+ /**
190
+ * Write plugins to file
191
+ */
192
+ private async writePlugins(): Promise<void> {
193
+ await writeJsonFile(this.getPath(PLUGINS_FILE_NAME), this.plugins);
194
+ }
195
+ /**
196
+ * Read projects from file
197
+ */
198
+ private async readProjects(): Promise<void> {
199
+ this.projects =
200
+ ((await readJsonFile(this.getPath(PROJECTS_FILE_NAME)).catch(
201
+ () => []
202
+ )) as ProfileProject[]) || this.projects;
203
+ }
204
+
205
+ /**
206
+ * Write projects to file
207
+ */
208
+ private async writeProjects(): Promise<void> {
209
+ await writeJsonFile(this.getPath(PROJECTS_FILE_NAME), this.projects);
210
+ }
211
+ }