create-agentic-pdlc 2.4.0 → 3.0.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.
- package/.agentic-pdlc/hooks/pdlc-stage-gate.sh +37 -10
- package/.claude/settings.json +18 -0
- package/.coderabbit.yaml +35 -0
- package/.github/workflows/project-automation.yml +13 -67
- package/CLAUDE.md +1 -1
- package/README.md +33 -32
- package/adapters/claude-code/skill.md +7 -3
- package/bin/cli.js +549 -209
- package/docs/superpowers/plans/2026-06-04-spec-format-issue-template.md +160 -0
- package/docs/superpowers/plans/2026-06-04-two-tier-installer.md +1056 -0
- package/docs/superpowers/specs/2026-06-04-spec-format-issue-template-design.md +46 -0
- package/package.json +2 -2
- package/templates/full/CLAUDE.md +30 -0
- package/templates/lite/AGENTS.md +121 -0
- package/templates/lite/CLAUDE.md +44 -0
- package/tests/cli.test.js +32 -0
- package/.github/workflows/agentic-metrics.yml +0 -545
- package/.github/workflows/qa-agent.yml +0 -139
- package/.github/workflows/qa-gate.yml +0 -51
- /package/templates/{AGENTS.md → full/AGENTS.md} +0 -0
- /package/templates/{docs → full/docs}/pdlc.md +0 -0
package/bin/cli.js
CHANGED
|
@@ -10,12 +10,12 @@ const targetDir = process.cwd();
|
|
|
10
10
|
// The directory where this script sits (globally/locally in node_modules)
|
|
11
11
|
const sourceDir = path.join(__dirname, '..');
|
|
12
12
|
|
|
13
|
-
const rl =
|
|
14
|
-
input: process.stdin,
|
|
15
|
-
|
|
16
|
-
});
|
|
13
|
+
const rl = require.main === module
|
|
14
|
+
? readline.createInterface({ input: process.stdin, output: process.stdout })
|
|
15
|
+
: null;
|
|
17
16
|
|
|
18
17
|
function askQuestion(query) {
|
|
18
|
+
if (!rl) throw new Error('askQuestion called in non-interactive context');
|
|
19
19
|
return new Promise(resolve => rl.question(query, resolve));
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -70,11 +70,16 @@ const i18n = {
|
|
|
70
70
|
update_jules_ask_handle: t(' Agent handle (e.g. @my-agent): ', ' Handle do agente (ex: @meu-agente): ', ' Handle del agente (ej: @mi-agente): '),
|
|
71
71
|
update_qa_header: t('— QA Agent (AC verification via GitHub Models — zero secrets) —', '— QA Agent (verificação de ACs via GitHub Models — zero secrets) —', '— QA Agent (verificación de ACs via GitHub Models — zero secrets) —'),
|
|
72
72
|
update_qa_ask: t(' Activate? Uses GITHUB_TOKEN — no extra secrets needed. (Y/n): ', ' Ativar? Usa GITHUB_TOKEN — nenhum secret extra necessário. (S/n): ', ' ¿Activar? Usa GITHUB_TOKEN — sin secrets adicionales. (S/n): '),
|
|
73
|
-
update_sentinel_header: t('— Sentinel (architecture
|
|
74
|
-
update_sentinel_ask: t(
|
|
73
|
+
update_sentinel_header: t('— Sentinel (architecture-violation label → board automation) —', '— Sentinel (label architecture-violation → automação de board) —', '— Sentinel (label architecture-violation → automatización de board) —'),
|
|
74
|
+
update_sentinel_ask: t(" Activate? CodeRabbit applies 'architecture-violation' label when violations are detected. (Y/n): ", " Ativar? CodeRabbit aplica a label 'architecture-violation' quando detecta violações. (S/n): ", " ¿Activar? CodeRabbit aplica la etiqueta 'architecture-violation' cuando detecta violaciones. (S/n): "),
|
|
75
75
|
configuring_protection: t('[3/3] Configuring branch protection...', '[3/3] Configurando proteção de branch...', '[3/3] Configurando protección de rama...'),
|
|
76
76
|
protection_ok: t('✅ Branch protection set — required checks: PDLC Stage Gate, QA Gate.', '✅ Proteção de branch configurada — checks obrigatórios: PDLC Stage Gate, QA Gate.', '✅ Protección de rama configurada — checks requeridos: PDLC Stage Gate, QA Gate.'),
|
|
77
77
|
protection_warn: t('⚠️ Branch protection could not be set automatically.\n Set required checks manually: Settings → Branches → main → Required status checks.\n Required: "PDLC Stage Gate" and "QA Gate"', '⚠️ Proteção de branch não pôde ser configurada automaticamente.\n Configure manualmente: Settings → Branches → main → Required status checks.\n Obrigatórios: "PDLC Stage Gate" e "QA Gate"', '⚠️ No se pudo configurar la protección de rama automáticamente.\n Configúralo en: Settings → Branches → main → Required status checks.\n Requeridos: "PDLC Stage Gate" y "QA Gate"'),
|
|
78
|
+
issue_templates_copied: t(
|
|
79
|
+
'✅ Issue templates copied to .github/ISSUE_TEMPLATE/',
|
|
80
|
+
'✅ Issue templates copiados para .github/ISSUE_TEMPLATE/',
|
|
81
|
+
'✅ Issue templates copiados a .github/ISSUE_TEMPLATE/'
|
|
82
|
+
),
|
|
78
83
|
};
|
|
79
84
|
|
|
80
85
|
const cyan = '\x1b[36m';
|
|
@@ -83,9 +88,21 @@ const green = '\x1b[32m';
|
|
|
83
88
|
const yellow = '\x1b[33m';
|
|
84
89
|
const red = '\x1b[31m';
|
|
85
90
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
91
|
+
const PDLC_LABELS = [
|
|
92
|
+
{ name: 'stage:exploration', color: '9b59b6', description: 'Issue is being evaluated' },
|
|
93
|
+
{ name: 'stage:brainstorming', color: 'e84393', description: 'Proposed approaches awaiting PM gate' },
|
|
94
|
+
{ name: 'stage:detailing', color: '3498db', description: 'Technical spec is being written' },
|
|
95
|
+
{ name: 'stage:development', color: 'e67e22', description: 'Agent is implementing the spec' },
|
|
96
|
+
{ name: 'stage:testing', color: '8e44ad', description: 'Agent is testing the implementation' },
|
|
97
|
+
{ name: 'spec:approved', color: '0e8a16', description: 'Spec approved — agent can implement' },
|
|
98
|
+
{ name: 'pr:in-review', color: 'e4e669', description: 'PR awaiting code review' },
|
|
99
|
+
{ name: 'pr:approved', color: '0e8a16', description: 'PR approved, ready for merge' },
|
|
100
|
+
{ name: 'architecture-violation', color: 'd93f0b', description: 'Invariant violation detected by CI' },
|
|
101
|
+
{ name: 'qa:approved', color: '0e8a16', description: 'QA Agent approved the implementation' },
|
|
102
|
+
{ name: 'qa:needs-work', color: 'd93f0b', description: 'QA Agent found issues' },
|
|
103
|
+
{ name: 'infra:qa-broken', color: 'F97316', description: 'QA Agent failed to run — manual review required' },
|
|
104
|
+
{ name: 'jules', color: '5319e7', description: 'Jules AI Agent' }
|
|
105
|
+
];
|
|
89
106
|
|
|
90
107
|
function buildBoardUrl(repoOwner, projectNumber, isOrg) {
|
|
91
108
|
const segment = isOrg ? 'orgs' : 'users';
|
|
@@ -118,7 +135,9 @@ function printSetupDone() {
|
|
|
118
135
|
console.log(`${green}${sep}${reset}\n`);
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
|
|
138
|
+
// ─── Shared helper functions ──────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
async function checkGhAuth() {
|
|
122
141
|
console.log(`${yellow}${i18n.checking_gh}${reset}`);
|
|
123
142
|
try {
|
|
124
143
|
execSync('gh auth status', { stdio: 'ignore' });
|
|
@@ -126,46 +145,236 @@ async function runSetup() {
|
|
|
126
145
|
} catch (error) {
|
|
127
146
|
console.error(`${red}${i18n.gh_error}${reset}`);
|
|
128
147
|
console.error(`${i18n.gh_install}`);
|
|
148
|
+
rl.close();
|
|
129
149
|
process.exit(1);
|
|
130
150
|
}
|
|
151
|
+
}
|
|
131
152
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
153
|
+
function getScopes() {
|
|
154
|
+
try {
|
|
155
|
+
const out = execFileSync('gh', ['api', 'user', '-i'], { stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' });
|
|
156
|
+
const line = out.split('\n').find(l => l.toLowerCase().startsWith('x-oauth-scopes:'));
|
|
157
|
+
return line ? line.split(':').slice(1).join(':').split(',').map(s => s.trim()) : [];
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return [];
|
|
140
160
|
}
|
|
161
|
+
}
|
|
141
162
|
|
|
163
|
+
async function checkAndRefreshProjectScope() {
|
|
142
164
|
const scopesBefore = getScopes();
|
|
143
|
-
if (scopesBefore.length
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
165
|
+
if (scopesBefore.length === 0 || scopesBefore.includes('project')) return;
|
|
166
|
+
|
|
167
|
+
console.log(`${yellow}⚠️ Token missing 'project' scope — required for GitHub Projects board.${reset}`);
|
|
168
|
+
console.log(`${yellow} Refreshing token now (browser may open)...${reset}\n`);
|
|
169
|
+
try {
|
|
170
|
+
execSync('gh auth refresh -h github.com -s project', { stdio: 'inherit' });
|
|
171
|
+
} catch (e) {
|
|
172
|
+
console.log(`${red}❌ Token refresh failed. Run manually: gh auth refresh -h github.com -s project${reset}`);
|
|
173
|
+
rl.close();
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
const scopesAfter = getScopes();
|
|
177
|
+
if (scopesAfter.length > 0 && !scopesAfter.includes('project')) {
|
|
178
|
+
console.log(`\n${red}❌ 'project' scope still missing after refresh.${reset}`);
|
|
179
|
+
console.log(`${yellow} Active scopes: ${scopesAfter.join(', ')}${reset}`);
|
|
180
|
+
console.log(`${yellow} Try manually: gh auth refresh -h github.com -s project${reset}`);
|
|
181
|
+
rl.close();
|
|
182
|
+
process.exit(1);
|
|
183
|
+
}
|
|
184
|
+
if (scopesAfter.length > 0) {
|
|
185
|
+
console.log(`\n${green}✅ Token refreshed. Active scopes: ${scopesAfter.join(', ')}${reset}\n`);
|
|
186
|
+
} else {
|
|
187
|
+
console.log(`\n${green}✅ Token refreshed with 'project' scope.${reset}\n`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function installHook(sourceDir, targetDir) {
|
|
192
|
+
const hookSrc = path.join(sourceDir, 'adapters', 'hooks', 'pdlc-stage-gate.sh');
|
|
193
|
+
const hookDir = path.join(targetDir, '.agentic-pdlc', 'hooks');
|
|
194
|
+
const hookDest = path.join(hookDir, 'pdlc-stage-gate.sh');
|
|
195
|
+
if (!fs.existsSync(hookSrc)) return;
|
|
196
|
+
|
|
197
|
+
fs.mkdirSync(hookDir, { recursive: true });
|
|
198
|
+
fs.copyFileSync(hookSrc, hookDest);
|
|
199
|
+
fs.chmodSync(hookDest, '755');
|
|
200
|
+
|
|
201
|
+
const settingsDir = path.join(targetDir, '.claude');
|
|
202
|
+
const settingsPath = path.join(settingsDir, 'settings.json');
|
|
203
|
+
if (!fs.existsSync(settingsPath)) {
|
|
204
|
+
fs.mkdirSync(settingsDir, { recursive: true });
|
|
205
|
+
fs.writeFileSync(settingsPath, JSON.stringify({
|
|
206
|
+
hooks: {
|
|
207
|
+
PreToolUse: [{
|
|
208
|
+
matcher: 'Bash',
|
|
209
|
+
hooks: [{ type: 'command', command: 'bash .agentic-pdlc/hooks/pdlc-stage-gate.sh' }]
|
|
210
|
+
}]
|
|
211
|
+
}
|
|
212
|
+
}, null, 2) + '\n');
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function setBranchProtection(repo, requiredChecks) {
|
|
217
|
+
console.log(`\n${cyan}${i18n.configuring_protection}${reset}`);
|
|
218
|
+
try {
|
|
219
|
+
const defaultBranch = execFileSync(
|
|
220
|
+
'gh', ['api', `repos/${repo}`, '--jq', '.default_branch'],
|
|
221
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }
|
|
222
|
+
).trim() || 'main';
|
|
223
|
+
|
|
224
|
+
const protectionPayload = JSON.stringify({
|
|
225
|
+
required_status_checks: { strict: false, contexts: requiredChecks },
|
|
226
|
+
enforce_admins: false,
|
|
227
|
+
required_pull_request_reviews: null,
|
|
228
|
+
restrictions: null
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
execFileSync(
|
|
232
|
+
'gh',
|
|
233
|
+
['api', `repos/${repo}/branches/${defaultBranch}/protection`, '--method', 'PUT', '--input', '-'],
|
|
234
|
+
{ input: protectionPayload, stdio: ['pipe', 'ignore', 'pipe'] }
|
|
235
|
+
);
|
|
236
|
+
console.log(` ${green}${i18n.protection_ok}${reset}`);
|
|
237
|
+
} catch (_) {
|
|
238
|
+
console.log(` ${yellow}${i18n.protection_warn}${reset}`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function copyAdapterFiles(agent, sourceDir, targetDir) {
|
|
243
|
+
const claudeSetupSrc = path.join(sourceDir, 'adapters', 'claude-code', 'skill.md');
|
|
244
|
+
const cursorSetupSrc = path.join(sourceDir, 'adapters', 'cursor', 'rules.md');
|
|
245
|
+
|
|
246
|
+
if (agent === 'cursor') {
|
|
247
|
+
if (fs.existsSync(cursorSetupSrc)) {
|
|
248
|
+
fs.copyFileSync(cursorSetupSrc, path.join(targetDir, '.cursorrules'));
|
|
249
|
+
console.log(`${i18n.cursor_rules_written}`);
|
|
152
250
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
251
|
+
}
|
|
252
|
+
if (fs.existsSync(claudeSetupSrc)) {
|
|
253
|
+
fs.copyFileSync(claudeSetupSrc, path.join(targetDir, '.agentic-setup.md'));
|
|
254
|
+
console.log(`${i18n.setup_written}`);
|
|
255
|
+
printSetupDone();
|
|
256
|
+
} else {
|
|
257
|
+
console.error(`${i18n.missing_claude}${claudeSetupSrc}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function scaffoldLiteTemplates(sourceDir, targetDir) {
|
|
262
|
+
const destTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
|
|
263
|
+
fs.mkdirSync(destTemplates, { recursive: true });
|
|
264
|
+
|
|
265
|
+
// CLAUDE.md — lite version
|
|
266
|
+
const liteClaudeSrc = path.join(sourceDir, 'templates', 'lite', 'CLAUDE.md');
|
|
267
|
+
if (fs.existsSync(liteClaudeSrc)) {
|
|
268
|
+
fs.copyFileSync(liteClaudeSrc, path.join(destTemplates, 'CLAUDE.md'));
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// AGENTS.md — lite version
|
|
272
|
+
const liteAgentsSrc = path.join(sourceDir, 'templates', 'lite', 'AGENTS.md');
|
|
273
|
+
if (fs.existsSync(liteAgentsSrc)) {
|
|
274
|
+
fs.copyFileSync(liteAgentsSrc, path.join(destTemplates, 'AGENTS.md'));
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Issue templates — shared between lite and full
|
|
278
|
+
const issueTemplateSrc = path.join(sourceDir, 'templates', '.github', 'ISSUE_TEMPLATE');
|
|
279
|
+
const issueTemplateDest = path.join(destTemplates, '.github', 'ISSUE_TEMPLATE');
|
|
280
|
+
if (fs.existsSync(issueTemplateSrc)) {
|
|
281
|
+
copyDirSync(issueTemplateSrc, issueTemplateDest);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName) {
|
|
286
|
+
const destTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
|
|
287
|
+
fs.mkdirSync(destTemplates, { recursive: true });
|
|
288
|
+
|
|
289
|
+
// CLAUDE.md — concatenate lite + full addon
|
|
290
|
+
const liteClaudeSrc = path.join(sourceDir, 'templates', 'lite', 'CLAUDE.md');
|
|
291
|
+
const fullClaudeSrc = path.join(sourceDir, 'templates', 'full', 'CLAUDE.md');
|
|
292
|
+
if (fs.existsSync(liteClaudeSrc) && fs.existsSync(fullClaudeSrc)) {
|
|
293
|
+
const combined = fs.readFileSync(liteClaudeSrc, 'utf8') + '\n' + fs.readFileSync(fullClaudeSrc, 'utf8');
|
|
294
|
+
fs.writeFileSync(path.join(destTemplates, 'CLAUDE.md'), combined);
|
|
295
|
+
} else if (fs.existsSync(liteClaudeSrc)) {
|
|
296
|
+
fs.copyFileSync(liteClaudeSrc, path.join(destTemplates, 'CLAUDE.md'));
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// AGENTS.md — full version
|
|
300
|
+
const fullAgentsSrc = path.join(sourceDir, 'templates', 'full', 'AGENTS.md');
|
|
301
|
+
if (fs.existsSync(fullAgentsSrc)) {
|
|
302
|
+
fs.copyFileSync(fullAgentsSrc, path.join(destTemplates, 'AGENTS.md'));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// All of templates/.github/ (issue templates + workflows)
|
|
306
|
+
const githubSrc = path.join(sourceDir, 'templates', '.github');
|
|
307
|
+
const githubDest = path.join(destTemplates, '.github');
|
|
308
|
+
if (fs.existsSync(githubSrc)) {
|
|
309
|
+
copyDirSync(githubSrc, githubDest);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// docs/pdlc.md — substitute board IDs
|
|
313
|
+
const pdlcSrc = path.join(sourceDir, 'templates', 'full', 'docs', 'pdlc.md');
|
|
314
|
+
const pdlcDest = path.join(destTemplates, 'docs', 'pdlc.md');
|
|
315
|
+
if (fs.existsSync(pdlcSrc)) {
|
|
316
|
+
fs.mkdirSync(path.join(destTemplates, 'docs'), { recursive: true });
|
|
317
|
+
let pdlcContent = fs.readFileSync(pdlcSrc, 'utf8');
|
|
318
|
+
if (projectId) pdlcContent = pdlcContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
|
|
319
|
+
if (statusFieldId) pdlcContent = pdlcContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
|
|
320
|
+
pdlcContent = pdlcContent.replace(/\{\{REPO_OWNER\}\}/g, () => repoOwner);
|
|
321
|
+
pdlcContent = pdlcContent.replace(/\{\{REPO_NAME\}\}/g, () => repoName);
|
|
322
|
+
if (Object.keys(optionMap).length > 0) {
|
|
323
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_IDEA\}\}/g, () => optionMap['💡 Idea - No move to Exploration directly'] || 'MISSING_ID');
|
|
324
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap['🔍 Exploration'] || 'MISSING_ID');
|
|
325
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_BRAINSTORMING\}\}/g, () => optionMap['🧠 Brainstorming'] || 'MISSING_ID');
|
|
326
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_DETAIL\}\}/g, () => optionMap['📐 Detail Solution'] || 'MISSING_ID');
|
|
327
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_APPROVAL\}\}/g, () => optionMap['✅ Approval'] || 'MISSING_ID');
|
|
328
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_DEVELOPMENT\}\}/g, () => optionMap['⚙️ Development'] || 'MISSING_ID');
|
|
329
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_TESTING\}\}/g, () => optionMap['🧪 Testing'] || 'MISSING_ID');
|
|
330
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_CODE_REVIEW_PR\}\}/g, () => optionMap['👁 Code Review / PR'] || 'MISSING_ID');
|
|
331
|
+
pdlcContent = pdlcContent.replace(/\{\{ID_READY_FOR_PRODUCTION\}\}/g,() => optionMap['🚀 Ready for Production']|| 'MISSING_ID');
|
|
161
332
|
}
|
|
162
|
-
|
|
163
|
-
|
|
333
|
+
fs.writeFileSync(pdlcDest, pdlcContent);
|
|
334
|
+
if (projectId && statusFieldId && Object.keys(optionMap).length > 0) {
|
|
335
|
+
console.log(`${i18n.pdlc_prefilled}`);
|
|
164
336
|
} else {
|
|
165
|
-
console.log(
|
|
337
|
+
console.log(`${yellow}⚠️ pdlc.md copied — Project IDs not filled (board creation failed). Re-run after fixing token.${reset}`);
|
|
166
338
|
}
|
|
167
339
|
}
|
|
168
340
|
|
|
341
|
+
// project-automation.yml — substitute IDs
|
|
342
|
+
const paPath = path.join(destTemplates, '.github', 'workflows', 'project-automation.yml');
|
|
343
|
+
if (fs.existsSync(paPath) && Object.keys(optionMap).length > 0) {
|
|
344
|
+
let wfContent = fs.readFileSync(paPath, 'utf8');
|
|
345
|
+
if (projectId) wfContent = wfContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
|
|
346
|
+
if (statusFieldId) wfContent = wfContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
|
|
347
|
+
wfContent = wfContent.replace(/\{\{ID_IDEA\}\}/g, () => optionMap['💡 Idea - No move to Exploration directly'] || 'MISSING_ID');
|
|
348
|
+
wfContent = wfContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap['🔍 Exploration'] || 'MISSING_ID');
|
|
349
|
+
wfContent = wfContent.replace(/\{\{ID_BRAINSTORMING\}\}/g, () => optionMap['🧠 Brainstorming'] || 'MISSING_ID');
|
|
350
|
+
wfContent = wfContent.replace(/\{\{ID_DETAILING\}\}/g, () => optionMap['📐 Detail Solution'] || 'MISSING_ID');
|
|
351
|
+
wfContent = wfContent.replace(/\{\{ID_APPROVAL\}\}/g, () => optionMap['✅ Approval'] || 'MISSING_ID');
|
|
352
|
+
wfContent = wfContent.replace(/\{\{ID_DEVELOPMENT\}\}/g, () => optionMap['⚙️ Development'] || 'MISSING_ID');
|
|
353
|
+
wfContent = wfContent.replace(/\{\{ID_TESTING\}\}/g, () => optionMap['🧪 Testing'] || 'MISSING_ID');
|
|
354
|
+
wfContent = wfContent.replace(/\{\{ID_CODE_REVIEW_PR\}\}/g, () => optionMap['👁 Code Review / PR'] || 'MISSING_ID');
|
|
355
|
+
wfContent = wfContent.replace(/\{\{ID_PRODUCTION\}\}/g, () => optionMap['🚀 Ready for Production']|| 'MISSING_ID');
|
|
356
|
+
fs.writeFileSync(paPath, wfContent);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
console.log(`${i18n.templates_copied}`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function writeCliContext(targetDir, profile, data) {
|
|
363
|
+
try {
|
|
364
|
+
const contextPath = path.join(targetDir, '.agentic-pdlc', 'cli-context.json');
|
|
365
|
+
fs.mkdirSync(path.join(targetDir, '.agentic-pdlc'), { recursive: true });
|
|
366
|
+
fs.writeFileSync(contextPath, JSON.stringify({ profile, ...data }, null, 2));
|
|
367
|
+
} catch (_) {
|
|
368
|
+
// Non-fatal — agent will ask for the values instead
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─── runFullSetup ─────────────────────────────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
async function runFullSetup() {
|
|
375
|
+
await checkGhAuth();
|
|
376
|
+
await checkAndRefreshProjectScope();
|
|
377
|
+
|
|
169
378
|
const agentAnswer = await askQuestion(i18n.ask_agent);
|
|
170
379
|
const agent = agentAnswer.trim().toLowerCase();
|
|
171
380
|
if (!['claude', 'cursor', 'copilot'].includes(agent)) {
|
|
@@ -204,21 +413,7 @@ async function runSetup() {
|
|
|
204
413
|
console.log(`\n${yellow}${i18n.starting_setup}${reset}`);
|
|
205
414
|
|
|
206
415
|
// Labels
|
|
207
|
-
const labels =
|
|
208
|
-
{ name: 'stage:exploration', color: '9b59b6', description: 'Issue is being evaluated' },
|
|
209
|
-
{ name: 'stage:brainstorming', color: 'e84393', description: 'Proposed approaches awaiting PM gate' },
|
|
210
|
-
{ name: 'stage:detailing', color: '3498db', description: 'Technical spec is being written' },
|
|
211
|
-
{ name: 'stage:development', color: 'e67e22', description: 'Agent is implementing the spec' },
|
|
212
|
-
{ name: 'stage:testing', color: '8e44ad', description: 'Agent is testing the implementation' },
|
|
213
|
-
{ name: 'spec:approved', color: '0e8a16', description: 'Spec approved — agent can implement' },
|
|
214
|
-
{ name: 'pr:in-review', color: 'e4e669', description: 'PR awaiting code review' },
|
|
215
|
-
{ name: 'pr:approved', color: '0e8a16', description: 'PR approved, ready for merge' },
|
|
216
|
-
{ name: 'architecture-violation', color: 'd93f0b', description: 'Invariant violation detected by CI' },
|
|
217
|
-
{ name: 'qa:approved', color: '0e8a16', description: 'QA Agent approved the implementation' },
|
|
218
|
-
{ name: 'qa:needs-work', color: 'd93f0b', description: 'QA Agent found issues' },
|
|
219
|
-
{ name: 'infra:qa-broken', color: 'F97316', description: 'QA Agent failed to run — manual review required' },
|
|
220
|
-
{ name: 'jules', color: '5319e7', description: 'Jules AI Agent' }
|
|
221
|
-
];
|
|
416
|
+
const labels = PDLC_LABELS;
|
|
222
417
|
|
|
223
418
|
console.log(`\n${cyan}${i18n.creating_labels}${reset}`);
|
|
224
419
|
for (const label of labels) {
|
|
@@ -358,163 +553,26 @@ async function runSetup() {
|
|
|
358
553
|
console.log(`\n${yellow}ℹ️ Org repo detected — PROJECT_PAT will require manual setup for security.${reset}`);
|
|
359
554
|
}
|
|
360
555
|
|
|
361
|
-
|
|
362
|
-
console.log(`\n${cyan}${i18n.configuring_protection}${reset}`);
|
|
363
|
-
try {
|
|
364
|
-
const defaultBranch = execFileSync('gh', ['api', `repos/${repo}`, '--jq', '.default_branch'],
|
|
365
|
-
{ stdio: ['ignore', 'pipe', 'ignore'], encoding: 'utf8' }).trim() || 'main';
|
|
366
|
-
const protectionPayload = JSON.stringify({
|
|
367
|
-
required_status_checks: { strict: false, contexts: ['PDLC Stage Gate', 'QA Gate'] },
|
|
368
|
-
enforce_admins: false,
|
|
369
|
-
required_pull_request_reviews: null,
|
|
370
|
-
restrictions: null
|
|
371
|
-
});
|
|
372
|
-
execFileSync('gh', ['api', `repos/${repo}/branches/${defaultBranch}/protection`, '--method', 'PUT', '--input', '-'],
|
|
373
|
-
{ input: protectionPayload, stdio: ['pipe', 'ignore', 'pipe'] });
|
|
374
|
-
console.log(` ${green}${i18n.protection_ok}${reset}`);
|
|
375
|
-
} catch (_) {
|
|
376
|
-
console.log(` ${yellow}${i18n.protection_warn}${reset}`);
|
|
377
|
-
}
|
|
556
|
+
await setBranchProtection(repo, ['PDLC Stage Gate', 'QA Gate']);
|
|
378
557
|
|
|
379
558
|
console.log(`\n${yellow}${i18n.scaffolding}${reset}`);
|
|
380
|
-
|
|
381
|
-
// We copy the templates folder so the agent has the real text logic to replace and rename
|
|
382
|
-
const sourceTemplates = path.join(sourceDir, 'templates');
|
|
383
|
-
const targetTemplates = path.join(targetDir, '.agentic-pdlc', 'templates');
|
|
384
|
-
|
|
385
|
-
if (fs.existsSync(sourceTemplates)) {
|
|
386
|
-
copyDirSync(sourceTemplates, targetTemplates);
|
|
387
|
-
console.log(`${i18n.templates_copied}`);
|
|
388
|
-
|
|
389
|
-
// Substitute values in docs/pdlc.md automatically
|
|
390
|
-
const pdlcDest = path.join(targetTemplates, 'docs', 'pdlc.md');
|
|
391
|
-
if (fs.existsSync(pdlcDest)) {
|
|
392
|
-
let pdlcContent = fs.readFileSync(pdlcDest, 'utf8');
|
|
393
|
-
|
|
394
|
-
if (projectId) pdlcContent = pdlcContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
|
|
395
|
-
if (statusFieldId) pdlcContent = pdlcContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
|
|
396
|
-
pdlcContent = pdlcContent.replace(/\{\{REPO_OWNER\}\}/g, () => repoOwner);
|
|
397
|
-
pdlcContent = pdlcContent.replace(/\{\{REPO_NAME\}\}/g, () => repoName);
|
|
398
|
-
|
|
399
|
-
if (Object.keys(optionMap).length > 0) {
|
|
400
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_IDEA\}\}/g, optionMap["💡 Idea"] || 'MISSING_ID');
|
|
401
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_EXPLORATION\}\}/g, optionMap["🔍 Exploration"] || 'MISSING_ID');
|
|
402
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_BRAINSTORMING\}\}/g, optionMap["🧠 Brainstorming"] || 'MISSING_ID');
|
|
403
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_DETAIL\}\}/g, optionMap["📐 Detail Solution"] || 'MISSING_ID');
|
|
404
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_APPROVAL\}\}/g, optionMap["✅ Approval"] || 'MISSING_ID');
|
|
405
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_DEVELOPMENT\}\}/g, optionMap["⚙️ Development"] || 'MISSING_ID');
|
|
406
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_TESTING\}\}/g, optionMap["🧪 Testing"] || 'MISSING_ID');
|
|
407
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_CODE_REVIEW_PR\}\}/g, optionMap["👁 Code Review / PR"] || 'MISSING_ID');
|
|
408
|
-
pdlcContent = pdlcContent.replace(/\{\{ID_READY_FOR_PRODUCTION\}\}/g, optionMap["🚀 Ready for Production"] || 'MISSING_ID');
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
fs.writeFileSync(pdlcDest, pdlcContent);
|
|
412
|
-
if (projectId && statusFieldId && Object.keys(optionMap).length > 0) {
|
|
413
|
-
console.log(`${i18n.pdlc_prefilled}`);
|
|
414
|
-
} else {
|
|
415
|
-
console.log(`${yellow}⚠️ pdlc.md copied — Project IDs not filled (board creation failed). Re-run after fixing token.${reset}`);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Pre-fill project-automation.yml with the same IDs so the agent doesn't need to map them
|
|
420
|
-
const workflowAutomationPath = path.join(targetTemplates, '.github', 'workflows', 'project-automation.yml');
|
|
421
|
-
if (fs.existsSync(workflowAutomationPath)) {
|
|
422
|
-
let wfContent = fs.readFileSync(workflowAutomationPath, 'utf8');
|
|
423
|
-
if (projectId) wfContent = wfContent.replace(/\{\{PROJECT_ID\}\}/g, () => projectId);
|
|
424
|
-
if (statusFieldId) wfContent = wfContent.replace(/\{\{STATUS_FIELD_ID\}\}/g, () => statusFieldId);
|
|
425
|
-
if (Object.keys(optionMap).length > 0) {
|
|
426
|
-
wfContent = wfContent.replace(/\{\{ID_IDEA\}\}/g, optionMap["💡 Idea"] || 'MISSING_ID');
|
|
427
|
-
wfContent = wfContent.replace(/\{\{ID_EXPLORATION\}\}/g, () => optionMap["🔍 Exploration"] || 'MISSING_ID');
|
|
428
|
-
wfContent = wfContent.replace(/\{\{ID_BRAINSTORMING\}\}/g, () => optionMap["🧠 Brainstorming"] || 'MISSING_ID');
|
|
429
|
-
wfContent = wfContent.replace(/\{\{ID_DETAILING\}\}/g, () => optionMap["📐 Detail Solution"] || 'MISSING_ID');
|
|
430
|
-
wfContent = wfContent.replace(/\{\{ID_APPROVAL\}\}/g, () => optionMap["✅ Approval"] || 'MISSING_ID');
|
|
431
|
-
wfContent = wfContent.replace(/\{\{ID_DEVELOPMENT\}\}/g, () => optionMap["⚙️ Development"] || 'MISSING_ID');
|
|
432
|
-
wfContent = wfContent.replace(/\{\{ID_TESTING\}\}/g, () => optionMap["🧪 Testing"] || 'MISSING_ID');
|
|
433
|
-
wfContent = wfContent.replace(/\{\{ID_CODE_REVIEW_PR\}\}/g, () => optionMap["👁 Code Review / PR"] || 'MISSING_ID');
|
|
434
|
-
wfContent = wfContent.replace(/\{\{ID_PRODUCTION\}\}/g, () => optionMap["🚀 Ready for Production"] || 'MISSING_ID');
|
|
435
|
-
}
|
|
436
|
-
fs.writeFileSync(workflowAutomationPath, wfContent);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
559
|
+
scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName);
|
|
439
560
|
|
|
440
561
|
// Write CLI context for the agent to consume in Setup Mode
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
patAutoSet
|
|
452
|
-
}, null, 2));
|
|
453
|
-
} catch (err) {
|
|
454
|
-
// Non-fatal — agent will ask for the values instead
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
// Install PDLC stage gate hook (all agents)
|
|
458
|
-
const hookSrc = path.join(sourceDir, 'adapters', 'hooks', 'pdlc-stage-gate.sh');
|
|
459
|
-
const hookDir = path.join(targetDir, '.agentic-pdlc', 'hooks');
|
|
460
|
-
const hookDest = path.join(hookDir, 'pdlc-stage-gate.sh');
|
|
461
|
-
if (fs.existsSync(hookSrc)) {
|
|
462
|
-
fs.mkdirSync(hookDir, { recursive: true });
|
|
463
|
-
fs.copyFileSync(hookSrc, hookDest);
|
|
464
|
-
fs.chmodSync(hookDest, '755');
|
|
465
|
-
}
|
|
466
|
-
const claudeSettingsDir = path.join(targetDir, '.claude');
|
|
467
|
-
const claudeSettingsPath = path.join(claudeSettingsDir, 'settings.json');
|
|
468
|
-
if (!fs.existsSync(claudeSettingsPath)) {
|
|
469
|
-
fs.mkdirSync(claudeSettingsDir, { recursive: true });
|
|
470
|
-
fs.writeFileSync(claudeSettingsPath, JSON.stringify({
|
|
471
|
-
hooks: {
|
|
472
|
-
PreToolUse: [{
|
|
473
|
-
matcher: 'Bash',
|
|
474
|
-
hooks: [{ type: 'command', command: 'bash .agentic-pdlc/hooks/pdlc-stage-gate.sh' }]
|
|
475
|
-
}]
|
|
476
|
-
}
|
|
477
|
-
}, null, 2) + '\n');
|
|
478
|
-
}
|
|
562
|
+
const boardUrl = projectNumber ? buildBoardUrl(repoOwner, projectNumber, isOrg) : null;
|
|
563
|
+
writeCliContext(targetDir, 'full', {
|
|
564
|
+
projectName,
|
|
565
|
+
repoOwner,
|
|
566
|
+
repoName,
|
|
567
|
+
projectNumber,
|
|
568
|
+
isOrg,
|
|
569
|
+
boardUrl,
|
|
570
|
+
patAutoSet
|
|
571
|
+
});
|
|
479
572
|
|
|
480
|
-
|
|
481
|
-
const claudeSetupSrc = path.join(sourceDir, 'adapters', 'claude-code', 'skill.md');
|
|
482
|
-
const cursorSetupSrc = path.join(sourceDir, 'adapters', 'cursor', 'rules.md');
|
|
483
|
-
|
|
484
|
-
if (agent === 'claude') {
|
|
485
|
-
if (fs.existsSync(claudeSetupSrc)) {
|
|
486
|
-
const dest = path.join(targetDir, '.agentic-setup.md');
|
|
487
|
-
fs.copyFileSync(claudeSetupSrc, dest);
|
|
488
|
-
console.log(`${i18n.setup_written}`);
|
|
489
|
-
printSetupDone();
|
|
490
|
-
} else {
|
|
491
|
-
console.error(`${i18n.missing_claude}${claudeSetupSrc}`);
|
|
492
|
-
}
|
|
493
|
-
} else if (agent === 'cursor') {
|
|
494
|
-
if (fs.existsSync(cursorSetupSrc)) {
|
|
495
|
-
const dest = path.join(targetDir, '.cursorrules');
|
|
496
|
-
fs.copyFileSync(cursorSetupSrc, dest);
|
|
497
|
-
console.log(`${i18n.cursor_rules_written}`);
|
|
573
|
+
installHook(sourceDir, targetDir);
|
|
498
574
|
|
|
499
|
-
|
|
500
|
-
const setupDest = path.join(targetDir, '.agentic-setup.md');
|
|
501
|
-
fs.copyFileSync(claudeSetupSrc, setupDest);
|
|
502
|
-
console.log(`${i18n.setup_written}`);
|
|
503
|
-
}
|
|
504
|
-
printSetupDone();
|
|
505
|
-
} else {
|
|
506
|
-
console.error(`${i18n.missing_claude}${cursorSetupSrc}`);
|
|
507
|
-
}
|
|
508
|
-
} else {
|
|
509
|
-
if (fs.existsSync(claudeSetupSrc)) {
|
|
510
|
-
const dest = path.join(targetDir, '.agentic-setup.md');
|
|
511
|
-
fs.copyFileSync(claudeSetupSrc, dest);
|
|
512
|
-
console.log(`${i18n.setup_written}`);
|
|
513
|
-
printSetupDone();
|
|
514
|
-
} else {
|
|
515
|
-
console.error(`${i18n.missing_claude}${claudeSetupSrc}`);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
575
|
+
copyAdapterFiles(agent, sourceDir, targetDir);
|
|
518
576
|
|
|
519
577
|
rl.close();
|
|
520
578
|
}
|
|
@@ -617,6 +675,13 @@ async function runUpdate() {
|
|
|
617
675
|
process.exit(1);
|
|
618
676
|
}
|
|
619
677
|
|
|
678
|
+
const ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
|
|
679
|
+
if ((ctx.profile || 'full') === 'lite') {
|
|
680
|
+
console.log(`\n${yellow}⚠️ Lite install detected. Run --upgrade-to-agentic to add the full board machine first.${reset}\n`);
|
|
681
|
+
rl.close();
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
620
685
|
const state = detectAgentState(targetDir);
|
|
621
686
|
const sep = '─'.repeat(55);
|
|
622
687
|
|
|
@@ -690,6 +755,24 @@ async function runUpdate() {
|
|
|
690
755
|
}
|
|
691
756
|
}
|
|
692
757
|
|
|
758
|
+
// Upgrade lite → full agentic profile (extend CLAUDE.md, do not replace)
|
|
759
|
+
const claudeMdPath = path.join(targetDir, 'CLAUDE.md');
|
|
760
|
+
const fullClaudeSrc = path.join(sourceDir, 'templates', 'full', 'CLAUDE.md');
|
|
761
|
+
if (fs.existsSync(claudeMdPath) && fs.existsSync(fullClaudeSrc)) {
|
|
762
|
+
const existing = fs.readFileSync(claudeMdPath, 'utf8');
|
|
763
|
+
if (!existing.includes('<!-- agentic-full -->')) {
|
|
764
|
+
console.log(`\n${cyan}— Agentic Profile Upgrade —${reset}`);
|
|
765
|
+
const upgradeAnswer = (await askQuestion(' Extend CLAUDE.md with the full multi-agent pipeline rulebook? (Y/n): ')).trim().toLowerCase();
|
|
766
|
+
if (!['n', 'no', 'não', 'nao'].includes(upgradeAnswer)) {
|
|
767
|
+
const extension = fs.readFileSync(fullClaudeSrc, 'utf8');
|
|
768
|
+
fs.writeFileSync(claudeMdPath, existing.trimEnd() + '\n\n' + extension + '\n');
|
|
769
|
+
results.push('✅ CLAUDE.md extended with full agentic profile');
|
|
770
|
+
} else {
|
|
771
|
+
results.push('⏭ Agentic profile upgrade — skipped');
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
693
776
|
console.log(`\n${cyan}${sep}${reset}`);
|
|
694
777
|
for (const r of results) console.log(` ${r}`);
|
|
695
778
|
console.log(`${cyan}${sep}${reset}\n`);
|
|
@@ -697,11 +780,268 @@ async function runUpdate() {
|
|
|
697
780
|
rl.close();
|
|
698
781
|
}
|
|
699
782
|
|
|
783
|
+
// ─── resolveMode ──────────────────────────────────────────────────────────────
|
|
784
|
+
|
|
785
|
+
function resolveMode(args) {
|
|
786
|
+
if (args.includes('--update')) return 'update';
|
|
787
|
+
if (args.includes('--upgrade-to-agentic')) return 'upgrade';
|
|
788
|
+
if (args.includes('--agentic')) return 'full';
|
|
789
|
+
return 'lite';
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Export for testing
|
|
793
|
+
if (typeof module !== 'undefined') module.exports = { resolveMode };
|
|
794
|
+
|
|
795
|
+
// ─── runLiteSetup ─────────────────────────────────────────────────────────────
|
|
796
|
+
|
|
797
|
+
async function runLiteSetup() {
|
|
798
|
+
await checkGhAuth();
|
|
799
|
+
|
|
800
|
+
const agentAnswer = await askQuestion(i18n.ask_agent);
|
|
801
|
+
const agent = agentAnswer.trim().toLowerCase();
|
|
802
|
+
if (!['claude', 'cursor', 'copilot'].includes(agent)) {
|
|
803
|
+
console.log(t(
|
|
804
|
+
`ℹ️ Generating Universal Setup for '${agent}' (Compatible with any Markdown-reading agent).`,
|
|
805
|
+
`ℹ️ Gerando Setup Universal para '${agent}' (Compatível com qualquer agente que leia Markdown).`,
|
|
806
|
+
`ℹ️ Generando Setup Universal para '${agent}' (Compatible con cualquier agente que lea Markdown).`
|
|
807
|
+
));
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
let repoOwner, repoName, repo;
|
|
811
|
+
while (true) {
|
|
812
|
+
let repoUrl = (await askQuestion(i18n.ask_repo)).trim();
|
|
813
|
+
if (repoUrl.endsWith('/')) repoUrl = repoUrl.slice(0, -1);
|
|
814
|
+
if (repoUrl.endsWith('.git')) repoUrl = repoUrl.slice(0, -4);
|
|
815
|
+
const repoParts = repoUrl.split('/');
|
|
816
|
+
if (repoParts.length >= 2) {
|
|
817
|
+
repoOwner = repoParts[repoParts.length - 2];
|
|
818
|
+
repoName = repoParts[repoParts.length - 1];
|
|
819
|
+
repo = `${repoOwner}/${repoName}`;
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
console.log(`${red}${i18n.invalid_repo}${reset}`);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
let isOrg = false;
|
|
826
|
+
try {
|
|
827
|
+
const ownerType = execFileSync('gh', ['api', `repos/${repo}`, '--jq', '.owner.type'],
|
|
828
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
829
|
+
isOrg = ownerType === 'Organization';
|
|
830
|
+
} catch (_) {}
|
|
831
|
+
|
|
832
|
+
console.log(`\n${yellow}${i18n.starting_setup}${reset}`);
|
|
833
|
+
|
|
834
|
+
installHook(sourceDir, targetDir);
|
|
835
|
+
|
|
836
|
+
console.log(`\n${yellow}${i18n.scaffolding}${reset}`);
|
|
837
|
+
scaffoldLiteTemplates(sourceDir, targetDir);
|
|
838
|
+
console.log(`${i18n.templates_copied}`);
|
|
839
|
+
|
|
840
|
+
await setBranchProtection(repo, ['PDLC Stage Gate']);
|
|
841
|
+
|
|
842
|
+
writeCliContext(targetDir, 'lite', {
|
|
843
|
+
repoOwner,
|
|
844
|
+
repoName,
|
|
845
|
+
projectNumber: null,
|
|
846
|
+
isOrg,
|
|
847
|
+
boardUrl: null,
|
|
848
|
+
patAutoSet: false
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
copyAdapterFiles(agent, sourceDir, targetDir);
|
|
852
|
+
|
|
853
|
+
rl.close();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function runUpgradeToAgentic() {
|
|
857
|
+
const contextPath = path.join(targetDir, '.agentic-pdlc', 'cli-context.json');
|
|
858
|
+
if (!fs.existsSync(contextPath)) {
|
|
859
|
+
console.error(`\n${red}${i18n.update_no_context}${reset}\n`);
|
|
860
|
+
rl.close();
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const ctx = JSON.parse(fs.readFileSync(contextPath, 'utf8'));
|
|
865
|
+
if ((ctx.profile || 'full') === 'full') {
|
|
866
|
+
console.log(`\n${green}✅ Already running full profile. Nothing to upgrade.${reset}\n`);
|
|
867
|
+
rl.close();
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
await checkGhAuth();
|
|
872
|
+
await checkAndRefreshProjectScope();
|
|
873
|
+
|
|
874
|
+
const { repoOwner, repoName } = ctx;
|
|
875
|
+
const repo = `${repoOwner}/${repoName}`;
|
|
876
|
+
|
|
877
|
+
const askProjectName = t(
|
|
878
|
+
`What is the project name for the board? (default: ${repoName.toUpperCase()}): `,
|
|
879
|
+
`Qual o nome do projeto em que o board será configurado? (padrão: ${repoName.toUpperCase()}): `,
|
|
880
|
+
`¿Cuál es el nombre del proyecto en el que se configurará el board? (por defecto: ${repoName.toUpperCase()}): `
|
|
881
|
+
);
|
|
882
|
+
const projectNameAnswer = await askQuestion(askProjectName);
|
|
883
|
+
const projectName = projectNameAnswer.trim() ? projectNameAnswer.trim().toUpperCase() : repoName.toUpperCase();
|
|
884
|
+
const boardName = `BOARD - ${projectName}`;
|
|
885
|
+
|
|
886
|
+
let isOrg = ctx.isOrg || false;
|
|
887
|
+
try {
|
|
888
|
+
const ownerType = execFileSync('gh', ['api', `repos/${repo}`, '--jq', '.owner.type'],
|
|
889
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
890
|
+
isOrg = ownerType === 'Organization';
|
|
891
|
+
} catch (_) {}
|
|
892
|
+
|
|
893
|
+
console.log(`\n${yellow}${i18n.starting_setup}${reset}`);
|
|
894
|
+
|
|
895
|
+
// Labels
|
|
896
|
+
const labels = PDLC_LABELS;
|
|
897
|
+
|
|
898
|
+
console.log(`\n${cyan}${i18n.creating_labels}${reset}`);
|
|
899
|
+
for (const label of labels) {
|
|
900
|
+
try {
|
|
901
|
+
execFileSync('gh', ['label', 'create', label.name, '--color', label.color, '--description', label.description, '--repo', repo, '--force'], { stdio: 'ignore' });
|
|
902
|
+
console.log(` ${i18n.label_ok}${label.name}`);
|
|
903
|
+
} catch (err) {
|
|
904
|
+
console.log(` ${i18n.label_warn}${label.name}`);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// Board
|
|
909
|
+
console.log(`\n${cyan}${i18n.creating_project}${reset}`);
|
|
910
|
+
let ownerId, projectId, projectNumber;
|
|
911
|
+
try {
|
|
912
|
+
if (isOrg) {
|
|
913
|
+
ownerId = execFileSync('gh', ['api', 'graphql', '-f', 'query=query($login: String!) { organization(login: $login) { id } }', '-f', `login=${repoOwner}`, '--jq', '.data.organization.id'],
|
|
914
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
915
|
+
} else {
|
|
916
|
+
ownerId = execFileSync('gh', ['api', 'graphql', '-f', 'query={ viewer { id } }', '--jq', '.data.viewer.id'],
|
|
917
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
const raw = execFileSync('gh', ['api', 'graphql', '-f',
|
|
921
|
+
'query=mutation($owner: ID!, $title: String!) { createProjectV2(input: {ownerId: $owner, title: $title}) { projectV2 { id number } } }',
|
|
922
|
+
'-f', `owner=${ownerId}`, '-f', `title=${boardName}`],
|
|
923
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] }).toString().trim();
|
|
924
|
+
const resp = raw ? JSON.parse(raw) : null;
|
|
925
|
+
if (resp?.errors) throw new Error(resp.errors.map(e => e.message).join('; '));
|
|
926
|
+
const pData = resp?.data?.createProjectV2?.projectV2;
|
|
927
|
+
projectId = pData?.id;
|
|
928
|
+
projectNumber = pData?.number;
|
|
929
|
+
console.log(` ${i18n.project_ok}${projectId})`);
|
|
930
|
+
|
|
931
|
+
try {
|
|
932
|
+
const repoNodeId = execFileSync('gh', ['api', `repos/${repo}`, '--jq', '.node_id'],
|
|
933
|
+
{ stdio: ['ignore', 'pipe', 'ignore'] }).toString().trim();
|
|
934
|
+
execFileSync('gh', ['api', 'graphql', '-f',
|
|
935
|
+
'query=mutation($projectId: ID!, $repositoryId: ID!) { linkProjectV2ToRepository(input: {projectId: $projectId, repositoryId: $repositoryId}) { repository { name } } }',
|
|
936
|
+
'-f', `projectId=${projectId}`, '-f', `repositoryId=${repoNodeId}`],
|
|
937
|
+
{ stdio: 'ignore' });
|
|
938
|
+
console.log(` ${i18n.link_project_ok}`);
|
|
939
|
+
} catch (_) {
|
|
940
|
+
console.log(` ${i18n.link_project_warn}`);
|
|
941
|
+
}
|
|
942
|
+
} catch (err) {
|
|
943
|
+
console.log(` ${i18n.project_err}${err.message}`);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
let statusFieldId;
|
|
947
|
+
let optionMap = {};
|
|
948
|
+
|
|
949
|
+
if (projectId) {
|
|
950
|
+
console.log(` ${cyan}${i18n.config_columns}${reset}`);
|
|
951
|
+
try {
|
|
952
|
+
statusFieldId = execFileSync('gh', ['api', 'graphql', '-f',
|
|
953
|
+
'query=query($projectId: ID!) { node(id: $projectId) { ... on ProjectV2 { fields(first: 20) { nodes { ... on ProjectV2SingleSelectField { id name } } } } } }',
|
|
954
|
+
'-f', `projectId=${projectId}`, '--jq', '.data.node.fields.nodes[] | select(.name == "Status") | .id'
|
|
955
|
+
]).toString().trim();
|
|
956
|
+
|
|
957
|
+
if (statusFieldId) {
|
|
958
|
+
const columns = [
|
|
959
|
+
{ name: '💡 Idea - No move to Exploration directly', description: 'Just tell your agent to work on issue #XX', color: 'GRAY' },
|
|
960
|
+
{ name: '🔍 Exploration', description: 'AI is analyzing code and context', color: 'PURPLE' },
|
|
961
|
+
{ name: '🧠 Brainstorming', description: 'AI proposed approaches and trade-offs', color: 'PINK' },
|
|
962
|
+
{ name: '📐 Detail Solution', description: 'AI is writing the technical spec', color: 'BLUE' },
|
|
963
|
+
{ name: '✅ Approval', description: 'Spec ready, awaiting `spec:approved` label', color: 'GREEN' },
|
|
964
|
+
{ name: '⚙️ Development', description: 'AI implementing the spec', color: 'ORANGE' },
|
|
965
|
+
{ name: '🧪 Testing', description: 'QA testing and CI pipeline checks', color: 'RED' },
|
|
966
|
+
{ name: '👁 Code Review / PR',description: 'PR opened, awaiting your review', color: 'YELLOW' },
|
|
967
|
+
{ name: '🚀 Ready for Production', description: 'Merged and ready for production', color: 'GREEN' }
|
|
968
|
+
];
|
|
969
|
+
|
|
970
|
+
const queryPayload = JSON.stringify({
|
|
971
|
+
query: `mutation($fieldId: ID!, $options: [ProjectV2SingleSelectFieldOptionInput!]) {
|
|
972
|
+
updateProjectV2Field(input: { fieldId: $fieldId, singleSelectOptions: $options }) {
|
|
973
|
+
projectV2Field { ... on ProjectV2SingleSelectField { options { id name } } }
|
|
974
|
+
}
|
|
975
|
+
}`,
|
|
976
|
+
variables: { fieldId: statusFieldId, options: columns }
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
const updateOutput = execFileSync('gh', ['api', 'graphql', '--input', '-'],
|
|
980
|
+
{ input: queryPayload }).toString().trim();
|
|
981
|
+
const jsonResponse = updateOutput ? JSON.parse(updateOutput) : null;
|
|
982
|
+
const returnedOptions = jsonResponse?.data?.updateProjectV2Field?.projectV2Field?.options || [];
|
|
983
|
+
for (const opt of returnedOptions) optionMap[opt.name] = opt.id;
|
|
984
|
+
console.log(` ${i18n.columns_ok}`);
|
|
985
|
+
}
|
|
986
|
+
} catch (_) {
|
|
987
|
+
console.log(` ${i18n.columns_warn}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Auto-provision PROJECT_PAT for personal repos
|
|
992
|
+
let patAutoSet = false;
|
|
993
|
+
if (projectId && !isOrg) {
|
|
994
|
+
try {
|
|
995
|
+
const tokenOut = execFileSync('gh', ['auth', 'token'],
|
|
996
|
+
{ stdio: ['ignore', 'pipe', 'pipe'], encoding: 'utf8' }).trim();
|
|
997
|
+
if (tokenOut) {
|
|
998
|
+
execFileSync('gh', ['secret', 'set', 'PROJECT_PAT', '--body', tokenOut, '--repo', repo],
|
|
999
|
+
{ stdio: ['ignore', 'pipe', 'pipe'] });
|
|
1000
|
+
patAutoSet = true;
|
|
1001
|
+
console.log(`\n${green}✅ PROJECT_PAT secret set automatically (uses your gh OAuth token).${reset}`);
|
|
1002
|
+
}
|
|
1003
|
+
} catch (_) {
|
|
1004
|
+
console.log(`\n${yellow}⚠️ Could not auto-set PROJECT_PAT. Agent will guide manual setup.${reset}`);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
console.log(`\n${yellow}${i18n.scaffolding}${reset}`);
|
|
1009
|
+
scaffoldFullTemplates(sourceDir, targetDir, projectId, statusFieldId, optionMap, repoOwner, repoName);
|
|
1010
|
+
|
|
1011
|
+
await setBranchProtection(repo, ['PDLC Stage Gate', 'QA Gate']);
|
|
1012
|
+
|
|
1013
|
+
const boardUrl = projectNumber ? buildBoardUrl(repoOwner, projectNumber, isOrg) : null;
|
|
1014
|
+
writeCliContext(targetDir, 'full', {
|
|
1015
|
+
projectName,
|
|
1016
|
+
repoOwner,
|
|
1017
|
+
repoName,
|
|
1018
|
+
projectNumber,
|
|
1019
|
+
isOrg,
|
|
1020
|
+
boardUrl,
|
|
1021
|
+
patAutoSet
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
const line1 = t('🎉 Upgrade complete! Board:', '🎉 Upgrade concluído! Board:', '🎉 ¡Actualización completada! Board:');
|
|
1025
|
+
console.log(`\n${green}${line1} ${boardUrl || '(board creation failed)'}${reset}\n`);
|
|
1026
|
+
|
|
1027
|
+
rl.close();
|
|
1028
|
+
}
|
|
1029
|
+
|
|
700
1030
|
// ─── Entry point ──────────────────────────────────────────────────────────────
|
|
701
1031
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
}
|
|
706
|
-
|
|
1032
|
+
if (require.main === module) {
|
|
1033
|
+
console.log(`${cyan}================================================================${reset}`);
|
|
1034
|
+
console.log(`${cyan}${i18n.welcome}${reset}`);
|
|
1035
|
+
console.log(`${cyan}================================================================${reset}\n`);
|
|
1036
|
+
|
|
1037
|
+
const args = process.argv.slice(2);
|
|
1038
|
+
const mode = resolveMode(args);
|
|
1039
|
+
|
|
1040
|
+
const handler =
|
|
1041
|
+
mode === 'update' ? runUpdate :
|
|
1042
|
+
mode === 'upgrade' ? runUpgradeToAgentic :
|
|
1043
|
+
mode === 'full' ? runFullSetup :
|
|
1044
|
+
runLiteSetup;
|
|
1045
|
+
|
|
1046
|
+
handler().catch(err => { console.error(err.message); rl.close(); process.exit(1); });
|
|
707
1047
|
}
|