@md2do/cli 0.2.3 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @md2do/cli
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [#26](https://github.com/TeamNickHart/md2do/pull/26) [`bffa9f7`](https://github.com/TeamNickHart/md2do/commit/bffa9f79d6b27c5ea7f677b7e9c2158051ca5a43) Thanks [@nickhart](https://github.com/nickhart)! - fix: pin chalk to exact version 4.1.2 to prevent ESM compatibility issues
8
+
9
+ Pinning chalk to exactly 4.1.2 (without caret) ensures that package managers don't accidentally install chalk v5 (which is ESM-only) when users install the CLI globally. This fixes "ERR_REQUIRE_ESM" errors that occurred when chalk v5 was resolved instead of v4.
10
+
11
+ Fixes: ERR_REQUIRE_ESM when installing @md2do/cli globally
12
+
13
+ ## 0.3.0
14
+
15
+ ### Minor Changes
16
+
17
+ - Add comprehensive warning system for markdown validation and VSCode extension
18
+ - Add warning configuration system with customizable rules
19
+ - Add VSCode extension with task explorer, diagnostics, hover tooltips, and smart completion
20
+ - Support for validating malformed checkboxes, missing metadata, duplicate IDs, and more
21
+ - Add .vsix distribution for beta testing
22
+
23
+ ### Patch Changes
24
+
25
+ - Updated dependencies []:
26
+ - @md2do/core@0.3.0
27
+ - @md2do/config@0.3.0
28
+ - @md2do/todoist@0.3.0
29
+
3
30
  ## 0.2.3
4
31
 
5
32
  ### Patch Changes
package/dist/cli.js CHANGED
@@ -31,6 +31,7 @@ var import_path = require("path");
31
31
  // src/commands/list.ts
32
32
  var import_commander = require("commander");
33
33
  var import_core2 = require("@md2do/core");
34
+ var import_config = require("@md2do/config");
34
35
 
35
36
  // src/scanner.ts
36
37
  var import_promises = require("fs/promises");
@@ -68,11 +69,16 @@ async function scanMarkdownFiles(options = {}) {
68
69
  allTasks.push(...result.tasks);
69
70
  allWarnings.push(...result.warnings);
70
71
  } catch (error) {
72
+ const message = `Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`;
71
73
  allWarnings.push({
74
+ severity: "error",
75
+ source: "md2do",
76
+ ruleId: "file-read-error",
72
77
  file,
73
78
  line: 0,
74
79
  text: "",
75
- reason: `Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`
80
+ message,
81
+ reason: message
76
82
  });
77
83
  }
78
84
  }
@@ -388,21 +394,30 @@ function createListCommand() {
388
394
  break;
389
395
  }
390
396
  console.log(output);
391
- if (options.warnings !== false && scanResult.warnings.length > 0) {
392
- console.error(
393
- `
394
- \u26A0\uFE0F ${scanResult.warnings.length} warning${scanResult.warnings.length > 1 ? "s" : ""} encountered during scanning`
397
+ if (options.warnings !== false) {
398
+ const config = await (0, import_config.loadConfig)({
399
+ cwd: options.path || process.cwd()
400
+ });
401
+ const warningConfig = config.warnings ?? import_config.DEFAULT_CONFIG.warnings;
402
+ const filteredWarnings = (0, import_core2.filterWarnings)(
403
+ scanResult.warnings,
404
+ warningConfig ?? {}
395
405
  );
396
- const warningsToShow = options.allWarnings ? scanResult.warnings : scanResult.warnings.slice(0, 5);
397
- for (const warning of warningsToShow) {
398
- console.error(
399
- ` ${warning.file}:${warning.line} - ${warning.reason}`
400
- );
401
- }
402
- if (!options.allWarnings && scanResult.warnings.length > 5) {
406
+ if (filteredWarnings.length > 0) {
403
407
  console.error(
404
- ` ... and ${scanResult.warnings.length - 5} more warning${scanResult.warnings.length - 5 > 1 ? "s" : ""} (use --all-warnings to see all)`
408
+ `
409
+ \u26A0\uFE0F ${filteredWarnings.length} warning${filteredWarnings.length > 1 ? "s" : ""} encountered during scanning`
405
410
  );
411
+ const warningsToShow = options.allWarnings ? filteredWarnings : filteredWarnings.slice(0, 5);
412
+ for (const warning of warningsToShow) {
413
+ const message = warning.message || warning.reason || "Unknown warning";
414
+ console.error(` ${warning.file}:${warning.line} - ${message}`);
415
+ }
416
+ if (!options.allWarnings && filteredWarnings.length > 5) {
417
+ console.error(
418
+ ` ... and ${filteredWarnings.length - 5} more warning${filteredWarnings.length - 5 > 1 ? "s" : ""} (use --all-warnings to see all)`
419
+ );
420
+ }
406
421
  }
407
422
  }
408
423
  } catch (error) {
@@ -652,7 +667,7 @@ function capitalize(str) {
652
667
 
653
668
  // src/commands/todoist.ts
654
669
  var import_commander3 = require("commander");
655
- var import_config = require("@md2do/config");
670
+ var import_config2 = require("@md2do/config");
656
671
  var import_todoist = require("@md2do/todoist");
657
672
  var import_core4 = require("@md2do/core");
658
673
  var import_promises2 = __toESM(require("fs/promises"));
@@ -681,7 +696,7 @@ function createTodoistListCommand() {
681
696
  return command;
682
697
  }
683
698
  async function todoistListAction(options) {
684
- const config = await (0, import_config.loadConfig)();
699
+ const config = await (0, import_config2.loadConfig)();
685
700
  if (!config.todoist?.apiToken) {
686
701
  console.error("\u274C Error: Todoist API token not configured");
687
702
  console.error("");
@@ -812,7 +827,7 @@ function createTodoistAddCommand() {
812
827
  return command;
813
828
  }
814
829
  async function todoistAddAction(taskContent, options) {
815
- const config = await (0, import_config.loadConfig)();
830
+ const config = await (0, import_config2.loadConfig)();
816
831
  if (!config.todoist?.apiToken) {
817
832
  console.error("\u274C Error: Todoist API token not configured");
818
833
  console.error("");
@@ -905,7 +920,7 @@ async function todoistImportAction(location, options) {
905
920
  console.error(`\u274C Error: Invalid line number: ${lineStr}`);
906
921
  process.exit(1);
907
922
  }
908
- const config = await (0, import_config.loadConfig)();
923
+ const config = await (0, import_config2.loadConfig)();
909
924
  if (!config.todoist?.apiToken) {
910
925
  console.error("\u274C Error: Todoist API token not configured");
911
926
  console.error("");
@@ -1019,7 +1034,7 @@ function createTodoistSyncCommand() {
1019
1034
  return command;
1020
1035
  }
1021
1036
  async function todoistSyncAction(options) {
1022
- const config = await (0, import_config.loadConfig)();
1037
+ const config = await (0, import_config2.loadConfig)();
1023
1038
  if (!config.todoist?.apiToken) {
1024
1039
  console.error("\u274C Error: Todoist API token not configured");
1025
1040
  console.error("");
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ module.exports = __toCommonJS(index_exports);
39
39
  // src/commands/list.ts
40
40
  var import_commander = require("commander");
41
41
  var import_core2 = require("@md2do/core");
42
+ var import_config = require("@md2do/config");
42
43
 
43
44
  // src/scanner.ts
44
45
  var import_promises = require("fs/promises");
@@ -76,11 +77,16 @@ async function scanMarkdownFiles(options = {}) {
76
77
  allTasks.push(...result.tasks);
77
78
  allWarnings.push(...result.warnings);
78
79
  } catch (error) {
80
+ const message = `Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`;
79
81
  allWarnings.push({
82
+ severity: "error",
83
+ source: "md2do",
84
+ ruleId: "file-read-error",
80
85
  file,
81
86
  line: 0,
82
87
  text: "",
83
- reason: `Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`
88
+ message,
89
+ reason: message
84
90
  });
85
91
  }
86
92
  }
@@ -396,21 +402,30 @@ function createListCommand() {
396
402
  break;
397
403
  }
398
404
  console.log(output);
399
- if (options.warnings !== false && scanResult.warnings.length > 0) {
400
- console.error(
401
- `
402
- \u26A0\uFE0F ${scanResult.warnings.length} warning${scanResult.warnings.length > 1 ? "s" : ""} encountered during scanning`
405
+ if (options.warnings !== false) {
406
+ const config = await (0, import_config.loadConfig)({
407
+ cwd: options.path || process.cwd()
408
+ });
409
+ const warningConfig = config.warnings ?? import_config.DEFAULT_CONFIG.warnings;
410
+ const filteredWarnings = (0, import_core2.filterWarnings)(
411
+ scanResult.warnings,
412
+ warningConfig ?? {}
403
413
  );
404
- const warningsToShow = options.allWarnings ? scanResult.warnings : scanResult.warnings.slice(0, 5);
405
- for (const warning of warningsToShow) {
406
- console.error(
407
- ` ${warning.file}:${warning.line} - ${warning.reason}`
408
- );
409
- }
410
- if (!options.allWarnings && scanResult.warnings.length > 5) {
414
+ if (filteredWarnings.length > 0) {
411
415
  console.error(
412
- ` ... and ${scanResult.warnings.length - 5} more warning${scanResult.warnings.length - 5 > 1 ? "s" : ""} (use --all-warnings to see all)`
416
+ `
417
+ \u26A0\uFE0F ${filteredWarnings.length} warning${filteredWarnings.length > 1 ? "s" : ""} encountered during scanning`
413
418
  );
419
+ const warningsToShow = options.allWarnings ? filteredWarnings : filteredWarnings.slice(0, 5);
420
+ for (const warning of warningsToShow) {
421
+ const message = warning.message || warning.reason || "Unknown warning";
422
+ console.error(` ${warning.file}:${warning.line} - ${message}`);
423
+ }
424
+ if (!options.allWarnings && filteredWarnings.length > 5) {
425
+ console.error(
426
+ ` ... and ${filteredWarnings.length - 5} more warning${filteredWarnings.length - 5 > 1 ? "s" : ""} (use --all-warnings to see all)`
427
+ );
428
+ }
414
429
  }
415
430
  }
416
431
  } catch (error) {
@@ -660,7 +675,7 @@ function capitalize(str) {
660
675
 
661
676
  // src/commands/todoist.ts
662
677
  var import_commander3 = require("commander");
663
- var import_config = require("@md2do/config");
678
+ var import_config2 = require("@md2do/config");
664
679
  var import_todoist = require("@md2do/todoist");
665
680
  var import_core4 = require("@md2do/core");
666
681
  var import_promises2 = __toESM(require("fs/promises"));
@@ -689,7 +704,7 @@ function createTodoistListCommand() {
689
704
  return command;
690
705
  }
691
706
  async function todoistListAction(options) {
692
- const config = await (0, import_config.loadConfig)();
707
+ const config = await (0, import_config2.loadConfig)();
693
708
  if (!config.todoist?.apiToken) {
694
709
  console.error("\u274C Error: Todoist API token not configured");
695
710
  console.error("");
@@ -820,7 +835,7 @@ function createTodoistAddCommand() {
820
835
  return command;
821
836
  }
822
837
  async function todoistAddAction(taskContent, options) {
823
- const config = await (0, import_config.loadConfig)();
838
+ const config = await (0, import_config2.loadConfig)();
824
839
  if (!config.todoist?.apiToken) {
825
840
  console.error("\u274C Error: Todoist API token not configured");
826
841
  console.error("");
@@ -913,7 +928,7 @@ async function todoistImportAction(location, options) {
913
928
  console.error(`\u274C Error: Invalid line number: ${lineStr}`);
914
929
  process.exit(1);
915
930
  }
916
- const config = await (0, import_config.loadConfig)();
931
+ const config = await (0, import_config2.loadConfig)();
917
932
  if (!config.todoist?.apiToken) {
918
933
  console.error("\u274C Error: Todoist API token not configured");
919
934
  console.error("");
@@ -1027,7 +1042,7 @@ function createTodoistSyncCommand() {
1027
1042
  return command;
1028
1043
  }
1029
1044
  async function todoistSyncAction(options) {
1030
- const config = await (0, import_config.loadConfig)();
1045
+ const config = await (0, import_config2.loadConfig)();
1031
1046
  if (!config.todoist?.apiToken) {
1032
1047
  console.error("\u274C Error: Todoist API token not configured");
1033
1048
  console.error("");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@md2do/cli",
3
- "version": "0.2.3",
3
+ "version": "0.3.1",
4
4
  "description": "CLI interface for md2do task manager",
5
5
  "keywords": [
6
6
  "markdown",
@@ -40,16 +40,16 @@
40
40
  },
41
41
  "dependencies": {
42
42
  "@doist/todoist-api-typescript": "^3.0.3",
43
- "chalk": "^5.6.2",
43
+ "chalk": "4.1.2",
44
44
  "cli-table3": "^0.6.5",
45
45
  "commander": "^11.1.0",
46
46
  "cosmiconfig": "^9.0.0",
47
47
  "date-fns": "^3.0.6",
48
48
  "fast-glob": "^3.3.3",
49
49
  "zod": "^3.22.4",
50
- "@md2do/config": "0.2.3",
51
- "@md2do/core": "0.2.3",
52
- "@md2do/todoist": "0.2.3"
50
+ "@md2do/config": "0.3.0",
51
+ "@md2do/core": "0.3.0",
52
+ "@md2do/todoist": "0.3.0"
53
53
  },
54
54
  "devDependencies": {
55
55
  "tsup": "^8.0.1"
@@ -1,5 +1,6 @@
1
1
  import { Command } from 'commander';
2
- import { filters, sorting } from '@md2do/core';
2
+ import { filters, sorting, filterWarnings } from '@md2do/core';
3
+ import { loadConfig, DEFAULT_CONFIG } from '@md2do/config';
3
4
  import { scanMarkdownFiles } from '../scanner.js';
4
5
  import { formatAsPretty, formatAsTable } from '../formatters/pretty.js';
5
6
  import { formatAsJson } from '../formatters/json.js';
@@ -211,27 +212,42 @@ export function createListCommand(): Command {
211
212
 
212
213
  console.log(output);
213
214
 
214
- // Show warnings if any (unless --no-warnings)
215
- if (options.warnings !== false && scanResult.warnings.length > 0) {
216
- console.error(
217
- `\n⚠️ ${scanResult.warnings.length} warning${scanResult.warnings.length > 1 ? 's' : ''} encountered during scanning`,
215
+ // Load config and apply warning filters (unless --no-warnings overrides)
216
+ if (options.warnings !== false) {
217
+ const config = await loadConfig({
218
+ cwd: options.path || process.cwd(),
219
+ });
220
+ const warningConfig = config.warnings ?? DEFAULT_CONFIG.warnings;
221
+
222
+ // Apply config-based filtering
223
+ const filteredWarnings = filterWarnings(
224
+ scanResult.warnings,
225
+ warningConfig ?? {},
218
226
  );
219
227
 
220
- // Show all warnings if --all-warnings, otherwise show first 5
221
- const warningsToShow = options.allWarnings
222
- ? scanResult.warnings
223
- : scanResult.warnings.slice(0, 5);
224
-
225
- for (const warning of warningsToShow) {
228
+ // Show warnings if any remain after filtering
229
+ if (filteredWarnings.length > 0) {
226
230
  console.error(
227
- ` ${warning.file}:${warning.line} - ${warning.reason}`,
231
+ `\n⚠️ ${filteredWarnings.length} warning${filteredWarnings.length > 1 ? 's' : ''} encountered during scanning`,
228
232
  );
229
- }
230
233
 
231
- if (!options.allWarnings && scanResult.warnings.length > 5) {
232
- console.error(
233
- ` ... and ${scanResult.warnings.length - 5} more warning${scanResult.warnings.length - 5 > 1 ? 's' : ''} (use --all-warnings to see all)`,
234
- );
234
+ // Show all warnings if --all-warnings, otherwise show first 5
235
+ const warningsToShow = options.allWarnings
236
+ ? filteredWarnings
237
+ : filteredWarnings.slice(0, 5);
238
+
239
+ for (const warning of warningsToShow) {
240
+ // Use message field (new) or fallback to reason (legacy)
241
+ const message =
242
+ warning.message || warning.reason || 'Unknown warning';
243
+ console.error(` ${warning.file}:${warning.line} - ${message}`);
244
+ }
245
+
246
+ if (!options.allWarnings && filteredWarnings.length > 5) {
247
+ console.error(
248
+ ` ... and ${filteredWarnings.length - 5} more warning${filteredWarnings.length - 5 > 1 ? 's' : ''} (use --all-warnings to see all)`,
249
+ );
250
+ }
235
251
  }
236
252
  }
237
253
  } catch (error) {
package/src/scanner.ts CHANGED
@@ -82,11 +82,16 @@ export async function scanMarkdownFiles(
82
82
  allWarnings.push(...result.warnings);
83
83
  } catch (error) {
84
84
  // Add warning for files that couldn't be read
85
+ const message = `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`;
85
86
  allWarnings.push({
87
+ severity: 'error',
88
+ source: 'md2do',
89
+ ruleId: 'file-read-error',
86
90
  file,
87
91
  line: 0,
88
92
  text: '',
89
- reason: `Failed to read file: ${error instanceof Error ? error.message : 'Unknown error'}`,
93
+ message,
94
+ reason: message,
90
95
  });
91
96
  }
92
97
  }
@@ -0,0 +1,104 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`E2E: Warning Configuration Profiles > custom: all disabled > all-disabled 1`] = `
4
+ "
5
+ Found 3 tasks
6
+ ✓ 1 completed | ○ 2 incomplete
7
+
8
+ ○ !! Valid task (2026-02-01) @alice #bug
9
+ tasks.md:6
10
+
11
+ ✓ Completed task without completion date
12
+ tasks.md:7
13
+
14
+ ○ Task without a due date
15
+ tasks.md:8
16
+
17
+ "
18
+ `;
19
+
20
+ exports[`E2E: Warning Configuration Profiles > custom: only spacing errors > custom-space-only 1`] = `
21
+ "
22
+ Found 3 tasks
23
+ ✓ 1 completed | ○ 2 incomplete
24
+
25
+ ○ !! Valid task (2026-02-01) @alice #bug
26
+ tasks.md:6
27
+
28
+ ✓ Completed task without completion date
29
+ tasks.md:7
30
+
31
+ ○ Task without a due date
32
+ tasks.md:8
33
+
34
+
35
+ ⚠️ 2 warnings encountered during scanning
36
+ tasks.md:4 - Missing space after checkbox. Use "- [x] Task" format.
37
+ tasks.md:5 - Missing space before checkbox. Use "- [x] Task" format.
38
+ "
39
+ `;
40
+
41
+ exports[`E2E: Warning Configuration Profiles > recommended profile (default) > recommended-default 1`] = `
42
+ "
43
+ Found 3 tasks
44
+ ✓ 1 completed | ○ 2 incomplete
45
+
46
+ ○ !! Valid task (2026-02-01) @alice #bug
47
+ tasks.md:6
48
+
49
+ ✓ Completed task without completion date
50
+ tasks.md:7
51
+
52
+ ○ Task without a due date
53
+ tasks.md:8
54
+
55
+
56
+ ⚠️ 3 warnings encountered during scanning
57
+ tasks.md:3 - Unsupported bullet marker (* or +). Use dash (-) for task lists.
58
+ tasks.md:4 - Missing space after checkbox. Use "- [x] Task" format.
59
+ tasks.md:5 - Missing space before checkbox. Use "- [x] Task" format.
60
+ "
61
+ `;
62
+
63
+ exports[`E2E: Warning Configuration Profiles > strict profile > strict-all-warnings 1`] = `
64
+ "
65
+ Found 3 tasks
66
+ ✓ 1 completed | ○ 2 incomplete
67
+
68
+ ○ !! Valid task (2026-02-01) @alice #bug
69
+ tasks.md:6
70
+
71
+ ✓ Completed task without completion date
72
+ tasks.md:7
73
+
74
+ ○ Task without a due date
75
+ tasks.md:8
76
+
77
+
78
+ ⚠️ 6 warnings encountered during scanning
79
+ tasks.md:3 - Unsupported bullet marker (* or +). Use dash (-) for task lists.
80
+ tasks.md:4 - Missing space after checkbox. Use "- [x] Task" format.
81
+ tasks.md:5 - Missing space before checkbox. Use "- [x] Task" format.
82
+ tasks.md:6 - Task has no due date. Add [due: YYYY-MM-DD] or place under a heading with a date.
83
+ tasks.md:7 - Completed task missing completion date. Add [completed: YYYY-MM-DD].
84
+ ... and 1 more warning (use --all-warnings to see all)
85
+ "
86
+ `;
87
+
88
+ exports[`E2E: Warning Filtering Edge Cases > respects CLI --no-warnings override > cli-no-warnings-override 1`] = `
89
+ "No tasks found
90
+ "
91
+ `;
92
+
93
+ exports[`E2E: Warning Filtering Edge Cases > shows warnings correctly with truncation > truncated-warnings 1`] = `
94
+ "No tasks found
95
+
96
+ ⚠️ 20 warnings encountered during scanning
97
+ many-warnings.md:1 - Unsupported bullet marker (* or +). Use dash (-) for task lists.
98
+ many-warnings.md:2 - Unsupported bullet marker (* or +). Use dash (-) for task lists.
99
+ many-warnings.md:3 - Unsupported bullet marker (* or +). Use dash (-) for task lists.
100
+ many-warnings.md:4 - Unsupported bullet marker (* or +). Use dash (-) for task lists.
101
+ many-warnings.md:5 - Unsupported bullet marker (* or +). Use dash (-) for task lists.
102
+ ... and 15 more warnings (use --all-warnings to see all)
103
+ "
104
+ `;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * E2E Tests: Warning Configuration Profiles
3
+ *
4
+ * Tests warning filtering with different configuration profiles using snapshots.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { execSync } from 'child_process';
9
+ import { join } from 'path';
10
+ import { mkdtempSync, writeFileSync, rmSync } from 'fs';
11
+ import { tmpdir } from 'os';
12
+
13
+ function runCLIWithConfig(
14
+ config: object,
15
+ testFiles: Record<string, string>,
16
+ ): string {
17
+ const tmpDir = mkdtempSync(join(tmpdir(), 'md2do-test-'));
18
+
19
+ try {
20
+ // Write config
21
+ writeFileSync(join(tmpDir, '.md2do.json'), JSON.stringify(config, null, 2));
22
+
23
+ // Write test files
24
+ for (const [filename, content] of Object.entries(testFiles)) {
25
+ writeFileSync(join(tmpDir, filename), content);
26
+ }
27
+
28
+ // Run CLI - need to capture both stdout and stderr since warnings go to stderr
29
+ const cliPath = join(__dirname, '../../dist/cli.js');
30
+ const output = execSync(
31
+ `node ${cliPath} list --path ${tmpDir} --no-colors 2>&1`,
32
+ {
33
+ encoding: 'utf-8',
34
+ },
35
+ );
36
+
37
+ // Normalize file paths for consistent snapshots across environments
38
+ // Replace absolute paths with relative paths
39
+ return output.replace(new RegExp('file://[^\\s]+/packages/cli/', 'g'), '');
40
+ } finally {
41
+ rmSync(tmpDir, { recursive: true });
42
+ }
43
+ }
44
+
45
+ describe('E2E: Warning Configuration Profiles', () => {
46
+ const testFiles = {
47
+ 'tasks.md': `
48
+ # Tasks
49
+
50
+ * [x] Wrong bullet type task
51
+ - [ ]Missing space after checkbox task
52
+ -[x] Missing space before checkbox task
53
+ - [ ] Valid task @alice !! #bug (2026-02-01)
54
+ - [x] Completed task without completion date
55
+ - [ ] Task without a due date
56
+ `.trim(),
57
+ };
58
+
59
+ it('recommended profile (default)', () => {
60
+ const output = runCLIWithConfig({}, testFiles);
61
+
62
+ // Format warnings shown
63
+ expect(output).toContain('⚠️');
64
+ expect(output).toContain('Unsupported bullet');
65
+ expect(output).toContain('Missing space');
66
+
67
+ // Metadata warnings NOT shown (stylistic choice)
68
+ expect(output).not.toContain('No due date');
69
+ expect(output).not.toContain('Completed without date');
70
+
71
+ expect(output).toMatchSnapshot('recommended-default');
72
+ });
73
+
74
+ it('strict profile', () => {
75
+ const output = runCLIWithConfig(
76
+ {
77
+ warnings: {
78
+ enabled: true,
79
+ rules: {
80
+ 'unsupported-bullet': 'error',
81
+ 'malformed-checkbox': 'error',
82
+ 'missing-space-after': 'error',
83
+ 'missing-space-before': 'error',
84
+ 'relative-date-no-context': 'error',
85
+ 'missing-due-date': 'warn',
86
+ 'missing-completed-date': 'warn',
87
+ 'duplicate-todoist-id': 'error',
88
+ 'file-read-error': 'error',
89
+ },
90
+ },
91
+ },
92
+ testFiles,
93
+ );
94
+
95
+ // ALL warnings shown
96
+ expect(output).toContain('⚠️');
97
+ expect(output).toContain('Unsupported bullet');
98
+ expect(output).toContain('Missing space');
99
+ expect(output).toContain('Task has no due date');
100
+ expect(output).toContain('Completed task missing completion date');
101
+
102
+ expect(output).toMatchSnapshot('strict-all-warnings');
103
+ });
104
+
105
+ it('custom: all disabled', () => {
106
+ const output = runCLIWithConfig(
107
+ {
108
+ warnings: { enabled: false },
109
+ },
110
+ testFiles,
111
+ );
112
+
113
+ expect(output).not.toContain('⚠️');
114
+ expect(output).toMatchSnapshot('all-disabled');
115
+ });
116
+
117
+ it('custom: only spacing errors', () => {
118
+ const output = runCLIWithConfig(
119
+ {
120
+ warnings: {
121
+ enabled: true,
122
+ rules: {
123
+ 'unsupported-bullet': 'off',
124
+ 'malformed-checkbox': 'off',
125
+ 'missing-space-after': 'error',
126
+ 'missing-space-before': 'error',
127
+ 'relative-date-no-context': 'off',
128
+ 'missing-due-date': 'off',
129
+ 'missing-completed-date': 'off',
130
+ 'duplicate-todoist-id': 'error',
131
+ 'file-read-error': 'error',
132
+ },
133
+ },
134
+ },
135
+ testFiles,
136
+ );
137
+
138
+ // Only space warnings
139
+ expect(output).toContain('Missing space');
140
+ expect(output).not.toContain('Unsupported bullet');
141
+ expect(output).not.toContain('Task has no due date');
142
+
143
+ expect(output).toMatchSnapshot('custom-space-only');
144
+ });
145
+ });
146
+
147
+ describe('E2E: Warning Filtering Edge Cases', () => {
148
+ it('shows warnings correctly with truncation', () => {
149
+ // Create many warnings to test truncation
150
+ const testFiles = {
151
+ 'many-warnings.md': Array.from(
152
+ { length: 20 },
153
+ (_, i) => `* [ ] Task ${i}`,
154
+ ).join('\n'),
155
+ };
156
+
157
+ const output = runCLIWithConfig({}, testFiles);
158
+
159
+ // Should show first 5, then "... and N more"
160
+ expect(output).toMatch(/and \d+ more warning/);
161
+ expect(output).toMatchSnapshot('truncated-warnings');
162
+ });
163
+
164
+ it('respects CLI --no-warnings override', () => {
165
+ const tmpDir = mkdtempSync(join(tmpdir(), 'md2do-test-'));
166
+
167
+ try {
168
+ const testFile = '* [x] Wrong bullet';
169
+ writeFileSync(join(tmpDir, 'tasks.md'), testFile);
170
+
171
+ const cliPath = join(__dirname, '../../dist/cli.js');
172
+ const output = execSync(
173
+ `node ${cliPath} list --path ${tmpDir} --no-warnings --no-colors`,
174
+ {
175
+ encoding: 'utf-8',
176
+ },
177
+ );
178
+
179
+ // Should not show warnings even though config has them enabled
180
+ expect(output).not.toContain('⚠️');
181
+ expect(output).toMatchSnapshot('cli-no-warnings-override');
182
+ } finally {
183
+ rmSync(tmpDir, { recursive: true });
184
+ }
185
+ });
186
+ });