@meza/adr-tools 2.0.3 → 2.0.4

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 (122) hide show
  1. package/dist/types/version.d.ts +1 -1
  2. package/dist/version.js +1 -1
  3. package/package.json +19 -9
  4. package/.adr-dir +0 -1
  5. package/.editorconfig +0 -13
  6. package/.gitattributes +0 -40
  7. package/.github/renovate.json +0 -4
  8. package/.github/stale.yml +0 -18
  9. package/.github/workflows/ci-pr.yml +0 -48
  10. package/.github/workflows/ci.yml +0 -67
  11. package/.github/workflows/security.yml +0 -49
  12. package/.releaserc.json +0 -55
  13. package/.yarnrc.yml +0 -8
  14. package/AGENTS.engineer.md +0 -236
  15. package/AGENTS.md +0 -11
  16. package/AGENTS.reviewer.md +0 -115
  17. package/CHANGELOG.md +0 -7
  18. package/CONTRIBUTING.md +0 -102
  19. package/biome.json +0 -28
  20. package/dist/index.test.js +0 -219
  21. package/dist/index.test.js.map +0 -1
  22. package/dist/inject-version.test.js +0 -27
  23. package/dist/inject-version.test.js.map +0 -1
  24. package/dist/lib/adr.test.js +0 -319
  25. package/dist/lib/adr.test.js.map +0 -1
  26. package/dist/lib/config.test.js +0 -60
  27. package/dist/lib/config.test.js.map +0 -1
  28. package/dist/lib/links.test.js +0 -66
  29. package/dist/lib/links.test.js.map +0 -1
  30. package/dist/lib/manipulator-errors.test.js +0 -21
  31. package/dist/lib/manipulator-errors.test.js.map +0 -1
  32. package/dist/lib/manipulator.test.js +0 -96
  33. package/dist/lib/manipulator.test.js.map +0 -1
  34. package/dist/lib/numbering.test.js +0 -39
  35. package/dist/lib/numbering.test.js.map +0 -1
  36. package/dist/lib/opening.test.js +0 -81
  37. package/dist/lib/opening.test.js.map +0 -1
  38. package/dist/lib/prompt.test.js +0 -33
  39. package/dist/lib/prompt.test.js.map +0 -1
  40. package/dist/lib/template.test.js +0 -62
  41. package/dist/lib/template.test.js.map +0 -1
  42. package/dist/types/index.test.d.ts +0 -2
  43. package/dist/types/index.test.d.ts.map +0 -1
  44. package/dist/types/inject-version.test.d.ts +0 -2
  45. package/dist/types/inject-version.test.d.ts.map +0 -1
  46. package/dist/types/lib/adr.test.d.ts +0 -2
  47. package/dist/types/lib/adr.test.d.ts.map +0 -1
  48. package/dist/types/lib/config.test.d.ts +0 -2
  49. package/dist/types/lib/config.test.d.ts.map +0 -1
  50. package/dist/types/lib/links.test.d.ts +0 -2
  51. package/dist/types/lib/links.test.d.ts.map +0 -1
  52. package/dist/types/lib/manipulator-errors.test.d.ts +0 -2
  53. package/dist/types/lib/manipulator-errors.test.d.ts.map +0 -1
  54. package/dist/types/lib/manipulator.test.d.ts +0 -2
  55. package/dist/types/lib/manipulator.test.d.ts.map +0 -1
  56. package/dist/types/lib/numbering.test.d.ts +0 -2
  57. package/dist/types/lib/numbering.test.d.ts.map +0 -1
  58. package/dist/types/lib/opening.test.d.ts +0 -2
  59. package/dist/types/lib/opening.test.d.ts.map +0 -1
  60. package/dist/types/lib/prompt.test.d.ts +0 -2
  61. package/dist/types/lib/prompt.test.d.ts.map +0 -1
  62. package/dist/types/lib/template.test.d.ts +0 -2
  63. package/dist/types/lib/template.test.d.ts.map +0 -1
  64. package/doc/adr/.adr-sequence.lock +0 -1
  65. package/doc/adr/0001-record-architecture-decisions.md +0 -21
  66. package/doc/adr/0002-using-heavy-e2e-tests.md +0 -20
  67. package/doc/adr/0003-esm.md +0 -34
  68. package/doc/adr/0004-gate-editor-opening-behind---open-and---open-with.md +0 -55
  69. package/doc/adr/decisions.md +0 -6
  70. package/lefthook.yml +0 -14
  71. package/scripts/inject-version.mjs +0 -34
  72. package/src/index.test.ts +0 -247
  73. package/src/index.ts +0 -267
  74. package/src/inject-version.test.ts +0 -31
  75. package/src/lib/adr.test.ts +0 -389
  76. package/src/lib/adr.ts +0 -384
  77. package/src/lib/config.test.ts +0 -69
  78. package/src/lib/config.ts +0 -36
  79. package/src/lib/links.test.ts +0 -74
  80. package/src/lib/links.ts +0 -34
  81. package/src/lib/manipulator-errors.test.ts +0 -22
  82. package/src/lib/manipulator.test.ts +0 -109
  83. package/src/lib/manipulator.ts +0 -98
  84. package/src/lib/numbering.test.ts +0 -45
  85. package/src/lib/numbering.ts +0 -25
  86. package/src/lib/opening.test.ts +0 -96
  87. package/src/lib/prompt.test.ts +0 -42
  88. package/src/lib/prompt.ts +0 -14
  89. package/src/lib/template.test.ts +0 -74
  90. package/src/lib/template.ts +0 -22
  91. package/src/templates/init.md +0 -21
  92. package/src/templates/template.md +0 -19
  93. package/src/types/environment.d.ts +0 -14
  94. package/src/version.ts +0 -1
  95. package/tests/.adr-dir +0 -1
  96. package/tests/__snapshots__/generate-graph.e2e.test.ts.snap +0 -39
  97. package/tests/__snapshots__/init-adr-repository.e2e.test.ts.snap +0 -51
  98. package/tests/__snapshots__/linking-records.e2e.test.ts.snap +0 -155
  99. package/tests/__snapshots__/new-adr.e2e.test.ts.snap +0 -54
  100. package/tests/__snapshots__/superseding-records.e2e.test.ts.snap +0 -122
  101. package/tests/__snapshots__/toc-prefixing.e2e.test.ts.snap +0 -9
  102. package/tests/__snapshots__/use-template-override.e2e.test.ts.snap +0 -17
  103. package/tests/edit-on-create.e2e.test.ts +0 -90
  104. package/tests/fake-editor +0 -5
  105. package/tests/fake-editor.cmd +0 -4
  106. package/tests/fake-visual +0 -5
  107. package/tests/fake-visual.cmd +0 -4
  108. package/tests/funny-characters.e2e.test.ts +0 -51
  109. package/tests/generate-graph.e2e.test.ts +0 -49
  110. package/tests/helpers/adr-cli.ts +0 -24
  111. package/tests/init-adr-repository.e2e.test.ts +0 -58
  112. package/tests/linking-records.e2e.test.ts +0 -72
  113. package/tests/list-adrs.e2e.test.ts +0 -60
  114. package/tests/new-adr.e2e.test.ts +0 -62
  115. package/tests/open-with.e2e.test.ts +0 -53
  116. package/tests/superseding-records.e2e.test.ts +0 -64
  117. package/tests/toc-prefixing.e2e.test.ts +0 -43
  118. package/tests/use-template-override.e2e.test.ts +0 -52
  119. package/tests/work-form-other-directories.e2e.test.ts +0 -48
  120. package/tsconfig.json +0 -55
  121. package/vitest.config.e2e.ts +0 -18
  122. package/vitest.config.ts +0 -30
package/src/lib/adr.ts DELETED
@@ -1,384 +0,0 @@
1
- import path from 'node:path';
2
- import { fileURLToPath } from 'node:url';
3
- import fs from 'fs/promises';
4
- import type { Options as OpenOptions } from 'open';
5
- import { getDir, workingDir } from './config.js';
6
- import { findMatchingFilesFor, getLinkDetails } from './links.js';
7
- import { getTitleFrom, injectLink, supersede } from './manipulator.js';
8
- import { newNumber } from './numbering.js';
9
- import { askForClarification } from './prompt.js';
10
- import { template } from './template.js';
11
-
12
- const __filename = fileURLToPath(import.meta.url);
13
-
14
- const normalizeEditorCommand = (raw: string | undefined): string | undefined => {
15
- if (!raw) {
16
- return undefined;
17
- }
18
-
19
- const trimmed = raw.trim();
20
- if (!trimmed) {
21
- return undefined;
22
- }
23
-
24
- const unquoted = trimmed.replace(/^['"](.+)['"]$/, '$1').trim();
25
- if (!unquoted) {
26
- return undefined;
27
- }
28
-
29
- const lowered = unquoted.toLowerCase();
30
- if (lowered === 'true' || lowered === 'false') {
31
- return undefined;
32
- }
33
-
34
- return unquoted;
35
- };
36
-
37
- const unescapeDoubleQuoted = (value: string): string => {
38
- let result = '';
39
- for (let i = 0; i < value.length; i++) {
40
- const ch = value[i];
41
- if (ch === '\\' && i + 1 < value.length) {
42
- const next = value[i + 1];
43
- if (next === '\\' || next === '"') {
44
- result += next;
45
- i++;
46
- continue;
47
- }
48
- }
49
- result += ch;
50
- }
51
- return result;
52
- };
53
-
54
- const splitCommandLine = (commandLine: string): string[] => {
55
- const tokens = commandLine.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g);
56
- if (!tokens) {
57
- return [];
58
- }
59
-
60
- return tokens.map((token) => {
61
- if (token.startsWith('"') && token.endsWith('"')) {
62
- return unescapeDoubleQuoted(token.slice(1, -1));
63
- }
64
-
65
- if (token.startsWith("'") && token.endsWith("'")) {
66
- return token.slice(1, -1);
67
- }
68
-
69
- return token;
70
- });
71
- };
72
-
73
- type OpenPlan = { type: 'none' } | { type: 'default' } | { type: 'app'; name: string; args: string[] };
74
-
75
- const openAdr = async (adrPath: string, options: OpenOptions, openPromise: Promise<typeof import('open')>) => {
76
- const { default: open } = await openPromise;
77
- await open(adrPath, options);
78
- };
79
-
80
- const resolveOpenPlan = (config?: NewOptions): OpenPlan => {
81
- if (!config || (config.open === undefined && !config.openWith)) {
82
- return chooseOpenPlan(undefined);
83
- }
84
-
85
- return chooseOpenPlan({
86
- ...(config.open !== undefined ? { open: config.open } : {}),
87
- ...(config.openWith ? { openWith: config.openWith } : {})
88
- });
89
- };
90
-
91
- const shouldLoadOpenLibrary = (openPlan: OpenPlan) => openPlan.type !== 'none';
92
-
93
- const openNewAdr = async (openPlan: OpenPlan, adrPath: string, openPromise?: Promise<typeof import('open')>) => {
94
- if (openPlan.type === 'none') {
95
- return;
96
- }
97
-
98
- if (openPlan.type === 'default') {
99
- await openAdr(adrPath, { wait: false }, openPromise!);
100
- return;
101
- }
102
-
103
- await openAdr(
104
- adrPath,
105
- { app: { name: openPlan.name, arguments: openPlan.args }, wait: process.platform === 'win32' },
106
- openPromise!
107
- );
108
- };
109
-
110
- const equalsIgnoreCase = (a: string, b: string) => a.toLowerCase() === b.toLowerCase();
111
-
112
- const isNpmInjectedEditor = (editor: string) => {
113
- const env = process.env as NodeJS.ProcessEnv & {
114
- npm_config_editor?: string;
115
- npm_execpath?: string;
116
- };
117
- const npmEditor = normalizeEditorCommand(env.npm_config_editor);
118
- if (!npmEditor) {
119
- return false;
120
- }
121
-
122
- if (!equalsIgnoreCase(editor, npmEditor)) {
123
- return false;
124
- }
125
-
126
- return Boolean(env.npm_execpath);
127
- };
128
-
129
- const toAppPlan = (commandLine: string | undefined): OpenPlan | undefined => {
130
- const normalized = normalizeEditorCommand(commandLine);
131
- if (!normalized) {
132
- return undefined;
133
- }
134
-
135
- const [name, ...args] = splitCommandLine(normalized);
136
- if (!name) {
137
- return undefined;
138
- }
139
-
140
- return { type: 'app', name, args };
141
- };
142
-
143
- export const chooseOpenPlan = (options?: { open?: boolean; openWith?: string }): OpenPlan => {
144
- const openWithPlan = toAppPlan(options?.openWith);
145
- const shouldOpen = Boolean(options?.open) || Boolean(openWithPlan);
146
- if (!shouldOpen) {
147
- return { type: 'none' };
148
- }
149
-
150
- if (openWithPlan) {
151
- return openWithPlan;
152
- }
153
-
154
- const visualPlan = toAppPlan(process.env.VISUAL);
155
- if (visualPlan) {
156
- return visualPlan;
157
- }
158
-
159
- const editorCommand = normalizeEditorCommand(process.env.EDITOR);
160
- if (editorCommand && !isNpmInjectedEditor(editorCommand)) {
161
- const editorPlan = toAppPlan(editorCommand);
162
- if (editorPlan) {
163
- return editorPlan;
164
- }
165
- }
166
-
167
- return { type: 'default' };
168
- };
169
-
170
- interface NewOptions {
171
- supersedes?: string[];
172
- date?: string | undefined;
173
- suppressPrompts?: boolean;
174
- template?: string;
175
- links?: string[];
176
- open?: boolean;
177
- openWith?: string;
178
- }
179
-
180
- // eslint-disable-next-line no-unused-vars
181
- enum LinkType {
182
- // eslint-disable-next-line no-unused-vars
183
- LINK = 'link',
184
- // eslint-disable-next-line no-unused-vars
185
- SUPERSEDE = 'supersede'
186
- }
187
-
188
- interface LinkTask {
189
- type: LinkType;
190
- sourcePath: string;
191
- targetPath: string;
192
- link: string;
193
- reverseLink: string;
194
- }
195
-
196
- const actuallyLink = async (task: LinkTask) => {
197
- const linkedFile = path.join(await getDir(), task.targetPath);
198
- const newAdrContent = await fs.readFile(task.sourcePath, 'utf8');
199
- const oldAdrContent = await fs.readFile(linkedFile, 'utf8');
200
- const oldTitle = getTitleFrom(oldAdrContent);
201
- const newTitle = getTitleFrom(newAdrContent);
202
- let dirtyOld = '',
203
- dirtyNew = '';
204
- switch (task.type) {
205
- case LinkType.LINK:
206
- dirtyOld = injectLink(
207
- oldAdrContent,
208
- `${task.reverseLink} [${newTitle}](${path.relative(await getDir(), task.sourcePath)})`
209
- );
210
- dirtyNew = injectLink(newAdrContent, `${task.link} [${oldTitle}](${task.targetPath})`);
211
- break;
212
- case LinkType.SUPERSEDE:
213
- dirtyOld = supersede(
214
- oldAdrContent,
215
- `${task.reverseLink} [${newTitle}](${path.relative(await getDir(), task.sourcePath)})`
216
- );
217
- dirtyNew = injectLink(newAdrContent, `${task.link} [${oldTitle}](${task.targetPath})`);
218
- break;
219
- }
220
-
221
- await fs.writeFile(linkedFile, dirtyOld);
222
- await fs.writeFile(task.sourcePath, dirtyNew);
223
- };
224
-
225
- const processSupersedes = async (sourcePath: string, supersedes: string[] = [], suppressPrompts: boolean = false) => {
226
- if (supersedes.length === 0) {
227
- return;
228
- }
229
-
230
- const supersedeStrings = await Promise.all(supersedes.map((link) => getLinkDetails(link, true)));
231
-
232
- for (const targetDetails of supersedeStrings) {
233
- const task: LinkTask = {
234
- type: LinkType.SUPERSEDE,
235
- sourcePath: sourcePath,
236
- link: targetDetails.link,
237
- reverseLink: targetDetails.reverseLink,
238
- targetPath: targetDetails.matches[0]
239
- };
240
-
241
- if (targetDetails.matches.length > 1) {
242
- if (suppressPrompts) {
243
- throw new Error(
244
- `Multiple files match the search pattern for "${targetDetails.original}".\n` +
245
- 'Please specify which file you want to targetDetails to more or remove the -q or --quiet options from the command line.'
246
- );
247
- }
248
- task.targetPath = await askForClarification(targetDetails.original, targetDetails.matches);
249
- }
250
-
251
- await actuallyLink(task);
252
- }
253
- };
254
-
255
- const injectLinksTo = async (sourcePath: string, links: string[] = [], suppressPrompts: boolean = false) => {
256
- if (links.length === 0) {
257
- return;
258
- }
259
-
260
- const linkStrings = await Promise.all(links.map((link) => getLinkDetails(link)));
261
-
262
- for (const targetDetails of linkStrings) {
263
- const task: LinkTask = {
264
- type: LinkType.LINK,
265
- sourcePath: sourcePath,
266
- link: targetDetails.link,
267
- reverseLink: targetDetails.reverseLink,
268
- targetPath: targetDetails.matches[0]
269
- };
270
-
271
- if (targetDetails.matches.length > 1) {
272
- if (suppressPrompts) {
273
- throw new Error(
274
- `Multiple files match the search pattern for "${targetDetails.original}".\n` +
275
- 'Please specify which file you want to targetDetails to more or remove the -q or --quiet options from the command line.'
276
- );
277
- }
278
- task.targetPath = await askForClarification(targetDetails.original, targetDetails.matches);
279
- }
280
-
281
- await actuallyLink(task);
282
- }
283
- };
284
- //Generate a table of contents for the adr directory
285
- export const generateToc = async (options?: { prefix?: string }) => {
286
- const adrDir = await getDir();
287
- const files = await fs.readdir(adrDir);
288
- const toc = files.filter((file) => file.match(/^\d{4,}-.*\.md$/)).sort();
289
-
290
- const titles = toc.map(async (file) => {
291
- const title = getTitleFrom(await fs.readFile(path.join(adrDir, file), 'utf8'));
292
- return `- [${title}](${options?.prefix || ''}${file})`;
293
- });
294
-
295
- const resolvedTitles = await Promise.all(titles);
296
-
297
- const tocFile = path.resolve(path.join(adrDir, 'decisions.md'));
298
- await fs.writeFile(tocFile, `# Table of Contents\n\n${resolvedTitles.join('\n')}`);
299
- };
300
-
301
- export const newAdr = async (title: string, config?: NewOptions) => {
302
- const openPlan = resolveOpenPlan(config);
303
- const openPromise = shouldLoadOpenLibrary(openPlan) ? import('open') : undefined;
304
-
305
- const newNum = await newNumber();
306
- const formattedDate = config?.date || new Date().toISOString().split('T')[0] || 'ERROR';
307
- const tpl = await template(config?.template);
308
- const adr = tpl
309
- .replace('DATE', formattedDate)
310
- .replace('TITLE', title)
311
- .replace('NUMBER', newNum.toString())
312
- .replace('STATUS', 'Accepted');
313
- const paddedNumber = newNum.toString().padStart(4, '0');
314
- const cleansedTitle = title.toLowerCase().replace(/\W+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
315
- const fileName = `${paddedNumber}-${cleansedTitle}.md`;
316
- const adrDirectory = await getDir();
317
- const adrPath = path.resolve(path.join(adrDirectory, fileName));
318
- await fs.writeFile(adrPath, adr);
319
- await fs.writeFile(path.resolve(adrDirectory, '.adr-sequence.lock'), newNum.toString());
320
-
321
- await processSupersedes(adrPath, config?.supersedes, config?.suppressPrompts);
322
- await injectLinksTo(adrPath, config?.links, config?.suppressPrompts);
323
- await generateToc();
324
-
325
- await openNewAdr(openPlan, adrPath, openPromise);
326
- };
327
-
328
- export const init = async (directory?: string) => {
329
- const dir = directory || (await getDir());
330
- await fs.mkdir(dir, { recursive: true });
331
- await fs.writeFile(path.join(workingDir(), '.adr-dir'), path.relative(workingDir(), dir));
332
- await newAdr('Record Architecture Decisions', {
333
- date: process.env.ADR_DATE,
334
- template: path.resolve(path.dirname(__filename), '../templates/init.md')
335
- });
336
- };
337
-
338
- export const link = async (
339
- source: string,
340
- link: string,
341
- target: string,
342
- reverseLink: string,
343
- options?: { quiet?: boolean }
344
- ) => {
345
- const getFor = async (pattern: string) => {
346
- const found = await findMatchingFilesFor(pattern);
347
- if (found.length === 1) {
348
- return found[0];
349
- }
350
-
351
- if (options?.quiet) {
352
- throw new Error(
353
- `Multiple files match the search pattern for "${pattern}".\n` +
354
- 'Please specify which file you want to targetDetails to more or remove the -q or --quiet options from the command line.'
355
- );
356
- }
357
-
358
- return askForClarification(pattern, found);
359
- };
360
-
361
- const sourceFile = await getFor(source);
362
-
363
- await injectLinksTo(path.resolve(await getDir(), sourceFile), [`${target}:${link}:${reverseLink}`]);
364
- };
365
-
366
- export const listAdrs = async () => {
367
- const dir = await getDir();
368
- const files = await fs.readdir(dir);
369
- const toc = files
370
- .map((f) => {
371
- const adrFile = f.match(/^0*(\d+)-.*$/);
372
- if (!adrFile) {
373
- return null;
374
- }
375
- return {
376
- number: Number.parseInt(adrFile[1], 10),
377
- path: path.resolve(dir, adrFile[0])
378
- };
379
- })
380
- .filter((entry): entry is { number: number; path: string } => Boolean(entry))
381
- .sort((a, b) => a.number - b.number)
382
- .map((entry) => entry.path);
383
- return toc;
384
- };
@@ -1,69 +0,0 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
3
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
-
5
- vi.mock('fs/promises', () => ({
6
- default: {
7
- access: vi.fn(),
8
- readFile: vi.fn(),
9
- mkdir: vi.fn()
10
- }
11
- }));
12
-
13
- const setupModule = async (cwd: string) => {
14
- vi.resetModules();
15
- vi.spyOn(process, 'cwd').mockReturnValue(cwd);
16
- const module = await import('./config.js');
17
- return module;
18
- };
19
-
20
- describe('config', () => {
21
- beforeEach(() => {
22
- vi.mocked(fs.access).mockReset();
23
- vi.mocked(fs.readFile).mockReset();
24
- vi.mocked(fs.mkdir).mockReset();
25
- });
26
-
27
- afterEach(() => {
28
- vi.restoreAllMocks();
29
- });
30
-
31
- it('uses configured adr directory when .adr-dir exists', async () => {
32
- const cwd = path.join(path.parse(process.cwd()).root, 'repo');
33
- const { getDir, workingDir } = await setupModule(cwd);
34
- vi.mocked(fs.access).mockResolvedValueOnce(undefined);
35
- vi.mocked(fs.readFile).mockResolvedValueOnce('doc/adr');
36
-
37
- const dir = await getDir();
38
- expect(workingDir()).toEqual(cwd);
39
- expect(dir).toEqual(path.join('doc', 'adr'));
40
- expect(fs.mkdir).toHaveBeenCalledWith(path.join('doc', 'adr'), { recursive: true });
41
- });
42
-
43
- it('falls back to doc/adr when no config exists', async () => {
44
- const cwd = path.join(path.parse(process.cwd()).root, 'repo');
45
- const { getDir } = await setupModule(cwd);
46
- vi.mocked(fs.access).mockRejectedValue(new Error('missing'));
47
-
48
- const dir = await getDir();
49
- expect(dir).toEqual(path.resolve(cwd, 'doc/adr'));
50
- expect(fs.mkdir).toHaveBeenCalledWith(path.resolve(cwd, 'doc/adr'), { recursive: true });
51
- });
52
-
53
- it('resolves .adr-dir from a parent directory', async () => {
54
- const repoRoot = path.join(path.parse(process.cwd()).root, 'repo');
55
- const cwd = path.join(repoRoot, 'subdir');
56
- const { getDir } = await setupModule(cwd);
57
- vi.mocked(fs.access).mockImplementation(async (target) => {
58
- const targetPath = typeof target === 'string' ? target : target.toString();
59
- if (targetPath === path.join(repoRoot, '.adr-dir')) {
60
- return;
61
- }
62
- throw new Error('missing');
63
- });
64
- vi.mocked(fs.readFile).mockResolvedValueOnce('doc/adr');
65
-
66
- const dir = await getDir();
67
- expect(dir).toEqual(path.join('..', 'doc/adr'));
68
- });
69
- });
package/src/lib/config.ts DELETED
@@ -1,36 +0,0 @@
1
- import { constants } from 'fs';
2
- import fs from 'fs/promises';
3
- import path from 'path';
4
-
5
- export const workingDir = () => process.cwd();
6
- const rootDir = path.parse(process.cwd()).root;
7
-
8
- const findTopLevelDir = async (dir: string): Promise<string> => {
9
- try {
10
- // CLI paths may point anywhere the invoking user can access; OS permissions are the security boundary.
11
- await fs.access(path.join(dir, '.adr-dir'), constants.F_OK); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
12
- return dir;
13
- } catch (_e) {
14
- if (dir === rootDir) {
15
- throw new Error('No ADR directory config found');
16
- }
17
- const newDir = path.join(dir, '..'); // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
18
- return findTopLevelDir(newDir);
19
- }
20
- };
21
-
22
- const getDirPath = async (): Promise<string> => {
23
- try {
24
- const configDir = await findTopLevelDir(workingDir());
25
- const configFile = await fs.readFile(path.join(configDir, '.adr-dir'), 'utf8');
26
- return path.relative(workingDir(), path.join(configDir, configFile.trim()));
27
- } catch (_e) {
28
- return path.resolve(path.join(workingDir(), 'doc/adr'));
29
- }
30
- };
31
-
32
- export const getDir = async (): Promise<string> => {
33
- const dir = await getDirPath();
34
- await fs.mkdir(dir, { recursive: true });
35
- return dir;
36
- };
@@ -1,74 +0,0 @@
1
- import fs from 'fs/promises';
2
- import { afterEach, describe, expect, it, vi } from 'vitest';
3
- import { getDir } from './config.js';
4
- import { getLinkDetails } from './links.js';
5
-
6
- vi.mock('./config.js');
7
- vi.mock('fs/promises');
8
-
9
- describe('The link lib', () => {
10
- const mockReaddir = vi.mocked(fs.readdir) as unknown as {
11
- mockResolvedValueOnce: (value: string[]) => void;
12
- };
13
-
14
- afterEach(() => {
15
- vi.resetAllMocks();
16
- });
17
-
18
- it('does not care if there are no matches', async () => {
19
- vi.mocked(getDir).mockResolvedValueOnce('/');
20
- mockReaddir.mockResolvedValueOnce([]);
21
- const linkString = '1:overrides:overriden by';
22
- const response = await getLinkDetails(linkString);
23
- expect(response).toEqual({
24
- pattern: '1',
25
- original: '1:overrides:overriden by',
26
- link: 'overrides',
27
- reverseLink: 'overriden by',
28
- matches: []
29
- });
30
- });
31
-
32
- it('does handles multiple matches', async () => {
33
- vi.mocked(getDir).mockResolvedValueOnce('/');
34
- mockReaddir.mockResolvedValueOnce(['1-one', '1-two', '1-three']);
35
- const linkString = '1:overrides:overriden by';
36
- const response = await getLinkDetails(linkString);
37
- expect(response).toEqual({
38
- pattern: '1',
39
- original: '1:overrides:overriden by',
40
- link: 'overrides',
41
- reverseLink: 'overriden by',
42
- matches: ['1-one', '1-two', '1-three']
43
- });
44
- });
45
-
46
- it('returns only files that match the pattern', async () => {
47
- vi.mocked(getDir).mockResolvedValueOnce('/');
48
- mockReaddir.mockResolvedValueOnce(['on1e', '1-two', 'three']);
49
- const linkString = '1:overrides:overriden by';
50
- const response = await getLinkDetails(linkString);
51
- expect(response).toEqual({
52
- pattern: '1',
53
- original: '1:overrides:overriden by',
54
- link: 'overrides',
55
- reverseLink: 'overriden by',
56
- matches: ['on1e', '1-two']
57
- });
58
- });
59
-
60
- it('handles superseding', async () => {
61
- vi.mocked(getDir).mockResolvedValueOnce('/');
62
- mockReaddir.mockResolvedValueOnce([]);
63
- const linkString = '1';
64
- const supersede = true;
65
- const response = await getLinkDetails(linkString, supersede);
66
- expect(response).toEqual({
67
- pattern: '1',
68
- original: '1',
69
- link: 'Supersedes',
70
- reverseLink: 'Superseded by',
71
- matches: []
72
- });
73
- });
74
- });
package/src/lib/links.ts DELETED
@@ -1,34 +0,0 @@
1
- import fs from 'fs/promises';
2
- import { getDir } from './config.js';
3
-
4
- export interface LinkDetails {
5
- pattern: string;
6
- original: string;
7
- link: string;
8
- reverseLink: string;
9
- matches: string[];
10
- }
11
-
12
- export const findMatchingFilesFor = async (pattern: string) => {
13
- const files = await fs.readdir(await getDir());
14
- return files.filter((file) => file.includes(pattern));
15
- };
16
-
17
- export const getLinkDetails = async (linkString: string, isSupersede: boolean = false): Promise<LinkDetails> => {
18
- const parts = linkString.split(':');
19
- const pattern = parts[0];
20
- let link = 'Supersedes';
21
- let reverseLink = 'Superseded by';
22
- if (!isSupersede) {
23
- link = parts[1];
24
- reverseLink = parts[2];
25
- }
26
- const files = await findMatchingFilesFor(pattern);
27
- return {
28
- pattern: pattern,
29
- original: linkString,
30
- link: link,
31
- reverseLink: reverseLink,
32
- matches: files
33
- };
34
- };
@@ -1,22 +0,0 @@
1
- import { describe, expect, it, vi } from 'vitest';
2
-
3
- vi.mock('marked', () => ({
4
- marked: {
5
- lexer: () => [
6
- {
7
- type: 'paragraph',
8
- text: {
9
- toString: () => 'Superseded by [1. title](0001-title.md)',
10
- match: () => null
11
- }
12
- }
13
- ]
14
- }
15
- }));
16
-
17
- describe('The ADR manipulator error cases', () => {
18
- it('throws when a link cannot be parsed', async () => {
19
- const { getLinksFrom } = await import('./manipulator.js');
20
- expect(() => getLinksFrom('ignored')).toThrowError('Could not parse link from');
21
- });
22
- });