@runium/cli 0.0.1 → 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 (79) hide show
  1. package/build.js +21 -0
  2. package/lib/app.js +2 -2
  3. package/lib/commands/plugin/plugin-add.js +1 -1
  4. package/lib/commands/plugin/plugin-disable.js +1 -1
  5. package/lib/commands/plugin/plugin-enable.js +1 -1
  6. package/lib/commands/plugin/plugin-remove.js +1 -1
  7. package/lib/commands/plugin/plugin.js +1 -1
  8. package/lib/commands/project/project-add.js +1 -1
  9. package/lib/commands/project/project-command.js +1 -1
  10. package/lib/commands/project/project-start.js +1 -1
  11. package/lib/commands/project/project-state-command.js +1 -1
  12. package/lib/commands/project/project-status.js +1 -1
  13. package/lib/commands/project/project-stop.js +1 -1
  14. package/lib/commands/project/project-validate.js +4 -1
  15. package/lib/commands/project/project.js +1 -1
  16. package/lib/commands/runium-command.js +1 -1
  17. package/lib/constants/error-code.js +1 -1
  18. package/lib/index.js +1 -1
  19. package/lib/macros/date.js +1 -0
  20. package/lib/macros/index.js +1 -1
  21. package/lib/macros/path.js +1 -1
  22. package/lib/package.json +3 -2
  23. package/lib/services/command.js +1 -0
  24. package/lib/services/config.js +1 -1
  25. package/lib/services/file.js +1 -0
  26. package/lib/services/index.js +1 -1
  27. package/lib/services/output.js +3 -3
  28. package/lib/services/plugin-context.js +1 -1
  29. package/lib/services/plugin.js +1 -1
  30. package/lib/services/profile.js +1 -1
  31. package/lib/services/project.js +1 -1
  32. package/lib/services/shutdown.js +1 -1
  33. package/lib/utils/get-version.js +1 -0
  34. package/lib/utils/index.js +1 -1
  35. package/lib/validation/create-validator.js +1 -0
  36. package/lib/validation/get-config-schema.js +1 -0
  37. package/lib/validation/get-error-messages.js +1 -0
  38. package/lib/validation/get-plugin-schema.js +1 -0
  39. package/lib/validation/index.js +1 -0
  40. package/package.json +5 -2
  41. package/src/app.ts +35 -20
  42. package/src/commands/plugin/plugin-add.ts +2 -2
  43. package/src/commands/plugin/plugin-disable.ts +1 -1
  44. package/src/commands/plugin/plugin-enable.ts +2 -2
  45. package/src/commands/plugin/plugin-remove.ts +1 -1
  46. package/src/commands/plugin/plugin.ts +11 -16
  47. package/src/commands/project/project-add.ts +23 -5
  48. package/src/commands/project/project-command.ts +8 -1
  49. package/src/commands/project/project-start.ts +18 -12
  50. package/src/commands/project/project-state-command.ts +4 -19
  51. package/src/commands/project/project-status.ts +16 -3
  52. package/src/commands/project/project-stop.ts +5 -1
  53. package/src/commands/project/project-validate.ts +23 -10
  54. package/src/commands/project/project.ts +13 -18
  55. package/src/commands/runium-command.ts +18 -16
  56. package/src/constants/error-code.ts +15 -2
  57. package/src/global.d.ts +6 -0
  58. package/src/index.ts +5 -2
  59. package/src/macros/date.ts +15 -0
  60. package/src/macros/index.ts +6 -1
  61. package/src/macros/path.ts +17 -2
  62. package/src/services/command.ts +171 -0
  63. package/src/services/config.ts +47 -4
  64. package/src/services/file.ts +272 -0
  65. package/src/services/index.ts +2 -0
  66. package/src/services/output.ts +5 -1
  67. package/src/services/plugin-context.ts +68 -9
  68. package/src/services/plugin.ts +122 -18
  69. package/src/services/profile.ts +37 -49
  70. package/src/services/project.ts +31 -3
  71. package/src/services/shutdown.ts +25 -8
  72. package/src/utils/get-version.ts +13 -0
  73. package/src/utils/index.ts +1 -0
  74. package/src/validation/create-validator.ts +27 -0
  75. package/src/validation/get-config-schema.ts +59 -0
  76. package/src/validation/get-error-messages.ts +35 -0
  77. package/src/validation/get-plugin-schema.ts +137 -0
  78. package/src/validation/index.ts +4 -0
  79. package/tsconfig.json +8 -10
@@ -0,0 +1,171 @@
1
+ import { Command } from 'commander';
2
+ import { Container, Service } from 'typedi';
3
+ import { isRuniumError, RuniumError } from '@runium/core';
4
+ import { ErrorCode } from '@constants';
5
+ import { PluginService } from '@services';
6
+ import {
7
+ RuniumCommand,
8
+ RuniumCommandConstructor,
9
+ } from '@commands/runium-command.js';
10
+
11
+ const PROGRAM_NAME = 'runium';
12
+
13
+ @Service()
14
+ export class CommandService {
15
+ /**
16
+ * Full path commands
17
+ */
18
+ private fullPathCommands: Map<string, RuniumCommand> = new Map();
19
+
20
+ /**
21
+ * Get command full path recursively
22
+ * @param command
23
+ */
24
+ private getCommandFullPath(command: RuniumCommand): string {
25
+ const commandName = command.command?.name();
26
+
27
+ if (commandName === PROGRAM_NAME || !command.command.parent) {
28
+ return '';
29
+ }
30
+
31
+ // find the parent command instance
32
+ const parentCommand = Array.from(this.fullPathCommands.values()).find(
33
+ cmd => cmd.command === command.command.parent
34
+ );
35
+
36
+ if (parentCommand) {
37
+ // recursively get parent's full path
38
+ const parentPath = this.getCommandFullPath(parentCommand);
39
+ return [parentPath, commandName].filter(Boolean).join(' ').trim();
40
+ }
41
+
42
+ // fallback: if parent not found in map, check if parent is the program
43
+ const parentName = command.command.parent.name();
44
+ if (parentName === PROGRAM_NAME) {
45
+ return commandName;
46
+ }
47
+
48
+ return [parentName, commandName].join(' ').trim();
49
+ }
50
+
51
+ /**
52
+ * Register command
53
+ * @param command
54
+ * @param program
55
+ * @param context
56
+ */
57
+ registerCommand(
58
+ command: RuniumCommandConstructor,
59
+ program: Command,
60
+ context: string = 'app'
61
+ ): void {
62
+ const addCommand = (
63
+ CommandConstructor: RuniumCommandConstructor,
64
+ parent: Command
65
+ ) => {
66
+ try {
67
+ if (!(CommandConstructor.prototype instanceof RuniumCommand)) {
68
+ throw new RuniumError(
69
+ `Command "${CommandConstructor.name}" for "${context}" must be a subclass of "RuniumCommand"`,
70
+ ErrorCode.COMMAND_INCORRECT,
71
+ {
72
+ context,
73
+ CommandConstructor,
74
+ }
75
+ );
76
+ }
77
+
78
+ const commandInstance: RuniumCommand = new CommandConstructor(parent);
79
+
80
+ const commandPath = this.getCommandFullPath(commandInstance);
81
+ this.fullPathCommands.set(commandPath, commandInstance);
82
+
83
+ if (commandInstance.subcommands.length > 0) {
84
+ commandInstance.subcommands.forEach(subcommand => {
85
+ addCommand(subcommand, commandInstance.command);
86
+ });
87
+ }
88
+ } catch (error) {
89
+ if (isRuniumError(error)) {
90
+ throw error;
91
+ }
92
+ throw new RuniumError(
93
+ `Failed to register command "${CommandConstructor.name}" for "${context}"`,
94
+ ErrorCode.COMMAND_REGISTRATION_ERROR,
95
+ { original: error }
96
+ );
97
+ }
98
+ };
99
+
100
+ addCommand(command, program);
101
+ }
102
+
103
+ /**
104
+ * Create run command
105
+ * @param handle
106
+ * @param command
107
+ */
108
+ createRunCommand(
109
+ handle: (...args: unknown[]) => Promise<void>,
110
+ command: RuniumCommand
111
+ ): (...args: unknown[]) => Promise<void> {
112
+ const pluginService = Container.get(PluginService);
113
+ return async (...args: unknown[]): Promise<void> => {
114
+ // run from command action contains command
115
+ // run from plugin context does not contain command
116
+ if (args?.length > 0 && args[args.length - 1] instanceof Command) {
117
+ args.pop();
118
+ }
119
+
120
+ const commandPath = this.getCommandFullPath(command);
121
+
122
+ await pluginService.runHook('app.beforeCommandRun', {
123
+ command: commandPath,
124
+ args,
125
+ });
126
+
127
+ await handle.call(command, ...args);
128
+
129
+ await pluginService.runHook('app.afterCommandRun', {
130
+ command: commandPath,
131
+ args,
132
+ });
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Check if command exists
138
+ * @param path
139
+ */
140
+ hasCommand(path: string): boolean {
141
+ return this.fullPathCommands.has(path);
142
+ }
143
+
144
+ /**
145
+ * Run command
146
+ * @param path
147
+ * @param args
148
+ */
149
+ async runCommand(path: string, ...args: unknown[]): Promise<void> {
150
+ const command = this.fullPathCommands.get(path);
151
+ if (!command) {
152
+ throw new RuniumError(
153
+ `Command "${path}" not found`,
154
+ ErrorCode.COMMAND_NOT_FOUND,
155
+ { path }
156
+ );
157
+ }
158
+ try {
159
+ await command.run(...args);
160
+ } catch (error) {
161
+ if (isRuniumError(error)) {
162
+ throw error;
163
+ }
164
+ throw new RuniumError(
165
+ `Failed to run command "${path}"`,
166
+ ErrorCode.COMMAND_RUN_ERROR,
167
+ { original: error }
168
+ );
169
+ }
170
+ }
171
+ }
@@ -2,7 +2,13 @@ import { existsSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { join, resolve } from 'node:path';
4
4
  import { Service } from 'typedi';
5
- import { readJsonFile } from '@runium/core';
5
+ import { readJsonFile, RuniumError } from '@runium/core';
6
+ import { ErrorCode } from '@constants';
7
+ import {
8
+ createValidator,
9
+ getConfigSchema,
10
+ getErrorMessages,
11
+ } from '@validation';
6
12
 
7
13
  interface ConfigEnv {
8
14
  path: string[];
@@ -16,10 +22,20 @@ interface ConfigProfile {
16
22
  path: string;
17
23
  }
18
24
 
25
+ interface ConfigPlugin {
26
+ disabled: boolean;
27
+ options: Record<string, unknown>;
28
+ }
29
+
30
+ interface ConfigPlugins {
31
+ [name: string]: ConfigPlugin;
32
+ }
33
+
19
34
  interface ConfigData {
20
35
  env: ConfigEnv;
21
36
  output: ConfigOutput;
22
37
  profile: ConfigProfile;
38
+ plugins: ConfigPlugins;
23
39
  }
24
40
 
25
41
  const CONFIG_FILE_NAME = '.runiumrc.json';
@@ -28,10 +44,31 @@ const PROFILE_DIR_NAME = '.runium';
28
44
  const HOME_PROFILE_PATH = join(homedir(), PROFILE_DIR_NAME);
29
45
  const CWD_PROFILE_PATH = join(process.cwd(), PROFILE_DIR_NAME);
30
46
 
47
+ /**
48
+ * Validate config data
49
+ * @param data
50
+ */
51
+ function validateConfigData(data: unknown): void {
52
+ const schema = getConfigSchema();
53
+ const validate = createValidator(schema);
54
+ const result = validate(data);
55
+
56
+ if (!result && validate.errors) {
57
+ const errorMessages = getErrorMessages(validate.errors);
58
+
59
+ throw new RuniumError(
60
+ `Invalid "${CONFIG_FILE_PATH}" configuration data`,
61
+ ErrorCode.CONFIG_INVALID_DATA,
62
+ errorMessages
63
+ );
64
+ }
65
+ }
66
+
31
67
  @Service()
32
68
  export class ConfigService {
33
69
  private data: ConfigData = {
34
70
  profile: { path: HOME_PROFILE_PATH },
71
+ plugins: {},
35
72
  output: { debug: false },
36
73
  env: { path: [] },
37
74
  };
@@ -45,10 +82,11 @@ export class ConfigService {
45
82
  }
46
83
 
47
84
  if (existsSync(CONFIG_FILE_PATH)) {
48
- // TODO validate config file
49
- const configData: ConfigData =
50
- await readJsonFile<ConfigData>(CONFIG_FILE_PATH);
85
+ const configData: Partial<ConfigData> =
86
+ await readJsonFile<Partial<ConfigData>>(CONFIG_FILE_PATH);
51
87
  if (configData) {
88
+ validateConfigData(configData);
89
+
52
90
  const data = {
53
91
  env: Object.assign({}, this.data.env, configData.env ?? {}),
54
92
  output: Object.assign({}, this.data.output, configData.output ?? {}),
@@ -57,6 +95,11 @@ export class ConfigService {
57
95
  this.data.profile,
58
96
  configData.profile ?? {}
59
97
  ),
98
+ plugins: Object.assign(
99
+ {},
100
+ this.data.plugins,
101
+ configData.plugins ?? {}
102
+ ),
60
103
  };
61
104
 
62
105
  data.env.path = data.env.path.map(envPath => resolve(envPath));
@@ -0,0 +1,272 @@
1
+ import { PathLike } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import {
4
+ access,
5
+ constants,
6
+ mkdir,
7
+ readFile,
8
+ rename,
9
+ writeFile,
10
+ } from 'node:fs/promises';
11
+ import { fileURLToPath } from 'node:url';
12
+
13
+ import { Service } from 'typedi';
14
+ import { JSONObject, RuniumError } from '@runium/core';
15
+ import { ErrorCode } from '@constants';
16
+
17
+ type Resolve = () => void;
18
+ type Reject = (error: Error) => void;
19
+ type Data = Parameters<typeof writeFile>[1];
20
+
21
+ @Service()
22
+ export class FileService {
23
+ /**
24
+ * Reads a file as text
25
+ * @param path
26
+ * @param options
27
+ */
28
+ async read(
29
+ path: string,
30
+ options: { encoding?: BufferEncoding } = {}
31
+ ): Promise<string> {
32
+ try {
33
+ return await readFile(path, { encoding: options.encoding || 'utf-8' });
34
+ } catch (ex) {
35
+ throw new RuniumError(
36
+ `Can not read file ${path}`,
37
+ ErrorCode.FILE_READ_ERROR,
38
+ { path, options, original: ex }
39
+ );
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Writes data to a file
45
+ * @param path
46
+ * @param data
47
+ * @param options
48
+ */
49
+ async write(
50
+ path: string,
51
+ data: string,
52
+ options: { encoding?: BufferEncoding } = {}
53
+ ): Promise<void> {
54
+ try {
55
+ await writeFile(path, data, { encoding: options.encoding || 'utf-8' });
56
+ } catch (ex) {
57
+ throw new RuniumError(
58
+ `Can not write file ${path}`,
59
+ ErrorCode.FILE_WRITE_ERROR,
60
+ { path, data, options, original: ex }
61
+ );
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Reads a JSON file
67
+ * @param path
68
+ */
69
+ async readJson<T = JSONObject>(path: string): Promise<T> {
70
+ try {
71
+ const data = await readFile(path, { encoding: 'utf-8' });
72
+ return JSON.parse(data);
73
+ } catch (ex) {
74
+ throw new RuniumError(
75
+ `Can not read JSON file ${path}`,
76
+ ErrorCode.FILE_READ_JSON_ERROR,
77
+ { path, original: ex }
78
+ );
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Writes a JSON file
84
+ * @param path
85
+ * @param data
86
+ */
87
+ async writeJson<T = JSONObject>(path: string, data: T): Promise<void> {
88
+ try {
89
+ await writeFile(path, JSON.stringify(data, null, 2), {
90
+ encoding: 'utf-8',
91
+ });
92
+ } catch (ex) {
93
+ throw new RuniumError(
94
+ `Can not write JSON file ${path}`,
95
+ ErrorCode.FILE_WRITE_JSON_ERROR,
96
+ { path, data, original: ex }
97
+ );
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Checks if a file or directory exists
103
+ * @param path
104
+ */
105
+ async isExists(path: string): Promise<boolean> {
106
+ try {
107
+ await access(path, constants.F_OK);
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+
114
+ /**
115
+ * Create directory recursively if it does not exist
116
+ * @param path
117
+ */
118
+ async ensureDirExists(path: string): Promise<void> {
119
+ try {
120
+ await mkdir(path, { recursive: true });
121
+ } catch (ex) {
122
+ throw new RuniumError(
123
+ `Can not create directory ${path}`,
124
+ ErrorCode.FILE_CREATE_DIR_ERROR,
125
+ { path, original: ex }
126
+ );
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Create an atomic file writer
132
+ * @param path
133
+ */
134
+ createAtomicWriter(path: PathLike): AtomicWriter {
135
+ return new AtomicWriter(path);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Creates a temporary file name for a given file
141
+ * @param file
142
+ */
143
+ function getTempFilename(file: PathLike): string {
144
+ const f = file instanceof URL ? fileURLToPath(file) : file.toString();
145
+ return join(dirname(f), `.${basename(f)}.tmp`);
146
+ }
147
+
148
+ /**
149
+ * Retries an asynchronous operation with a delay between retries and a maximum retry count
150
+ * @param fn
151
+ * @param maxRetries
152
+ * @param delayMs
153
+ */
154
+ async function retryAsyncOperation(
155
+ fn: () => Promise<void>,
156
+ maxRetries: number,
157
+ delayMs: number
158
+ ): Promise<void> {
159
+ for (let i = 0; i < maxRetries; i++) {
160
+ try {
161
+ return await fn();
162
+ } catch (error) {
163
+ if (i < maxRetries - 1) {
164
+ await new Promise(resolve => setTimeout(resolve, delayMs));
165
+ } else {
166
+ throw error;
167
+ }
168
+ }
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Atomic file writer
174
+ *
175
+ * Allows writing to a file atomically
176
+ * based on https://github.com/typicode/steno
177
+ */
178
+ export class AtomicWriter {
179
+ private readonly filename: PathLike;
180
+ private readonly tempFilename: PathLike;
181
+ private locked = false;
182
+ private prev: [Resolve, Reject] | null = null;
183
+ private next: [Resolve, Reject] | null = null;
184
+ private nextPromise: Promise<void> | null = null;
185
+ private nextData: Data | null = null;
186
+
187
+ constructor(filename: PathLike) {
188
+ this.filename = filename;
189
+ this.tempFilename = getTempFilename(filename);
190
+ }
191
+
192
+ /**
193
+ * Add data for later write
194
+ * @param data
195
+ */
196
+ private addData(data: Data): Promise<void> {
197
+ // keep only most recent data
198
+ this.nextData = data;
199
+
200
+ // create a singleton promise to resolve all next promises once next data is written
201
+ this.nextPromise ||= new Promise((resolve, reject) => {
202
+ this.next = [resolve, reject];
203
+ });
204
+
205
+ // return a promise that will resolve at the same time as next promise
206
+ return new Promise((resolve, reject) => {
207
+ this.nextPromise?.then(resolve).catch(reject);
208
+ });
209
+ }
210
+
211
+ /**
212
+ * Write data to a file atomically
213
+ * @param data
214
+ */
215
+ private async writeData(data: Data): Promise<void> {
216
+ this.locked = true;
217
+ try {
218
+ await writeFile(this.tempFilename, data, 'utf-8');
219
+ await retryAsyncOperation(
220
+ async () => {
221
+ await rename(this.tempFilename, this.filename);
222
+ },
223
+ 10,
224
+ 100
225
+ );
226
+
227
+ // resolve
228
+ this.prev?.[0]();
229
+ } catch (err) {
230
+ // reject
231
+ if (err instanceof Error) {
232
+ this.prev?.[1](err);
233
+ }
234
+ throw err;
235
+ } finally {
236
+ this.locked = false;
237
+
238
+ this.prev = this.next;
239
+ this.next = this.nextPromise = null;
240
+
241
+ if (this.nextData !== null) {
242
+ const nextData = this.nextData;
243
+ this.nextData = null;
244
+ await this.write(nextData);
245
+ }
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Write data to a file atomically
251
+ * @param data
252
+ */
253
+ async write(data: Data): Promise<void> {
254
+ try {
255
+ await (this.locked ? this.addData(data) : this.writeData(data));
256
+ } catch (ex) {
257
+ throw new RuniumError(
258
+ `Can not write file ${this.filename}`,
259
+ ErrorCode.FILE_WRITE_ERROR,
260
+ { path: this.filename, data, original: ex }
261
+ );
262
+ }
263
+ }
264
+
265
+ /**
266
+ * Writes a JSON file
267
+ * @param data
268
+ */
269
+ async writeJson<T = JSONObject>(data: T): Promise<void> {
270
+ return this.write(JSON.stringify(data, null, 2));
271
+ }
272
+ }
@@ -1,4 +1,6 @@
1
+ export * from './command.js';
1
2
  export * from './config.js';
3
+ export * from './file.js';
2
4
  export * from './output.js';
3
5
  export * from './profile.js';
4
6
  export * from './plugin.js';
@@ -1,5 +1,6 @@
1
1
  import { Console } from 'node:console';
2
2
  import { Transform } from 'node:stream';
3
+ import { inspect } from 'node:util';
3
4
 
4
5
  import { Service } from 'typedi';
5
6
 
@@ -20,6 +21,8 @@ class ConsoleDumper extends Console {
20
21
  private readonly transform: Transform;
21
22
 
22
23
  constructor() {
24
+ inspect.defaultOptions.depth = 5;
25
+
23
26
  const transform = new Transform({
24
27
  transform: (chunk, _, cb) => cb(null, chunk),
25
28
  });
@@ -50,7 +53,8 @@ class ConsoleDumper extends Console {
50
53
  return original
51
54
  .split('\n')
52
55
  .map((line: string) => line.charAt(0) + line.slice(columnWidth))
53
- .join('\n');
56
+ .join('\n')
57
+ .replace(/'([^']*)'/g, '$1 ');
54
58
  }
55
59
  }
56
60
 
@@ -1,34 +1,80 @@
1
+ import { delimiter } from 'node:path';
1
2
  import { Inject, Service } from 'typedi';
2
-
3
3
  import {
4
4
  RuniumError,
5
5
  isRuniumError,
6
6
  RuniumTask,
7
7
  RuniumTrigger,
8
- readJsonFile,
9
- writeJsonFile,
10
8
  applyMacros,
9
+ TaskEvent,
11
10
  TaskStatus,
12
11
  ProjectEvent,
13
12
  ProjectStatus,
14
13
  } from '@runium/core';
15
14
  import { RuniumCommand } from '@commands/runium-command.js';
16
- import { OutputLevel, OutputService, ShutdownService } from '@services';
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';
17
25
 
18
- // @ts-expect-error global object access
19
26
  global.runium = null;
20
27
 
21
28
  @Service()
22
29
  export class PluginContextService {
23
30
  constructor(
31
+ @Inject() private commandService: CommandService,
24
32
  @Inject() private outputService: OutputService,
25
- @Inject() private shutdownService: ShutdownService
33
+ @Inject() private shutdownService: ShutdownService,
34
+ @Inject() private fileService: FileService,
35
+ @Inject() private profileService: ProfileService
26
36
  ) {}
27
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
+
28
73
  /**
29
74
  * Initialize the plugin context service
30
75
  */
31
76
  async init(): Promise<void> {
77
+ const command = this.commandService;
32
78
  const output = this.outputService;
33
79
  const shutdown = this.shutdownService;
34
80
 
@@ -51,13 +97,12 @@ export class PluginContextService {
51
97
  ),
52
98
  ProjectEvent,
53
99
  ProjectStatus,
100
+ TaskEvent,
54
101
  TaskStatus,
55
102
  },
56
103
  utils: {
57
104
  applyMacros,
58
105
  isRuniumError,
59
- readJsonFile,
60
- writeJsonFile,
61
106
  },
62
107
  output: {
63
108
  getLevel: output.getLevel.bind(output),
@@ -74,8 +119,22 @@ export class PluginContextService {
74
119
  addBlocker: shutdown.addBlocker.bind(shutdown),
75
120
  removeBlocker: shutdown.removeBlocker.bind(shutdown),
76
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(),
77
137
  };
78
- // @ts-expect-error global object access
79
138
  global.runium = Object.freeze(runium);
80
139
  }
81
140
  }