diff --git a/README.md b/README.md index 616acd5..591129f 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Checks one or more prompt files against a simple JSON rules file. promptdiff check prompts/*.md --rules promptdiff.rules.json --fail-on high ``` -Both commands parse options strictly. Unknown options and options missing their required values are errors, and `compare` accepts exactly two file arguments. For `check`, every file or glob supplied on the command line must match; PromptDiff does not emit a partial report when one input is unmatched. Globs support `*` and `?` within a path segment and recursive `**` matching, such as `examples/**/*.md`. Results are sorted and deduplicated before checking. +Both commands parse options strictly. Unknown options and options missing their required values are errors, and `compare` accepts exactly two file arguments. For `check`, every file or glob supplied on the command line must match; PromptDiff does not emit a partial report when one input is unmatched. Globs support `*` and `?` within a path segment and recursive `**` matching, such as `examples/**/*.md`. Glob traversal skips `.git`, `dist`, and `node_modules` directories so broad patterns check project-owned inputs instead of VCS, generated, or dependency content. Explicit file inputs remain available for any path. Results are sorted and deduplicated before checking. The rules document must be a JSON object. `requiredPhrases`, `forbiddenPhrases`, and `requireSections`, when present, must be arrays containing only non-empty strings. `maxSeverity` must be one of `info`, `low`, `medium`, `high`, or `critical`. Invalid rules exit with code `1`, print a field-specific diagnostic to stderr, and do not emit a report. diff --git a/src/cli.ts b/src/cli.ts index 1ea7621..e03271d 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -20,6 +20,8 @@ const packageVersion = JSON.parse( readFileSync(new URL('../package.json', import.meta.url), 'utf8'), ) as { version: string }; +const ignoredGlobDirectories = new Set(['.git', 'dist', 'node_modules']); + function parseArgs(argv: string[]): ParsedArgs { const [command, ...rest] = argv; const flags = new Map(); @@ -102,7 +104,7 @@ async function listFiles(root: string): Promise { const files: string[] = []; for (const entry of entries.sort((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0)) { const path = join(root, entry.name); - if (entry.isDirectory()) files.push(...await listFiles(path)); + if (entry.isDirectory() && !ignoredGlobDirectories.has(entry.name)) files.push(...await listFiles(path)); else if (entry.isFile()) files.push(path); } return files; diff --git a/tests/cli.test.mjs b/tests/cli.test.mjs index 972a87c..48b3ea8 100644 --- a/tests/cli.test.mjs +++ b/tests/cli.test.mjs @@ -1,7 +1,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; -import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -93,6 +93,37 @@ test('cli check expands recursive globs into sorted unique files', () => { assert.ok(report.files.includes('examples/prompts/safe.md')); }); +test('cli check recursive globs skip dependency, VCS, and generated directories', () => { + const directory = mkdtempSync(join(tmpdir(), 'promptdiff-cli-globs-')); + const cliPath = join(process.cwd(), 'dist', 'cli.js'); + for (const path of ['prompts/nested', 'node_modules/example', '.git/objects', 'dist/generated']) { + mkdirSync(join(directory, path), { recursive: true }); + } + writeFileSync(join(directory, 'prompts', 'own.md'), '# Own\n'); + writeFileSync(join(directory, 'prompts', 'nested', 'second.md'), '# Second\n'); + writeFileSync(join(directory, 'node_modules', 'example', 'README.md'), '# Dependency\n'); + writeFileSync(join(directory, '.git', 'objects', 'fixture.md'), '# VCS\n'); + writeFileSync(join(directory, 'dist', 'generated', 'output.md'), '# Generated\n'); + + try { + const run = spawnSync(process.execPath, [cliPath, 'check', '**/*.md', '--format', 'json'], { + cwd: directory, + encoding: 'utf8', + }); + assert.equal(run.status, 0); + assert.equal(run.stderr, ''); + assert.deepEqual(JSON.parse(run.stdout).files, ['prompts/nested/second.md', 'prompts/own.md']); + + const explicit = spawnSync(process.execPath, [ + cliPath, 'check', 'dist/generated/output.md', '--format', 'json', + ], { cwd: directory, encoding: 'utf8' }); + assert.equal(explicit.status, 0); + assert.deepEqual(JSON.parse(explicit.stdout).files, ['dist/generated/output.md']); + } finally { + rmSync(directory, { recursive: true }); + } +}); + test('cli rejects malformed rules without emitting a report', () => { const directory = mkdtempSync(join(tmpdir(), 'promptdiff-cli-rules-')); const rulesPath = join(directory, 'rules.json');