@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.
Files changed (87) hide show
  1. package/.eslintrc.json +31 -0
  2. package/.prettierrc.json +10 -0
  3. package/README.md +3 -0
  4. package/build.js +104 -0
  5. package/lib/app.js +6 -0
  6. package/lib/commands/index.js +1 -0
  7. package/lib/commands/plugin/plugin-add.js +1 -0
  8. package/lib/commands/plugin/plugin-command.js +1 -0
  9. package/lib/commands/plugin/plugin-disable.js +1 -0
  10. package/lib/commands/plugin/plugin-enable.js +1 -0
  11. package/lib/commands/plugin/plugin-list.js +1 -0
  12. package/lib/commands/plugin/plugin-remove.js +1 -0
  13. package/lib/commands/plugin/plugin.js +1 -0
  14. package/lib/commands/project/project-add.js +1 -0
  15. package/lib/commands/project/project-command.js +1 -0
  16. package/lib/commands/project/project-list.js +1 -0
  17. package/lib/commands/project/project-remove.js +1 -0
  18. package/lib/commands/project/project-start.js +1 -0
  19. package/lib/commands/project/project-state-command.js +1 -0
  20. package/lib/commands/project/project-status.js +1 -0
  21. package/lib/commands/project/project-stop.js +1 -0
  22. package/lib/commands/project/project-validate.js +1 -0
  23. package/lib/commands/project/project.js +1 -0
  24. package/lib/commands/runium-command.js +1 -0
  25. package/lib/constants/error-code.js +1 -0
  26. package/lib/constants/index.js +1 -0
  27. package/lib/index.js +2 -0
  28. package/lib/macros/conditional.js +1 -0
  29. package/lib/macros/empty.js +1 -0
  30. package/lib/macros/env.js +1 -0
  31. package/lib/macros/index.js +1 -0
  32. package/lib/macros/path.js +1 -0
  33. package/lib/package.json +21 -0
  34. package/lib/services/config.js +1 -0
  35. package/lib/services/index.js +1 -0
  36. package/lib/services/output.js +3 -0
  37. package/lib/services/plugin-context.js +1 -0
  38. package/lib/services/plugin.js +1 -0
  39. package/lib/services/profile.js +1 -0
  40. package/lib/services/project.js +1 -0
  41. package/lib/services/shutdown.js +1 -0
  42. package/lib/utils/convert-path-to-valid-file-name.js +1 -0
  43. package/lib/utils/debounce.js +1 -0
  44. package/lib/utils/format-timestamp.js +1 -0
  45. package/lib/utils/index.js +1 -0
  46. package/package.json +44 -0
  47. package/src/app.ts +175 -0
  48. package/src/commands/index.ts +2 -0
  49. package/src/commands/plugin/plugin-add.ts +48 -0
  50. package/src/commands/plugin/plugin-command.ts +36 -0
  51. package/src/commands/plugin/plugin-disable.ts +46 -0
  52. package/src/commands/plugin/plugin-enable.ts +50 -0
  53. package/src/commands/plugin/plugin-list.ts +61 -0
  54. package/src/commands/plugin/plugin-remove.ts +42 -0
  55. package/src/commands/plugin/plugin.ts +41 -0
  56. package/src/commands/project/project-add.ts +46 -0
  57. package/src/commands/project/project-command.ts +36 -0
  58. package/src/commands/project/project-list.ts +32 -0
  59. package/src/commands/project/project-remove.ts +41 -0
  60. package/src/commands/project/project-start.ts +152 -0
  61. package/src/commands/project/project-state-command.ts +68 -0
  62. package/src/commands/project/project-status.ts +103 -0
  63. package/src/commands/project/project-stop.ts +55 -0
  64. package/src/commands/project/project-validate.ts +43 -0
  65. package/src/commands/project/project.ts +45 -0
  66. package/src/commands/runium-command.ts +50 -0
  67. package/src/constants/error-code.ts +15 -0
  68. package/src/constants/index.ts +1 -0
  69. package/src/index.ts +21 -0
  70. package/src/macros/conditional.ts +31 -0
  71. package/src/macros/empty.ts +6 -0
  72. package/src/macros/env.ts +8 -0
  73. package/src/macros/index.ts +12 -0
  74. package/src/macros/path.ts +9 -0
  75. package/src/services/config.ts +76 -0
  76. package/src/services/index.ts +7 -0
  77. package/src/services/output.ts +201 -0
  78. package/src/services/plugin-context.ts +81 -0
  79. package/src/services/plugin.ts +144 -0
  80. package/src/services/profile.ts +211 -0
  81. package/src/services/project.ts +114 -0
  82. package/src/services/shutdown.ts +130 -0
  83. package/src/utils/convert-path-to-valid-file-name.ts +39 -0
  84. package/src/utils/debounce.ts +23 -0
  85. package/src/utils/format-timestamp.ts +17 -0
  86. package/src/utils/index.ts +3 -0
  87. package/tsconfig.json +40 -0
@@ -0,0 +1,68 @@
1
+ import { convertPathToValidFileName } from '@utils';
2
+ import { ProjectCommand } from './project-command.js';
3
+ import { ProjectState, TaskState } from '@runium/core';
4
+
5
+ export interface ProjectData {
6
+ id: string;
7
+ pid: number;
8
+ cwd: string;
9
+ path: string;
10
+ state: {
11
+ project: ProjectState[];
12
+ tasks: Record<string, TaskState[]>;
13
+ };
14
+ }
15
+
16
+ /**
17
+ * Base project state command
18
+ */
19
+ export abstract class ProjectStateCommand extends ProjectCommand {
20
+ /**
21
+ * Get project data file name
22
+ * @param name
23
+ */
24
+ protected getProjectDataFileName(name: string): string {
25
+ let fileName = convertPathToValidFileName(name);
26
+ if (!fileName.endsWith('.json')) {
27
+ fileName = fileName + '.json';
28
+ }
29
+ return fileName;
30
+ }
31
+
32
+ /**
33
+ * Read project data
34
+ * @param fileName
35
+ */
36
+ protected async readProjectData(
37
+ fileName: string
38
+ ): Promise<ProjectData | null> {
39
+ return this.profileService
40
+ .readJsonFile('projects', fileName)
41
+ .catch(() => null) as Promise<ProjectData | null>;
42
+ }
43
+
44
+ /**
45
+ * Write project data
46
+ * @param data
47
+ * @param fileName
48
+ */
49
+ protected async writeProjectData(
50
+ data: ProjectData,
51
+ fileName: string
52
+ ): Promise<void> {
53
+ return this.profileService.writeJsonFile(data, 'projects', fileName);
54
+ }
55
+
56
+ /**
57
+ * Checks if a project process is started
58
+ * @param pid
59
+ */
60
+ protected isProjectProcessStarted(pid: number): boolean {
61
+ // and process started
62
+ try {
63
+ return process.kill(Number(pid), 0);
64
+ } catch (ex) {
65
+ return (ex as { code?: string }).code === 'EPERM';
66
+ }
67
+ }
68
+ }
@@ -0,0 +1,103 @@
1
+ import { formatTimestamp } from '@utils';
2
+ import { ProjectStateCommand } from './project-state-command.js';
3
+
4
+ interface StateRecord {
5
+ name: string;
6
+ status: string;
7
+ time: string;
8
+ timestamp: number;
9
+ }
10
+
11
+ /**
12
+ * Project status command
13
+ */
14
+ export class ProjectStatusCommand extends ProjectStateCommand {
15
+ /**
16
+ * Config command
17
+ */
18
+ protected config(): void {
19
+ this.command
20
+ .name('status')
21
+ .description('get project status')
22
+ .option('-f, --file', 'use file path instead of project name')
23
+ .option('-t, --tasks', 'show task status')
24
+ .option('-a, --all', 'show status change history')
25
+ .argument('<name>', 'project name');
26
+ }
27
+
28
+ /**
29
+ * Handle command
30
+ * @param name
31
+ * @param file
32
+ * @param tasks
33
+ * @param all
34
+ */
35
+ protected async handle(
36
+ name: string,
37
+ { file, tasks, all }: { file: boolean; tasks: boolean; all: boolean }
38
+ ): Promise<void> {
39
+ const path = file
40
+ ? this.projectService.resolvePath(name)
41
+ : this.ensureProfileProject(name).path;
42
+
43
+ const projectDataFileName = this.getProjectDataFileName(file ? path : name);
44
+
45
+ const projectData = await this.readProjectData(projectDataFileName);
46
+ if (projectData) {
47
+ let { project: projectState = [] } = projectData.state;
48
+ if (!all) {
49
+ projectState =
50
+ projectState.length > 0
51
+ ? [projectState[projectState.length - 1]]
52
+ : [];
53
+ }
54
+
55
+ if (tasks) {
56
+ const projectMappedState = projectState.map(state => {
57
+ return {
58
+ name: 'Project',
59
+ status: state.status,
60
+ time: formatTimestamp(state.timestamp),
61
+ timestamp: state.timestamp,
62
+ };
63
+ });
64
+
65
+ const { tasks: tasksState = [] } = projectData.state;
66
+
67
+ const tasksMappedState: StateRecord[] = [];
68
+ Object.entries(tasksState).forEach(([key, value]) => {
69
+ if (!all) {
70
+ value = value.length > 0 ? [value[value.length - 1]] : [];
71
+ }
72
+ value.forEach(task => {
73
+ tasksMappedState.push({
74
+ name: key,
75
+ status: task.status,
76
+ time: formatTimestamp(task.timestamp),
77
+ timestamp: task.timestamp,
78
+ });
79
+ });
80
+ });
81
+
82
+ const mappedState: StateRecord[] = [
83
+ ...projectMappedState,
84
+ ...tasksMappedState,
85
+ ];
86
+ mappedState.sort((a, b) => a.timestamp - b.timestamp);
87
+
88
+ this.outputService.table(mappedState, ['time', 'name', 'status']);
89
+ } else {
90
+ const mappedState = projectState.map(state => {
91
+ return {
92
+ status: state.status,
93
+ time: formatTimestamp(state.timestamp),
94
+ };
95
+ });
96
+
97
+ this.outputService.table(mappedState, ['time', 'status']);
98
+ }
99
+ } else {
100
+ this.outputService.info(`No project status for "${name}"`);
101
+ }
102
+ }
103
+ }
@@ -0,0 +1,55 @@
1
+ import { RuniumError } from '@runium/core';
2
+ import { ErrorCode } from '@constants';
3
+ import { ProjectStateCommand } from './project-state-command.js';
4
+
5
+ /**
6
+ * Project stop command
7
+ */
8
+ export class ProjectStopCommand extends ProjectStateCommand {
9
+ /**
10
+ * Config command
11
+ */
12
+ protected config(): void {
13
+ this.command
14
+ .name('stop')
15
+ .description('stop project')
16
+ .option('-f, --file', 'use file path instead of project name')
17
+ .argument('<name>', 'project name');
18
+ }
19
+
20
+ /**
21
+ * Handle command
22
+ * @param name
23
+ * @param file
24
+ */
25
+ protected async handle(
26
+ name: string,
27
+ { file }: { file: boolean }
28
+ ): Promise<void> {
29
+ const path = file
30
+ ? this.projectService.resolvePath(name)
31
+ : this.ensureProfileProject(name).path;
32
+
33
+ const projectDataFileName = this.getProjectDataFileName(file ? path : name);
34
+
35
+ // check if project is started
36
+ const projectData = await this.readProjectData(projectDataFileName);
37
+ if (!projectData || !this.isProjectProcessStarted(projectData.pid)) {
38
+ throw new RuniumError(
39
+ `Project "${name}" is not started`,
40
+ ErrorCode.PROJECT_NOT_STARTED,
41
+ { name }
42
+ );
43
+ }
44
+
45
+ try {
46
+ process.kill(projectData.pid, 'SIGTERM');
47
+ } catch (ex) {
48
+ throw new RuniumError(
49
+ `Failed to stop project "${name}"`,
50
+ ErrorCode.PROJECT_STOP_ERROR,
51
+ { name, original: ex }
52
+ );
53
+ }
54
+ }
55
+ }
@@ -0,0 +1,43 @@
1
+ import { ProjectCommand } from './project-command.js';
2
+
3
+ /**
4
+ * Project validate command
5
+ */
6
+ export class ProjectValidateCommand extends ProjectCommand {
7
+ /**
8
+ * Config command
9
+ */
10
+ protected config(): void {
11
+ this.command
12
+ .name('validate')
13
+ .description('validate project')
14
+ .option('-f, --file', 'use file path instead of project name')
15
+ .argument('<project>', 'project name or file path');
16
+ }
17
+
18
+ /**
19
+ * Handle command
20
+ * @param project
21
+ * @param file
22
+ */
23
+ protected async handle(
24
+ project: string,
25
+ { file }: { file: boolean }
26
+ ): Promise<void> {
27
+ const path = file
28
+ ? this.projectService.resolvePath(project)
29
+ : this.ensureProfileProject(project).path;
30
+ const projectInstance = await this.projectService.initProject(path);
31
+ try {
32
+ await projectInstance.validate();
33
+ this.outputService.success(`Project "%s" is valid`, project);
34
+ } catch (error) {
35
+ // TODO stringify validation errors
36
+ this.outputService.error(
37
+ `Project "%s" validation failed`,
38
+ project,
39
+ error
40
+ );
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,45 @@
1
+ import { RuniumCommand } from '@commands/runium-command.js';
2
+ import { ProjectAddCommand } from './project-add.js';
3
+ import { ProjectListCommand } from './project-list.js';
4
+ import { ProjectRemoveCommand } from './project-remove.js';
5
+ import { ProjectStartCommand } from './project-start.js';
6
+ import { ProjectStopCommand } from './project-stop.js';
7
+ import { ProjectStatusCommand } from './project-status.js';
8
+ import { ProjectValidateCommand } from './project-validate.js';
9
+
10
+ /**
11
+ * Project group command
12
+ */
13
+ export class ProjectCommand extends RuniumCommand {
14
+ /**
15
+ * Config command
16
+ */
17
+ protected config(): void {
18
+ this.command.name('project').description('manage projects');
19
+ }
20
+
21
+ /**
22
+ * Handle command
23
+ */
24
+ protected async handle(): Promise<void> {
25
+ this.command.help();
26
+ }
27
+
28
+ /**
29
+ * Add subcommands
30
+ */
31
+ protected addSubcommands(): void {
32
+ const constructors = [
33
+ ProjectListCommand,
34
+ ProjectAddCommand,
35
+ ProjectRemoveCommand,
36
+ ProjectStartCommand,
37
+ ProjectStopCommand,
38
+ ProjectStatusCommand,
39
+ ProjectValidateCommand,
40
+ ];
41
+ for (const CommandConstructor of constructors) {
42
+ this.subcommands.push(new CommandConstructor(this.command));
43
+ }
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import { Command } from 'commander';
2
+ import { Container } from 'typedi';
3
+ import { OutputService } from '@services';
4
+
5
+ /**
6
+ * Base runium command
7
+ */
8
+ export abstract class RuniumCommand {
9
+ protected outputService: OutputService;
10
+
11
+ /**
12
+ * Parent command
13
+ */
14
+ protected parent: Command;
15
+
16
+ /**
17
+ * Current command
18
+ */
19
+ protected command: Command;
20
+
21
+ /**
22
+ * Subcommands
23
+ */
24
+ protected subcommands: RuniumCommand[] = [];
25
+
26
+ constructor(parent: Command) {
27
+ this.outputService = Container.get(OutputService);
28
+ this.parent = parent;
29
+ this.command = new Command();
30
+ this.config();
31
+ this.command.action(this.handle.bind(this));
32
+ this.addSubcommands();
33
+ this.parent.addCommand(this.command);
34
+ }
35
+
36
+ /**
37
+ * Add subcommands
38
+ */
39
+ protected addSubcommands(): void {}
40
+
41
+ /**
42
+ * Config command
43
+ */
44
+ protected abstract config(): void;
45
+
46
+ /**
47
+ * Handle command
48
+ */
49
+ protected abstract handle(...args: unknown[]): Promise<void>;
50
+ }
@@ -0,0 +1,15 @@
1
+ export enum ErrorCode {
2
+ PLUGIN_NOT_FOUND = 'plugin-not-found',
3
+ PLUGIN_FILE_NOT_FOUND = 'plugin-file-not-found',
4
+ PLUGIN_INCORRECT_MODULE = 'plugin-incorrect-module',
5
+ PLUGIN_INCORRECT_PLUGIN = 'plugin-incorrect-plugin',
6
+ PLUGIN_PATH_RESOLVE_ERROR = 'plugin-path-resolve-error',
7
+ PROJECT_ALREADY_STARTED = 'project-already-started',
8
+ PROJECT_NOT_STARTED = 'project-not-started',
9
+ PROJECT_STOP_ERROR = 'project-stop-error',
10
+ PROJECT_NOT_FOUND = 'project-not-found',
11
+ PROJECT_FILE_NOT_FOUND = 'project-file-not-found',
12
+ PROJECT_FILE_CAN_NOT_READ = 'project-file-can-not-read',
13
+ PROJECT_JSON_PARSE_ERROR = 'project-json-parse-error',
14
+ PROFILE_JSON_WRITE_ERROR = 'profile-json-write-error',
15
+ }
@@ -0,0 +1 @@
1
+ export * from './error-code.js';
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+
3
+ import 'reflect-metadata';
4
+ import { Container } from 'typedi';
5
+ import { RuniumCliApp } from './app.js';
6
+ import { OutputService } from '@services';
7
+
8
+ async function main() {
9
+ const app = new RuniumCliApp();
10
+ await app.start();
11
+ }
12
+
13
+ main().catch(error => {
14
+ const outputService = Container.get(OutputService);
15
+ outputService.error('Error: %s', error.message);
16
+ outputService.debug('Error details:', {
17
+ code: error.code,
18
+ payload: error.payload,
19
+ });
20
+ process.exit(1);
21
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Equal
3
+ * @param value1
4
+ * @param value2
5
+ * @param trueValue
6
+ * @param falseValue
7
+ */
8
+ export function eqMacro(
9
+ value1: string,
10
+ value2: string,
11
+ trueValue?: string,
12
+ falseValue?: string
13
+ ): string {
14
+ return value1 === value2 ? (trueValue ?? '') : (falseValue ?? '');
15
+ }
16
+
17
+ /**
18
+ * Not equal
19
+ * @param value1
20
+ * @param value2
21
+ * @param trueValue
22
+ * @param falseValue
23
+ */
24
+ export function neMacro(
25
+ value1: string,
26
+ value2: string,
27
+ trueValue?: string,
28
+ falseValue?: string
29
+ ): string {
30
+ return value1 !== value2 ? (trueValue ?? '') : (falseValue ?? '');
31
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Empty string
3
+ */
4
+ export function emptyMacro(): string {
5
+ return '';
6
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Get environment variable
3
+ * @param name
4
+ * @param defaultValue
5
+ */
6
+ export function envMacro(name: string, defaultValue?: string): string {
7
+ return process.env[name] ?? defaultValue ?? '';
8
+ }
@@ -0,0 +1,12 @@
1
+ import { eqMacro, neMacro } from './conditional.js';
2
+ import { emptyMacro } from './empty.js';
3
+ import { envMacro } from './env.js';
4
+ import { pathMacro } from './path.js';
5
+
6
+ export const macros = {
7
+ env: envMacro,
8
+ empty: emptyMacro,
9
+ eq: eqMacro,
10
+ ne: neMacro,
11
+ path: pathMacro,
12
+ };
@@ -0,0 +1,9 @@
1
+ import { resolve } from 'node:path';
2
+
3
+ /**
4
+ * Resolve path
5
+ * @param path
6
+ */
7
+ export function pathMacro(path: string): string {
8
+ return resolve(path);
9
+ }
@@ -0,0 +1,76 @@
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 } from '@runium/core';
6
+
7
+ interface ConfigEnv {
8
+ path: string[];
9
+ }
10
+
11
+ interface ConfigOutput {
12
+ debug: boolean;
13
+ }
14
+
15
+ interface ConfigProfile {
16
+ path: string;
17
+ }
18
+
19
+ interface ConfigData {
20
+ env: ConfigEnv;
21
+ output: ConfigOutput;
22
+ profile: ConfigProfile;
23
+ }
24
+
25
+ const CONFIG_FILE_NAME = '.runiumrc.json';
26
+ const CONFIG_FILE_PATH = join(process.cwd(), CONFIG_FILE_NAME);
27
+ const PROFILE_DIR_NAME = '.runium';
28
+ const HOME_PROFILE_PATH = join(homedir(), PROFILE_DIR_NAME);
29
+ const CWD_PROFILE_PATH = join(process.cwd(), PROFILE_DIR_NAME);
30
+
31
+ @Service()
32
+ export class ConfigService {
33
+ private data: ConfigData = {
34
+ profile: { path: HOME_PROFILE_PATH },
35
+ output: { debug: false },
36
+ env: { path: [] },
37
+ };
38
+
39
+ /**
40
+ * Initialize the config service
41
+ */
42
+ async init(): Promise<void> {
43
+ if (existsSync(CWD_PROFILE_PATH)) {
44
+ this.data.profile.path = CWD_PROFILE_PATH;
45
+ }
46
+
47
+ if (existsSync(CONFIG_FILE_PATH)) {
48
+ // TODO validate config file
49
+ const configData: ConfigData =
50
+ await readJsonFile<ConfigData>(CONFIG_FILE_PATH);
51
+ if (configData) {
52
+ const data = {
53
+ env: Object.assign({}, this.data.env, configData.env ?? {}),
54
+ output: Object.assign({}, this.data.output, configData.output ?? {}),
55
+ profile: Object.assign(
56
+ {},
57
+ this.data.profile,
58
+ configData.profile ?? {}
59
+ ),
60
+ };
61
+
62
+ data.env.path = data.env.path.map(envPath => resolve(envPath));
63
+ data.profile.path = resolve(data.profile.path);
64
+ this.data = data;
65
+ }
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Get a configuration value
71
+ * @param key
72
+ */
73
+ get<T extends keyof ConfigData>(key: T): ConfigData[T] {
74
+ return this.data[key];
75
+ }
76
+ }
@@ -0,0 +1,7 @@
1
+ export * from './config.js';
2
+ export * from './output.js';
3
+ export * from './profile.js';
4
+ export * from './plugin.js';
5
+ export * from './project.js';
6
+ export * from './shutdown.js';
7
+ export * from './plugin-context.js';