@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,48 @@
1
+ import { RuniumError } from '@runium/core';
2
+ import { ErrorCode } from '@constants';
3
+ import { PluginCommand } from './plugin-command.js';
4
+ /**
5
+ * Plugin add command
6
+ */
7
+ export class PluginAddCommand extends PluginCommand {
8
+ /**
9
+ * Config command
10
+ */
11
+ protected config(): void {
12
+ this.command
13
+ .name('add')
14
+ .description('add plugin')
15
+ .option('-f, --file', 'use file path instead of plugin package name')
16
+ .argument('<plugin>', 'plugin package name or absolute file path');
17
+ }
18
+
19
+ /**
20
+ * Handle command
21
+ * @param path
22
+ * @param file
23
+ */
24
+ protected async handle(
25
+ path: string,
26
+ { file: isFile }: { file: boolean }
27
+ ): Promise<void> {
28
+ const pluginPath = this.pluginService.resolvePath(path, isFile);
29
+ const name = await this.pluginService.loadPlugin(pluginPath);
30
+ const plugin = this.pluginService.getPluginByName(name);
31
+ if (plugin) {
32
+ await this.profileService.addPlugin({
33
+ name,
34
+ path: isFile ? pluginPath : path,
35
+ file: isFile,
36
+ disabled: false,
37
+ options: {},
38
+ });
39
+ this.outputService.success(`Plugin "%s" successfully added`, name);
40
+ } else {
41
+ throw new RuniumError(
42
+ `Failed to add plugin "${path}"`,
43
+ ErrorCode.PLUGIN_NOT_FOUND,
44
+ { name, path }
45
+ );
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,36 @@
1
+ import { Command } from 'commander';
2
+ import { Container } from 'typedi';
3
+ import { RuniumError } from '@runium/core';
4
+ import { RuniumCommand } from '@commands/runium-command.js';
5
+ import { ErrorCode } from '@constants';
6
+ import { PluginService, ProfileService, ProfilePlugin } from '@services';
7
+
8
+ /**
9
+ * Base plugin command
10
+ */
11
+ export abstract class PluginCommand extends RuniumCommand {
12
+ protected pluginService: PluginService;
13
+ protected profileService: ProfileService;
14
+
15
+ constructor(parent: Command) {
16
+ super(parent);
17
+ this.pluginService = Container.get(PluginService);
18
+ this.profileService = Container.get(ProfileService);
19
+ }
20
+
21
+ /**
22
+ * Ensure plugin exists
23
+ * @param name
24
+ */
25
+ protected ensureProfilePlugin(name: string): ProfilePlugin {
26
+ const plugin = this.profileService.getPluginByName(name);
27
+ if (!plugin) {
28
+ throw new RuniumError(
29
+ `Plugin "${name}" not found`,
30
+ ErrorCode.PLUGIN_NOT_FOUND,
31
+ { name }
32
+ );
33
+ }
34
+ return plugin;
35
+ }
36
+ }
@@ -0,0 +1,46 @@
1
+ import { PluginCommand } from './plugin-command.js';
2
+
3
+ /**
4
+ * Plugin disable command
5
+ */
6
+ export class PluginDisableCommand extends PluginCommand {
7
+ /**
8
+ * Config command
9
+ */
10
+ protected config(): void {
11
+ this.command
12
+ .name('disable')
13
+ .description('disable plugin')
14
+ .option('-a, --all', 'disable all plugins')
15
+ .argument('[plugin...]', 'plugin names');
16
+ }
17
+
18
+ /**
19
+ * Handle command
20
+ * @param names
21
+ * @param all
22
+ */
23
+ protected async handle(
24
+ names: string[],
25
+ { all }: { all: boolean }
26
+ ): Promise<void> {
27
+ if (names.length === 0 && !all) {
28
+ this.outputService.warn('No plugins specified to disable');
29
+ return;
30
+ }
31
+ if (all) {
32
+ names = this.profileService.getPlugins().map(p => p.name);
33
+ }
34
+
35
+ for (const name of names) {
36
+ const plugin = this.ensureProfilePlugin(name);
37
+ if (plugin.disabled) {
38
+ this.outputService.info(`Plugin "%s" is already disabled`, name);
39
+ continue;
40
+ }
41
+ await this.profileService.updatePlugin(name, { disabled: true });
42
+ await this.pluginService.unloadPlugin(name);
43
+ this.outputService.success(`Plugin "%s" successfully disabled`, name);
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,50 @@
1
+ import { PluginCommand } from './plugin-command.js';
2
+
3
+ /**
4
+ * Plugin enable command
5
+ */
6
+ export class PluginEnableCommand extends PluginCommand {
7
+ /**
8
+ * Config command
9
+ */
10
+ protected config(): void {
11
+ this.command
12
+ .name('enable')
13
+ .description('enable plugin')
14
+ .option('-a, --all', 'enable all plugins')
15
+ .argument('[plugin...]', 'plugin names');
16
+ }
17
+
18
+ /**
19
+ * Handle command
20
+ * @param names
21
+ * @param all
22
+ */
23
+ protected async handle(
24
+ names: string[],
25
+ { all }: { all: boolean }
26
+ ): Promise<void> {
27
+ if (names.length === 0 && !all) {
28
+ this.outputService.warn('No plugins specified to enable');
29
+ return;
30
+ }
31
+ if (all) {
32
+ names = this.profileService.getPlugins().map(p => p.name);
33
+ }
34
+
35
+ for (const name of names) {
36
+ const plugin = this.ensureProfilePlugin(name);
37
+ if (!plugin.disabled) {
38
+ this.outputService.info(`Plugin "%s" is already enabled`, name);
39
+ continue;
40
+ }
41
+ await this.profileService.updatePlugin(name, { disabled: false });
42
+ const pluginPath = this.pluginService.resolvePath(
43
+ plugin.path,
44
+ plugin.file
45
+ );
46
+ await this.pluginService.loadPlugin(pluginPath);
47
+ this.outputService.success(`Plugin "%s" successfully enabled`, name);
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,61 @@
1
+ import { Option } from 'commander';
2
+ import { PluginCommand } from './plugin-command.js';
3
+
4
+ /**
5
+ * Plugin list command
6
+ */
7
+ export class PluginListCommand extends PluginCommand {
8
+ /**
9
+ * Config command
10
+ */
11
+ protected config(): void {
12
+ this.command
13
+ .name('list')
14
+ .addOption(
15
+ new Option('-d, --disabled', 'show only disabled plugins').conflicts(
16
+ 'enabled'
17
+ )
18
+ )
19
+ .addOption(
20
+ new Option('-e, --enabled', 'show only enabled plugins').conflicts(
21
+ 'disabled'
22
+ )
23
+ )
24
+ .option('-s, --sort', 'sort by name')
25
+ .description('list plugins');
26
+ }
27
+
28
+ /**
29
+ * Handle command
30
+ */
31
+ protected async handle({
32
+ disabled,
33
+ enabled,
34
+ sort,
35
+ }: {
36
+ disabled: boolean;
37
+ enabled: boolean;
38
+ sort: boolean;
39
+ }): Promise<void> {
40
+ let plugins = this.profileService.getPlugins();
41
+ if (disabled) {
42
+ plugins = plugins.filter(p => p.disabled === true);
43
+ }
44
+ if (enabled) {
45
+ plugins = plugins.filter(p => p.disabled === false);
46
+ }
47
+ if (sort) {
48
+ plugins.sort((a, b) => a.name.localeCompare(b.name));
49
+ }
50
+
51
+ if (plugins.length !== 0) {
52
+ this.outputService.table(plugins, [
53
+ 'name',
54
+ 'path',
55
+ ...(disabled || enabled ? [] : ['disabled']),
56
+ ]);
57
+ } else {
58
+ this.outputService.warn('No plugins found');
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,42 @@
1
+ import { PluginCommand } from './plugin-command.js';
2
+
3
+ /**
4
+ * Plugin remove command
5
+ */
6
+ export class PluginRemoveCommand extends PluginCommand {
7
+ /**
8
+ * Config command
9
+ */
10
+ protected config(): void {
11
+ this.command
12
+ .name('remove')
13
+ .description('remove plugin')
14
+ .option('-a, --all', 'remove all plugins')
15
+ .argument('[plugin...]', 'plugin names');
16
+ }
17
+
18
+ /**
19
+ * Handle command
20
+ * @param names
21
+ * @param all
22
+ */
23
+ protected async handle(
24
+ names: string[],
25
+ { all }: { all: boolean }
26
+ ): Promise<void> {
27
+ if (names.length === 0 && !all) {
28
+ this.outputService.warn('No plugins specified to remove');
29
+ return;
30
+ }
31
+ if (all) {
32
+ names = this.profileService.getPlugins().map(p => p.name);
33
+ }
34
+
35
+ for (const name of names) {
36
+ this.ensureProfilePlugin(name);
37
+ await this.profileService.removePlugin(name);
38
+ await this.pluginService.unloadPlugin(name);
39
+ this.outputService.success(`Plugin "%s" successfully removed`, name);
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,41 @@
1
+ import { RuniumCommand } from '@commands/runium-command.js';
2
+ import { PluginAddCommand } from './plugin-add.js';
3
+ import { PluginDisableCommand } from './plugin-disable.js';
4
+ import { PluginEnableCommand } from './plugin-enable.js';
5
+ import { PluginListCommand } from './plugin-list.js';
6
+ import { PluginRemoveCommand } from './plugin-remove.js';
7
+
8
+ /**
9
+ * Plugin group command
10
+ */
11
+ export class PluginCommand extends RuniumCommand {
12
+ /**
13
+ * Config command
14
+ */
15
+ protected config(): void {
16
+ this.command.name('plugin').description('manage plugins');
17
+ }
18
+
19
+ /**
20
+ * Handle command
21
+ */
22
+ protected async handle(): Promise<void> {
23
+ this.command.help();
24
+ }
25
+
26
+ /**
27
+ * Add subcommands
28
+ */
29
+ protected addSubcommands(): void {
30
+ const constructors = [
31
+ PluginListCommand,
32
+ PluginAddCommand,
33
+ PluginRemoveCommand,
34
+ PluginDisableCommand,
35
+ PluginEnableCommand,
36
+ ];
37
+ for (const CommandConstructor of constructors) {
38
+ this.subcommands.push(new CommandConstructor(this.command));
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,46 @@
1
+ import { RuniumError } from '@runium/core';
2
+ import { ErrorCode } from '@constants';
3
+ import { ProjectCommand } from './project-command.js';
4
+ /**
5
+ * Project add command
6
+ */
7
+ export class ProjectAddCommand extends ProjectCommand {
8
+ /**
9
+ * Config command
10
+ */
11
+ protected config(): void {
12
+ this.command
13
+ .name('add')
14
+ .description('add project')
15
+ .argument('<path>', 'project file path')
16
+ .argument('[name]', 'project name (default: project config id)');
17
+ }
18
+
19
+ /**
20
+ * Handle command
21
+ * @param path
22
+ * @param name
23
+ */
24
+ protected async handle(path: string, name?: string): Promise<void> {
25
+ const projectPath = this.projectService.resolvePath(path);
26
+ const project = await this.projectService.initProject(projectPath);
27
+ if (project) {
28
+ await this.profileService.addProject({
29
+ // TODO validate name
30
+ // optionally save project file to profile
31
+ name: name ?? project.getConfig().id,
32
+ path: projectPath,
33
+ });
34
+ this.outputService.success(
35
+ `Project "%s" successfully added`,
36
+ name ?? project.getConfig().id
37
+ );
38
+ } else {
39
+ throw new RuniumError(
40
+ `Failed to add project "${projectPath}"`,
41
+ ErrorCode.PROJECT_NOT_FOUND,
42
+ { path: projectPath }
43
+ );
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,36 @@
1
+ import { Command } from 'commander';
2
+ import { Container } from 'typedi';
3
+ import { RuniumError } from '@runium/core';
4
+ import { RuniumCommand } from '@commands/runium-command.js';
5
+ import { ErrorCode } from '@constants';
6
+ import { ProfileService, ProjectService, ProfileProject } from '@services';
7
+
8
+ /**
9
+ * Base project command
10
+ */
11
+ export abstract class ProjectCommand extends RuniumCommand {
12
+ protected projectService: ProjectService;
13
+ protected profileService: ProfileService;
14
+
15
+ constructor(parent: Command) {
16
+ super(parent);
17
+ this.projectService = Container.get(ProjectService);
18
+ this.profileService = Container.get(ProfileService);
19
+ }
20
+
21
+ /**
22
+ * Ensure project exists
23
+ * @param name
24
+ */
25
+ protected ensureProfileProject(name: string): ProfileProject {
26
+ const project = this.profileService.getProjectByName(name);
27
+ if (!project) {
28
+ throw new RuniumError(
29
+ `Project "${name}" not found`,
30
+ ErrorCode.PROJECT_NOT_FOUND,
31
+ { name }
32
+ );
33
+ }
34
+ return project;
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ import { ProjectCommand } from './project-command.js';
2
+
3
+ /**
4
+ * Project list command
5
+ */
6
+ export class ProjectListCommand extends ProjectCommand {
7
+ /**
8
+ * Config command
9
+ */
10
+ protected config(): void {
11
+ this.command
12
+ .name('list')
13
+ .description('list projects')
14
+ .option('-s, --sort', 'sort by name');
15
+ }
16
+
17
+ /**
18
+ * Handle command
19
+ */
20
+ protected async handle({ sort }: { sort: boolean }): Promise<void> {
21
+ const projects = this.profileService.getProjects();
22
+ if (sort) {
23
+ projects.sort((a, b) => a.name.localeCompare(b.name));
24
+ }
25
+ // handle empty list
26
+ if (projects.length !== 0) {
27
+ this.outputService.table(projects, ['name', 'path']);
28
+ } else {
29
+ this.outputService.warn('No projects found');
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,41 @@
1
+ import { ProjectCommand } from './project-command.js';
2
+
3
+ /**
4
+ * Project remove command
5
+ */
6
+ export class ProjectRemoveCommand extends ProjectCommand {
7
+ /**
8
+ * Config command
9
+ */
10
+ protected config(): void {
11
+ this.command
12
+ .name('remove')
13
+ .description('remove project')
14
+ .option('-a, --all', 'remove all projects')
15
+ .argument('[name...]', 'project names');
16
+ }
17
+
18
+ /**
19
+ * Handle command
20
+ * @param names
21
+ * @param all
22
+ */
23
+ protected async handle(
24
+ names: string[],
25
+ { all }: { all: boolean }
26
+ ): Promise<void> {
27
+ if (names.length === 0 && !all) {
28
+ this.outputService.warn('No projects specified to remove');
29
+ return;
30
+ }
31
+ if (all) {
32
+ names = this.profileService.getProjects().map(p => p.name);
33
+ }
34
+
35
+ for (const name of names) {
36
+ this.ensureProfileProject(name);
37
+ await this.profileService.removeProject(name);
38
+ this.outputService.success(`Project "%s" successfully removed`, name);
39
+ }
40
+ }
41
+ }
@@ -0,0 +1,152 @@
1
+ import { dirname } from 'node:path';
2
+ import { Command, Option } from 'commander';
3
+ import { Container } from 'typedi';
4
+ import {
5
+ Project,
6
+ ProjectEvent,
7
+ ProjectState,
8
+ RuniumError,
9
+ TaskState,
10
+ } from '@runium/core';
11
+ import { ErrorCode } from '@constants';
12
+ import { ShutdownService } from '@services';
13
+ import { debounce } from '@utils';
14
+ import { ProjectData, ProjectStateCommand } from './project-state-command.js';
15
+
16
+ const WRITE_PROJECT_DATA_DEBOUNCE_WAIT = 100;
17
+
18
+ /**
19
+ * Project start command
20
+ */
21
+ export class ProjectStartCommand extends ProjectStateCommand {
22
+ protected shutdownService: ShutdownService;
23
+
24
+ constructor(parent: Command) {
25
+ super(parent);
26
+ this.shutdownService = Container.get(ShutdownService);
27
+ }
28
+
29
+ /**
30
+ * Config command
31
+ */
32
+ protected config(): void {
33
+ this.command
34
+ .name('start')
35
+ .description('start project')
36
+ .option('-f, --file', 'use file path instead of project name')
37
+ .option('-o, --output', 'output project state changes')
38
+ .addOption(
39
+ new Option('-w, --working-dir <choice>', 'set working directory')
40
+ .choices(['cwd', 'project'])
41
+ .default('cwd')
42
+ )
43
+ .argument('<name>', 'project name');
44
+ }
45
+
46
+ /**
47
+ * Handle command
48
+ * @param name
49
+ * @param file
50
+ * @param workingDir
51
+ * @param output
52
+ */
53
+ protected async handle(
54
+ name: string,
55
+ {
56
+ file,
57
+ workingDir,
58
+ output,
59
+ }: { file: boolean; workingDir?: string; output?: boolean }
60
+ ): Promise<void> {
61
+ const path = file
62
+ ? this.projectService.resolvePath(name)
63
+ : this.ensureProfileProject(name).path;
64
+
65
+ const projectDataFileName = this.getProjectDataFileName(file ? path : name);
66
+
67
+ // check if project is started
68
+ const projectData = await this.readProjectData(projectDataFileName);
69
+ if (projectData && this.isProjectProcessStarted(projectData.pid)) {
70
+ throw new RuniumError(
71
+ `Project "${name}" is already started`,
72
+ ErrorCode.PROJECT_ALREADY_STARTED,
73
+ { name }
74
+ );
75
+ }
76
+
77
+ if (workingDir === 'project') {
78
+ const projectDir = dirname(path);
79
+ if (projectDir !== process.cwd()) {
80
+ process.chdir(projectDir);
81
+ }
82
+ }
83
+
84
+ const project = await this.projectService.initProject(path);
85
+ this.shutdownService.addBlocker(() => project.stop());
86
+
87
+ this.addProjectListeners(project, {
88
+ dataFileName: projectDataFileName,
89
+ projectPath: path,
90
+ output,
91
+ });
92
+
93
+ await project.start();
94
+ }
95
+
96
+ /**
97
+ * Add project listeners
98
+ * @param project
99
+ * @param options
100
+ */
101
+ protected addProjectListeners(
102
+ project: Project,
103
+ options: { dataFileName: string; projectPath: string; output?: boolean }
104
+ ): void {
105
+ const { dataFileName, projectPath, output } = options;
106
+
107
+ const projectData: ProjectData = {
108
+ id: project.getConfig().id,
109
+ pid: process.pid,
110
+ cwd: process.cwd(),
111
+ path: projectPath,
112
+ state: {
113
+ project: [],
114
+ tasks: {},
115
+ },
116
+ };
117
+
118
+ const writeProjectData = debounce(() => {
119
+ this.writeProjectData(projectData, dataFileName);
120
+ }, WRITE_PROJECT_DATA_DEBOUNCE_WAIT);
121
+
122
+ // write initial data
123
+ writeProjectData();
124
+
125
+ project.on(ProjectEvent.STATE_CHANGE, async (state: ProjectState) => {
126
+ projectData.state.project.push(state);
127
+ writeProjectData();
128
+ if (output) {
129
+ this.outputService.info('Project %s', state.status);
130
+ }
131
+ });
132
+
133
+ project.on(
134
+ ProjectEvent.TASK_STATE_CHANGE,
135
+ (taskId: string, state: TaskState) => {
136
+ if (!projectData.state.tasks[taskId]) {
137
+ projectData.state.tasks[taskId] = [];
138
+ }
139
+ projectData.state.tasks[taskId].push(state);
140
+ writeProjectData();
141
+ if (output) {
142
+ this.outputService.info(
143
+ 'Task %s %s %s',
144
+ taskId,
145
+ state.status,
146
+ state.exitCode || state.error || ''
147
+ );
148
+ }
149
+ }
150
+ );
151
+ }
152
+ }