cadr-cli 0.0.1 → 1.9.2

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.
Files changed (121) hide show
  1. package/dist/adr.d.ts +50 -0
  2. package/dist/adr.d.ts.map +1 -0
  3. package/dist/adr.js +156 -0
  4. package/dist/adr.js.map +1 -0
  5. package/dist/adr.test.d.ts +8 -0
  6. package/dist/adr.test.d.ts.map +1 -0
  7. package/dist/adr.test.js +256 -0
  8. package/dist/adr.test.js.map +1 -0
  9. package/dist/analysis.d.ts +24 -0
  10. package/dist/analysis.d.ts.map +1 -0
  11. package/dist/analysis.js +281 -0
  12. package/dist/analysis.js.map +1 -0
  13. package/dist/analysis.test.d.ts +8 -0
  14. package/dist/analysis.test.d.ts.map +1 -0
  15. package/dist/analysis.test.js +351 -0
  16. package/dist/analysis.test.js.map +1 -0
  17. package/dist/commands/analyze.d.ts +14 -0
  18. package/dist/commands/analyze.d.ts.map +1 -0
  19. package/dist/commands/analyze.js +56 -0
  20. package/dist/commands/analyze.js.map +1 -0
  21. package/dist/commands/init.d.ts +12 -0
  22. package/dist/commands/init.d.ts.map +1 -0
  23. package/dist/commands/init.js +93 -0
  24. package/dist/commands/init.js.map +1 -0
  25. package/dist/commands/init.test.d.ts +2 -0
  26. package/dist/commands/init.test.d.ts.map +1 -0
  27. package/dist/commands/init.test.js +56 -0
  28. package/dist/commands/init.test.js.map +1 -0
  29. package/dist/config.d.ts +40 -0
  30. package/dist/config.d.ts.map +1 -0
  31. package/dist/config.js +208 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/config.test.d.ts +2 -0
  34. package/dist/config.test.d.ts.map +1 -0
  35. package/dist/config.test.js +97 -0
  36. package/dist/config.test.js.map +1 -0
  37. package/dist/git.d.ts +42 -0
  38. package/dist/git.d.ts.map +1 -1
  39. package/dist/git.js +157 -0
  40. package/dist/git.js.map +1 -1
  41. package/dist/index.d.ts +2 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +78 -62
  44. package/dist/index.js.map +1 -1
  45. package/dist/index.test.d.ts +2 -0
  46. package/dist/index.test.d.ts.map +1 -0
  47. package/dist/index.test.js +51 -0
  48. package/dist/index.test.js.map +1 -0
  49. package/dist/llm.d.ts +73 -0
  50. package/dist/llm.d.ts.map +1 -0
  51. package/dist/llm.js +263 -0
  52. package/dist/llm.js.map +1 -0
  53. package/dist/llm.test.d.ts +2 -0
  54. package/dist/llm.test.d.ts.map +1 -0
  55. package/dist/llm.test.js +592 -0
  56. package/dist/llm.test.js.map +1 -0
  57. package/dist/logger.d.ts.map +1 -1
  58. package/dist/logger.js +5 -3
  59. package/dist/logger.js.map +1 -1
  60. package/dist/logger.test.d.ts +2 -0
  61. package/dist/logger.test.d.ts.map +1 -0
  62. package/dist/logger.test.js +78 -0
  63. package/dist/logger.test.js.map +1 -0
  64. package/dist/prompts.d.ts +49 -0
  65. package/dist/prompts.d.ts.map +1 -0
  66. package/dist/prompts.js +195 -0
  67. package/dist/prompts.js.map +1 -0
  68. package/dist/prompts.test.d.ts +2 -0
  69. package/dist/prompts.test.d.ts.map +1 -0
  70. package/dist/prompts.test.js +427 -0
  71. package/dist/prompts.test.js.map +1 -0
  72. package/dist/providers/gemini.d.ts +3 -0
  73. package/dist/providers/gemini.d.ts.map +1 -0
  74. package/dist/providers/gemini.js +39 -0
  75. package/dist/providers/gemini.js.map +1 -0
  76. package/dist/providers/index.d.ts +2 -0
  77. package/dist/providers/index.d.ts.map +1 -0
  78. package/dist/providers/index.js +6 -0
  79. package/dist/providers/index.js.map +1 -0
  80. package/dist/providers/openai.d.ts +3 -0
  81. package/dist/providers/openai.d.ts.map +1 -0
  82. package/dist/providers/openai.js +25 -0
  83. package/dist/providers/openai.js.map +1 -0
  84. package/dist/providers/registry.d.ts +4 -0
  85. package/dist/providers/registry.d.ts.map +1 -0
  86. package/dist/providers/registry.js +16 -0
  87. package/dist/providers/registry.js.map +1 -0
  88. package/dist/providers/types.d.ts +12 -0
  89. package/dist/providers/types.d.ts.map +1 -0
  90. package/dist/providers/types.js +5 -0
  91. package/dist/providers/types.js.map +1 -0
  92. package/dist/version.test.d.ts +3 -0
  93. package/dist/version.test.d.ts.map +1 -0
  94. package/dist/version.test.js +25 -0
  95. package/dist/version.test.js.map +1 -0
  96. package/package.json +14 -5
  97. package/src/adr.test.ts +278 -0
  98. package/src/adr.ts +136 -0
  99. package/src/analysis.test.ts +396 -0
  100. package/src/analysis.ts +262 -0
  101. package/src/commands/analyze.ts +56 -0
  102. package/src/commands/init.test.ts +27 -0
  103. package/src/commands/init.ts +99 -0
  104. package/src/config.test.ts +79 -0
  105. package/src/config.ts +214 -0
  106. package/src/git.ts +240 -0
  107. package/src/index.test.ts +59 -0
  108. package/src/index.ts +80 -60
  109. package/src/llm.test.ts +701 -0
  110. package/src/llm.ts +344 -0
  111. package/src/logger.test.ts +90 -0
  112. package/src/logger.ts +6 -3
  113. package/src/prompts.test.ts +515 -0
  114. package/src/prompts.ts +174 -0
  115. package/src/providers/gemini.ts +41 -0
  116. package/src/providers/index.ts +1 -0
  117. package/src/providers/openai.ts +22 -0
  118. package/src/providers/registry.ts +16 -0
  119. package/src/providers/types.ts +14 -0
  120. package/src/version.test.ts +29 -0
  121. package/bin/cadr.js +0 -16
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ // Test version constants in CLI
3
+ const CORE_VERSION = '0.0.1';
4
+ const CLI_VERSION = '0.0.1';
5
+ describe('CLI Version Constants', () => {
6
+ test('exports CORE_VERSION constant', () => {
7
+ expect(CORE_VERSION).toBe('0.0.1');
8
+ });
9
+ test('CORE_VERSION is a string', () => {
10
+ expect(typeof CORE_VERSION).toBe('string');
11
+ });
12
+ test('CORE_VERSION matches semantic version pattern', () => {
13
+ expect(CORE_VERSION).toMatch(/^\d+\.\d+\.\d+$/);
14
+ });
15
+ test('exports CLI_VERSION constant', () => {
16
+ expect(CLI_VERSION).toBe('0.0.1');
17
+ });
18
+ test('CLI_VERSION is a string', () => {
19
+ expect(typeof CLI_VERSION).toBe('string');
20
+ });
21
+ test('CLI_VERSION matches semantic version pattern', () => {
22
+ expect(CLI_VERSION).toMatch(/^\d+\.\d+\.\d+$/);
23
+ });
24
+ });
25
+ //# sourceMappingURL=version.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.test.js","sourceRoot":"","sources":["../src/version.test.ts"],"names":[],"mappings":";AAAA,gCAAgC;AAChC,MAAM,YAAY,GAAG,OAAO,CAAC;AAC7B,MAAM,WAAW,GAAG,OAAO,CAAC;AAE5B,QAAQ,CAAC,uBAAuB,EAAE,GAAG,EAAE;IACrC,IAAI,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACzC,MAAM,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACrC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,0BAA0B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,OAAO,YAAY,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACzD,MAAM,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IAClD,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,8BAA8B,EAAE,GAAG,EAAE;QACxC,MAAM,CAAC,WAAW,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,yBAAyB,EAAE,GAAG,EAAE;QACnC,MAAM,CAAC,OAAO,WAAW,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;IAEH,IAAI,CAAC,8CAA8C,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,WAAW,CAAC,CAAC,OAAO,CAAC,iBAAiB,CAAC,CAAC;IACjD,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
package/package.json CHANGED
@@ -1,13 +1,14 @@
1
1
  {
2
2
  "name": "cadr-cli",
3
- "version": "0.0.1",
3
+ "version": "1.9.2",
4
4
  "description": "Continuous Architectural Decision Records - Automatically capture ADRs as you code",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
- "cadr": "./bin/cadr.js"
7
+ "cadr": "./dist/index.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "tsc",
10
+ "build": "tsc && npm run postbuild",
11
+ "postbuild": "if [ -f dist/index.js ]; then if ! head -1 dist/index.js | grep -q '^#!/usr/bin/env node$'; then echo '#!/usr/bin/env node' | cat - dist/index.js > dist/index.tmp && mv dist/index.tmp dist/index.js; fi; chmod +x dist/index.js; fi",
11
12
  "test": "jest"
12
13
  },
13
14
  "keywords": [
@@ -17,7 +18,7 @@
17
18
  "decision-records",
18
19
  "documentation"
19
20
  ],
20
- "author": "",
21
+ "author": "Yotpo Ltd.",
21
22
  "license": "MIT",
22
23
  "repository": {
23
24
  "type": "git",
@@ -26,7 +27,15 @@
26
27
  "engines": {
27
28
  "node": ">=20.0.0"
28
29
  },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
29
33
  "dependencies": {
30
- "pino": "^8.17.2"
34
+ "pino": "^8.17.2",
35
+ "openai": "^4.0.0",
36
+ "@google/generative-ai": "^0.21.0",
37
+ "js-yaml": "^4.1.0",
38
+ "yup": "^1.0.0",
39
+ "commander": "^11.0.0"
31
40
  }
32
41
  }
@@ -0,0 +1,278 @@
1
+ /**
2
+ * ADR File Management Module Tests
3
+ *
4
+ * Tests for ADR file creation, numbering, and management.
5
+ * Following TDD: These tests are written BEFORE implementation.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import {
11
+ titleToSlug,
12
+ getNextADRNumber,
13
+ generateADRFilename,
14
+ ensureADRDirectory,
15
+ saveADR,
16
+ DEFAULT_ADR_DIR,
17
+ } from './adr';
18
+
19
+ describe('ADR Module', () => {
20
+ const testDir = path.join(__dirname, '../test-adrs');
21
+
22
+ beforeEach(() => {
23
+ // Clean up test directory before each test
24
+ if (fs.existsSync(testDir)) {
25
+ fs.rmSync(testDir, { recursive: true, force: true });
26
+ }
27
+ });
28
+
29
+ afterEach(() => {
30
+ // Clean up test directory after each test
31
+ if (fs.existsSync(testDir)) {
32
+ fs.rmSync(testDir, { recursive: true, force: true });
33
+ }
34
+ });
35
+
36
+ describe('titleToSlug', () => {
37
+ it('converts title to lowercase slug', () => {
38
+ expect(titleToSlug('Use PostgreSQL for Storage')).toBe('use-postgresql-for-storage');
39
+ });
40
+
41
+ it('replaces spaces with hyphens', () => {
42
+ expect(titleToSlug('My Great Decision')).toBe('my-great-decision');
43
+ });
44
+
45
+ it('handles special characters', () => {
46
+ expect(titleToSlug('API v2.0: New Endpoints!')).toBe('api-v2-0-new-endpoints');
47
+ });
48
+
49
+ it('removes leading and trailing hyphens', () => {
50
+ expect(titleToSlug('---Test Title---')).toBe('test-title');
51
+ });
52
+
53
+ it('handles multiple consecutive spaces', () => {
54
+ expect(titleToSlug('Too Many Spaces')).toBe('too-many-spaces');
55
+ });
56
+
57
+ it('handles empty string', () => {
58
+ expect(titleToSlug('')).toBe('');
59
+ });
60
+
61
+ it('handles only special characters', () => {
62
+ expect(titleToSlug('!!!')).toBe('');
63
+ });
64
+ });
65
+
66
+ describe('generateADRFilename', () => {
67
+ it('generates properly formatted filename', () => {
68
+ expect(generateADRFilename(1, 'Use PostgreSQL')).toBe('0001-use-postgresql.md');
69
+ });
70
+
71
+ it('pads numbers with zeros to 4 digits', () => {
72
+ expect(generateADRFilename(42, 'Test Decision')).toBe('0042-test-decision.md');
73
+ });
74
+
75
+ it('handles three-digit numbers', () => {
76
+ expect(generateADRFilename(123, 'Another Decision')).toBe('0123-another-decision.md');
77
+ });
78
+
79
+ it('handles four-digit numbers', () => {
80
+ expect(generateADRFilename(9999, 'Max Decision')).toBe('9999-max-decision.md');
81
+ });
82
+
83
+ it('handles titles with special characters', () => {
84
+ expect(generateADRFilename(5, 'Switch to React v18!')).toBe('0005-switch-to-react-v18.md');
85
+ });
86
+ });
87
+
88
+ describe('getNextADRNumber', () => {
89
+ it('returns 1 for non-existent directory', () => {
90
+ expect(getNextADRNumber(testDir)).toBe(1);
91
+ });
92
+
93
+ it('returns 1 for empty directory', () => {
94
+ fs.mkdirSync(testDir, { recursive: true });
95
+ expect(getNextADRNumber(testDir)).toBe(1);
96
+ });
97
+
98
+ it('returns next number after existing ADRs', () => {
99
+ fs.mkdirSync(testDir, { recursive: true });
100
+ fs.writeFileSync(path.join(testDir, '0001-first.md'), '# First');
101
+ fs.writeFileSync(path.join(testDir, '0002-second.md'), '# Second');
102
+ expect(getNextADRNumber(testDir)).toBe(3);
103
+ });
104
+
105
+ it('handles non-sequential numbers correctly', () => {
106
+ fs.mkdirSync(testDir, { recursive: true });
107
+ fs.writeFileSync(path.join(testDir, '0001-first.md'), '# First');
108
+ fs.writeFileSync(path.join(testDir, '0005-fifth.md'), '# Fifth');
109
+ expect(getNextADRNumber(testDir)).toBe(6);
110
+ });
111
+
112
+ it('ignores files without number prefix', () => {
113
+ fs.mkdirSync(testDir, { recursive: true });
114
+ fs.writeFileSync(path.join(testDir, '0001-first.md'), '# First');
115
+ fs.writeFileSync(path.join(testDir, 'README.md'), '# README');
116
+ fs.writeFileSync(path.join(testDir, 'template.md'), '# Template');
117
+ expect(getNextADRNumber(testDir)).toBe(2);
118
+ });
119
+
120
+ it('ignores files with incorrect number format', () => {
121
+ fs.mkdirSync(testDir, { recursive: true });
122
+ fs.writeFileSync(path.join(testDir, '0001-first.md'), '# First');
123
+ fs.writeFileSync(path.join(testDir, '1-wrong.md'), '# Wrong');
124
+ fs.writeFileSync(path.join(testDir, '00002-also-wrong.md'), '# Also Wrong');
125
+ expect(getNextADRNumber(testDir)).toBe(2);
126
+ });
127
+
128
+ it('handles directory with only non-ADR files', () => {
129
+ fs.mkdirSync(testDir, { recursive: true });
130
+ fs.writeFileSync(path.join(testDir, 'README.md'), '# README');
131
+ expect(getNextADRNumber(testDir)).toBe(1);
132
+ });
133
+ });
134
+
135
+ describe('ensureADRDirectory', () => {
136
+ it('creates directory if it does not exist', () => {
137
+ ensureADRDirectory(testDir);
138
+ expect(fs.existsSync(testDir)).toBe(true);
139
+ expect(fs.statSync(testDir).isDirectory()).toBe(true);
140
+ });
141
+
142
+ it('creates nested directories recursively', () => {
143
+ const nestedDir = path.join(testDir, 'nested', 'path');
144
+ ensureADRDirectory(nestedDir);
145
+ expect(fs.existsSync(nestedDir)).toBe(true);
146
+ expect(fs.statSync(nestedDir).isDirectory()).toBe(true);
147
+ });
148
+
149
+ it('does not error if directory already exists', () => {
150
+ fs.mkdirSync(testDir, { recursive: true });
151
+ expect(() => ensureADRDirectory(testDir)).not.toThrow();
152
+ expect(fs.existsSync(testDir)).toBe(true);
153
+ });
154
+
155
+ it('can be called multiple times safely', () => {
156
+ ensureADRDirectory(testDir);
157
+ ensureADRDirectory(testDir);
158
+ ensureADRDirectory(testDir);
159
+ expect(fs.existsSync(testDir)).toBe(true);
160
+ });
161
+ });
162
+
163
+ describe('saveADR', () => {
164
+ it('saves ADR with correct filename in specified directory', () => {
165
+ const content = '# Use PostgreSQL\n\n* Status: accepted\n\nContent here';
166
+ const result = saveADR(content, 'Use PostgreSQL', testDir);
167
+
168
+ expect(result.success).toBe(true);
169
+ expect(result.filePath).toBe(path.join(testDir, '0001-use-postgresql.md'));
170
+ expect(result.error).toBeUndefined();
171
+ expect(fs.existsSync(result.filePath!)).toBe(true);
172
+ });
173
+
174
+ it('creates directory automatically if it does not exist', () => {
175
+ const content = '# Test ADR\n\nContent';
176
+ const result = saveADR(content, 'Test ADR', testDir);
177
+
178
+ expect(fs.existsSync(testDir)).toBe(true);
179
+ expect(result.success).toBe(true);
180
+ });
181
+
182
+ it('increments number for multiple ADRs', () => {
183
+ const result1 = saveADR('# First\n\nFirst ADR', 'First', testDir);
184
+ const result2 = saveADR('# Second\n\nSecond ADR', 'Second', testDir);
185
+ const result3 = saveADR('# Third\n\nThird ADR', 'Third', testDir);
186
+
187
+ expect(result1.filePath).toContain('0001-first.md');
188
+ expect(result2.filePath).toContain('0002-second.md');
189
+ expect(result3.filePath).toContain('0003-third.md');
190
+
191
+ expect(fs.existsSync(result1.filePath!)).toBe(true);
192
+ expect(fs.existsSync(result2.filePath!)).toBe(true);
193
+ expect(fs.existsSync(result3.filePath!)).toBe(true);
194
+ });
195
+
196
+ it('saves correct content to file', () => {
197
+ const content = '# My Decision\n\n* Status: accepted\n* Date: 2025-10-21\n\n## Context\n\nSome context here.';
198
+ const result = saveADR(content, 'My Decision', testDir);
199
+
200
+ const savedContent = fs.readFileSync(result.filePath!, 'utf-8');
201
+ expect(savedContent).toBe(content);
202
+ });
203
+
204
+ it('uses default directory when not specified', () => {
205
+ // Clean up default directory
206
+ const defaultPath = path.join(process.cwd(), DEFAULT_ADR_DIR);
207
+ if (fs.existsSync(defaultPath)) {
208
+ fs.rmSync(defaultPath, { recursive: true, force: true });
209
+ }
210
+
211
+ const content = '# Default Location\n\nContent';
212
+ const result = saveADR(content, 'Default Location');
213
+
214
+ expect(result.success).toBe(true);
215
+ expect(result.filePath).toContain(DEFAULT_ADR_DIR);
216
+
217
+ // Cleanup
218
+ if (fs.existsSync(defaultPath)) {
219
+ fs.rmSync(defaultPath, { recursive: true, force: true });
220
+ }
221
+ });
222
+
223
+ it('handles file write errors gracefully', () => {
224
+ // Create a read-only directory to trigger write error
225
+ fs.mkdirSync(testDir, { recursive: true });
226
+
227
+ // Make directory read-only (if not Windows)
228
+ if (process.platform !== 'win32') {
229
+ fs.chmodSync(testDir, 0o444);
230
+
231
+ const content = '# Test\n\nContent';
232
+ const result = saveADR(content, 'Test', testDir);
233
+
234
+ expect(result.success).toBe(false);
235
+ expect(result.error).toBeDefined();
236
+ expect(result.filePath).toBeUndefined();
237
+
238
+ // Restore permissions for cleanup
239
+ fs.chmodSync(testDir, 0o755);
240
+ } else {
241
+ // On Windows, just verify error handling structure exists
242
+ // Skip actual permission test as Windows permissions work differently
243
+ const content = '# Test\n\nContent';
244
+ const result = saveADR(content, 'Test', testDir);
245
+
246
+ // Should succeed on Windows since we can't easily simulate permission errors
247
+ expect(result).toHaveProperty('success');
248
+ expect(typeof result.success).toBe('boolean');
249
+ }
250
+ });
251
+
252
+ it('handles long titles correctly', () => {
253
+ const longTitle = 'This is a very long title that should still work correctly when converted to a filename slug';
254
+ const content = `# ${longTitle}\n\nContent`;
255
+ const result = saveADR(content, longTitle, testDir);
256
+
257
+ expect(result.success).toBe(true);
258
+ expect(result.filePath).toContain('0001-this-is-a-very-long-title');
259
+ });
260
+
261
+ it('handles titles with unicode characters', () => {
262
+ const content = '# Decision with émojis 🚀\n\nContent';
263
+ const result = saveADR(content, 'Decision with émojis 🚀', testDir);
264
+
265
+ expect(result.success).toBe(true);
266
+ // Should strip unicode to safe characters
267
+ expect(result.filePath).toMatch(/0001-.+\.md$/);
268
+ });
269
+ });
270
+
271
+ describe('DEFAULT_ADR_DIR constant', () => {
272
+ it('is defined and has expected value', () => {
273
+ expect(DEFAULT_ADR_DIR).toBeDefined();
274
+ expect(DEFAULT_ADR_DIR).toBe('docs/adr');
275
+ });
276
+ });
277
+ });
278
+
package/src/adr.ts ADDED
@@ -0,0 +1,136 @@
1
+ /**
2
+ * ADR File Management Module
3
+ *
4
+ * Handles creation and management of Architectural Decision Record files.
5
+ * Follows MADR (Markdown Architectural Decision Records) format.
6
+ */
7
+
8
+ import * as fs from 'fs';
9
+ import * as path from 'path';
10
+ import { loggerInstance as logger } from './logger';
11
+
12
+ /**
13
+ * Default ADR directory relative to project root
14
+ */
15
+ export const DEFAULT_ADR_DIR = 'docs/adr';
16
+
17
+ /**
18
+ * Convert a title to a filename-safe slug
19
+ * Example: "Use PostgreSQL for Storage" -> "use-postgresql-for-storage"
20
+ */
21
+ export function titleToSlug(title: string): string {
22
+ return title
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with hyphens
25
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
26
+ }
27
+
28
+ /**
29
+ * Get the next ADR number by scanning existing ADR files
30
+ *
31
+ * @param adrDir - Directory containing ADR files
32
+ * @returns Next available ADR number (e.g., 1, 2, 3...)
33
+ */
34
+ export function getNextADRNumber(adrDir: string): number {
35
+ try {
36
+ if (!fs.existsSync(adrDir)) {
37
+ return 1; // First ADR
38
+ }
39
+
40
+ const files = fs.readdirSync(adrDir);
41
+ const adrNumbers: number[] = [];
42
+
43
+ // Extract numbers from existing ADR files (format: 0001-title.md)
44
+ for (const file of files) {
45
+ const match = file.match(/^(\d{4})-/);
46
+ if (match) {
47
+ adrNumbers.push(parseInt(match[1], 10));
48
+ }
49
+ }
50
+
51
+ if (adrNumbers.length === 0) {
52
+ return 1;
53
+ }
54
+
55
+ // Return next number after the highest existing number
56
+ return Math.max(...adrNumbers) + 1;
57
+ } catch (error) {
58
+ logger.warn('Failed to scan existing ADRs, defaulting to 1', { error, adrDir });
59
+ return 1;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Ensure ADR directory exists, create if it doesn't
65
+ *
66
+ * @param adrDir - Directory path to ensure exists
67
+ */
68
+ export function ensureADRDirectory(adrDir: string): void {
69
+ if (!fs.existsSync(adrDir)) {
70
+ logger.info('Creating ADR directory', { adrDir });
71
+ fs.mkdirSync(adrDir, { recursive: true });
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Generate ADR filename from number and title
77
+ *
78
+ * @param number - ADR number (will be zero-padded to 4 digits)
79
+ * @param title - ADR title (will be converted to slug)
80
+ * @returns Filename like "0001-use-postgresql-for-storage.md"
81
+ */
82
+ export function generateADRFilename(number: number, title: string): string {
83
+ const paddedNumber = String(number).padStart(4, '0');
84
+ const slug = titleToSlug(title);
85
+ return `${paddedNumber}-${slug}.md`;
86
+ }
87
+
88
+ /**
89
+ * Save ADR content to file
90
+ *
91
+ * @param content - Full markdown content of the ADR
92
+ * @param title - Title extracted from ADR content
93
+ * @param adrDir - Directory to save ADR in (defaults to docs/adr)
94
+ * @returns Object with success status, file path, and any error
95
+ */
96
+ export function saveADR(
97
+ content: string,
98
+ title: string,
99
+ adrDir: string = DEFAULT_ADR_DIR
100
+ ): { success: boolean; filePath?: string; error?: string } {
101
+ try {
102
+ // Ensure directory exists
103
+ ensureADRDirectory(adrDir);
104
+
105
+ // Get next ADR number
106
+ const adrNumber = getNextADRNumber(adrDir);
107
+
108
+ // Generate filename
109
+ const filename = generateADRFilename(adrNumber, title);
110
+ const filePath = path.join(adrDir, filename);
111
+
112
+ // Check if file already exists (shouldn't happen, but safety check)
113
+ if (fs.existsSync(filePath)) {
114
+ logger.warn('ADR file already exists, using alternative name', { filePath });
115
+ const alternativeFilename = generateADRFilename(adrNumber + 1, title);
116
+ const alternativeFilePath = path.join(adrDir, alternativeFilename);
117
+
118
+ fs.writeFileSync(alternativeFilePath, content, 'utf-8');
119
+ logger.info('ADR saved successfully', { filePath: alternativeFilePath });
120
+
121
+ return { success: true, filePath: alternativeFilePath };
122
+ }
123
+
124
+ // Write ADR content to file
125
+ fs.writeFileSync(filePath, content, 'utf-8');
126
+
127
+ logger.info('ADR saved successfully', { filePath, adrNumber });
128
+
129
+ return { success: true, filePath };
130
+ } catch (error) {
131
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
132
+ logger.error('Failed to save ADR', { error, adrDir });
133
+ return { success: false, error: errorMessage };
134
+ }
135
+ }
136
+