@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.
Files changed (111) hide show
  1. package/.github/renovate.json +2 -1
  2. package/.github/workflows/ci-pr.yml +2 -25
  3. package/.github/workflows/ci.yml +12 -46
  4. package/.releaserc.json +18 -8
  5. package/AGENTS.engineer.md +236 -0
  6. package/AGENTS.md +11 -0
  7. package/AGENTS.reviewer.md +115 -0
  8. package/CONTRIBUTING.md +102 -0
  9. package/README.md +16 -4
  10. package/biome.json +18 -139
  11. package/dist/index.js +164 -81
  12. package/dist/index.js.map +1 -1
  13. package/dist/index.test.js +189 -0
  14. package/dist/index.test.js.map +1 -0
  15. package/dist/inject-version.test.js +27 -0
  16. package/dist/inject-version.test.js.map +1 -0
  17. package/dist/lib/adr.js +132 -27
  18. package/dist/lib/adr.js.map +1 -1
  19. package/dist/lib/adr.test.js +308 -0
  20. package/dist/lib/adr.test.js.map +1 -0
  21. package/dist/lib/config.js +3 -2
  22. package/dist/lib/config.js.map +1 -1
  23. package/dist/lib/config.test.js +60 -0
  24. package/dist/lib/config.test.js.map +1 -0
  25. package/dist/lib/links.test.js +5 -4
  26. package/dist/lib/links.test.js.map +1 -1
  27. package/dist/lib/manipulator-errors.test.js +21 -0
  28. package/dist/lib/manipulator-errors.test.js.map +1 -0
  29. package/dist/lib/manipulator.test.js +21 -1
  30. package/dist/lib/manipulator.test.js.map +1 -1
  31. package/dist/lib/numbering.js +1 -1
  32. package/dist/lib/numbering.js.map +1 -1
  33. package/dist/lib/numbering.test.js +7 -0
  34. package/dist/lib/numbering.test.js.map +1 -1
  35. package/dist/lib/opening.test.js +81 -0
  36. package/dist/lib/opening.test.js.map +1 -0
  37. package/dist/lib/prompt.js +1 -1
  38. package/dist/lib/prompt.js.map +1 -1
  39. package/dist/lib/template.test.js +62 -0
  40. package/dist/lib/template.test.js.map +1 -0
  41. package/dist/types/index.d.ts +25 -0
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/dist/types/index.test.d.ts +2 -0
  44. package/dist/types/index.test.d.ts.map +1 -0
  45. package/dist/types/inject-version.test.d.ts +2 -0
  46. package/dist/types/inject-version.test.d.ts.map +1 -0
  47. package/dist/types/lib/adr.d.ts +15 -0
  48. package/dist/types/lib/adr.d.ts.map +1 -1
  49. package/dist/types/lib/adr.test.d.ts +2 -0
  50. package/dist/types/lib/adr.test.d.ts.map +1 -0
  51. package/dist/types/lib/config.d.ts.map +1 -1
  52. package/dist/types/lib/config.test.d.ts +2 -0
  53. package/dist/types/lib/config.test.d.ts.map +1 -0
  54. package/dist/types/lib/manipulator-errors.test.d.ts +2 -0
  55. package/dist/types/lib/manipulator-errors.test.d.ts.map +1 -0
  56. package/dist/types/lib/opening.test.d.ts +2 -0
  57. package/dist/types/lib/opening.test.d.ts.map +1 -0
  58. package/dist/types/lib/prompt.d.ts.map +1 -1
  59. package/dist/types/lib/template.test.d.ts +2 -0
  60. package/dist/types/lib/template.test.d.ts.map +1 -0
  61. package/dist/types/version.d.ts +1 -1
  62. package/dist/types/version.d.ts.map +1 -1
  63. package/dist/version.js +1 -1
  64. package/dist/version.js.map +1 -1
  65. package/doc/adr/.adr-sequence.lock +1 -1
  66. package/doc/adr/0001-record-architecture-decisions.md +21 -0
  67. package/doc/adr/0002-using-heavy-e2e-tests.md +20 -0
  68. package/doc/adr/0003-esm.md +34 -0
  69. package/doc/adr/0004-gate-editor-opening-behind---open-and---open-with.md +55 -0
  70. package/doc/adr/decisions.md +4 -1
  71. package/lefthook.yml +14 -0
  72. package/package.json +24 -26
  73. package/scripts/inject-version.mjs +34 -0
  74. package/src/index.test.ts +212 -0
  75. package/src/index.ts +229 -108
  76. package/src/inject-version.test.ts +31 -0
  77. package/src/lib/adr.test.ts +376 -0
  78. package/src/lib/adr.ts +173 -27
  79. package/src/lib/config.test.ts +69 -0
  80. package/src/lib/config.ts +3 -2
  81. package/src/lib/links.test.ts +8 -4
  82. package/src/lib/manipulator-errors.test.ts +22 -0
  83. package/src/lib/manipulator.test.ts +25 -1
  84. package/src/lib/numbering.test.ts +8 -0
  85. package/src/lib/numbering.ts +1 -1
  86. package/src/lib/opening.test.ts +96 -0
  87. package/src/lib/prompt.ts +1 -1
  88. package/src/lib/template.test.ts +74 -0
  89. package/src/version.ts +1 -2
  90. package/tests/edit-on-create.e2e.test.ts +47 -16
  91. package/tests/fake-editor.cmd +2 -0
  92. package/tests/fake-visual.cmd +2 -0
  93. package/tests/funny-characters.e2e.test.ts +12 -11
  94. package/tests/generate-graph.e2e.test.ts +13 -17
  95. package/tests/helpers/adr-cli.ts +24 -0
  96. package/tests/init-adr-repository.e2e.test.ts +7 -6
  97. package/tests/linking-records.e2e.test.ts +14 -13
  98. package/tests/list-adrs.e2e.test.ts +26 -19
  99. package/tests/new-adr.e2e.test.ts +10 -9
  100. package/tests/open-with.e2e.test.ts +53 -0
  101. package/tests/superseding-records.e2e.test.ts +11 -10
  102. package/tests/toc-prefixing.e2e.test.ts +10 -9
  103. package/tests/use-template-override.e2e.test.ts +9 -8
  104. package/tests/work-form-other-directories.e2e.test.ts +10 -8
  105. package/vitest.config.e2e.ts +8 -1
  106. package/vitest.config.ts +20 -4
  107. package/.github/workflows/sync-deps-to-main.yml +0 -25
  108. package/.github/workflows/sync-to-deps.yml +0 -26
  109. package/.husky/commit-msg +0 -4
  110. package/CHANGELOG.md +0 -121
  111. 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, '..');
@@ -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
- vi.mocked(fs.readdir).mockResolvedValueOnce([] as any);
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
- vi.mocked(fs.readdir).mockResolvedValueOnce(['1-one', '1-two', '1-three'] as any);
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
- vi.mocked(fs.readdir).mockResolvedValueOnce(['on1e', '1-two', 'three'] as any);
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
- vi.mocked(fs.readdir).mockResolvedValueOnce([] as any);
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
  });
@@ -1,5 +1,5 @@
1
- import * as path from 'path';
2
1
  import fs from 'fs/promises';
2
+ import * as path from 'path';
3
3
  import { getDir } from './config.js';
4
4
 
5
5
  export const newNumber = async () => {
@@ -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 = '1.0.12';
2
-
1
+ export const LIB_VERSION = '2.0.1';
@@ -1,15 +1,32 @@
1
- import * as childProcess from 'child_process';
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 command = `npx tsx ${adr}`;
11
- const visualHelper = path.resolve(path.dirname(__filename), './fake-visual');
12
- const editorHelper = path.resolve(path.dirname(__filename), './fake-editor');
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.rmdirSync(workDir, {
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
- childProcess.execSync(`VISUAL="${visualHelper}" ${command} new Example ADR`, { timeout: 10000, cwd: workDir });
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
- expect(fileContents.trim()).toEqual(`VISUAL ${adrDirectory}/0001-example-adr.md`);
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.skip('should open a new ADR in the EDITOR', () => {
41
- childProcess.execSync(`EDITOR="${editorHelper}" ${command} new Example ADR`, { timeout: 10000, cwd: workDir });
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
- expect(fileContents.trim()).toEqual(`EDITOR ${adrDirectory}/0001-example-adr.md`);
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
- childProcess.execSync(`EDITOR="${editorHelper}" VISUAL="${visualHelper}" ${command} new Example ADR`, {
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
- expect(fileContents.trim()).toEqual(`VISUAL ${adrDirectory}/0001-example-adr.md`);
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
  });
@@ -0,0 +1,2 @@
1
+ @echo off
2
+ echo EDITOR %~1 > editor.out
@@ -0,0 +1,2 @@
1
+ @echo off
2
+ echo VISUAL %~1 > visual.out
@@ -1,24 +1,25 @@
1
- import * as childProcess from 'child_process';
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.skip('Funny Characters', () => {
12
+ describe('Funny Characters', () => {
13
13
  const adr: string = path.resolve(path.dirname(__filename), '../src/index.ts');
14
- const command = `npx tsx ${adr}`;
14
+ const cli = createAdrCli(adr);
15
15
 
16
16
  let adrDirectory: string;
17
17
  let workDir: string;
18
18
 
19
19
  afterEach(() => {
20
- fs.rmdirSync(workDir, {
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
- childProcess.execSync(`${command} init ${adrDirectory}`, { timeout: 10000, cwd: workDir });
31
+ cli.run(['init', adrDirectory], { cwd: workDir });
31
32
  });
32
33
 
33
34
  it('should handle titles with periods in them', async () => {
34
- childProcess.execSync(`${command} new Something About Node.JS`, { timeout: 10000, cwd: workDir });
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.skip('should handle titles with slashes in them', async () => {
40
- childProcess.execSync(`${command} new Slash/Slash/Slash/`, { timeout: 10000, cwd: workDir });
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.skip('should handle titles with other weirdness in them', async () => {
46
- childProcess.execSync(`${command} new -- "-Bar-"`, { timeout: 10000, cwd: workDir });
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 * as childProcess from 'child_process';
2
- import { realpathSync, rmdirSync } from 'node:fs';
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 command = `npx tsx ${adr}`;
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
- childProcess.execSync(`${command} init`, { timeout: 10000, cwd: workDir });
19
- childProcess.execSync(`${command} new An idea that seems good at the time`, { timeout: 10000, cwd: workDir });
20
- childProcess.execSync(`${command} new -s 2 A better idea`, { timeout: 10000, cwd: workDir });
21
- childProcess.execSync(`${command} new This will work`, { timeout: 10000, cwd: workDir });
22
- childProcess.execSync(`${command} new -s 3 The end`, { timeout: 10000, cwd: workDir });
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
- rmdirSync(workDir, {
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 child = childProcess.execSync(`${command} generate graph`, { timeout: 10000, cwd: workDir });
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 child = childProcess.execSync(`${command} generate graph -p http://example.com/ -e .xxx`, {
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
  });