Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
4 changes: 3 additions & 1 deletion src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string | boolean>();
Expand Down Expand Up @@ -102,7 +104,7 @@ async function listFiles(root: string): Promise<string[]> {
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;
Expand Down
33 changes: 32 additions & 1 deletion tests/cli.test.mjs
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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');
Expand Down