@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,24 @@
1
+ import { resolve } from 'node:path';
2
+ import { homedir, tmpdir } from 'node:os';
3
+
4
+ /**
5
+ * Resolve path
6
+ * @param path
7
+ */
8
+ export function pathMacro(...path: string[]): string {
9
+ return resolve(...path);
10
+ }
11
+
12
+ /**
13
+ * Tmpdir path
14
+ */
15
+ export function tmpDirMacro(): string {
16
+ return tmpdir();
17
+ }
18
+
19
+ /**
20
+ * Home directory path
21
+ */
22
+ export function homeDirMacro(): string {
23
+ return homedir();
24
+ }
@@ -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
+ }
@@ -0,0 +1,119 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join, resolve } from 'node:path';
4
+ import { Service } from 'typedi';
5
+ import { readJsonFile, RuniumError } from '@runium/core';
6
+ import { ErrorCode } from '@constants';
7
+ import {
8
+ createValidator,
9
+ getConfigSchema,
10
+ getErrorMessages,
11
+ } from '@validation';
12
+
13
+ interface ConfigEnv {
14
+ path: string[];
15
+ }
16
+
17
+ interface ConfigOutput {
18
+ debug: boolean;
19
+ }
20
+
21
+ interface ConfigProfile {
22
+ path: string;
23
+ }
24
+
25
+ interface ConfigPlugin {
26
+ disabled: boolean;
27
+ options: Record<string, unknown>;
28
+ }
29
+
30
+ interface ConfigPlugins {
31
+ [name: string]: ConfigPlugin;
32
+ }
33
+
34
+ interface ConfigData {
35
+ env: ConfigEnv;
36
+ output: ConfigOutput;
37
+ profile: ConfigProfile;
38
+ plugins: ConfigPlugins;
39
+ }
40
+
41
+ const CONFIG_FILE_NAME = '.runiumrc.json';
42
+ const CONFIG_FILE_PATH = join(process.cwd(), CONFIG_FILE_NAME);
43
+ const PROFILE_DIR_NAME = '.runium';
44
+ const HOME_PROFILE_PATH = join(homedir(), PROFILE_DIR_NAME);
45
+ const CWD_PROFILE_PATH = join(process.cwd(), PROFILE_DIR_NAME);
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
+
67
+ @Service()
68
+ export class ConfigService {
69
+ private data: ConfigData = {
70
+ profile: { path: HOME_PROFILE_PATH },
71
+ plugins: {},
72
+ output: { debug: false },
73
+ env: { path: [] },
74
+ };
75
+
76
+ /**
77
+ * Initialize the config service
78
+ */
79
+ async init(): Promise<void> {
80
+ if (existsSync(CWD_PROFILE_PATH)) {
81
+ this.data.profile.path = CWD_PROFILE_PATH;
82
+ }
83
+
84
+ if (existsSync(CONFIG_FILE_PATH)) {
85
+ const configData: Partial<ConfigData> =
86
+ await readJsonFile<Partial<ConfigData>>(CONFIG_FILE_PATH);
87
+ if (configData) {
88
+ validateConfigData(configData);
89
+
90
+ const data = {
91
+ env: Object.assign({}, this.data.env, configData.env ?? {}),
92
+ output: Object.assign({}, this.data.output, configData.output ?? {}),
93
+ profile: Object.assign(
94
+ {},
95
+ this.data.profile,
96
+ configData.profile ?? {}
97
+ ),
98
+ plugins: Object.assign(
99
+ {},
100
+ this.data.plugins,
101
+ configData.plugins ?? {}
102
+ ),
103
+ };
104
+
105
+ data.env.path = data.env.path.map(envPath => resolve(envPath));
106
+ data.profile.path = resolve(data.profile.path);
107
+ this.data = data;
108
+ }
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Get a configuration value
114
+ * @param key
115
+ */
116
+ get<T extends keyof ConfigData>(key: T): ConfigData[T] {
117
+ return this.data[key];
118
+ }
119
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export * from './command.js';
2
+ export * from './config.js';
3
+ export * from './file.js';
4
+ export * from './output.js';
5
+ export * from './profile.js';
6
+ export * from './plugin.js';
7
+ export * from './project.js';
8
+ export * from './shutdown.js';
9
+ export * from './plugin-context.js';