@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Paula Oliveira
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.en.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# PDD — Patch-Driven Development
|
|
2
|
+

|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
> Ship safe changes in living systems.
|
|
7
|
+
|
|
8
|
+
PDD is an open-source framework focused on **bugfixes and incremental feature development in existing systems**.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## The Problem
|
|
13
|
+
|
|
14
|
+
Most development is not greenfield.
|
|
15
|
+
|
|
16
|
+
It is:
|
|
17
|
+
- fixing bugs in production
|
|
18
|
+
- evolving legacy systems
|
|
19
|
+
- adding features without breaking things
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## The Solution: PDD
|
|
24
|
+
|
|
25
|
+
PDD focuses on:
|
|
26
|
+
- understanding existing systems
|
|
27
|
+
- identifying root causes
|
|
28
|
+
- applying minimal safe changes
|
|
29
|
+
- validating with evidence
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Core Flow
|
|
34
|
+
|
|
35
|
+
Issue → Recon → Delta Spec → Patch Plan → Change → Verify
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Principles
|
|
40
|
+
|
|
41
|
+
- Change-first
|
|
42
|
+
- Evidence before edit
|
|
43
|
+
- Minimal safe delta
|
|
44
|
+
- Root-cause over symptom patch
|
|
45
|
+
- Regression-aware
|
|
46
|
+
- Reuse existing patterns
|
|
47
|
+
- Verifiable outcomes
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Structure
|
|
52
|
+
|
|
53
|
+
.pdd/
|
|
54
|
+
examples/
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Vision
|
|
59
|
+
|
|
60
|
+
Make AI-assisted development safe for real-world systems.
|
package/README.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# PDD — Patch-Driven Development
|
|
2
|
+
|
|
3
|
+
> Safe changes in real systems.
|
|
4
|
+
|
|
5
|
+
PDD is a framework focused on executing safe changes in existing systems.
|
|
6
|
+
|
|
7
|
+
## CLI
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pdd init
|
|
11
|
+
pdd fix "bug"
|
|
12
|
+
pdd fix "bug" --dry-run
|
|
13
|
+
pdd fix "bug" --no-validate
|
|
14
|
+
pdd fix "bug" --open-pr
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Flow
|
|
18
|
+
|
|
19
|
+
1. describe issue
|
|
20
|
+
2. generate artifacts
|
|
21
|
+
3. validate
|
|
22
|
+
4. prepare PR (IDE handles opening)
|
|
23
|
+
|
|
24
|
+
## Goal
|
|
25
|
+
|
|
26
|
+
Reliable execution engine for safe software changes.
|
package/README.pt-BR.md
ADDED
package/bin/pdd-ai.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { runAiFixAnalysis } from '../src/ai/run-fix-analysis.js';
|
|
4
|
+
|
|
5
|
+
async function main() {
|
|
6
|
+
try {
|
|
7
|
+
const result = await runAiFixAnalysis(process.argv.slice(2));
|
|
8
|
+
|
|
9
|
+
console.log('\n🤖 PDD AI Analysis');
|
|
10
|
+
console.log('----------------------');
|
|
11
|
+
console.log(`Provider: ${result.provider}`);
|
|
12
|
+
console.log(`Model: ${result.model}`);
|
|
13
|
+
console.log(`Issue: ${result.issue}`);
|
|
14
|
+
console.log('\nResult:\n');
|
|
15
|
+
|
|
16
|
+
console.log(JSON.stringify(result.result, null, 2));
|
|
17
|
+
} catch (err) {
|
|
18
|
+
console.error('❌ Error:', err.message);
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main();
|
package/bin/pdd-pro.js
ADDED
package/bin/pdd.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
const command = process.argv[2];
|
|
7
|
+
|
|
8
|
+
if (command === 'init') {
|
|
9
|
+
const targetDir = process.cwd();
|
|
10
|
+
const pddDir = path.join(targetDir, '.pdd');
|
|
11
|
+
|
|
12
|
+
if (fs.existsSync(pddDir)) {
|
|
13
|
+
console.log('PDD already initialized.');
|
|
14
|
+
process.exit(0);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fs.mkdirSync(pddDir, { recursive: true });
|
|
18
|
+
fs.mkdirSync(path.join(pddDir, 'templates'));
|
|
19
|
+
fs.mkdirSync(path.join(pddDir, 'commands'));
|
|
20
|
+
fs.mkdirSync(path.join(pddDir, 'memory'));
|
|
21
|
+
|
|
22
|
+
fs.writeFileSync(path.join(pddDir, 'README.md'), 'PDD initialized');
|
|
23
|
+
|
|
24
|
+
console.log('✅ PDD initialized successfully.');
|
|
25
|
+
} else {
|
|
26
|
+
console.log('Usage: pdd init');
|
|
27
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pcoliveira90/pdd",
|
|
3
|
+
"version": "0.2.1-beta.0",
|
|
4
|
+
"description": "Patch-Driven Development CLI — safe, resilient and guided code changes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pdd": "bin/pdd-pro.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node bin/pdd-pro.js",
|
|
15
|
+
"dev": "node bin/pdd-pro.js",
|
|
16
|
+
"test": "echo \"No tests yet\"",
|
|
17
|
+
"lint": "echo \"No lint configured\"",
|
|
18
|
+
"prepublishOnly": "npm pack"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=18"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"cli",
|
|
25
|
+
"developer-tools",
|
|
26
|
+
"patch",
|
|
27
|
+
"refactoring",
|
|
28
|
+
"ai-workflow",
|
|
29
|
+
"engineering",
|
|
30
|
+
"pdd"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/pcoliveira90/pdd.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/pcoliveira90/pdd/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/pcoliveira90/pdd#readme",
|
|
40
|
+
"author": "Paula Oliveira",
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
function readIfExists(filePath) {
|
|
5
|
+
if (!fs.existsSync(filePath)) return null;
|
|
6
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function buildAiAnalysisContext(baseDir = process.cwd()) {
|
|
10
|
+
const context = {
|
|
11
|
+
constitution: readIfExists(path.join(baseDir, '.pdd', 'constitution.md')),
|
|
12
|
+
deltaSpec: readIfExists(path.join(baseDir, '.pdd', 'templates', 'delta-spec.md')),
|
|
13
|
+
patchPlan: readIfExists(path.join(baseDir, '.pdd', 'templates', 'patch-plan.md')),
|
|
14
|
+
verification: readIfExists(path.join(baseDir, '.pdd', 'templates', 'verification-report.md'))
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return context;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function buildBugfixPrompt({ issue, baseDir = process.cwd() }) {
|
|
21
|
+
const context = buildAiAnalysisContext(baseDir);
|
|
22
|
+
|
|
23
|
+
return [
|
|
24
|
+
'You are a senior engineer working in an existing system.',
|
|
25
|
+
'Follow Patch-Driven Development principles.',
|
|
26
|
+
'Focus on root cause, minimal safe delta, and regression awareness.',
|
|
27
|
+
'',
|
|
28
|
+
'Issue:',
|
|
29
|
+
issue,
|
|
30
|
+
'',
|
|
31
|
+
'Available context:',
|
|
32
|
+
JSON.stringify(context, null, 2),
|
|
33
|
+
'',
|
|
34
|
+
'Return:',
|
|
35
|
+
'- root cause hypothesis',
|
|
36
|
+
'- impacted areas',
|
|
37
|
+
'- minimal safe delta',
|
|
38
|
+
'- regression risks',
|
|
39
|
+
'- validation plan'
|
|
40
|
+
].join('\n');
|
|
41
|
+
}
|
package/src/ai/engine.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function getAiProviderConfig(provider = 'openai') {
|
|
2
|
+
const providers = {
|
|
3
|
+
openai: {
|
|
4
|
+
name: 'openai',
|
|
5
|
+
envKey: 'OPENAI_API_KEY',
|
|
6
|
+
baseUrlEnv: 'OPENAI_BASE_URL',
|
|
7
|
+
defaultModel: 'gpt-5'
|
|
8
|
+
},
|
|
9
|
+
claude: {
|
|
10
|
+
name: 'claude',
|
|
11
|
+
envKey: 'ANTHROPIC_API_KEY',
|
|
12
|
+
baseUrlEnv: 'ANTHROPIC_BASE_URL',
|
|
13
|
+
defaultModel: 'claude-sonnet-4-20250514'
|
|
14
|
+
},
|
|
15
|
+
openrouter: {
|
|
16
|
+
name: 'openrouter',
|
|
17
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
18
|
+
baseUrlEnv: 'OPENROUTER_BASE_URL',
|
|
19
|
+
defaultModel: 'openai/gpt-4.1-mini'
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const config = providers[provider];
|
|
24
|
+
|
|
25
|
+
if (!config) {
|
|
26
|
+
throw new Error(`Unsupported AI provider: ${provider}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return config;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function listSupportedProviders() {
|
|
33
|
+
return ['openai', 'claude', 'openrouter'];
|
|
34
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { buildBugfixPrompt } from './analyze-change.js';
|
|
2
|
+
import { getAiProviderConfig } from './engine.js';
|
|
3
|
+
|
|
4
|
+
function extractArgValue(args, name, fallback = null) {
|
|
5
|
+
const prefix = `${name}=`;
|
|
6
|
+
const direct = args.find(arg => arg.startsWith(prefix));
|
|
7
|
+
if (direct) return direct.slice(prefix.length);
|
|
8
|
+
|
|
9
|
+
const index = args.findIndex(arg => arg === name);
|
|
10
|
+
if (index >= 0 && args[index + 1]) return args[index + 1];
|
|
11
|
+
|
|
12
|
+
return fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getIssueFromArgs(args) {
|
|
16
|
+
const filtered = args.filter(arg => !arg.startsWith('--provider') && !arg.startsWith('--model') && arg !== 'fix' && arg !== '--ai');
|
|
17
|
+
return filtered.join(' ').trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function buildStructuredInstruction() {
|
|
21
|
+
return [
|
|
22
|
+
'Return valid JSON only.',
|
|
23
|
+
'Use exactly these keys:',
|
|
24
|
+
'{',
|
|
25
|
+
' "root_cause_hypothesis": string,',
|
|
26
|
+
' "impacted_areas": string[],',
|
|
27
|
+
' "minimal_safe_delta": string,',
|
|
28
|
+
' "regression_risks": string[],',
|
|
29
|
+
' "validation_plan": string[]',
|
|
30
|
+
'}'
|
|
31
|
+
].join('\n');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function callOpenAI({ prompt, model, apiKey, baseUrl }) {
|
|
35
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
Authorization: `Bearer ${apiKey}`
|
|
40
|
+
},
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
model,
|
|
43
|
+
temperature: 0.2,
|
|
44
|
+
messages: [
|
|
45
|
+
{ role: 'system', content: buildStructuredInstruction() },
|
|
46
|
+
{ role: 'user', content: prompt }
|
|
47
|
+
]
|
|
48
|
+
})
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
throw new Error(`OpenAI request failed (${response.status}): ${text}`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = await response.json();
|
|
57
|
+
return data.choices?.[0]?.message?.content ?? '';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function callClaude({ prompt, model, apiKey, baseUrl }) {
|
|
61
|
+
const response = await fetch(`${baseUrl}/messages`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
headers: {
|
|
64
|
+
'Content-Type': 'application/json',
|
|
65
|
+
'x-api-key': apiKey,
|
|
66
|
+
'anthropic-version': '2023-06-01'
|
|
67
|
+
},
|
|
68
|
+
body: JSON.stringify({
|
|
69
|
+
model,
|
|
70
|
+
max_tokens: 1200,
|
|
71
|
+
temperature: 0.2,
|
|
72
|
+
system: buildStructuredInstruction(),
|
|
73
|
+
messages: [
|
|
74
|
+
{ role: 'user', content: prompt }
|
|
75
|
+
]
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (!response.ok) {
|
|
80
|
+
const text = await response.text();
|
|
81
|
+
throw new Error(`Claude request failed (${response.status}): ${text}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const data = await response.json();
|
|
85
|
+
return data.content?.[0]?.text ?? '';
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function callOpenRouter({ prompt, model, apiKey, baseUrl }) {
|
|
89
|
+
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
headers: {
|
|
92
|
+
'Content-Type': 'application/json',
|
|
93
|
+
Authorization: `Bearer ${apiKey}`,
|
|
94
|
+
'HTTP-Referer': 'https://github.com/pcoliveira90/pdd',
|
|
95
|
+
'X-Title': 'PDD CLI'
|
|
96
|
+
},
|
|
97
|
+
body: JSON.stringify({
|
|
98
|
+
model,
|
|
99
|
+
temperature: 0.2,
|
|
100
|
+
messages: [
|
|
101
|
+
{ role: 'system', content: buildStructuredInstruction() },
|
|
102
|
+
{ role: 'user', content: prompt }
|
|
103
|
+
]
|
|
104
|
+
})
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
const text = await response.text();
|
|
109
|
+
throw new Error(`OpenRouter request failed (${response.status}): ${text}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const data = await response.json();
|
|
113
|
+
return data.choices?.[0]?.message?.content ?? '';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function resolveBaseUrl(providerConfig) {
|
|
117
|
+
const envValue = process.env[providerConfig.baseUrlEnv];
|
|
118
|
+
if (envValue) return envValue;
|
|
119
|
+
|
|
120
|
+
if (providerConfig.name === 'openai') return 'https://api.openai.com/v1';
|
|
121
|
+
if (providerConfig.name === 'claude') return 'https://api.anthropic.com/v1';
|
|
122
|
+
if (providerConfig.name === 'openrouter') return 'https://openrouter.ai/api/v1';
|
|
123
|
+
|
|
124
|
+
throw new Error(`Missing base URL for provider: ${providerConfig.name}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function parseJsonSafely(text) {
|
|
128
|
+
try {
|
|
129
|
+
return JSON.parse(text);
|
|
130
|
+
} catch {
|
|
131
|
+
return {
|
|
132
|
+
raw_output: text
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function runAiFixAnalysis(argv = process.argv.slice(2)) {
|
|
138
|
+
const provider = extractArgValue(argv, '--provider', 'openai');
|
|
139
|
+
const providerConfig = getAiProviderConfig(provider);
|
|
140
|
+
const model = extractArgValue(argv, '--model', providerConfig.defaultModel);
|
|
141
|
+
const issue = getIssueFromArgs(argv);
|
|
142
|
+
|
|
143
|
+
if (!issue) {
|
|
144
|
+
throw new Error('Missing issue description. Example: pdd fix --ai "login not saving incomeStatus"');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const apiKey = process.env[providerConfig.envKey];
|
|
148
|
+
if (!apiKey) {
|
|
149
|
+
throw new Error(`Missing API key. Set ${providerConfig.envKey}.`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const baseUrl = resolveBaseUrl(providerConfig);
|
|
153
|
+
const prompt = buildBugfixPrompt({ issue });
|
|
154
|
+
|
|
155
|
+
let raw;
|
|
156
|
+
if (provider === 'openai') {
|
|
157
|
+
raw = await callOpenAI({ prompt, model, apiKey, baseUrl });
|
|
158
|
+
} else if (provider === 'claude') {
|
|
159
|
+
raw = await callClaude({ prompt, model, apiKey, baseUrl });
|
|
160
|
+
} else if (provider === 'openrouter') {
|
|
161
|
+
raw = await callOpenRouter({ prompt, model, apiKey, baseUrl });
|
|
162
|
+
} else {
|
|
163
|
+
throw new Error(`Unsupported provider: ${provider}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const parsed = parseJsonSafely(raw);
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
provider,
|
|
170
|
+
model,
|
|
171
|
+
issue,
|
|
172
|
+
result: parsed
|
|
173
|
+
};
|
|
174
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { PDD_TEMPLATE_VERSION } from '../core/template-registry.js';
|
|
4
|
+
import { runDoctorFix } from './doctor-fix.js';
|
|
5
|
+
import { buildDoctorRemediationPlan, printDoctorRemediationPlan } from '../core/remediation-advisor.js';
|
|
6
|
+
|
|
7
|
+
function exists(baseDir, relativePath) {
|
|
8
|
+
return fs.existsSync(path.join(baseDir, relativePath));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readVersion(baseDir) {
|
|
12
|
+
const file = path.join(baseDir, '.pdd/version.json');
|
|
13
|
+
if (!fs.existsSync(file)) return null;
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8')).templateVersion;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function print(label, ok, detail = '') {
|
|
23
|
+
const icon = ok ? '✅' : '⚠️';
|
|
24
|
+
console.log(`${icon} ${label}${detail ? ` — ${detail}` : ''}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function runDoctor(baseDir = process.cwd(), argv = []) {
|
|
28
|
+
const shouldFix = argv.includes('--fix');
|
|
29
|
+
|
|
30
|
+
console.log('🩺 PDD Doctor\n');
|
|
31
|
+
|
|
32
|
+
const coreChecks = {
|
|
33
|
+
constitution: exists(baseDir, '.pdd/constitution.md'),
|
|
34
|
+
delta: exists(baseDir, '.pdd/templates/delta-spec.md'),
|
|
35
|
+
patch: exists(baseDir, '.pdd/templates/patch-plan.md'),
|
|
36
|
+
verification: exists(baseDir, '.pdd/templates/verification-report.md'),
|
|
37
|
+
memory: exists(baseDir, '.pdd/memory/system-map.md')
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const adapters = {
|
|
41
|
+
claude: exists(baseDir, '.claude/commands/pdd.md'),
|
|
42
|
+
cursor: exists(baseDir, '.cursor/pdd.prompt.md'),
|
|
43
|
+
copilot: exists(baseDir, '.github/copilot/pdd.prompt.md')
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const installedVersion = readVersion(baseDir);
|
|
47
|
+
|
|
48
|
+
// Raw checks
|
|
49
|
+
print('Core constitution', coreChecks.constitution);
|
|
50
|
+
print('Delta spec', coreChecks.delta);
|
|
51
|
+
print('Patch plan', coreChecks.patch);
|
|
52
|
+
print('Verification report', coreChecks.verification);
|
|
53
|
+
print('System map', coreChecks.memory);
|
|
54
|
+
|
|
55
|
+
console.log('');
|
|
56
|
+
|
|
57
|
+
print('Claude adapter', adapters.claude, adapters.claude ? 'installed' : 'missing');
|
|
58
|
+
print('Cursor adapter', adapters.cursor, adapters.cursor ? 'installed' : 'missing');
|
|
59
|
+
print('Copilot adapter', adapters.copilot, adapters.copilot ? 'installed' : 'missing');
|
|
60
|
+
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
if (!installedVersion) {
|
|
64
|
+
console.log('⚠️ No template version detected. Run `pdd init --here --force`.');
|
|
65
|
+
} else if (installedVersion !== PDD_TEMPLATE_VERSION) {
|
|
66
|
+
console.log(`⚠️ Templates outdated (${installedVersion} → ${PDD_TEMPLATE_VERSION})`);
|
|
67
|
+
console.log('👉 Run: pdd init --here --upgrade');
|
|
68
|
+
} else {
|
|
69
|
+
console.log('🎉 Templates up to date');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!adapters.claude && !adapters.cursor && !adapters.copilot) {
|
|
73
|
+
console.log('ℹ️ No IDE adapters installed');
|
|
74
|
+
console.log('👉 Run: pdd init --here --ide=claude (or cursor/copilot)');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Guided remediation
|
|
78
|
+
const plan = buildDoctorRemediationPlan({
|
|
79
|
+
coreChecks,
|
|
80
|
+
adapters,
|
|
81
|
+
installedVersion,
|
|
82
|
+
currentVersion: PDD_TEMPLATE_VERSION
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
printDoctorRemediationPlan(plan);
|
|
86
|
+
|
|
87
|
+
if (shouldFix) {
|
|
88
|
+
console.log('');
|
|
89
|
+
const result = runDoctorFix(baseDir);
|
|
90
|
+
|
|
91
|
+
if (result.changed) {
|
|
92
|
+
console.log('🔧 Auto-repair applied:');
|
|
93
|
+
result.repaired.forEach(file => console.log(`- fixed: ${file}`));
|
|
94
|
+
if (result.installedVersion && result.installedVersion !== result.currentVersion) {
|
|
95
|
+
console.log(`ℹ️ Version updated: ${result.installedVersion} → ${result.currentVersion}`);
|
|
96
|
+
}
|
|
97
|
+
} else {
|
|
98
|
+
console.log('✅ No fixes needed');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { CORE_TEMPLATES, PDD_TEMPLATE_VERSION } from '../core/template-registry.js';
|
|
4
|
+
|
|
5
|
+
function ensureDir(filePath) {
|
|
6
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function writeFile(baseDir, relativePath, content) {
|
|
10
|
+
const fullPath = path.join(baseDir, relativePath);
|
|
11
|
+
ensureDir(fullPath);
|
|
12
|
+
fs.writeFileSync(fullPath, content, 'utf-8');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function readInstalledVersion(baseDir) {
|
|
16
|
+
const versionFile = path.join(baseDir, '.pdd/version.json');
|
|
17
|
+
if (!fs.existsSync(versionFile)) return null;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
return JSON.parse(fs.readFileSync(versionFile, 'utf-8')).templateVersion || null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isCoreFile(relativePath) {
|
|
27
|
+
return relativePath.startsWith('.pdd/');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function runDoctorFix(baseDir = process.cwd()) {
|
|
31
|
+
const installedVersion = readInstalledVersion(baseDir);
|
|
32
|
+
const repaired = [];
|
|
33
|
+
|
|
34
|
+
for (const [relativePath, content] of Object.entries(CORE_TEMPLATES)) {
|
|
35
|
+
const fullPath = path.join(baseDir, relativePath);
|
|
36
|
+
const missing = !fs.existsSync(fullPath);
|
|
37
|
+
const outdatedVersionFile = relativePath === '.pdd/version.json' && installedVersion !== PDD_TEMPLATE_VERSION;
|
|
38
|
+
|
|
39
|
+
if (isCoreFile(relativePath) && (missing || outdatedVersionFile)) {
|
|
40
|
+
writeFile(baseDir, relativePath, content);
|
|
41
|
+
repaired.push(relativePath);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
repaired,
|
|
47
|
+
installedVersion,
|
|
48
|
+
currentVersion: PDD_TEMPLATE_VERSION,
|
|
49
|
+
changed: repaired.length > 0
|
|
50
|
+
};
|
|
51
|
+
}
|