@copado/agentia-cli 1.0.0-beta.0 → 1.0.0-beta.2
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.
- package/CHANGELOG.md +40 -0
- package/README.md +505 -329
- package/dist/commands/auth/get.js +45 -136
- package/dist/commands/auth/set.js +3 -3
- package/dist/commands/cicd/cloud/commit.js +252 -0
- package/dist/commands/cicd/cloud/promote.js +139 -0
- package/dist/commands/cicd/health-check.js +77 -0
- package/dist/commands/cicd/work/commit.js +10 -324
- package/dist/commands/cicd/work/environment-sync.js +120 -0
- package/dist/commands/cicd/work/get.js +25 -4
- package/dist/commands/cicd/work/promote.js +93 -97
- package/dist/commands/cicd/work/set.js +4 -2
- package/dist/commands/cicd/work/submit.js +7 -0
- package/dist/lib/ai-config.js +2 -1
- package/dist/lib/auth-status-format.js +6 -0
- package/dist/lib/defaults-store.js +2 -1
- package/dist/lib/git-sizer.js +5 -0
- package/dist/lib/git-worktree.js +16 -1
- package/dist/lib/health-check.js +3 -0
- package/dist/lib/mcp/command-id.js +1 -0
- package/dist/lib/mcp/health-check-tools.js +16 -0
- package/dist/lib/mcp/server.js +2 -1
- package/dist/lib/mcp/tool-annotations.js +265 -0
- package/dist/lib/mcp/tools.js +3 -0
- package/dist/lib/mcp/work-tools.js +96 -78
- package/dist/lib/metadata-pathspecs.js +4 -0
- package/dist/lib/metadataRegistry.json +5427 -0
- package/dist/lib/promotion-types.js +4 -1
- package/dist/lib/runtime.js +1 -1
- package/dist/lib/work-environment-sync-errors.js +4 -0
- package/dist/lib/work-environment-sync-ready.js +18 -0
- package/dist/lib/work-environment-sync.js +42 -0
- package/dist/lib/work-promote-deploy.js +13 -0
- package/dist/lib/work-promote-errors.js +5 -0
- package/dist/lib/work-promote.js +12 -0
- package/dist/lib/work-set-selection.js +2 -1
- package/dist/lib/work-submit-ahead-behind.js +8 -0
- package/dist/lib/work-types.js +2 -1
- package/oclif.manifest.json +2725 -2282
- package/package.json +5 -2
- package/templates/skills/agentia-cicd/SKILL.md +2 -2
- package/templates/skills/agentia-cicd/references/promotions.md +8 -8
- package/templates/skills/agentia-cicd/references/work.md +16 -14
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { Flags } from '@oclif/core';
|
|
1
|
+
import { Flags, ux } from '@oclif/core';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { AgentiaCommand } from '../../lib/agentia-command.js';
|
|
4
|
-
import { getCrtAuthStatus } from '../../lib/auth-config.js';
|
|
4
|
+
import { getAiAuthStatus, getCicdAuthStatus, getCrtAuthStatus } from '../../lib/auth-config.js';
|
|
5
5
|
import { readAuthProfile } from '../../lib/auth-profile.js';
|
|
6
|
-
import {
|
|
6
|
+
import { formatAiStatusLine, formatCicdStatusLine, formatCrtStatusLine } from '../../lib/auth-status-format.js';
|
|
7
|
+
import { ALL_CREDENTIAL_TYPES, credentialLabel, } from '../../lib/credential-store.js';
|
|
7
8
|
import { getDefaultsStorePaths } from '../../lib/defaults-store.js';
|
|
8
|
-
import { normalizeLocalGatewayBaseUrl, readLocalGatewayConfig } from '../../lib/local-gateway-config.js';
|
|
9
|
-
import { formatTestingConfigurationFieldList, } from '../../lib/testing-config.js';
|
|
10
9
|
export default class AuthGet extends AgentiaCommand {
|
|
11
|
-
static description = 'Show stored authentication credentials
|
|
10
|
+
static description = 'Show stored authentication credentials and their configured endpoints and scopes';
|
|
12
11
|
static examples = [
|
|
13
12
|
'<%= config.bin %> <%= command.id %>',
|
|
14
13
|
'<%= config.bin %> <%= command.id %> --ai',
|
|
@@ -42,55 +41,32 @@ export default class AuthGet extends AgentiaCommand {
|
|
|
42
41
|
}
|
|
43
42
|
const selectedTypes = types.length > 0 ? types : ALL_CREDENTIAL_TYPES;
|
|
44
43
|
const credentials = selectedTypes.map((type) => this.buildCredentialResult(type));
|
|
44
|
+
this.display(() => {
|
|
45
|
+
for (const type of ['cicd', 'ai', 'crt']) {
|
|
46
|
+
if (!selectedTypes.includes(type))
|
|
47
|
+
continue;
|
|
48
|
+
const scopes = this.hasLocalAuthentication(type) ? ['global', 'local'] : [undefined];
|
|
49
|
+
this.log('');
|
|
50
|
+
this.log(ux.colorize('cyan', `${type.toUpperCase()} authentication`));
|
|
51
|
+
for (const [index, scope] of scopes.entries()) {
|
|
52
|
+
if (index > 0)
|
|
53
|
+
this.log('');
|
|
54
|
+
this.log(this.formatCredentialLine(this.buildCredentialResult(type, scope)));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
});
|
|
45
58
|
const configFiles = this.buildConfigFileResults();
|
|
46
|
-
for (const credential of credentials) {
|
|
47
|
-
this.log(this.formatCredentialLine(credential));
|
|
48
|
-
}
|
|
49
|
-
for (const configFile of configFiles) {
|
|
50
|
-
this.log(this.formatConfigFileLine(configFile));
|
|
51
|
-
}
|
|
52
59
|
return { configFiles, credentials };
|
|
53
60
|
}
|
|
54
|
-
buildCicdCredentialResult() {
|
|
55
|
-
const
|
|
56
|
-
const label = credentialLabel('cicd');
|
|
57
|
-
const localProfile = readAuthProfile('local');
|
|
58
|
-
const profile = localProfile?.cicd ?? readAuthProfile('global')?.cicd;
|
|
59
|
-
if (localConfig.enabled) {
|
|
60
|
-
const apiKey = localConfig.apiKey || (localProfile?.cicd === undefined ? getCredential('cicd') : getCredentialForKey('cicd', localProfile.keychainId));
|
|
61
|
-
if (apiKey === null || apiKey.trim().length === 0) {
|
|
62
|
-
return {
|
|
63
|
-
baseUrl: normalizeLocalGatewayBaseUrl(localConfig.url),
|
|
64
|
-
label,
|
|
65
|
-
local: true,
|
|
66
|
-
set: false,
|
|
67
|
-
type: 'cicd',
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
return {
|
|
71
|
-
baseUrl: normalizeLocalGatewayBaseUrl(localConfig.url),
|
|
72
|
-
label,
|
|
73
|
-
local: true,
|
|
74
|
-
masked: maskSecret(apiKey),
|
|
75
|
-
set: true,
|
|
76
|
-
source: 'local',
|
|
77
|
-
type: 'cicd',
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
const credential = localProfile?.cicd === undefined ? getCredential('cicd') : getCredentialForKey('cicd', localProfile.keychainId);
|
|
81
|
-
if (credential === null) {
|
|
82
|
-
return {
|
|
83
|
-
label,
|
|
84
|
-
set: false,
|
|
85
|
-
type: 'cicd',
|
|
86
|
-
};
|
|
87
|
-
}
|
|
61
|
+
buildCicdCredentialResult(scope) {
|
|
62
|
+
const status = getCicdAuthStatus(scope);
|
|
88
63
|
return {
|
|
89
|
-
baseUrl:
|
|
90
|
-
label,
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
64
|
+
baseUrl: status.baseUrl,
|
|
65
|
+
label: credentialLabel('cicd'),
|
|
66
|
+
local: status.local,
|
|
67
|
+
masked: status.masked,
|
|
68
|
+
set: status.set,
|
|
69
|
+
source: status.source,
|
|
94
70
|
type: 'cicd',
|
|
95
71
|
};
|
|
96
72
|
}
|
|
@@ -101,38 +77,25 @@ export default class AuthGet extends AgentiaCommand {
|
|
|
101
77
|
{ exists: existsSync(userPath), label: 'User config', path: userPath, scope: 'user' },
|
|
102
78
|
];
|
|
103
79
|
}
|
|
104
|
-
buildCredentialResult(type) {
|
|
80
|
+
buildCredentialResult(type, scope) {
|
|
105
81
|
if (type === 'cicd') {
|
|
106
|
-
return this.buildCicdCredentialResult();
|
|
82
|
+
return this.buildCicdCredentialResult(scope);
|
|
107
83
|
}
|
|
108
84
|
if (type === 'ai') {
|
|
109
|
-
const
|
|
110
|
-
const metadata = local?.ai ?? readAuthProfile('global')?.ai;
|
|
111
|
-
const credential = local?.ai === undefined ? getCredential(type) : getCredentialForKey(type, local.keychainId);
|
|
112
|
-
if (credential === null) {
|
|
113
|
-
return {
|
|
114
|
-
domain: metadata?.domain ?? getAiDomain(),
|
|
115
|
-
gatewayUrl: metadata?.gatewayUrl ?? getAiGatewayUrl(),
|
|
116
|
-
label: credentialLabel(type),
|
|
117
|
-
orgId: metadata?.orgId ?? getAiOrgId(),
|
|
118
|
-
set: false,
|
|
119
|
-
type,
|
|
120
|
-
workspaceId: metadata?.workspaceId ?? getAiWorkspaceId(),
|
|
121
|
-
};
|
|
122
|
-
}
|
|
85
|
+
const status = getAiAuthStatus(scope);
|
|
123
86
|
return {
|
|
124
|
-
domain:
|
|
125
|
-
gatewayUrl:
|
|
87
|
+
domain: status.domain,
|
|
88
|
+
gatewayUrl: status.gatewayUrl,
|
|
126
89
|
label: credentialLabel(type),
|
|
127
|
-
masked:
|
|
128
|
-
orgId:
|
|
129
|
-
set:
|
|
130
|
-
source:
|
|
90
|
+
masked: status.masked,
|
|
91
|
+
orgId: status.orgId,
|
|
92
|
+
set: status.set,
|
|
93
|
+
source: status.source,
|
|
131
94
|
type,
|
|
132
|
-
workspaceId:
|
|
95
|
+
workspaceId: status.workspaceId,
|
|
133
96
|
};
|
|
134
97
|
}
|
|
135
|
-
const status = getCrtAuthStatus();
|
|
98
|
+
const status = getCrtAuthStatus(scope);
|
|
136
99
|
return {
|
|
137
100
|
domain: status.domain,
|
|
138
101
|
issues: status.issues,
|
|
@@ -146,74 +109,20 @@ export default class AuthGet extends AgentiaCommand {
|
|
|
146
109
|
type,
|
|
147
110
|
};
|
|
148
111
|
}
|
|
149
|
-
formatConfigFileLine(configFile) {
|
|
150
|
-
return `${configFile.label}: ${configFile.path} (${configFile.exists ? 'found' : 'not found'})`;
|
|
151
|
-
}
|
|
152
112
|
formatCredentialLine(credential) {
|
|
153
|
-
if (credential.type === 'crt') {
|
|
154
|
-
return this.formatCrtCredentialLine(credential);
|
|
155
|
-
}
|
|
156
|
-
if (!credential.set) {
|
|
157
|
-
if (credential.type === 'cicd' && credential.local && credential.baseUrl) {
|
|
158
|
-
return `${credential.label}: not set (endpoint: ${credential.baseUrl}, local)`;
|
|
159
|
-
}
|
|
160
|
-
if (credential.type === 'ai') {
|
|
161
|
-
const domain = credential.domain === null || credential.domain === undefined ? 'not set' : credential.domain;
|
|
162
|
-
const orgId = credential.orgId === null || credential.orgId === undefined ? 'not set' : credential.orgId;
|
|
163
|
-
const workspaceId = credential.workspaceId === null || credential.workspaceId === undefined ? 'not set' : credential.workspaceId;
|
|
164
|
-
const gatewayUrl = credential.gatewayUrl === null || credential.gatewayUrl === undefined ? 'derived' : credential.gatewayUrl;
|
|
165
|
-
return `${credential.label}: not set (domain: ${domain}, org: ${orgId}, workspace: ${workspaceId}, gateway: ${gatewayUrl})`;
|
|
166
|
-
}
|
|
167
|
-
return `${credential.label}: not set`;
|
|
168
|
-
}
|
|
169
113
|
if (credential.type === 'cicd') {
|
|
170
|
-
|
|
171
|
-
const localSuffix = credential.local ? ', local' : '';
|
|
172
|
-
const sourceSuffix = credential.source === undefined ? '' : `, source: ${credential.source}`;
|
|
173
|
-
return `${credential.label}: ${credential.masked} (endpoint: ${endpointLabel}${localSuffix}${sourceSuffix})`;
|
|
114
|
+
return formatCicdStatusLine(credential);
|
|
174
115
|
}
|
|
175
116
|
if (credential.type === 'ai') {
|
|
176
|
-
|
|
177
|
-
const orgId = credential.orgId === null || credential.orgId === undefined ? 'not set' : credential.orgId;
|
|
178
|
-
const workspaceId = credential.workspaceId === null || credential.workspaceId === undefined ? 'not set' : credential.workspaceId;
|
|
179
|
-
const gatewayUrl = credential.gatewayUrl === null || credential.gatewayUrl === undefined ? 'derived' : credential.gatewayUrl;
|
|
180
|
-
const sourceSuffix = credential.source === undefined ? '' : `, source: ${credential.source}`;
|
|
181
|
-
return `${credential.label}: ${credential.masked} (domain: ${domain}, org: ${orgId}, workspace: ${workspaceId}, gateway: ${gatewayUrl}${sourceSuffix})`;
|
|
117
|
+
return formatAiStatusLine(credential);
|
|
182
118
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
formatCrtCredentialLine(credential) {
|
|
186
|
-
const domain = credential.domain === null || credential.domain === undefined ? 'not set' : credential.domain;
|
|
187
|
-
const orgId = credential.orgId === null || credential.orgId === undefined ? 'not set' : credential.orgId;
|
|
188
|
-
const ready = credential.ready ?? credential.set;
|
|
189
|
-
if (!ready) {
|
|
190
|
-
const pak = credential.masked ?? 'not set';
|
|
191
|
-
const details = this.formatCrtIssueDetails(credential);
|
|
192
|
-
return `${credential.label}: incomplete (PAK: ${pak}, domain: ${domain}, org: ${orgId}${details})`;
|
|
119
|
+
if (credential.type === 'crt') {
|
|
120
|
+
return formatCrtStatusLine(credential);
|
|
193
121
|
}
|
|
194
|
-
|
|
195
|
-
return `${credential.label}: ${credential.masked} (domain: ${domain}, org: ${orgId}${sourceSuffix})`;
|
|
122
|
+
return '';
|
|
196
123
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const missing = issues.filter((issue) => issue.kind === 'missing').map((issue) => issue.field);
|
|
200
|
-
const invalid = issues.filter((issue) => issue.kind === 'invalid');
|
|
201
|
-
const details = [];
|
|
202
|
-
if (missing.length > 0) {
|
|
203
|
-
details.push(`missing: ${formatTestingConfigurationFieldList(missing)}`);
|
|
204
|
-
}
|
|
205
|
-
if (invalid.length > 0) {
|
|
206
|
-
details.push(`invalid: ${invalid
|
|
207
|
-
.map((issue) => {
|
|
208
|
-
const label = formatTestingConfigurationFieldList([issue.field]);
|
|
209
|
-
return issue.message === undefined ? label : `${label} (${issue.message})`;
|
|
210
|
-
})
|
|
211
|
-
.join(', ')}`);
|
|
212
|
-
}
|
|
213
|
-
if (details.length === 0 && credential.missing !== undefined && credential.missing.length > 0) {
|
|
214
|
-
details.push(`missing: ${formatTestingConfigurationFieldList(credential.missing)}`);
|
|
215
|
-
}
|
|
216
|
-
return details.length === 0 ? '' : `; ${details.join('; ')}`;
|
|
124
|
+
hasLocalAuthentication(type) {
|
|
125
|
+
return readAuthProfile('local')?.[type] !== undefined;
|
|
217
126
|
}
|
|
218
127
|
}
|
|
219
128
|
//# sourceMappingURL=get.js.map
|
|
@@ -6,13 +6,13 @@ export default class AuthSet extends AgentiaCommand {
|
|
|
6
6
|
static description = 'Store authentication credentials securely in the system keychain';
|
|
7
7
|
static examples = [
|
|
8
8
|
'<%= config.bin %> <%= command.id %> --ai <pak> --ai-domain robotic.copado.com --ai-org 42888',
|
|
9
|
+
'<%= config.bin %> <%= command.id %> --ai <pak> --ai-org 42888',
|
|
9
10
|
'<%= config.bin %> <%= command.id %> --ai <pak> --ai-domain robotic.copado.com --ai-org 42888 --ai-workspace 42d0b908-37b7-4565-9ec9-86eda40c9782',
|
|
10
11
|
'<%= config.bin %> <%= command.id %> --ai <pak> --ai-gateway-url http://localhost:8080/organizations/42888',
|
|
11
12
|
'<%= config.bin %> <%= command.id %> --ai-workspace 42d0b908-37b7-4565-9ec9-86eda40c9782',
|
|
12
13
|
'<%= config.bin %> <%= command.id %> --cicd <copado-api-key> --region na',
|
|
13
14
|
'<%= config.bin %> <%= command.id %> --cicd <copado-api-key> --custom-url https://dev.api.copado.com',
|
|
14
15
|
'<%= config.bin %> <%= command.id %> --cicd <copado-api-key>',
|
|
15
|
-
'<%= config.bin %> <%= command.id %> --crt <pak>',
|
|
16
16
|
'<%= config.bin %> <%= command.id %> --crt <pak> --crt-org <crt-org-id>',
|
|
17
17
|
'<%= config.bin %> <%= command.id %> --crt <pak> --crt-domain robotic.copado.com --crt-org <crt-org-id>',
|
|
18
18
|
'<%= config.bin %> <%= command.id %> --cicd <copado-api-key> --region na --crt <pak>',
|
|
@@ -21,7 +21,7 @@ export default class AuthSet extends AgentiaCommand {
|
|
|
21
21
|
static flags = {
|
|
22
22
|
ai: Flags.string({ description: 'PAK for Copado AI authentication' }),
|
|
23
23
|
'ai-domain': Flags.string({
|
|
24
|
-
description: 'Copado AI domain hostname
|
|
24
|
+
description: 'Optional Copado AI domain hostname; omit to auto-discover from the PAK and organization ID',
|
|
25
25
|
}),
|
|
26
26
|
'ai-gateway-url': Flags.string({
|
|
27
27
|
description: 'Full Copado AI gateway base URL used instead of deriving from domain and organization; HTTP is allowed for local testing',
|
|
@@ -35,7 +35,7 @@ export default class AuthSet extends AgentiaCommand {
|
|
|
35
35
|
cicd: Flags.string({
|
|
36
36
|
description: 'Copado API key for CICD authentication',
|
|
37
37
|
}),
|
|
38
|
-
crt: Flags.string({ description: 'PAK for CRT authentication' }),
|
|
38
|
+
crt: Flags.string({ description: 'PAK for CRT authentication (requires --crt-org)' }),
|
|
39
39
|
'crt-domain': Flags.string({
|
|
40
40
|
description: 'CRT domain hostname for robotic testing commands',
|
|
41
41
|
}),
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { AgentiaCommand } from '../../../lib/agentia-command.js';
|
|
4
|
+
import { resolveWorkItemId, WorkActiveResolutionError } from '../../../lib/work-active.js';
|
|
5
|
+
import { buildWorkCommitRequest, formatWorkCommitResult, normalizeWorkCommitInput, performWorkCommit, resolveWorkCommitPlatformExperience, WORK_COMMIT_ACTIONS, WORK_COMMIT_DEFAULT_TIMEOUT_SECONDS, WORK_COMMIT_METADATA_CATEGORY_HELP, WorkCommitJobFailedError, WorkCommitTimeoutError, WorkCommitValidationError, } from '../../../lib/work-commit.js';
|
|
6
|
+
function readStdin() {
|
|
7
|
+
if (process.stdin.isTTY) {
|
|
8
|
+
throw new WorkCommitValidationError('Cannot read --stdin from an interactive terminal. Pipe JSON input or use --file <path>.');
|
|
9
|
+
}
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let data = '';
|
|
12
|
+
process.stdin.setEncoding('utf8');
|
|
13
|
+
process.stdin.on('data', (chunk) => {
|
|
14
|
+
data += chunk;
|
|
15
|
+
});
|
|
16
|
+
process.stdin.on('end', () => resolve(data));
|
|
17
|
+
process.stdin.on('error', reject);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function readJsonFile(path) {
|
|
21
|
+
try {
|
|
22
|
+
return readFileSync(path, 'utf8');
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new WorkCommitValidationError(`Cannot read JSON request file ${path}: ${error.message}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function parseJsonValue(raw, label) {
|
|
29
|
+
try {
|
|
30
|
+
return JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
throw new WorkCommitValidationError(`${label} must contain valid JSON: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
function hasSimpleChangeFlags(flags) {
|
|
37
|
+
return (flags.action !== undefined ||
|
|
38
|
+
flags['metadata-category'] !== undefined ||
|
|
39
|
+
flags['metadata-message'] !== undefined ||
|
|
40
|
+
flags['metadata-name'] !== undefined ||
|
|
41
|
+
flags['metadata-raw-json'] !== undefined ||
|
|
42
|
+
flags['metadata-type'] !== undefined ||
|
|
43
|
+
flags.selection !== undefined);
|
|
44
|
+
}
|
|
45
|
+
function simpleInputFromFlags(flags) {
|
|
46
|
+
const rawJson = flags['metadata-raw-json'] === undefined
|
|
47
|
+
? undefined
|
|
48
|
+
: parseJsonValue(flags['metadata-raw-json'], '--metadata-raw-json');
|
|
49
|
+
return {
|
|
50
|
+
baseBranch: flags['base-branch'] ?? null,
|
|
51
|
+
changes: [
|
|
52
|
+
{
|
|
53
|
+
...(flags.action === undefined ? {} : { a: flags.action }),
|
|
54
|
+
...(flags['metadata-category'] === undefined ? {} : { c: flags['metadata-category'] }),
|
|
55
|
+
...(rawJson === undefined ? {} : { j: rawJson }),
|
|
56
|
+
...(flags['metadata-message'] === undefined ? {} : { m: flags['metadata-message'] }),
|
|
57
|
+
n: flags['metadata-name'],
|
|
58
|
+
...(flags.selection === undefined ? {} : { selections: flags.selection }),
|
|
59
|
+
t: flags['metadata-type'],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
commitId: flags['commit-id'],
|
|
63
|
+
executeCommit: flags['execute-commit'],
|
|
64
|
+
message: flags.message,
|
|
65
|
+
recreateFeatureBranch: flags['recreate-feature-branch'],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
async function jsonInputFromFlags(flags) {
|
|
69
|
+
const raw = flags.file === undefined ? await readStdin() : readJsonFile(flags.file);
|
|
70
|
+
const input = normalizeWorkCommitInput(parseJsonValue(raw, flags.file === undefined ? 'stdin' : flags.file));
|
|
71
|
+
return {
|
|
72
|
+
...input,
|
|
73
|
+
...(flags['base-branch'] === undefined ? {} : { baseBranch: flags['base-branch'] }),
|
|
74
|
+
...(flags['commit-id'] === undefined ? {} : { commitId: flags['commit-id'] }),
|
|
75
|
+
...(flags['execute-commit'] ? {} : { executeCommit: false }),
|
|
76
|
+
...(flags.message === undefined ? {} : { message: flags.message }),
|
|
77
|
+
...(flags['recreate-feature-branch'] ? { recreateFeatureBranch: true } : {}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
async function commitInputFromFlags(flags) {
|
|
81
|
+
const jsonModes = [flags.file !== undefined, flags.stdin].filter(Boolean).length;
|
|
82
|
+
const simpleMode = hasSimpleChangeFlags(flags);
|
|
83
|
+
if (jsonModes > 1) {
|
|
84
|
+
throw new WorkCommitValidationError('Use only one JSON input mode: --file or --stdin.');
|
|
85
|
+
}
|
|
86
|
+
if (jsonModes > 0 && simpleMode) {
|
|
87
|
+
throw new WorkCommitValidationError('Use JSON input or simple metadata flags, not both.');
|
|
88
|
+
}
|
|
89
|
+
if (jsonModes > 0) {
|
|
90
|
+
return jsonInputFromFlags(flags);
|
|
91
|
+
}
|
|
92
|
+
if (!simpleMode) {
|
|
93
|
+
throw new WorkCommitValidationError('Requires simple metadata flags such as --metadata-name and --metadata-type, or JSON input with --file/--stdin.');
|
|
94
|
+
}
|
|
95
|
+
return simpleInputFromFlags(flags);
|
|
96
|
+
}
|
|
97
|
+
export default class CloudCommit extends AgentiaCommand {
|
|
98
|
+
static args = {
|
|
99
|
+
id: Args.string({
|
|
100
|
+
description: 'User story ID (defaults to the active work item from `work set`)',
|
|
101
|
+
required: false,
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
static description = [
|
|
105
|
+
'Create a Copado cloud metadata commit for a user story.',
|
|
106
|
+
'Preflights the story, submits POST /release/commit, and polls the job monitor by default.',
|
|
107
|
+
'For local Git work, use git add/commit then `cicd work publish`. Permission and full-metadata selections belong on `cicd work publish`.',
|
|
108
|
+
].join(' ');
|
|
109
|
+
static examples = [
|
|
110
|
+
'<%= config.bin %> <%= command.id %> --message "Commit AccountService" --metadata-name AccountService --metadata-type ApexClass --metadata-category SFDX --action Add',
|
|
111
|
+
'<%= config.bin %> <%= command.id %> a07XXXXXXXXXXXXXXX --message "Update Aura files" --metadata-name accountEditor --metadata-type AuraDefinitionBundle --metadata-category SFDX --action SelectiveCommit --selection accountEditor/accountEditor.cmp --selection accountEditor/accountEditorController.js',
|
|
112
|
+
'<%= config.bin %> <%= command.id %> --file commit-request.json # {"message":"Commit AccountService","changes":[{"a":"Add","c":"SFDX","n":"AccountService","t":"ApexClass"}]}',
|
|
113
|
+
'cat commit-request.json | <%= config.bin %> <%= command.id %> --stdin --json',
|
|
114
|
+
];
|
|
115
|
+
static flags = {
|
|
116
|
+
action: Flags.string({
|
|
117
|
+
description: 'Metadata action for simple flag input. Defaults to Add.',
|
|
118
|
+
options: [...WORK_COMMIT_ACTIONS],
|
|
119
|
+
}),
|
|
120
|
+
'base-branch': Flags.string({
|
|
121
|
+
description: 'Optional base branch to include in the commit request.',
|
|
122
|
+
}),
|
|
123
|
+
cloud: Flags.boolean({
|
|
124
|
+
default: false,
|
|
125
|
+
description: 'Ignored. Cloud is implied by cicd cloud commit.',
|
|
126
|
+
hidden: true,
|
|
127
|
+
}),
|
|
128
|
+
'commit-id': Flags.string({
|
|
129
|
+
description: 'Existing local Git commit identifier to register when --no-execute-commit is used.',
|
|
130
|
+
}),
|
|
131
|
+
'execute-commit': Flags.boolean({
|
|
132
|
+
allowNo: true,
|
|
133
|
+
default: true,
|
|
134
|
+
description: 'Execute the Copado commit job. Use --no-execute-commit only with --commit-id.',
|
|
135
|
+
}),
|
|
136
|
+
file: Flags.string({
|
|
137
|
+
char: 'f',
|
|
138
|
+
description: 'Path to a JSON commit request body.',
|
|
139
|
+
}),
|
|
140
|
+
message: Flags.string({
|
|
141
|
+
char: 'm',
|
|
142
|
+
description: 'Commit message. Required unless provided by JSON input.',
|
|
143
|
+
}),
|
|
144
|
+
'metadata-category': Flags.string({
|
|
145
|
+
description: WORK_COMMIT_METADATA_CATEGORY_HELP,
|
|
146
|
+
}),
|
|
147
|
+
'metadata-message': Flags.string({
|
|
148
|
+
description: 'Optional per-change message for simple flag input.',
|
|
149
|
+
}),
|
|
150
|
+
'metadata-name': Flags.string({
|
|
151
|
+
description: 'Metadata full name for simple flag input.',
|
|
152
|
+
}),
|
|
153
|
+
'metadata-raw-json': Flags.string({
|
|
154
|
+
description: 'Optional raw metadata JSON value for simple flag input.',
|
|
155
|
+
}),
|
|
156
|
+
'metadata-type': Flags.string({
|
|
157
|
+
description: 'Metadata type for simple flag input, for example ApexClass.',
|
|
158
|
+
}),
|
|
159
|
+
'recreate-feature-branch': Flags.boolean({
|
|
160
|
+
default: false,
|
|
161
|
+
description: 'Ask Copado to recreate the feature branch before committing.',
|
|
162
|
+
}),
|
|
163
|
+
selection: Flags.string({
|
|
164
|
+
description: 'Selected file inside a bundle for SelectiveCommit. Can be provided multiple times.',
|
|
165
|
+
multiple: true,
|
|
166
|
+
}),
|
|
167
|
+
stdin: Flags.boolean({
|
|
168
|
+
default: false,
|
|
169
|
+
description: 'Read a JSON commit request body from stdin.',
|
|
170
|
+
}),
|
|
171
|
+
wait: Flags.boolean({
|
|
172
|
+
allowNo: true,
|
|
173
|
+
default: true,
|
|
174
|
+
description: 'Poll the commit job to a terminal status when a jobExecution Id is returned.',
|
|
175
|
+
}),
|
|
176
|
+
'wait-timeout': Flags.integer({
|
|
177
|
+
default: WORK_COMMIT_DEFAULT_TIMEOUT_SECONDS,
|
|
178
|
+
description: 'Maximum seconds to wait for the commit job.',
|
|
179
|
+
}),
|
|
180
|
+
};
|
|
181
|
+
async run() {
|
|
182
|
+
const { args, flags } = await this.parse(CloudCommit);
|
|
183
|
+
const commitFlags = flags;
|
|
184
|
+
if (!Number.isInteger(commitFlags['wait-timeout']) || commitFlags['wait-timeout'] <= 0) {
|
|
185
|
+
this.error('--wait-timeout must be a positive integer.', { exit: 2 });
|
|
186
|
+
}
|
|
187
|
+
let workItemId;
|
|
188
|
+
try {
|
|
189
|
+
workItemId = await resolveWorkItemId(args.id);
|
|
190
|
+
}
|
|
191
|
+
catch (error) {
|
|
192
|
+
if (error instanceof WorkActiveResolutionError) {
|
|
193
|
+
this.error(error.message, {
|
|
194
|
+
exit: 1,
|
|
195
|
+
suggestions: [`${this.config.bin} cicd work set <id>`],
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
const input = await commitInputFromFlags(commitFlags);
|
|
202
|
+
const request = buildWorkCommitRequest(input, { message: commitFlags.message, userStoryId: workItemId });
|
|
203
|
+
const initialPlatformExperience = await resolveWorkCommitPlatformExperience({
|
|
204
|
+
userStoryId: workItemId,
|
|
205
|
+
});
|
|
206
|
+
const result = await performWorkCommit({
|
|
207
|
+
hooks: {
|
|
208
|
+
log: (message) => this.display(() => this.log(message)),
|
|
209
|
+
withSpinner: (message, action) => this.withSpinner(message, action),
|
|
210
|
+
},
|
|
211
|
+
initialPlatformExperience,
|
|
212
|
+
request,
|
|
213
|
+
timeoutSeconds: commitFlags['wait-timeout'],
|
|
214
|
+
wait: commitFlags.wait,
|
|
215
|
+
});
|
|
216
|
+
this.display(() => {
|
|
217
|
+
this.log(formatWorkCommitResult(result));
|
|
218
|
+
});
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
if (error instanceof WorkCommitValidationError) {
|
|
223
|
+
this.error(error.message, { exit: 2 });
|
|
224
|
+
}
|
|
225
|
+
if (error instanceof WorkCommitJobFailedError || error instanceof WorkCommitTimeoutError) {
|
|
226
|
+
this.display(() => {
|
|
227
|
+
if (error.result !== undefined) {
|
|
228
|
+
this.log(formatWorkCommitResult(error.result));
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
error.oclif = { exit: 1 };
|
|
232
|
+
throw error;
|
|
233
|
+
}
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
toErrorJson(err) {
|
|
238
|
+
const base = super.toErrorJson(err);
|
|
239
|
+
if (err instanceof WorkCommitJobFailedError || err instanceof WorkCommitTimeoutError) {
|
|
240
|
+
return {
|
|
241
|
+
...base,
|
|
242
|
+
error: {
|
|
243
|
+
...(base.error && typeof base.error === 'object' ? base.error : {}),
|
|
244
|
+
...(err.jobMonitor === undefined ? {} : { jobMonitor: err.jobMonitor }),
|
|
245
|
+
...(err.result === undefined ? {} : { result: err.result }),
|
|
246
|
+
},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
return base;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=commit.js.map
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { confirm } from '@inquirer/prompts';
|
|
2
|
+
import { Args, Flags } from '@oclif/core';
|
|
3
|
+
import { AgentiaCommand } from '../../../lib/agentia-command.js';
|
|
4
|
+
import { formatPromotionRunResult, isPromotionRunExecutionError, parseIgnoredMetadataJson, performPromotionRun, PROMOTION_RUN_DEFAULT_TIMEOUT_SECONDS, promotionRunErrorJson, PromotionRunValidationError, resolvePromotionIdForWorkItem, } from '../../../lib/promotion-run.js';
|
|
5
|
+
import { resolveWorkItemId, WorkActiveResolutionError } from '../../../lib/work-active.js';
|
|
6
|
+
export default class CloudPromote extends AgentiaCommand {
|
|
7
|
+
static args = {
|
|
8
|
+
id: Args.string({
|
|
9
|
+
description: 'User Story ID (defaults to the active work item from `work set`)',
|
|
10
|
+
required: false,
|
|
11
|
+
}),
|
|
12
|
+
};
|
|
13
|
+
static description = [
|
|
14
|
+
'Promote the explicit or active User Story through its Copado Promotion.',
|
|
15
|
+
'Selects promote-only unless --deploy is passed.',
|
|
16
|
+
].join(' ');
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> <%= command.id %>',
|
|
19
|
+
'<%= config.bin %> <%= command.id %> a07XXXXXXXXXXXXXXX --promotion-id a0BXXXXXXXXXXXXXXX',
|
|
20
|
+
'<%= config.bin %> <%= command.id %> --deploy --json',
|
|
21
|
+
];
|
|
22
|
+
static flags = {
|
|
23
|
+
cloud: Flags.boolean({
|
|
24
|
+
default: false,
|
|
25
|
+
description: 'Ignored. Cloud is implied by cicd cloud promote.',
|
|
26
|
+
hidden: true,
|
|
27
|
+
}),
|
|
28
|
+
deploy: Flags.boolean({
|
|
29
|
+
default: false,
|
|
30
|
+
description: 'Use merge_and_deploy instead of promote-only merge.',
|
|
31
|
+
}),
|
|
32
|
+
'deployment-step-id': Flags.string({
|
|
33
|
+
description: 'Ordered deployment-step Salesforce ID. Can be provided multiple times with --deploy.',
|
|
34
|
+
multiple: true,
|
|
35
|
+
}),
|
|
36
|
+
'ignored-metadata-json': Flags.string({
|
|
37
|
+
description: 'JSON array to persist through ignored-metadata before starting the promotion.',
|
|
38
|
+
}),
|
|
39
|
+
'promotion-id': Flags.string({
|
|
40
|
+
description: 'Explicit 18-character Promotion ID when the User Story belongs to multiple promotions.',
|
|
41
|
+
}),
|
|
42
|
+
'recreate-promotion-branch': Flags.boolean({
|
|
43
|
+
description: 'Recreate the promotion branch before execution. Requires confirmation or --yes.',
|
|
44
|
+
}),
|
|
45
|
+
wait: Flags.boolean({
|
|
46
|
+
allowNo: true,
|
|
47
|
+
default: true,
|
|
48
|
+
description: 'Poll promotion and deployment executions to terminal status.',
|
|
49
|
+
}),
|
|
50
|
+
'wait-timeout': Flags.integer({
|
|
51
|
+
default: PROMOTION_RUN_DEFAULT_TIMEOUT_SECONDS,
|
|
52
|
+
description: 'Maximum total seconds to wait for promotion and deployment executions.',
|
|
53
|
+
}),
|
|
54
|
+
yes: Flags.boolean({
|
|
55
|
+
char: 'y',
|
|
56
|
+
default: false,
|
|
57
|
+
description: 'Confirm promotion branch recreation without prompting.',
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
async run() {
|
|
61
|
+
const { args, flags } = await this.parse(CloudPromote);
|
|
62
|
+
const promoteFlags = flags;
|
|
63
|
+
if (!Number.isInteger(promoteFlags['wait-timeout']) || promoteFlags['wait-timeout'] <= 0) {
|
|
64
|
+
this.error('--wait-timeout must be a positive integer.', { exit: 2 });
|
|
65
|
+
}
|
|
66
|
+
if (!promoteFlags.deploy && promoteFlags['deployment-step-id'] !== undefined) {
|
|
67
|
+
this.error('--deployment-step-id requires --deploy.', { exit: 2 });
|
|
68
|
+
}
|
|
69
|
+
let workItemId;
|
|
70
|
+
try {
|
|
71
|
+
workItemId = await resolveWorkItemId(args.id);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error instanceof WorkActiveResolutionError) {
|
|
75
|
+
this.error(error.message, {
|
|
76
|
+
exit: 1,
|
|
77
|
+
suggestions: [`${this.config.bin} cicd work set <id>`],
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
try {
|
|
83
|
+
const promotionId = await this.withSpinner('Resolving User Story promotion', () => resolvePromotionIdForWorkItem(workItemId, promoteFlags['promotion-id']));
|
|
84
|
+
if (promoteFlags['recreate-promotion-branch'] === true && !promoteFlags.yes) {
|
|
85
|
+
if (this.jsonEnabled() || !process.stdin.isTTY) {
|
|
86
|
+
this.error('Promotion branch recreation requires confirmation. Pass --yes for headless or JSON execution.', {
|
|
87
|
+
exit: 2,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const confirmed = await confirm({
|
|
91
|
+
default: false,
|
|
92
|
+
message: `Recreate the promotion branch for ${promotionId}? Existing promotion branch changes may be replaced.`,
|
|
93
|
+
});
|
|
94
|
+
if (!confirmed) {
|
|
95
|
+
this.error('Promotion canceled.', { exit: 1 });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
const result = await performPromotionRun({
|
|
99
|
+
deploymentStepIds: promoteFlags['deployment-step-id'],
|
|
100
|
+
hooks: {
|
|
101
|
+
log: (message) => this.display(() => this.log(message)),
|
|
102
|
+
withSpinner: (message, action) => this.withSpinner(message, action),
|
|
103
|
+
},
|
|
104
|
+
ignoredMetadata: parseIgnoredMetadataJson(promoteFlags['ignored-metadata-json']),
|
|
105
|
+
operation: promoteFlags.deploy ? 'merge_and_deploy' : 'merge',
|
|
106
|
+
promotionId,
|
|
107
|
+
recreatePromotionBranch: promoteFlags['recreate-promotion-branch'],
|
|
108
|
+
timeoutSeconds: promoteFlags['wait-timeout'],
|
|
109
|
+
wait: promoteFlags.wait,
|
|
110
|
+
});
|
|
111
|
+
const workResult = { ...result, workItemId };
|
|
112
|
+
this.display(() => {
|
|
113
|
+
this.log(`User Story: ${workItemId}`);
|
|
114
|
+
this.log(formatPromotionRunResult(result));
|
|
115
|
+
});
|
|
116
|
+
return workResult;
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error instanceof PromotionRunValidationError) {
|
|
120
|
+
this.error(error.message, { exit: 2 });
|
|
121
|
+
}
|
|
122
|
+
if (isPromotionRunExecutionError(error)) {
|
|
123
|
+
this.display(() => {
|
|
124
|
+
if (error.result !== undefined) {
|
|
125
|
+
this.log(`User Story: ${workItemId}`);
|
|
126
|
+
this.log(formatPromotionRunResult(error.result));
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
error.oclif = { exit: 1 };
|
|
130
|
+
}
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
toErrorJson(err) {
|
|
135
|
+
const base = super.toErrorJson(err);
|
|
136
|
+
return promotionRunErrorJson(base, err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=promote.js.map
|