@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.
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +21 -9
- package/.adr-dir +0 -1
- package/.editorconfig +0 -13
- package/.gitattributes +0 -40
- package/.github/renovate.json +0 -4
- package/.github/stale.yml +0 -18
- package/.github/workflows/ci-pr.yml +0 -48
- package/.github/workflows/ci.yml +0 -67
- package/.github/workflows/security.yml +0 -49
- package/.releaserc.json +0 -49
- package/.yarnrc.yml +0 -8
- package/AGENTS.engineer.md +0 -236
- package/AGENTS.md +0 -11
- package/AGENTS.reviewer.md +0 -115
- package/CONTRIBUTING.md +0 -102
- package/biome.json +0 -28
- package/dist/index.test.js +0 -197
- package/dist/index.test.js.map +0 -1
- package/dist/inject-version.test.js +0 -27
- package/dist/inject-version.test.js.map +0 -1
- package/dist/lib/adr.test.js +0 -319
- package/dist/lib/adr.test.js.map +0 -1
- package/dist/lib/config.test.js +0 -60
- package/dist/lib/config.test.js.map +0 -1
- package/dist/lib/links.test.js +0 -66
- package/dist/lib/links.test.js.map +0 -1
- package/dist/lib/manipulator-errors.test.js +0 -21
- package/dist/lib/manipulator-errors.test.js.map +0 -1
- package/dist/lib/manipulator.test.js +0 -96
- package/dist/lib/manipulator.test.js.map +0 -1
- package/dist/lib/numbering.test.js +0 -39
- package/dist/lib/numbering.test.js.map +0 -1
- package/dist/lib/opening.test.js +0 -81
- package/dist/lib/opening.test.js.map +0 -1
- package/dist/lib/prompt.test.js +0 -33
- package/dist/lib/prompt.test.js.map +0 -1
- package/dist/lib/template.test.js +0 -62
- package/dist/lib/template.test.js.map +0 -1
- package/dist/types/index.test.d.ts +0 -2
- package/dist/types/index.test.d.ts.map +0 -1
- package/dist/types/inject-version.test.d.ts +0 -2
- package/dist/types/inject-version.test.d.ts.map +0 -1
- package/dist/types/lib/adr.test.d.ts +0 -2
- package/dist/types/lib/adr.test.d.ts.map +0 -1
- package/dist/types/lib/config.test.d.ts +0 -2
- package/dist/types/lib/config.test.d.ts.map +0 -1
- package/dist/types/lib/links.test.d.ts +0 -2
- package/dist/types/lib/links.test.d.ts.map +0 -1
- package/dist/types/lib/manipulator-errors.test.d.ts +0 -2
- package/dist/types/lib/manipulator-errors.test.d.ts.map +0 -1
- package/dist/types/lib/manipulator.test.d.ts +0 -2
- package/dist/types/lib/manipulator.test.d.ts.map +0 -1
- package/dist/types/lib/numbering.test.d.ts +0 -2
- package/dist/types/lib/numbering.test.d.ts.map +0 -1
- package/dist/types/lib/opening.test.d.ts +0 -2
- package/dist/types/lib/opening.test.d.ts.map +0 -1
- package/dist/types/lib/prompt.test.d.ts +0 -2
- package/dist/types/lib/prompt.test.d.ts.map +0 -1
- package/dist/types/lib/template.test.d.ts +0 -2
- package/dist/types/lib/template.test.d.ts.map +0 -1
- package/doc/adr/.adr-sequence.lock +0 -1
- package/doc/adr/0001-record-architecture-decisions.md +0 -21
- package/doc/adr/0002-using-heavy-e2e-tests.md +0 -20
- package/doc/adr/0003-esm.md +0 -34
- package/doc/adr/0004-gate-editor-opening-behind---open-and---open-with.md +0 -55
- package/doc/adr/decisions.md +0 -6
- package/lefthook.yml +0 -14
- package/scripts/inject-version.mjs +0 -34
- package/src/index.test.ts +0 -222
- package/src/index.ts +0 -261
- package/src/inject-version.test.ts +0 -31
- package/src/lib/adr.test.ts +0 -389
- package/src/lib/adr.ts +0 -384
- package/src/lib/config.test.ts +0 -69
- package/src/lib/config.ts +0 -36
- package/src/lib/links.test.ts +0 -74
- package/src/lib/links.ts +0 -34
- package/src/lib/manipulator-errors.test.ts +0 -22
- package/src/lib/manipulator.test.ts +0 -109
- package/src/lib/manipulator.ts +0 -98
- package/src/lib/numbering.test.ts +0 -45
- package/src/lib/numbering.ts +0 -25
- package/src/lib/opening.test.ts +0 -96
- package/src/lib/prompt.test.ts +0 -42
- package/src/lib/prompt.ts +0 -14
- package/src/lib/template.test.ts +0 -74
- package/src/lib/template.ts +0 -22
- package/src/templates/init.md +0 -21
- package/src/templates/template.md +0 -19
- package/src/types/environment.d.ts +0 -14
- package/src/version.ts +0 -1
- package/tests/.adr-dir +0 -1
- package/tests/__snapshots__/generate-graph.e2e.test.ts.snap +0 -39
- package/tests/__snapshots__/init-adr-repository.e2e.test.ts.snap +0 -51
- package/tests/__snapshots__/linking-records.e2e.test.ts.snap +0 -155
- package/tests/__snapshots__/new-adr.e2e.test.ts.snap +0 -54
- package/tests/__snapshots__/superseding-records.e2e.test.ts.snap +0 -122
- package/tests/__snapshots__/toc-prefixing.e2e.test.ts.snap +0 -9
- package/tests/__snapshots__/use-template-override.e2e.test.ts.snap +0 -17
- package/tests/edit-on-create.e2e.test.ts +0 -90
- package/tests/fake-editor +0 -5
- package/tests/fake-editor.cmd +0 -4
- package/tests/fake-visual +0 -5
- package/tests/fake-visual.cmd +0 -4
- package/tests/funny-characters.e2e.test.ts +0 -51
- package/tests/generate-graph.e2e.test.ts +0 -49
- package/tests/helpers/adr-cli.ts +0 -24
- package/tests/init-adr-repository.e2e.test.ts +0 -58
- package/tests/linking-records.e2e.test.ts +0 -72
- package/tests/list-adrs.e2e.test.ts +0 -60
- package/tests/new-adr.e2e.test.ts +0 -62
- package/tests/open-with.e2e.test.ts +0 -53
- package/tests/superseding-records.e2e.test.ts +0 -64
- package/tests/toc-prefixing.e2e.test.ts +0 -43
- package/tests/use-template-override.e2e.test.ts +0 -52
- package/tests/work-form-other-directories.e2e.test.ts +0 -48
- package/tsconfig.json +0 -55
- package/vitest.config.e2e.ts +0 -18
- package/vitest.config.ts +0 -30
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { getLinksFrom, getTitleFrom, injectLink, supersede } from './manipulator.js';
|
|
3
|
-
|
|
4
|
-
describe('The ADR manipulator', () => {
|
|
5
|
-
const original =
|
|
6
|
-
'# NUMBER. TITLE\n' +
|
|
7
|
-
'\n' +
|
|
8
|
-
'Date: DATE\n' +
|
|
9
|
-
'\n' +
|
|
10
|
-
'## Status\n' +
|
|
11
|
-
'\n' +
|
|
12
|
-
'STATUS\n' +
|
|
13
|
-
'\n' +
|
|
14
|
-
'## Context\n' +
|
|
15
|
-
'\n' +
|
|
16
|
-
'The issue motivating this decision, and any context that influences or constrains the decision.\n' +
|
|
17
|
-
'\n' +
|
|
18
|
-
'## Decision\n' +
|
|
19
|
-
'\n' +
|
|
20
|
-
"The change that we're proposing or have agreed to implement.\n" +
|
|
21
|
-
'\n' +
|
|
22
|
-
'## Consequences\n' +
|
|
23
|
-
'\n' +
|
|
24
|
-
'What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n';
|
|
25
|
-
|
|
26
|
-
const modified =
|
|
27
|
-
'# NUMBER. TITLE\n' +
|
|
28
|
-
'\n' +
|
|
29
|
-
'Date: DATE\n' +
|
|
30
|
-
'\n' +
|
|
31
|
-
'## Status\n' +
|
|
32
|
-
'\n' +
|
|
33
|
-
'STATUS\n' +
|
|
34
|
-
'\n' +
|
|
35
|
-
'INJECTED STUFF\n' +
|
|
36
|
-
'\n' +
|
|
37
|
-
'## Context\n' +
|
|
38
|
-
'\n' +
|
|
39
|
-
'The issue motivating this decision, and any context that influences or constrains the decision.\n' +
|
|
40
|
-
'\n' +
|
|
41
|
-
'## Decision\n' +
|
|
42
|
-
'\n' +
|
|
43
|
-
"The change that we're proposing or have agreed to implement.\n" +
|
|
44
|
-
'\n' +
|
|
45
|
-
'## Consequences\n' +
|
|
46
|
-
'\n' +
|
|
47
|
-
'What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.\n';
|
|
48
|
-
|
|
49
|
-
it('can inject links to the status section', () => {
|
|
50
|
-
const test = injectLink(original, 'INJECTED STUFF');
|
|
51
|
-
expect(test).toEqual(modified);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('throws an error when status cannot be found', () => {
|
|
55
|
-
const noStatus = '# NUMBER. TITLE\n';
|
|
56
|
-
const test = () => injectLink(noStatus, 'INJECTED STUFF');
|
|
57
|
-
expect(test).toThrowError('Could not find status section');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('adds to the status section even when there are no following headers', () => {
|
|
61
|
-
const noFollowingHeaders = '## STATUS\nACCEPTED\n\n';
|
|
62
|
-
const expected = '## STATUS\nACCEPTED\n\nINJECTED STUFF\n\n';
|
|
63
|
-
const test = injectLink(noFollowingHeaders, 'INJECTED STUFF');
|
|
64
|
-
expect(test).toEqual(expected);
|
|
65
|
-
});
|
|
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
|
-
|
|
86
|
-
it('can return the title and number', () => {
|
|
87
|
-
const original = '# 2. This is the title\n';
|
|
88
|
-
const test = getTitleFrom(original);
|
|
89
|
-
expect(test).toEqual('2. This is the title');
|
|
90
|
-
});
|
|
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
|
-
|
|
97
|
-
it('can extract links from the status section', () => {
|
|
98
|
-
const original = '## Status\n' + '\n' + 'Superseded by [3. title here](and-a-link-here.md)\n';
|
|
99
|
-
const extractedLink = getLinksFrom(original);
|
|
100
|
-
|
|
101
|
-
expect(extractedLink[0]).toEqual({
|
|
102
|
-
label: 'Superseded by',
|
|
103
|
-
targetNumber: '3',
|
|
104
|
-
text: '3. title here',
|
|
105
|
-
href: 'and-a-link-here.md',
|
|
106
|
-
raw: 'Superseded by [3. title here](and-a-link-here.md)'
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
});
|
package/src/lib/manipulator.ts
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { marked, type Token, type Tokens } from 'marked';
|
|
2
|
-
|
|
3
|
-
const convertToMd = (tokens: Token[]) => {
|
|
4
|
-
return tokens.map((token) => token.raw).join('');
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export const getLinksFrom = (markdown: string) => {
|
|
8
|
-
const tokens = marked.lexer(markdown);
|
|
9
|
-
const linkRegex = new RegExp(/^(.*)\s\[(.*)\]\((.*)\)$/);
|
|
10
|
-
const links = tokens.filter((token) => token.type === 'paragraph' && linkRegex.test(token.text));
|
|
11
|
-
|
|
12
|
-
return links.map((link) => {
|
|
13
|
-
const linkToken = link as Tokens.Paragraph;
|
|
14
|
-
const linkMatches = linkToken.text.match(linkRegex);
|
|
15
|
-
if (!linkMatches || linkMatches.length < 3) {
|
|
16
|
-
throw new Error(`Could not parse link from "${linkToken.text}"`);
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
targetNumber: linkMatches[2].substring(0, linkMatches[2].indexOf('.')),
|
|
20
|
-
label: linkMatches[1],
|
|
21
|
-
href: linkMatches[3],
|
|
22
|
-
text: linkMatches[2],
|
|
23
|
-
raw: linkMatches[0]
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export const getTitleFrom = (adr: string) => {
|
|
29
|
-
const tokens = marked.lexer(adr);
|
|
30
|
-
const mainHead = tokens.find((token) => token.type === 'heading' && token.depth === 1);
|
|
31
|
-
if (!mainHead) {
|
|
32
|
-
throw new Error('No main heading found');
|
|
33
|
-
}
|
|
34
|
-
return (mainHead as Tokens.Heading).text.trim();
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
export const supersede = (markdown: string, link: string) => {
|
|
38
|
-
const tokens = marked.lexer(markdown);
|
|
39
|
-
const statusIndex = tokens.findIndex((token) => token.type === 'heading' && token.text.toLowerCase() === 'status');
|
|
40
|
-
if (statusIndex < 0) {
|
|
41
|
-
throw new Error('Could not find status section');
|
|
42
|
-
}
|
|
43
|
-
const statusDepth = (tokens[statusIndex] as Tokens.Heading).depth;
|
|
44
|
-
const followingHeadingIndex = tokens.findIndex(
|
|
45
|
-
(token, index) => token.type === 'heading' && token.depth === statusDepth && index > statusIndex
|
|
46
|
-
);
|
|
47
|
-
const followingParagraphIndex = tokens.findIndex(
|
|
48
|
-
(token, index) => token.type === 'paragraph' && index > statusIndex && index < followingHeadingIndex
|
|
49
|
-
);
|
|
50
|
-
|
|
51
|
-
if (followingParagraphIndex > followingHeadingIndex || followingParagraphIndex === -1) {
|
|
52
|
-
throw new Error('There is no status paragraph. Please format your adr properly');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
tokens[followingParagraphIndex] = {
|
|
56
|
-
type: 'paragraph',
|
|
57
|
-
text: link,
|
|
58
|
-
raw: link,
|
|
59
|
-
tokens: [
|
|
60
|
-
{
|
|
61
|
-
type: 'text',
|
|
62
|
-
raw: link,
|
|
63
|
-
text: link
|
|
64
|
-
}
|
|
65
|
-
]
|
|
66
|
-
};
|
|
67
|
-
return convertToMd(tokens);
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
export const injectLink = (markdown: string, link: string) => {
|
|
71
|
-
const tokens = marked.lexer(markdown);
|
|
72
|
-
const statusIndex = tokens.findIndex((token) => token.type === 'heading' && token.text.toLowerCase() === 'status');
|
|
73
|
-
if (statusIndex < 0) {
|
|
74
|
-
throw new Error('Could not find status section');
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const statusDepth = (tokens[statusIndex] as Tokens.Heading).depth;
|
|
78
|
-
let followingHeadingIndex = tokens.findIndex(
|
|
79
|
-
(token, index) => token.type === 'heading' && token.depth === statusDepth && index > statusIndex
|
|
80
|
-
);
|
|
81
|
-
if (followingHeadingIndex < 0) {
|
|
82
|
-
followingHeadingIndex = tokens.length;
|
|
83
|
-
}
|
|
84
|
-
tokens.splice(followingHeadingIndex, 0, {
|
|
85
|
-
type: 'paragraph',
|
|
86
|
-
text: link,
|
|
87
|
-
raw: link,
|
|
88
|
-
tokens: [
|
|
89
|
-
{
|
|
90
|
-
type: 'text',
|
|
91
|
-
raw: link,
|
|
92
|
-
text: link
|
|
93
|
-
}
|
|
94
|
-
]
|
|
95
|
-
});
|
|
96
|
-
tokens.splice(followingHeadingIndex + 1, 0, { type: 'space', raw: '\n\n' });
|
|
97
|
-
return convertToMd(tokens);
|
|
98
|
-
};
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
3
|
-
import { newNumber } from './numbering.js';
|
|
4
|
-
|
|
5
|
-
type ReaddirMock = () => Promise<string[]>;
|
|
6
|
-
|
|
7
|
-
vi.mock('fs/promises');
|
|
8
|
-
vi.mock('./config.js', () => ({
|
|
9
|
-
getDir: vi.fn().mockResolvedValue('/')
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
describe('The numbering logic', () => {
|
|
13
|
-
afterAll(() => {
|
|
14
|
-
vi.resetAllMocks();
|
|
15
|
-
});
|
|
16
|
-
it('can read from the sequence file', async () => {
|
|
17
|
-
const random = Math.floor(Math.random() * 100);
|
|
18
|
-
vi.mocked(fs.readFile).mockResolvedValueOnce(random.toString());
|
|
19
|
-
const num = await newNumber();
|
|
20
|
-
expect(num).toEqual(random + 1);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('returns 1 if there are no files', async () => {
|
|
24
|
-
vi.mocked(fs.readFile).mockRejectedValueOnce('no sequence file');
|
|
25
|
-
vi.mocked(fs.readdir).mockResolvedValueOnce([]);
|
|
26
|
-
const num = await newNumber();
|
|
27
|
-
expect(num).toEqual(1);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it('processes existing files if there is no lockfile', async () => {
|
|
31
|
-
const fakeFiles: string[] = ['0001-first-file.md', '0002-first-file.md'];
|
|
32
|
-
vi.mocked(fs.readFile).mockRejectedValueOnce('no sequence file');
|
|
33
|
-
vi.mocked(fs.readdir as unknown as ReaddirMock).mockResolvedValueOnce(fakeFiles);
|
|
34
|
-
const num = await newNumber();
|
|
35
|
-
expect(num).toEqual(3);
|
|
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
|
-
});
|
|
45
|
-
});
|
package/src/lib/numbering.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import * as path from 'path';
|
|
3
|
-
import { getDir } from './config.js';
|
|
4
|
-
|
|
5
|
-
export const newNumber = async () => {
|
|
6
|
-
try {
|
|
7
|
-
const lockfile = await fs.readFile(path.resolve(await getDir(), '.adr-sequence.lock'));
|
|
8
|
-
return parseInt(lockfile.toString().trim(), 10) + 1;
|
|
9
|
-
} catch (_e) {
|
|
10
|
-
// This is for backward compatibility. If someone upgrades from an older tool without a lockfile,
|
|
11
|
-
// we create one.
|
|
12
|
-
const filePattern = /^0*(\d+)-.*$/;
|
|
13
|
-
const files = await fs.readdir(await getDir());
|
|
14
|
-
const numbers = files.map((f) => {
|
|
15
|
-
const adrFile = f.match(filePattern);
|
|
16
|
-
if (adrFile) {
|
|
17
|
-
return parseInt(adrFile[1], 10);
|
|
18
|
-
}
|
|
19
|
-
return 0;
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
const largestNumber = numbers.reduce((a, b) => Math.max(a, b), 0);
|
|
23
|
-
return largestNumber + 1;
|
|
24
|
-
}
|
|
25
|
-
};
|
package/src/lib/opening.test.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
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.test.ts
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { faker } from '@faker-js/faker';
|
|
2
|
-
import inquirer, { Question } from 'inquirer';
|
|
3
|
-
import { describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { askForClarification } from './prompt.js';
|
|
5
|
-
|
|
6
|
-
vi.mock('inquirer');
|
|
7
|
-
vi.mock('chalk', () => ({
|
|
8
|
-
default: {
|
|
9
|
-
blue: (str: string) => {
|
|
10
|
-
return `BLUE[${str}]`;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
}));
|
|
14
|
-
describe('The prompt helper', () => {
|
|
15
|
-
it('calls inquirer correctly', async () => {
|
|
16
|
-
const prompt = vi.mocked(inquirer.prompt);
|
|
17
|
-
prompt.mockResolvedValueOnce({ target: 'does not matter' });
|
|
18
|
-
|
|
19
|
-
const randomChoices = [faker.system.filePath(), faker.system.filePath(), faker.system.filePath()];
|
|
20
|
-
await askForClarification('this is the search string', randomChoices);
|
|
21
|
-
|
|
22
|
-
expect(prompt).toHaveBeenCalledOnce();
|
|
23
|
-
const calledWith = prompt.mock.calls[0][0] as Question[];
|
|
24
|
-
expect(calledWith[0].type).toEqual('select');
|
|
25
|
-
expect(calledWith[0].choices).toEqual(randomChoices);
|
|
26
|
-
expect(calledWith[0].message).toMatchInlineSnapshot(
|
|
27
|
-
'"Which file do you want to link to for BLUE[this is the search string]?"'
|
|
28
|
-
);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns the selected choice', async () => {
|
|
32
|
-
const prompt = vi.mocked(inquirer.prompt);
|
|
33
|
-
|
|
34
|
-
const randomResult = faker.system.filePath();
|
|
35
|
-
|
|
36
|
-
prompt.mockResolvedValueOnce({ target: randomResult });
|
|
37
|
-
|
|
38
|
-
const result = await askForClarification('this is the search string', ['does not matter']);
|
|
39
|
-
|
|
40
|
-
expect(result).toEqual(randomResult);
|
|
41
|
-
});
|
|
42
|
-
});
|
package/src/lib/prompt.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
|
|
3
|
-
export const askForClarification = async (searchString: string, matches: string[]) => {
|
|
4
|
-
const { default: inquirer } = await import('inquirer');
|
|
5
|
-
const selection = await inquirer.prompt<{ target: string }>([
|
|
6
|
-
{
|
|
7
|
-
type: 'select',
|
|
8
|
-
name: 'target',
|
|
9
|
-
message: `Which file do you want to link to for ${chalk.blue(searchString)}?`,
|
|
10
|
-
choices: matches
|
|
11
|
-
}
|
|
12
|
-
]);
|
|
13
|
-
return selection.target;
|
|
14
|
-
};
|
package/src/lib/template.test.ts
DELETED
|
@@ -1,74 +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
|
-
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/lib/template.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path';
|
|
2
|
-
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import fs from 'fs/promises';
|
|
4
|
-
import { getDir } from './config.js';
|
|
5
|
-
|
|
6
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
-
const __dirname = path.dirname(__filename);
|
|
8
|
-
|
|
9
|
-
export const template = async (templateFile?: string): Promise<string> => {
|
|
10
|
-
if (templateFile) {
|
|
11
|
-
return await fs.readFile(path.resolve(templateFile), 'utf8');
|
|
12
|
-
}
|
|
13
|
-
if (process.env.ADR_TEMPLATE) {
|
|
14
|
-
return await fs.readFile(path.resolve(process.env.ADR_TEMPLATE), 'utf8');
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
return await fs.readFile(path.join(await getDir(), 'templates/template.md'), 'utf8');
|
|
19
|
-
} catch (_e) {
|
|
20
|
-
return await fs.readFile(path.resolve(path.join(__dirname, '../templates/template.md')), 'utf8');
|
|
21
|
-
}
|
|
22
|
-
};
|
package/src/templates/init.md
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# 1. Record architecture decisions
|
|
2
|
-
|
|
3
|
-
Date: DATE
|
|
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](https://cognitect.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,19 +0,0 @@
|
|
|
1
|
-
# NUMBER. TITLE
|
|
2
|
-
|
|
3
|
-
Date: DATE
|
|
4
|
-
|
|
5
|
-
## Status
|
|
6
|
-
|
|
7
|
-
STATUS
|
|
8
|
-
|
|
9
|
-
## Context
|
|
10
|
-
|
|
11
|
-
The issue motivating this decision, and any context that influences or constrains the decision.
|
|
12
|
-
|
|
13
|
-
## Decision
|
|
14
|
-
|
|
15
|
-
The change that we're proposing or have agreed to implement.
|
|
16
|
-
|
|
17
|
-
## Consequences
|
|
18
|
-
|
|
19
|
-
What becomes easier or more difficult to do and any risks introduced by the change that will need to be mitigated.
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
declare global {
|
|
2
|
-
// eslint-disable-next-line no-unused-vars
|
|
3
|
-
namespace NodeJS {
|
|
4
|
-
// eslint-disable-next-line no-unused-vars
|
|
5
|
-
interface ProcessEnv {
|
|
6
|
-
ADR_TEMPLATE: string;
|
|
7
|
-
ADR_DATE: string;
|
|
8
|
-
VISUAL: string;
|
|
9
|
-
EDITOR: string;
|
|
10
|
-
}
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export {};
|
package/src/version.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const LIB_VERSION = '2.0.2';
|
package/tests/.adr-dir
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
f8877fa6-908f-4cf5-91cb-9a910b9376f7
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
-
|
|
3
|
-
exports[`Generating Graphs > should generate a graph 1`] = `
|
|
4
|
-
"digraph {
|
|
5
|
-
node [shape=plaintext];
|
|
6
|
-
subgraph {
|
|
7
|
-
_1 [label="1. Record architecture decisions"; URL="0001-record-architecture-decisions.html"];
|
|
8
|
-
_2 [label="2. An idea that seems good at the time"; URL="0002-an-idea-that-seems-good-at-the-time.html"];
|
|
9
|
-
_1 -> _2 [style="dotted", weight=1];
|
|
10
|
-
_3 [label="3. A better idea"; URL="0003-a-better-idea.html"];
|
|
11
|
-
_2 -> _3 [style="dotted", weight=1];
|
|
12
|
-
_4 [label="4. This will work"; URL="0004-this-will-work.html"];
|
|
13
|
-
_3 -> _4 [style="dotted", weight=1];
|
|
14
|
-
_5 [label="5. The end"; URL="0005-the-end.html"];
|
|
15
|
-
_4 -> _5 [style="dotted", weight=1];
|
|
16
|
-
}
|
|
17
|
-
_3 -> _2 [label="Supersedes", weight=0]
|
|
18
|
-
_5 -> _3 [label="Supersedes", weight=0]
|
|
19
|
-
}"
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
exports[`Generating Graphs > should generate a graph with specified route and extension 1`] = `
|
|
23
|
-
"digraph {
|
|
24
|
-
node [shape=plaintext];
|
|
25
|
-
subgraph {
|
|
26
|
-
_1 [label="1. Record architecture decisions"; URL="http://example.com/0001-record-architecture-decisions.xxx"];
|
|
27
|
-
_2 [label="2. An idea that seems good at the time"; URL="http://example.com/0002-an-idea-that-seems-good-at-the-time.xxx"];
|
|
28
|
-
_1 -> _2 [style="dotted", weight=1];
|
|
29
|
-
_3 [label="3. A better idea"; URL="http://example.com/0003-a-better-idea.xxx"];
|
|
30
|
-
_2 -> _3 [style="dotted", weight=1];
|
|
31
|
-
_4 [label="4. This will work"; URL="http://example.com/0004-this-will-work.xxx"];
|
|
32
|
-
_3 -> _4 [style="dotted", weight=1];
|
|
33
|
-
_5 [label="5. The end"; URL="http://example.com/0005-the-end.xxx"];
|
|
34
|
-
_4 -> _5 [style="dotted", weight=1];
|
|
35
|
-
}
|
|
36
|
-
_3 -> _2 [label="Supersedes", weight=0]
|
|
37
|
-
_5 -> _3 [label="Supersedes", weight=0]
|
|
38
|
-
}"
|
|
39
|
-
`;
|