@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.
- package/.eslintrc.json +31 -0
- package/.prettierrc.json +10 -0
- package/README.md +3 -0
- package/build.js +125 -0
- package/lib/app.js +6 -0
- package/{commands → lib/commands}/index.js +0 -0
- package/lib/commands/plugin/plugin-add.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-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-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 +4 -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/{constants → lib/constants}/index.js +0 -0
- package/lib/index.js +2 -0
- package/lib/macros/date.js +1 -0
- package/lib/macros/index.js +1 -0
- package/lib/macros/path.js +1 -0
- package/lib/package.json +22 -0
- package/lib/services/command.js +1 -0
- package/lib/services/config.js +1 -0
- package/lib/services/file.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/get-version.js +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/validation/create-validator.js +1 -0
- package/lib/validation/get-config-schema.js +1 -0
- package/lib/validation/get-error-messages.js +1 -0
- package/lib/validation/get-plugin-schema.js +1 -0
- package/lib/validation/index.js +1 -0
- package/package.json +33 -7
- package/src/app.ts +190 -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 +36 -0
- package/src/commands/project/project-add.ts +64 -0
- package/src/commands/project/project-command.ts +43 -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 +158 -0
- package/src/commands/project/project-state-command.ts +53 -0
- package/src/commands/project/project-status.ts +116 -0
- package/src/commands/project/project-stop.ts +59 -0
- package/src/commands/project/project-validate.ts +56 -0
- package/src/commands/project/project.ts +40 -0
- package/src/commands/runium-command.ts +52 -0
- package/src/constants/error-code.ts +28 -0
- package/src/constants/index.ts +1 -0
- package/src/global.d.ts +6 -0
- package/src/index.ts +24 -0
- package/src/macros/conditional.ts +31 -0
- package/src/macros/date.ts +15 -0
- package/src/macros/empty.ts +6 -0
- package/src/macros/env.ts +8 -0
- package/src/macros/index.ts +17 -0
- package/src/macros/path.ts +24 -0
- package/src/services/command.ts +171 -0
- package/src/services/config.ts +119 -0
- package/src/services/file.ts +272 -0
- package/src/services/index.ts +9 -0
- package/src/services/output.ts +205 -0
- package/src/services/plugin-context.ts +140 -0
- package/src/services/plugin.ts +248 -0
- package/src/services/profile.ts +199 -0
- package/src/services/project.ts +142 -0
- package/src/services/shutdown.ts +147 -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/get-version.ts +13 -0
- package/src/utils/index.ts +4 -0
- package/src/validation/create-validator.ts +27 -0
- package/src/validation/get-config-schema.ts +59 -0
- package/src/validation/get-error-messages.ts +35 -0
- package/src/validation/get-plugin-schema.ts +137 -0
- package/src/validation/index.ts +4 -0
- package/tsconfig.json +38 -0
- package/app.js +0 -6
- package/commands/plugin/plugin-add.js +0 -1
- package/commands/plugin/plugin-disable.js +0 -1
- package/commands/plugin/plugin-enable.js +0 -1
- package/commands/plugin/plugin-remove.js +0 -1
- package/commands/plugin/plugin.js +0 -1
- package/commands/project/project-add.js +0 -1
- package/commands/project/project-command.js +0 -1
- package/commands/project/project-start.js +0 -1
- package/commands/project/project-state-command.js +0 -1
- package/commands/project/project-status.js +0 -1
- package/commands/project/project-stop.js +0 -1
- package/commands/project/project-validate.js +0 -1
- package/commands/project/project.js +0 -1
- package/commands/runium-command.js +0 -1
- package/constants/error-code.js +0 -1
- package/index.js +0 -2
- package/macros/index.js +0 -1
- package/macros/path.js +0 -1
- package/services/config.js +0 -1
- package/services/index.js +0 -1
- package/services/output.js +0 -3
- package/services/plugin-context.js +0 -1
- package/services/plugin.js +0 -1
- package/services/profile.js +0 -1
- package/services/project.js +0 -1
- package/services/shutdown.js +0 -1
- package/utils/index.js +0 -1
- /package/{commands → lib/commands}/plugin/plugin-command.js +0 -0
- /package/{commands → lib/commands}/plugin/plugin-list.js +0 -0
- /package/{commands → lib/commands}/project/project-list.js +0 -0
- /package/{commands → lib/commands}/project/project-remove.js +0 -0
- /package/{macros → lib/macros}/conditional.js +0 -0
- /package/{macros → lib/macros}/empty.js +0 -0
- /package/{macros → lib/macros}/env.js +0 -0
- /package/{utils → lib/utils}/convert-path-to-valid-file-name.js +0 -0
- /package/{utils → lib/utils}/debounce.js +0 -0
- /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
|
+
}
|