hereya-cli 0.64.2 → 0.65.0

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 +71 -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 +18 -2
  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 +41 -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 +159 -1
  41. package/package.json +1 -1
@@ -0,0 +1,110 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
3
+ import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
4
+ import { getBackend } from '../../../../backend/index.js';
5
+ import { getExecutor } from '../../../../executor/index.js';
6
+ import { getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
7
+ import { delay } from '../../../../lib/shell.js';
8
+ const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
9
+ export default class WorkspaceExecutorInstall extends Command {
10
+ static description = 'Install a remote executor into a workspace';
11
+ static flags = {
12
+ debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
13
+ workspace: Flags.string({
14
+ char: 'w',
15
+ description: 'name of the workspace',
16
+ required: true,
17
+ }),
18
+ };
19
+ async run() {
20
+ const { flags } = await this.parse(WorkspaceExecutorInstall);
21
+ setDebug(flags.debug);
22
+ const myLogger = new ListrLogger({ useIcons: false });
23
+ const task = new Listr([
24
+ {
25
+ async task(_ctx, task) {
26
+ return task.newListr([
27
+ {
28
+ async task() {
29
+ const backend = await getBackend();
30
+ if (!(backend instanceof CloudBackend)) {
31
+ throw new TypeError('Remote executor requires cloud backend. Run `hereya login` first.');
32
+ }
33
+ const workspace$ = await backend.getWorkspace(flags.workspace);
34
+ if (!workspace$.found) {
35
+ throw new Error(`Workspace ${flags.workspace} not found`);
36
+ }
37
+ if (workspace$.hasError) {
38
+ throw new Error(workspace$.error);
39
+ }
40
+ if (workspace$.workspace.hasExecutor) {
41
+ throw new Error(`Workspace ${flags.workspace} already has an executor installed`);
42
+ }
43
+ await delay(500);
44
+ },
45
+ title: 'Validating workspace',
46
+ },
47
+ {
48
+ async task(ctx) {
49
+ const backend = await getBackend();
50
+ const tokenResult = await backend.generateExecutorToken({
51
+ workspace: flags.workspace,
52
+ });
53
+ if (!tokenResult.success) {
54
+ throw new Error(`Failed to generate executor token: ${tokenResult.reason}`);
55
+ }
56
+ ctx.executorToken = tokenResult.token;
57
+ await delay(500);
58
+ },
59
+ title: 'Generating executor token',
60
+ },
61
+ {
62
+ rendererOptions: {
63
+ persistentOutput: isDebug(),
64
+ },
65
+ async task(ctx, task) {
66
+ const executor$ = getExecutor();
67
+ if (!executor$.success) {
68
+ throw new Error(executor$.reason);
69
+ }
70
+ const { executor } = executor$;
71
+ const provisionOutput = await executor.provision({
72
+ logger: getLogger(task),
73
+ package: DEFAULT_EXECUTOR_PACKAGE,
74
+ parameters: {
75
+ EXECUTOR_TOKEN: ctx.executorToken,
76
+ WORKSPACE: flags.workspace,
77
+ },
78
+ skipDeploy: true,
79
+ });
80
+ if (!provisionOutput.success) {
81
+ throw new Error(provisionOutput.reason);
82
+ }
83
+ },
84
+ title: 'Provisioning remote executor infrastructure',
85
+ },
86
+ {
87
+ async task() {
88
+ const backend = await getBackend();
89
+ await backend.updateWorkspace({
90
+ hasExecutor: true,
91
+ name: flags.workspace,
92
+ });
93
+ await delay(500);
94
+ },
95
+ title: 'Registering executor on workspace',
96
+ },
97
+ ], { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
98
+ },
99
+ title: `Installing executor on workspace ${flags.workspace}`,
100
+ },
101
+ ], { concurrent: false });
102
+ try {
103
+ await task.run();
104
+ myLogger.log(ListrLogLevels.COMPLETED, 'Executor installed successfully');
105
+ }
106
+ catch (error) {
107
+ this.error(`${error.message}\n\nSee ${getLogPath()} for more details`);
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,8 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class WorkspaceExecutorToken extends Command {
3
+ static description: string;
4
+ static flags: {
5
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
6
+ };
7
+ run(): Promise<void>;
8
+ }
@@ -0,0 +1,41 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
3
+ import { getBackend } from '../../../../backend/index.js';
4
+ export default class WorkspaceExecutorToken extends Command {
5
+ static description = 'Generate a workspace-scoped executor token for the remote executor';
6
+ static flags = {
7
+ workspace: Flags.string({
8
+ char: 'w',
9
+ description: 'name of the workspace',
10
+ required: true,
11
+ }),
12
+ };
13
+ async run() {
14
+ const { flags } = await this.parse(WorkspaceExecutorToken);
15
+ const backend = await getBackend();
16
+ if (!(backend instanceof CloudBackend)) {
17
+ this.error('Remote executor requires cloud backend. Run `hereya login` first.');
18
+ }
19
+ const workspace$ = await backend.getWorkspace(flags.workspace);
20
+ if (!workspace$.found) {
21
+ this.error(`Workspace ${flags.workspace} not found`);
22
+ }
23
+ if (workspace$.hasError) {
24
+ this.error(workspace$.error);
25
+ }
26
+ const tokenResult = await backend.generateExecutorToken({
27
+ workspace: flags.workspace,
28
+ });
29
+ if (!tokenResult.success) {
30
+ this.error(`Failed to generate executor token: ${tokenResult.reason}`);
31
+ }
32
+ await backend.updateWorkspace({
33
+ hasExecutor: true,
34
+ name: flags.workspace,
35
+ });
36
+ this.log(tokenResult.token);
37
+ this.log('');
38
+ this.log(`Expires: ${new Date(tokenResult.expiresAt).toLocaleDateString()}`);
39
+ this.log(`Use this token with: hereya login --token ${tokenResult.token}`);
40
+ }
41
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class WorkspaceExecutorUninstall extends Command {
3
+ static description: string;
4
+ static flags: {
5
+ debug: import("@oclif/core/interfaces").BooleanFlag<boolean>;
6
+ workspace: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
7
+ };
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,102 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { Listr, ListrLogger, ListrLogLevels } from 'listr2';
3
+ import { CloudBackend } from '../../../../backend/cloud/cloud-backend.js';
4
+ import { getBackend } from '../../../../backend/index.js';
5
+ import { getExecutor } from '../../../../executor/index.js';
6
+ import { getLogger, getLogPath, isDebug, setDebug } from '../../../../lib/log.js';
7
+ import { delay } from '../../../../lib/shell.js';
8
+ const DEFAULT_EXECUTOR_PACKAGE = 'hereya/remote-executor-aws';
9
+ export default class WorkspaceExecutorUninstall extends Command {
10
+ static description = 'Uninstall the remote executor from a workspace';
11
+ static flags = {
12
+ debug: Flags.boolean({ default: false, description: 'enable debug mode' }),
13
+ workspace: Flags.string({
14
+ char: 'w',
15
+ description: 'name of the workspace',
16
+ required: true,
17
+ }),
18
+ };
19
+ async run() {
20
+ const { flags } = await this.parse(WorkspaceExecutorUninstall);
21
+ setDebug(flags.debug);
22
+ const myLogger = new ListrLogger({ useIcons: false });
23
+ const task = new Listr([
24
+ {
25
+ async task(_ctx, task) {
26
+ return task.newListr([
27
+ {
28
+ async task() {
29
+ const backend = await getBackend();
30
+ if (!(backend instanceof CloudBackend)) {
31
+ throw new TypeError('Remote executor requires cloud backend.');
32
+ }
33
+ const workspace$ = await backend.getWorkspace(flags.workspace);
34
+ if (!workspace$.found) {
35
+ throw new Error(`Workspace ${flags.workspace} not found`);
36
+ }
37
+ if (workspace$.hasError) {
38
+ throw new Error(workspace$.error);
39
+ }
40
+ if (!workspace$.workspace.hasExecutor) {
41
+ throw new Error(`Workspace ${flags.workspace} does not have an executor installed`);
42
+ }
43
+ await delay(500);
44
+ },
45
+ title: 'Validating workspace',
46
+ },
47
+ {
48
+ rendererOptions: {
49
+ persistentOutput: isDebug(),
50
+ },
51
+ async task(_ctx, task) {
52
+ const executor$ = getExecutor();
53
+ if (!executor$.success) {
54
+ throw new Error(executor$.reason);
55
+ }
56
+ const { executor } = executor$;
57
+ const destroyOutput = await executor.destroy({
58
+ logger: getLogger(task),
59
+ package: DEFAULT_EXECUTOR_PACKAGE,
60
+ skipDeploy: true,
61
+ });
62
+ if (!destroyOutput.success) {
63
+ throw new Error(destroyOutput.reason);
64
+ }
65
+ },
66
+ title: 'Destroying remote executor infrastructure',
67
+ },
68
+ {
69
+ async task() {
70
+ const backend = await getBackend();
71
+ await backend.revokeExecutorToken({
72
+ workspace: flags.workspace,
73
+ });
74
+ await delay(500);
75
+ },
76
+ title: 'Revoking executor token',
77
+ },
78
+ {
79
+ async task() {
80
+ const backend = await getBackend();
81
+ await backend.updateWorkspace({
82
+ hasExecutor: false,
83
+ name: flags.workspace,
84
+ });
85
+ await delay(500);
86
+ },
87
+ title: 'Removing executor from workspace',
88
+ },
89
+ ], { concurrent: false, rendererOptions: { collapseSubtasks: !isDebug() } });
90
+ },
91
+ title: `Uninstalling executor from workspace ${flags.workspace}`,
92
+ },
93
+ ], { concurrent: false });
94
+ try {
95
+ await task.run();
96
+ myLogger.log(ListrLogLevels.COMPLETED, 'Executor uninstalled successfully');
97
+ }
98
+ catch (error) {
99
+ this.error(`${error.message}\n\nSee ${getLogPath()} for more details`);
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,2 @@
1
+ import { GetExecutorOutput } from './index.js';
2
+ export declare function getExecutorForWorkspace(workspaceName?: string, project?: string): Promise<GetExecutorOutput>;
@@ -0,0 +1,39 @@
1
+ import { getCloudCredentials, loadBackendConfig } from '../backend/config.js';
2
+ import { BackendType, getBackend } from '../backend/index.js';
3
+ import { resolveWorkspaceName } from '../lib/workspace-utils.js';
4
+ import { getExecutor } from './index.js';
5
+ export async function getExecutorForWorkspace(workspaceName, project) {
6
+ if (!workspaceName) {
7
+ return getExecutor();
8
+ }
9
+ const backendConfig = await loadBackendConfig();
10
+ if (backendConfig.current !== BackendType.Cloud) {
11
+ return getExecutor();
12
+ }
13
+ if (!backendConfig.cloud) {
14
+ return getExecutor();
15
+ }
16
+ const resolvedName = resolveWorkspaceName(workspaceName, project);
17
+ const backend = await getBackend();
18
+ const workspace$ = await backend.getWorkspace(resolvedName);
19
+ if (!workspace$.found || workspace$.hasError) {
20
+ return getExecutor();
21
+ }
22
+ const { workspace } = workspace$;
23
+ if (!workspace.hasExecutor) {
24
+ return getExecutor();
25
+ }
26
+ const credentials = await getCloudCredentials(backendConfig.cloud.clientId);
27
+ if (!credentials) {
28
+ return getExecutor();
29
+ }
30
+ return getExecutor({
31
+ cloudConfig: {
32
+ accessToken: credentials.accessToken,
33
+ clientId: backendConfig.cloud.clientId,
34
+ refreshToken: credentials.refreshToken,
35
+ url: backendConfig.cloud.url,
36
+ },
37
+ workspace: resolvedName,
38
+ });
39
+ }
@@ -0,0 +1,15 @@
1
+ import { Executor, ExecutorDestroyInput, ExecutorDestroyOutput, ExecutorImportInput, ExecutorImportOutput, ExecutorProvisionInput, ExecutorProvisionOutput, ExecutorResolveEnvValuesInput, ExecutorResolveEnvValuesOutput, ExecutorSetEnvVarInput, ExecutorSetEnvVarOutput, ExecutorUnsetEnvVarInput, ExecutorUnsetEnvVarOutput } from './interface.js';
2
+ import { LocalExecutor } from './local.js';
3
+ import { RemoteExecutor } from './remote.js';
4
+ export declare class DelegatingExecutor implements Executor {
5
+ private readonly localExecutor;
6
+ private readonly remoteExecutor;
7
+ constructor(localExecutor: LocalExecutor, remoteExecutor: RemoteExecutor);
8
+ destroy(input: ExecutorDestroyInput): Promise<ExecutorDestroyOutput>;
9
+ import(input: ExecutorImportInput): Promise<ExecutorImportOutput>;
10
+ provision(input: ExecutorProvisionInput): Promise<ExecutorProvisionOutput>;
11
+ resolveEnvValues(input: ExecutorResolveEnvValuesInput): Promise<ExecutorResolveEnvValuesOutput>;
12
+ setEnvVar(input: ExecutorSetEnvVarInput): Promise<ExecutorSetEnvVarOutput>;
13
+ unsetEnvVar(input: ExecutorUnsetEnvVarInput): Promise<ExecutorUnsetEnvVarOutput>;
14
+ private selectExecutor;
15
+ }
@@ -0,0 +1,50 @@
1
+ import { InfrastructureType } from '../infrastructure/common.js';
2
+ import { resolvePackage } from '../lib/package/index.js';
3
+ export class DelegatingExecutor {
4
+ localExecutor;
5
+ remoteExecutor;
6
+ constructor(localExecutor, remoteExecutor) {
7
+ this.localExecutor = localExecutor;
8
+ this.remoteExecutor = remoteExecutor;
9
+ }
10
+ async destroy(input) {
11
+ const executor = await this.selectExecutor(input.package, input.projectRootDir, input.isDeploying);
12
+ return executor.destroy(input);
13
+ }
14
+ async import(input) {
15
+ return this.localExecutor.import(input);
16
+ }
17
+ async provision(input) {
18
+ const executor = await this.selectExecutor(input.package, input.projectRootDir, input.isDeploying);
19
+ return executor.provision(input);
20
+ }
21
+ async resolveEnvValues(input) {
22
+ return this.remoteExecutor.resolveEnvValues(input);
23
+ }
24
+ async setEnvVar(input) {
25
+ return this.localExecutor.setEnvVar(input);
26
+ }
27
+ async unsetEnvVar(input) {
28
+ return this.localExecutor.unsetEnvVar(input);
29
+ }
30
+ async selectExecutor(pkg, projectRootDir, isDeploying) {
31
+ // Deploy operations always run locally
32
+ if (isDeploying) {
33
+ return this.localExecutor;
34
+ }
35
+ // Resolve package to check infra type
36
+ try {
37
+ const resolved = await resolvePackage({
38
+ package: pkg,
39
+ projectRootDir,
40
+ });
41
+ if (resolved.found && resolved.metadata.infra !== InfrastructureType.local) {
42
+ return this.remoteExecutor;
43
+ }
44
+ }
45
+ catch {
46
+ // If we can't resolve, fall back to local
47
+ }
48
+ return this.localExecutor;
49
+ }
50
+ }
@@ -1,7 +1,16 @@
1
- import { Executor } from "./interface.js";
2
- import { LocalExecutor } from "./local.js";
1
+ import { Executor } from './interface.js';
2
+ import { LocalExecutor } from './local.js';
3
3
  export declare const localExecutor: LocalExecutor;
4
- export declare function getExecutor(): GetExecutorOutput;
4
+ export type GetExecutorInput = {
5
+ cloudConfig?: {
6
+ accessToken: string;
7
+ clientId: string;
8
+ refreshToken: string;
9
+ url: string;
10
+ };
11
+ workspace?: string;
12
+ };
13
+ export declare function getExecutor(input?: GetExecutorInput): GetExecutorOutput;
5
14
  export type GetExecutorOutput = {
6
15
  executor: Executor;
7
16
  success: true;
@@ -1,6 +1,17 @@
1
- import { LocalExecutor } from "./local.js";
1
+ import { CloudBackend } from '../backend/cloud/cloud-backend.js';
2
+ import { DelegatingExecutor } from './delegating.js';
3
+ import { LocalExecutor } from './local.js';
4
+ import { RemoteExecutor } from './remote.js';
2
5
  export const localExecutor = new LocalExecutor();
3
- export function getExecutor() {
6
+ export function getExecutor(input) {
7
+ if (input?.cloudConfig && input?.workspace) {
8
+ const cloudBackend = new CloudBackend(input.cloudConfig);
9
+ const remoteExecutor = new RemoteExecutor(cloudBackend, input.workspace);
10
+ return {
11
+ executor: new DelegatingExecutor(localExecutor, remoteExecutor),
12
+ success: true,
13
+ };
14
+ }
4
15
  return {
5
16
  executor: localExecutor,
6
17
  success: true,
@@ -0,0 +1,16 @@
1
+ import { CloudBackend } from '../backend/cloud/cloud-backend.js';
2
+ import { Executor, ExecutorDestroyInput, ExecutorDestroyOutput, ExecutorImportInput, ExecutorImportOutput, ExecutorProvisionInput, ExecutorProvisionOutput, ExecutorResolveEnvValuesInput, ExecutorResolveEnvValuesOutput, ExecutorSetEnvVarInput, ExecutorSetEnvVarOutput, ExecutorUnsetEnvVarInput, ExecutorUnsetEnvVarOutput } from './interface.js';
3
+ export declare class RemoteExecutor implements Executor {
4
+ private readonly cloudBackend;
5
+ private readonly workspace;
6
+ private localExecutor;
7
+ constructor(cloudBackend: CloudBackend, workspace: string);
8
+ destroy(input: ExecutorDestroyInput): Promise<ExecutorDestroyOutput>;
9
+ import(input: ExecutorImportInput): Promise<ExecutorImportOutput>;
10
+ provision(input: ExecutorProvisionInput): Promise<ExecutorProvisionOutput>;
11
+ resolveEnvValues(input: ExecutorResolveEnvValuesInput): Promise<ExecutorResolveEnvValuesOutput>;
12
+ setEnvVar(input: ExecutorSetEnvVarInput): Promise<ExecutorSetEnvVarOutput>;
13
+ unsetEnvVar(input: ExecutorUnsetEnvVarInput): Promise<ExecutorUnsetEnvVarOutput>;
14
+ private executeRemote;
15
+ private resolveEnvRemotely;
16
+ }
@@ -0,0 +1,168 @@
1
+ import { LocalExecutor } from './local.js';
2
+ export class RemoteExecutor {
3
+ cloudBackend;
4
+ workspace;
5
+ localExecutor = new LocalExecutor();
6
+ constructor(cloudBackend, workspace) {
7
+ this.cloudBackend = cloudBackend;
8
+ this.workspace = workspace;
9
+ }
10
+ async destroy(input) {
11
+ return this.executeRemote('destroy', input);
12
+ }
13
+ async import(input) {
14
+ return this.localExecutor.import(input);
15
+ }
16
+ async provision(input) {
17
+ return this.executeRemote('provision', input);
18
+ }
19
+ async resolveEnvValues(input) {
20
+ const localEntries = {};
21
+ const remoteEntries = {};
22
+ for (const [key, value] of Object.entries(input.env)) {
23
+ const colonIndex = value.indexOf(':');
24
+ if (colonIndex === -1) {
25
+ // No colon - plain value, resolve locally
26
+ localEntries[key] = value;
27
+ }
28
+ else {
29
+ const prefix = value.slice(0, colonIndex);
30
+ if (prefix === 'local') {
31
+ localEntries[key] = value;
32
+ }
33
+ else {
34
+ remoteEntries[key] = value;
35
+ }
36
+ }
37
+ }
38
+ // Resolve local entries via local executor
39
+ const localResolved = Object.keys(localEntries).length > 0
40
+ ? await this.localExecutor.resolveEnvValues({ env: localEntries, markSecret: input.markSecret })
41
+ : {};
42
+ // Resolve remote entries via cloud backend
43
+ const remoteResolved = Object.keys(remoteEntries).length > 0
44
+ ? await this.resolveEnvRemotely(remoteEntries, input.markSecret)
45
+ : {};
46
+ return { ...localResolved, ...remoteResolved };
47
+ }
48
+ async setEnvVar(input) {
49
+ return this.localExecutor.setEnvVar(input);
50
+ }
51
+ async unsetEnvVar(input) {
52
+ return this.localExecutor.unsetEnvVar(input);
53
+ }
54
+ async executeRemote(type, input) {
55
+ // Submit job to cloud
56
+ const submitResult = await this.cloudBackend.submitExecutorJob({
57
+ payload: {
58
+ isDeploying: input.isDeploying,
59
+ package: input.package,
60
+ parameters: input.parameters,
61
+ project: input.project,
62
+ projectEnv: input.projectEnv,
63
+ skipDeploy: input.skipDeploy,
64
+ workspace: input.workspace,
65
+ },
66
+ type,
67
+ workspace: this.workspace,
68
+ });
69
+ if (!submitResult.success) {
70
+ return { reason: `Failed to submit remote executor job: ${submitResult.reason}`, success: false };
71
+ }
72
+ const { jobId } = submitResult;
73
+ input.logger?.info?.(`Remote executor job submitted (${jobId}). Waiting for result...`);
74
+ // Poll for result
75
+ const timeout = 10 * 60 * 1000; // 10 minutes
76
+ const startTime = Date.now();
77
+ let lastLogLength = 0;
78
+ let lastStatus = 'pending';
79
+ while (Date.now() - startTime < timeout) {
80
+ // eslint-disable-next-line no-await-in-loop
81
+ const statusResult = await this.cloudBackend.getExecutorJobStatus({
82
+ jobId,
83
+ lastStatus,
84
+ poll: true,
85
+ workspace: this.workspace,
86
+ });
87
+ if (!statusResult.success) {
88
+ return { reason: `Failed to get job status: ${statusResult.reason}`, success: false };
89
+ }
90
+ const { job } = statusResult;
91
+ lastStatus = job.status;
92
+ // Forward new logs
93
+ if (job.logs && job.logs.length > lastLogLength) {
94
+ const newLogs = job.logs.slice(lastLogLength);
95
+ if (newLogs.trim()) {
96
+ input.logger?.info?.(newLogs);
97
+ }
98
+ lastLogLength = job.logs.length;
99
+ }
100
+ // Check if job is done
101
+ if (job.status === 'completed') {
102
+ if (job.result) {
103
+ return job.result;
104
+ }
105
+ return { reason: 'Job completed but no result returned', success: false };
106
+ }
107
+ if (job.status === 'failed') {
108
+ const reason = job.result?.reason || 'Remote executor job failed';
109
+ return { reason, success: false };
110
+ }
111
+ // Small delay between polls (server-side long polling handles most of the wait)
112
+ // eslint-disable-next-line no-await-in-loop
113
+ await new Promise(resolve => {
114
+ setTimeout(resolve, 1000);
115
+ });
116
+ }
117
+ return { reason: 'Remote executor job timed out after 10 minutes', success: false };
118
+ }
119
+ async resolveEnvRemotely(env, markSecret) {
120
+ const submitResult = await this.cloudBackend.submitExecutorJob({
121
+ payload: { env, markSecret },
122
+ type: 'resolve-env',
123
+ workspace: this.workspace,
124
+ });
125
+ if (!submitResult.success) {
126
+ console.warn(`Failed to submit resolve-env job: ${submitResult.reason}. Returning unresolved values.`);
127
+ return env;
128
+ }
129
+ const { jobId } = submitResult;
130
+ // Poll for result with 2-minute timeout
131
+ const timeout = 2 * 60 * 1000;
132
+ const startTime = Date.now();
133
+ let lastStatus = 'pending';
134
+ while (Date.now() - startTime < timeout) {
135
+ // eslint-disable-next-line no-await-in-loop
136
+ const statusResult = await this.cloudBackend.getExecutorJobStatus({
137
+ jobId,
138
+ lastStatus,
139
+ poll: true,
140
+ workspace: this.workspace,
141
+ });
142
+ if (!statusResult.success) {
143
+ console.warn(`Failed to get resolve-env job status: ${statusResult.reason}. Returning unresolved values.`);
144
+ return env;
145
+ }
146
+ const { job } = statusResult;
147
+ lastStatus = job.status;
148
+ if (job.status === 'completed') {
149
+ if (job.result?.env) {
150
+ return job.result.env;
151
+ }
152
+ console.warn('resolve-env job completed but no env in result. Returning unresolved values.');
153
+ return env;
154
+ }
155
+ if (job.status === 'failed') {
156
+ const reason = job.result?.reason || 'resolve-env job failed';
157
+ console.warn(`resolve-env job failed: ${reason}. Returning unresolved values.`);
158
+ return env;
159
+ }
160
+ // eslint-disable-next-line no-await-in-loop
161
+ await new Promise(resolve => {
162
+ setTimeout(resolve, 1000);
163
+ });
164
+ }
165
+ console.warn('resolve-env job timed out after 2 minutes. Returning unresolved values.');
166
+ return env;
167
+ }
168
+ }