@pcoliveira90/pdd 0.2.4 → 0.2.6
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/package.json +1 -1
- package/src/cli/index.js +19 -2
- package/src/cli/init-command.js +21 -0
- package/src/core/project-review-agent.js +301 -0
package/package.json
CHANGED
package/src/cli/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { dirname, join } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
1
4
|
import { runValidation } from '../core/validator.js';
|
|
2
5
|
import { openPullRequest } from '../core/pr-manager.js';
|
|
3
6
|
import { generatePatchArtifacts } from '../core/patch-generator.js';
|
|
@@ -6,6 +9,14 @@ import { runDoctor } from './doctor-command.js';
|
|
|
6
9
|
import { runStatus } from './status-command.js';
|
|
7
10
|
import { runResilientFixWorkflow } from '../core/fix-runner.js';
|
|
8
11
|
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
function readCliVersion() {
|
|
15
|
+
const pkgPath = join(__dirname, '../../package.json');
|
|
16
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
17
|
+
return pkg.version;
|
|
18
|
+
}
|
|
19
|
+
|
|
9
20
|
function parseFixArgs(argv) {
|
|
10
21
|
const issue = argv
|
|
11
22
|
.filter(arg => !arg.startsWith('--') && arg !== 'fix')
|
|
@@ -24,6 +35,11 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
24
35
|
const command = argv[0];
|
|
25
36
|
const cwd = process.cwd();
|
|
26
37
|
|
|
38
|
+
if (command === '--version' || command === '-v' || command === '-V' || command === 'version') {
|
|
39
|
+
console.log(readCliVersion());
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
27
43
|
if (command === 'init') {
|
|
28
44
|
await runInit(argv);
|
|
29
45
|
return;
|
|
@@ -90,14 +106,15 @@ export async function runCli(argv = process.argv.slice(2)) {
|
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
if (command === 'help' || !command) {
|
|
93
|
-
console.log(
|
|
109
|
+
console.log(`PDD CLI ${readCliVersion()}`);
|
|
94
110
|
console.log('');
|
|
95
111
|
console.log('Commands:');
|
|
96
112
|
console.log(' pdd init <project-name>');
|
|
97
|
-
console.log(' pdd init --here [--force] [--upgrade] [-y] [--no-ide-prompt] [--ide=claude|cursor|copilot|...]');
|
|
113
|
+
console.log(' pdd init --here [--force] [--upgrade] [-y] [--no-ide-prompt] [--no-project-review] [--ide=claude|cursor|copilot|...]');
|
|
98
114
|
console.log(' pdd doctor [--fix]');
|
|
99
115
|
console.log(' pdd status');
|
|
100
116
|
console.log(' pdd fix "description" [--open-pr] [--dry-run] [--no-validate]');
|
|
117
|
+
console.log(' pdd version (or: pdd --version, pdd -v)');
|
|
101
118
|
console.log('');
|
|
102
119
|
console.log('Examples:');
|
|
103
120
|
console.log(' pdd doctor --fix');
|
package/src/cli/init-command.js
CHANGED
|
@@ -4,6 +4,7 @@ import readline from 'node:readline/promises';
|
|
|
4
4
|
import { stdin as input, stdout as output } from 'node:process';
|
|
5
5
|
import { CORE_TEMPLATES, IDE_ADAPTERS, PDD_TEMPLATE_VERSION } from '../core/template-registry.js';
|
|
6
6
|
import { buildTemplateUpgradePlan, applyTemplateUpgradePlan } from '../core/template-upgrade.js';
|
|
7
|
+
import { runInitialProjectReviewAgent } from '../core/project-review-agent.js';
|
|
7
8
|
import {
|
|
8
9
|
IDE_ORDER,
|
|
9
10
|
IDE_LABELS,
|
|
@@ -90,6 +91,16 @@ function printUpgradeSummary(summary) {
|
|
|
90
91
|
}
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
function ensureConstitution(baseDir, force = false) {
|
|
95
|
+
const constitutionPath = path.join(baseDir, '.pdd/constitution.md');
|
|
96
|
+
if (!force && fs.existsSync(constitutionPath)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
writeFile(baseDir, '.pdd/constitution.md', CORE_TEMPLATES['.pdd/constitution.md']);
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
93
104
|
function resolveIdeSelectionFromInput(raw, presence) {
|
|
94
105
|
const t = raw.trim().toLowerCase();
|
|
95
106
|
if (t === '' || t === 'y' || t === 'yes' || t === 'sim' || t === 's') {
|
|
@@ -204,6 +215,7 @@ export async function runInit(argv = process.argv.slice(2)) {
|
|
|
204
215
|
const here = argv.includes('--here');
|
|
205
216
|
const force = argv.includes('--force');
|
|
206
217
|
const upgrade = argv.includes('--upgrade');
|
|
218
|
+
const noProjectReview = argv.includes('--no-project-review');
|
|
207
219
|
|
|
208
220
|
const projectName = !here && argv[1] && !argv[1].startsWith('--') ? argv[1] : null;
|
|
209
221
|
const baseDir = here ? cwd : path.join(cwd, projectName || 'pdd-project');
|
|
@@ -233,6 +245,11 @@ export async function runInit(argv = process.argv.slice(2)) {
|
|
|
233
245
|
console.log('🚀 PDD initialized');
|
|
234
246
|
}
|
|
235
247
|
|
|
248
|
+
const constitutionCreated = ensureConstitution(baseDir, force);
|
|
249
|
+
if (constitutionCreated) {
|
|
250
|
+
console.log('📜 Constitution ensured: .pdd/constitution.md');
|
|
251
|
+
}
|
|
252
|
+
|
|
236
253
|
let ideList = normalizeIdeList(argv);
|
|
237
254
|
if (ideList.length > 0) {
|
|
238
255
|
const unknown = ideList.filter(id => !IDE_ADAPTERS[id]);
|
|
@@ -246,4 +263,8 @@ export async function runInit(argv = process.argv.slice(2)) {
|
|
|
246
263
|
|
|
247
264
|
const ideResults = installIdeAdapters(baseDir, ideList, force);
|
|
248
265
|
ideResults.forEach(r => console.log(`- ${r.status}: ${r.path}`));
|
|
266
|
+
|
|
267
|
+
if (!noProjectReview) {
|
|
268
|
+
await runInitialProjectReviewAgent(baseDir, argv);
|
|
269
|
+
}
|
|
249
270
|
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import readline from 'node:readline/promises';
|
|
4
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
5
|
+
|
|
6
|
+
const REVIEW_DIR = '.pdd/review';
|
|
7
|
+
const REVIEW_FILE = `${REVIEW_DIR}/project-review.md`;
|
|
8
|
+
const REVIEW_STATUS_FILE = `${REVIEW_DIR}/project-review-status.json`;
|
|
9
|
+
const REVIEW_FEEDBACK_FILE = `${REVIEW_DIR}/project-review-feedback.md`;
|
|
10
|
+
|
|
11
|
+
const IGNORED_DIRS = new Set([
|
|
12
|
+
'.git',
|
|
13
|
+
'.pdd',
|
|
14
|
+
'node_modules',
|
|
15
|
+
'dist',
|
|
16
|
+
'build',
|
|
17
|
+
'coverage',
|
|
18
|
+
'.next',
|
|
19
|
+
'.turbo',
|
|
20
|
+
'.idea',
|
|
21
|
+
'.vscode'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
function ensureDir(filePath) {
|
|
25
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeFile(baseDir, relativePath, content) {
|
|
29
|
+
const fullPath = path.join(baseDir, relativePath);
|
|
30
|
+
ensureDir(fullPath);
|
|
31
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readJson(filePath) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safeRead(filePath) {
|
|
43
|
+
try {
|
|
44
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function listFiles(rootDir, currentDir, depth = 0, maxDepth = 3) {
|
|
51
|
+
if (depth > maxDepth) return [];
|
|
52
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
53
|
+
const files = [];
|
|
54
|
+
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
57
|
+
const relative = path.relative(rootDir, fullPath);
|
|
58
|
+
|
|
59
|
+
if (entry.isDirectory()) {
|
|
60
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
61
|
+
files.push(...listFiles(rootDir, fullPath, depth + 1, maxDepth));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
files.push(relative);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return files;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function detectTechnologies(baseDir, files) {
|
|
72
|
+
const tech = new Set();
|
|
73
|
+
const packageJsonPath = path.join(baseDir, 'package.json');
|
|
74
|
+
const pyprojectPath = path.join(baseDir, 'pyproject.toml');
|
|
75
|
+
const requirementsPath = path.join(baseDir, 'requirements.txt');
|
|
76
|
+
const goModPath = path.join(baseDir, 'go.mod');
|
|
77
|
+
const cargoPath = path.join(baseDir, 'Cargo.toml');
|
|
78
|
+
|
|
79
|
+
const packageJson = fs.existsSync(packageJsonPath) ? readJson(packageJsonPath) : null;
|
|
80
|
+
if (packageJson) {
|
|
81
|
+
tech.add('Node.js');
|
|
82
|
+
if (packageJson.type === 'module') {
|
|
83
|
+
tech.add('JavaScript (ESM)');
|
|
84
|
+
}
|
|
85
|
+
const deps = {
|
|
86
|
+
...(packageJson.dependencies || {}),
|
|
87
|
+
...(packageJson.devDependencies || {})
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (deps.typescript) tech.add('TypeScript');
|
|
91
|
+
if (deps.react) tech.add('React');
|
|
92
|
+
if (deps.next) tech.add('Next.js');
|
|
93
|
+
if (deps.express) tech.add('Express');
|
|
94
|
+
if (deps.vite) tech.add('Vite');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (fs.existsSync(pyprojectPath) || fs.existsSync(requirementsPath)) tech.add('Python');
|
|
98
|
+
if (fs.existsSync(goModPath)) tech.add('Go');
|
|
99
|
+
if (fs.existsSync(cargoPath)) tech.add('Rust');
|
|
100
|
+
|
|
101
|
+
if (files.some(file => file.endsWith('.sql'))) tech.add('SQL');
|
|
102
|
+
if (files.some(file => file.endsWith('.md'))) tech.add('Documentation-heavy');
|
|
103
|
+
if (files.some(file => file.endsWith('.yml') || file.endsWith('.yaml'))) tech.add('CI/Automation');
|
|
104
|
+
|
|
105
|
+
return Array.from(tech);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function inferPurpose(baseDir) {
|
|
109
|
+
const packageJson = readJson(path.join(baseDir, 'package.json'));
|
|
110
|
+
if (packageJson?.description) {
|
|
111
|
+
return packageJson.description.trim();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const readme = safeRead(path.join(baseDir, 'README.md')) || safeRead(path.join(baseDir, 'README.pt-BR.md'));
|
|
115
|
+
if (!readme) {
|
|
116
|
+
return 'Project purpose not explicitly documented.';
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const nonEmpty = readme
|
|
120
|
+
.split('\n')
|
|
121
|
+
.map(line => line.trim())
|
|
122
|
+
.find(line => line.length > 0 && !line.startsWith('#'));
|
|
123
|
+
|
|
124
|
+
return nonEmpty || 'Project purpose not explicitly documented.';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function pickCoreAreas(files) {
|
|
128
|
+
const rootFolders = new Map();
|
|
129
|
+
for (const file of files) {
|
|
130
|
+
const first = file.split(path.sep)[0];
|
|
131
|
+
if (!first || first.includes('.')) continue;
|
|
132
|
+
rootFolders.set(first, (rootFolders.get(first) || 0) + 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return Array.from(rootFolders.entries())
|
|
136
|
+
.sort((a, b) => b[1] - a[1])
|
|
137
|
+
.slice(0, 6)
|
|
138
|
+
.map(([name]) => name);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function pickKeyFiles(files) {
|
|
142
|
+
const preferred = [
|
|
143
|
+
'README.md',
|
|
144
|
+
'README.pt-BR.md',
|
|
145
|
+
'package.json',
|
|
146
|
+
'pyproject.toml',
|
|
147
|
+
'go.mod',
|
|
148
|
+
'Cargo.toml',
|
|
149
|
+
'src/cli/index.js',
|
|
150
|
+
'src/index.js',
|
|
151
|
+
'src/main.js'
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const normalized = new Set(files.map(file => file.replace(/\\/g, '/')));
|
|
155
|
+
const first = preferred.filter(file => normalized.has(file));
|
|
156
|
+
const rest = files
|
|
157
|
+
.map(file => file.replace(/\\/g, '/'))
|
|
158
|
+
.filter(file => !first.includes(file))
|
|
159
|
+
.slice(0, 6);
|
|
160
|
+
|
|
161
|
+
return [...first, ...rest].slice(0, 10);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function buildReport({ purpose, technologies, coreAreas, keyFiles }) {
|
|
165
|
+
return [
|
|
166
|
+
'# Initial Project Review',
|
|
167
|
+
'',
|
|
168
|
+
'## What this project appears to be',
|
|
169
|
+
purpose,
|
|
170
|
+
'',
|
|
171
|
+
'## Main technologies',
|
|
172
|
+
...(technologies.length > 0 ? technologies.map(item => `- ${item}`) : ['- Not detected']),
|
|
173
|
+
'',
|
|
174
|
+
'## Main areas in repository',
|
|
175
|
+
...(coreAreas.length > 0 ? coreAreas.map(area => `- ${area}`) : ['- Not detected']),
|
|
176
|
+
'',
|
|
177
|
+
'## Key files inspected by review agent',
|
|
178
|
+
...(keyFiles.length > 0 ? keyFiles.map(file => `- ${file}`) : ['- Not detected']),
|
|
179
|
+
'',
|
|
180
|
+
'## Reviewer checklist',
|
|
181
|
+
'- [ ] Is the project purpose correct?',
|
|
182
|
+
'- [ ] Are the technologies correct?',
|
|
183
|
+
'- [ ] Are there important modules missing in the summary?',
|
|
184
|
+
''
|
|
185
|
+
].join('\n');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function printReviewSummary({ purpose, technologies, coreAreas, reportPath }) {
|
|
189
|
+
console.log('');
|
|
190
|
+
console.log('🤖 Initial project review agent');
|
|
191
|
+
console.log(`- Purpose: ${purpose}`);
|
|
192
|
+
console.log(`- Technologies: ${technologies.length > 0 ? technologies.join(', ') : 'Not detected'}`);
|
|
193
|
+
console.log(`- Main areas: ${coreAreas.length > 0 ? coreAreas.join(', ') : 'Not detected'}`);
|
|
194
|
+
console.log(`- Full report: ./${reportPath}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function parseDecision(answer) {
|
|
198
|
+
const value = answer.trim().toLowerCase();
|
|
199
|
+
if (value === '' || value === 'ok' || value === 'y' || value === 'yes' || value === 's' || value === 'sim') {
|
|
200
|
+
return 'approved';
|
|
201
|
+
}
|
|
202
|
+
if (value === 'a' || value === 'ajustes' || value === 'ajuste' || value === 'adjust') {
|
|
203
|
+
return 'needs-adjustments';
|
|
204
|
+
}
|
|
205
|
+
if (value === 'skip' || value === 'n' || value === 'no') {
|
|
206
|
+
return 'skipped';
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function askReviewDecision() {
|
|
212
|
+
if (!process.stdin.isTTY) {
|
|
213
|
+
return { status: 'skipped', feedback: null };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const rl = readline.createInterface({ input, output });
|
|
217
|
+
try {
|
|
218
|
+
console.log('');
|
|
219
|
+
console.log('Project review generated. Please review now:');
|
|
220
|
+
console.log(`- ./${REVIEW_FILE}`);
|
|
221
|
+
console.log('');
|
|
222
|
+
console.log('Type:');
|
|
223
|
+
console.log(' Enter / ok -> approve');
|
|
224
|
+
console.log(' ajustes / a -> request adjustments');
|
|
225
|
+
console.log(' skip / n -> skip this step');
|
|
226
|
+
console.log('');
|
|
227
|
+
|
|
228
|
+
let answer = await rl.question('> ');
|
|
229
|
+
let status = parseDecision(answer);
|
|
230
|
+
while (status === null) {
|
|
231
|
+
console.log('Invalid option. Use Enter, ok, ajustes, a, skip, or n.');
|
|
232
|
+
answer = await rl.question('> ');
|
|
233
|
+
status = parseDecision(answer);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (status !== 'needs-adjustments') {
|
|
237
|
+
return { status, feedback: null };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
console.log('');
|
|
241
|
+
const feedback = await rl.question('Describe the adjustments you want in this review: ');
|
|
242
|
+
return { status, feedback: feedback.trim() || null };
|
|
243
|
+
} finally {
|
|
244
|
+
rl.close();
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function runInitialProjectReviewAgent(baseDir = process.cwd(), argv = process.argv.slice(2)) {
|
|
249
|
+
const files = listFiles(baseDir, baseDir);
|
|
250
|
+
const purpose = inferPurpose(baseDir);
|
|
251
|
+
const technologies = detectTechnologies(baseDir, files);
|
|
252
|
+
const coreAreas = pickCoreAreas(files);
|
|
253
|
+
const keyFiles = pickKeyFiles(files);
|
|
254
|
+
const report = buildReport({ purpose, technologies, coreAreas, keyFiles });
|
|
255
|
+
|
|
256
|
+
writeFile(baseDir, REVIEW_FILE, report);
|
|
257
|
+
printReviewSummary({ purpose, technologies, coreAreas, reportPath: REVIEW_FILE });
|
|
258
|
+
|
|
259
|
+
if (argv.includes('-y') || argv.includes('--yes') || argv.includes('--no-review-prompt')) {
|
|
260
|
+
writeFile(
|
|
261
|
+
baseDir,
|
|
262
|
+
REVIEW_STATUS_FILE,
|
|
263
|
+
JSON.stringify({ status: 'approved', decidedAt: new Date().toISOString(), feedback: null }, null, 2) + '\n'
|
|
264
|
+
);
|
|
265
|
+
console.log(`✅ Initial review approved automatically: ${REVIEW_FILE}`);
|
|
266
|
+
return { status: 'approved', reportPath: REVIEW_FILE };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const decision = await askReviewDecision();
|
|
270
|
+
writeFile(
|
|
271
|
+
baseDir,
|
|
272
|
+
REVIEW_STATUS_FILE,
|
|
273
|
+
JSON.stringify(
|
|
274
|
+
{
|
|
275
|
+
status: decision.status,
|
|
276
|
+
decidedAt: new Date().toISOString(),
|
|
277
|
+
feedback: decision.feedback
|
|
278
|
+
},
|
|
279
|
+
null,
|
|
280
|
+
2
|
|
281
|
+
) + '\n'
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
if (decision.feedback) {
|
|
285
|
+
writeFile(
|
|
286
|
+
baseDir,
|
|
287
|
+
REVIEW_FEEDBACK_FILE,
|
|
288
|
+
`# Project Review Feedback\n\n${decision.feedback}\n`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (decision.status === 'approved') {
|
|
293
|
+
console.log(`✅ Initial review approved: ${REVIEW_FILE}`);
|
|
294
|
+
} else if (decision.status === 'needs-adjustments') {
|
|
295
|
+
console.log(`📝 Adjustments requested and saved: ${REVIEW_FEEDBACK_FILE}`);
|
|
296
|
+
} else {
|
|
297
|
+
console.log('⏭️ Initial project review was skipped.');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { status: decision.status, reportPath: REVIEW_FILE };
|
|
301
|
+
}
|