@pcoliveira90/pdd 0.2.1-beta.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/LICENSE +21 -0
- package/README.en.md +60 -0
- package/README.md +26 -0
- package/README.pt-BR.md +12 -0
- package/bin/pdd-ai.js +23 -0
- package/bin/pdd-pro.js +5 -0
- package/bin/pdd.js +27 -0
- package/package.json +42 -0
- package/src/ai/analyze-change.js +41 -0
- package/src/ai/engine.js +34 -0
- package/src/ai/run-fix-analysis.js +174 -0
- package/src/cli/doctor-command.js +101 -0
- package/src/cli/doctor-fix.js +51 -0
- package/src/cli/index.js +113 -0
- package/src/cli/init-command.js +123 -0
- package/src/cli/status-command.js +33 -0
- package/src/core/fix-runner.js +135 -0
- package/src/core/patch-generator.js +126 -0
- package/src/core/pr-manager.js +21 -0
- package/src/core/remediation-advisor.js +91 -0
- package/src/core/state-manager.js +71 -0
- package/src/core/template-registry.js +174 -0
- package/src/core/template-upgrade.js +68 -0
- package/src/core/validator.js +38 -0
package/src/cli/index.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { runValidation } from '../core/validator.js';
|
|
2
|
+
import { openPullRequest } from '../core/pr-manager.js';
|
|
3
|
+
import { generatePatchArtifacts } from '../core/patch-generator.js';
|
|
4
|
+
import { runInit } from './init-command.js';
|
|
5
|
+
import { runDoctor } from './doctor-command.js';
|
|
6
|
+
import { runStatus } from './status-command.js';
|
|
7
|
+
import { runResilientFixWorkflow } from '../core/fix-runner.js';
|
|
8
|
+
|
|
9
|
+
function parseFixArgs(argv) {
|
|
10
|
+
const issue = argv
|
|
11
|
+
.filter(arg => !arg.startsWith('--') && arg !== 'fix')
|
|
12
|
+
.join(' ')
|
|
13
|
+
.trim();
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
issue,
|
|
17
|
+
openPr: argv.includes('--open-pr'),
|
|
18
|
+
dryRun: argv.includes('--dry-run'),
|
|
19
|
+
noValidate: argv.includes('--no-validate')
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function runCli(argv = process.argv.slice(2)) {
|
|
24
|
+
const command = argv[0];
|
|
25
|
+
const cwd = process.cwd();
|
|
26
|
+
|
|
27
|
+
if (command === 'init') {
|
|
28
|
+
runInit(argv);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (command === 'doctor') {
|
|
33
|
+
runDoctor(cwd, argv);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (command === 'status') {
|
|
38
|
+
runStatus(cwd);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (command === 'fix') {
|
|
43
|
+
const { issue, openPr, dryRun, noValidate } = parseFixArgs(argv);
|
|
44
|
+
|
|
45
|
+
if (!issue) {
|
|
46
|
+
console.error('❌ Missing issue description.');
|
|
47
|
+
console.log('Use: pdd fix "description" [--open-pr] [--dry-run] [--no-validate]');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
console.log('🔧 PDD Fix Workflow');
|
|
52
|
+
console.log(`Issue: ${issue}`);
|
|
53
|
+
console.log(`Open PR prep: ${openPr ? 'yes' : 'no'}`);
|
|
54
|
+
console.log(`Dry run: ${dryRun ? 'yes' : 'no'}`);
|
|
55
|
+
console.log(`Validation: ${noValidate ? 'skipped' : 'enabled'}`);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const result = await runResilientFixWorkflow({
|
|
59
|
+
baseDir: cwd,
|
|
60
|
+
issue,
|
|
61
|
+
dryRun,
|
|
62
|
+
noValidate,
|
|
63
|
+
openPr,
|
|
64
|
+
generatePatchArtifacts,
|
|
65
|
+
runValidation,
|
|
66
|
+
openPullRequest
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (result.status === 'dry-run') {
|
|
70
|
+
console.log('📝 Dry run only. No files created.');
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (result.changeId) {
|
|
75
|
+
console.log(`Tracking change: ${result.changeId}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (Array.isArray(result.files) && result.files.length > 0) {
|
|
79
|
+
console.log('🧩 Patch artifacts created:');
|
|
80
|
+
result.files.forEach(file => console.log(`- ${file}`));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
console.log('✅ Fix workflow finished.');
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`❌ ${error.message}`);
|
|
86
|
+
process.exitCode = 1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (command === 'help' || !command) {
|
|
93
|
+
console.log('PDD CLI');
|
|
94
|
+
console.log('');
|
|
95
|
+
console.log('Commands:');
|
|
96
|
+
console.log(' pdd init <project-name>');
|
|
97
|
+
console.log(' pdd init --here [--force] [--upgrade] [--ide=claude|cursor|copilot|claude,cursor,copilot]');
|
|
98
|
+
console.log(' pdd doctor [--fix]');
|
|
99
|
+
console.log(' pdd status');
|
|
100
|
+
console.log(' pdd fix "description" [--open-pr] [--dry-run] [--no-validate]');
|
|
101
|
+
console.log('');
|
|
102
|
+
console.log('Examples:');
|
|
103
|
+
console.log(' pdd doctor --fix');
|
|
104
|
+
console.log(' pdd status');
|
|
105
|
+
console.log(' pdd init --here --upgrade');
|
|
106
|
+
console.log(' pdd fix "login not saving incomeStatus" --open-pr');
|
|
107
|
+
console.log('');
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
console.log(`❌ Unknown command: ${command}`);
|
|
112
|
+
console.log('Use: pdd help');
|
|
113
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CORE_TEMPLATES, IDE_ADAPTERS, PDD_TEMPLATE_VERSION } from '../core/template-registry.js';
|
|
4
|
+
import { buildTemplateUpgradePlan, applyTemplateUpgradePlan } from '../core/template-upgrade.js';
|
|
5
|
+
|
|
6
|
+
function ensureDir(filePath) {
|
|
7
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function writeFile(baseDir, relativePath, content) {
|
|
11
|
+
const fullPath = path.join(baseDir, relativePath);
|
|
12
|
+
ensureDir(fullPath);
|
|
13
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function normalizeIdeList(argv) {
|
|
17
|
+
const ideArg = argv.find(arg => arg.startsWith('--ide='));
|
|
18
|
+
if (!ideArg) return [];
|
|
19
|
+
|
|
20
|
+
return ideArg
|
|
21
|
+
.split('=')[1]
|
|
22
|
+
?.split(',')
|
|
23
|
+
.map(v => v.trim())
|
|
24
|
+
.filter(Boolean) || [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readInstalledVersion(baseDir) {
|
|
28
|
+
const versionFile = path.join(baseDir, '.pdd/version.json');
|
|
29
|
+
if (!fs.existsSync(versionFile)) return null;
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(fs.readFileSync(versionFile, 'utf-8')).templateVersion;
|
|
33
|
+
} catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function installIdeAdapters(baseDir, ideList, force) {
|
|
39
|
+
const results = [];
|
|
40
|
+
|
|
41
|
+
for (const ide of ideList) {
|
|
42
|
+
const adapter = IDE_ADAPTERS[ide];
|
|
43
|
+
if (!adapter) {
|
|
44
|
+
results.push({ path: `adapter:${ide}`, status: 'unknown' });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const [file, content] of Object.entries(adapter)) {
|
|
49
|
+
const fullPath = path.join(baseDir, file);
|
|
50
|
+
if (!force && fs.existsSync(fullPath)) {
|
|
51
|
+
results.push({ path: file, status: 'skipped' });
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
writeFile(baseDir, file, content);
|
|
56
|
+
results.push({ path: file, status: 'written' });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return results;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printUpgradeSummary(summary) {
|
|
64
|
+
console.log('⬆️ PDD upgraded');
|
|
65
|
+
|
|
66
|
+
summary.created.forEach(f => console.log(`✔️ created: ${f}`));
|
|
67
|
+
summary.updated.forEach(f => console.log(`🔁 updated: ${f}`));
|
|
68
|
+
summary.conflicts.forEach(f => console.log(`⚠️ conflict: ${f}`));
|
|
69
|
+
summary.skipped.forEach(f => console.log(`⏭️ skipped: ${f}`));
|
|
70
|
+
|
|
71
|
+
console.log('');
|
|
72
|
+
console.log(`Summary:`);
|
|
73
|
+
console.log(`- created: ${summary.created.length}`);
|
|
74
|
+
console.log(`- updated: ${summary.updated.length}`);
|
|
75
|
+
console.log(`- conflicts: ${summary.conflicts.length}`);
|
|
76
|
+
console.log(`- skipped: ${summary.skipped.length}`);
|
|
77
|
+
|
|
78
|
+
if (summary.conflicts.length > 0) {
|
|
79
|
+
console.log('👉 Run with --force to overwrite conflicts');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function runInit(argv = process.argv.slice(2)) {
|
|
84
|
+
const cwd = process.cwd();
|
|
85
|
+
const here = argv.includes('--here');
|
|
86
|
+
const force = argv.includes('--force');
|
|
87
|
+
const upgrade = argv.includes('--upgrade');
|
|
88
|
+
const ideList = normalizeIdeList(argv);
|
|
89
|
+
|
|
90
|
+
const projectName = !here && argv[1] && !argv[1].startsWith('--') ? argv[1] : null;
|
|
91
|
+
const baseDir = here ? cwd : path.join(cwd, projectName || 'pdd-project');
|
|
92
|
+
|
|
93
|
+
if (!here && !fs.existsSync(baseDir)) {
|
|
94
|
+
fs.mkdirSync(baseDir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const installedVersion = readInstalledVersion(baseDir);
|
|
98
|
+
|
|
99
|
+
if (upgrade) {
|
|
100
|
+
const plan = buildTemplateUpgradePlan(baseDir, CORE_TEMPLATES);
|
|
101
|
+
const summary = applyTemplateUpgradePlan(baseDir, CORE_TEMPLATES, plan, force);
|
|
102
|
+
|
|
103
|
+
// always update version.json
|
|
104
|
+
writeFile(baseDir, '.pdd/version.json', JSON.stringify({ templateVersion: PDD_TEMPLATE_VERSION }, null, 2));
|
|
105
|
+
|
|
106
|
+
printUpgradeSummary(summary);
|
|
107
|
+
|
|
108
|
+
if (installedVersion === PDD_TEMPLATE_VERSION) {
|
|
109
|
+
console.log('ℹ️ Templates were already up to date.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
} else {
|
|
113
|
+
// basic init (no intelligence needed yet)
|
|
114
|
+
for (const [file, content] of Object.entries(CORE_TEMPLATES)) {
|
|
115
|
+
writeFile(baseDir, file, content);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('🚀 PDD initialized');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const ideResults = installIdeAdapters(baseDir, ideList, force);
|
|
122
|
+
ideResults.forEach(r => console.log(`- ${r.status}: ${r.path}`));
|
|
123
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readProjectState } from '../core/state-manager.js';
|
|
2
|
+
|
|
3
|
+
export function runStatus(baseDir = process.cwd()) {
|
|
4
|
+
const state = readProjectState(baseDir);
|
|
5
|
+
|
|
6
|
+
console.log('📊 PDD Status\n');
|
|
7
|
+
|
|
8
|
+
console.log(`Status: ${state.status}`);
|
|
9
|
+
|
|
10
|
+
if (state.activeChange) {
|
|
11
|
+
console.log(`Active change: ${state.activeChange}`);
|
|
12
|
+
} else {
|
|
13
|
+
console.log('Active change: none');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (state.lastChange) {
|
|
17
|
+
console.log(`Last change: ${state.lastChange}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (state.updatedAt) {
|
|
21
|
+
console.log(`Updated at: ${state.updatedAt}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log('');
|
|
25
|
+
|
|
26
|
+
if (state.status === 'in-progress') {
|
|
27
|
+
console.log('⚠️ A change is currently in progress');
|
|
28
|
+
} else if (state.status === 'failed') {
|
|
29
|
+
console.log('❌ Last operation failed');
|
|
30
|
+
} else {
|
|
31
|
+
console.log('✅ Project is healthy');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import {
|
|
4
|
+
readProjectState,
|
|
5
|
+
setActiveChange,
|
|
6
|
+
clearActiveChange,
|
|
7
|
+
writeProjectState
|
|
8
|
+
} from './state-manager.js';
|
|
9
|
+
|
|
10
|
+
function ensureDir(filePath) {
|
|
11
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function appendText(filePath, content) {
|
|
15
|
+
ensureDir(filePath);
|
|
16
|
+
fs.appendFileSync(filePath, content, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function writeJson(filePath, value) {
|
|
20
|
+
ensureDir(filePath);
|
|
21
|
+
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + '\n', 'utf-8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function nowIso() {
|
|
25
|
+
return new Date().toISOString();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildFailurePayload({ issue, changeId, phase, error }) {
|
|
29
|
+
return {
|
|
30
|
+
issue,
|
|
31
|
+
changeId,
|
|
32
|
+
phase,
|
|
33
|
+
error: error?.message || String(error),
|
|
34
|
+
failedAt: nowIso()
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function persistFailureArtifacts(baseDir, payload) {
|
|
39
|
+
const changeDir = path.join(baseDir, 'changes', payload.changeId);
|
|
40
|
+
const failureReportPath = path.join(changeDir, 'failure-report.json');
|
|
41
|
+
const verificationReportPath = path.join(changeDir, 'verification-report.md');
|
|
42
|
+
|
|
43
|
+
writeJson(failureReportPath, payload);
|
|
44
|
+
appendText(
|
|
45
|
+
verificationReportPath,
|
|
46
|
+
`\n## Failure\n- phase: ${payload.phase}\n- error: ${payload.error}\n- failedAt: ${payload.failedAt}\n`
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
failureReportPath,
|
|
51
|
+
verificationReportPath
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function runResilientFixWorkflow({
|
|
56
|
+
baseDir = process.cwd(),
|
|
57
|
+
issue,
|
|
58
|
+
dryRun = false,
|
|
59
|
+
noValidate = false,
|
|
60
|
+
openPr = false,
|
|
61
|
+
generatePatchArtifacts,
|
|
62
|
+
runValidation,
|
|
63
|
+
openPullRequest
|
|
64
|
+
}) {
|
|
65
|
+
const current = readProjectState(baseDir);
|
|
66
|
+
if (current.status === 'in-progress' && current.activeChange) {
|
|
67
|
+
throw new Error(`Another change is already in progress: ${current.activeChange}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (dryRun) {
|
|
71
|
+
return {
|
|
72
|
+
status: 'dry-run',
|
|
73
|
+
issue
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const changeId = `change-${Date.now()}`;
|
|
78
|
+
setActiveChange(baseDir, changeId, 'in-progress');
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
let phase = 'patch-generation';
|
|
82
|
+
const patch = generatePatchArtifacts({ issue, baseDir });
|
|
83
|
+
|
|
84
|
+
if (!noValidate) {
|
|
85
|
+
phase = 'validation';
|
|
86
|
+
runValidation(baseDir);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (openPr) {
|
|
90
|
+
phase = 'pr-preparation';
|
|
91
|
+
await openPullRequest({
|
|
92
|
+
issue,
|
|
93
|
+
changeId: patch.changeId,
|
|
94
|
+
changeDir: patch.changeDir,
|
|
95
|
+
baseDir
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
clearActiveChange(baseDir, 'completed');
|
|
100
|
+
writeProjectState(baseDir, {
|
|
101
|
+
activeChange: null,
|
|
102
|
+
lastChange: patch.changeId,
|
|
103
|
+
status: 'completed',
|
|
104
|
+
lastResult: 'success',
|
|
105
|
+
lastIssue: issue,
|
|
106
|
+
lastPhase: 'completed',
|
|
107
|
+
lastError: null
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
status: 'completed',
|
|
112
|
+
changeId: patch.changeId,
|
|
113
|
+
files: patch.files
|
|
114
|
+
};
|
|
115
|
+
} catch (error) {
|
|
116
|
+
const phase = current.status === 'in-progress' ? current.lastPhase || 'unknown' : 'unknown';
|
|
117
|
+
const payload = buildFailurePayload({ issue, changeId, phase, error });
|
|
118
|
+
const artifacts = persistFailureArtifacts(baseDir, payload);
|
|
119
|
+
|
|
120
|
+
writeProjectState(baseDir, {
|
|
121
|
+
activeChange: changeId,
|
|
122
|
+
lastChange: changeId,
|
|
123
|
+
status: 'failed',
|
|
124
|
+
lastResult: 'failed',
|
|
125
|
+
lastIssue: issue,
|
|
126
|
+
lastPhase: phase,
|
|
127
|
+
lastError: payload.error,
|
|
128
|
+
lastFailureReport: artifacts.failureReportPath
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const wrapped = new Error(`Fix failed during ${phase}: ${payload.error}`);
|
|
132
|
+
wrapped.cause = error;
|
|
133
|
+
throw wrapped;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function ensureDir(filePath) {
|
|
5
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function writeFile(filePath, content) {
|
|
9
|
+
ensureDir(filePath);
|
|
10
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function slugify(value) {
|
|
14
|
+
return value
|
|
15
|
+
.toLowerCase()
|
|
16
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
17
|
+
.replace(/^-+|-+$/g, '')
|
|
18
|
+
.slice(0, 48);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function generatePatchArtifacts({ issue, baseDir = process.cwd() }) {
|
|
22
|
+
const timestamp = Date.now();
|
|
23
|
+
const changeId = `change-${timestamp}-${slugify(issue || 'update')}`;
|
|
24
|
+
const changeDir = path.join(baseDir, 'changes', changeId);
|
|
25
|
+
|
|
26
|
+
const files = [
|
|
27
|
+
path.join('changes', changeId, 'delta-spec.md'),
|
|
28
|
+
path.join('changes', changeId, 'patch-plan.md'),
|
|
29
|
+
path.join('changes', changeId, 'verification-report.md')
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
writeFile(
|
|
33
|
+
path.join(changeDir, 'delta-spec.md'),
|
|
34
|
+
`# Delta Spec
|
|
35
|
+
|
|
36
|
+
## Change ID
|
|
37
|
+
${changeId}
|
|
38
|
+
|
|
39
|
+
## Issue
|
|
40
|
+
${issue}
|
|
41
|
+
|
|
42
|
+
## Type
|
|
43
|
+
bugfix | feature | refactor-safe | hotfix
|
|
44
|
+
|
|
45
|
+
## Context
|
|
46
|
+
|
|
47
|
+
## Current Behavior
|
|
48
|
+
|
|
49
|
+
## Expected Behavior
|
|
50
|
+
|
|
51
|
+
## Evidence
|
|
52
|
+
|
|
53
|
+
## Root Cause Hypothesis
|
|
54
|
+
|
|
55
|
+
## Impacted Areas
|
|
56
|
+
|
|
57
|
+
## Constraints
|
|
58
|
+
|
|
59
|
+
## Minimal Safe Delta
|
|
60
|
+
|
|
61
|
+
## Alternatives Considered
|
|
62
|
+
|
|
63
|
+
## Acceptance Criteria
|
|
64
|
+
|
|
65
|
+
## Verification Strategy
|
|
66
|
+
`
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
writeFile(
|
|
70
|
+
path.join(changeDir, 'patch-plan.md'),
|
|
71
|
+
`# Patch Plan
|
|
72
|
+
|
|
73
|
+
## Change ID
|
|
74
|
+
${changeId}
|
|
75
|
+
|
|
76
|
+
## Issue
|
|
77
|
+
${issue}
|
|
78
|
+
|
|
79
|
+
## Files to Inspect
|
|
80
|
+
|
|
81
|
+
## Files to Change
|
|
82
|
+
|
|
83
|
+
## Execution Steps
|
|
84
|
+
1. Reproduce issue
|
|
85
|
+
2. Confirm root cause
|
|
86
|
+
3. Apply minimal change
|
|
87
|
+
4. Adjust tests
|
|
88
|
+
5. Run validations
|
|
89
|
+
|
|
90
|
+
## Regression Risks
|
|
91
|
+
|
|
92
|
+
## Rollback Strategy
|
|
93
|
+
`
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
writeFile(
|
|
97
|
+
path.join(changeDir, 'verification-report.md'),
|
|
98
|
+
`# Verification Report
|
|
99
|
+
|
|
100
|
+
## Change ID
|
|
101
|
+
${changeId}
|
|
102
|
+
|
|
103
|
+
## Issue
|
|
104
|
+
${issue}
|
|
105
|
+
|
|
106
|
+
## Reproduction
|
|
107
|
+
|
|
108
|
+
## Changes Made
|
|
109
|
+
|
|
110
|
+
## Tests Run
|
|
111
|
+
|
|
112
|
+
## Manual Validation
|
|
113
|
+
|
|
114
|
+
## Residual Risks
|
|
115
|
+
|
|
116
|
+
## Final Status
|
|
117
|
+
pending
|
|
118
|
+
`
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
changeId,
|
|
123
|
+
changeDir,
|
|
124
|
+
files
|
|
125
|
+
};
|
|
126
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
|
|
5
|
+
function exec(command) {
|
|
6
|
+
execSync(command, { stdio: 'inherit' });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function openPullRequest({ issue, changeId, changeDir }) {
|
|
10
|
+
const branch = `pdd/${changeId}`;
|
|
11
|
+
const title = `fix: ${issue}`;
|
|
12
|
+
|
|
13
|
+
fs.writeFileSync(path.join(changeDir, 'pr-title.txt'), title);
|
|
14
|
+
fs.writeFileSync(path.join(changeDir, 'pr-body.md'), issue);
|
|
15
|
+
|
|
16
|
+
exec(`git checkout -b ${branch}`);
|
|
17
|
+
exec('git add .');
|
|
18
|
+
exec(`git commit -m "${title}"`);
|
|
19
|
+
|
|
20
|
+
console.log('PR ready (use IDE to open)');
|
|
21
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export function buildDoctorRemediationPlan({ coreChecks, adapters, installedVersion, currentVersion }) {
|
|
2
|
+
const problems = [];
|
|
3
|
+
|
|
4
|
+
const missingCore = Object.entries(coreChecks)
|
|
5
|
+
.filter(([, ok]) => !ok)
|
|
6
|
+
.map(([key]) => key);
|
|
7
|
+
|
|
8
|
+
if (missingCore.length > 0) {
|
|
9
|
+
problems.push({
|
|
10
|
+
severity: 'high',
|
|
11
|
+
code: 'missing-core',
|
|
12
|
+
summary: 'Core PDD files are missing',
|
|
13
|
+
details: missingCore,
|
|
14
|
+
action: 'Run `pdd doctor --fix` to restore missing core files.'
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!installedVersion) {
|
|
19
|
+
problems.push({
|
|
20
|
+
severity: 'high',
|
|
21
|
+
code: 'missing-version',
|
|
22
|
+
summary: 'Template version metadata is missing',
|
|
23
|
+
details: ['.pdd/version.json'],
|
|
24
|
+
action: 'Run `pdd doctor --fix` or `pdd init --here --force`.'
|
|
25
|
+
});
|
|
26
|
+
} else if (installedVersion !== currentVersion) {
|
|
27
|
+
problems.push({
|
|
28
|
+
severity: 'medium',
|
|
29
|
+
code: 'outdated-templates',
|
|
30
|
+
summary: `Templates are outdated (${installedVersion} → ${currentVersion})`,
|
|
31
|
+
details: [],
|
|
32
|
+
action: 'Run `pdd init --here --upgrade` to update templates safely.'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const missingAdapters = Object.entries(adapters)
|
|
37
|
+
.filter(([, ok]) => !ok)
|
|
38
|
+
.map(([key]) => key);
|
|
39
|
+
|
|
40
|
+
if (missingAdapters.length === Object.keys(adapters).length) {
|
|
41
|
+
problems.push({
|
|
42
|
+
severity: 'low',
|
|
43
|
+
code: 'no-adapters',
|
|
44
|
+
summary: 'No IDE adapters are installed',
|
|
45
|
+
details: missingAdapters,
|
|
46
|
+
action: 'Run `pdd init --here --ide=claude` (or cursor/copilot).'
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const ordered = problems.sort((a, b) => rankSeverity(a.severity) - rankSeverity(b.severity));
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
ok: ordered.length === 0,
|
|
54
|
+
problems: ordered,
|
|
55
|
+
nextAction: ordered[0]?.action || null
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rankSeverity(severity) {
|
|
60
|
+
switch (severity) {
|
|
61
|
+
case 'high':
|
|
62
|
+
return 0;
|
|
63
|
+
case 'medium':
|
|
64
|
+
return 1;
|
|
65
|
+
default:
|
|
66
|
+
return 2;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function printDoctorRemediationPlan(plan) {
|
|
71
|
+
if (plan.ok) {
|
|
72
|
+
console.log('✅ No guided remediation needed.');
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log('🧭 Guided remediation');
|
|
78
|
+
|
|
79
|
+
plan.problems.forEach((problem, index) => {
|
|
80
|
+
console.log(`${index + 1}. [${problem.severity.toUpperCase()}] ${problem.summary}`);
|
|
81
|
+
if (problem.details.length > 0) {
|
|
82
|
+
console.log(` Details: ${problem.details.join(', ')}`);
|
|
83
|
+
}
|
|
84
|
+
console.log(` Action: ${problem.action}`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (plan.nextAction) {
|
|
88
|
+
console.log('');
|
|
89
|
+
console.log(`👉 Recommended next step: ${plan.nextAction}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_STATE = {
|
|
5
|
+
activeChange: null,
|
|
6
|
+
status: 'idle',
|
|
7
|
+
lastChange: null,
|
|
8
|
+
updatedAt: null
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function ensureDir(filePath) {
|
|
12
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getStatePath(baseDir = process.cwd()) {
|
|
16
|
+
return path.join(baseDir, '.pdd/state.json');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readProjectState(baseDir = process.cwd()) {
|
|
20
|
+
const statePath = getStatePath(baseDir);
|
|
21
|
+
if (!fs.existsSync(statePath)) {
|
|
22
|
+
return { ...DEFAULT_STATE };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const raw = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
27
|
+
return { ...DEFAULT_STATE, ...raw };
|
|
28
|
+
} catch {
|
|
29
|
+
return { ...DEFAULT_STATE };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function writeProjectState(baseDir = process.cwd(), nextState = {}) {
|
|
34
|
+
const statePath = getStatePath(baseDir);
|
|
35
|
+
ensureDir(statePath);
|
|
36
|
+
|
|
37
|
+
const merged = {
|
|
38
|
+
...DEFAULT_STATE,
|
|
39
|
+
...nextState,
|
|
40
|
+
updatedAt: new Date().toISOString()
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
fs.writeFileSync(statePath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
|
|
44
|
+
return merged;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function setActiveChange(baseDir = process.cwd(), changeId, status = 'in-progress') {
|
|
48
|
+
return writeProjectState(baseDir, {
|
|
49
|
+
activeChange: changeId,
|
|
50
|
+
lastChange: changeId,
|
|
51
|
+
status
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function markProjectState(baseDir = process.cwd(), status = 'idle') {
|
|
56
|
+
const current = readProjectState(baseDir);
|
|
57
|
+
return writeProjectState(baseDir, {
|
|
58
|
+
...current,
|
|
59
|
+
status,
|
|
60
|
+
activeChange: status === 'in-progress' ? current.activeChange : null
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clearActiveChange(baseDir = process.cwd(), status = 'idle') {
|
|
65
|
+
const current = readProjectState(baseDir);
|
|
66
|
+
return writeProjectState(baseDir, {
|
|
67
|
+
...current,
|
|
68
|
+
activeChange: null,
|
|
69
|
+
status
|
|
70
|
+
});
|
|
71
|
+
}
|