Instructions for AI to help develop and maintain the npm-script-selector CLI tool - a TypeScript-based interactive script runner for package.json files
NPM Script Selector is a TypeScript CLI tool that helps users discover and run npm scripts from package.json files interactively. The entire application is a single-class implementation in `app.ts`.
The `ScriptRunner` class in `app.ts` handles all functionality:
All user-facing strings (error messages, CLI options) are centralized in `package.json` under `config` section:
```json
{
"config": {
"scriptName": "npmss",
"cliOptions": { /* option definitions */ },
"errorMessages": { /* all error strings */ }
}
}
```
When adding features or changing messages, always update `package.json` config first, then reference via `appConfig` import.
```bash
npm run build # Compile TypeScript to dist/
npm test # Run Jasmine specs from spec/
npm run devstart # Build and run with -h flag
npm run devStartWithFile # Build and run with test package.json path
```
Tests are in `spec/app.spec.mjs` using ES module syntax (`.mjs`).
The `devStartWithFile` script uses an absolute Windows path - update with your local path when testing:
```json
"devStartWithFile": "npm run build && node ./dist/app.js -f C:\\Users\\justi\\npm-script-selector\\package.json"
```
All imports use named imports from package.json:
```typescript
import { name as appName, version, config as appConfig } from './package.json';
```
Always resolve absolute paths using `path.resolve()` before file operations:
```typescript
this.absoluteFilePath = path.resolve(filePath);
```
Then change working directory to package location: `process.chdir(packageDir)`
1. Check for error condition
2. Set `this.running = false`
3. Log error using `appConfig.errorMessages.*`
4. Call `process.exit(1)`
The tool uses async/await extensively:
Commander.js options are defined from `package.json` config. Four main options:
Scripts run via `spawn('npm', ['run ${scriptName}'], { stdio: 'inherit', shell: true })`. The `stdio: 'inherit'` preserves TTY for interactive scripts (webpack dev server, test watchers, etc.).
1. Add option definition to `package.json` under `config.cliOptions`
2. Add `.option()` call in ScriptRunner constructor using config
3. Read option value from `this.program.opts()`
4. Add validation/processing logic in constructor
Banner configuration is in constructor. Three states:
The `runScript()` method uses Promise wrapper around spawn. To modify:
1. Update version in `package.json` following semver
2. Run `npm run build` to compile TypeScript to dist/
3. Test locally: `npm link` then `npmss -f /path/to/test/package.json`
4. Publish: `npm publish`
The package includes:
Files excluded via `.gitignore`:
**Problem**: Module resolution errors when importing from package.json
**Solution**: Ensure `resolveJsonModule: true` and `esModuleInterop: true` in tsconfig.json
**Problem**: "Cannot find module" at runtime
**Solution**: Use Node16 module system, not CommonJS or ES2015. Check `module: "Node16"` in tsconfig.json
**Problem**: CLI doesn't run after `npm link` or global install
**Solution**:
1. Verify shebang at top of `dist/app.js`: `#!/usr/bin/env node`
2. Check file has execute permissions on Unix systems
3. Confirm `bin` field in package.json points to `dist/app.js`
**Problem**: "Cannot find package.json" errors
**Solution**: Use absolute paths or ensure current directory is correct. Path resolution uses `path.resolve()` which is relative to execution context, not script location.
**Problem**: Interactive scripts (webpack dev server, jest --watch) don't work
**Solution**: This should work - verify `stdio: 'inherit'` is set in spawn options. This preserves TTY for interactive processes.
**Problem**: Script output not showing
**Solution**: Check `stdio: 'inherit'` in spawn call. Without this, stdout/stderr won't pipe to parent process.
**Problem**: Script runs in wrong directory
**Solution**: Verify `process.chdir(packageDir)` is called before spawn. The tool intentionally changes CWD to package location.
**Problem**: Tests import from `../dist/app.js` but file doesn't exist
**Solution**: Run `npm run build` before `npm test`. Tests use compiled output, not source TS files.
**Problem**: Jasmine can't find test specs
**Solution**: Check spec files use `.mjs` extension (not `.js`) and match pattern in `spec/support/jasmine.json`: `**/*[sS]pec.?(m)js`
The codebase uses WebStorm/IntelliJ inspection suppressions:
```typescript
// noinspection JSIgnoredPromiseFromCall
runner.runProgram();
```
This is intentional - the main entry point launches async without await since it's the program root.
Critical pattern for multi-project support:
1. Accept file path from CLI (can be relative or absolute)
2. Convert to absolute: `path.resolve(filePath)` (L56 in app.ts)
3. Extract directory: `path.dirname(this.absoluteFilePath)` (L96)
4. Change CWD: `process.chdir(packageDir)` (L98)
5. Run npm scripts in that context
This ensures scripts execute in the correct project directory, not where `npmss` was invoked from.
Inquirer-autocomplete uses dynamic import to handle default exports from ES modules:
```typescript
const { default: autocomplete } = await import("inquirer-autocomplete-standalone");
```
This pattern is necessary when importing packages that don't provide named exports in Node16 module system.
The search function includes `setTimeout(..., 300 + 30)` - the 330ms delay is intentional for UX smoothness, preventing overly rapid UI updates during fuzzy search filtering.
Tests in `spec/app.spec.mjs` mock at the API boundary:
This approach tests business logic without filesystem or process dependencies.
Leave a review
No reviews yet. Be the first to review this skill!
# Download SKILL.md from killerskills.ai/api/skills/npm-script-selector-github-copilot-instructions/raw