agent-master-devsecops 0.0.0-stage → 3.9.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 (47) hide show
  1. package/.cursorignore +23 -0
  2. package/README.md +371 -3
  3. package/agents/AGENT_DEVSECOPS.md +417 -0
  4. package/agents/AGENT_ORCHESTRATOR.md +264 -0
  5. package/agents/AGENT_QA.md +186 -0
  6. package/agents/AGENT_SCRUM.md +190 -0
  7. package/agents/AGENT_SRE.md +173 -0
  8. package/agents/AGENT_UIUX.md +162 -0
  9. package/agents/compact/AGENT_ORCHESTRATOR_COMPACTO.md +53 -0
  10. package/docs/ARCHITECTURE.md +148 -0
  11. package/docs/CALCULATIONS.md +17 -0
  12. package/docs/CHANGELOG.md +49 -0
  13. package/docs/ENGINEERING_RULES.md +252 -0
  14. package/docs/IDEAS.md +17 -0
  15. package/docs/STATE.md +43 -0
  16. package/docs/playbooks/PLAYBOOK_CIRCUIT_BREAKER_LLM.md +58 -0
  17. package/docs/playbooks/PLAYBOOK_OPENTELEMETRY_TRACING.md +58 -0
  18. package/package.json +47 -6
  19. package/scripts/a11y_auditor.js +187 -0
  20. package/scripts/ast_skeletonizer.js +220 -0
  21. package/scripts/atlassian_client.js +238 -0
  22. package/scripts/atlassian_sync_action.js +269 -0
  23. package/scripts/bootstrap_cli.js +468 -0
  24. package/scripts/circuit_breaker.js +330 -0
  25. package/scripts/code_quality_auditor.js +142 -0
  26. package/scripts/confluence_sync.js +337 -0
  27. package/scripts/context_compressor.js +139 -0
  28. package/scripts/contract_test_validator.js +166 -0
  29. package/scripts/credentials_guard.js +216 -0
  30. package/scripts/domain_idea_miner.js +364 -0
  31. package/scripts/eval_harness.js +100 -0
  32. package/scripts/fast_feature_scaffold.js +338 -0
  33. package/scripts/git_hygiene_linter.js +186 -0
  34. package/scripts/hot_state_cache.js +95 -0
  35. package/scripts/jira_manager.js +408 -0
  36. package/scripts/magic_prompt_generator.js +205 -0
  37. package/scripts/mcp_server.js +1884 -0
  38. package/scripts/multi_agent_orchestrator.js +258 -0
  39. package/scripts/otel_tracer.js +228 -0
  40. package/scripts/perf_benchmark.js +153 -0
  41. package/scripts/pr_reviewer.js +373 -0
  42. package/scripts/sast_scanner.js +180 -0
  43. package/scripts/sca_dependency_auditor.js +167 -0
  44. package/scripts/smart_onboarding.js +610 -0
  45. package/scripts/sre_remediation_engine.js +174 -0
  46. package/scripts/universal_packager.js +313 -0
  47. package/scripts/web_inspector.js +355 -0
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/atlassian_client.js
4
+ *
5
+ * Cliente Unificado e Utilitários de Comunicação com APIs REST Atlassian (Jira e Confluence).
6
+ * Centraliza autenticação, sanitização de domínio, introspecção de git e execução de requisições HTTPS.
7
+ *
8
+ * Governança: Tríade Git + Jira + Confluence (Regra 11 e Regra 15).
9
+ */
10
+
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+ const https = require('node:https');
14
+ const { execSync, execFileSync } = require('node:child_process');
15
+
16
+ function sanitizeDomain(domainStr) {
17
+ if (!domainStr) return null;
18
+ return domainStr.replace(/^https?:\/\//, '').replace(/\/+$/, '');
19
+ }
20
+
21
+ function resolveMcpConfig() {
22
+ const homeDir = process.env.USERPROFILE || process.env.HOME || '';
23
+ const candidatePaths = [
24
+ path.join(homeDir, '.gemini', 'config', 'mcp_config.json'),
25
+ path.join(homeDir, '.antigravity', 'config', 'mcp_config.json'),
26
+ path.resolve(process.cwd(), '.mcp_config.json')
27
+ ];
28
+
29
+ for (const p of candidatePaths) {
30
+ if (fs.existsSync(p)) {
31
+ try {
32
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
33
+ } catch (_) {}
34
+ }
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function getAtlassianCredentials(targetDir = process.cwd()) {
40
+ let token = process.env.ATLASSIAN_API_TOKEN || process.env.JIRA_API_TOKEN || process.env.CONFLUENCE_API_TOKEN || process.env.PAT;
41
+ let email = process.env.ATLASSIAN_EMAIL || process.env.JIRA_EMAIL || process.env.CONFLUENCE_EMAIL || process.env.CONFLUENCE_USERNAME;
42
+ let rawDomain = process.env.ATLASSIAN_DOMAIN || process.env.JIRA_DOMAIN || process.env.CONFLUENCE_DOMAIN || process.env.JIRA_BASE_URL || process.env.CONFLUENCE_BASE_URL;
43
+ let source = (token && email && rawDomain) ? 'environment_variables' : null;
44
+
45
+ if (!process.env.IGNORE_LOCAL_ENV && (!token || !email || !rawDomain)) {
46
+ const isLocalWorkspace = path.resolve(targetDir) === path.resolve(process.cwd());
47
+ const candidateFiles = isLocalWorkspace
48
+ ? [
49
+ path.resolve(targetDir, '.env'),
50
+ path.resolve(targetDir, '..', '.env'),
51
+ path.resolve(process.cwd(), '.env'),
52
+ path.resolve(process.cwd(), '.env.local')
53
+ ]
54
+ : [
55
+ path.resolve(targetDir, '.env'),
56
+ path.resolve(targetDir, '.env.local')
57
+ ];
58
+
59
+ for (const envPath of candidateFiles) {
60
+ if (fs.existsSync(envPath)) {
61
+ try {
62
+ const raw = fs.readFileSync(envPath, 'utf8');
63
+ const dMatch = raw.match(/(?:ATLASSIAN_DOMAIN|JIRA_DOMAIN|CONFLUENCE_DOMAIN)=(.*)/);
64
+ const eMatch = raw.match(/(?:ATLASSIAN_EMAIL|JIRA_EMAIL|CONFLUENCE_EMAIL)=(.*)/);
65
+ const tMatch = raw.match(/(?:ATLASSIAN_API_TOKEN|JIRA_API_TOKEN|CONFLUENCE_API_TOKEN)=(.*)/);
66
+ if (dMatch && dMatch[1] && !rawDomain) rawDomain = dMatch[1].trim();
67
+ if (eMatch && eMatch[1] && !email) email = eMatch[1].trim();
68
+ if (tMatch && tMatch[1] && !token) token = tMatch[1].trim();
69
+ if (rawDomain && email && token) {
70
+ source = source || `local_file:${path.basename(envPath)}`;
71
+ break;
72
+ }
73
+ } catch (_) {}
74
+ }
75
+ }
76
+ }
77
+
78
+ const isLocalTarget = path.resolve(targetDir) === path.resolve(process.cwd());
79
+ if ((!token || !email || !rawDomain) && isLocalTarget) {
80
+ const mcp = resolveMcpConfig();
81
+ if (mcp && mcp.mcpServers) {
82
+ if (mcp.mcpServers.jira && mcp.mcpServers.jira.env) {
83
+ token = token || mcp.mcpServers.jira.env.JIRA_API_TOKEN;
84
+ email = email || mcp.mcpServers.jira.env.JIRA_EMAIL;
85
+ rawDomain = rawDomain || mcp.mcpServers.jira.env.JIRA_BASE_URL;
86
+ if (token && email && rawDomain) source = source || 'mcp_config:jira';
87
+ }
88
+ if (mcp.mcpServers.confluence && mcp.mcpServers.confluence.env) {
89
+ token = token || mcp.mcpServers.confluence.env.PAT;
90
+ if (token) source = source || 'mcp_config:confluence';
91
+ }
92
+ }
93
+ }
94
+
95
+ const cleanDomain = sanitizeDomain(rawDomain);
96
+ const isConfigured = Boolean(token && email && cleanDomain);
97
+ const auth = isConfigured ? Buffer.from(`${email}:${token}`).toString('base64') : null;
98
+
99
+ return {
100
+ domain: cleanDomain,
101
+ email: email || null,
102
+ token: token || null,
103
+ auth,
104
+ authBase64: auth,
105
+ isConfigured,
106
+ source
107
+ };
108
+ }
109
+
110
+ function checkGithubAuth(options = {}) {
111
+ const { globalStateCache } = require('./hot_state_cache.js');
112
+ const cacheKey = `gh_auth_${process.env.GITHUB_TOKEN || ''}_${process.env.GH_TOKEN || ''}`;
113
+ if (options && options.forceFresh) {
114
+ globalStateCache.invalidate(cacheKey);
115
+ }
116
+
117
+ return globalStateCache.wrap(cacheKey, () => {
118
+ let token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
119
+ let isAuthenticated = false;
120
+ let user = null;
121
+ let details = '';
122
+
123
+ try {
124
+ const stdout = execFileSync('gh', ['auth', 'status'], { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
125
+ isAuthenticated = true;
126
+ details = stdout.trim();
127
+ const userMatch = stdout.match(/Logged in to [^\s]+ account ([^\s\(\)]+)/i) || stdout.match(/account ([^\s\(\)]+)/i);
128
+ if (userMatch) user = userMatch[1];
129
+ } catch (err) {
130
+ const output = (err.stdout || '') + (err.stderr || '');
131
+ if (/Logged in to/i.test(output)) {
132
+ isAuthenticated = true;
133
+ details = output.trim();
134
+ const userMatch = output.match(/account ([^\s\(\)]+)/i);
135
+ if (userMatch) user = userMatch[1];
136
+ } else if (token) {
137
+ isAuthenticated = true;
138
+ user = 'token-authenticated';
139
+ details = 'Autenticado via GITHUB_TOKEN.';
140
+ } else {
141
+ isAuthenticated = false;
142
+ details = output.trim() || 'Usuário não autenticado no GitHub CLI.';
143
+ }
144
+ }
145
+
146
+ return {
147
+ installed: true,
148
+ authenticated: isAuthenticated,
149
+ isAuthenticated,
150
+ token: token || null,
151
+ user,
152
+ details
153
+ };
154
+ }, 10000); // 10s TTL
155
+ }
156
+
157
+ function getGitRepositoryUrl() {
158
+ if (process.env.GITHUB_REPOSITORY) {
159
+ return `https://github.com/${process.env.GITHUB_REPOSITORY}`;
160
+ }
161
+
162
+ try {
163
+ const rawRemote = execFileSync('git', ['config', '--get', 'remote.origin.url'], { encoding: 'utf8' }).trim();
164
+ if (rawRemote.startsWith('git@github.com:')) {
165
+ return `https://github.com/${rawRemote.replace('git@github.com:', '').replace(/\.git$/, '')}`;
166
+ } else if (rawRemote.startsWith('https://github.com/')) {
167
+ return rawRemote.replace(/\.git$/, '');
168
+ }
169
+ } catch (_) {}
170
+
171
+ return 'https://github.com/ContalApps/agent-master-devsecops';
172
+ }
173
+
174
+ function getGitMetadata(options = {}) {
175
+ const { globalStateCache } = require('./hot_state_cache.js');
176
+ if (options && options.forceFresh) {
177
+ globalStateCache.invalidate('git_metadata');
178
+ }
179
+
180
+ return globalStateCache.wrap('git_metadata', () => {
181
+ let sha = '';
182
+ let branch = '';
183
+ let message = '';
184
+
185
+ try {
186
+ sha = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
187
+ branch = execFileSync('git', ['rev-parse', '--abbrev-ref', 'HEAD'], { encoding: 'utf8' }).trim();
188
+ message = execFileSync('git', ['log', '-1', '--format=%s', sha], { encoding: 'utf8' }).trim();
189
+ } catch (_) {}
190
+
191
+ return {
192
+ sha,
193
+ branch,
194
+ message,
195
+ repoUrl: getGitRepositoryUrl()
196
+ };
197
+ }, 5000); // 5s TTL
198
+ }
199
+
200
+ async function requestAtlassian(credentials, method, endpointPath, bodyData = null) {
201
+ return new Promise((resolve, reject) => {
202
+ const postData = bodyData ? JSON.stringify(bodyData) : null;
203
+ const req = https.request({
204
+ hostname: credentials.domain,
205
+ path: endpointPath,
206
+ method,
207
+ headers: {
208
+ 'Authorization': `Basic ${credentials.auth || credentials.authBase64}`,
209
+ 'Content-Type': 'application/json',
210
+ 'Accept': 'application/json',
211
+ ...(postData ? { 'Content-Length': Buffer.byteLength(postData) } : {})
212
+ }
213
+ }, res => {
214
+ let data = '';
215
+ res.on('data', chunk => { data += chunk; });
216
+ res.on('end', () => {
217
+ try {
218
+ resolve({ status: res.statusCode, body: data ? JSON.parse(data) : {} });
219
+ } catch (_) {
220
+ resolve({ status: res.statusCode, body: data });
221
+ }
222
+ });
223
+ });
224
+
225
+ req.on('error', reject);
226
+ if (postData) req.write(postData);
227
+ req.end();
228
+ });
229
+ }
230
+
231
+ module.exports = {
232
+ sanitizeDomain,
233
+ getAtlassianCredentials,
234
+ checkGithubAuth,
235
+ getGitRepositoryUrl,
236
+ getGitMetadata,
237
+ requestAtlassian
238
+ };
@@ -0,0 +1,269 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * scripts/atlassian_sync_action.js
4
+ *
5
+ * Sincronizador Automático Git ↔ Atlassian via Actions & Local (AGDEV-23 / AGDEV-35).
6
+ * Conecta automaticamente commits, branches e Pull Requests aos cards do Jira
7
+ * utilizando a API oficial de Remote Links do Jira Cloud (REST API v3).
8
+ *
9
+ * Blindado pela Regra 15: Imutabilidade de Requisitos & Zero Falhas Silenciosas.
10
+ */
11
+
12
+ const { execFileSync } = require('child_process');
13
+ const path = require('path');
14
+ const {
15
+ getAtlassianConfig,
16
+ getGitMetadata,
17
+ getGitRepositoryUrl,
18
+ validateRule15
19
+ } = require('./credentials_guard');
20
+
21
+ function getAuth() {
22
+ const cfg = getAtlassianConfig();
23
+ return cfg.authBase64;
24
+ }
25
+
26
+ function extractIssueKeys(text) {
27
+ if (!text) return [];
28
+ const matches = text.match(/\b[A-Z]{2,10}-\d+\b/g) || [];
29
+ return [...new Set(matches.map(k => k.toUpperCase()))];
30
+ }
31
+
32
+ function extractPrNumber(text) {
33
+ if (!text) return null;
34
+ const match = text.match(/#(\d+)\b/) || text.match(/pull request #(\d+)/i);
35
+ return match ? match[1] : null;
36
+ }
37
+
38
+ function formatRemoteLinkPayload(url, title, type = 'commit', globalId = null, summary = null) {
39
+ let iconUrl = 'https://github.githubassets.com/favicons/favicon.png';
40
+ let relationship = 'causes';
41
+
42
+ if (type === 'pr') {
43
+ relationship = 'relates to';
44
+ } else if (type === 'branch') {
45
+ relationship = 'builds';
46
+ }
47
+
48
+ const payload = {
49
+ relationship,
50
+ object: {
51
+ url,
52
+ title,
53
+ icon: {
54
+ url16x16: iconUrl,
55
+ title: `GitHub ${type.toUpperCase()}`
56
+ }
57
+ }
58
+ };
59
+
60
+ if (globalId) {
61
+ payload.globalId = globalId;
62
+ }
63
+ if (summary) {
64
+ payload.object.summary = summary;
65
+ }
66
+
67
+ return payload;
68
+ }
69
+
70
+ async function linkUrlToJira(issueKey, url, title, type = 'commit', authOverride = null, globalId = null, summary = null) {
71
+ const atlassian = getAtlassianConfig();
72
+ const auth = authOverride || atlassian.authBase64;
73
+ if (!auth) {
74
+ return { status: 'FAILED', message: 'Credenciais Atlassian não configuradas no ambiente (Regra 15).' };
75
+ }
76
+
77
+ const cleanDomain = atlassian.domain;
78
+ if (!cleanDomain) {
79
+ return { status: 'FAILED', message: 'Domínio Atlassian não configurado para este projeto (Regra 15).' };
80
+ }
81
+ const payload = formatRemoteLinkPayload(url, title, type, globalId, summary);
82
+ const endpoint = `https://${cleanDomain}/rest/api/3/issue/${issueKey}/remotelink`;
83
+
84
+ try {
85
+ const res = await fetch(endpoint, {
86
+ method: 'POST',
87
+ headers: {
88
+ 'Authorization': `Basic ${auth}`,
89
+ 'Content-Type': 'application/json',
90
+ 'Accept': 'application/json'
91
+ },
92
+ body: JSON.stringify(payload)
93
+ });
94
+
95
+ if (res.ok || res.status === 201 || res.status === 200) {
96
+ return { status: 'SUCCESS', issueKey, url, type, httpStatus: res.status };
97
+ } else {
98
+ const errText = await res.text();
99
+ return { status: 'FAILED', issueKey, error: `HTTP ${res.status}: ${errText}` };
100
+ }
101
+ } catch (err) {
102
+ return { status: 'ERROR', issueKey, error: err.message };
103
+ }
104
+ }
105
+
106
+ async function backfillHistory(limit = 30) {
107
+ console.log(`[Atlassian Sync] Iniciando sincronização retroativa dos últimos ${limit} commits...`);
108
+ const atlassian = getAtlassianConfig();
109
+ if (!atlassian.isConfigured) {
110
+ throw new Error('Credenciais Atlassian ausentes para sincronização retroativa.');
111
+ }
112
+
113
+ const repoUrl = getGitRepositoryUrl();
114
+ const repoSlug = repoUrl.replace('https://github.com/', '');
115
+
116
+ let rawLog = '';
117
+ try {
118
+ rawLog = execFileSync('git', ['log', `-${limit}`, '--format=%H|%s'], { encoding: 'utf8' });
119
+ } catch (err) {
120
+ console.error('[Atlassian Sync] Falha ao extrair histórico git:', err.message);
121
+ return;
122
+ }
123
+
124
+ const lines = rawLog.split('\n').filter(l => l.trim().length > 0);
125
+ const results = [];
126
+
127
+ for (const line of lines) {
128
+ const [sha, ...subjectParts] = line.split('|');
129
+ const subject = subjectParts.join('|');
130
+ const keys = extractIssueKeys(subject);
131
+ const prNumber = extractPrNumber(subject);
132
+
133
+ for (const key of keys) {
134
+ // 1. Link Commit
135
+ const commitGlobalId = `system=github&repo=${repoSlug}&type=commit&sha=${sha}`;
136
+ const commitRes = await linkUrlToJira(
137
+ key,
138
+ `${repoUrl}/commit/${sha}`,
139
+ `GitHub Commit: ${sha.slice(0, 7)} (${subject})`,
140
+ 'commit',
141
+ atlassian.authBase64,
142
+ commitGlobalId
143
+ );
144
+ results.push({ key, type: 'commit', ref: sha.slice(0, 7), status: commitRes.status });
145
+
146
+ // 2. Link PR if present in commit title
147
+ if (prNumber) {
148
+ const prGlobalId = `system=github&repo=${repoSlug}&type=pr&id=${prNumber}`;
149
+ const prRes = await linkUrlToJira(
150
+ key,
151
+ `${repoUrl}/pull/${prNumber}`,
152
+ `GitHub Pull Request #${prNumber}`,
153
+ 'pr',
154
+ atlassian.authBase64,
155
+ prGlobalId,
156
+ subject
157
+ );
158
+ results.push({ key, type: 'pr', ref: `#${prNumber}`, status: prRes.status });
159
+ }
160
+ }
161
+ }
162
+
163
+ const successCount = results.filter(r => r.status === 'SUCCESS').length;
164
+ console.log(`[Atlassian Sync] Sincronização retroativa concluída: ${successCount}/${results.length} links processados com sucesso.`);
165
+ return results;
166
+ }
167
+
168
+ async function runCli() {
169
+ const isCI = process.env.GITHUB_ACTIONS === 'true';
170
+
171
+ // Validação Compulsória de Requisitos (Regra 15)
172
+ // Em CI, a ausência de secrets agora é ERRO CRÍTICO para impedir perda silenciosa
173
+ const validation = validateRule15({ failFast: isCI, requireGitHub: false, requireAtlassian: true });
174
+ if (!validation.valid) {
175
+ console.error(`[Atlassian Sync] Falha de validação da Regra 15:\n ${validation.missing.join(', ')}`);
176
+ console.error(` Remediação: ${validation.remediation.join('\n ')}`);
177
+ if (isCI) {
178
+ process.exit(1);
179
+ }
180
+ return;
181
+ }
182
+
183
+ const args = process.argv.slice(2);
184
+ if (args.includes('--retroactive') || args.includes('-r')) {
185
+ const limitIdx = args.findIndex(a => a === '--retroactive' || a === '-r');
186
+ const limitVal = parseInt(args[limitIdx + 1], 10) || 30;
187
+ await backfillHistory(limitVal);
188
+ return;
189
+ }
190
+
191
+ const atlassian = getAtlassianConfig();
192
+ const gitMeta = getGitMetadata();
193
+ const repoUrl = gitMeta.repoUrl;
194
+ const repoSlug = repoUrl.replace('https://github.com/', '');
195
+
196
+ const sha = gitMeta.sha;
197
+ const branch = gitMeta.branch;
198
+ const message = gitMeta.message;
199
+ const prNumber = process.env.PR_NUMBER || extractPrNumber(process.env.PR_TITLE || '') || extractPrNumber(message);
200
+
201
+ const combined = `${branch} ${message} ${process.env.PR_TITLE || ''}`;
202
+ const keys = extractIssueKeys(combined);
203
+
204
+ if (keys.length === 0) {
205
+ console.log('[Atlassian Sync] Nenhuma chave Jira encontrada na branch ou mensagem de commit.');
206
+ return;
207
+ }
208
+
209
+ console.log(`[Atlassian Sync] Chaves Jira identificadas: ${keys.join(', ')}`);
210
+
211
+ for (const key of keys) {
212
+ if (sha && repoUrl) {
213
+ const commitGlobalId = `system=github&repo=${repoSlug}&type=commit&sha=${sha}`;
214
+ const commitRes = await linkUrlToJira(
215
+ key,
216
+ `${repoUrl}/commit/${sha}`,
217
+ `GitHub Commit: ${sha.slice(0, 7)} (${message})`,
218
+ 'commit',
219
+ atlassian.authBase64,
220
+ commitGlobalId
221
+ );
222
+ console.log(`[Atlassian Sync] Commit ${sha.slice(0, 7)} vinculado a ${key}: ${commitRes.status}`);
223
+ }
224
+
225
+ if (branch && branch !== 'main' && branch !== 'HEAD' && repoUrl) {
226
+ const branchGlobalId = `system=github&repo=${repoSlug}&type=branch&name=${branch}`;
227
+ const branchRes = await linkUrlToJira(
228
+ key,
229
+ `${repoUrl}/tree/${branch}`,
230
+ `GitHub Branch: ${branch}`,
231
+ 'branch',
232
+ atlassian.authBase64,
233
+ branchGlobalId
234
+ );
235
+ console.log(`[Atlassian Sync] Branch ${branch} vinculada a ${key}: ${branchRes.status}`);
236
+ }
237
+
238
+ if (prNumber && repoUrl) {
239
+ const prGlobalId = `system=github&repo=${repoSlug}&type=pr&id=${prNumber}`;
240
+ const prRes = await linkUrlToJira(
241
+ key,
242
+ `${repoUrl}/pull/${prNumber}`,
243
+ `GitHub Pull Request #${prNumber}`,
244
+ 'pr',
245
+ atlassian.authBase64,
246
+ prGlobalId,
247
+ process.env.PR_TITLE || message
248
+ );
249
+ console.log(`[Atlassian Sync] PR #${prNumber} vinculado a ${key}: ${prRes.status}`);
250
+ }
251
+ }
252
+ }
253
+
254
+ if (require.main === module) {
255
+ runCli().catch(err => {
256
+ console.error('[Atlassian Sync] Erro fatal na execução:', err);
257
+ process.exit(1);
258
+ });
259
+ }
260
+
261
+ module.exports = {
262
+ getAuth,
263
+ extractIssueKeys,
264
+ extractPrNumber,
265
+ formatRemoteLinkPayload,
266
+ linkUrlToJira,
267
+ backfillHistory,
268
+ runCli
269
+ };