@meza/adr-tools 1.0.12 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/renovate.json +2 -1
- package/.github/workflows/ci-pr.yml +2 -25
- package/.github/workflows/ci.yml +12 -46
- package/.releaserc.json +18 -8
- package/AGENTS.engineer.md +236 -0
- package/AGENTS.md +11 -0
- package/AGENTS.reviewer.md +115 -0
- package/CONTRIBUTING.md +102 -0
- package/README.md +16 -4
- package/biome.json +18 -139
- package/dist/index.js +164 -81
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +189 -0
- package/dist/index.test.js.map +1 -0
- package/dist/inject-version.test.js +27 -0
- package/dist/inject-version.test.js.map +1 -0
- package/dist/lib/adr.js +132 -27
- package/dist/lib/adr.js.map +1 -1
- package/dist/lib/adr.test.js +308 -0
- package/dist/lib/adr.test.js.map +1 -0
- package/dist/lib/config.js +3 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/config.test.js +60 -0
- package/dist/lib/config.test.js.map +1 -0
- package/dist/lib/links.test.js +5 -4
- package/dist/lib/links.test.js.map +1 -1
- package/dist/lib/manipulator-errors.test.js +21 -0
- package/dist/lib/manipulator-errors.test.js.map +1 -0
- package/dist/lib/manipulator.test.js +21 -1
- package/dist/lib/manipulator.test.js.map +1 -1
- package/dist/lib/numbering.js +1 -1
- package/dist/lib/numbering.js.map +1 -1
- package/dist/lib/numbering.test.js +7 -0
- package/dist/lib/numbering.test.js.map +1 -1
- package/dist/lib/opening.test.js +81 -0
- package/dist/lib/opening.test.js.map +1 -0
- package/dist/lib/prompt.js +1 -1
- package/dist/lib/prompt.js.map +1 -1
- package/dist/lib/template.test.js +62 -0
- package/dist/lib/template.test.js.map +1 -0
- package/dist/types/index.d.ts +25 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.test.d.ts +2 -0
- package/dist/types/index.test.d.ts.map +1 -0
- package/dist/types/inject-version.test.d.ts +2 -0
- package/dist/types/inject-version.test.d.ts.map +1 -0
- package/dist/types/lib/adr.d.ts +15 -0
- package/dist/types/lib/adr.d.ts.map +1 -1
- package/dist/types/lib/adr.test.d.ts +2 -0
- package/dist/types/lib/adr.test.d.ts.map +1 -0
- package/dist/types/lib/config.d.ts.map +1 -1
- package/dist/types/lib/config.test.d.ts +2 -0
- package/dist/types/lib/config.test.d.ts.map +1 -0
- package/dist/types/lib/manipulator-errors.test.d.ts +2 -0
- package/dist/types/lib/manipulator-errors.test.d.ts.map +1 -0
- package/dist/types/lib/opening.test.d.ts +2 -0
- package/dist/types/lib/opening.test.d.ts.map +1 -0
- package/dist/types/lib/prompt.d.ts.map +1 -1
- package/dist/types/lib/template.test.d.ts +2 -0
- package/dist/types/lib/template.test.d.ts.map +1 -0
- package/dist/types/version.d.ts +1 -1
- package/dist/types/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/doc/adr/.adr-sequence.lock +1 -1
- package/doc/adr/0001-record-architecture-decisions.md +21 -0
- package/doc/adr/0002-using-heavy-e2e-tests.md +20 -0
- package/doc/adr/0003-esm.md +34 -0
- package/doc/adr/0004-gate-editor-opening-behind---open-and---open-with.md +55 -0
- package/doc/adr/decisions.md +4 -1
- package/lefthook.yml +14 -0
- package/package.json +24 -26
- package/scripts/inject-version.mjs +34 -0
- package/src/index.test.ts +212 -0
- package/src/index.ts +229 -108
- package/src/inject-version.test.ts +31 -0
- package/src/lib/adr.test.ts +376 -0
- package/src/lib/adr.ts +173 -27
- package/src/lib/config.test.ts +69 -0
- package/src/lib/config.ts +3 -2
- package/src/lib/links.test.ts +8 -4
- package/src/lib/manipulator-errors.test.ts +22 -0
- package/src/lib/manipulator.test.ts +25 -1
- package/src/lib/numbering.test.ts +8 -0
- package/src/lib/numbering.ts +1 -1
- package/src/lib/opening.test.ts +96 -0
- package/src/lib/prompt.ts +1 -1
- package/src/lib/template.test.ts +74 -0
- package/src/version.ts +1 -2
- package/tests/edit-on-create.e2e.test.ts +47 -16
- package/tests/fake-editor.cmd +2 -0
- package/tests/fake-visual.cmd +2 -0
- package/tests/funny-characters.e2e.test.ts +12 -11
- package/tests/generate-graph.e2e.test.ts +13 -17
- package/tests/helpers/adr-cli.ts +24 -0
- package/tests/init-adr-repository.e2e.test.ts +7 -6
- package/tests/linking-records.e2e.test.ts +14 -13
- package/tests/list-adrs.e2e.test.ts +26 -19
- package/tests/new-adr.e2e.test.ts +10 -9
- package/tests/open-with.e2e.test.ts +53 -0
- package/tests/superseding-records.e2e.test.ts +11 -10
- package/tests/toc-prefixing.e2e.test.ts +10 -9
- package/tests/use-template-override.e2e.test.ts +9 -8
- package/tests/work-form-other-directories.e2e.test.ts +10 -8
- package/vitest.config.e2e.ts +8 -1
- package/vitest.config.ts +20 -4
- package/.github/workflows/sync-deps-to-main.yml +0 -25
- package/.github/workflows/sync-to-deps.yml +0 -26
- package/.husky/commit-msg +0 -4
- package/CHANGELOG.md +0 -121
- package/scripts/inject-version.sh +0 -4
|
@@ -0,0 +1,69 @@
|
|
|
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
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { constants } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
2
|
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
4
|
|
|
5
5
|
export const workingDir = () => process.cwd();
|
|
6
|
+
const rootDir = path.parse(process.cwd()).root;
|
|
6
7
|
|
|
7
8
|
const findTopLevelDir = async (dir: string): Promise<string> => {
|
|
8
9
|
try {
|
|
9
10
|
await fs.access(path.join(dir, '.adr-dir'), constants.F_OK);
|
|
10
11
|
return dir;
|
|
11
12
|
} catch (_e) {
|
|
12
|
-
if (dir ===
|
|
13
|
+
if (dir === rootDir) {
|
|
13
14
|
throw new Error('No ADR directory config found');
|
|
14
15
|
}
|
|
15
16
|
const newDir = path.join(dir, '..');
|
package/src/lib/links.test.ts
CHANGED
|
@@ -7,13 +7,17 @@ vi.mock('./config.js');
|
|
|
7
7
|
vi.mock('fs/promises');
|
|
8
8
|
|
|
9
9
|
describe('The link lib', () => {
|
|
10
|
+
const mockReaddir = vi.mocked(fs.readdir) as unknown as {
|
|
11
|
+
mockResolvedValueOnce: (value: string[]) => void;
|
|
12
|
+
};
|
|
13
|
+
|
|
10
14
|
afterEach(() => {
|
|
11
15
|
vi.resetAllMocks();
|
|
12
16
|
});
|
|
13
17
|
|
|
14
18
|
it('does not care if there are no matches', async () => {
|
|
15
19
|
vi.mocked(getDir).mockResolvedValueOnce('/');
|
|
16
|
-
|
|
20
|
+
mockReaddir.mockResolvedValueOnce([]);
|
|
17
21
|
const linkString = '1:overrides:overriden by';
|
|
18
22
|
const response = await getLinkDetails(linkString);
|
|
19
23
|
expect(response).toEqual({
|
|
@@ -27,7 +31,7 @@ describe('The link lib', () => {
|
|
|
27
31
|
|
|
28
32
|
it('does handles multiple matches', async () => {
|
|
29
33
|
vi.mocked(getDir).mockResolvedValueOnce('/');
|
|
30
|
-
|
|
34
|
+
mockReaddir.mockResolvedValueOnce(['1-one', '1-two', '1-three']);
|
|
31
35
|
const linkString = '1:overrides:overriden by';
|
|
32
36
|
const response = await getLinkDetails(linkString);
|
|
33
37
|
expect(response).toEqual({
|
|
@@ -41,7 +45,7 @@ describe('The link lib', () => {
|
|
|
41
45
|
|
|
42
46
|
it('returns only files that match the pattern', async () => {
|
|
43
47
|
vi.mocked(getDir).mockResolvedValueOnce('/');
|
|
44
|
-
|
|
48
|
+
mockReaddir.mockResolvedValueOnce(['on1e', '1-two', 'three']);
|
|
45
49
|
const linkString = '1:overrides:overriden by';
|
|
46
50
|
const response = await getLinkDetails(linkString);
|
|
47
51
|
expect(response).toEqual({
|
|
@@ -55,7 +59,7 @@ describe('The link lib', () => {
|
|
|
55
59
|
|
|
56
60
|
it('handles superseding', async () => {
|
|
57
61
|
vi.mocked(getDir).mockResolvedValueOnce('/');
|
|
58
|
-
|
|
62
|
+
mockReaddir.mockResolvedValueOnce([]);
|
|
59
63
|
const linkString = '1';
|
|
60
64
|
const supersede = true;
|
|
61
65
|
const response = await getLinkDetails(linkString, supersede);
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getLinksFrom, getTitleFrom, injectLink } from './manipulator.js';
|
|
2
|
+
import { getLinksFrom, getTitleFrom, injectLink, supersede } from './manipulator.js';
|
|
3
3
|
|
|
4
4
|
describe('The ADR manipulator', () => {
|
|
5
5
|
const original =
|
|
@@ -64,12 +64,36 @@ describe('The ADR manipulator', () => {
|
|
|
64
64
|
expect(test).toEqual(expected);
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
+
it('throws when status section is missing for supersede', () => {
|
|
68
|
+
const noStatus = '# NUMBER. TITLE\n';
|
|
69
|
+
const test = () => supersede(noStatus, 'Superseded by [ADR 1](0001-adr.md)');
|
|
70
|
+
expect(test).toThrowError('Could not find status section');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('throws when status section has no paragraph', () => {
|
|
74
|
+
const noStatusParagraph = '# NUMBER. TITLE\n\n## Status\n\n## Context\n';
|
|
75
|
+
const test = () => supersede(noStatusParagraph, 'Superseded by [ADR 1](0001-adr.md)');
|
|
76
|
+
expect(test).toThrowError('There is no status paragraph. Please format your adr properly');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('replaces status paragraph when superseding', () => {
|
|
80
|
+
const markdown = '# NUMBER. TITLE\n\n## Status\n\nAccepted\n\n## Context\n';
|
|
81
|
+
const link = 'Superseded by [ADR 1](0001-adr.md)';
|
|
82
|
+
const result = supersede(markdown, link);
|
|
83
|
+
expect(result).toEqual('# NUMBER. TITLE\n\n## Status\n\nSuperseded by [ADR 1](0001-adr.md)\n\n## Context\n');
|
|
84
|
+
});
|
|
85
|
+
|
|
67
86
|
it('can return the title and number', () => {
|
|
68
87
|
const original = '# 2. This is the title\n';
|
|
69
88
|
const test = getTitleFrom(original);
|
|
70
89
|
expect(test).toEqual('2. This is the title');
|
|
71
90
|
});
|
|
72
91
|
|
|
92
|
+
it('throws when main heading is missing', () => {
|
|
93
|
+
const test = () => getTitleFrom('No heading here');
|
|
94
|
+
expect(test).toThrowError('No main heading found');
|
|
95
|
+
});
|
|
96
|
+
|
|
73
97
|
it('can extract links from the status section', () => {
|
|
74
98
|
const original = '## Status\n' + '\n' + 'Superseded by [3. title here](and-a-link-here.md)\n';
|
|
75
99
|
const extractedLink = getLinksFrom(original);
|
|
@@ -34,4 +34,12 @@ describe('The numbering logic', () => {
|
|
|
34
34
|
const num = await newNumber();
|
|
35
35
|
expect(num).toEqual(3);
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
it('ignores non-adr filenames when deriving numbers', async () => {
|
|
39
|
+
const fakeFiles: string[] = ['notes.txt', '0005-fifth.md'];
|
|
40
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce('no sequence file');
|
|
41
|
+
vi.mocked(fs.readdir as unknown as ReaddirMock).mockResolvedValueOnce(fakeFiles);
|
|
42
|
+
const num = await newNumber();
|
|
43
|
+
expect(num).toEqual(6);
|
|
44
|
+
});
|
|
37
45
|
});
|
package/src/lib/numbering.ts
CHANGED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { chooseOpenPlan } from './adr.js';
|
|
3
|
+
|
|
4
|
+
describe('chooseOpenPlan', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
vi.unstubAllEnvs();
|
|
7
|
+
});
|
|
8
|
+
it('returns none when open is false', () => {
|
|
9
|
+
expect(chooseOpenPlan({ open: false })).toEqual({ type: 'none' });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('treats open-with as enabling open', () => {
|
|
13
|
+
expect(chooseOpenPlan({ openWith: 'code --wait' })).toEqual({
|
|
14
|
+
type: 'app',
|
|
15
|
+
name: 'code',
|
|
16
|
+
args: ['--wait']
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('parses quoted arguments', () => {
|
|
21
|
+
expect(chooseOpenPlan({ openWith: 'code "arg with space"' })).toEqual({
|
|
22
|
+
type: 'app',
|
|
23
|
+
name: 'code',
|
|
24
|
+
args: ['arg with space']
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('unescapes quoted arguments', () => {
|
|
29
|
+
expect(chooseOpenPlan({ openWith: 'code "arg \\"quoted\\""' })).toEqual({
|
|
30
|
+
type: 'app',
|
|
31
|
+
name: 'code',
|
|
32
|
+
args: ['arg "quoted"']
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('parses single-quoted arguments', () => {
|
|
37
|
+
expect(chooseOpenPlan({ openWith: "code 'arg with space'" })).toEqual({
|
|
38
|
+
type: 'app',
|
|
39
|
+
name: 'code',
|
|
40
|
+
args: ['arg with space']
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns none when open-with is not parseable', () => {
|
|
45
|
+
expect(chooseOpenPlan({ openWith: '"' })).toEqual({ type: 'none' });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns none when open-with is whitespace', () => {
|
|
49
|
+
expect(chooseOpenPlan({ openWith: ' ' })).toEqual({ type: 'none' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('returns none when open-with is only quoted whitespace', () => {
|
|
53
|
+
expect(chooseOpenPlan({ openWith: '" "' })).toEqual({ type: 'none' });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('handles quoted empty command', () => {
|
|
57
|
+
expect(chooseOpenPlan({ openWith: '""', open: true })).toEqual({ type: 'default' });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('ignores boolean-like editor values', () => {
|
|
61
|
+
vi.stubEnv('VISUAL', 'true');
|
|
62
|
+
vi.stubEnv('EDITOR', 'false');
|
|
63
|
+
expect(chooseOpenPlan({ open: true })).toEqual({ type: 'default' });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('prefers VISUAL over EDITOR when both are set', () => {
|
|
67
|
+
vi.stubEnv('VISUAL', 'code');
|
|
68
|
+
vi.stubEnv('EDITOR', 'vim');
|
|
69
|
+
expect(chooseOpenPlan({ open: true })).toEqual({ type: 'app', name: 'code', args: [] });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('uses EDITOR when it is not npm injected', () => {
|
|
73
|
+
vi.stubEnv('EDITOR', 'vim');
|
|
74
|
+
vi.stubEnv('npm_execpath', '');
|
|
75
|
+
vi.stubEnv('npm_config_editor', '');
|
|
76
|
+
expect(chooseOpenPlan({ open: true })).toEqual({ type: 'app', name: 'vim', args: [] });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('falls back to default when EDITOR is npm injected', () => {
|
|
80
|
+
vi.stubEnv('npm_execpath', '/usr/bin/npm');
|
|
81
|
+
vi.stubEnv('npm_config_editor', 'vi');
|
|
82
|
+
vi.stubEnv('EDITOR', 'vi');
|
|
83
|
+
vi.stubEnv('VISUAL', '');
|
|
84
|
+
|
|
85
|
+
expect(chooseOpenPlan({ open: true })).toEqual({ type: 'default' });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('uses EDITOR when npm_config_editor is different', () => {
|
|
89
|
+
vi.stubEnv('npm_execpath', '/usr/bin/npm');
|
|
90
|
+
vi.stubEnv('npm_config_editor', 'code');
|
|
91
|
+
vi.stubEnv('EDITOR', 'vim');
|
|
92
|
+
vi.stubEnv('VISUAL', '');
|
|
93
|
+
|
|
94
|
+
expect(chooseOpenPlan({ open: true })).toEqual({ type: 'app', name: 'vim', args: [] });
|
|
95
|
+
});
|
|
96
|
+
});
|
package/src/lib/prompt.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
2
|
|
|
4
3
|
export const askForClarification = async (searchString: string, matches: string[]) => {
|
|
4
|
+
const { default: inquirer } = await import('inquirer');
|
|
5
5
|
const selection = await inquirer.prompt([
|
|
6
6
|
{
|
|
7
7
|
type: 'list',
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
readFile: vi.fn()
|
|
8
|
+
}
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock('./config.js', () => ({
|
|
12
|
+
getDir: vi.fn()
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
const mockEnv = (key: string, value: string | undefined) => {
|
|
16
|
+
if (value === undefined) {
|
|
17
|
+
delete process.env[key];
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
process.env[key] = value;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
describe('template', () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.resetModules();
|
|
26
|
+
vi.mocked(fs.readFile).mockReset();
|
|
27
|
+
mockEnv('ADR_TEMPLATE', undefined);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
mockEnv('ADR_TEMPLATE', undefined);
|
|
32
|
+
vi.restoreAllMocks();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('uses the explicit template file when provided', async () => {
|
|
36
|
+
const { template } = await import('./template.js');
|
|
37
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('custom');
|
|
38
|
+
|
|
39
|
+
const result = await template('/tmp/custom.md');
|
|
40
|
+
expect(result).toEqual('custom');
|
|
41
|
+
expect(fs.readFile).toHaveBeenCalledWith(path.resolve('/tmp/custom.md'), 'utf8');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('uses ADR_TEMPLATE when set', async () => {
|
|
45
|
+
const { template } = await import('./template.js');
|
|
46
|
+
mockEnv('ADR_TEMPLATE', '/tmp/env.md');
|
|
47
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('env');
|
|
48
|
+
|
|
49
|
+
const result = await template();
|
|
50
|
+
expect(result).toEqual('env');
|
|
51
|
+
expect(fs.readFile).toHaveBeenCalledWith(path.resolve('/tmp/env.md'), 'utf8');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('uses template from adr directory when available', async () => {
|
|
55
|
+
const { template } = await import('./template.js');
|
|
56
|
+
const { getDir } = await import('./config.js');
|
|
57
|
+
vi.mocked(getDir).mockResolvedValueOnce('/repo/doc/adr');
|
|
58
|
+
vi.mocked(fs.readFile).mockResolvedValueOnce('repo-template');
|
|
59
|
+
|
|
60
|
+
const result = await template();
|
|
61
|
+
expect(result).toEqual('repo-template');
|
|
62
|
+
expect(fs.readFile).toHaveBeenCalledWith(path.join('/repo/doc/adr', 'templates/template.md'), 'utf8');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('falls back to built-in template when repo template is missing', async () => {
|
|
66
|
+
const { template } = await import('./template.js');
|
|
67
|
+
const { getDir } = await import('./config.js');
|
|
68
|
+
vi.mocked(getDir).mockResolvedValueOnce('/repo/doc/adr');
|
|
69
|
+
vi.mocked(fs.readFile).mockRejectedValueOnce(new Error('missing')).mockResolvedValueOnce('built-in');
|
|
70
|
+
|
|
71
|
+
const result = await template();
|
|
72
|
+
expect(result).toEqual('built-in');
|
|
73
|
+
});
|
|
74
|
+
});
|
package/src/version.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export const LIB_VERSION = '
|
|
2
|
-
|
|
1
|
+
export const LIB_VERSION = '2.0.1';
|
|
@@ -1,15 +1,32 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as fs from 'fs';
|
|
1
|
+
import * as fs from 'node:fs';
|
|
3
2
|
import * as os from 'os';
|
|
4
3
|
import * as path from 'path';
|
|
5
4
|
/* eslint-disable no-sync */
|
|
6
5
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
6
|
+
import { createAdrCli } from './helpers/adr-cli';
|
|
7
|
+
|
|
8
|
+
const waitForFile = async (filePath: string, timeoutMs: number) => {
|
|
9
|
+
const startedAt = Date.now();
|
|
10
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
11
|
+
if (fs.existsSync(filePath)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`Timed out waiting for file: ${filePath}`);
|
|
17
|
+
};
|
|
7
18
|
|
|
8
19
|
describe('Edit new Adrs on creation', () => {
|
|
9
20
|
const adr = path.resolve(path.dirname(__filename), '../src/index.ts');
|
|
10
|
-
const
|
|
11
|
-
const visualHelper =
|
|
12
|
-
|
|
21
|
+
const cli = createAdrCli(adr);
|
|
22
|
+
const visualHelper =
|
|
23
|
+
process.platform === 'win32'
|
|
24
|
+
? path.resolve(path.dirname(__filename), './fake-visual.cmd')
|
|
25
|
+
: path.resolve(path.dirname(__filename), './fake-visual');
|
|
26
|
+
const editorHelper =
|
|
27
|
+
process.platform === 'win32'
|
|
28
|
+
? path.resolve(path.dirname(__filename), './fake-editor.cmd')
|
|
29
|
+
: path.resolve(path.dirname(__filename), './fake-editor');
|
|
13
30
|
|
|
14
31
|
let adrDirectory: string;
|
|
15
32
|
let workDir: string;
|
|
@@ -22,38 +39,52 @@ describe('Edit new Adrs on creation', () => {
|
|
|
22
39
|
});
|
|
23
40
|
|
|
24
41
|
afterEach(() => {
|
|
25
|
-
fs.
|
|
42
|
+
fs.rmSync(workDir, {
|
|
26
43
|
recursive: true,
|
|
44
|
+
force: true,
|
|
27
45
|
maxRetries: 3,
|
|
28
46
|
retryDelay: 500
|
|
29
47
|
});
|
|
30
48
|
});
|
|
31
49
|
|
|
32
|
-
it('should open a new ADR in the VISUAL', () => {
|
|
33
|
-
|
|
50
|
+
it('should open a new ADR in the VISUAL', async () => {
|
|
51
|
+
cli.run(['new', '--open', 'Example', 'ADR'], {
|
|
52
|
+
cwd: workDir,
|
|
53
|
+
env: { VISUAL: visualHelper }
|
|
54
|
+
});
|
|
34
55
|
|
|
35
56
|
const expectedNewFile: string = path.join(workDir, 'visual.out');
|
|
57
|
+
await waitForFile(expectedNewFile, 2000);
|
|
36
58
|
const fileContents = fs.readFileSync(expectedNewFile, 'utf8');
|
|
37
|
-
|
|
59
|
+
const reported = fileContents.trim().replace(/^VISUAL\s+/, '');
|
|
60
|
+
expect(path.normalize(reported)).toEqual(path.normalize(`${adrDirectory}/0001-example-adr.md`));
|
|
38
61
|
});
|
|
39
62
|
|
|
40
|
-
it
|
|
41
|
-
|
|
63
|
+
it('should open a new ADR in the EDITOR', async () => {
|
|
64
|
+
cli.run(['new', '--open', 'Example', 'ADR'], {
|
|
65
|
+
cwd: workDir,
|
|
66
|
+
env: { EDITOR: editorHelper }
|
|
67
|
+
});
|
|
42
68
|
|
|
43
69
|
const expectedNewFile: string = path.join(workDir, 'editor.out');
|
|
70
|
+
await waitForFile(expectedNewFile, 2000);
|
|
44
71
|
const fileContents = fs.readFileSync(expectedNewFile, 'utf8');
|
|
45
|
-
|
|
72
|
+
const reported = fileContents.trim().replace(/^EDITOR\s+/, '');
|
|
73
|
+
expect(path.normalize(reported)).toEqual(path.normalize(`${adrDirectory}/0001-example-adr.md`));
|
|
46
74
|
});
|
|
47
75
|
|
|
48
|
-
it('should open a new ADR in the VISUAL if both VISUAL and EDITOR is supplied', () => {
|
|
49
|
-
|
|
50
|
-
cwd: workDir
|
|
76
|
+
it('should open a new ADR in the VISUAL if both VISUAL and EDITOR is supplied', async () => {
|
|
77
|
+
cli.run(['new', '--open', 'Example', 'ADR'], {
|
|
78
|
+
cwd: workDir,
|
|
79
|
+
env: { EDITOR: editorHelper, VISUAL: visualHelper }
|
|
51
80
|
});
|
|
52
81
|
|
|
53
82
|
expect(fs.existsSync(path.join(workDir, 'editor.out'))).toBeFalsy();
|
|
54
83
|
|
|
55
84
|
const expectedNewFile: string = path.join(workDir, 'visual.out');
|
|
85
|
+
await waitForFile(expectedNewFile, 2000);
|
|
56
86
|
const fileContents = fs.readFileSync(expectedNewFile, 'utf8');
|
|
57
|
-
|
|
87
|
+
const reported = fileContents.trim().replace(/^VISUAL\s+/, '');
|
|
88
|
+
expect(path.normalize(reported)).toEqual(path.normalize(`${adrDirectory}/0001-example-adr.md`));
|
|
58
89
|
});
|
|
59
90
|
});
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import * as
|
|
2
|
-
import * as fs from 'fs';
|
|
1
|
+
import * as fs from 'node:fs';
|
|
3
2
|
import { fileURLToPath } from 'node:url';
|
|
4
3
|
import os from 'os';
|
|
5
4
|
import * as path from 'path';
|
|
6
5
|
|
|
7
6
|
/* eslint-disable no-sync */
|
|
8
7
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
8
|
+
import { createAdrCli } from './helpers/adr-cli';
|
|
9
9
|
|
|
10
10
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
11
|
|
|
12
|
-
describe
|
|
12
|
+
describe('Funny Characters', () => {
|
|
13
13
|
const adr: string = path.resolve(path.dirname(__filename), '../src/index.ts');
|
|
14
|
-
const
|
|
14
|
+
const cli = createAdrCli(adr);
|
|
15
15
|
|
|
16
16
|
let adrDirectory: string;
|
|
17
17
|
let workDir: string;
|
|
18
18
|
|
|
19
19
|
afterEach(() => {
|
|
20
|
-
fs.
|
|
20
|
+
fs.rmSync(workDir, {
|
|
21
21
|
recursive: true,
|
|
22
|
+
force: true,
|
|
22
23
|
maxRetries: 3,
|
|
23
24
|
retryDelay: 500
|
|
24
25
|
});
|
|
@@ -27,23 +28,23 @@ describe.skip('Funny Characters', () => {
|
|
|
27
28
|
beforeEach(() => {
|
|
28
29
|
workDir = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'adr-')));
|
|
29
30
|
adrDirectory = path.resolve(path.join(workDir, 'doc/adr'));
|
|
30
|
-
|
|
31
|
+
cli.run(['init', adrDirectory], { cwd: workDir });
|
|
31
32
|
});
|
|
32
33
|
|
|
33
34
|
it('should handle titles with periods in them', async () => {
|
|
34
|
-
|
|
35
|
+
cli.run(['new', 'Something', 'About', 'Node.JS'], { cwd: workDir });
|
|
35
36
|
const expectedFile: string = path.join(adrDirectory, '0002-something-about-node-js.md');
|
|
36
37
|
expect(fs.existsSync(expectedFile)).toBeTruthy();
|
|
37
38
|
});
|
|
38
39
|
|
|
39
|
-
it
|
|
40
|
-
|
|
40
|
+
it('should handle titles with slashes in them', async () => {
|
|
41
|
+
cli.run(['new', 'Slash/Slash/Slash/'], { cwd: workDir });
|
|
41
42
|
const expectedFile: string = path.join(adrDirectory, '0002-slash-slash-slash.md');
|
|
42
43
|
expect(fs.existsSync(expectedFile)).toBeTruthy();
|
|
43
44
|
});
|
|
44
45
|
|
|
45
|
-
it
|
|
46
|
-
|
|
46
|
+
it('should handle titles with other weirdness in them', async () => {
|
|
47
|
+
cli.run(['new', '--', '-Bar-'], { cwd: workDir });
|
|
47
48
|
const expectedFile: string = path.join(adrDirectory, '0002-bar.md');
|
|
48
49
|
expect(fs.existsSync(expectedFile)).toBeTruthy();
|
|
49
50
|
});
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { realpathSync, rmSync } from 'node:fs';
|
|
2
|
+
import * as fs from 'fs/promises';
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
|
-
import * as fs from 'fs/promises';
|
|
6
5
|
/* eslint-disable no-sync */
|
|
7
6
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
7
|
+
import { createAdrCli } from './helpers/adr-cli';
|
|
8
8
|
|
|
9
9
|
describe('Generating Graphs', () => {
|
|
10
10
|
const adr = path.resolve(path.dirname(__filename), '../src/index.ts');
|
|
11
|
-
const
|
|
11
|
+
const cli = createAdrCli(adr);
|
|
12
12
|
let workDir: string;
|
|
13
13
|
|
|
14
14
|
beforeAll(async () => {
|
|
15
15
|
// @ts-ignore
|
|
16
16
|
process.env.ADR_DATE = '1992-01-12';
|
|
17
17
|
workDir = path.resolve(realpathSync(await fs.mkdtemp(path.join(os.tmpdir(), 'adr-'))));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
cli.run(['init'], { cwd: workDir });
|
|
19
|
+
cli.run(['new', 'An', 'idea', 'that', 'seems', 'good', 'at', 'the', 'time'], { cwd: workDir });
|
|
20
|
+
cli.run(['new', '-s', '2', 'A', 'better', 'idea'], { cwd: workDir });
|
|
21
|
+
cli.run(['new', 'This', 'will', 'work'], { cwd: workDir });
|
|
22
|
+
cli.run(['new', '-s', '3', 'The', 'end'], { cwd: workDir });
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
afterEach(() => {
|
|
@@ -27,26 +27,22 @@ describe('Generating Graphs', () => {
|
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
afterAll(() => {
|
|
30
|
-
|
|
30
|
+
rmSync(workDir, {
|
|
31
31
|
recursive: true,
|
|
32
|
+
force: true,
|
|
32
33
|
maxRetries: 3,
|
|
33
34
|
retryDelay: 500
|
|
34
35
|
});
|
|
35
36
|
});
|
|
36
37
|
|
|
37
38
|
it('should generate a graph', async () => {
|
|
38
|
-
const
|
|
39
|
-
const childContent = child.toString().trim();
|
|
39
|
+
const childContent = cli.run(['generate', 'graph'], { cwd: workDir });
|
|
40
40
|
|
|
41
41
|
expect(childContent).toMatchSnapshot();
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
it('should generate a graph with specified route and extension ', async () => {
|
|
45
|
-
const
|
|
46
|
-
timeout: 10000,
|
|
47
|
-
cwd: workDir
|
|
48
|
-
});
|
|
49
|
-
const childContent = child.toString().trim();
|
|
45
|
+
const childContent = cli.run(['generate', 'graph', '-p', 'http://example.com/', '-e', '.xxx'], { cwd: workDir });
|
|
50
46
|
|
|
51
47
|
expect(childContent).toMatchSnapshot();
|
|
52
48
|
});
|