@meza/adr-tools 2.0.2 → 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 (124) hide show
  1. package/dist/index.js +8 -1
  2. package/dist/index.js.map +1 -1
  3. package/dist/types/index.d.ts.map +1 -1
  4. package/dist/types/version.d.ts +1 -1
  5. package/dist/version.js +1 -1
  6. package/package.json +21 -9
  7. package/.adr-dir +0 -1
  8. package/.editorconfig +0 -13
  9. package/.gitattributes +0 -40
  10. package/.github/renovate.json +0 -4
  11. package/.github/stale.yml +0 -18
  12. package/.github/workflows/ci-pr.yml +0 -48
  13. package/.github/workflows/ci.yml +0 -67
  14. package/.github/workflows/security.yml +0 -49
  15. package/.releaserc.json +0 -49
  16. package/.yarnrc.yml +0 -8
  17. package/AGENTS.engineer.md +0 -236
  18. package/AGENTS.md +0 -11
  19. package/AGENTS.reviewer.md +0 -115
  20. package/CONTRIBUTING.md +0 -102
  21. package/biome.json +0 -28
  22. package/dist/index.test.js +0 -197
  23. package/dist/index.test.js.map +0 -1
  24. package/dist/inject-version.test.js +0 -27
  25. package/dist/inject-version.test.js.map +0 -1
  26. package/dist/lib/adr.test.js +0 -319
  27. package/dist/lib/adr.test.js.map +0 -1
  28. package/dist/lib/config.test.js +0 -60
  29. package/dist/lib/config.test.js.map +0 -1
  30. package/dist/lib/links.test.js +0 -66
  31. package/dist/lib/links.test.js.map +0 -1
  32. package/dist/lib/manipulator-errors.test.js +0 -21
  33. package/dist/lib/manipulator-errors.test.js.map +0 -1
  34. package/dist/lib/manipulator.test.js +0 -96
  35. package/dist/lib/manipulator.test.js.map +0 -1
  36. package/dist/lib/numbering.test.js +0 -39
  37. package/dist/lib/numbering.test.js.map +0 -1
  38. package/dist/lib/opening.test.js +0 -81
  39. package/dist/lib/opening.test.js.map +0 -1
  40. package/dist/lib/prompt.test.js +0 -33
  41. package/dist/lib/prompt.test.js.map +0 -1
  42. package/dist/lib/template.test.js +0 -62
  43. package/dist/lib/template.test.js.map +0 -1
  44. package/dist/types/index.test.d.ts +0 -2
  45. package/dist/types/index.test.d.ts.map +0 -1
  46. package/dist/types/inject-version.test.d.ts +0 -2
  47. package/dist/types/inject-version.test.d.ts.map +0 -1
  48. package/dist/types/lib/adr.test.d.ts +0 -2
  49. package/dist/types/lib/adr.test.d.ts.map +0 -1
  50. package/dist/types/lib/config.test.d.ts +0 -2
  51. package/dist/types/lib/config.test.d.ts.map +0 -1
  52. package/dist/types/lib/links.test.d.ts +0 -2
  53. package/dist/types/lib/links.test.d.ts.map +0 -1
  54. package/dist/types/lib/manipulator-errors.test.d.ts +0 -2
  55. package/dist/types/lib/manipulator-errors.test.d.ts.map +0 -1
  56. package/dist/types/lib/manipulator.test.d.ts +0 -2
  57. package/dist/types/lib/manipulator.test.d.ts.map +0 -1
  58. package/dist/types/lib/numbering.test.d.ts +0 -2
  59. package/dist/types/lib/numbering.test.d.ts.map +0 -1
  60. package/dist/types/lib/opening.test.d.ts +0 -2
  61. package/dist/types/lib/opening.test.d.ts.map +0 -1
  62. package/dist/types/lib/prompt.test.d.ts +0 -2
  63. package/dist/types/lib/prompt.test.d.ts.map +0 -1
  64. package/dist/types/lib/template.test.d.ts +0 -2
  65. package/dist/types/lib/template.test.d.ts.map +0 -1
  66. package/doc/adr/.adr-sequence.lock +0 -1
  67. package/doc/adr/0001-record-architecture-decisions.md +0 -21
  68. package/doc/adr/0002-using-heavy-e2e-tests.md +0 -20
  69. package/doc/adr/0003-esm.md +0 -34
  70. package/doc/adr/0004-gate-editor-opening-behind---open-and---open-with.md +0 -55
  71. package/doc/adr/decisions.md +0 -6
  72. package/lefthook.yml +0 -14
  73. package/scripts/inject-version.mjs +0 -34
  74. package/src/index.test.ts +0 -222
  75. package/src/index.ts +0 -261
  76. package/src/inject-version.test.ts +0 -31
  77. package/src/lib/adr.test.ts +0 -389
  78. package/src/lib/adr.ts +0 -384
  79. package/src/lib/config.test.ts +0 -69
  80. package/src/lib/config.ts +0 -36
  81. package/src/lib/links.test.ts +0 -74
  82. package/src/lib/links.ts +0 -34
  83. package/src/lib/manipulator-errors.test.ts +0 -22
  84. package/src/lib/manipulator.test.ts +0 -109
  85. package/src/lib/manipulator.ts +0 -98
  86. package/src/lib/numbering.test.ts +0 -45
  87. package/src/lib/numbering.ts +0 -25
  88. package/src/lib/opening.test.ts +0 -96
  89. package/src/lib/prompt.test.ts +0 -42
  90. package/src/lib/prompt.ts +0 -14
  91. package/src/lib/template.test.ts +0 -74
  92. package/src/lib/template.ts +0 -22
  93. package/src/templates/init.md +0 -21
  94. package/src/templates/template.md +0 -19
  95. package/src/types/environment.d.ts +0 -14
  96. package/src/version.ts +0 -1
  97. package/tests/.adr-dir +0 -1
  98. package/tests/__snapshots__/generate-graph.e2e.test.ts.snap +0 -39
  99. package/tests/__snapshots__/init-adr-repository.e2e.test.ts.snap +0 -51
  100. package/tests/__snapshots__/linking-records.e2e.test.ts.snap +0 -155
  101. package/tests/__snapshots__/new-adr.e2e.test.ts.snap +0 -54
  102. package/tests/__snapshots__/superseding-records.e2e.test.ts.snap +0 -122
  103. package/tests/__snapshots__/toc-prefixing.e2e.test.ts.snap +0 -9
  104. package/tests/__snapshots__/use-template-override.e2e.test.ts.snap +0 -17
  105. package/tests/edit-on-create.e2e.test.ts +0 -90
  106. package/tests/fake-editor +0 -5
  107. package/tests/fake-editor.cmd +0 -4
  108. package/tests/fake-visual +0 -5
  109. package/tests/fake-visual.cmd +0 -4
  110. package/tests/funny-characters.e2e.test.ts +0 -51
  111. package/tests/generate-graph.e2e.test.ts +0 -49
  112. package/tests/helpers/adr-cli.ts +0 -24
  113. package/tests/init-adr-repository.e2e.test.ts +0 -58
  114. package/tests/linking-records.e2e.test.ts +0 -72
  115. package/tests/list-adrs.e2e.test.ts +0 -60
  116. package/tests/new-adr.e2e.test.ts +0 -62
  117. package/tests/open-with.e2e.test.ts +0 -53
  118. package/tests/superseding-records.e2e.test.ts +0 -64
  119. package/tests/toc-prefixing.e2e.test.ts +0 -43
  120. package/tests/use-template-override.e2e.test.ts +0 -52
  121. package/tests/work-form-other-directories.e2e.test.ts +0 -48
  122. package/tsconfig.json +0 -55
  123. package/vitest.config.e2e.ts +0 -18
  124. package/vitest.config.ts +0 -30
@@ -1,21 +0,0 @@
1
- # 1. Record architecture decisions
2
-
3
- Date: 2022-06-22
4
-
5
- ## Status
6
-
7
- Accepted
8
-
9
- ## Context
10
-
11
- We need to record the architectural decisions made on this project.
12
-
13
- ## Decision
14
-
15
- We will use Architecture Decision Records, as [described by Michael Nygard](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions).
16
-
17
- ## Consequences
18
-
19
- See Michael Nygard's article, linked above.
20
- For a lightweight ADR toolset, see Nat Pryce's [adr-tools](https://github.com/npryce/adr-tools).
21
- > For a node version of the same tooling, see Meza's [adr-tools](https://github.com/meza/adr-tools).
@@ -1,20 +0,0 @@
1
- # 2. Using Heavy E2E Tests
2
-
3
- Date: 2022-06-22
4
-
5
- ## Status
6
-
7
- Accepted
8
-
9
- ## Context
10
-
11
- The original tool has an [exhaustive test suite](https://github.com/npryce/adr-tools/tree/master/tests) that allows us to make sure that we're backwards compatible.
12
-
13
- ## Decision
14
-
15
- We'll be re-implementing those tests for ourselves too. This means that we will be using the original examples,
16
- expectations and the ethos of invoking the tool with the given examples.
17
-
18
- ## Consequences
19
-
20
- The E2E test suite will be very slow to run and it can only be executed sequentially.
@@ -1,34 +0,0 @@
1
- # 3. ESM
2
-
3
- Date: 2022-06-29
4
-
5
- ## Status
6
-
7
- Accepted
8
-
9
- ## Context
10
-
11
- With typescript 4.7 come proper ESM modules.
12
-
13
- ECMAScript modules are the official standard format to package JavaScript code for reuse. Modules are defined using a variety of import and export statements.
14
-
15
- ## Decision
16
-
17
- Since this is a new project, there's no reason to _not_ go with the flow.
18
-
19
- ## Consequences
20
-
21
- There's some changes to the way the project is set up.
22
- Everything is described here, in [Typescript's official documentation](https://devblogs.microsoft.com/typescript/announcing-typescript-4-7-beta/#ecmascript-module-support-in-node-js)
23
-
24
- However, some highlights:
25
-
26
- We **must** import code with extensions and `.js` at that (even though the file's actual type is .ts)
27
-
28
- `__dirname` and `__filename` are no longer available. Use this instead:
29
- ```typescript
30
- import path from 'node:path';
31
- import { fileURLToPath } from 'node:url';
32
-
33
- const __filename = fileURLToPath(import.meta.url);
34
- const __dirname = path.dirname(__filename);```
@@ -1,55 +0,0 @@
1
- # 4. Gate editor opening behind --open and --open-with
2
-
3
- Date: 2026-01-04
4
-
5
- ## Status
6
-
7
- Accepted
8
-
9
- ## Context
10
-
11
- This tool historically tries to open a newly created ADR in the user's editor by reading the `VISUAL` and `EDITOR`
12
- environment variables.
13
-
14
- In 2025, a common way to run Node CLIs is via `npx`/`npm exec`. npm injects an `EDITOR` value into child processes
15
- based on npm config (`npm_config_editor`), even when the user did not intend to configure an editor for this tool.
16
-
17
- This creates two classes of problems:
18
-
19
- 1. Unwanted side effects. Creating an ADR can unexpectedly launch an editor or attempt to launch a placeholder value.
20
- 2. Cross-platform friction. Windows shells, quoting rules, and editor invocation are inconsistent, making the default
21
- "auto open" behavior more fragile than the rest of the CLI.
22
-
23
- We still want a good "create and open" workflow, but it must be explicit and predictable, and it must work well on
24
- Windows, macOS, and Linux.
25
-
26
- ## Decision
27
-
28
- We will stop automatically opening an editor after `adr new` by default.
29
-
30
- We will add two flags to the CLI:
31
-
32
- - `--open`: Open the newly created ADR file after creation.
33
- - `--open-with <command>`: Open the newly created ADR file using the provided command (optionally including args).
34
-
35
- When `--open` is supplied, the tool will select an opener in this order:
36
-
37
- 1. `--open-with <command>` if provided.
38
- 2. `VISUAL` if it is set by the user and not empty.
39
- 3. `EDITOR` if it is set by the user and not empty.
40
- 4. OS default application for the file type.
41
-
42
- To handle cross-platform differences, we will use a dedicated dependency (`open`) to perform the actual launching
43
- when possible, rather than building platform-specific process spawning logic in this project.
44
-
45
- ## Consequences
46
-
47
- This makes ADR creation deterministic and side-effect free by default, including when invoked via `npx` or within CI.
48
-
49
- Users who want the "open after create" workflow can still have it, but must opt in via `--open` or explicitly specify
50
- an opener via `--open-with`.
51
-
52
- This reduces platform-specific bugs and maintenance burden by relying on a well-maintained cross-platform opener.
53
-
54
- This is a breaking behavior change for users who relied on implicit editor opening when `VISUAL`/`EDITOR` were set,
55
- but the new behavior is more predictable and better aligned with modern Node execution environments.
@@ -1,6 +0,0 @@
1
- # Table of Contents
2
-
3
- - [1. Record architecture decisions](0001-record-architecture-decisions.md)
4
- - [2. Using Heavy E2E Tests](0002-using-heavy-e2e-tests.md)
5
- - [3. ESM](0003-esm.md)
6
- - [4. Gate editor opening behind --open and --open-with](0004-gate-editor-opening-behind---open-and---open-with.md)
package/lefthook.yml DELETED
@@ -1,14 +0,0 @@
1
- pre-commit:
2
- jobs:
3
- - name: lint
4
- run: yarn lint
5
-
6
- pre-push:
7
- jobs:
8
- - name: ci
9
- run: yarn ci
10
-
11
- commit-msg:
12
- jobs:
13
- - name: commitlint
14
- run: yarn -s commitlint --edit {1}
@@ -1,34 +0,0 @@
1
- import { readFileSync, writeFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
3
- import { pathToFileURL } from 'node:url';
4
-
5
- export const buildVersionSource = (version) => `export const LIB_VERSION = '${version}';\n`;
6
-
7
- export const resolveTargetPath = (argv) => {
8
- const target = argv[2];
9
- if (!target) {
10
- throw new Error('Usage: node scripts/inject-version.mjs <output-file>');
11
- }
12
- return target;
13
- };
14
-
15
- export const writeVersionFile = (target, version) => {
16
- writeFileSync(resolve(process.cwd(), target), buildVersionSource(version), 'utf8');
17
- };
18
-
19
- const readPackageVersion = () =>
20
- JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
21
-
22
- const run = () => {
23
- try {
24
- const target = resolveTargetPath(process.argv);
25
- writeVersionFile(target, readPackageVersion());
26
- } catch (error) {
27
- console.error(error.message);
28
- process.exit(1);
29
- }
30
- };
31
-
32
- if (pathToFileURL(process.argv[1]).href === import.meta.url) {
33
- run();
34
- }
package/src/index.test.ts DELETED
@@ -1,222 +0,0 @@
1
- import path from 'node:path';
2
- import { pathToFileURL } from 'node:url';
3
- import chalk from 'chalk';
4
- import type { Command } from 'commander';
5
- import { describe, expect, it, vi } from 'vitest';
6
- import { buildProgram, defaultOnError, isDirectRun, maybeRun, run } from './index.js';
7
-
8
- const createDeps = () => {
9
- const deps = {
10
- generateToc: vi.fn(),
11
- init: vi.fn(),
12
- link: vi.fn(),
13
- listAdrs: vi.fn(),
14
- newAdr: vi.fn(),
15
- workingDir: vi.fn(),
16
- readFile: vi.fn(),
17
- getLinksFrom: vi.fn(),
18
- getTitleFrom: vi.fn(),
19
- version: '1.2.3',
20
- onError: vi.fn(),
21
- log: vi.fn(),
22
- warn: vi.fn()
23
- };
24
-
25
- return deps;
26
- };
27
-
28
- describe('adr CLI', () => {
29
- it('wires program metadata', () => {
30
- const deps = createDeps();
31
- const program = buildProgram(deps);
32
- expect(program.name()).toEqual('adr');
33
- expect(program.version()).toEqual('1.2.3');
34
- });
35
-
36
- it('runs the new command with options', async () => {
37
- const deps = createDeps();
38
- await run(['node', 'adr', 'new', 'My', 'ADR', '--open'], deps);
39
- expect(deps.newAdr).toHaveBeenCalledWith('My ADR', {
40
- supersedes: [],
41
- date: process.env.ADR_DATE,
42
- suppressPrompts: false,
43
- links: [],
44
- open: true,
45
- openWith: undefined
46
- });
47
- });
48
-
49
- it('invokes error handler when newAdr fails', async () => {
50
- const deps = createDeps();
51
- deps.newAdr.mockRejectedValueOnce(new Error('boom'));
52
- await run(['node', 'adr', 'new', 'My', 'ADR'], deps);
53
- expect(deps.onError).toHaveBeenCalled();
54
- });
55
-
56
- it('routes generate toc', async () => {
57
- const deps = createDeps();
58
- await run(['node', 'adr', 'generate', 'toc', '-p', '/prefix/'], deps);
59
- expect(deps.generateToc).toHaveBeenCalledWith({ prefix: '/prefix/' });
60
- });
61
-
62
- it('generates graph output', async () => {
63
- const deps = createDeps();
64
- deps.listAdrs.mockResolvedValue(['/repo/doc/adr/0001-one.md', '/repo/doc/adr/0002-two.md']);
65
- deps.readFile.mockResolvedValue('# Title');
66
- deps.getTitleFrom.mockReturnValue('ADR Title');
67
- deps.getLinksFrom.mockReturnValue([
68
- { label: 'Relates to', targetNumber: '0002' },
69
- { label: 'Superseded by', targetNumber: '0003' }
70
- ]);
71
- await run(['node', 'adr', 'generate', 'graph', '-e', '.md', '-p', '/p/'], deps);
72
- const output = deps.log.mock.calls[0][0] as string;
73
- expect(output).toContain('digraph');
74
- expect(output).toContain('_1 -> _2 [style="dotted"');
75
- expect(output).toContain('label="Relates to"');
76
- expect(output).not.toContain('Superseded by');
77
- });
78
-
79
- it('generates graph output with default prefix', async () => {
80
- const deps = createDeps();
81
- deps.listAdrs.mockResolvedValue(['/repo/doc/adr/0001-one.md']);
82
- deps.readFile.mockResolvedValue('# Title');
83
- deps.getTitleFrom.mockReturnValue('ADR Title');
84
- deps.getLinksFrom.mockReturnValue([]);
85
- await run(['node', 'adr', 'generate', 'graph'], deps);
86
- const output = deps.log.mock.calls[0][0] as string;
87
- expect(output).toContain('0001-one.html');
88
- });
89
-
90
- it('links graph nodes by adr number', async () => {
91
- const deps = createDeps();
92
- deps.listAdrs.mockResolvedValue(['/repo/doc/adr/0001-one.md', '/repo/doc/adr/0003-three.md']);
93
- deps.readFile.mockResolvedValue('# Title');
94
- deps.getTitleFrom.mockReturnValue('ADR Title');
95
- deps.getLinksFrom.mockReturnValue([{ label: 'Relates to', targetNumber: '3' }]);
96
- await run(['node', 'adr', 'generate', 'graph'], deps);
97
- const output = deps.log.mock.calls[0][0] as string;
98
- expect(output).toContain('_1 -> _3 [style="dotted"');
99
- expect(output).toContain('_1 -> _3 [label="Relates to"');
100
- });
101
-
102
- it('skips graph links that target missing nodes', async () => {
103
- const deps = createDeps();
104
- deps.listAdrs.mockResolvedValue(['/repo/doc/adr/0001-one.md']);
105
- deps.readFile.mockResolvedValue('# Title');
106
- deps.getTitleFrom.mockReturnValue('ADR Title');
107
- deps.getLinksFrom.mockReturnValue([{ label: 'Relates to', targetNumber: '99' }]);
108
- await run(['node', 'adr', 'generate', 'graph'], deps);
109
- const output = deps.log.mock.calls[0][0] as string;
110
- expect(output).not.toContain('_1 -> _99');
111
- expect(deps.warn).toHaveBeenCalledWith('Skipping graph link from ADR 1 to missing ADR 99: Relates to');
112
- });
113
-
114
- it('skips files without numeric prefixes', async () => {
115
- const deps = createDeps();
116
- deps.listAdrs.mockResolvedValue(['/repo/doc/adr/notes.md', '/repo/doc/adr/0001-one.md']);
117
- deps.readFile.mockResolvedValue('# Title');
118
- deps.getTitleFrom.mockReturnValue('ADR Title');
119
- deps.getLinksFrom.mockReturnValue([]);
120
- await run(['node', 'adr', 'generate', 'graph'], deps);
121
- expect(deps.readFile).toHaveBeenCalledTimes(2);
122
- expect(deps.readFile).toHaveBeenNthCalledWith(1, '/repo/doc/adr/0001-one.md', 'utf8');
123
- expect(deps.readFile).toHaveBeenNthCalledWith(2, '/repo/doc/adr/0001-one.md', 'utf8');
124
- });
125
-
126
- it('escapes graph labels and urls', async () => {
127
- const deps = createDeps();
128
- deps.listAdrs.mockResolvedValue(['/repo/doc/adr/0001-one.md']);
129
- deps.readFile.mockResolvedValue('# Title');
130
- deps.getTitleFrom.mockReturnValue('ADR "Title" \\\\ path\nline');
131
- deps.getLinksFrom.mockReturnValue([]);
132
- await run(['node', 'adr', 'generate', 'graph', '-e', '.md', '-p', '/p/"weird"/'], deps);
133
- const output = deps.log.mock.calls[0][0] as string;
134
- const match = output.match(/label="([\s\S]*?)"; URL="([^"]+)"/);
135
- expect(match?.[1]).toContain(String.raw`ADR \"Title\" \\\\ path\\nline`);
136
- expect(match?.[1]).not.toContain('\n');
137
- expect(output).toContain(String.raw`URL="/p/\"weird\"/0001-one.md"`);
138
- });
139
-
140
- it('routes link command', async () => {
141
- const deps = createDeps();
142
- await run(['node', 'adr', 'link', '1', 'Amends', '2', 'Amended by'], deps);
143
- const call = deps.link.mock.calls[0];
144
- expect(call.slice(0, 4)).toEqual(['1', 'Amends', '2', 'Amended by']);
145
- });
146
-
147
- it('routes init command', async () => {
148
- const deps = createDeps();
149
- await run(['node', 'adr', 'init', 'doc/adr'], deps);
150
- expect(deps.init).toHaveBeenCalledWith('doc/adr');
151
- });
152
-
153
- it('lists adrs relative to working dir', async () => {
154
- const deps = createDeps();
155
- deps.listAdrs.mockResolvedValue(['/repo/doc/adr/0001-one.md']);
156
- deps.workingDir.mockReturnValue('/repo');
157
- await run(['node', 'adr', 'list'], deps);
158
- expect(deps.log).toHaveBeenCalledWith(path.join('doc', 'adr', '0001-one.md'));
159
- });
160
-
161
- it('collects repeated options for links and supersedes', async () => {
162
- const deps = createDeps();
163
- await run(
164
- [
165
- 'node',
166
- 'adr',
167
- 'new',
168
- 'My',
169
- 'ADR',
170
- '-l',
171
- '1:Links:Linked by',
172
- '-l',
173
- '2:Relates:Related by',
174
- '-s',
175
- '1',
176
- '-s',
177
- '2'
178
- ],
179
- deps
180
- );
181
- const [, options] = deps.newAdr.mock.calls[0];
182
- expect(options.links).toEqual(['1:Links:Linked by', '2:Relates:Related by']);
183
- expect(options.supersedes).toEqual(['1', '2']);
184
- });
185
-
186
- it('detects direct runs based on argv', () => {
187
- const indexPath = path.resolve(process.cwd(), 'src/index.ts');
188
- const moduleUrl = pathToFileURL(indexPath).href;
189
- expect(isDirectRun(['node', indexPath], moduleUrl)).toBe(true);
190
- expect(isDirectRun(['node'], moduleUrl)).toBe(false);
191
- });
192
-
193
- it('returns false when maybeRun is not direct', () => {
194
- const deps = createDeps();
195
- expect(maybeRun(['node', '/other/entry'], deps)).toBe(false);
196
- });
197
-
198
- it('runs when maybeRun detects direct execution', async () => {
199
- const deps = createDeps();
200
- deps.listAdrs.mockResolvedValue([]);
201
- const indexPath = path.resolve(process.cwd(), 'src/index.ts');
202
- expect(maybeRun(['node', indexPath, 'list'], deps)).toBe(true);
203
- expect(deps.listAdrs).toHaveBeenCalled();
204
- });
205
-
206
- it('handles rejected direct runs without an unhandled rejection', async () => {
207
- const deps = createDeps();
208
- deps.listAdrs.mockRejectedValueOnce(new Error('boom'));
209
- const indexPath = path.resolve(process.cwd(), 'src/index.ts');
210
-
211
- expect(maybeRun(['node', indexPath, 'list'], deps)).toBe(true);
212
- await vi.waitFor(() => expect(deps.listAdrs).toHaveBeenCalled());
213
- await new Promise<void>((resolve) => setImmediate(resolve));
214
- });
215
-
216
- it('formats errors with the default handler', () => {
217
- const errorHandler = vi.fn();
218
- const program = { error: errorHandler } as unknown as Command;
219
- defaultOnError(program, new Error('boom'));
220
- expect(errorHandler).toHaveBeenCalledWith(chalk.red('boom'), { exitCode: 1 });
221
- });
222
- });
package/src/index.ts DELETED
@@ -1,261 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from 'node:fs/promises';
4
- import * as path from 'node:path';
5
- import { fileURLToPath } from 'node:url';
6
- import chalk from 'chalk';
7
- import { Command } from 'commander';
8
- import { generateToc, init, link, listAdrs, newAdr } from './lib/adr.js';
9
- import { workingDir } from './lib/config.js';
10
- import { getLinksFrom, getTitleFrom } from './lib/manipulator.js';
11
- import { LIB_VERSION } from './version.js';
12
-
13
- const collectLinks = (val: string, memo: string[]) => {
14
- memo.push(val);
15
- return memo;
16
- };
17
-
18
- const collectSupersedes = (val: string, memo: string[]) => {
19
- memo.push(val);
20
- return memo;
21
- };
22
-
23
- type ProgramDeps = {
24
- generateToc: typeof generateToc;
25
- init: typeof init;
26
- link: typeof link;
27
- listAdrs: typeof listAdrs;
28
- newAdr: typeof newAdr;
29
- workingDir: typeof workingDir;
30
- readFile: typeof fs.readFile;
31
- getLinksFrom: typeof getLinksFrom;
32
- getTitleFrom: typeof getTitleFrom;
33
- version: string;
34
- onError: (program: Command, error: Error) => void;
35
- log: (message: string) => void;
36
- warn: (message: string) => void;
37
- };
38
-
39
- export const defaultOnError = (program: Command, error: Error) => {
40
- program.error(chalk.red(error.message), { exitCode: 1 });
41
- };
42
-
43
- const defaultDeps: ProgramDeps = {
44
- generateToc,
45
- init,
46
- link,
47
- listAdrs,
48
- newAdr,
49
- workingDir,
50
- readFile: fs.readFile,
51
- getLinksFrom,
52
- getTitleFrom,
53
- version: LIB_VERSION,
54
- onError: defaultOnError,
55
- log: console.log,
56
- warn: console.warn
57
- };
58
-
59
- const escapeDotString = (value: string) =>
60
- value
61
- .replace(/\\/g, '\\\\')
62
- .replace(/"/g, '\\"')
63
- .replace(/\r\n|\n|\r/g, '\\\\n');
64
-
65
- const getAdrNumberFromPath = (adrPath: string): number | undefined => {
66
- const baseName = path.basename(adrPath, '.md');
67
- const match = baseName.match(/^0*(\d+)-/);
68
- if (!match) {
69
- return undefined;
70
- }
71
- return Number.parseInt(match[1], 10);
72
- };
73
-
74
- type GraphNode = { adrPath: string; number: number };
75
-
76
- const resolveGraphNodes = (adrs: string[]): GraphNode[] =>
77
- adrs
78
- .map((adrPath) => ({ adrPath, number: getAdrNumberFromPath(adrPath) }))
79
- .filter((node): node is GraphNode => node.number !== undefined);
80
-
81
- const appendGraphNodes = async (
82
- deps: ProgramDeps,
83
- nodes: GraphNode[],
84
- options: { prefix?: string; extension?: string } | undefined,
85
- lines: string[]
86
- ) => {
87
- for (let i = 0; i < nodes.length; i++) {
88
- const n = nodes[i].number;
89
- const adrPath = nodes[i].adrPath;
90
- const contents = await deps.readFile(adrPath, 'utf8');
91
- const title = deps.getTitleFrom(contents);
92
- const url = `${options?.prefix || ''}${path.basename(adrPath, '.md')}${options?.extension}`;
93
- lines.push(` _${n} [label="${escapeDotString(title)}"; URL="${escapeDotString(url)}"];`);
94
- const previous = nodes[i - 1]?.number;
95
- if (previous !== undefined) {
96
- lines.push(` _${previous} -> _${n} [style="dotted", weight=1];`);
97
- }
98
- }
99
- };
100
-
101
- const resolveTargetNodeId = (nodeIds: Set<number>, targetNumber: string): number | undefined => {
102
- const number = Number.parseInt(targetNumber, 10);
103
- if (!Number.isFinite(number) || !nodeIds.has(number)) {
104
- return undefined;
105
- }
106
- return number;
107
- };
108
-
109
- const appendGraphEdges = async (deps: ProgramDeps, nodes: GraphNode[], lines: string[]) => {
110
- const nodeIds = new Set(nodes.map((node) => node.number));
111
- for (const node of nodes) {
112
- const contents = await deps.readFile(node.adrPath, 'utf8');
113
- const linksInADR = deps.getLinksFrom(contents);
114
- for (const link of linksInADR) {
115
- if (link.label.endsWith('by')) {
116
- continue;
117
- }
118
- const target = resolveTargetNodeId(nodeIds, link.targetNumber);
119
- if (target !== undefined) {
120
- lines.push(` _${node.number} -> _${target} [label="${link.label}", weight=0]`);
121
- } else {
122
- deps.warn(`Skipping graph link from ADR ${node.number} to missing ADR ${link.targetNumber}: ${link.label}`);
123
- }
124
- }
125
- }
126
- };
127
-
128
- const generateGraph = async (deps: ProgramDeps, options?: { prefix: string; extension: string }) => {
129
- const lines: string[] = [];
130
- lines.push('digraph {');
131
- lines.push(' node [shape=plaintext];');
132
- lines.push(' subgraph {');
133
-
134
- const adrs = await deps.listAdrs();
135
- const nodes = resolveGraphNodes(adrs);
136
- await appendGraphNodes(deps, nodes, options, lines);
137
-
138
- lines.push(' }');
139
- await appendGraphEdges(deps, nodes, lines);
140
- lines.push('}');
141
-
142
- deps.log(`${lines.join('\n')}\n`);
143
- };
144
-
145
- export const buildProgram = (deps: ProgramDeps = defaultDeps) => {
146
- const program = new Command();
147
- program.name('adr').version(deps.version).description('Manage Architecture Decision Logs');
148
-
149
- program
150
- .command('new')
151
- .argument('<title...>', 'The title of the decision')
152
- .option(
153
- '-q, --quiet',
154
- 'Do not ask for clarification. If multiple files match the search pattern, an error will be thrown.'
155
- )
156
- .option('--open', 'Open the created ADR after writing it (use OS default or `--open-with`).')
157
- .option(
158
- '--open-with <COMMAND>',
159
- 'Open the created ADR with a specific command (optionally with args); implies `--open`.'
160
- )
161
- .option(
162
- '-s, --supersede <SUPERSEDE>',
163
- 'A reference (number or partial filename) of a previous decision that the new decision supercedes.\n' +
164
- 'A Markdown link to the superceded ADR is inserted into the Status section.\n' +
165
- 'The status of the superceded ADR is changed to record that it has been superceded by the new ADR.',
166
- collectSupersedes,
167
- []
168
- )
169
- .option(
170
- '-l, --link "<TARGET:LINK:REVERSE-LINK>"',
171
- 'Links the new ADR to a previous ADR.\n' +
172
- `${chalk.bold('TARGET')} is a reference (number or partial filename) of a previous decision.\n` +
173
- `${chalk.bold('LINK')} is the description of the link created in the new ADR.\n` +
174
- `${chalk.bold('REVERSE-LINK')} is the description of the link created in the existing ADR that will refer to the new ADR`,
175
- collectLinks,
176
- []
177
- )
178
- .action(async (title: string[], options) => {
179
- try {
180
- await deps.newAdr(title.join(' '), {
181
- supersedes: options.supersede,
182
- date: process.env.ADR_DATE,
183
- suppressPrompts: options.quiet || false,
184
- links: options.link,
185
- open: options.open || Boolean(options.openWith),
186
- openWith: options.openWith
187
- });
188
- } catch (e) {
189
- deps.onError(program, e as Error);
190
- }
191
- });
192
-
193
- const generate = program.command('generate');
194
-
195
- generate
196
- .command('toc')
197
- .option('-p, --prefix <PREFIX>', 'The prefix to use for each file link in the generated TOC.')
198
- .action((options) => deps.generateToc(options));
199
-
200
- generate
201
- .command('graph')
202
- .option('-p, --prefix <PREFIX>', 'Prefix each decision file link with PREFIX.')
203
- .option(
204
- '-e, --extension <EXTENSION>',
205
- 'the file extension of the documents to which generated links refer. Defaults to .html',
206
- '.html'
207
- )
208
- .action(async (options) => {
209
- await generateGraph(deps, options);
210
- });
211
-
212
- program
213
- .command('link')
214
- .argument('<SOURCE>', 'Full or Partial reference number to an ADR')
215
- .argument('<LINK>', 'The description of the link created in the SOURCE')
216
- .argument('<TARGET>', 'Full or Partial reference number to an ADR')
217
- .argument('<REVERSE-LINK>', 'The description of the link created in the TARGET')
218
- .option(
219
- '-q, --quiet',
220
- 'Do not ask for clarification. If multiple files match the search pattern, an error will be thrown.'
221
- )
222
- .action(deps.link);
223
-
224
- program
225
- .command('init')
226
- .argument('[directory]', 'Initialize a new ADR directory')
227
- .action(async (directory?: string) => {
228
- await deps.init(directory);
229
- });
230
-
231
- program.command('list').action(async () => {
232
- const adrs = await deps.listAdrs();
233
- deps.log(adrs.map((adr) => path.relative(deps.workingDir(), adr)).join('\n'));
234
- });
235
-
236
- return program;
237
- };
238
-
239
- export const run = async (argv = process.argv, deps: ProgramDeps = defaultDeps) => {
240
- const program = buildProgram(deps);
241
- await program.parseAsync(argv);
242
- return program;
243
- };
244
-
245
- export const isDirectRun = (argv = process.argv, moduleUrl = import.meta.url) => {
246
- if (!argv[1]) {
247
- return false;
248
- }
249
- return path.resolve(argv[1]) === fileURLToPath(moduleUrl);
250
- };
251
-
252
- export const maybeRun = (argv = process.argv, deps?: ProgramDeps) => {
253
- if (!isDirectRun(argv)) {
254
- return false;
255
- }
256
- const runPromise = run(argv, deps);
257
- runPromise.catch(() => undefined);
258
- return true;
259
- };
260
-
261
- maybeRun();