Skip to content

Commit d1e0dda

Browse files
authored
Merge pull request #57 from constructive-io/devin/1768871428-inquirerer-cli-utils
feat(inquirerer): add core CLI utilities and consolidate @inquirerer/utils
2 parents f8ad3c8 + 9050181 commit d1e0dda

12 files changed

Lines changed: 2905 additions & 5294 deletions

File tree

packages/inquirerer-utils/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,7 @@
2929
},
3030
"dependencies": {
3131
"appstash": "workspace:*",
32-
"find-and-require-package-json": "workspace:*",
33-
"minimist": "^1.2.8"
32+
"inquirerer": "workspace:*"
3433
},
3534
"devDependencies": {
3635
"@types/minimist": "^1.2.5",

packages/inquirerer-utils/src/argv.ts

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
// Argv utilities
2-
export { extractFirst } from './argv';
3-
export type { ParsedArgs } from './argv';
1+
// Re-export core CLI utilities from inquirerer
2+
export {
3+
parseArgv,
4+
extractFirst,
5+
cliExitWithError,
6+
getPackageJson,
7+
getPackageVersion,
8+
getPackageName
9+
} from 'inquirerer';
10+
export type { ParsedArgs, ParseArgvOptions, CliExitOptions, PackageJson } from 'inquirerer';
411

5-
// CLI error handling
6-
export { cliExitWithError } from './cli-error';
7-
export type { CliExitOptions } from './cli-error';
8-
9-
// Update checking
12+
// Update checking (requires appstash, not available in inquirerer)
1013
export { checkForUpdates, shouldSkipUpdateCheck } from './update-check';
1114
export type { UpdateCheckOptions, UpdateCheckResult } from './update-check';
12-
13-
// Package.json utilities
14-
export { getSelfPackageJson, getSelfVersion, getSelfName } from './package-json';
15-
export type { PackageJsonInfo } from './package-json';

packages/inquirerer-utils/src/package-json.ts

Lines changed: 0 additions & 29 deletions
This file was deleted.

packages/inquirerer/README.md

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ npm install inquirerer
6262
- [Custom Resolvers](#custom-resolvers)
6363
- [Resolver Examples](#resolver-examples)
6464
- [CLI Helper](#cli-helper)
65+
- [CLI Utilities](#cli-utilities)
66+
- [Package Information](#package-information)
67+
- [Argument Parsing](#argument-parsing)
68+
- [Error Handling](#error-handling)
6569
- [UI Components](#ui-components)
6670
- [Spinner](#spinner)
6771
- [Progress Bar](#progress-bar)
@@ -1248,6 +1252,130 @@ const cli = new CLI(handler, options);
12481252
await cli.run();
12491253
```
12501254

1255+
## CLI Utilities
1256+
1257+
inquirerer provides a complete set of utilities for building CLI applications, so you can import everything from a single package.
1258+
1259+
### Package Information
1260+
1261+
Get information about your CLI's package.json for version display and other metadata:
1262+
1263+
```typescript
1264+
import { getPackageJson, getPackageVersion, getPackageName } from 'inquirerer';
1265+
1266+
// Get the full package.json object
1267+
const pkg = getPackageJson(__dirname);
1268+
console.log(`${pkg.name}@${pkg.version}`);
1269+
1270+
// Or use the convenience helpers
1271+
if (argv.version) {
1272+
console.log(getPackageVersion(__dirname));
1273+
process.exit(0);
1274+
}
1275+
1276+
const toolName = getPackageName(__dirname);
1277+
console.log(`Welcome to ${toolName}!`);
1278+
```
1279+
1280+
### Argument Parsing
1281+
1282+
Parse command-line arguments and extract subcommands:
1283+
1284+
```typescript
1285+
import { parseArgv, extractFirst } from 'inquirerer';
1286+
1287+
const argv = parseArgv(process.argv);
1288+
const { first, newArgv } = extractFirst(argv);
1289+
1290+
// Running: mycli generate --output ./dist
1291+
// first = 'generate'
1292+
// newArgv = { output: './dist', _: [] }
1293+
1294+
switch (first) {
1295+
case 'generate':
1296+
await handleGenerate(newArgv);
1297+
break;
1298+
case 'init':
1299+
await handleInit(newArgv);
1300+
break;
1301+
default:
1302+
console.log('Unknown command');
1303+
}
1304+
```
1305+
1306+
### Error Handling
1307+
1308+
Exit gracefully with error messages and optional cleanup:
1309+
1310+
```typescript
1311+
import { cliExitWithError, CliExitOptions } from 'inquirerer';
1312+
1313+
try {
1314+
await riskyOperation();
1315+
} catch (error) {
1316+
await cliExitWithError(error, {
1317+
context: { operation: 'build', target: 'production' },
1318+
beforeExit: async () => {
1319+
await cleanup();
1320+
},
1321+
logger: customLogger // optional, defaults to console
1322+
});
1323+
}
1324+
```
1325+
1326+
### Complete CLI Example
1327+
1328+
Here's a complete example using all the CLI utilities:
1329+
1330+
```typescript
1331+
import {
1332+
CLI,
1333+
CLIOptions,
1334+
Inquirerer,
1335+
extractFirst,
1336+
cliExitWithError,
1337+
getPackageVersion,
1338+
ParsedArgs
1339+
} from 'inquirerer';
1340+
1341+
const options: Partial<CLIOptions> = {
1342+
minimistOpts: {
1343+
alias: { v: 'version', h: 'help' },
1344+
boolean: ['help', 'version']
1345+
}
1346+
};
1347+
1348+
const handler = async (argv: Partial<ParsedArgs>, prompter: Inquirerer) => {
1349+
if (argv.version) {
1350+
console.log(getPackageVersion(__dirname));
1351+
process.exit(0);
1352+
}
1353+
1354+
const { first, newArgv } = extractFirst(argv);
1355+
1356+
try {
1357+
switch (first) {
1358+
case 'init':
1359+
await handleInit(newArgv, prompter);
1360+
break;
1361+
case 'build':
1362+
await handleBuild(newArgv, prompter);
1363+
break;
1364+
default:
1365+
console.log('Usage: mycli <command> [options]');
1366+
console.log('Commands: init, build');
1367+
}
1368+
} catch (error) {
1369+
await cliExitWithError(error, {
1370+
context: { command: first }
1371+
});
1372+
}
1373+
};
1374+
1375+
const cli = new CLI(handler, options);
1376+
await cli.run();
1377+
```
1378+
12511379
## UI Components
12521380

12531381
inquirerer includes a set of UI components for building rich terminal interfaces beyond simple prompts. These are useful for showing progress, loading states, and streaming output.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { parseArgv, extractFirst, getPackageJson, getPackageVersion, getPackageName } from '../src';
2+
3+
describe('CLI utilities', () => {
4+
describe('parseArgv', () => {
5+
it('parses flags and positional args', () => {
6+
const argv = parseArgv(['node', 'cli', 'generate', '--config', 'test.json', '-v']);
7+
expect(argv._).toEqual(['generate']);
8+
expect(argv['config']).toBe('test.json');
9+
expect(argv['v']).toBe(true);
10+
});
11+
});
12+
13+
describe('extractFirst', () => {
14+
it('extracts first positional argument', () => {
15+
const { first, newArgv } = extractFirst({ _: ['init', 'myproject'], config: 'test.json' } as any);
16+
expect(first).toBe('init');
17+
expect(newArgv._).toEqual(['myproject']);
18+
expect((newArgv as any).config).toBe('test.json');
19+
});
20+
21+
it('handles empty positional args', () => {
22+
const { first, newArgv } = extractFirst({ _: [] });
23+
expect(first).toBeUndefined();
24+
expect(newArgv._).toEqual([]);
25+
});
26+
});
27+
28+
describe('package helpers', () => {
29+
it('gets package.json from __dirname', () => {
30+
const pkg = getPackageJson(__dirname);
31+
expect(pkg.name).toBe('inquirerer');
32+
expect(pkg.version).toBeDefined();
33+
});
34+
35+
it('getPackageVersion returns version string', () => {
36+
const version = getPackageVersion(__dirname);
37+
expect(typeof version).toBe('string');
38+
expect(version).toMatch(/^\d+\.\d+\.\d+/);
39+
});
40+
41+
it('getPackageName returns name string', () => {
42+
const name = getPackageName(__dirname);
43+
expect(name).toBe('inquirerer');
44+
});
45+
});
46+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import minimist, { Opts, ParsedArgs } from 'minimist';
2+
3+
/**
4+
* Parse command-line arguments using minimist.
5+
* Wrapper around minimist so you don't need to import it directly.
6+
*
7+
* @example
8+
* ```typescript
9+
* const argv = parseArgv(process.argv);
10+
* console.log(argv.config); // --config value
11+
* ```
12+
*/
13+
export const parseArgv = (args: string[] = process.argv, opts?: Opts): ParsedArgs => {
14+
return minimist(args.slice(2), opts);
15+
};
16+
17+
/**
18+
* Extracts the first positional argument from argv and returns it along with the remaining argv.
19+
* Useful for command routing where the first argument is a subcommand.
20+
*
21+
* @example
22+
* ```typescript
23+
* const { first: command, newArgv } = extractFirst(argv);
24+
* if (command === 'init') {
25+
* await handleInit(newArgv);
26+
* }
27+
* ```
28+
*/
29+
export const extractFirst = (argv: Partial<ParsedArgs>) => {
30+
const first = argv._?.[0];
31+
const newArgv = {
32+
...argv,
33+
_: argv._?.slice(1) ?? []
34+
};
35+
return { first, newArgv };
36+
};
37+
38+
export type { ParsedArgs, Opts as ParseArgvOptions } from 'minimist';

packages/inquirerer-utils/src/cli-error.ts renamed to packages/inquirerer/src/cli/error.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,18 @@ export interface CliExitOptions {
1111
/**
1212
* Exits the CLI with an error message and optional cleanup.
1313
* Supports a beforeExit hook for cleanup operations (e.g., closing database connections).
14+
*
15+
* @example
16+
* ```typescript
17+
* await cliExitWithError('Invalid configuration file');
18+
*
19+
* // With cleanup
20+
* await cliExitWithError(error, {
21+
* beforeExit: async () => {
22+
* await db.close();
23+
* }
24+
* });
25+
* ```
1426
*/
1527
export const cliExitWithError = async (
1628
error: Error | string,
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export { parseArgv, extractFirst } from './argv';
2+
export type { ParsedArgs, ParseArgvOptions } from './argv';
3+
4+
export { cliExitWithError } from './error';
5+
export type { CliExitOptions } from './error';
6+
7+
export { getPackageJson, getPackageVersion, getPackageName } from './package';
8+
export type { PackageJson } from './package';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { findAndRequirePackageJson } from 'find-and-require-package-json';
2+
3+
export interface PackageJson {
4+
name: string;
5+
version: string;
6+
[key: string]: any;
7+
}
8+
9+
/**
10+
* Gets the package.json for the current package by searching up from the given directory.
11+
* This is useful for CLIs to get their own package information.
12+
*
13+
* @example
14+
* ```typescript
15+
* const pkg = getPackageJson(__dirname);
16+
* console.log(`${pkg.name}@${pkg.version}`);
17+
* ```
18+
*/
19+
export const getPackageJson = (dirname: string): PackageJson => {
20+
return findAndRequirePackageJson(dirname);
21+
};
22+
23+
/**
24+
* Gets the version from the package.json for the current package.
25+
* Shorthand for `getPackageJson(dirname).version`.
26+
*
27+
* @example
28+
* ```typescript
29+
* if (argv.version) {
30+
* console.log(getPackageVersion(__dirname));
31+
* process.exit(0);
32+
* }
33+
* ```
34+
*/
35+
export const getPackageVersion = (dirname: string): string => {
36+
return getPackageJson(dirname).version;
37+
};
38+
39+
/**
40+
* Gets the name from the package.json for the current package.
41+
* Shorthand for `getPackageJson(dirname).name`.
42+
*
43+
* @example
44+
* ```typescript
45+
* const toolName = getPackageName(__dirname);
46+
* console.log(`Welcome to ${toolName}!`);
47+
* ```
48+
*/
49+
export const getPackageName = (dirname: string): string => {
50+
return getPackageJson(dirname).name;
51+
};

0 commit comments

Comments
 (0)