@copado/agentia-cli 1.0.0-beta.2 → 1.0.0-beta.5

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 (55) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +416 -293
  3. package/dist/commands/cicd/data/template/save-detail.js +2 -2
  4. package/dist/commands/cicd/repository/auth/https/configure.js +21 -0
  5. package/dist/commands/cicd/repository/auth/oauth/login.js +56 -0
  6. package/dist/commands/cicd/repository/auth/setup.js +28 -0
  7. package/dist/commands/cicd/repository/auth/ssh/configure.js +22 -0
  8. package/dist/commands/cicd/repository/create.js +2 -2
  9. package/dist/commands/cicd/repository/reset.js +17 -5
  10. package/dist/commands/cicd/repository/update.js +6 -14
  11. package/dist/commands/cicd/work/environment-sync.js +66 -61
  12. package/dist/commands/cicd/work/get.js +5 -9
  13. package/dist/commands/cicd/work/submit.js +1 -1
  14. package/dist/commands/cicd/work/test/apex.js +37 -4
  15. package/dist/commands/cicd/work/test.js +20 -1
  16. package/dist/commands/runtime.js +26 -0
  17. package/dist/hooks/finally/ide-command-event.js +1 -1
  18. package/dist/hooks/init/plugin-host.js +6 -0
  19. package/dist/lib/agentia-command.js +16 -0
  20. package/dist/lib/cicd-gateway.js +2 -1
  21. package/dist/lib/cursor-mcp-config.js +3 -1
  22. package/dist/lib/data-types.js +3 -1
  23. package/dist/lib/git-worktree.js +2 -1
  24. package/dist/lib/local-quality-gates.js +4 -1
  25. package/dist/lib/mcp/data-tools.js +2 -2
  26. package/dist/lib/mcp/server.js +33 -7
  27. package/dist/lib/mcp/tool-annotations.js +3 -0
  28. package/dist/lib/mcp/tools.js +55 -12
  29. package/dist/lib/mcp/work-tools.js +44 -13
  30. package/dist/lib/plugin-host/auth.js +4 -0
  31. package/dist/lib/plugin-host/event-log.js +3 -0
  32. package/dist/lib/plugin-host/format.js +3 -0
  33. package/dist/lib/plugin-host/index.js +7 -0
  34. package/dist/lib/plugin-host/organization.js +4 -0
  35. package/dist/lib/plugin-host/projection-utils.js +6 -0
  36. package/dist/lib/plugin-host/registry.js +6 -0
  37. package/dist/lib/plugin-host/request-scope.js +4 -0
  38. package/dist/lib/plugin-host/work.js +4 -0
  39. package/dist/lib/project-root.js +3 -1
  40. package/dist/lib/repository-auth.js +8 -0
  41. package/dist/lib/repository-flags.js +2 -2
  42. package/dist/lib/repository-types.js +6 -1
  43. package/dist/lib/repository-url-prefill.js +4 -0
  44. package/dist/lib/runtime-invoke-context.js +6 -0
  45. package/dist/lib/runtime-rpc.js +14 -0
  46. package/dist/lib/runtime.js +1 -1
  47. package/dist/lib/setup-templates.js +2 -1
  48. package/dist/lib/work-apex-test-failures.js +13 -0
  49. package/dist/lib/work-apex-test.js +4 -1
  50. package/dist/lib/work-environment-sync.js +1 -2
  51. package/dist/lib/work-get.js +3 -0
  52. package/oclif.manifest.json +315 -174
  53. package/package.json +4 -3
  54. package/templates/skills/agentia-cicd/SKILL.md +1 -1
  55. package/templates/skills/agentia-cicd/references/work.md +2 -2
@@ -2,7 +2,7 @@ import { Args } from '@oclif/core';
2
2
  import { AgentiaCommand } from '../../../../lib/agentia-command.js';
3
3
  import { CicdGateway } from '../../../../lib/cicd-gateway.js';
4
4
  import { assertJsonObject, jsonBodyFlags, readJsonBodyFromFileOrStdin, } from '../../../../lib/data-flags.js';
5
- import { dataTemplateDetailEndpoint } from '../../../../lib/data-types.js';
5
+ import { dataTemplateDetailEndpoint, normalizeDataTemplateSavePayload, } from '../../../../lib/data-types.js';
6
6
  export default class DataTemplateSaveDetail extends AgentiaCommand {
7
7
  static args = {
8
8
  id: Args.string({
@@ -26,7 +26,7 @@ export default class DataTemplateSaveDetail extends AgentiaCommand {
26
26
  required: true,
27
27
  stdin: flags.stdin,
28
28
  });
29
- const body = assertJsonObject(raw, 'template detail');
29
+ const body = normalizeDataTemplateSavePayload(assertJsonObject(raw, 'template detail'));
30
30
  const result = await this.withSpinner('Saving data template detail', () => CicdGateway.call({
31
31
  body,
32
32
  endpoint: dataTemplateDetailEndpoint(args.id),
@@ -0,0 +1,21 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { AgentiaCommand } from '../../../../../lib/agentia-command.js';
3
+ import { configureHttps, promptHttpsPayload, readHttpsAuthPayloadFromStdin } from '../../../../../lib/repository-auth.js';
4
+ export default class RepositoryAuthHttpsConfigure extends AgentiaCommand {
5
+ static args = { id: Args.string({ description: 'Copado Git repository ID', required: true }) };
6
+ static description = 'Configure HTTPS authentication. Use a personal access token as the password when required by the provider.';
7
+ static flags = {
8
+ 'input-stdin': Flags.boolean({ description: 'Read the complete HTTPS authentication JSON payload from stdin' }),
9
+ overwrite: Flags.boolean({ description: 'Replace an existing validated HTTPS credential' }),
10
+ };
11
+ async run() {
12
+ const { args, flags } = await this.parse(RepositoryAuthHttpsConfigure);
13
+ if (this.jsonEnabled() && !flags['input-stdin'])
14
+ this.error('--json requires --input-stdin so secrets cannot be prompted.', { exit: 2 });
15
+ const payload = flags['input-stdin'] ? await readHttpsAuthPayloadFromStdin() : await promptHttpsPayload(args.id);
16
+ await this.withSpinner('Configuring HTTPS authentication', () => configureHttps(args.id, payload, flags.overwrite, !flags['input-stdin']));
17
+ this.display(() => this.log(`Configured HTTPS authentication for repository ${args.id}.`));
18
+ return { id: args.id };
19
+ }
20
+ }
21
+ //# sourceMappingURL=configure.js.map
@@ -0,0 +1,56 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { AgentiaCommand } from '../../../../../lib/agentia-command.js';
4
+ import { CicdGateway } from '../../../../../lib/cicd-gateway.js';
5
+ import { createLoopbackCallbackServer } from '../../../../../lib/loopback-callback-server.js';
6
+ import { isHeadlessEnvironment, openUrl } from '../../../../../lib/open-browser.js';
7
+ import { repositoryOAuthEndpoint, repositoryOAuthInitEndpoint, repositoryOAuthRepositoriesEndpoint, } from '../../../../../lib/repository-types.js';
8
+ export default class RepositoryAuthOAuthLogin extends AgentiaCommand {
9
+ static args = { id: Args.string({ description: 'Copado Git repository ID', required: true }) };
10
+ static description = 'Authenticate a repository through browser OAuth and select a remote repository';
11
+ static flags = {
12
+ port: Flags.integer({ description: 'Local callback port (default: scan 8080-8090)' }),
13
+ timeout: Flags.integer({ default: 300, description: 'Seconds to wait for browser authentication' }),
14
+ };
15
+ async run() {
16
+ const { args, flags } = await this.parse(RepositoryAuthOAuthLogin);
17
+ if (this.jsonEnabled())
18
+ this.error('OAuth login requires an interactive terminal to select a repository.', { exit: 2 });
19
+ const callback = await createLoopbackCallbackServer({ port: flags.port });
20
+ if (!callback)
21
+ this.error('Could not bind a local OAuth callback port.', { exit: 2 });
22
+ try {
23
+ const init = await this.withSpinner('Generating OAuth authorization URL', () => CicdGateway.call({
24
+ endpoint: repositoryOAuthInitEndpoint(args.id, callback.callbackUrl),
25
+ method: 'POST',
26
+ }));
27
+ if (isHeadlessEnvironment()) {
28
+ this.log(`Complete authentication in your browser by opening this URL:\n${init.authUrl}`);
29
+ }
30
+ else {
31
+ this.log('Complete authentication in the browser window that opens.');
32
+ await openUrl(init.authUrl);
33
+ }
34
+ const result = await callback.waitForCallback({ timeoutMs: flags.timeout * 1000 });
35
+ if (!result.ok)
36
+ this.error(result.errorDescription ?? result.errorCode ?? 'OAuth authentication failed.', { exit: 1 });
37
+ const available = await CicdGateway.call({ endpoint: repositoryOAuthRepositoriesEndpoint(args.id), method: 'GET' });
38
+ const remoteRepositoryId = await select({
39
+ choices: available.repositories.map((repository) => ({ name: repository.name, value: repository.id })),
40
+ message: `Select ${available.provider} repository`,
41
+ });
42
+ await this.withSpinner('Saving selected repository', () => CicdGateway.call({
43
+ body: { remoteRepositoryId },
44
+ endpoint: repositoryOAuthEndpoint(args.id),
45
+ headers: { 'Content-Type': 'application/json' },
46
+ method: 'POST',
47
+ }));
48
+ this.log(`Configured OAuth authentication for repository ${args.id}.`);
49
+ return { id: args.id, remoteRepositoryId };
50
+ }
51
+ finally {
52
+ await callback.close().catch(() => { });
53
+ }
54
+ }
55
+ }
56
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1,28 @@
1
+ import { select } from '@inquirer/prompts';
2
+ import { Args, Flags } from '@oclif/core';
3
+ import { AgentiaCommand } from '../../../../lib/agentia-command.js';
4
+ export default class RepositoryAuthSetup extends AgentiaCommand {
5
+ static args = { id: Args.string({ description: 'Copado Git repository ID', required: true }) };
6
+ static description = 'Interactively configure repository authentication; OAuth is selected by default';
7
+ static flags = { overwrite: Flags.boolean({ description: 'Replace an existing HTTPS or SSH credential' }) };
8
+ async run() {
9
+ const { args, flags } = await this.parse(RepositoryAuthSetup);
10
+ if (this.jsonEnabled())
11
+ this.error('Repository auth setup requires an interactive terminal.', { exit: 2 });
12
+ const authType = await select({
13
+ choices: [
14
+ { name: 'OAuth (recommended)', value: 'oauth' },
15
+ { name: 'HTTPS', value: 'https' },
16
+ { name: 'SSH', value: 'ssh' },
17
+ ],
18
+ default: 'oauth',
19
+ message: 'Authentication type',
20
+ });
21
+ const command = authType === 'oauth'
22
+ ? 'cicd:repository:auth:oauth:login'
23
+ : `cicd:repository:auth:${authType}:configure`;
24
+ const argv = [args.id, ...(flags.overwrite && authType !== 'oauth' ? ['--overwrite'] : [])];
25
+ await this.config.runCommand(command, argv);
26
+ }
27
+ }
28
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1,22 @@
1
+ import { Args, Flags } from '@oclif/core';
2
+ import { AgentiaCommand } from '../../../../../lib/agentia-command.js';
3
+ import { configureSsh, promptConnection } from '../../../../../lib/repository-auth.js';
4
+ export default class RepositoryAuthSshConfigure extends AgentiaCommand {
5
+ static args = { id: Args.string({ description: 'Copado Git repository ID', required: true }) };
6
+ static description = 'Generate SSH authentication and a public key. Add the key to your Git provider as a deploy, access, or SSH key.';
7
+ static flags = {
8
+ overwrite: Flags.boolean({ description: 'Replace an existing SSH key after confirmation' }),
9
+ };
10
+ async run() {
11
+ const { args, flags } = await this.parse(RepositoryAuthSshConfigure);
12
+ if (this.jsonEnabled())
13
+ this.error('SSH authentication requires an interactive terminal to collect connection details.', { exit: 2 });
14
+ const payload = await promptConnection(args.id, 'SSH');
15
+ const publicKey = await this.withSpinner('Generating SSH key', () => configureSsh(args.id, payload, flags.overwrite));
16
+ this.display(() => {
17
+ this.log(`Add this public key to your Git provider's repository deploy/access/SSH keys, then retain it for this repository:\n${publicKey}`);
18
+ });
19
+ return { id: args.id, publicKey };
20
+ }
21
+ }
22
+ //# sourceMappingURL=configure.js.map
@@ -10,9 +10,9 @@ export default class RepositoryCreate extends AgentiaCommand {
10
10
  required: true,
11
11
  }),
12
12
  };
13
- static description = 'Create a Copado Git repository';
13
+ static description = 'Create a Copado Git repository shell';
14
14
  static examples = [
15
- '<%= config.bin %> <%= command.id %> my-repo --source-format DX --provider Github --uri https://github.com/org/repo.git',
15
+ '<%= config.bin %> <%= command.id %> my-repo --source-format DX --provider Github',
16
16
  '<%= config.bin %> <%= command.id %> my-repo --source-format DX --provider GitLab --json',
17
17
  ];
18
18
  static flags = repositoryCreateFlags;
@@ -1,4 +1,5 @@
1
- import { Args } from '@oclif/core';
1
+ import { confirm } from '@inquirer/prompts';
2
+ import { Args, Flags } from '@oclif/core';
2
3
  import { AgentiaCommand } from '../../../lib/agentia-command.js';
3
4
  import { CicdGateway } from '../../../lib/cicd-gateway.js';
4
5
  import { repositoryResetEndpoint } from '../../../lib/repository-types.js';
@@ -9,13 +10,24 @@ export default class RepositoryReset extends AgentiaCommand {
9
10
  required: true,
10
11
  }),
11
12
  };
12
- static description = 'Reset a Copado Git repository';
13
+ static description = 'Reset a Copado Git repository and its authentication configuration';
13
14
  static examples = [
14
- '<%= config.bin %> <%= command.id %> repo-001',
15
- '<%= config.bin %> <%= command.id %> repo-001 --json',
15
+ '<%= config.bin %> <%= command.id %> repo-001 --yes',
16
+ '<%= config.bin %> <%= command.id %> repo-001 --yes --json',
16
17
  ];
18
+ static flags = {
19
+ yes: Flags.boolean({ char: 'y', default: false, description: 'Confirm reset without prompting' }),
20
+ };
17
21
  async run() {
18
- const { args } = await this.parse(RepositoryReset);
22
+ const { args, flags } = await this.parse(RepositoryReset);
23
+ if (!flags.yes) {
24
+ if (this.jsonEnabled() || process.stdin.isTTY !== true || process.stdout.isTTY !== true) {
25
+ this.error('Repository reset requires confirmation. Pass --yes for headless or JSON execution.', { exit: 2 });
26
+ }
27
+ if (!(await confirm({ default: false, message: `Reset repository ${args.id} and remove its authentication configuration?` }))) {
28
+ this.error('Repository reset canceled.', { exit: 1 });
29
+ }
30
+ }
19
31
  await this.withSpinner('Resetting repository', () => CicdGateway.call({
20
32
  endpoint: repositoryResetEndpoint(args.id),
21
33
  method: 'PUT',
@@ -2,7 +2,7 @@ import { Args } from '@oclif/core';
2
2
  import { AgentiaCommand } from '../../../lib/agentia-command.js';
3
3
  import { CicdGateway } from '../../../lib/cicd-gateway.js';
4
4
  import { buildRepositoryMutationPayload, collectRepositoryMutationFlagKeys, REPOSITORY_STRING_VALIDATION_KEYS, repositoryUpdateFlags, validateNonEmptyStringFields, validateRepositoryMutationFlags, } from '../../../lib/repository-flags.js';
5
- import { normalizeRepository, repositoryEndpoint, withRequiredRepositoryFields, } from '../../../lib/repository-types.js';
5
+ import { normalizeRepository, repositoryEndpoint } from '../../../lib/repository-types.js';
6
6
  export default class RepositoryUpdate extends AgentiaCommand {
7
7
  static args = {
8
8
  id: Args.string({
@@ -13,7 +13,7 @@ export default class RepositoryUpdate extends AgentiaCommand {
13
13
  static description = 'Update a Copado Git repository by ID';
14
14
  static examples = [
15
15
  '<%= config.bin %> <%= command.id %> repo-001 --uri https://github.com/org/repo.git',
16
- '<%= config.bin %> <%= command.id %> repo-001 --clear-password',
16
+ '<%= config.bin %> <%= command.id %> repo-001 --clear-uri',
17
17
  '<%= config.bin %> <%= command.id %> repo-001 --provider GitLab --json',
18
18
  ];
19
19
  static flags = repositoryUpdateFlags;
@@ -25,7 +25,7 @@ export default class RepositoryUpdate extends AgentiaCommand {
25
25
  exit: 2,
26
26
  suggestions: [
27
27
  `${this.config.bin} cicd repository update ${args.id} --uri <uri>`,
28
- `${this.config.bin} cicd repository update ${args.id} --clear-password`,
28
+ `${this.config.bin} cicd repository update ${args.id} --clear-uri`,
29
29
  ],
30
30
  });
31
31
  }
@@ -33,18 +33,10 @@ export default class RepositoryUpdate extends AgentiaCommand {
33
33
  if (flagError !== undefined) {
34
34
  this.error(flagError, { exit: 2 });
35
35
  }
36
- const currentRepository = await this.withSpinner('Fetching repository', () => CicdGateway.call({
37
- endpoint: repositoryEndpoint(args.id),
38
- method: 'GET',
39
- }));
40
- const current = normalizeRepository(currentRepository);
41
- const payload = withRequiredRepositoryFields(buildRepositoryMutationPayload(flags), current);
36
+ const payload = buildRepositoryMutationPayload(flags);
42
37
  const validationError = validateNonEmptyStringFields(payload, REPOSITORY_STRING_VALIDATION_KEYS);
43
- if (payload.name === undefined ||
44
- payload.sourceFormat === undefined ||
45
- payload.provider === undefined ||
46
- validationError !== undefined) {
47
- this.error('Repository name, source format, and provider are required to update a repository.', { exit: 2 });
38
+ if (validationError !== undefined) {
39
+ this.error(validationError, { exit: 2 });
48
40
  }
49
41
  const response = await this.withSpinner('Updating repository', () => CicdGateway.call({
50
42
  body: payload,
@@ -1,5 +1,6 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { AgentiaCommand } from '../../../lib/agentia-command.js';
3
+ import { commandStdioCaptureOptions, withCapturedCommandOutput } from '../../../lib/command-output-buffer.js';
3
4
  import { assertEnvironmentSyncDestOrgSelector, formatWorkEnvironmentSyncResult, performWorkEnvironmentSync, WorkEnvironmentSyncConflictError, WorkEnvironmentSyncValidationError, } from '../../../lib/work-environment-sync.js';
4
5
  export default class WorkEnvironmentSync extends AgentiaCommand {
5
6
  static description = [
@@ -28,7 +29,7 @@ export default class WorkEnvironmentSync extends AgentiaCommand {
28
29
  }),
29
30
  continue: Flags.boolean({
30
31
  default: false,
31
- description: 'Commit resolved merge conflicts and finish the remaining environment-sync git steps.',
32
+ description: 'Finish after merge conflicts. If the merge is still in progress (conflicts fixed and staged, but not committed), commits it; if you already committed, continues from there through the remaining git steps.',
32
33
  exclusive: ['abort'],
33
34
  }),
34
35
  'destination-credential': Flags.string({
@@ -51,70 +52,74 @@ export default class WorkEnvironmentSync extends AgentiaCommand {
51
52
  };
52
53
  async run() {
53
54
  const { flags } = await this.parse(WorkEnvironmentSync);
54
- try {
55
- await assertEnvironmentSyncDestOrgSelector({
56
- abort: flags.abort === true,
57
- continue: flags.continue === true,
58
- destinationEnvironment: flags['destination-environment'],
59
- devOrgBranch: flags['dev-org-branch'],
60
- promotion: flags.promotion,
61
- });
62
- const result = await performWorkEnvironmentSync({
63
- abort: flags.abort === true,
64
- cliBin: this.config.bin,
65
- continue: flags.continue === true,
66
- hooks: {
67
- log: (message) => {
68
- this.display(() => {
69
- this.log(message);
70
- });
55
+ return withCapturedCommandOutput(async () => {
56
+ const startedAt = Date.now();
57
+ try {
58
+ await assertEnvironmentSyncDestOrgSelector({
59
+ abort: flags.abort === true,
60
+ continue: flags.continue === true,
61
+ destinationEnvironment: flags['destination-environment'],
62
+ devOrgBranch: flags['dev-org-branch'],
63
+ promotion: flags.promotion,
64
+ });
65
+ const result = await performWorkEnvironmentSync({
66
+ abort: flags.abort === true,
67
+ cliBin: this.config.bin,
68
+ continue: flags.continue === true,
69
+ hooks: {
70
+ log: (message) => {
71
+ this.display(() => {
72
+ this.log(message);
73
+ });
74
+ },
75
+ withSpinner: (message, action) => this.withSpinner(message, action),
71
76
  },
72
- withSpinner: (message, action) => this.withSpinner(message, action),
73
- },
74
- json: this.jsonEnabled(),
75
- ...(flags['destination-credential'] === undefined
76
- ? {}
77
- : { destinationCredentialId: flags['destination-credential'] }),
78
- ...(flags['destination-environment'] === undefined
79
- ? {}
80
- : { destinationEnvironment: flags['destination-environment'] }),
81
- ...(flags['dev-org-branch'] === undefined ? {} : { devOrgBranch: flags['dev-org-branch'] }),
82
- ...(flags['pipeline-id'] === undefined ? {} : { pipelineId: flags['pipeline-id'] }),
83
- ...(flags.promotion === undefined ? {} : { promotion: flags.promotion }),
84
- });
85
- this.display(() => {
86
- const formatted = formatWorkEnvironmentSyncResult(result);
87
- if (formatted.length > 0) {
88
- this.log(formatted);
89
- }
90
- });
91
- return result;
92
- }
93
- catch (error) {
94
- if (error instanceof WorkEnvironmentSyncConflictError) {
95
- const { suggestions } = error;
96
- const suggestionOptions = suggestions === undefined ? {} : { suggestions };
97
- if (this.jsonEnabled()) {
98
- const message = error.gitStatus === undefined || error.gitStatus.length === 0
99
- ? error.message
100
- : `${error.message}\n\n${error.gitStatus}`;
101
- this.error(message, { exit: error.exit, ...suggestionOptions });
77
+ json: this.jsonEnabled(),
78
+ ...(flags['destination-credential'] === undefined
79
+ ? {}
80
+ : { destinationCredentialId: flags['destination-credential'] }),
81
+ ...(flags['destination-environment'] === undefined
82
+ ? {}
83
+ : { destinationEnvironment: flags['destination-environment'] }),
84
+ ...(flags['dev-org-branch'] === undefined ? {} : { devOrgBranch: flags['dev-org-branch'] }),
85
+ ...(flags['pipeline-id'] === undefined ? {} : { pipelineId: flags['pipeline-id'] }),
86
+ ...(flags.promotion === undefined ? {} : { promotion: flags.promotion }),
87
+ });
88
+ this.display(() => {
89
+ const formatted = formatWorkEnvironmentSyncResult(result);
90
+ if (formatted.length > 0) {
91
+ this.log(formatted);
92
+ }
93
+ });
94
+ this.logElapsed(startedAt);
95
+ return result;
96
+ }
97
+ catch (error) {
98
+ if (error instanceof WorkEnvironmentSyncConflictError) {
99
+ const { suggestions } = error;
100
+ const suggestionOptions = suggestions === undefined ? {} : { suggestions };
101
+ if (this.jsonEnabled()) {
102
+ const message = error.gitStatus === undefined || error.gitStatus.length === 0
103
+ ? error.message
104
+ : `${error.message}\n\n${error.gitStatus}`;
105
+ this.error(message, { exit: error.exit, ...suggestionOptions });
106
+ }
107
+ this.error(error.message, { exit: false, ...suggestionOptions });
108
+ if (error.gitStatus !== undefined && error.gitStatus.length > 0) {
109
+ this.log('');
110
+ this.log(error.gitStatus);
111
+ }
112
+ this.exit(error.exit);
102
113
  }
103
- this.error(error.message, { exit: false, ...suggestionOptions });
104
- if (error.gitStatus !== undefined && error.gitStatus.length > 0) {
105
- this.log('');
106
- this.log(error.gitStatus);
114
+ if (error instanceof WorkEnvironmentSyncValidationError) {
115
+ this.error(error.message, {
116
+ exit: error.exit,
117
+ ...(error.suggestions === undefined ? {} : { suggestions: error.suggestions }),
118
+ });
107
119
  }
108
- this.exit(error.exit);
109
- }
110
- if (error instanceof WorkEnvironmentSyncValidationError) {
111
- this.error(error.message, {
112
- exit: error.exit,
113
- ...(error.suggestions === undefined ? {} : { suggestions: error.suggestions }),
114
- });
120
+ throw error;
115
121
  }
116
- throw error;
117
- }
122
+ }, commandStdioCaptureOptions(this));
118
123
  }
119
124
  }
120
125
  //# sourceMappingURL=environment-sync.js.map
@@ -1,9 +1,9 @@
1
1
  import { Args, Flags } from '@oclif/core';
2
2
  import { AgentiaCommand } from '../../../lib/agentia-command.js';
3
- import { CicdGateway } from '../../../lib/cicd-gateway.js';
4
- import { resolveWorkItemId, WorkActiveResolutionError } from '../../../lib/work-active.js';
3
+ import { WorkActiveResolutionError } from '../../../lib/work-active.js';
4
+ import { fetchUserStory } from '../../../lib/work-get.js';
5
5
  import { formatUserStoriesAheadBehindLines, reportUserStoriesAheadBehindForStory, } from '../../../lib/work-submit-ahead-behind.js';
6
- import { formatUserStoryDetail, workEndpoint, } from '../../../lib/work-types.js';
6
+ import { formatUserStoryDetail, } from '../../../lib/work-types.js';
7
7
  export default class WorkGet extends AgentiaCommand {
8
8
  static args = {
9
9
  id: Args.string({
@@ -27,9 +27,9 @@ export default class WorkGet extends AgentiaCommand {
27
27
  };
28
28
  async run() {
29
29
  const { args, flags } = await this.parse(WorkGet);
30
- let workItemId;
30
+ let userStory;
31
31
  try {
32
- workItemId = await resolveWorkItemId(args.id);
32
+ userStory = await this.withSpinner('Fetching user story', () => fetchUserStory(args.id));
33
33
  }
34
34
  catch (error) {
35
35
  if (error instanceof WorkActiveResolutionError) {
@@ -40,10 +40,6 @@ export default class WorkGet extends AgentiaCommand {
40
40
  }
41
41
  throw error;
42
42
  }
43
- const userStory = await this.withSpinner('Fetching user story', () => CicdGateway.call({
44
- endpoint: workEndpoint(workItemId),
45
- method: 'GET',
46
- }));
47
43
  let userStoriesAheadBehind;
48
44
  if (flags['user-stories-ahead-behind']) {
49
45
  userStoriesAheadBehind = await reportUserStoriesAheadBehindForStory(userStory, {
@@ -20,7 +20,7 @@ export default class WorkSubmit extends AgentiaCommand {
20
20
  'pushes and registers commits when the branch is ahead of origin, then calls headless work/submit',
21
21
  '(Promote Action with validate=true).',
22
22
  'When the last pending related job template name contains Commit, waits up to 5 minutes for that job before submitting.',
23
- 'Before submit or promote, counts user stories ahead and behind between the source environment and the next pipeline environment, and warns when stories behind might cause promotion conflicts.',
23
+ 'Before submit or promote, counts user stories ahead and behind between the source environment and the next pipeline environment, and warns when stories behind might cause promotion conflicts (advising `agentia cicd work environment-sync`).',
24
24
  'With --done or --deploy, matches Copado UI Submit when promoting (validate=false / headless work/done).',
25
25
  'cicd work done is a compatibility alias of this command with --done.',
26
26
  ].join(' ');
@@ -1,22 +1,32 @@
1
1
  import { Flags } from '@oclif/core';
2
2
  import { AgentiaCommand } from '../../../../lib/agentia-command.js';
3
- import { DEFAULT_APEX_TEST_WAIT_MINUTES, formatWorkApexTestHuman, parseApexTestClassNames, runWorkApexTests, validateApexTestWaitMinutes, WorkApexTestError, } from '../../../../lib/work-apex-test.js';
3
+ import { planWorkApexTestRun, recordWorkApexTestOutcome, WorkApexRerunError, } from '../../../../lib/work-apex-test-failures.js';
4
+ import { DEFAULT_APEX_TEST_WAIT_MINUTES, formatWorkApexTestHuman, parseApexTestClassNames, runWorkApexTests, skippedWorkApexTestResult, validateApexTestWaitMinutes, WorkApexTestError, } from '../../../../lib/work-apex-test.js';
4
5
  export default class WorkTestApex extends AgentiaCommand {
5
6
  static description = [
6
7
  'Run specified Apex test classes via Salesforce CLI (`sf apex run test`) with code coverage.',
7
8
  'Usually run from project-root local quality gates (`.agentia_quality_gates.sh` / `.agentia_quality_gates.cmd`) when those scripts are configured.',
9
+ 'When `--apex-test-classes` is omitted or empty, logs that no Apex tests were specified and exits without running Salesforce CLI.',
10
+ 'Failed runs save `.agentia/apex.tests.<user-story>.json`; successful runs delete that file.',
11
+ '`--rerun-failed-apex-tests` runs the previously failed classes (optionally filtered by `--apex-test-classes`).',
8
12
  'Prints the test-run summary when the run passes, or failing tests with their messages and stack traces when it fails.',
13
+ 'Exits with the same process exit code as Salesforce CLI (`sf`), including 100 when Apex tests fail.',
9
14
  ].join(' ');
10
15
  static examples = [
11
16
  '<%= config.bin %> <%= command.id %> -t HelloWorldTest',
12
17
  '<%= config.bin %> <%= command.id %> --apex-test-classes=MyTest,OtherTest',
13
18
  '<%= config.bin %> <%= command.id %> -t MyTest,OtherTest -w 10 --json',
19
+ '<%= config.bin %> <%= command.id %> -t "$AGENTIA_APEX_TEST_CLASSES"',
20
+ '<%= config.bin %> <%= command.id %> --rerun-failed-apex-tests',
14
21
  ];
15
22
  static flags = {
16
23
  'apex-test-classes': Flags.string({
17
24
  char: 't',
18
- description: 'Comma-separated Apex test class names to run',
19
- required: true,
25
+ description: 'Comma-separated Apex test class names to run. Omit or leave empty to skip without running Salesforce CLI.',
26
+ }),
27
+ 'rerun-failed-apex-tests': Flags.boolean({
28
+ default: false,
29
+ description: 'Rerun Apex classes saved from the last failure for this user story (`.agentia/apex.tests.<user-story>.json`). Combined with --apex-test-classes, only matching failed classes run.',
20
30
  }),
21
31
  wait: Flags.integer({
22
32
  char: 'w',
@@ -36,8 +46,30 @@ export default class WorkTestApex extends AgentiaCommand {
36
46
  catch (error) {
37
47
  this.error(error.message, { exit: 2 });
38
48
  }
49
+ let plan;
50
+ try {
51
+ plan = await planWorkApexTestRun({
52
+ requestedClassNames: classNames,
53
+ rerunFailed: flags['rerun-failed-apex-tests'] === true,
54
+ });
55
+ }
56
+ catch (error) {
57
+ if (error instanceof WorkApexRerunError) {
58
+ this.error(error.message, { exit: 2 });
59
+ }
60
+ throw error;
61
+ }
62
+ if (plan.kind === 'skip') {
63
+ const result = skippedWorkApexTestResult(waitMinutes);
64
+ this.display(() => {
65
+ this.log(plan.message);
66
+ });
67
+ this.logElapsed(startedAt);
68
+ return result;
69
+ }
39
70
  try {
40
- const result = await this.withSpinner(`Running Apex tests (${classNames.join(', ')}), waiting up to ${waitMinutes} minute(s)`, () => runWorkApexTests({ classNames, waitMinutes }));
71
+ const result = await this.withSpinner(`Running Apex tests (${plan.classNames.join(', ')}), waiting up to ${waitMinutes} minute(s)`, () => runWorkApexTests({ classNames: plan.classNames, waitMinutes }));
72
+ await recordWorkApexTestOutcome({ failed: false, result, userStory: plan.userStory });
41
73
  this.display(() => {
42
74
  this.log(formatWorkApexTestHuman(result));
43
75
  });
@@ -46,6 +78,7 @@ export default class WorkTestApex extends AgentiaCommand {
46
78
  }
47
79
  catch (error) {
48
80
  if (error instanceof WorkApexTestError) {
81
+ await recordWorkApexTestOutcome({ failed: true, result: error.result, userStory: plan.userStory });
49
82
  this.display(() => {
50
83
  this.log(formatWorkApexTestHuman(error.result));
51
84
  });
@@ -2,7 +2,7 @@ import { Flags } from '@oclif/core';
2
2
  import path from 'node:path';
3
3
  import { AgentiaCommand } from '../../../lib/agentia-command.js';
4
4
  import { getCurrentBranch } from '../../../lib/git-worktree.js';
5
- import { LocalQualityGatesError, resolveLocalQualityGatesScript } from '../../../lib/local-quality-gates.js';
5
+ import { LocalQualityGatesError, parseQualityGatesEnvAssignments, resolveLocalQualityGatesScript, } from '../../../lib/local-quality-gates.js';
6
6
  import { runWorkTestLocalQualityGates } from '../../../lib/stock-quality-gates.js';
7
7
  import { reportLocalQualityGatesFailure, reportLocalQualityGatesOutcome, } from '../../../lib/work-local-run-tests.js';
8
8
  import { extractUserStoryFromFeatureBranch } from '../../../lib/work-types.js';
@@ -17,6 +17,8 @@ export default class WorkTest extends AgentiaCommand {
17
17
  static examples = [
18
18
  '<%= config.bin %> <%= command.id %>',
19
19
  '<%= config.bin %> <%= command.id %> --local --apex-test-classes=MyTest,OtherTest',
20
+ '<%= config.bin %> <%= command.id %> --local --rerun-failed-apex-tests',
21
+ '<%= config.bin %> <%= command.id %> -e SKIP_LWC_TESTS=1 -e TOOL_API_KEY=secret',
20
22
  '<%= config.bin %> <%= command.id %> --json',
21
23
  '<%= config.bin %> <%= command.id %> --cloud',
22
24
  ];
@@ -28,10 +30,19 @@ export default class WorkTest extends AgentiaCommand {
28
30
  default: false,
29
31
  description: 'Run remote Copado quality gates (not implemented yet).',
30
32
  }),
33
+ env: Flags.string({
34
+ char: 'e',
35
+ description: 'Extra environment variable for the quality-gate script as NAME=value. Repeatable. Applied before reserved AGENTIA_APEX_* variables.',
36
+ multiple: true,
37
+ }),
31
38
  local: Flags.boolean({
32
39
  default: false,
33
40
  description: 'Run local project-root quality gates scripts. Used by default when --cloud is omitted.',
34
41
  }),
42
+ 'rerun-failed-apex-tests': Flags.boolean({
43
+ default: false,
44
+ description: 'Pass AGENTIA_APEX_RERUN_FAILED=--rerun-failed-apex-tests to local quality gates so sample scripts forward that flag to work test apex.',
45
+ }),
35
46
  };
36
47
  async run() {
37
48
  const { flags } = await this.parse(WorkTest);
@@ -45,6 +56,12 @@ export default class WorkTest extends AgentiaCommand {
45
56
  suggestions: [`${this.config.bin} cicd work test --local`],
46
57
  });
47
58
  }
59
+ try {
60
+ parseQualityGatesEnvAssignments(flags.env);
61
+ }
62
+ catch (error) {
63
+ this.error(error.message, { exit: 2 });
64
+ }
48
65
  let qualityGates;
49
66
  try {
50
67
  const scriptPath = resolveLocalQualityGatesScript();
@@ -60,7 +77,9 @@ export default class WorkTest extends AgentiaCommand {
60
77
  }
61
78
  qualityGates = await runWorkTestLocalQualityGates({
62
79
  apexTestClasses: flags['apex-test-classes'],
80
+ env: flags.env,
63
81
  onWarning: (message) => this.warn(message),
82
+ rerunFailed: flags['rerun-failed-apex-tests'] === true,
64
83
  });
65
84
  if (qualityGates.ran && qualityGates.scriptPath !== undefined) {
66
85
  this.display(() => {
@@ -0,0 +1,26 @@
1
+ import { Command, Flags } from '@oclif/core';
2
+ import { AGENTIA_RUNTIME_DEFAULT_IDLE_MS, serveRuntimeRpc } from '../lib/runtime-rpc.js';
3
+ /** Long-lived stdio NDJSON server so the IDE can reuse one Agentia process. */
4
+ export default class Runtime extends Command {
5
+ static description = 'Start a stdio NDJSON runtime for in-process Agentia command invokes';
6
+ static enableJsonFlag = false;
7
+ static flags = {
8
+ 'idle-ms': Flags.integer({
9
+ default: AGENTIA_RUNTIME_DEFAULT_IDLE_MS,
10
+ description: 'Exit after this many idle milliseconds with no in-flight requests',
11
+ min: 1000,
12
+ }),
13
+ };
14
+ static hidden = true;
15
+ async run() {
16
+ const { flags: { 'idle-ms': idleMs } } = await this.parse(Runtime);
17
+ const { config } = this;
18
+ await serveRuntimeRpc({
19
+ cliVersion: config.version,
20
+ finder: config,
21
+ idleMs,
22
+ runCommand: (id, argv) => config.runCommand(id, argv),
23
+ });
24
+ }
25
+ }
26
+ //# sourceMappingURL=runtime.js.map
@@ -2,7 +2,7 @@ import { consumeIdeCommandError, publishIdeCommandCompleted } from '../../lib/id
2
2
  const hook = async function ({ config, error, id }) {
3
3
  try {
4
4
  const rememberedError = consumeIdeCommandError();
5
- if (id.trim().length === 0) {
5
+ if (id.trim().length === 0 || id === 'runtime') {
6
6
  return;
7
7
  }
8
8
  await publishIdeCommandCompleted({
@@ -0,0 +1,6 @@
1
+ import { createPluginHost, registerPluginHost } from '../../lib/plugin-host/index.js';
2
+ const hook = async function () {
3
+ registerPluginHost(createPluginHost({ cliVersion: this.config.version }));
4
+ };
5
+ export default hook;
6
+ //# sourceMappingURL=plugin-host.js.map