hereya-cli 0.83.2 → 0.84.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.
@@ -6,6 +6,72 @@ interface CloudBackendConfig {
6
6
  refreshToken: string;
7
7
  url: string;
8
8
  }
9
+ export interface AppSummary {
10
+ description?: null | string;
11
+ name: string;
12
+ visibility?: string;
13
+ }
14
+ export interface AppVersionSummary {
15
+ commit?: string;
16
+ publishedAt?: string;
17
+ repository?: string;
18
+ sha256?: string;
19
+ version: string;
20
+ }
21
+ export interface AppDeploymentSummary {
22
+ appName?: string;
23
+ env?: Record<string, string>;
24
+ lastJobId?: null | string;
25
+ parameters?: Record<string, unknown>;
26
+ state?: unknown;
27
+ status: string;
28
+ version?: string;
29
+ workspace: string;
30
+ }
31
+ export type DeployAppInput = {
32
+ hereyaVarsYaml?: string;
33
+ name: string;
34
+ parameters?: Record<string, string>;
35
+ version?: string;
36
+ workspace: string;
37
+ };
38
+ export type DestroyAppInput = {
39
+ name: string;
40
+ workspace: string;
41
+ };
42
+ export type GetAppDeploymentInput = {
43
+ name: string;
44
+ workspace: string;
45
+ };
46
+ export type PublishAppVersionInput = {
47
+ commit: string;
48
+ description?: string;
49
+ hereyaYaml: string;
50
+ name: string;
51
+ repository: string;
52
+ sha256: string;
53
+ version: string;
54
+ visibility?: 'private' | 'public';
55
+ };
56
+ export type PublishAppVersionOutput = {
57
+ app: {
58
+ id?: string;
59
+ name: string;
60
+ version: string;
61
+ };
62
+ success: true;
63
+ } | {
64
+ reason: string;
65
+ success: false;
66
+ };
67
+ export type UpdateAppDeploymentInput = {
68
+ env?: Record<string, string>;
69
+ lastJobId?: string;
70
+ name: string;
71
+ state?: unknown;
72
+ status: string;
73
+ workspace: string;
74
+ };
9
75
  export declare class CloudBackend implements Backend {
10
76
  private readonly config;
11
77
  constructor(config: CloudBackendConfig);
@@ -13,6 +79,21 @@ export declare class CloudBackend implements Backend {
13
79
  createWorkspace(input: CreateWorkspaceInput): Promise<CreateWorkspaceOutput>;
14
80
  deleteState(input: DeleteStateInput): Promise<DeleteStateOutput>;
15
81
  deleteWorkspace(input: DeleteWorkspaceInput): Promise<DeleteWorkspaceOutput>;
82
+ deployApp(input: DeployAppInput): Promise<{
83
+ deploymentId: string;
84
+ jobId: string;
85
+ success: true;
86
+ } | {
87
+ reason: string;
88
+ success: false;
89
+ }>;
90
+ destroyApp(input: DestroyAppInput): Promise<{
91
+ jobId: string;
92
+ success: true;
93
+ } | {
94
+ reason: string;
95
+ success: false;
96
+ }>;
16
97
  exportBackend(): Promise<ExportBackendOutput>;
17
98
  generateExecutorToken(input: {
18
99
  workspace: string;
@@ -24,6 +105,20 @@ export declare class CloudBackend implements Backend {
24
105
  reason: string;
25
106
  success: false;
26
107
  }>;
108
+ getApp(name: string): Promise<{
109
+ app: AppSummary;
110
+ success: true;
111
+ } | {
112
+ reason: string;
113
+ success: false;
114
+ }>;
115
+ getAppDeployment(input: GetAppDeploymentInput): Promise<{
116
+ deployment: AppDeploymentSummary;
117
+ success: true;
118
+ } | {
119
+ reason: string;
120
+ success: false;
121
+ }>;
27
122
  getExecutorJobStatus(input: {
28
123
  jobId: string;
29
124
  lastStatus?: string;
@@ -51,6 +146,27 @@ export declare class CloudBackend implements Backend {
51
146
  getWorkspaceEnv(input: GetWorkspaceEnvInput): Promise<GetWorkspaceEnvOutput>;
52
147
  importBackend(input: ImportBackendInput): Promise<ImportBackendOutput>;
53
148
  init(input: InitProjectInput): Promise<InitProjectOutput>;
149
+ listAppDeployments(name: string): Promise<{
150
+ deployments: AppDeploymentSummary[];
151
+ success: true;
152
+ } | {
153
+ reason: string;
154
+ success: false;
155
+ }>;
156
+ listApps(): Promise<{
157
+ apps: AppSummary[];
158
+ success: true;
159
+ } | {
160
+ reason: string;
161
+ success: false;
162
+ }>;
163
+ listAppVersions(name: string): Promise<{
164
+ reason: string;
165
+ success: false;
166
+ } | {
167
+ success: true;
168
+ versions: AppVersionSummary[];
169
+ }>;
54
170
  listPackageVersions(name: string): Promise<ListPackageVersionsOutput>;
55
171
  listProjects(): Promise<ListProjectsOutput>;
56
172
  listWorkspaces(input?: ListWorkspacesInput): Promise<string[]>;
@@ -69,6 +185,7 @@ export declare class CloudBackend implements Backend {
69
185
  success: false;
70
186
  unauthorized?: true;
71
187
  }>;
188
+ publishAppVersion(input: PublishAppVersionInput): Promise<PublishAppVersionOutput>;
72
189
  publishPackage(input: PublishPackageInput): Promise<PublishPackageOutput>;
73
190
  removePackageFromWorkspace(input: RemovePackageFromWorkspaceInput): Promise<RemovePackageFromWorkspaceOutput>;
74
191
  revokeExecutorToken(input: {
@@ -85,7 +202,7 @@ export declare class CloudBackend implements Backend {
85
202
  setEnvVar(input: SetEnvVarInput): Promise<SetEnvVarOutput>;
86
203
  submitExecutorJob(input: {
87
204
  payload: object;
88
- type: 'deploy' | 'destroy' | 'provision' | 'resolve-env' | 'undeploy';
205
+ type: 'app-deploy' | 'app-destroy' | 'deploy' | 'destroy' | 'provision' | 'resolve-env' | 'undeploy';
89
206
  workspace: string;
90
207
  }): Promise<{
91
208
  jobId: string;
@@ -95,6 +212,12 @@ export declare class CloudBackend implements Backend {
95
212
  success: false;
96
213
  }>;
97
214
  unsetEnvVar(input: UnsetEnvVarInput): Promise<UnsetEnvVarOutput>;
215
+ updateAppDeployment(input: UpdateAppDeploymentInput): Promise<{
216
+ reason: string;
217
+ success: false;
218
+ } | {
219
+ success: true;
220
+ }>;
98
221
  updateExecutorJob(input: {
99
222
  jobId: string;
100
223
  logs?: string;
@@ -101,6 +101,43 @@ export class CloudBackend {
101
101
  success: true,
102
102
  };
103
103
  }
104
+ async deployApp(input) {
105
+ const body = {
106
+ workspace: input.workspace,
107
+ };
108
+ if (input.version)
109
+ body.version = input.version;
110
+ if (input.parameters)
111
+ body.parameters = input.parameters;
112
+ if (input.hereyaVarsYaml !== undefined)
113
+ body.hereyaVarsYaml = input.hereyaVarsYaml;
114
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(input.name)}/deployments`, {
115
+ body: JSON.stringify(body),
116
+ headers: {
117
+ 'Authorization': `Bearer ${this.config.accessToken}`,
118
+ 'Content-Type': 'application/json',
119
+ },
120
+ method: 'POST',
121
+ });
122
+ if (!response.ok) {
123
+ const error = await this.safeResponseJson(response);
124
+ return { reason: error.error || `Failed to deploy app (status ${response.status})`, success: false };
125
+ }
126
+ const result = await response.json();
127
+ return { deploymentId: result.deploymentId, jobId: result.jobId, success: true };
128
+ }
129
+ async destroyApp(input) {
130
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(input.name)}/deployments/${encodeURIComponent(input.workspace)}`, {
131
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
132
+ method: 'DELETE',
133
+ });
134
+ if (!response.ok) {
135
+ const error = await this.safeResponseJson(response);
136
+ return { reason: error.error || `Failed to destroy app (status ${response.status})`, success: false };
137
+ }
138
+ const result = await response.json();
139
+ return { jobId: result.jobId, success: true };
140
+ }
104
141
  async exportBackend() {
105
142
  const response = await fetch(`${this.config.url}/api/export`, {
106
143
  headers: {
@@ -138,6 +175,38 @@ export class CloudBackend {
138
175
  const result = await response.json();
139
176
  return { expiresAt: result.expiresAt, success: true, token: result.token };
140
177
  }
178
+ async getApp(name) {
179
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(name)}`, {
180
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
181
+ method: 'GET',
182
+ });
183
+ if (!response.ok) {
184
+ const error = await this.safeResponseJson(response);
185
+ let errorMessage = error.error || `Failed to get app (status ${response.status})`;
186
+ if (response.status === 404) {
187
+ errorMessage = `App '${name}' not found`;
188
+ }
189
+ return { reason: errorMessage, success: false };
190
+ }
191
+ const result = await response.json();
192
+ return { app: result.app, success: true };
193
+ }
194
+ async getAppDeployment(input) {
195
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(input.name)}/deployments/${encodeURIComponent(input.workspace)}`, {
196
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
197
+ method: 'GET',
198
+ });
199
+ if (!response.ok) {
200
+ const error = await this.safeResponseJson(response);
201
+ let errorMessage = error.error || `Failed to get app deployment (status ${response.status})`;
202
+ if (response.status === 404) {
203
+ errorMessage = `Deployment for app '${input.name}' on workspace '${input.workspace}' not found`;
204
+ }
205
+ return { reason: errorMessage, success: false };
206
+ }
207
+ const result = await response.json();
208
+ return { deployment: result.deployment, success: true };
209
+ }
141
210
  async getExecutorJobStatus(input) {
142
211
  const url = new URL(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/jobs/${encodeURIComponent(input.jobId)}`);
143
212
  if (input.poll) {
@@ -418,6 +487,42 @@ export class CloudBackend {
418
487
  },
419
488
  };
420
489
  }
490
+ async listAppDeployments(name) {
491
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(name)}/deployments`, {
492
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
493
+ method: 'GET',
494
+ });
495
+ if (!response.ok) {
496
+ const error = await this.safeResponseJson(response);
497
+ return { reason: error.error || `Failed to list app deployments (status ${response.status})`, success: false };
498
+ }
499
+ const result = await response.json();
500
+ return { deployments: result.deployments || [], success: true };
501
+ }
502
+ async listApps() {
503
+ const response = await fetch(`${this.config.url}/api/apps`, {
504
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
505
+ method: 'GET',
506
+ });
507
+ if (!response.ok) {
508
+ const error = await this.safeResponseJson(response);
509
+ return { reason: error.error || `Failed to list apps (status ${response.status})`, success: false };
510
+ }
511
+ const result = await response.json();
512
+ return { apps: result.apps || [], success: true };
513
+ }
514
+ async listAppVersions(name) {
515
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(name)}/versions`, {
516
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
517
+ method: 'GET',
518
+ });
519
+ if (!response.ok) {
520
+ const error = await this.safeResponseJson(response);
521
+ return { reason: error.error || `Failed to list app versions (status ${response.status})`, success: false };
522
+ }
523
+ const result = await response.json();
524
+ return { success: true, versions: result.versions || [] };
525
+ }
421
526
  async listPackageVersions(name) {
422
527
  const response = await fetch(`${this.config.url}/api/registry/packages/${encodeURIComponent(name)}?versions=all`, {
423
528
  headers: {
@@ -504,6 +609,55 @@ export class CloudBackend {
504
609
  const result = await response.json();
505
610
  return { job: result.job, success: true };
506
611
  }
612
+ async publishAppVersion(input) {
613
+ const requestBody = {
614
+ commit: input.commit,
615
+ hereyaYaml: input.hereyaYaml,
616
+ repository: input.repository,
617
+ sha256: input.sha256,
618
+ version: input.version,
619
+ };
620
+ if (input.description !== undefined)
621
+ requestBody.description = input.description;
622
+ if (input.visibility !== undefined)
623
+ requestBody.visibility = input.visibility;
624
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(input.name)}/versions`, {
625
+ body: JSON.stringify(requestBody),
626
+ headers: {
627
+ 'Authorization': `Bearer ${this.config.accessToken}`,
628
+ 'Content-Type': 'application/json',
629
+ },
630
+ method: 'POST',
631
+ });
632
+ const result = await this.safeResponseJson(response);
633
+ if (!response.ok) {
634
+ let errorMessage = result.error || 'Failed to publish app version';
635
+ switch (response.status) {
636
+ case 401: {
637
+ errorMessage = 'Authentication failed. Please run `hereya login` to refresh your credentials.';
638
+ break;
639
+ }
640
+ case 403: {
641
+ errorMessage = `Permission denied: ${errorMessage}`;
642
+ break;
643
+ }
644
+ case 409: {
645
+ errorMessage = `Conflict: ${errorMessage}`;
646
+ break;
647
+ }
648
+ }
649
+ return { reason: errorMessage, success: false };
650
+ }
651
+ const app = result.appVersion ?? result.app ?? { name: input.name, version: input.version };
652
+ return {
653
+ app: {
654
+ id: app.id,
655
+ name: app.name ?? input.name,
656
+ version: app.version ?? input.version,
657
+ },
658
+ success: true,
659
+ };
660
+ }
507
661
  async publishPackage(input) {
508
662
  const formData = new FormData();
509
663
  formData.append('name', input.name);
@@ -782,6 +936,28 @@ export class CloudBackend {
782
936
  success: true,
783
937
  };
784
938
  }
939
+ async updateAppDeployment(input) {
940
+ const body = { status: input.status };
941
+ if (input.state !== undefined)
942
+ body.state = input.state;
943
+ if (input.env !== undefined)
944
+ body.env = input.env;
945
+ if (input.lastJobId !== undefined)
946
+ body.lastJobId = input.lastJobId;
947
+ const response = await fetch(`${this.config.url}/api/apps/${encodeURIComponent(input.name)}/deployments/${encodeURIComponent(input.workspace)}`, {
948
+ body: JSON.stringify(body),
949
+ headers: {
950
+ 'Authorization': `Bearer ${this.config.accessToken}`,
951
+ 'Content-Type': 'application/json',
952
+ },
953
+ method: 'PATCH',
954
+ });
955
+ if (!response.ok) {
956
+ const error = await this.safeResponseJson(response);
957
+ return { reason: error.error || `Failed to update app deployment (status ${response.status})`, success: false };
958
+ }
959
+ return { success: true };
960
+ }
785
961
  async updateExecutorJob(input) {
786
962
  const formData = new FormData();
787
963
  if (input.logs) {
@@ -0,0 +1,16 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AppDeploy extends Command {
3
+ static args: {
4
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ parameter: import("@oclif/core/interfaces").OptionFlag<string[], import("@oclif/core/interfaces").CustomOptions>;
10
+ vars: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
11
+ 'vars-file': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
12
+ version: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
13
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
14
+ };
15
+ run(): Promise<void>;
16
+ }
@@ -0,0 +1,113 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { ListrLogger, ListrLogLevels } from 'listr2';
3
+ import fs from 'node:fs/promises';
4
+ import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
5
+ import { getBackend } from '../../../backend/index.js';
6
+ import { arrayOfStringToObject } from '../../../lib/object-utils.js';
7
+ import { pollExecutorJob } from '../../../lib/remote-job-utils.js';
8
+ export default class AppDeploy extends Command {
9
+ static args = {
10
+ name: Args.string({ description: 'app name in org/name format', required: true }),
11
+ };
12
+ static description = 'Deploy a hereya-app to a workspace.';
13
+ static examples = [
14
+ '<%= config.bin %> <%= command.id %> my-org/my-app -w my-workspace',
15
+ '<%= config.bin %> <%= command.id %> my-org/my-app -w prod --version 1.2.0',
16
+ '<%= config.bin %> <%= command.id %> my-org/my-app -w prod -V \'app.yaml: "key: value"\'',
17
+ '<%= config.bin %> <%= command.id %> my-org/my-app -w prod --vars-file ./hereyavars.yaml',
18
+ ];
19
+ static flags = {
20
+ parameter: Flags.string({
21
+ char: 'p',
22
+ default: [],
23
+ description: 'parameter for the app deployment, in the form of key=value (repeatable)',
24
+ multiple: true,
25
+ }),
26
+ vars: Flags.string({
27
+ char: 'V',
28
+ description: 'YAML string mapping hereyavars filename -> YAML body (mutually exclusive with --vars-file)',
29
+ exclusive: ['vars-file'],
30
+ }),
31
+ 'vars-file': Flags.string({
32
+ description: 'path to a YAML file mapping hereyavars filename -> YAML body (mutually exclusive with --vars)',
33
+ exclusive: ['vars'],
34
+ }),
35
+ version: Flags.string({
36
+ description: 'specific app version to deploy (defaults to latest)',
37
+ }),
38
+ workspace: Flags.string({
39
+ char: 'w',
40
+ description: 'name of the workspace to deploy the app to',
41
+ required: true,
42
+ }),
43
+ };
44
+ async run() {
45
+ const { args, flags } = await this.parse(AppDeploy);
46
+ const backend = await getBackend();
47
+ if (!(backend instanceof CloudBackend)) {
48
+ this.error('App deployment requires the cloud backend. Run `hereya login` first.');
49
+ }
50
+ const cloudBackend = backend;
51
+ const myLogger = new ListrLogger({ useIcons: false });
52
+ // Parse parameters
53
+ const parameters = arrayOfStringToObject(flags.parameter);
54
+ // Resolve hereyaVarsYaml
55
+ let hereyaVarsYaml;
56
+ if (flags.vars && flags['vars-file']) {
57
+ this.error('Cannot use both --vars and --vars-file at the same time.');
58
+ }
59
+ else if (flags.vars) {
60
+ hereyaVarsYaml = flags.vars;
61
+ }
62
+ else if (flags['vars-file']) {
63
+ try {
64
+ hereyaVarsYaml = await fs.readFile(flags['vars-file'], 'utf8');
65
+ }
66
+ catch (error) {
67
+ this.error(`Failed to read vars file ${flags['vars-file']}: ${error.message}`);
68
+ }
69
+ }
70
+ myLogger.log(ListrLogLevels.STARTED, `Deploying ${args.name}${flags.version ? `@${flags.version}` : ''} to workspace ${flags.workspace}...`);
71
+ const deployResult = await cloudBackend.deployApp({
72
+ hereyaVarsYaml,
73
+ name: args.name,
74
+ parameters: Object.keys(parameters).length > 0 ? parameters : undefined,
75
+ version: flags.version,
76
+ workspace: flags.workspace,
77
+ });
78
+ if (!deployResult.success) {
79
+ this.error(`Failed to deploy app: ${deployResult.reason}`);
80
+ }
81
+ myLogger.log(ListrLogLevels.COMPLETED, `Deployment ${deployResult.deploymentId} created. Job: ${deployResult.jobId}`);
82
+ // Poll for results
83
+ const logger = {
84
+ debug: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
85
+ error: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
86
+ info: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
87
+ };
88
+ const pollResult = await pollExecutorJob({
89
+ cloudBackend,
90
+ jobId: deployResult.jobId,
91
+ logger,
92
+ workspace: flags.workspace,
93
+ });
94
+ if (!pollResult.success) {
95
+ this.error(pollResult.reason);
96
+ }
97
+ myLogger.log(ListrLogLevels.COMPLETED, `App ${args.name} deployed to ${flags.workspace}`);
98
+ // Print final state
99
+ const status = await cloudBackend.getAppDeployment({ name: args.name, workspace: flags.workspace });
100
+ if (status.success) {
101
+ this.log(`\nStatus: ${status.deployment.status}`);
102
+ if (status.deployment.version) {
103
+ this.log(`Version: ${status.deployment.version}`);
104
+ }
105
+ if (status.deployment.env && Object.keys(status.deployment.env).length > 0) {
106
+ this.log('\nEnv outputs:');
107
+ for (const [key, value] of Object.entries(status.deployment.env)) {
108
+ this.log(` ${key}=${value}`);
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AppDeployments extends Command {
3
+ static args: {
4
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,34 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
3
+ import { getBackend } from '../../../backend/index.js';
4
+ export default class AppDeployments extends Command {
5
+ static args = {
6
+ name: Args.string({ description: 'app name in org/name format', required: true }),
7
+ };
8
+ static description = 'List workspaces a hereya-app has been deployed to.';
9
+ static examples = ['<%= config.bin %> <%= command.id %> my-org/my-app'];
10
+ async run() {
11
+ const { args } = await this.parse(AppDeployments);
12
+ const backend = await getBackend();
13
+ if (!(backend instanceof CloudBackend)) {
14
+ this.error('Listing app deployments requires the cloud backend. Run `hereya login` first.');
15
+ }
16
+ const cloudBackend = backend;
17
+ const result = await cloudBackend.listAppDeployments(args.name);
18
+ if (!result.success) {
19
+ this.error(`Failed to list deployments: ${result.reason}`);
20
+ }
21
+ if (result.deployments.length === 0) {
22
+ this.log(`No deployments found for app ${args.name}.`);
23
+ return;
24
+ }
25
+ const wsWidth = Math.max(9, ...result.deployments.map((d) => d.workspace.length));
26
+ const versionWidth = Math.max(7, ...result.deployments.map((d) => (d.version ?? '').length));
27
+ const statusWidth = Math.max(6, ...result.deployments.map((d) => d.status.length));
28
+ this.log(`${'Workspace'.padEnd(wsWidth)} ${'Version'.padEnd(versionWidth)} ${'Status'.padEnd(statusWidth)}`);
29
+ this.log('-'.repeat(wsWidth + versionWidth + statusWidth + 4));
30
+ for (const deployment of result.deployments) {
31
+ this.log(`${deployment.workspace.padEnd(wsWidth)} ${(deployment.version ?? '').padEnd(versionWidth)} ${deployment.status.padEnd(statusWidth)}`);
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,12 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AppDestroy extends Command {
3
+ static args: {
4
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ static flags: {
9
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
10
+ };
11
+ run(): Promise<void>;
12
+ }
@@ -0,0 +1,52 @@
1
+ import { Args, Command, Flags } from '@oclif/core';
2
+ import { ListrLogger, ListrLogLevels } from 'listr2';
3
+ import { CloudBackend } from '../../../backend/cloud/cloud-backend.js';
4
+ import { getBackend } from '../../../backend/index.js';
5
+ import { pollExecutorJob } from '../../../lib/remote-job-utils.js';
6
+ export default class AppDestroy extends Command {
7
+ static args = {
8
+ name: Args.string({ description: 'app name in org/name format', required: true }),
9
+ };
10
+ static description = 'Destroy a hereya-app deployment from a workspace.';
11
+ static examples = ['<%= config.bin %> <%= command.id %> my-org/my-app -w my-workspace'];
12
+ static flags = {
13
+ workspace: Flags.string({
14
+ char: 'w',
15
+ description: 'workspace where the app is currently deployed',
16
+ required: true,
17
+ }),
18
+ };
19
+ async run() {
20
+ const { args, flags } = await this.parse(AppDestroy);
21
+ const backend = await getBackend();
22
+ if (!(backend instanceof CloudBackend)) {
23
+ this.error('App destroy requires the cloud backend. Run `hereya login` first.');
24
+ }
25
+ const cloudBackend = backend;
26
+ const myLogger = new ListrLogger({ useIcons: false });
27
+ myLogger.log(ListrLogLevels.STARTED, `Destroying ${args.name} on workspace ${flags.workspace}...`);
28
+ const destroyResult = await cloudBackend.destroyApp({
29
+ name: args.name,
30
+ workspace: flags.workspace,
31
+ });
32
+ if (!destroyResult.success) {
33
+ this.error(`Failed to destroy app: ${destroyResult.reason}`);
34
+ }
35
+ myLogger.log(ListrLogLevels.COMPLETED, `Destroy job submitted: ${destroyResult.jobId}`);
36
+ const logger = {
37
+ debug: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
38
+ error: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
39
+ info: (msg) => myLogger.log(ListrLogLevels.OUTPUT, msg),
40
+ };
41
+ const pollResult = await pollExecutorJob({
42
+ cloudBackend,
43
+ jobId: destroyResult.jobId,
44
+ logger,
45
+ workspace: flags.workspace,
46
+ });
47
+ if (!pollResult.success) {
48
+ this.error(pollResult.reason);
49
+ }
50
+ myLogger.log(ListrLogLevels.COMPLETED, `App ${args.name} destroyed from ${flags.workspace}`);
51
+ }
52
+ }
@@ -0,0 +1,13 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class AppEnv extends Command {
3
+ static args: {
4
+ name: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ key: import("@oclif/core/interfaces").Arg<string | undefined, Record<string, unknown>>;
6
+ };
7
+ static description: string;
8
+ static examples: string[];
9
+ static flags: {
10
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
11
+ };
12
+ run(): Promise<void>;
13
+ }