r2pde-ai 0.1.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/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +373 -0
- package/dist/cli.js +97 -0
- package/dist/commands/config-lang.js +32 -0
- package/dist/commands/config-set.js +38 -0
- package/dist/commands/contract-create.js +101 -0
- package/dist/commands/contract-delete.js +45 -0
- package/dist/commands/doctor.js +112 -0
- package/dist/commands/init.js +190 -0
- package/dist/commands/logs.js +35 -0
- package/dist/commands/manifesto-create.js +93 -0
- package/dist/commands/manifesto-delete.js +45 -0
- package/dist/commands/requirement-create.js +100 -0
- package/dist/commands/requirement-delete.js +46 -0
- package/dist/commands/reset.js +33 -0
- package/dist/commands/score-config.js +31 -0
- package/dist/commands/score.js +43 -0
- package/dist/commands/wave-prompt.js +130 -0
- package/dist/core/ai/adapter-factory.js +9 -0
- package/dist/core/ai/ai-adapter.js +1 -0
- package/dist/core/ai/api-adapter.js +38 -0
- package/dist/core/ai/mock-adapter.js +28 -0
- package/dist/core/config.js +61 -0
- package/dist/core/git.js +20 -0
- package/dist/core/logger.js +22 -0
- package/dist/core/paths.js +17 -0
- package/dist/core/scorer.js +135 -0
- package/dist/index.js +1 -0
- package/dist/templates/contract.template.md +14 -0
- package/dist/templates/manifesto.template.md +15 -0
- package/dist/templates/requirement.template.md +17 -0
- package/package.json +50 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getPaths } from '../core/paths.js';
|
|
5
|
+
import { logError, logWarn, logInfo, logSuccess } from '../core/logger.js';
|
|
6
|
+
import { gitAddAndCommit } from '../core/git.js';
|
|
7
|
+
import { loadConfig } from '../core/config.js';
|
|
8
|
+
export async function contractDeleteCommand() {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const paths = getPaths(cwd);
|
|
11
|
+
if (!fs.existsSync(paths.root)) {
|
|
12
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const files = fs.readdirSync(paths.contracts).filter(f => f.endsWith('.md'));
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
logInfo('No contracts found.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
logInfo('Select the artifact to permanently delete. This action cannot be undone and will remove the file from disk.');
|
|
21
|
+
const selected = await select({
|
|
22
|
+
message: 'Select a contract to delete:',
|
|
23
|
+
choices: files.map(f => ({ name: f.replace('.md', ''), value: f })),
|
|
24
|
+
});
|
|
25
|
+
const name = selected.replace(/\.md$/, '').replace(/-/g, ' ');
|
|
26
|
+
logWarn('This action cannot be undone.');
|
|
27
|
+
logInfo('Select Yes to confirm deletion.');
|
|
28
|
+
const doDelete = await confirm({ message: `Delete contract '${name}'?`, default: false });
|
|
29
|
+
if (!doDelete) {
|
|
30
|
+
logInfo('Operation cancelled.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const filePath = path.join(paths.contracts, selected);
|
|
34
|
+
fs.unlinkSync(filePath);
|
|
35
|
+
// Step 5 — Log entry
|
|
36
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
37
|
+
const logLine = `- ${new Date().toISOString()} | contract:delete | ${selected} | deleted\n`;
|
|
38
|
+
fs.ensureFileSync(logPath);
|
|
39
|
+
fs.appendFileSync(logPath, logLine, { encoding: 'utf8' });
|
|
40
|
+
// Step 6 — Git commit if enabled
|
|
41
|
+
const config = loadConfig(cwd);
|
|
42
|
+
gitAddAndCommit(cwd, `feat(contract): remove ${name}`, config);
|
|
43
|
+
// Step 7 — Final output
|
|
44
|
+
logSuccess(`Contract '${name}' deleted.`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getPaths } from '../core/paths.js';
|
|
4
|
+
import { loadConfig } from '../core/config.js';
|
|
5
|
+
import { logSuccess, logWarn, logError, logInfo } from '../core/logger.js';
|
|
6
|
+
import { isGitRepo } from '../core/git.js';
|
|
7
|
+
export async function doctorCommand() {
|
|
8
|
+
const cwd = process.cwd();
|
|
9
|
+
const paths = getPaths(cwd);
|
|
10
|
+
let hasError = false;
|
|
11
|
+
let hasWarn = false;
|
|
12
|
+
// Check 1 — r2pde-ai initialized
|
|
13
|
+
if (!fs.existsSync(paths.root)) {
|
|
14
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
15
|
+
hasError = true;
|
|
16
|
+
return printSummary(hasError, hasWarn);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
logSuccess('r2pde-ai folder found');
|
|
20
|
+
}
|
|
21
|
+
// Check 2 — pde.config.json exists and is valid
|
|
22
|
+
if (!fs.existsSync(paths.config)) {
|
|
23
|
+
logError('pde.config.json not found. Run r2pde-ai init to generate.');
|
|
24
|
+
hasError = true;
|
|
25
|
+
return printSummary(hasError, hasWarn);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
try {
|
|
29
|
+
loadConfig(cwd);
|
|
30
|
+
logSuccess('pde.config.json found and valid');
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
logError('pde.config.json is invalid JSON.');
|
|
34
|
+
hasError = true;
|
|
35
|
+
return printSummary(hasError, hasWarn);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Check 3 — pde.index.md exists
|
|
39
|
+
if (fs.existsSync(paths.index)) {
|
|
40
|
+
logSuccess('pde.index.md found');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
logWarn('pde.index.md not found. Run r2pde-ai init to regenerate.');
|
|
44
|
+
hasWarn = true;
|
|
45
|
+
}
|
|
46
|
+
// Check 4 — All required folders exist
|
|
47
|
+
const requiredFolders = [
|
|
48
|
+
paths.manifestos,
|
|
49
|
+
paths.contracts,
|
|
50
|
+
paths.requirements,
|
|
51
|
+
paths.waves,
|
|
52
|
+
paths.prompts,
|
|
53
|
+
paths.logs,
|
|
54
|
+
paths.templates,
|
|
55
|
+
];
|
|
56
|
+
let missingFolders = [];
|
|
57
|
+
for (const folder of requiredFolders) {
|
|
58
|
+
if (!fs.existsSync(folder)) {
|
|
59
|
+
missingFolders.push(path.basename(folder));
|
|
60
|
+
logWarn(`${path.basename(folder)} folder not found`);
|
|
61
|
+
hasWarn = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (missingFolders.length === 0) {
|
|
65
|
+
logSuccess('All required folders found');
|
|
66
|
+
}
|
|
67
|
+
// Check 5 — Templates present
|
|
68
|
+
const templateFiles = [
|
|
69
|
+
'manifesto.template.md',
|
|
70
|
+
'contract.template.md',
|
|
71
|
+
'requirement.template.md',
|
|
72
|
+
];
|
|
73
|
+
let missingTemplates = [];
|
|
74
|
+
for (const file of templateFiles) {
|
|
75
|
+
if (!fs.existsSync(path.join(paths.templates, file))) {
|
|
76
|
+
missingTemplates.push(file);
|
|
77
|
+
logWarn(`${file} not found in templates`);
|
|
78
|
+
hasWarn = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (missingTemplates.length === 0) {
|
|
82
|
+
logSuccess('All templates found');
|
|
83
|
+
}
|
|
84
|
+
// Check 6 — Git repository
|
|
85
|
+
if (isGitRepo(cwd)) {
|
|
86
|
+
logSuccess('Git repository detected');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
logWarn('No git repository detected. Strongly recommended.');
|
|
90
|
+
hasWarn = true;
|
|
91
|
+
}
|
|
92
|
+
// Check 7 — AI API configured
|
|
93
|
+
const config = loadConfig(cwd);
|
|
94
|
+
if (config.ai.apiUrl && config.ai.apiKey) {
|
|
95
|
+
logSuccess('AI API configured — prompts will be optimized by AI before Copilot use.');
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
logInfo('AI API not configured — prompts will be generated in offline mode for manual copy/paste into Copilot.');
|
|
99
|
+
}
|
|
100
|
+
printSummary(hasError, hasWarn);
|
|
101
|
+
}
|
|
102
|
+
function printSummary(hasError, hasWarn) {
|
|
103
|
+
if (hasError) {
|
|
104
|
+
logError('Environment has critical issues. Fix before proceeding.');
|
|
105
|
+
}
|
|
106
|
+
else if (hasWarn) {
|
|
107
|
+
logWarn('Environment has warnings. Review above.');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
logSuccess('All checks passed. Environment is healthy.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Removed legacy inquirer import
|
|
2
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
3
|
+
import fs from 'fs-extra';
|
|
4
|
+
import path, { dirname } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { getPaths } from '../core/paths.js';
|
|
7
|
+
import { saveConfig, DEFAULT_CONFIG } from '../core/config.js';
|
|
8
|
+
import { logWarn, logInfo, logSuccess } from '../core/logger.js';
|
|
9
|
+
import { isGitRepo, gitAddAndCommit } from '../core/git.js';
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
export async function initCommand() {
|
|
13
|
+
const cwd = process.cwd();
|
|
14
|
+
const paths = getPaths(cwd);
|
|
15
|
+
// Step 1 — Validate environment FIRST
|
|
16
|
+
const alreadyInitialized = fs.existsSync(paths.root);
|
|
17
|
+
if (alreadyInitialized) {
|
|
18
|
+
logWarn('r2pde-ai is already initialized in this project.');
|
|
19
|
+
logInfo('Reinitialize will overwrite existing configuration.');
|
|
20
|
+
const reinit = await confirm({
|
|
21
|
+
message: 'r2pde-ai is already initialized. Reinitialize?',
|
|
22
|
+
default: false
|
|
23
|
+
});
|
|
24
|
+
if (!reinit) {
|
|
25
|
+
logInfo('Initialization cancelled.');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (!isGitRepo(cwd)) {
|
|
30
|
+
logWarn('No git repository detected. It is strongly recommended to initialize git before proceeding.');
|
|
31
|
+
}
|
|
32
|
+
// Step 2 — Create folders AFTER validation
|
|
33
|
+
fs.ensureDirSync(paths.root);
|
|
34
|
+
fs.ensureDirSync(paths.templates);
|
|
35
|
+
fs.ensureDirSync(paths.manifestos);
|
|
36
|
+
fs.ensureDirSync(paths.contracts);
|
|
37
|
+
fs.ensureDirSync(paths.requirements);
|
|
38
|
+
fs.ensureDirSync(paths.waves);
|
|
39
|
+
fs.ensureDirSync(paths.prompts);
|
|
40
|
+
fs.ensureDirSync(paths.logs);
|
|
41
|
+
// Step 3 — Interactive project setup
|
|
42
|
+
const folderName = path.basename(cwd);
|
|
43
|
+
logInfo('The name of your project. Used in generated files and git commits.');
|
|
44
|
+
const projectName = await input({ message: 'Project name:', default: folderName });
|
|
45
|
+
logInfo('The type of product you are building. Affects which contracts and waves are prioritized.');
|
|
46
|
+
const projectType = await select({ message: 'Project type:', choices: [
|
|
47
|
+
{ name: 'Micro SaaS', value: 'micro-saas' },
|
|
48
|
+
{ name: 'Landing Page', value: 'landing-page' },
|
|
49
|
+
{ name: 'API', value: 'api' },
|
|
50
|
+
{ name: 'Dashboard', value: 'dashboard' },
|
|
51
|
+
{ name: 'E-commerce', value: 'e-commerce' },
|
|
52
|
+
{ name: 'Other', value: 'other' }
|
|
53
|
+
] });
|
|
54
|
+
logInfo('The structural pattern of your codebase. Impacts folder structure and separation of concerns.');
|
|
55
|
+
const architecture = await select({ message: 'Architecture:', choices: [
|
|
56
|
+
{ name: 'Monolith', value: 'monolith' },
|
|
57
|
+
{ name: 'Microservices', value: 'microservices' },
|
|
58
|
+
{ name: 'Monorepo', value: 'monorepo' },
|
|
59
|
+
{ name: 'Serverless', value: 'serverless' }
|
|
60
|
+
] });
|
|
61
|
+
logInfo('The coding philosophy to follow. Guides naming, structure, and complexity decisions.');
|
|
62
|
+
const codePattern = await select({ message: 'Code pattern:', choices: [
|
|
63
|
+
{ name: 'Clean Code', value: 'clean-code' },
|
|
64
|
+
{ name: 'DDD', value: 'ddd' },
|
|
65
|
+
{ name: 'SOLID', value: 'solid' },
|
|
66
|
+
{ name: 'Pragmatic', value: 'pragmatic' }
|
|
67
|
+
] });
|
|
68
|
+
logInfo('The current stage of the project. MVP relaxes some contracts; production enforces all.');
|
|
69
|
+
const maturity = await select({ message: 'Maturity:', choices: [
|
|
70
|
+
{ name: 'MVP', value: 'mvp' },
|
|
71
|
+
{ name: 'Production', value: 'production' }
|
|
72
|
+
] });
|
|
73
|
+
logInfo('The primary protocol between services or client and server.');
|
|
74
|
+
const communication = await select({ message: 'Communication:', choices: [
|
|
75
|
+
{ name: 'HTTP REST', value: 'http-rest' },
|
|
76
|
+
{ name: 'GraphQL', value: 'graphql' },
|
|
77
|
+
{ name: 'gRPC', value: 'grpc' },
|
|
78
|
+
{ name: 'WebSockets', value: 'websockets' },
|
|
79
|
+
{ name: 'Other', value: 'other' }
|
|
80
|
+
] });
|
|
81
|
+
logInfo('The type of data storage your project will use.');
|
|
82
|
+
const persistence = await select({ message: 'Persistence:', choices: [
|
|
83
|
+
{ name: 'SQL', value: 'sql' },
|
|
84
|
+
{ name: 'NoSQL', value: 'nosql' },
|
|
85
|
+
{ name: 'In-memory', value: 'in-memory' },
|
|
86
|
+
{ name: 'Other', value: 'other' }
|
|
87
|
+
] });
|
|
88
|
+
logInfo('Whether the project requires user login and session management.');
|
|
89
|
+
const hasAuth = await confirm({ message: 'Has authentication?', default: true });
|
|
90
|
+
logInfo('Whether the project handles billing, subscriptions, or transactions.');
|
|
91
|
+
const hasPayment = await confirm({ message: 'Has payment?', default: false });
|
|
92
|
+
logInfo('Whether the project connects to third-party APIs or services.');
|
|
93
|
+
const hasIntegrations = await confirm({ message: 'Has external integrations?', default: false });
|
|
94
|
+
logInfo('Who will use this product. Affects UX contracts and compliance requirements.');
|
|
95
|
+
const audience = await select({
|
|
96
|
+
message: 'Target audience:',
|
|
97
|
+
choices: [
|
|
98
|
+
{ name: 'B2B (Business to Business)', value: 'b2b' },
|
|
99
|
+
{ name: 'B2C (Business to Consumer)', value: 'b2c' },
|
|
100
|
+
{ name: 'Internal', value: 'internal' },
|
|
101
|
+
],
|
|
102
|
+
});
|
|
103
|
+
logInfo('The main technology stack. Used to generate stack-specific prompts for your AI copilot.');
|
|
104
|
+
const stack = await input({
|
|
105
|
+
message: 'Primary stack:',
|
|
106
|
+
default: 'Node.js / TypeScript',
|
|
107
|
+
});
|
|
108
|
+
logInfo('The language used in CLI output messages.');
|
|
109
|
+
const language = await select({ message: 'Language for messages:', choices: [
|
|
110
|
+
{ name: 'English', value: 'en' },
|
|
111
|
+
{ name: 'Português', value: 'pt' }
|
|
112
|
+
] });
|
|
113
|
+
logInfo('The language used in generated markdown artifact files.');
|
|
114
|
+
const artifactsLanguage = await select({ message: 'Language for artifacts:', choices: [
|
|
115
|
+
{ name: 'English', value: 'en' },
|
|
116
|
+
{ name: 'Português', value: 'pt' }
|
|
117
|
+
] });
|
|
118
|
+
// Compose config object from prompt results
|
|
119
|
+
const config = {
|
|
120
|
+
...DEFAULT_CONFIG,
|
|
121
|
+
version: '0.1.0',
|
|
122
|
+
language,
|
|
123
|
+
artifactsLanguage,
|
|
124
|
+
git: { ...DEFAULT_CONFIG.git },
|
|
125
|
+
score: { ...DEFAULT_CONFIG.score },
|
|
126
|
+
ai: { ...DEFAULT_CONFIG.ai },
|
|
127
|
+
project: {
|
|
128
|
+
type: projectType,
|
|
129
|
+
architecture,
|
|
130
|
+
maturity,
|
|
131
|
+
hasAuth,
|
|
132
|
+
hasPayment,
|
|
133
|
+
hasIntegrations,
|
|
134
|
+
audience,
|
|
135
|
+
stack,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
// Step 4 — Save config file
|
|
139
|
+
saveConfig(cwd, config);
|
|
140
|
+
// Step 5 — Generate pde.index.md
|
|
141
|
+
const indexContent = `# PDE Index — ${projectName}
|
|
142
|
+
|
|
143
|
+
> Always paste this file into your AI copilot before any prompt.
|
|
144
|
+
|
|
145
|
+
## Project Profile
|
|
146
|
+
- **Type**: ${projectType}
|
|
147
|
+
- **Architecture**: ${architecture}
|
|
148
|
+
- **Code Pattern**: ${codePattern}
|
|
149
|
+
- **Maturity**: ${maturity}
|
|
150
|
+
- **Communication**: ${communication}
|
|
151
|
+
- **Persistence**: ${persistence}
|
|
152
|
+
- **Authentication**: ${hasAuth ? 'yes' : 'no'}
|
|
153
|
+
- **Payment**: ${hasPayment ? 'yes' : 'no'}
|
|
154
|
+
- **Integrations**: ${hasIntegrations ? 'yes' : 'no'}
|
|
155
|
+
- **Audience**: ${audience}
|
|
156
|
+
- **Stack**: ${stack}
|
|
157
|
+
|
|
158
|
+
## Artifact Map
|
|
159
|
+
- Manifestos: .r2pde-ai/manifestos/
|
|
160
|
+
- Contracts: .r2pde-ai/contracts/
|
|
161
|
+
- Requirements: .r2pde-ai/requirements/
|
|
162
|
+
- Waves: .r2pde-ai/waves/
|
|
163
|
+
- Prompts: .r2pde-ai/prompts/
|
|
164
|
+
- Logs: .r2pde-ai/logs/
|
|
165
|
+
|
|
166
|
+
## Framework Version
|
|
167
|
+
- r2pde-ai: 0.1.0
|
|
168
|
+
`;
|
|
169
|
+
fs.writeFileSync(paths.index, indexContent, { encoding: 'utf8' });
|
|
170
|
+
// Step 6 — Copy templates
|
|
171
|
+
const templateFiles = [
|
|
172
|
+
{ src: path.resolve(__dirname, '../templates/manifesto.template.md'), dest: path.resolve(paths.templates, 'manifesto.template.md') },
|
|
173
|
+
{ src: path.resolve(__dirname, '../templates/contract.template.md'), dest: path.resolve(paths.templates, 'contract.template.md') },
|
|
174
|
+
{ src: path.resolve(__dirname, '../templates/requirement.template.md'), dest: path.resolve(paths.templates, 'requirement.template.md') },
|
|
175
|
+
];
|
|
176
|
+
const runtimeTemplateDir = path.resolve(__dirname, '../templates');
|
|
177
|
+
for (const { dest } of templateFiles) {
|
|
178
|
+
const src = path.resolve(runtimeTemplateDir, path.basename(dest));
|
|
179
|
+
fs.copyFileSync(src, dest);
|
|
180
|
+
}
|
|
181
|
+
// Step 7 — Generate GUIDE.md
|
|
182
|
+
const guideContent = `# r2pde-ai GUIDE\n\n## What is a Manifesto?\nA Manifesto defines guiding principles for your project. Create one when you want to establish a core value or philosophy.\n\n**Structure:**\n- Purpose\n- Principles\n- Scope\n- Exceptions\n\n## What is a Contract?\nA Contract enforces rules or agreements in your project. Create one to formalize expectations.\n\n**Structure:**\n- Purpose\n- Rules\n- Violations\n- Exceptions\n\n## What is a Requirement?\nA Requirement describes a functional, non-functional, or business rule. Create one for every need or constraint.\n\n**Structure:**\n- Type\n- Description\n- Acceptance Criteria\n- Dependencies\n- Notes\n\n## What are Waves?\nWaves are iterative cycles of delivery. Each wave advances the project with new artifacts.\n\n**How to advance:**\n- Complete all requirements and contracts for the current wave.\n- Review quality score.\n- Start the next wave.\n\n## What is the Quality Score?\nThe Quality Score measures project health:\n- 🟢 Green: 0-30\n- 🟡 Yellow: 31-70\n- 🔴 Red: 71-100\n\n## How to use pde.index.md\nPaste .r2pde-ai/pde.index.md into your AI copilot before any prompt to provide full project context.\n\n## Prompt Generation (wave:prompt)\n\n- **Offline mode:** Generates a structured prompt for copy/paste into GitHub Copilot.\n- **API mode:** Generates an AI-optimized prompt for copy/paste into GitHub Copilot.\n- **Copilot always generates the code** — the AI API only improves the prompt quality.\n\n## CLI Commands Reference\n- init: Initialize r2pde-ai in the current project\n- doctor: Diagnose project health\n`;
|
|
183
|
+
fs.writeFileSync(paths.guide, guideContent, { encoding: 'utf8' });
|
|
184
|
+
// Step 8 — Git commit if enabled
|
|
185
|
+
gitAddAndCommit(cwd, 'init: project initialized', config);
|
|
186
|
+
// Step 9 — Final output
|
|
187
|
+
logSuccess('r2pde-ai initialized successfully.');
|
|
188
|
+
logInfo('Start by reading .r2pde-ai/GUIDE.md');
|
|
189
|
+
logInfo('Then paste .r2pde-ai/pde.index.md into your AI copilot before any prompt.');
|
|
190
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { getPaths } from '../core/paths.js';
|
|
3
|
+
import { logInfo, logSuccess, logWarn } from '../core/logger.js';
|
|
4
|
+
import fs from 'fs-extra';
|
|
5
|
+
import { confirm } from '@inquirer/prompts';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
export const logsCommand = new Command('logs')
|
|
8
|
+
.description('View the audit log')
|
|
9
|
+
.option('--clear', 'Clear all logs')
|
|
10
|
+
.action(async (opts) => {
|
|
11
|
+
const cwd = process.cwd();
|
|
12
|
+
const paths = getPaths(cwd);
|
|
13
|
+
if (!fs.existsSync(paths.root)) {
|
|
14
|
+
logWarn('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
18
|
+
if (opts.clear) {
|
|
19
|
+
logInfo('This will permanently delete all audit log entries. This action cannot be undone.');
|
|
20
|
+
const doClear = await confirm({ message: 'Clear all logs? This cannot be undone.', default: false });
|
|
21
|
+
if (!doClear) {
|
|
22
|
+
logInfo('Operation cancelled.');
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
fs.writeFileSync(logPath, '', { encoding: 'utf8' });
|
|
26
|
+
logSuccess('Logs cleared.');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (!fs.existsSync(logPath)) {
|
|
30
|
+
logInfo('No logs found.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
34
|
+
logInfo('\n' + content);
|
|
35
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getPaths } from '../core/paths.js';
|
|
5
|
+
import { logError, logWarn, logInfo, logSuccess } from '../core/logger.js';
|
|
6
|
+
import { gitAddAndCommit } from '../core/git.js';
|
|
7
|
+
import { loadConfig } from '../core/config.js';
|
|
8
|
+
function toKebabCase(str) {
|
|
9
|
+
return str
|
|
10
|
+
.normalize('NFD')
|
|
11
|
+
.replace(/[^\w\s-]/g, '')
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/\s+/g, '-')
|
|
15
|
+
.replace(/-+/g, '-');
|
|
16
|
+
}
|
|
17
|
+
export async function manifestoCreateCommand() {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const paths = getPaths(cwd);
|
|
20
|
+
if (!fs.existsSync(paths.root)) {
|
|
21
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Step 2 — Interactive prompts
|
|
25
|
+
logInfo('The name of this manifesto. Should reflect the principle it establishes (e.g. UI Principles, Code Philosophy).');
|
|
26
|
+
const name = await input({ message: 'Manifesto name:', validate: (inputVal) => inputVal.trim() !== '' || 'Name is required' });
|
|
27
|
+
logInfo('The area of the project this manifesto governs.');
|
|
28
|
+
const scope = await select({ message: 'Scope:', choices: [
|
|
29
|
+
{ name: 'UI', value: 'ui' },
|
|
30
|
+
{ name: 'UX', value: 'ux' },
|
|
31
|
+
{ name: 'Code Philosophy', value: 'code-philosophy' },
|
|
32
|
+
{ name: 'Development Culture', value: 'development-culture' },
|
|
33
|
+
{ name: 'Other', value: 'other' }
|
|
34
|
+
] });
|
|
35
|
+
logInfo('A brief summary of what this manifesto enforces and why it exists.');
|
|
36
|
+
const description = await input({ message: 'Brief description:', validate: (inputVal) => inputVal.trim() !== '' || 'Description is required' });
|
|
37
|
+
const kebabName = toKebabCase(name);
|
|
38
|
+
const filename = `${kebabName}.md`;
|
|
39
|
+
const filePath = path.join(paths.manifestos, filename);
|
|
40
|
+
// Step 3 — Check for duplicates
|
|
41
|
+
if (fs.existsSync(filePath)) {
|
|
42
|
+
logWarn('Manifesto already exists.');
|
|
43
|
+
logInfo('Select Yes to overwrite the existing manifesto file.');
|
|
44
|
+
const overwrite = await confirm({ message: 'Manifesto already exists. Overwrite?', default: false });
|
|
45
|
+
if (!overwrite) {
|
|
46
|
+
logInfo('Operation cancelled.');
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// Step 4 — Generate manifesto file
|
|
51
|
+
const templatePath = path.join(paths.templates, 'manifesto.template.md');
|
|
52
|
+
if (!fs.existsSync(templatePath)) {
|
|
53
|
+
logError('Manifesto template not found.');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
let template = fs.readFileSync(templatePath, 'utf8');
|
|
57
|
+
template = template.replace('[Name]', name);
|
|
58
|
+
const meta = `---\nname: ${name}\nscope: ${scope}\ndescription: ${description}\ncreatedAt: ${new Date().toISOString()}\n---\n\n`;
|
|
59
|
+
const content = meta + template;
|
|
60
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf8' });
|
|
61
|
+
// Step 5 — Update pde.index.md
|
|
62
|
+
if (fs.existsSync(paths.index)) {
|
|
63
|
+
let indexContent = fs.readFileSync(paths.index, { encoding: 'utf8' });
|
|
64
|
+
const manifestosSection = '- Manifestos: .r2pde-ai/manifestos/';
|
|
65
|
+
const newLine = ` - ${name}: .r2pde-ai/manifestos/${filename}`;
|
|
66
|
+
if (indexContent.includes(manifestosSection)) {
|
|
67
|
+
const lines = indexContent.split('\n');
|
|
68
|
+
const idx = lines.findIndex(l => l.trim() === manifestosSection);
|
|
69
|
+
if (idx !== -1) {
|
|
70
|
+
// Insert after Manifestos: section, but before next section or end
|
|
71
|
+
let insertAt = idx + 1;
|
|
72
|
+
while (insertAt < lines.length && (lines[insertAt].startsWith(' - ') || lines[insertAt].trim() === ''))
|
|
73
|
+
insertAt++;
|
|
74
|
+
lines.splice(insertAt, 0, newLine);
|
|
75
|
+
indexContent = lines.join('\n');
|
|
76
|
+
fs.writeFileSync(paths.index, indexContent, { encoding: 'utf8' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
logWarn('pde.index.md not found. Skipping index update.');
|
|
82
|
+
}
|
|
83
|
+
// Step 6 — Log entry
|
|
84
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
85
|
+
const logLine = `- ${new Date().toISOString()} | manifesto:create | ${filename} | created\n`;
|
|
86
|
+
fs.ensureFileSync(logPath);
|
|
87
|
+
fs.appendFileSync(logPath, logLine, { encoding: 'utf8' });
|
|
88
|
+
// Step 7 — Git commit if enabled
|
|
89
|
+
const config = loadConfig(cwd);
|
|
90
|
+
gitAddAndCommit(cwd, `feat(manifesto): add ${name}`, config);
|
|
91
|
+
// Step 8 — Final output
|
|
92
|
+
logSuccess(`Manifesto '${name}' created at .r2pde-ai/manifestos/${filename}`);
|
|
93
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getPaths } from '../core/paths.js';
|
|
5
|
+
import { logError, logWarn, logInfo, logSuccess } from '../core/logger.js';
|
|
6
|
+
import { gitAddAndCommit } from '../core/git.js';
|
|
7
|
+
import { loadConfig } from '../core/config.js';
|
|
8
|
+
export async function manifestoDeleteCommand() {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const paths = getPaths(cwd);
|
|
11
|
+
if (!fs.existsSync(paths.root)) {
|
|
12
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const files = fs.readdirSync(paths.manifestos).filter(f => f.endsWith('.md'));
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
logInfo('No manifestos found.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
logInfo('Select the artifact to permanently delete. This action cannot be undone and will remove the file from disk.');
|
|
21
|
+
const selected = await select({
|
|
22
|
+
message: 'Select a manifesto to delete:',
|
|
23
|
+
choices: files.map(f => ({ name: f.replace('.md', ''), value: f })),
|
|
24
|
+
});
|
|
25
|
+
const name = selected.replace(/\.md$/, '').replace(/-/g, ' ');
|
|
26
|
+
logWarn('This action cannot be undone.');
|
|
27
|
+
logInfo('Select Yes to confirm deletion.');
|
|
28
|
+
const doDelete = await confirm({ message: `Delete manifesto '${name}'?`, default: false });
|
|
29
|
+
if (!doDelete) {
|
|
30
|
+
logInfo('Operation cancelled.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const filePath = path.join(paths.manifestos, selected);
|
|
34
|
+
fs.unlinkSync(filePath);
|
|
35
|
+
// Step 5 — Log entry
|
|
36
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
37
|
+
const logLine = `- ${new Date().toISOString()} | manifesto:delete | ${selected} | deleted\n`;
|
|
38
|
+
fs.ensureFileSync(logPath);
|
|
39
|
+
fs.appendFileSync(logPath, logLine, { encoding: 'utf8' });
|
|
40
|
+
// Step 6 — Git commit if enabled
|
|
41
|
+
const config = loadConfig(cwd);
|
|
42
|
+
gitAddAndCommit(cwd, `feat(manifesto): remove ${name}`, config);
|
|
43
|
+
// Step 7 — Final output
|
|
44
|
+
logSuccess(`Manifesto '${name}' deleted.`);
|
|
45
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { input, select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getPaths } from '../core/paths.js';
|
|
5
|
+
import { logError, logWarn, logInfo, logSuccess } from '../core/logger.js';
|
|
6
|
+
import { gitAddAndCommit } from '../core/git.js';
|
|
7
|
+
import { loadConfig } from '../core/config.js';
|
|
8
|
+
function toKebabCase(str) {
|
|
9
|
+
return str
|
|
10
|
+
.normalize('NFD')
|
|
11
|
+
.replace(/[^\w\s-]/g, '')
|
|
12
|
+
.trim()
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/\s+/g, '-')
|
|
15
|
+
.replace(/-+/g, '-');
|
|
16
|
+
}
|
|
17
|
+
export async function requirementCreateCommand() {
|
|
18
|
+
const cwd = process.cwd();
|
|
19
|
+
const paths = getPaths(cwd);
|
|
20
|
+
if (!fs.existsSync(paths.root)) {
|
|
21
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
// Step 2 — Interactive prompts
|
|
25
|
+
logInfo('The name of this requirement. Should be clear and specific (e.g. User Registration, Password Reset).');
|
|
26
|
+
const name = await input({ message: 'Requirement name:', validate: (inputVal) => inputVal.trim() !== '' || 'Name is required' });
|
|
27
|
+
logInfo('Functional requirements describe behavior. Non-functional describe quality. Business rules describe constraints.');
|
|
28
|
+
const type = await select({ message: 'Type:', choices: [
|
|
29
|
+
{ name: 'Functional', value: 'functional' },
|
|
30
|
+
{ name: 'Non-functional', value: 'non-functional' },
|
|
31
|
+
{ name: 'Business rule', value: 'business-rule' }
|
|
32
|
+
] });
|
|
33
|
+
logInfo('High priority requirements are evaluated first in the quality score.');
|
|
34
|
+
const priority = await select({ message: 'Priority:', choices: [
|
|
35
|
+
{ name: 'High', value: 'high' },
|
|
36
|
+
{ name: 'Medium', value: 'medium' },
|
|
37
|
+
{ name: 'Low', value: 'low' }
|
|
38
|
+
] });
|
|
39
|
+
logInfo('A clear, objective description of what this requirement specifies.');
|
|
40
|
+
const description = await input({ message: 'Brief description:', validate: (inputVal) => inputVal.trim() !== '' || 'Description is required' });
|
|
41
|
+
logInfo('The conditions that must be true for this requirement to be considered complete. Comma-separated.');
|
|
42
|
+
const criteria = await input({ message: 'Acceptance criteria (comma-separated):', validate: (inputVal) => inputVal.trim() !== '' || 'Acceptance criteria required' });
|
|
43
|
+
const kebabName = toKebabCase(name);
|
|
44
|
+
const filename = `${kebabName}.md`;
|
|
45
|
+
const filePath = path.join(paths.requirements, filename);
|
|
46
|
+
// Step 3 — Check for duplicates
|
|
47
|
+
if (fs.existsSync(filePath)) {
|
|
48
|
+
logWarn('Requirement already exists.');
|
|
49
|
+
logInfo('Select Yes to overwrite the existing requirement file.');
|
|
50
|
+
const overwrite = await confirm({ message: 'Requirement already exists. Overwrite?', default: false });
|
|
51
|
+
if (!overwrite) {
|
|
52
|
+
logInfo('Operation cancelled.');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Step 4 — Generate requirement file
|
|
57
|
+
const templatePath = path.join(paths.templates, 'requirement.template.md');
|
|
58
|
+
if (!fs.existsSync(templatePath)) {
|
|
59
|
+
logError('Requirement template not found.');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
let template = fs.readFileSync(templatePath, { encoding: 'utf8' });
|
|
63
|
+
template = template.replace('[Name]', name);
|
|
64
|
+
const meta = `---\nname: ${name}\ntype: ${type}\npriority: ${priority}\ndescription: ${description}\ncreatedAt: ${new Date().toISOString()}\n---\n\n`;
|
|
65
|
+
const criteriaLines = criteria.split(',').map((c) => `- ${c.trim()}`).join('\n');
|
|
66
|
+
const criteriaSection = `## Acceptance Criteria\n${criteriaLines}\n\n`;
|
|
67
|
+
const content = meta + criteriaSection + template;
|
|
68
|
+
fs.writeFileSync(filePath, content, { encoding: 'utf8' });
|
|
69
|
+
// Step 5 — Update pde.index.md
|
|
70
|
+
if (fs.existsSync(paths.index)) {
|
|
71
|
+
let indexContent = fs.readFileSync(paths.index, { encoding: 'utf8' });
|
|
72
|
+
const requirementsSection = '- Requirements: .r2pde-ai/requirements/';
|
|
73
|
+
const newLine = ` - ${name}: .r2pde-ai/requirements/${filename}`;
|
|
74
|
+
if (indexContent.includes(requirementsSection)) {
|
|
75
|
+
const lines = indexContent.split('\n');
|
|
76
|
+
const idx = lines.findIndex(l => l.trim() === requirementsSection);
|
|
77
|
+
if (idx !== -1) {
|
|
78
|
+
let insertAt = idx + 1;
|
|
79
|
+
while (insertAt < lines.length && (lines[insertAt].startsWith(' - ') || lines[insertAt].trim() === ''))
|
|
80
|
+
insertAt++;
|
|
81
|
+
lines.splice(insertAt, 0, newLine);
|
|
82
|
+
indexContent = lines.join('\n');
|
|
83
|
+
fs.writeFileSync(paths.index, indexContent, { encoding: 'utf8' });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
logWarn('pde.index.md not found. Skipping index update.');
|
|
89
|
+
}
|
|
90
|
+
// Step 6 — Log entry
|
|
91
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
92
|
+
const logLine = `- ${new Date().toISOString()} | requirement:create | ${filename} | created\n`;
|
|
93
|
+
fs.ensureFileSync(logPath);
|
|
94
|
+
fs.appendFileSync(logPath, logLine, { encoding: 'utf8' });
|
|
95
|
+
// Step 7 — Git commit if enabled
|
|
96
|
+
const config = loadConfig(cwd);
|
|
97
|
+
gitAddAndCommit(cwd, `feat(requirement): add ${name}`, config);
|
|
98
|
+
// Step 8 — Final output
|
|
99
|
+
logSuccess(`Requirement '${name}' created at .r2pde-ai/requirements/${filename}`);
|
|
100
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { select, confirm } from '@inquirer/prompts';
|
|
2
|
+
import fs from 'fs-extra';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { getPaths } from '../core/paths.js';
|
|
5
|
+
import { logError, logWarn, logInfo, logSuccess } from '../core/logger.js';
|
|
6
|
+
import { gitAddAndCommit } from '../core/git.js';
|
|
7
|
+
import { loadConfig } from '../core/config.js';
|
|
8
|
+
export async function requirementDeleteCommand() {
|
|
9
|
+
const cwd = process.cwd();
|
|
10
|
+
const paths = getPaths(cwd);
|
|
11
|
+
if (!fs.existsSync(paths.root)) {
|
|
12
|
+
logError('r2pde-ai not initialized. Run r2pde-ai init first.');
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const files = fs.readdirSync(paths.requirements).filter(f => f.endsWith('.md'));
|
|
16
|
+
if (files.length === 0) {
|
|
17
|
+
logInfo('No requirements found.');
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
logInfo('Select the artifact to permanently delete. This action cannot be undone and will remove the file from disk.');
|
|
21
|
+
const selectedName = await select({
|
|
22
|
+
message: 'Select a requirement to delete:',
|
|
23
|
+
choices: files.map(f => ({ name: f.replace('.md', ''), value: f.replace('.md', '') })),
|
|
24
|
+
});
|
|
25
|
+
const selected = `${selectedName}.md`;
|
|
26
|
+
const name = selected.replace(/\.md$/, '').replace(/-/g, ' ');
|
|
27
|
+
logWarn('This action cannot be undone.');
|
|
28
|
+
logInfo('Select Yes to confirm deletion.');
|
|
29
|
+
const doDelete = await confirm({ message: `Delete requirement '${name}'?`, default: false });
|
|
30
|
+
if (!doDelete) {
|
|
31
|
+
logInfo('Operation cancelled.');
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const filePath = path.join(paths.requirements, selected);
|
|
35
|
+
fs.unlinkSync(filePath);
|
|
36
|
+
// Step 5 — Log entry
|
|
37
|
+
const logPath = path.join(paths.logs, 'pde.log.md');
|
|
38
|
+
const logLine = `- ${new Date().toISOString()} | requirement:delete | ${selected} | deleted\n`;
|
|
39
|
+
fs.ensureFileSync(logPath);
|
|
40
|
+
fs.appendFileSync(logPath, logLine, { encoding: 'utf8' });
|
|
41
|
+
// Step 6 — Git commit if enabled
|
|
42
|
+
const config = loadConfig(cwd);
|
|
43
|
+
gitAddAndCommit(cwd, `feat(requirement): remove ${name}`, config);
|
|
44
|
+
// Step 7 — Final output
|
|
45
|
+
logSuccess(`Requirement '${name}' deleted.`);
|
|
46
|
+
}
|