@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,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';
|