hereya-cli 0.64.1 → 0.64.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +133 -43
  2. package/dist/backend/cloud/cloud-backend.d.ts +70 -0
  3. package/dist/backend/cloud/cloud-backend.js +96 -0
  4. package/dist/backend/common.d.ts +4 -0
  5. package/dist/backend/common.js +1 -0
  6. package/dist/backend/index.d.ts +5 -1
  7. package/dist/backend/index.js +62 -22
  8. package/dist/commands/add/index.js +109 -2
  9. package/dist/commands/deploy/index.js +8 -2
  10. package/dist/commands/docker/run/index.js +1 -0
  11. package/dist/commands/down/index.js +111 -3
  12. package/dist/commands/env/index.js +1 -0
  13. package/dist/commands/executor/start/index.d.ts +11 -0
  14. package/dist/commands/executor/start/index.js +176 -0
  15. package/dist/commands/remove/index.js +138 -4
  16. package/dist/commands/run/index.js +1 -0
  17. package/dist/commands/undeploy/index.js +4 -1
  18. package/dist/commands/up/index.js +102 -5
  19. package/dist/commands/workspace/executor/install/index.d.ts +9 -0
  20. package/dist/commands/workspace/executor/install/index.js +110 -0
  21. package/dist/commands/workspace/executor/token/index.d.ts +8 -0
  22. package/dist/commands/workspace/executor/token/index.js +40 -0
  23. package/dist/commands/workspace/executor/uninstall/index.d.ts +9 -0
  24. package/dist/commands/workspace/executor/uninstall/index.js +102 -0
  25. package/dist/executor/context.d.ts +2 -0
  26. package/dist/executor/context.js +39 -0
  27. package/dist/executor/delegating.d.ts +15 -0
  28. package/dist/executor/delegating.js +50 -0
  29. package/dist/executor/index.d.ts +12 -3
  30. package/dist/executor/index.js +13 -2
  31. package/dist/executor/remote.d.ts +16 -0
  32. package/dist/executor/remote.js +168 -0
  33. package/dist/infrastructure/index.js +55 -22
  34. package/dist/lib/config/common.d.ts +5 -0
  35. package/dist/lib/config/simple.js +43 -24
  36. package/dist/lib/env/index.d.ts +9 -0
  37. package/dist/lib/env/index.js +101 -15
  38. package/dist/lib/package/index.d.ts +12 -0
  39. package/dist/lib/package/index.js +4 -0
  40. package/oclif.manifest.json +183 -25
  41. package/package.json +1 -1
@@ -126,6 +126,37 @@ export class CloudBackend {
126
126
  success: true,
127
127
  };
128
128
  }
129
+ async generateExecutorToken(input) {
130
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/executor-token`, {
131
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
132
+ method: 'POST',
133
+ });
134
+ if (!response.ok) {
135
+ const error = await response.json();
136
+ return { reason: error.error || 'Failed to generate executor token', success: false };
137
+ }
138
+ const result = await response.json();
139
+ return { success: true, token: result.token };
140
+ }
141
+ async getExecutorJobStatus(input) {
142
+ const url = new URL(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/jobs/${encodeURIComponent(input.jobId)}`);
143
+ if (input.poll) {
144
+ url.searchParams.set('poll', 'true');
145
+ }
146
+ if (input.lastStatus) {
147
+ url.searchParams.set('lastStatus', input.lastStatus);
148
+ }
149
+ const response = await fetch(url.toString(), {
150
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
151
+ method: 'GET',
152
+ });
153
+ if (!response.ok) {
154
+ const error = await response.json();
155
+ return { reason: error.error || 'Failed to get job status', success: false };
156
+ }
157
+ const result = await response.json();
158
+ return { job: result.job, success: true };
159
+ }
129
160
  async getPackageByVersion(name, version) {
130
161
  const response = await fetch(`${this.config.url}/api/registry/packages/${encodeURIComponent(name)}/${encodeURIComponent(version)}`, {
131
162
  headers: {
@@ -411,6 +442,18 @@ export class CloudBackend {
411
442
  const result = await response.json();
412
443
  return result.workspaces.map((workspace) => workspace.name);
413
444
  }
445
+ async pollExecutorJobs(input) {
446
+ const response = await fetch(`${this.config.url}/api/executor/jobs?workspace=${encodeURIComponent(input.workspace)}`, {
447
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
448
+ method: 'GET',
449
+ });
450
+ if (!response.ok) {
451
+ const error = await response.json();
452
+ return { reason: error.error || 'Failed to poll for jobs', success: false };
453
+ }
454
+ const result = await response.json();
455
+ return { job: result.job, success: true };
456
+ }
414
457
  async publishPackage(input) {
415
458
  const formData = new FormData();
416
459
  formData.append('name', input.name);
@@ -533,6 +576,17 @@ export class CloudBackend {
533
576
  workspace: this.convertWorkspace(result.workspace),
534
577
  };
535
578
  }
579
+ async revokeExecutorToken(input) {
580
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/executor-token`, {
581
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
582
+ method: 'DELETE',
583
+ });
584
+ if (!response.ok) {
585
+ const error = await response.json();
586
+ return { reason: error.error || 'Failed to revoke executor token', success: false };
587
+ }
588
+ return { success: true };
589
+ }
536
590
  async saveState(config, workspace) {
537
591
  const formData = new FormData();
538
592
  if (workspace) {
@@ -622,6 +676,22 @@ export class CloudBackend {
622
676
  success: true,
623
677
  };
624
678
  }
679
+ async submitExecutorJob(input) {
680
+ const formData = new FormData();
681
+ formData.append('type', input.type);
682
+ formData.append('payload', JSON.stringify(input.payload));
683
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/jobs`, {
684
+ body: formData,
685
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
686
+ method: 'POST',
687
+ });
688
+ if (!response.ok) {
689
+ const error = await response.json();
690
+ return { reason: error.error || 'Failed to submit executor job', success: false };
691
+ }
692
+ const result = await response.json();
693
+ return { jobId: result.jobId, success: true };
694
+ }
625
695
  async unsetEnvVar(input) {
626
696
  const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/env/${encodeURIComponent(input.name)}`, {
627
697
  headers: {
@@ -639,11 +709,36 @@ export class CloudBackend {
639
709
  success: true,
640
710
  };
641
711
  }
712
+ async updateExecutorJob(input) {
713
+ const formData = new FormData();
714
+ if (input.logs) {
715
+ formData.append('logs', input.logs);
716
+ }
717
+ if (input.status) {
718
+ formData.append('status', input.status);
719
+ }
720
+ if (input.result) {
721
+ formData.append('result', JSON.stringify(input.result));
722
+ }
723
+ const response = await fetch(`${this.config.url}/api/executor/jobs/${encodeURIComponent(input.jobId)}`, {
724
+ body: formData,
725
+ headers: { 'Authorization': `Bearer ${this.config.accessToken}` },
726
+ method: 'PATCH',
727
+ });
728
+ if (!response.ok) {
729
+ const error = await response.json();
730
+ return { reason: error.error || 'Failed to update job', success: false };
731
+ }
732
+ return { success: true };
733
+ }
642
734
  async updateWorkspace(input) {
643
735
  const formData = new FormData();
644
736
  if (input.profile !== undefined) {
645
737
  formData.append('profile', input.profile === null ? '' : input.profile);
646
738
  }
739
+ if (input.hasExecutor !== undefined) {
740
+ formData.append('hasExecutor', input.hasExecutor === null ? '' : String(input.hasExecutor));
741
+ }
647
742
  if (input.isDeploy !== undefined) {
648
743
  formData.append('isDeploy', input.isDeploy === null ? '' : String(input.isDeploy));
649
744
  }
@@ -693,6 +788,7 @@ export class CloudBackend {
693
788
  }
694
789
  return {
695
790
  env,
791
+ hasExecutor: workspace.hasExecutor ?? undefined,
696
792
  id: workspace.id,
697
793
  isDeploy: workspace.isDeploy,
698
794
  mirrorOf: workspace.mirrorOf?.name,
@@ -27,6 +27,7 @@ export interface Backend {
27
27
  }
28
28
  export declare const WorkspaceSchema: z.ZodObject<{
29
29
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
30
+ hasExecutor: z.ZodOptional<z.ZodBoolean>;
30
31
  id: z.ZodString;
31
32
  isDeploy: z.ZodOptional<z.ZodBoolean>;
32
33
  mirrorOf: z.ZodOptional<z.ZodString>;
@@ -51,6 +52,7 @@ export declare const WorkspaceSchema: z.ZodObject<{
51
52
  parameters?: Record<string, any> | undefined;
52
53
  }> | undefined;
53
54
  profile?: string | undefined;
55
+ hasExecutor?: boolean | undefined;
54
56
  isDeploy?: boolean | undefined;
55
57
  mirrorOf?: string | undefined;
56
58
  }, {
@@ -62,6 +64,7 @@ export declare const WorkspaceSchema: z.ZodObject<{
62
64
  parameters?: Record<string, any> | undefined;
63
65
  }> | undefined;
64
66
  profile?: string | undefined;
67
+ hasExecutor?: boolean | undefined;
65
68
  isDeploy?: boolean | undefined;
66
69
  mirrorOf?: string | undefined;
67
70
  }>;
@@ -222,6 +225,7 @@ export type UnsetEnvVarInput = {
222
225
  };
223
226
  export type UnsetEnvVarOutput = SetEnvVarOutput;
224
227
  export type UpdateWorkspaceInput = {
228
+ hasExecutor?: boolean | null;
225
229
  isDeploy?: boolean | null;
226
230
  name: string;
227
231
  profile?: null | string;
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  export const WorkspaceSchema = z.object({
3
3
  env: z.record(z.string()).optional(),
4
+ hasExecutor: z.boolean().optional(),
4
5
  id: z.string().min(2),
5
6
  isDeploy: z.boolean().optional(),
6
7
  mirrorOf: z.string().optional(),
@@ -1,5 +1,9 @@
1
1
  import { Backend } from './common.js';
2
- export declare function getBackend(type?: BackendType): Promise<Backend>;
2
+ export interface GetBackendOptions {
3
+ token?: string;
4
+ url?: string;
5
+ }
6
+ export declare function getBackend(typeOrOptions?: BackendType | GetBackendOptions): Promise<Backend>;
3
7
  export declare function clearBackend(): void;
4
8
  export declare function setBackendType(type: BackendType): void;
5
9
  export declare enum BackendType {
@@ -1,11 +1,54 @@
1
1
  import { getAwsConfig } from '../infrastructure/aws-config.js';
2
2
  import { CloudBackend } from './cloud/cloud-backend.js';
3
- import { refreshToken } from './cloud/login.js';
3
+ import { loginWithToken, refreshToken } from './cloud/login.js';
4
4
  import { getCloudCredentials, loadBackendConfig, saveCloudCredentials } from './config.js';
5
5
  import { LocalFileBackend } from './local.js';
6
6
  import { S3FileBackend } from './s3.js';
7
7
  let backend;
8
8
  let currentBackendType;
9
+ function sleep(ms) {
10
+ return new Promise(resolve => {
11
+ setTimeout(resolve, ms);
12
+ });
13
+ }
14
+ async function refreshCloudCredentials(cloudConfig) {
15
+ const maxAttempts = 3;
16
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
17
+ // Re-read credentials from disk on each attempt, as another process may have refreshed them
18
+ // eslint-disable-next-line no-await-in-loop
19
+ const credentials = await getCloudCredentials(cloudConfig.clientId);
20
+ if (!credentials) {
21
+ throw new Error('Cloud credentials not found. Please run `hereya login` first.');
22
+ }
23
+ let { accessToken, refreshToken: refreshTokenValue } = credentials;
24
+ // If token is still valid (possibly refreshed by another process), we're done
25
+ if (!isTokenExpired(accessToken)) {
26
+ return { accessToken, refreshToken: refreshTokenValue };
27
+ }
28
+ // Token is expired, attempt to refresh
29
+ // eslint-disable-next-line no-await-in-loop
30
+ const refreshResult = await refreshToken(cloudConfig.url, refreshTokenValue);
31
+ if (refreshResult.success) {
32
+ accessToken = refreshResult.accessToken;
33
+ refreshTokenValue = refreshResult.refreshToken;
34
+ // eslint-disable-next-line no-await-in-loop
35
+ await saveCloudCredentials({
36
+ accessToken,
37
+ clientId: cloudConfig.clientId,
38
+ refreshToken: refreshTokenValue,
39
+ url: cloudConfig.url,
40
+ });
41
+ return { accessToken, refreshToken: refreshTokenValue };
42
+ }
43
+ // Refresh failed — another process may have consumed the refresh token.
44
+ // Wait with backoff before retrying so the other process can finish saving new credentials.
45
+ if (attempt < maxAttempts) {
46
+ // eslint-disable-next-line no-await-in-loop
47
+ await sleep(attempt * 500);
48
+ }
49
+ }
50
+ throw new Error('Failed to refresh token. Please run `hereya login` again.');
51
+ }
9
52
  function isTokenExpired(token) {
10
53
  try {
11
54
  const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
@@ -19,10 +62,26 @@ function isTokenExpired(token) {
19
62
  return true;
20
63
  }
21
64
  }
22
- export async function getBackend(type) {
65
+ export async function getBackend(typeOrOptions) {
23
66
  if (backend) {
24
67
  return backend;
25
68
  }
69
+ // Handle token-based auth
70
+ if (typeOrOptions && typeof typeOrOptions === 'object' && typeOrOptions.token) {
71
+ const url = typeOrOptions.url || 'https://cloud.hereya.dev';
72
+ const result = await loginWithToken(url, typeOrOptions.token);
73
+ if (!result.success) {
74
+ throw new Error(`Failed to authenticate with token: ${result.error}`);
75
+ }
76
+ backend = new CloudBackend({
77
+ accessToken: result.accessToken,
78
+ clientId: result.clientId,
79
+ refreshToken: result.refreshToken,
80
+ url,
81
+ });
82
+ return backend;
83
+ }
84
+ const type = typeof typeOrOptions === 'string' ? typeOrOptions : undefined;
26
85
  const backendConfig = await loadBackendConfig();
27
86
  const backendType = type ?? currentBackendType ?? backendConfig.current;
28
87
  switch (backendType) {
@@ -30,26 +89,7 @@ export async function getBackend(type) {
30
89
  if (!backendConfig.cloud) {
31
90
  throw new Error('Cloud credentials not found. Please run `hereya login` first.');
32
91
  }
33
- const credentials = await getCloudCredentials(backendConfig.cloud.clientId);
34
- if (!credentials) {
35
- throw new Error('Cloud credentials not found. Please run `hereya login` first.');
36
- }
37
- let { accessToken, refreshToken: refreshTokenValue } = credentials;
38
- // Check if token is expired and refresh if necessary
39
- if (isTokenExpired(accessToken)) {
40
- const refreshResult = await refreshToken(backendConfig.cloud.url, refreshTokenValue);
41
- if (!refreshResult.success) {
42
- throw new Error('Failed to refresh token. Please run `hereya login` again.');
43
- }
44
- accessToken = refreshResult.accessToken;
45
- refreshTokenValue = refreshResult.refreshToken;
46
- await saveCloudCredentials({
47
- accessToken,
48
- clientId: backendConfig.cloud.clientId,
49
- refreshToken: refreshTokenValue,
50
- url: backendConfig.cloud.url,
51
- });
52
- }
92
+ const { accessToken, refreshToken: refreshTokenValue } = await refreshCloudCredentials(backendConfig.cloud);
53
93
  backend = new CloudBackend({
54
94
  accessToken,
55
95
  clientId: backendConfig.cloud.clientId,
@@ -1,7 +1,7 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
2
  import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
3
3
  import { getBackend } from '../../backend/index.js';
4
- import { getExecutor } from '../../executor/index.js';
4
+ import { getExecutorForWorkspace } from '../../executor/context.js';
5
5
  import { getConfigManager } from '../../lib/config/index.js';
6
6
  import { logEnv } from '../../lib/env-utils.js';
7
7
  import { getEnvManager } from '../../lib/env/index.js';
@@ -107,7 +107,7 @@ export default class Add extends Command {
107
107
  persistentOutput: isDebug(),
108
108
  },
109
109
  async task(ctx, task) {
110
- const executor$ = getExecutor();
110
+ const executor$ = await getExecutorForWorkspace(ctx.workspace, ctx.configOutput.config.project);
111
111
  if (!executor$.success) {
112
112
  throw new Error(executor$.reason);
113
113
  }
@@ -135,7 +135,9 @@ export default class Add extends Command {
135
135
  await envManager.addProjectEnv({
136
136
  env,
137
137
  infra: metadata.originalInfra ?? metadata.infra,
138
+ pkg: ctx.provisionOutput.pkgName,
138
139
  projectRootDir,
140
+ snakeCase: metadata.snakeCase,
139
141
  workspace: ctx.workspace,
140
142
  });
141
143
  await delay(500);
@@ -176,6 +178,111 @@ export default class Add extends Command {
176
178
  },
177
179
  title: 'Saving state',
178
180
  },
181
+ {
182
+ async task(ctx) {
183
+ const configManager = getConfigManager();
184
+ const { config } = await configManager.loadConfig({ projectRootDir });
185
+ const devDeploy = config.devDeploy ?? {};
186
+ if (Object.keys(devDeploy).length === 0) {
187
+ return;
188
+ }
189
+ const backend = await getBackend();
190
+ const profile = await getProfileFromWorkspace(backend, ctx.workspace, ctx.configOutput.config.project);
191
+ const envManager = getEnvManager();
192
+ const getProjectEnvOutput = await envManager.getProjectEnv({
193
+ excludeDevDeploy: true,
194
+ markSecret: false,
195
+ profile,
196
+ project: ctx.configOutput.config.project,
197
+ projectRootDir,
198
+ workspace: ctx.workspace,
199
+ });
200
+ if (!getProjectEnvOutput.success) {
201
+ throw new Error(getProjectEnvOutput.reason);
202
+ }
203
+ ctx.projectEnv = getProjectEnvOutput.env;
204
+ // Update configOutput with fresh config
205
+ ctx.configOutput = { config, found: true };
206
+ await delay(500);
207
+ },
208
+ title: 'Loading project environment',
209
+ },
210
+ {
211
+ skip(ctx) {
212
+ const devDeploy = ctx.configOutput.config.devDeploy ?? {};
213
+ return Object.keys(devDeploy).length === 0;
214
+ },
215
+ async task(ctx, task) {
216
+ const configManager = getConfigManager();
217
+ const { config } = await configManager.loadConfig({ projectRootDir });
218
+ const devDeployPackages = config.devDeploy ?? {};
219
+ const devDeployEntries = Object.entries(devDeployPackages).map(([name, info]) => ({
220
+ name,
221
+ packageSpec: info.version ? `${name}@${info.version}` : name,
222
+ version: info.version || '',
223
+ }));
224
+ return task.newListr(devDeployEntries.map((pkg) => ({
225
+ rendererOptions: {
226
+ persistentOutput: isDebug(),
227
+ },
228
+ async task(_, task) {
229
+ const parameterManager = getParameterManager();
230
+ const backend = await getBackend();
231
+ const profile = await getProfileFromWorkspace(backend, ctx.workspace, ctx.configOutput.config.project);
232
+ const { parameters } = await parameterManager.getPackageParameters({
233
+ package: pkg.name,
234
+ profile,
235
+ projectRootDir,
236
+ });
237
+ const executor$ = await getExecutorForWorkspace(ctx.workspace, ctx.configOutput.config.project);
238
+ if (!executor$.success) {
239
+ throw new Error(executor$.reason);
240
+ }
241
+ const { executor } = executor$;
242
+ const provisionOutput = await executor.provision({
243
+ logger: getLogger(task),
244
+ package: pkg.packageSpec,
245
+ parameters,
246
+ project: ctx.configOutput.config.project,
247
+ projectEnv: ctx.projectEnv ?? {},
248
+ workspace: ctx.workspace,
249
+ });
250
+ if (!provisionOutput.success) {
251
+ throw new Error(provisionOutput.reason);
252
+ }
253
+ const { env, metadata } = provisionOutput;
254
+ const output = ctx.devDeployAdded || [];
255
+ output.push({ env, metadata, packageName: pkg.name });
256
+ ctx.devDeployAdded = output;
257
+ },
258
+ title: `Provisioning ${pkg.name}`,
259
+ })), { concurrent: true, rendererOptions: { collapseSubtasks: !isDebug() } });
260
+ },
261
+ title: 'Provisioning devDeploy packages',
262
+ },
263
+ {
264
+ skip: (ctx) => !ctx.devDeployAdded || ctx.devDeployAdded.length === 0,
265
+ async task(ctx) {
266
+ if (!ctx.devDeployAdded || ctx.devDeployAdded.length === 0) {
267
+ return;
268
+ }
269
+ const envManager = getEnvManager();
270
+ for (const { env, metadata, packageName } of ctx.devDeployAdded) {
271
+ // eslint-disable-next-line no-await-in-loop
272
+ await envManager.addProjectEnv({
273
+ env,
274
+ infra: metadata.originalInfra ?? metadata.infra,
275
+ isDevDeploy: true,
276
+ pkg: packageName,
277
+ projectRootDir,
278
+ snakeCase: metadata.snakeCase,
279
+ workspace: ctx.workspace,
280
+ });
281
+ }
282
+ await delay(500);
283
+ },
284
+ title: 'Adding env vars from devDeploy packages',
285
+ },
179
286
  ], { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
180
287
  },
181
288
  title: `Adding ${args.package}`,
@@ -81,6 +81,7 @@ export default class Deploy extends Command {
81
81
  const getProjectEnvOutput = await envManager.getProjectEnv({
82
82
  markSecret: true,
83
83
  profile,
84
+ project: ctx.configOutput.config.project,
84
85
  projectRootDir,
85
86
  workspace: ctx.workspace,
86
87
  });
@@ -267,12 +268,14 @@ export default class Deploy extends Command {
267
268
  return;
268
269
  }
269
270
  const envManager = getEnvManager();
270
- for (const { env, metadata } of ctx.removed) {
271
+ for (const { env, metadata, packageName } of ctx.removed) {
271
272
  // eslint-disable-next-line no-await-in-loop
272
273
  await envManager.removeProjectEnv({
273
274
  env,
274
275
  infra: metadata.originalInfra ?? metadata.infra,
276
+ pkg: packageName,
275
277
  projectRootDir,
278
+ snakeCase: metadata.snakeCase,
276
279
  workspace: ctx.workspace,
277
280
  });
278
281
  }
@@ -287,12 +290,14 @@ export default class Deploy extends Command {
287
290
  return;
288
291
  }
289
292
  const envManager = getEnvManager();
290
- for (const { env, metadata } of ctx.added) {
293
+ for (const { env, metadata, packageName } of ctx.added) {
291
294
  // eslint-disable-next-line no-await-in-loop
292
295
  await envManager.addProjectEnv({
293
296
  env,
294
297
  infra: metadata.originalInfra ?? metadata.infra,
298
+ pkg: packageName,
295
299
  projectRootDir,
300
+ snakeCase: metadata.snakeCase,
296
301
  workspace: ctx.workspace,
297
302
  });
298
303
  }
@@ -319,6 +324,7 @@ export default class Deploy extends Command {
319
324
  const getProjectEnvOutput = await envManager.getProjectEnv({
320
325
  markSecret: true,
321
326
  profile,
327
+ project: ctx.configOutput.config.project,
322
328
  projectRootDir,
323
329
  workspace: ctx.workspace,
324
330
  });
@@ -62,6 +62,7 @@ export default class DockerRun extends Command {
62
62
  const envManager = getEnvManager();
63
63
  const getProjectEnvOutput = await envManager.getProjectEnv({
64
64
  profile,
65
+ project: config.project,
65
66
  projectRootDir,
66
67
  workspace,
67
68
  });
@@ -1,7 +1,7 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
2
  import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
3
3
  import { getBackend } from '../../backend/index.js';
4
- import { getExecutor } from '../../executor/index.js';
4
+ import { getExecutorForWorkspace } from '../../executor/context.js';
5
5
  import { getConfigManager } from '../../lib/config/index.js';
6
6
  import { getEnvManager } from '../../lib/env/index.js';
7
7
  import { getLogger, getLogPath, isDebug, setDebug } from '../../lib/log.js';
@@ -115,10 +115,116 @@ export default class Down extends Command {
115
115
  packagesWithVersions = selectedPackages;
116
116
  }
117
117
  ctx.packages = [...packagesWithVersions, ...removedPackagesWithVersions];
118
+ // Collect devDeploy packages from config and saved state
119
+ const configDevDeploy = ctx.configOutput.config.devDeploy ?? {};
120
+ const devDeployFromConfig = Object.entries(configDevDeploy).map(([name, info]) => ({
121
+ name,
122
+ packageSpec: info.version ? `${name}@${info.version}` : name,
123
+ version: info.version || '',
124
+ }));
125
+ let devDeployFromState = [];
126
+ if (savedStateOutput.found) {
127
+ const savedDevDeploy = savedStateOutput.config.devDeploy ?? {};
128
+ devDeployFromState = Object.entries(savedDevDeploy)
129
+ .filter(([name]) => !configDevDeploy[name])
130
+ .map(([name, info]) => ({
131
+ name,
132
+ packageSpec: info.version ? `${name}@${info.version}` : name,
133
+ version: info.version || '',
134
+ }));
135
+ }
136
+ ctx.devDeployPackages = [...devDeployFromConfig, ...devDeployFromState];
118
137
  await delay(500);
119
138
  },
120
139
  title: 'Identifying packages to destroy',
121
140
  },
141
+ {
142
+ skip: (ctx) => !ctx.devDeployPackages || ctx.devDeployPackages.length === 0,
143
+ async task(ctx) {
144
+ // Load project env for devDeploy destroy
145
+ const backend = await getBackend();
146
+ const profile = await getProfileFromWorkspace(backend, ctx.workspace, ctx.configOutput.config.project);
147
+ const envManager = getEnvManager();
148
+ const getProjectEnvOutput = await envManager.getProjectEnv({
149
+ excludeDevDeploy: true,
150
+ markSecret: false,
151
+ profile,
152
+ project: ctx.configOutput.config.project,
153
+ projectRootDir,
154
+ workspace: ctx.workspace,
155
+ });
156
+ if (!getProjectEnvOutput.success) {
157
+ throw new Error(getProjectEnvOutput.reason);
158
+ }
159
+ ctx.projectEnv = getProjectEnvOutput.env;
160
+ await delay(500);
161
+ },
162
+ title: 'Loading project environment for devDeploy',
163
+ },
164
+ {
165
+ skip: (ctx) => !ctx.devDeployPackages || ctx.devDeployPackages.length === 0,
166
+ async task(ctx) {
167
+ return task.newListr(ctx.devDeployPackages.map((pkg) => ({
168
+ rendererOptions: {
169
+ persistentOutput: isDebug(),
170
+ },
171
+ async task(_, task) {
172
+ const parameterManager = getParameterManager();
173
+ const backend = await getBackend();
174
+ const profile = await getProfileFromWorkspace(backend, ctx.workspace, ctx.configOutput.config.project);
175
+ const { parameters } = await parameterManager.getPackageParameters({
176
+ package: pkg.name,
177
+ profile,
178
+ projectRootDir,
179
+ });
180
+ const executor$ = await getExecutorForWorkspace(ctx.workspace, ctx.configOutput.config.project);
181
+ if (!executor$.success) {
182
+ throw new Error(executor$.reason);
183
+ }
184
+ const { executor } = executor$;
185
+ const destroyOutput = await executor.destroy({
186
+ logger: getLogger(task),
187
+ package: pkg.packageSpec,
188
+ parameters,
189
+ project: ctx.configOutput.config.project,
190
+ projectEnv: ctx.projectEnv,
191
+ workspace: ctx.workspace,
192
+ });
193
+ if (!destroyOutput.success) {
194
+ throw new Error(destroyOutput.reason);
195
+ }
196
+ const { env, metadata } = destroyOutput;
197
+ const output = ctx.devDeployDestroyed || [];
198
+ output.push({ env, metadata, packageName: pkg.name });
199
+ ctx.devDeployDestroyed = output;
200
+ },
201
+ title: `Destroying ${pkg.name}`,
202
+ })), { concurrent: true, rendererOptions: { collapseSubtasks: !isDebug() } });
203
+ },
204
+ title: 'Destroying devDeploy packages',
205
+ },
206
+ {
207
+ skip: (ctx) => !ctx.devDeployDestroyed || ctx.devDeployDestroyed.length === 0,
208
+ async task(ctx) {
209
+ if (!ctx.devDeployDestroyed || ctx.devDeployDestroyed.length === 0) {
210
+ return;
211
+ }
212
+ const envManager = getEnvManager();
213
+ for (const { env, metadata, packageName } of ctx.devDeployDestroyed) {
214
+ // eslint-disable-next-line no-await-in-loop
215
+ await envManager.removeProjectEnv({
216
+ env,
217
+ infra: metadata.originalInfra ?? metadata.infra,
218
+ pkg: packageName,
219
+ projectRootDir,
220
+ snakeCase: metadata.snakeCase,
221
+ workspace: ctx.workspace,
222
+ });
223
+ }
224
+ await delay(500);
225
+ },
226
+ title: 'Removing env vars from devDeploy packages',
227
+ },
122
228
  {
123
229
  skip: (ctx) => !ctx.packages || ctx.packages.length === 0,
124
230
  async task(ctx) {
@@ -138,7 +244,7 @@ export default class Down extends Command {
138
244
  profile,
139
245
  projectRootDir,
140
246
  });
141
- const executor$ = getExecutor();
247
+ const executor$ = await getExecutorForWorkspace(ctx.workspace, ctx.configOutput.config.project);
142
248
  if (!executor$.success) {
143
249
  throw new Error(executor$.reason);
144
250
  }
@@ -173,12 +279,14 @@ export default class Down extends Command {
173
279
  return;
174
280
  }
175
281
  const envManager = getEnvManager();
176
- for (const { env, metadata } of destroyed) {
282
+ for (const { env, metadata, packageName } of destroyed) {
177
283
  // eslint-disable-next-line no-await-in-loop
178
284
  await envManager.removeProjectEnv({
179
285
  env,
180
286
  infra: metadata.originalInfra ?? metadata.infra,
287
+ pkg: packageName,
181
288
  projectRootDir,
289
+ snakeCase: metadata.snakeCase,
182
290
  workspace,
183
291
  });
184
292
  }
@@ -53,6 +53,7 @@ export default class Env extends Command {
53
53
  const envManager = getEnvManager();
54
54
  const getProjectEnvOutput = await envManager.getProjectEnv({
55
55
  profile,
56
+ project: config.project,
56
57
  projectRootDir,
57
58
  workspace,
58
59
  });