Skip to content

Commit dcf850d

Browse files
committed
feat: core modules Help, Positionals, JackSpeak
1 parent cb0d342 commit dcf850d

11 files changed

Lines changed: 476 additions & 180 deletions

File tree

examples/dev-positionals/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Example: Command
2+
3+
A distributable command-line app
4+
5+
# Notes
6+
7+
- [Command.ts](./src/Command.ts) is a plugin that defines the command. See [@battis/qui-cli.plugin](../../packages/plugin/README.md#usage) for plugin documentation.
8+
- [index.ts](./src/index.ts) registers the `Command` plugin and run the app.
9+
- [package.json](./package.json) `bin` points to compiled script
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "@examples/dev-positionals",
3+
"version": "0.0.0",
4+
"homepage": "https://github.com/battis/qui-cli/tree/main/examples/dev-command#readme",
5+
"repository": {
6+
"type": "git",
7+
"url": "https://github.com/battis/qui-cli.git",
8+
"directory": "examples/dev-command"
9+
},
10+
"author": {
11+
"name": "Seth Battis",
12+
"url": "https://github.com/battis"
13+
},
14+
"type": "module",
15+
"main": "./dist/index.js",
16+
"types": "./dist/index.d.ts",
17+
"bin": {
18+
"myCommand": "./dist/index.js"
19+
},
20+
"scripts": {
21+
"clean": "del ./dist",
22+
"build": "run-s build:*",
23+
"build:clean": "run-s clean",
24+
"build:compile": "tsc"
25+
},
26+
"dependencies": {
27+
"@battis/qui-cli.colors": "workspace:*",
28+
"@battis/qui-cli.core": "workspace:*",
29+
"@battis/qui-cli.log": "workspace:*",
30+
"@battis/qui-cli.plugin": "workspace:*"
31+
},
32+
"devDependencies": {
33+
"@tsconfig/node20": "^20.1.6",
34+
"del-cli": "^6.0.0",
35+
"npm-run-all": "^4.1.5",
36+
"typescript": "^5.8.3"
37+
}
38+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { Core, Positionals } from '@battis/qui-cli.core';
2+
3+
// require at least 2 and no more than 5 unnamed positional args (3 optional)
4+
Positionals.configure({ min: 2, max: 5 });
5+
6+
// also require three named args
7+
Positionals.require({ alpha: {}, beta: {}, gamma: {} });
8+
9+
// also require six more named args
10+
Positionals.require({
11+
a: {},
12+
b: {},
13+
c: {},
14+
d: {},
15+
e: {},
16+
f: {}
17+
});
18+
19+
await Core.run();
20+
console.log(Positionals.get('gamma'));
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "@tsconfig/node20/tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "./dist"
5+
},
6+
"include": ["./src"]
7+
}

packages/core/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
"jackspeak": "^4.1.1"
2828
},
2929
"devDependencies": {
30+
"@battis/qui-cli.colors": "workspace:*",
3031
"@battis/qui-cli.plugin": "workspace:*",
3132
"@tsconfig/node20": "^20.1.6",
3233
"@types/node": "^22.15.32",
@@ -36,6 +37,7 @@
3637
"typescript": "^5.8.3"
3738
},
3839
"peerDependencies": {
40+
"@battis/qui-cli.colors": "2.x",
3941
"@battis/qui-cli.plugin": "^2.4"
4042
},
4143
"packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee",

packages/core/src/Core.ts

Lines changed: 35 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,37 @@
1+
import * as Colors from '@battis/qui-cli.colors/dist/Colors.js';
12
import * as Plugin from '@battis/qui-cli.plugin';
2-
import { Jack, JackOptions } from 'jackspeak';
3+
import * as JackSpeak from './JackSpeak.js';
4+
import * as Positionals from './Positionals.js';
5+
import { usage } from './Usage.js';
36

47
export { Options } from '@battis/qui-cli.plugin';
8+
export * from './Usage.js';
59

6-
export type Configuration = Record<string, Plugin.Configuration> & {
7-
core?: JackOptions & {
10+
export type Configuration = Plugin.Registrar.Configuration & {
11+
/** @deprecated Use `jackspeak` property */
12+
core?: JackSpeak.Configuration & {
13+
/** @deprecated Use `Positional` plugin */
814
requirePositionals?: boolean | number;
915
};
1016
};
1117

12-
let _jack: Jack | undefined = undefined;
13-
function jack() {
14-
if (!_jack) {
15-
configure();
16-
}
17-
if (!_jack) {
18-
throw new Error('Bad things!');
19-
}
20-
return _jack;
21-
}
22-
23-
// TODO automate positionals documentation
24-
let requirePositionals: boolean | number | undefined = undefined;
2518
let initialized = false;
26-
let positionals: (string | undefined)[] = [];
27-
28-
export async function configure({ core, ...pluginConfig }: Configuration = {}) {
29-
requirePositionals = Plugin.hydrate(
30-
core?.requirePositionals,
31-
requirePositionals
32-
);
33-
34-
_jack = new Jack({
35-
...core,
36-
allowPositionals: !!requirePositionals
37-
});
3819

39-
await Plugin.Registrar.configure(pluginConfig);
20+
export async function configure(config: Configuration = {}) {
21+
const { core = {}, jackspeak: jackOptions, positionals = {} } = config;
22+
const { requirePositionals, ...deprecated } = core;
23+
const jackspeak = {
24+
...deprecated,
25+
allowPositionals: !!Positionals.requirePositionals(
26+
positionals,
27+
requirePositionals
28+
),
29+
...jackOptions
30+
};
31+
if (jackspeak.allowPositionals === false) {
32+
positionals.max = 0;
33+
}
34+
await Plugin.Registrar.configure({ positionals, jackspeak });
4035
}
4136

4237
export async function options(externalOptions: Plugin.Options = {}) {
@@ -48,35 +43,6 @@ export async function options(externalOptions: Plugin.Options = {}) {
4843
return Plugin.mergeOptions(await Plugin.Registrar.options(), externalOptions);
4944
}
5045

51-
function apply({
52-
num,
53-
numList,
54-
opt,
55-
optList,
56-
flag,
57-
flagList,
58-
fields,
59-
man = []
60-
}: Plugin.Options) {
61-
jack()
62-
.num({ ...num })
63-
.numList({ ...numList })
64-
.opt({ ...opt })
65-
.optList({ ...optList })
66-
.flag({ ...flag })
67-
.flagList({ ...flagList })
68-
.addFields({ ...fields });
69-
for (const paragraph of man) {
70-
if (paragraph.level) {
71-
jack().heading(paragraph.text, paragraph.level, {
72-
pre: paragraph.pre
73-
});
74-
} else {
75-
jack().description(paragraph.text, { pre: paragraph.pre });
76-
}
77-
}
78-
}
79-
8046
export async function init(
8147
externalOptions?: Plugin.Options
8248
): Promise<Plugin.Arguments<Plugin.Options>> {
@@ -87,17 +53,13 @@ export async function init(
8753
}
8854
for (const plugin of Plugin.Registrar.registered()) {
8955
if (plugin.options) {
90-
apply(await plugin.options());
56+
JackSpeak.args(await plugin.options());
9157
}
9258
}
9359
if (externalOptions) {
94-
apply(externalOptions);
60+
JackSpeak.args(externalOptions);
9561
}
96-
97-
const args = jack().parse() as Plugin.Arguments<Plugin.Options>;
98-
const { positionals: p } = args;
99-
positionals = p;
100-
62+
const args = JackSpeak.parse();
10163
await Plugin.Registrar.init(args);
10264
initialized = true;
10365
return args;
@@ -106,45 +68,14 @@ export async function init(
10668
export async function run(
10769
externalOptions?: Plugin.Options
10870
): Promise<Plugin.AccumulatedResults | undefined> {
109-
if (!initialized) {
110-
await init(externalOptions);
111-
}
112-
113-
if (
114-
requirePositionals &&
115-
(!positionals.length ||
116-
(typeof requirePositionals == 'number' &&
117-
positionals.length < requirePositionals))
118-
) {
119-
throw new Error(
120-
`Incorrect positional arguments (${requirePositionals} expected, ${positionals.length} provided)`
121-
);
122-
}
123-
124-
return await Plugin.Registrar.run();
125-
}
126-
127-
export function usage() {
128-
let usage = jack().usage();
129-
if (requirePositionals) {
130-
if (typeof requirePositionals === 'number') {
131-
if (requirePositionals > 1) {
132-
usage = usage.replace(
133-
/\n\n/m, // FIXME hilariously unreliable regex!
134-
// Issue URL: https://github.com/battis/qui-cli/issues/25
135-
` arg0..arg${requirePositionals - 1}\n\n`
136-
);
137-
} else {
138-
usage = usage.replace(/\n\n/m, ' argument\n\n');
139-
}
140-
} else {
141-
usage = usage.replace(/\n\n/m, ' argument0...\n\n');
71+
try {
72+
if (!initialized) {
73+
await init(externalOptions);
14274
}
75+
return await Plugin.Registrar.run();
76+
} catch (e) {
77+
const error = e as Error;
78+
console.log(`${Colors.error(error.message)}\n\n${usage()}`);
79+
process.exit(1);
14380
}
144-
return usage;
145-
}
146-
147-
export function usageMarkdown() {
148-
return jack().usageMarkdown(); // FIXME format arguments
149-
// Issue URL: https://github.com/battis/qui-cli/issues/24
15081
}

packages/core/src/JackSpeak.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import * as Plugin from '@battis/qui-cli.plugin';
2+
import { Jack, JackOptions } from 'jackspeak';
3+
4+
export type Configuration = Plugin.Configuration & JackOptions;
5+
6+
export const name = 'jackspeak';
7+
8+
let instance: Jack | undefined = undefined;
9+
10+
function jack() {
11+
if (!instance) {
12+
configure();
13+
}
14+
if (!instance) {
15+
throw new Error(`JackSpeak configuration failed.`);
16+
}
17+
return instance;
18+
}
19+
20+
export function configure(config: Configuration = {}) {
21+
instance = new Jack(config);
22+
}
23+
24+
export function args({
25+
num,
26+
numList,
27+
opt,
28+
optList,
29+
flag,
30+
flagList,
31+
fields,
32+
man = []
33+
}: Plugin.Options) {
34+
jack()
35+
.num({ ...num })
36+
.numList({ ...numList })
37+
.opt({ ...opt })
38+
.optList({ ...optList })
39+
.flag({ ...flag })
40+
.flagList({ ...flagList })
41+
.addFields({ ...fields });
42+
for (const paragraph of man) {
43+
if (paragraph.level) {
44+
jack().heading(paragraph.text, paragraph.level, {
45+
pre: paragraph.pre
46+
});
47+
} else {
48+
jack().description(paragraph.text, { pre: paragraph.pre });
49+
}
50+
}
51+
}
52+
53+
export function parse() {
54+
return jack().parse();
55+
}
56+
57+
export function usage() {
58+
return jack().usage();
59+
}
60+
61+
export function usageMarkdown() {
62+
return jack().usageMarkdown();
63+
}

0 commit comments

Comments
 (0)