@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,205 @@
1
+ import { Console } from 'node:console';
2
+ import { Transform } from 'node:stream';
3
+ import { inspect } from 'node:util';
4
+
5
+ import { Service } from 'typedi';
6
+
7
+ export enum OutputLevel {
8
+ TRACE = 0,
9
+ DEBUG = 1,
10
+ INFO = 2,
11
+ WARN = 3,
12
+ ERROR = 4,
13
+ SILENT = 5,
14
+ }
15
+
16
+ /**
17
+ * Console dumper
18
+ * wrapper around node console with a transform stream
19
+ */
20
+ class ConsoleDumper extends Console {
21
+ private readonly transform: Transform;
22
+
23
+ constructor() {
24
+ inspect.defaultOptions.depth = 5;
25
+
26
+ const transform = new Transform({
27
+ transform: (chunk, _, cb) => cb(null, chunk),
28
+ });
29
+ super({
30
+ stdout: transform,
31
+ stderr: transform,
32
+ colorMode: false,
33
+ });
34
+ this.transform = transform;
35
+ }
36
+
37
+ /**
38
+ * Get a table output with index column removed
39
+ * @param data
40
+ * @param columns
41
+ */
42
+ getPatchedTable(data: unknown[], columns?: string[]): string {
43
+ this.table(data, columns);
44
+
45
+ const original = (this.transform.read() || '').toString();
46
+
47
+ // Tables should all start with roughly:
48
+ // ┌─────────┬──────
49
+ // │ (index) │
50
+ // ├─────────┼
51
+ const columnWidth = original.indexOf('┬') + 1;
52
+
53
+ return original
54
+ .split('\n')
55
+ .map((line: string) => line.charAt(0) + line.slice(columnWidth))
56
+ .join('\n')
57
+ .replace(/'([^']*)'/g, '$1 ');
58
+ }
59
+ }
60
+
61
+ const dumper = new ConsoleDumper();
62
+
63
+ @Service()
64
+ export class OutputService {
65
+ private outputLevel: OutputLevel = OutputLevel.INFO;
66
+
67
+ /**
68
+ * Set output level
69
+ * @param level
70
+ */
71
+ setLevel(level: OutputLevel): void {
72
+ this.outputLevel = level;
73
+ }
74
+
75
+ /**
76
+ * Get output level
77
+ */
78
+ getLevel(): OutputLevel {
79
+ return this.outputLevel;
80
+ }
81
+
82
+ /**
83
+ * Log a trace message
84
+ * @param message
85
+ * @param args
86
+ */
87
+ trace(message: string, ...args: unknown[]): void {
88
+ if (this.outputLevel <= OutputLevel.TRACE) {
89
+ // eslint-disable-next-line no-console
90
+ console.log(message, ...args);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Log a debug message
96
+ * @param message
97
+ * @param args
98
+ */
99
+ debug(message: string, ...args: unknown[]): void {
100
+ if (this.outputLevel <= OutputLevel.DEBUG) {
101
+ // eslint-disable-next-line no-console
102
+ console.log(message, ...args);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Log an info message
108
+ * @param message
109
+ * @param args
110
+ */
111
+ info(message: string, ...args: unknown[]): void {
112
+ if (this.outputLevel <= OutputLevel.INFO) {
113
+ // eslint-disable-next-line no-console
114
+ console.log(message, ...args);
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Log a success message
120
+ * @param message
121
+ * @param args
122
+ */
123
+ success(message: string, ...args: unknown[]): void {
124
+ if (this.outputLevel <= OutputLevel.INFO) {
125
+ // eslint-disable-next-line no-console
126
+ console.log(message, ...args);
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Log a warning message
132
+ * @param message
133
+ * @param args
134
+ */
135
+ warn(message: string, ...args: unknown[]): void {
136
+ if (this.outputLevel <= OutputLevel.WARN) {
137
+ // eslint-disable-next-line no-console
138
+ console.warn(message, ...args);
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Log an error message
144
+ * @param message
145
+ * @param args
146
+ */
147
+ error(message: string, ...args: unknown[]): void {
148
+ if (this.outputLevel <= OutputLevel.ERROR) {
149
+ // eslint-disable-next-line no-console
150
+ console.error(message, ...args);
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Log a message without level
156
+ * @param message
157
+ * @param args
158
+ */
159
+ log(message: string, ...args: unknown[]): void {
160
+ if (this.outputLevel < OutputLevel.SILENT) {
161
+ // eslint-disable-next-line no-console
162
+ console.log(message, ...args);
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Output a table
168
+ * @param data
169
+ * @param columns
170
+ */
171
+ table(data: unknown[], columns?: string[]): void {
172
+ if (this.outputLevel < OutputLevel.SILENT) {
173
+ const patchedData = data.map((item, index) => ({
174
+ ...(item as object),
175
+ '#': index + 1,
176
+ }));
177
+ const patchedOutput = dumper.getPatchedTable(
178
+ patchedData,
179
+ columns ? ['#', ...columns] : undefined
180
+ );
181
+ // eslint-disable-next-line no-console
182
+ console.log(patchedOutput);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Output a blank line
188
+ */
189
+ newLine(): void {
190
+ if (this.outputLevel < OutputLevel.SILENT) {
191
+ // eslint-disable-next-line no-console
192
+ console.log('');
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Clear output
198
+ */
199
+ clear(): void {
200
+ if (this.outputLevel < OutputLevel.SILENT) {
201
+ // eslint-disable-next-line no-console
202
+ console.clear();
203
+ }
204
+ }
205
+ }
@@ -0,0 +1,140 @@
1
+ import { delimiter } from 'node:path';
2
+ import { Inject, Service } from 'typedi';
3
+ import {
4
+ RuniumError,
5
+ isRuniumError,
6
+ RuniumTask,
7
+ RuniumTrigger,
8
+ applyMacros,
9
+ TaskEvent,
10
+ TaskStatus,
11
+ ProjectEvent,
12
+ ProjectStatus,
13
+ } from '@runium/core';
14
+ import { RuniumCommand } from '@commands/runium-command.js';
15
+ import {
16
+ CommandService,
17
+ FileService,
18
+ OutputLevel,
19
+ OutputService,
20
+ ProfileService,
21
+ ShutdownService,
22
+ } from '@services';
23
+ import { getVersion } from '@utils';
24
+ import { ErrorCode } from '@constants';
25
+
26
+ global.runium = null;
27
+
28
+ @Service()
29
+ export class PluginContextService {
30
+ constructor(
31
+ @Inject() private commandService: CommandService,
32
+ @Inject() private outputService: OutputService,
33
+ @Inject() private shutdownService: ShutdownService,
34
+ @Inject() private fileService: FileService,
35
+ @Inject() private profileService: ProfileService
36
+ ) {}
37
+
38
+ /**
39
+ * Create a wrapper for a file service method that processes path parts
40
+ * @param methodName
41
+ */
42
+ private createStorageWrapper<T extends keyof FileService>(
43
+ methodName: T
44
+ ): FileService[T] {
45
+ return ((...args: unknown[]) => {
46
+ const [pathParts, ...rest] = args;
47
+ const resolvedPath = this.resolveProfilePath(
48
+ pathParts as string | string[]
49
+ );
50
+ const method = this.fileService[methodName] as (
51
+ ...args: unknown[]
52
+ ) => unknown;
53
+ return method(resolvedPath, ...rest);
54
+ }) as FileService[T];
55
+ }
56
+
57
+ /**
58
+ * Resolve path parts using profileService
59
+ * @param pathParts
60
+ */
61
+ private resolveProfilePath(pathParts: string | string[]): string {
62
+ const parts = Array.isArray(pathParts)
63
+ ? pathParts
64
+ : pathParts.split(delimiter);
65
+ if (parts.length === 0 || parts.every(part => part.trim() === '')) {
66
+ throw new RuniumError('Invalid path', ErrorCode.INVALID_PATH, {
67
+ path: pathParts,
68
+ });
69
+ }
70
+ return this.profileService.getPath(...parts);
71
+ }
72
+
73
+ /**
74
+ * Initialize the plugin context service
75
+ */
76
+ async init(): Promise<void> {
77
+ const command = this.commandService;
78
+ const output = this.outputService;
79
+ const shutdown = this.shutdownService;
80
+
81
+ const runium = {
82
+ class: {
83
+ RuniumCommand,
84
+ RuniumError,
85
+ RuniumTask,
86
+ RuniumTrigger,
87
+ },
88
+ enum: {
89
+ OutputLevel: Object.keys(OutputLevel)
90
+ .filter(key => isNaN(Number(key)))
91
+ .reduce(
92
+ (acc, key) => {
93
+ acc[key] = OutputLevel[key as keyof typeof OutputLevel];
94
+ return acc;
95
+ },
96
+ {} as Record<string, number>
97
+ ),
98
+ ProjectEvent,
99
+ ProjectStatus,
100
+ TaskEvent,
101
+ TaskStatus,
102
+ },
103
+ utils: {
104
+ applyMacros,
105
+ isRuniumError,
106
+ },
107
+ output: {
108
+ getLevel: output.getLevel.bind(output),
109
+ setLevel: output.setLevel.bind(output),
110
+ trace: output.trace.bind(output),
111
+ debug: output.debug.bind(output),
112
+ info: output.info.bind(output),
113
+ warn: output.warn.bind(output),
114
+ error: output.error.bind(output),
115
+ table: output.table.bind(output),
116
+ log: output.log.bind(output),
117
+ },
118
+ shutdown: {
119
+ addBlocker: shutdown.addBlocker.bind(shutdown),
120
+ removeBlocker: shutdown.removeBlocker.bind(shutdown),
121
+ },
122
+ command: {
123
+ has: command.hasCommand.bind(command),
124
+ run: command.runCommand.bind(command),
125
+ },
126
+ storage: {
127
+ read: this.createStorageWrapper('read'),
128
+ write: this.createStorageWrapper('write'),
129
+ readJson: this.createStorageWrapper('readJson'),
130
+ writeJson: this.createStorageWrapper('writeJson'),
131
+ isExists: this.createStorageWrapper('isExists'),
132
+ ensureDirExists: this.createStorageWrapper('ensureDirExists'),
133
+ createAtomicWriter: this.createStorageWrapper('createAtomicWriter'),
134
+ getPath: this.resolveProfilePath.bind(this),
135
+ },
136
+ version: getVersion(),
137
+ };
138
+ global.runium = Object.freeze(runium);
139
+ }
140
+ }
@@ -0,0 +1,248 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { Inject, Service } from 'typedi';
4
+ import {
5
+ isRuniumError,
6
+ MacrosCollection,
7
+ Project,
8
+ ProjectConfig,
9
+ ProjectSchemaExtension,
10
+ RuniumError,
11
+ RuniumTaskConstructor,
12
+ RuniumTriggerConstructor,
13
+ RuniumTriggerOptions,
14
+ } from '@runium/core';
15
+ import { RuniumCommandConstructor } from '@commands/runium-command.js';
16
+ import { ErrorCode } from '@constants';
17
+ import { OutputService } from '@services';
18
+ import {
19
+ createValidator,
20
+ getErrorMessages,
21
+ getPluginSchema,
22
+ } from '@validation';
23
+
24
+ type PluginModule = { default: (options?: PluginOptions) => Plugin };
25
+
26
+ type PluginHookErrorHandler = (error: RuniumError) => void;
27
+
28
+ type PluginHookName =
29
+ | `app.${keyof PluginAppHooksDefinition}`
30
+ | `project.${keyof PluginProjectHooksDefinition}`;
31
+
32
+ interface PluginHookOptions<M extends boolean> {
33
+ mutable?: M;
34
+ onError?: PluginHookErrorHandler;
35
+ }
36
+
37
+ export interface PluginProjectDefinition {
38
+ macros?: MacrosCollection;
39
+ tasks?: Record<string, RuniumTaskConstructor>;
40
+ actions?: Record<string, (payload: unknown) => void>;
41
+ triggers?: Record<string, RuniumTriggerConstructor<RuniumTriggerOptions>>;
42
+ validationSchema?: ProjectSchemaExtension;
43
+ }
44
+
45
+ export type PluginOptions = Record<string, unknown>;
46
+
47
+ export interface PluginAppDefinition {
48
+ commands?: RuniumCommandConstructor[];
49
+ }
50
+
51
+ export interface PluginProjectHooksDefinition {
52
+ beforeConfigRead?(path: string): Promise<void>;
53
+ afterConfigRead?(content: string): Promise<string>;
54
+ afterConfigMacrosApply?(content: string): Promise<string>;
55
+ afterConfigParse?<T extends ProjectConfig>(config: T): Promise<T>;
56
+ beforeStart?(project: Project): Promise<void>;
57
+ }
58
+
59
+ export interface PluginAppHooksDefinition {
60
+ afterInit?(params: { profilePath: string }): Promise<void>;
61
+ beforeExit?(reason?: string): Promise<void>;
62
+ beforeCommandRun?(params: {
63
+ command: string;
64
+ args: unknown[];
65
+ }): Promise<void>;
66
+ afterCommandRun?(params: { command: string; args: unknown[] }): Promise<void>;
67
+ }
68
+
69
+ export interface PluginHooksDefinition {
70
+ app?: PluginAppHooksDefinition;
71
+ project?: PluginProjectHooksDefinition;
72
+ }
73
+
74
+ export interface Plugin {
75
+ name: string;
76
+ project?: PluginProjectDefinition;
77
+ options?: PluginOptions;
78
+ app?: PluginAppDefinition;
79
+ hooks?: PluginHooksDefinition;
80
+ }
81
+
82
+ @Service()
83
+ export class PluginService {
84
+ /**
85
+ * Loaded plugins
86
+ */
87
+ private plugins: Map<string, Plugin> = new Map();
88
+
89
+ /**
90
+ * Validate plugin schema
91
+ * @private
92
+ */
93
+ private validator: ReturnType<typeof createValidator> =
94
+ createValidator(getPluginSchema());
95
+
96
+ constructor(@Inject() private outputService: OutputService) {}
97
+
98
+ /**
99
+ * Get all plugins
100
+ */
101
+ getAllPlugins(): Plugin[] {
102
+ return Array.from(this.plugins.values());
103
+ }
104
+
105
+ /**
106
+ * Get plugin by name
107
+ * @param name
108
+ */
109
+ getPluginByName(name: string): Plugin | undefined {
110
+ return this.plugins.get(name);
111
+ }
112
+
113
+ /**
114
+ * Load plugin
115
+ * @param path
116
+ * @param options
117
+ */
118
+ async loadPlugin(path: string, options?: PluginOptions): Promise<string> {
119
+ if (!path || !existsSync(path)) {
120
+ throw new RuniumError(
121
+ `Plugin file "${path}" does not exist`,
122
+ ErrorCode.PLUGIN_FILE_NOT_FOUND,
123
+ { path }
124
+ );
125
+ }
126
+
127
+ try {
128
+ const pluginModule = (await import(path)) as PluginModule;
129
+ const { default: getPlugin } = pluginModule;
130
+ if (!getPlugin || typeof getPlugin !== 'function') {
131
+ throw new RuniumError(
132
+ 'Plugin module must have a default function',
133
+ ErrorCode.PLUGIN_INCORRECT_MODULE,
134
+ { path }
135
+ );
136
+ }
137
+ const plugin = getPlugin(options);
138
+
139
+ this.validate(plugin);
140
+
141
+ this.plugins.set(plugin.name, plugin);
142
+
143
+ return plugin.name;
144
+ } catch (error) {
145
+ if (isRuniumError(error)) {
146
+ throw error;
147
+ }
148
+ throw new RuniumError(
149
+ `Failed to load plugin "${path}"`,
150
+ ErrorCode.PLUGIN_LOAD_ERROR,
151
+ { path, original: error }
152
+ );
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Unload plugin
158
+ * @param name
159
+ */
160
+ async unloadPlugin(name: string): Promise<boolean> {
161
+ return this.plugins.delete(name);
162
+ }
163
+
164
+ /**
165
+ * Resolve path
166
+ * @param path
167
+ * @param isFile
168
+ */
169
+ resolvePath(path: string, isFile: boolean = false): string {
170
+ try {
171
+ const resolvedPath = isFile ? resolve(path) : import.meta.resolve(path);
172
+ return resolvedPath.replace('file://', '');
173
+ } catch (error) {
174
+ throw new RuniumError(
175
+ `Failed to resolve plugin path "${path}"`,
176
+ ErrorCode.PLUGIN_PATH_RESOLVE_ERROR,
177
+ { path, original: error }
178
+ );
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Run plugin hook
184
+ * @param name
185
+ * @param params
186
+ * @param options
187
+ */
188
+ async runHook<T, M extends boolean>(
189
+ name: PluginHookName,
190
+ params: T,
191
+ { mutable, onError }: PluginHookOptions<M> = {}
192
+ ): Promise<M extends true ? T : void> {
193
+ const plugins = this.getAllPlugins();
194
+
195
+ const [group, hookName] = name.split('.') as [
196
+ keyof PluginHooksDefinition,
197
+ keyof PluginHooksDefinition[keyof PluginHooksDefinition],
198
+ ];
199
+
200
+ for (const plugin of plugins) {
201
+ const hook = plugin.hooks?.[group]?.[hookName] as
202
+ | ((params: T) => Promise<T>)
203
+ | undefined;
204
+ if (hook) {
205
+ try {
206
+ if (!mutable) {
207
+ await hook(params);
208
+ } else {
209
+ params = (await hook(params)) ?? params;
210
+ }
211
+ } catch (ex) {
212
+ const error = new RuniumError(
213
+ `Failed to run "${plugin.name}.${name}" hook`,
214
+ ErrorCode.PLUGIN_HOOK_ERROR,
215
+ { plugin: plugin.name, hook: name, original: ex }
216
+ );
217
+ if (onError) {
218
+ onError(error);
219
+ } else {
220
+ this.outputService.error('Error: %s', error.message);
221
+ this.outputService.debug('Error details:', {
222
+ code: error.code,
223
+ payload: error.payload,
224
+ });
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ return (mutable ? params : undefined) as M extends true ? T : void;
231
+ }
232
+
233
+ /**
234
+ * Validate plugin
235
+ * @param plugin
236
+ */
237
+ private validate(plugin: Plugin): void {
238
+ const result = this.validator(plugin || {});
239
+ if (!result && this.validator.errors) {
240
+ const errorMessages = getErrorMessages(this.validator.errors);
241
+ throw new RuniumError(
242
+ 'Incorrect plugin format',
243
+ ErrorCode.PLUGIN_INVALID,
244
+ { errors: errorMessages }
245
+ );
246
+ }
247
+ }
248
+ }