@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.
- package/.eslintrc.json +31 -0
- package/.prettierrc.json +10 -0
- package/README.md +3 -0
- package/build.js +104 -0
- package/lib/app.js +6 -0
- package/lib/commands/index.js +1 -0
- package/lib/commands/plugin/plugin-add.js +1 -0
- package/lib/commands/plugin/plugin-command.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-list.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-list.js +1 -0
- package/lib/commands/project/project-remove.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 +1 -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/lib/constants/index.js +1 -0
- package/lib/index.js +2 -0
- package/lib/macros/conditional.js +1 -0
- package/lib/macros/empty.js +1 -0
- package/lib/macros/env.js +1 -0
- package/lib/macros/index.js +1 -0
- package/lib/macros/path.js +1 -0
- package/lib/package.json +21 -0
- package/lib/services/config.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/convert-path-to-valid-file-name.js +1 -0
- package/lib/utils/debounce.js +1 -0
- package/lib/utils/format-timestamp.js +1 -0
- package/lib/utils/index.js +1 -0
- package/package.json +44 -0
- package/src/app.ts +175 -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 +41 -0
- package/src/commands/project/project-add.ts +46 -0
- package/src/commands/project/project-command.ts +36 -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 +152 -0
- package/src/commands/project/project-state-command.ts +68 -0
- package/src/commands/project/project-status.ts +103 -0
- package/src/commands/project/project-stop.ts +55 -0
- package/src/commands/project/project-validate.ts +43 -0
- package/src/commands/project/project.ts +45 -0
- package/src/commands/runium-command.ts +50 -0
- package/src/constants/error-code.ts +15 -0
- package/src/constants/index.ts +1 -0
- package/src/index.ts +21 -0
- package/src/macros/conditional.ts +31 -0
- package/src/macros/empty.ts +6 -0
- package/src/macros/env.ts +8 -0
- package/src/macros/index.ts +12 -0
- package/src/macros/path.ts +9 -0
- package/src/services/config.ts +76 -0
- package/src/services/index.ts +7 -0
- package/src/services/output.ts +201 -0
- package/src/services/plugin-context.ts +81 -0
- package/src/services/plugin.ts +144 -0
- package/src/services/profile.ts +211 -0
- package/src/services/project.ts +114 -0
- package/src/services/shutdown.ts +130 -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/index.ts +3 -0
- 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
|
+
}
|