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.
@@ -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
+ }