@usrrname/cursorrules
Version:
A wicked npx-able lib of cursor rules with Otaku AI agents
276 lines (226 loc) • 11.8 kB
JavaScript
// @ts-nocheck
import assert from 'node:assert/strict';
import { execFile } from 'node:child_process';
import { existsSync } from 'node:fs';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import { after, afterEach, beforeEach, describe, test } from 'node:test';
import { promisify } from 'node:util';
import { projectRoot } from '../index.mjs';
const execFileAsync = promisify(execFile);
describe('CLI', () => {
describe('default', () => {
test('should accept default command with no flags', async () => {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs']);
assert.ok(stdout.includes('Loading @usrrname/cursorrules ...'));
});
after(async () => {
try {
await fs.rm(path.join(projectRoot, 'output'), { recursive: true, force: true });
} catch (err) {
console.error(`Error: ${err}`);
}
})
})
describe('help', () => {
test('should accept -h and print usage instructions', async () => {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '-h']);
assert.ok(stdout.includes('Usage:'));
});
test('should accept --help value and print usage instructions', async () => {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '--help']);
assert.ok(stdout.includes('Usage:'));
});
})
describe('version', () => {
test('should accept -v and print package version', async () => {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '-v']);
assert.ok(stdout.includes('@usrrname/cursorrules'));
});
test('should accept --version value and print package version', async () => {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '--version']);
assert.ok(stdout.includes('@usrrname/cursorrules'));
});
})
describe('output', () => {
test('should accept --output flag for output directory and download the cursorrules', async () => {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '--output', 'output']);
if (existsSync(path.join(projectRoot, 'output'))) {
assert.ok(stdout.includes('Success'));
}
})
test('should accept -o flag for output directory and download the cursorrules', async () => {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '-o', 'output']);
if (existsSync(path.join(projectRoot, 'output'))) {
assert.ok(stdout.includes('Success'));
}
})
test('should reject empty output directory name if -o flag provided', async () => {
try {
await execFileAsync('node', ['./cli/index.mjs', '-o', '']);
assert.fail('Expected command to fail for empty output directory');
} catch (error) {
assert.ok(error.stderr.includes('Output directory cannot be empty') || error.message.includes('Output directory is required'));
assert.strictEqual(error.code, 1);
}
});
test('should handle output flag with valid directory', async () => {
const outputDir = path.join(projectRoot, 'valid-output');
const { stdout } = await execFileAsync('node', ['./cli/index.mjs', '-o', outputDir]);
// Should successfully create output directory
assert.ok(stdout.includes('Success'));
assert.ok(existsSync(outputDir));
await fs.rm(outputDir, { recursive: true, force: true });
});
test('should handle output flag with relative path', async () => {
const outputDir = './test-relative-output';
const { stdout } = await execFileAsync('node', ['./cli/index.mjs', '-o', outputDir]);
// Should successfully create output directory
assert.ok(stdout.includes('Success'));
assert.ok(existsSync(outputDir));
// Cleanup
await fs.rm(outputDir, { recursive: true, force: true });
});
after(async () => {
try {
if (existsSync(path.join(projectRoot, 'output'))) {
await fs.rm(path.join(projectRoot, 'output'), { recursive: true, force: true });
}
} catch (err) {
console.error(`Error: ${err}`);
}
})
describe('output directory validation', () => {
const testBaseDir = path.join(projectRoot, 'test-output-validation');
beforeEach(async (t) => {
await fs.mkdir(testBaseDir, { recursive: true });
});
afterEach(async (t) => {
if (existsSync(testBaseDir)) {
await fs.rm(testBaseDir, { recursive: true, force: true });
}
});
test('should accept a valid output directory name within project root', async (t) => {
const validDir = testBaseDir;
const { stdout } = await execFileAsync('node', ['./cli/index.mjs', '-o', validDir]);
assert.ok(stdout.includes('Success'));
assert.ok(existsSync(validDir));
});
test('should reject path traversal attempts (e.g., ../)', async () => {
const maliciousDir = '../evil-lair';
try {
await execFileAsync('node', ['./cli/index.mjs', '-o', maliciousDir]);
assert.fail('Expected command to fail for path traversal');
} catch (error) {
assert.ok(error.stderr.includes(`Output directory contains invalid characters`));
}
});
const invalidCharFolderNames = ['folder:name', '{{something}}.com', '!folder', '@something', 'folder#name', '!folder']
invalidCharFolderNames.forEach(item => {
test(`should reject ${item} as output dir name`, async (ctx) => {
const invalidCharDir = item;
try {
await execFileAsync('node', ['./cli/index.mjs', '-o', invalidCharDir]);
assert.fail('Expected command to fail for invalid characters');
} catch (error) {
assert.ok(error.stderr.includes(`Output directory contains invalid characters`));
assert.strictEqual(error.code, 1);
assert.ok(!existsSync(path.join(projectRoot, invalidCharDir)))
}
})
});
});
})
describe('interactive mode', () => {
const testDir = path.join(projectRoot, 'test-interactive');
const testRulesDir = path.join(testDir, '.cursor', 'rules');
beforeEach(async () => {
// Create test directory structure
await fs.mkdir(path.join(testRulesDir, 'standards'), { recursive: true });
await fs.mkdir(path.join(testRulesDir, 'test'), { recursive: true });
await fs.mkdir(path.join(testRulesDir, 'utils'), { recursive: true });
// Create test rule files
await fs.writeFile(
path.join(testRulesDir, 'standards', 'test-rule-1.mdc'),
'---\ndescription: Test rule 1\nglobs: *.js\nalwaysApply: false\n---\n# Test Rule 1\nTest content'
);
await fs.writeFile(
path.join(testRulesDir, 'standards', 'test-rule-2.mdc'),
'---\ndescription: Test rule 2\nglobs: *.ts\nalwaysApply: false\n---\n# Test Rule 2\nTest content'
);
await fs.writeFile(
path.join(testRulesDir, 'test', 'test-test-rule.mdc'),
'---\ndescription: Test test rule\nglobs: *.test.js\nalwaysApply: false\n---\n# Test Test Rule\nTest content'
);
await fs.writeFile(
path.join(testRulesDir, 'utils', 'test-utils-rule.mdc'),
'---\ndescription: Test utils rule\nglobs: *.json\nalwaysApply: false\n---\n# Test Utils Rule\nTest content'
);
});
afterEach(async () => {
// Clean up test directory
if (existsSync(testDir)) {
await fs.rm(testDir, { recursive: true, force: true });
}
});
test('should accept -i flag and start interactive mode', async (t) => {
try {
const { stdin, stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '-i']);
t.mock.method(process, 'stdin', 'isTTY', true);
assert.call(stdin.isTTY, t.mock.calls.length > 0);
assert.ok(stdout.includes('Starting interactive mode') || stdout.includes('Loading'));
assert.ok(stdout.includes('Select rules by category'))
} catch (error) {
// Expected to fail in non-TTY environment due to setRawMode
assert.strictEqual(error.code, 1)
}
});
test('should accept --interactive flag and start interactive mode', async () => {
try {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '--interactive']);
// Should start interactive mode
assert.ok(stdout.includes('Starting interactive mode') || stdout.includes('Loading'));
assert.ok(stdout.includes('Select rules by category'))
} catch (error) {
if (process.platform === 'win32') {
assert.ok(error.code, 'ERR_TEST_FAILURE'); // Windows error for non-TTY
}
else {
// Expected to fail in non-TTY environment due to setRawMode
assert.strictEqual(error.code, 1)
}
}
});
test('should handle interactive mode with custom output directory', async () => {
const customOutputDir = path.join(testDir, 'custom-output');
try {
const { stdout, stderr } = await execFileAsync('node', ['./cli/index.mjs', '-i', '-o', customOutputDir]);
// Should start interactive mode with custom output
assert.ok(stdout.includes('Starting interactive mode') || stdout.includes('Loading'));
assert.ok(existsSync(customOutputDir))
} catch (error) {
if (process.platform === 'win32') {
assert.strictEqual(error.code, 1)
}
else {
// Expected to fail in non-TTY environment due to setRawMode
assert.strictEqual(error.code, 1)
}
}
});
test('should filter only .mdc files correctly', async () => {
// Create a non-.mdc file that should be ignored
await fs.writeFile(
path.join(testRulesDir, 'standards', 'ignore-me.txt'),
'This should be ignored'
);
const outputDir = path.join(testDir, 'filter-test');
const { stdout } = await execFileAsync('node', ['./cli/index.mjs', '-o', outputDir]);
// Should successfully process only .mdc files
assert.ok(stdout.includes('Success'));
assert.ok(existsSync(outputDir));
// The .txt file should not be copied
assert.ok(!existsSync(path.join(outputDir, 'rules', 'standards', 'ignore-me.txt')));
});
});
})