asljs-rq-cli 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md ADDED
@@ -0,0 +1,20 @@
1
+ # LICENSE
2
+
3
+ Copyright (c) 2026 Alexandrite Software Ltd
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,220 @@
1
- # Temporary Holding Version
1
+ # rq
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > Part of [Alexandrite Software Library][#1] - a set of high-quality, performant
4
+ > JavaScript libraries and tools for everyday use.
5
+
6
+ AI-assisted markdown-based requirements management: AI skills to manage and
7
+ verify them, tools for structural modification and reporting, presentation apps
8
+ to manage requirement graphs visually.
9
+
10
+ ## Overview
11
+
12
+ A requirement is a markdown file with a statement about the system. A big
13
+ requirement is decomposed into smaller ones, and each requirement links to the
14
+ requirements and tests that implement it. A test is a markdown file with steps
15
+ that show a requirement holds: deterministic, reproducible commands. The results
16
+ of each run are kept apart, in `.rq/E<n> <slug>.md` execution files that also
17
+ record the git commit, branch and changed files.
18
+
19
+ The links form a directed graph with no cycles: the requirements are a strict
20
+ hierarchy, each with one parent, and a test may be linked from several
21
+ requirements. A requirement nothing links to is a root, and there may be
22
+ several. A requirement passes when every test and requirement it links to
23
+ passes, by the results they record, and holds when its statements are also fully
24
+ covered by them.
25
+
26
+ - Commands add, link, unlink, move and remove requirements and tests, and log
27
+ results, keeping every link valid, the graph free of cycles and each
28
+ requirement with one parent; others list a requirement's links, its backlinks,
29
+ or the whole graph as JSON.
30
+ - `rq check [path]` checks the structure; `rq test <target>...` runs the tests
31
+ of requirements, tests, ids such as `R10` or folders, with `--recurse` those
32
+ below them too, records the run in an execution file and reports each result;
33
+ test steps are shell commands, JavaScript or .NET tests, or instructions an AI
34
+ agent carries out. Each test and requirement keeps its latest result in its
35
+ `## Status` section; a failed test fails every requirement above it.
36
+ - `rq coverage <target>...` asks an AI agent whether each requirement's
37
+ sub-requirements and tests fully cover it, and records the verdict.
38
+ - `rq view [path]` serves the graph as a clickable diagram, each border coloured
39
+ by the result and drawn solid, dashed or dotted by the coverage, and each
40
+ document of the documents folder rendered as HTML, including documents outside
41
+ the requirements folder that the requirements link to.
42
+ - `rq agents` prints the formats and the rules every change keeps, for an AI
43
+ agent that edits the files without `rq`.
44
+ - `rq config` prints the folders, `rq.json`, the agent and the log settings, and
45
+ where each comes from.
46
+ - The [skills][SK] tell an AI agent how to decompose requirements, write tests,
47
+ close the coverage gaps `rq coverage` describes in each requirement's `##
48
+ Coverage` section, and update the related requirements after a change: it
49
+ writes the text, and changes the structure with the commands.
50
+
51
+ The model and the commands themselves are functions of the [rq library][RQL].
52
+ This package is the `rq` command, the `rq-mcp` server and `rq view` over them;
53
+ an application that wants the functions without a command line imports
54
+ `asljs-rq` instead.
55
+
56
+ ## Scope
57
+
58
+ - Requirements and tests are plain markdown files, kept with the code.
59
+ - The graph holds requirements, `R<n> <name>.md`, and tests, `T<n> <name>.md`,
60
+ and nothing else; its edges are the links in each requirement's `##
61
+ Implementation` list. Other documents are not part of the graph, though `rq
62
+ remove` and `rq move` fix the links to what they remove or move in every `.md`
63
+ file of the documents folder.
64
+ - `rq.json` or `--requirements-dir` and `--documents-dir` set where the
65
+ requirements are and where every document is; by default both are the working
66
+ folder.
67
+ - Steps run in order, in the test's folder, until the first that fails. An
68
+ instruction step needs an AI agent, `claude` or `copilot`; the first one
69
+ installed is used unless `--ai` names one.
70
+ - The view loads Mermaid from a CDN to draw the diagram; the documents and their
71
+ links work without it.
72
+
73
+ ## Installation
74
+
75
+ ```bash
76
+ npm install asljs-rq-cli
77
+ ```
78
+
79
+ ## Usage
80
+
81
+ Every command takes `--loglevel <level>`, `--logfile <target>` and `--logformat
82
+ <format>`, or `RQ_LOG_LEVEL`, `RQ_LOG_FILE` and `RQ_LOG_FORMAT`, to log what it
83
+ does, e.g. `--loglevel debug --logfile stderr`; nothing is logged without them.
84
+
85
+ Build the graph with the commands, and write the statements, descriptions and
86
+ steps as text:
87
+
88
+ ```bash
89
+ npx rq add requirement "requirements/R1 Export.md" "CSV export" --statement "The report exports as CSV."
90
+ npx rq add test "requirements/R1 Export.md" "PDF export" --description "The PDF export test produces a valid PDF." --step 'npm test -- --test-name-pattern="PDF export"'
91
+ ```
92
+
93
+ The parent, `requirements/R1 Export.md`, now lists them:
94
+
95
+ ```markdown
96
+ # R1 Export
97
+
98
+ The application exports a report as CSV and as PDF.
99
+
100
+ ## Implementation
101
+
102
+ - [R2 CSV export](<R2 CSV export.md>)
103
+ - [T1 PDF export](<tests/T1 PDF export.md>)
104
+ ```
105
+
106
+ Query the structure:
107
+
108
+ ```bash
109
+ npx rq links "requirements/R1 Export.md" --json
110
+ npx rq backlinks "requirements/tests/T1 PDF export.md"
111
+ npx rq tojson requirements
112
+ ```
113
+
114
+ Check it, then run the test:
115
+
116
+ ```bash
117
+ npx rq check requirements
118
+ npx rq test requirements
119
+ ```
120
+
121
+ ```text
122
+ PASS requirements/tests/T1 PDF export.md - 1 step
123
+ FAIL requirements/R1 Export.md - 1 of 2 links failed: R2 CSV export: links to no requirement or test
124
+ FAIL requirements/R2 CSV export.md - links to no requirement or test
125
+ Results .rq/E1 requirements.md
126
+ ```
127
+
128
+ The execution file records the run and the working directory, then each test:
129
+
130
+ ````markdown
131
+ # E1 requirements
132
+
133
+ - Date: 2026-10-08T09:30:00.000Z
134
+ - Command: `rq test requirements`
135
+ - Result: PASS - 1 of 1 tests passed
136
+ - Commit: 3f2a9c1d0b8e7f6a5c4d3e2f1a0b9c8d7e6f5a4b
137
+ - Branch: main
138
+ - Changed files: none
139
+
140
+ ## T1 PDF export
141
+
142
+ - File: <requirements/tests/T1 PDF export.md>
143
+ - Result: PASS - 1 step
144
+
145
+ ```text
146
+ [step 1] Step 1
147
+ $ npm test -- --test-name-pattern="PDF export"
148
+ ...
149
+ ```
150
+ ````
151
+
152
+ Run a single requirement and everything below it by id, from any folder:
153
+
154
+ ```bash
155
+ npx rq test R1 --recurse --working-dir path/to/project
156
+ ```
157
+
158
+ Browse the graph and the documents:
159
+
160
+ ```bash
161
+ npx rq view requirements --port 8080
162
+ ```
163
+
164
+ With `rq.json` at the project root, the commands default to the requirements
165
+ folder, and the view serves the whole project:
166
+
167
+ ```json
168
+ {
169
+ "requirementsDir": "requirements",
170
+ "documentsDir": "."
171
+ }
172
+ ```
173
+
174
+ ```bash
175
+ npx rq view
176
+ npx rq config
177
+ ```
178
+
179
+ ## Further reading
180
+
181
+ - [Requirement files][RF] - the formats of requirements, tests, `## Status`
182
+ sections and execution files, and the rules every change keeps.
183
+ - [Requirements][RM] - how `rq` reads and writes them, and how the graph is
184
+ built.
185
+ - [Querying the graph][QG] and [Changing the graph][CG] - the structural
186
+ commands.
187
+ - [rq check][CK], [rq test][TS], [rq coverage][CV], [rq view][VW] and
188
+ [rq config][CF] - checking, testing, checking coverage, viewing and the
189
+ configuration.
190
+ - [rq agents][AG] - the instructions for an AI agent that edits the files
191
+ without `rq`.
192
+ - [rq-mcp][MC] - the MCP server with a tool per command.
193
+ - [Skills][SK] - the AI skills for managing requirements.
194
+
195
+ Questions and bugs: [asljs/issues][IS].
196
+
197
+ ## Related packages
198
+
199
+ - [rq library][RQL] - the requirements model and the commands as functions.
200
+ - `asljs-part` defines project artefacts in markdown and checks them with rules.
201
+
202
+ ## License
203
+
204
+ MIT
205
+
206
+ [#1]: https://github.com/AlexandriteSoftware/asljs
207
+ [AG]: <docs/rq agents.md>
208
+ [CF]: <docs/rq config.md>
209
+ [CG]: <docs/Changing the graph.md>
210
+ [CK]: <docs/rq check.md>
211
+ [CV]: <docs/rq coverage.md>
212
+ [IS]: https://github.com/AlexandriteSoftware/asljs/issues
213
+ [MC]: docs/rq-mcp.md
214
+ [QG]: <docs/Querying the graph.md>
215
+ [RF]: <../../libs/rq/docs/Requirement files.md>
216
+ [RM]: ../../libs/rq/docs/Requirements.md
217
+ [RQL]: ../../libs/rq/README.md
218
+ [SK]: skills
219
+ [TS]: <docs/rq test.md>
220
+ [VW]: <docs/rq view.md>
package/bin/rq-mcp.js ADDED
@@ -0,0 +1,22 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main }
4
+ from '../dist/mcp.js';
5
+
6
+ main(
7
+ process.argv.slice(2),
8
+ { cwd: process.cwd(),
9
+ env: process.env,
10
+ stdout: process.stderr,
11
+ stderr: process.stderr },
12
+ process.stdin,
13
+ line => process.stdout.write(line))
14
+ .catch(
15
+ error => {
16
+ console.error(
17
+ error instanceof Error
18
+ ? error.message
19
+ : String(error));
20
+
21
+ process.exitCode = 1;
22
+ });
package/bin/rq.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main }
4
+ from '../dist/cli.js';
5
+
6
+ process.exitCode =
7
+ await main(
8
+ process.argv.slice(2),
9
+ { cwd: process.cwd(),
10
+ env: process.env,
11
+ stdout: process.stdout,
12
+ stderr: process.stderr });
package/dist/cli.d.ts ADDED
@@ -0,0 +1,38 @@
1
+ import { type Folders, Io } from 'asljs-rq';
2
+ import { Command } from 'commander';
3
+ import { Server } from 'node:http';
4
+ /**
5
+ * What a run of the command line leaves: its exit code, and the Io of the
6
+ * command, with `--working-dir`, `--requirements-dir` and
7
+ * `--documents-dir` applied.
8
+ */
9
+ export interface CliState {
10
+ exitCode: number;
11
+ io: Io;
12
+ /**
13
+ * The folders of the command and where they come from; `null` before a
14
+ * command runs.
15
+ */
16
+ folders: Folders | null;
17
+ }
18
+ /**
19
+ * The `rq` command line, not yet run: the program, and the state its
20
+ * actions set.
21
+ */
22
+ export declare function createCli(args: string[], io: Io, servers?: Server[]): {
23
+ cli: Command;
24
+ state: CliState;
25
+ };
26
+ /**
27
+ * Runs the `rq` command line and returns the exit code. `rq view`
28
+ * returns once the server listens, and adds it to `servers`; it serves until
29
+ * whoever passed `servers` closes it.
30
+ */
31
+ export declare function runCli(args: string[], io: Io, servers?: Server[]): Promise<number>;
32
+ /**
33
+ * The `rq` executable: runs the command line with a logger provider made
34
+ * from `--loglevel`, `--logfile`, `--logformat` and the `RQ_LOG_`
35
+ * variables, and disposes it before it returns the exit code. A server that
36
+ * `view` started serves until SIGINT or SIGTERM first.
37
+ */
38
+ export declare function main(args: string[], io: Io): Promise<number>;
package/dist/cli.js ADDED
@@ -0,0 +1,315 @@
1
+ import { AI_AGENTS, parseAgentSpec } from 'asljs-agents';
2
+ import { createLoggerProvider, readLoggerOptions } from 'asljs-logging';
3
+ import { postProcess, takeWritten, untilStopped } from 'asljs-mdcli';
4
+ import { execAdd, execAgents, execBacklinks, execCheck, execCoverage, execLink, execLinks, execList, execLog, execMove, execRemove, execTest, execToJson, execUnlink, getLogger, resolveFolders } from 'asljs-rq';
5
+ import { Command, CommanderError } from 'commander';
6
+ import path from 'node:path';
7
+ import { execConfig } from './config.js';
8
+ import { execView } from './view.js';
9
+ const WORKING_DIR = 'The folder to work in: paths resolve against it, rq.json is looked for from it up, and its .rq folder holds the results; the current directory by default';
10
+ const REQUIREMENTS_DIR = 'The folder of the requirements: the default path, and where ids and .md names are searched for; requirementsDir of rq.json, else the working folder';
11
+ const DOCUMENTS_DIR = 'The folder of every document, containing the requirements folder: what view serves, and where backlinks and links to rewrite are searched for; documentsDir of rq.json, else the requirements folder';
12
+ /**
13
+ * The `rq` command line, not yet run: the program, and the state its
14
+ * actions set.
15
+ */
16
+ export function createCli(args, io, servers = []) {
17
+ const state = { exitCode: 0,
18
+ io,
19
+ folders: null };
20
+ const command = ['rq',
21
+ ...args.map(arg => /^[\w./:=@+-]+$/.test(arg)
22
+ ? arg
23
+ : JSON.stringify(arg))]
24
+ .join(' ');
25
+ const cli = new Command();
26
+ cli.name('rq')
27
+ .description('AI-assisted requirements management: test and view requirements and tests written in markdown.')
28
+ .helpCommand(false)
29
+ .configureOutput({ writeOut: value => io.stdout.write(value),
30
+ writeErr: value => io.stderr.write(value) })
31
+ .exitOverride()
32
+ .option('--loglevel <level>', 'Log level: trace, debug, information, warning, error')
33
+ .option('--logfile <target>', 'Where logs go: a file path, stdout or stderr')
34
+ .option('--logformat <format>', 'Log format: auto, json, text or pretty')
35
+ .hook('preAction', async (_cli, action) => {
36
+ const options = action.opts();
37
+ const cwd = options.workingDir === undefined
38
+ ? io.cwd
39
+ : path.resolve(io.cwd, options.workingDir);
40
+ state.folders =
41
+ await resolveFolders(cwd, options);
42
+ // A folder left to its default stays unset, so that a command can
43
+ // tell it from one that was given; `view` serves the folder of its
44
+ // path then.
45
+ const given = (setting) => setting.source === 'default'
46
+ ? undefined
47
+ : setting.value;
48
+ state.io =
49
+ { ...io,
50
+ cwd,
51
+ requirementsDir: given(state.folders.requirementsDir),
52
+ documentsDir: given(state.folders.documentsDir) };
53
+ });
54
+ cli.command('test')
55
+ .description('Run the tests of requirements and tests, record the results in .rq/E<n> <slug>.md, and report their results')
56
+ .argument('<targets...>', 'Requirement or test files, folders, .md names, or ids such as R10 or T12')
57
+ .option('--recurse', 'Also run the tests of every requirement below a requirement target')
58
+ .option('--name <slug>', 'The slug of the results file; the targets by default')
59
+ .option('--ai [agent]', `The AI agent and model, [${AI_AGENTS.join('|')}][:<model>], e.g. claude:fable, for instruction steps; the first installed agent by default`)
60
+ .action(async (targets, options) => {
61
+ state.exitCode =
62
+ await execTest(state.io, { targets,
63
+ recurse: options.recurse,
64
+ name: options.name,
65
+ command,
66
+ ai: options.ai === undefined
67
+ ? undefined
68
+ : parseAgentSpec(options.ai) });
69
+ });
70
+ cli.command('coverage')
71
+ .description('Ask an AI agent whether the sub-requirements and tests of each requirement fully cover it, and record the verdict in its Status')
72
+ .argument('<targets...>', 'Requirement files, folders, .md names, or ids such as R10')
73
+ .option('--recurse', 'Also check every requirement below a requirement target')
74
+ .option('--ai [agent]', `The AI agent and model, [${AI_AGENTS.join('|')}][:<model>], e.g. claude:fable; the first installed agent by default`)
75
+ .action(async (targets, options) => {
76
+ state.exitCode =
77
+ await execCoverage(state.io, { targets,
78
+ recurse: options.recurse,
79
+ ai: options.ai === undefined
80
+ ? undefined
81
+ : parseAgentSpec(options.ai) });
82
+ });
83
+ cli.command('view')
84
+ .description('Serve the requirements graph and the rendered documents over HTTP')
85
+ .argument('[path]', 'A requirement file, or a folder of requirements; the requirements folder by default')
86
+ .option('--port <port>', 'Port to listen on, exactly; 0 picks a free one. By default the first free port from 3000 on')
87
+ .action(async (target, options) => {
88
+ const port = options.port === undefined
89
+ ? undefined
90
+ : Number(options.port);
91
+ if (port !== undefined
92
+ && (!Number.isInteger(port)
93
+ || port < 0
94
+ || port > 65535)) {
95
+ throw new Error(`Invalid port: ${options.port}`);
96
+ }
97
+ servers.push(await execView(state.io, { target,
98
+ port }));
99
+ });
100
+ cli.command('check')
101
+ .description('Check the structure of the graph and its documents without running anything')
102
+ .argument('[path]', 'A requirement file, or a folder of requirements; the requirements folder by default')
103
+ .action(async (target) => {
104
+ state.exitCode =
105
+ await execCheck(state.io, { target });
106
+ });
107
+ cli.command('list')
108
+ .description('List the requirements and tests of the graph, from the roots down')
109
+ .argument('[path]', 'A requirement file, or a folder of requirements; the requirements folder by default')
110
+ .option('--json', 'Print JSON')
111
+ .action(async (target, options) => {
112
+ state.exitCode =
113
+ await execList(state.io, { target,
114
+ json: options.json });
115
+ });
116
+ cli.command('links')
117
+ .description('List the requirements and tests a requirement links to')
118
+ .argument('<file>', 'A requirement')
119
+ .option('--json', 'Print JSON')
120
+ .action(async (file, options) => {
121
+ state.exitCode =
122
+ await execLinks(state.io, { file,
123
+ json: options.json });
124
+ });
125
+ cli.command('backlinks')
126
+ .description('List the requirements that link to a requirement or a test')
127
+ .argument('<file>', 'A requirement or a test')
128
+ .option('--json', 'Print JSON')
129
+ .action(async (file, options) => {
130
+ state.exitCode =
131
+ await execBacklinks(state.io, { file,
132
+ json: options.json });
133
+ });
134
+ cli.command('tojson')
135
+ .description('Print the graph as JSON with the structural fields, the result and the coverage of every document')
136
+ .argument('[path]', 'A requirement file, or a folder of requirements; the requirements folder by default')
137
+ .action(async (target) => {
138
+ state.exitCode =
139
+ await execToJson(state.io, { target });
140
+ });
141
+ cli.command('agents')
142
+ .description('Print instructions for an AI agent that edits the requirement files directly: their formats and the rules every change keeps')
143
+ .action(async () => {
144
+ state.exitCode =
145
+ await execAgents(state.io);
146
+ });
147
+ cli.command('config')
148
+ .description('Print the configuration: the folders and where they come from, rq.json, the AI agent, logging, and the environment variables')
149
+ .option('--json', 'Print JSON')
150
+ .action(async (options) => {
151
+ state.exitCode =
152
+ await execConfig(state.io, { folders: state.folders,
153
+ args,
154
+ json: options.json });
155
+ });
156
+ const add = cli.command('add')
157
+ .description('Create a requirement or a test and link it from a requirement');
158
+ add.command('requirement')
159
+ .description('Create R<n> <name>.md next to the parent and link it from the parent')
160
+ .argument('<parent>', 'The requirement it implements')
161
+ .argument('<name>', 'The name after the id')
162
+ .option('--statement <text>', 'The statement of the requirement')
163
+ .option('--path <file>', 'The path of the new document, instead of R<n> <name>.md')
164
+ .action(async (parent, name, options) => {
165
+ state.exitCode =
166
+ await execAdd(state.io, { kind: 'requirement',
167
+ parent,
168
+ name,
169
+ body: options.statement,
170
+ path: options.path });
171
+ });
172
+ add.command('test')
173
+ .description('Create tests/T<n> <name>.md next to the parent and link it from the parent')
174
+ .argument('<parent>', 'The requirement it shows to hold')
175
+ .argument('<name>', 'The name after the id')
176
+ .option('--description <text>', 'What the test shows')
177
+ .option('--step <command>', 'A shell step running the command; repeat for several', collect, [])
178
+ .option('--path <file>', 'The path of the new document, instead of tests/T<n> <name>.md')
179
+ .action(async (parent, name, options) => {
180
+ state.exitCode =
181
+ await execAdd(state.io, { kind: 'test',
182
+ parent,
183
+ name,
184
+ body: options.description,
185
+ steps: options.step,
186
+ path: options.path });
187
+ });
188
+ cli.command('link')
189
+ .description('Link an existing requirement or test from a requirement')
190
+ .argument('<parent>', 'The requirement')
191
+ .argument('<child>', 'The requirement or test that implements it')
192
+ .action(async (parent, child) => {
193
+ state.exitCode =
194
+ await execLink(state.io, { parent,
195
+ child });
196
+ });
197
+ cli.command('unlink')
198
+ .description('Remove the links from a requirement to a requirement or test')
199
+ .argument('<parent>', 'The requirement')
200
+ .argument('<child>', 'The requirement or test it links to')
201
+ .action(async (parent, child) => {
202
+ state.exitCode =
203
+ await execUnlink(state.io, { parent,
204
+ child });
205
+ });
206
+ cli.command('remove')
207
+ .description('Delete a requirement or a test and the links to it')
208
+ .argument('<file>', 'A requirement or a test')
209
+ .option('--recursive', 'Also delete what it links to that nothing else links to')
210
+ .action(async (file, options) => {
211
+ state.exitCode =
212
+ await execRemove(state.io, { file,
213
+ recursive: options.recursive });
214
+ });
215
+ cli.command('move')
216
+ .description('Move or rename a requirement or a test and rewrite the links to it')
217
+ .argument('<file>', 'A requirement or a test')
218
+ .argument('<destination>', 'The new path, or a folder to move it into')
219
+ .action(async (file, destination) => {
220
+ state.exitCode =
221
+ await execMove(state.io, { file,
222
+ destination });
223
+ });
224
+ cli.command('log')
225
+ .description('Record a result established another way in .rq/E<n> <test>.md')
226
+ .argument('<test>', 'A test file, .md name or id, e.g. T12')
227
+ .requiredOption('--result <result>', 'PASS or FAIL')
228
+ .option('--note <text>', 'A short note')
229
+ .option('--time <time>', 'The time in ISO 8601; now by default')
230
+ .action(async (file, options) => {
231
+ state.exitCode =
232
+ await execLog(state.io, { file,
233
+ result: options.result,
234
+ note: options.note,
235
+ time: options.time,
236
+ command });
237
+ });
238
+ for (const command of [...cli.commands,
239
+ ...add.commands]) {
240
+ if (command !== add) {
241
+ command.option('--working-dir <folder>', WORKING_DIR)
242
+ .option('--requirements-dir <folder>', REQUIREMENTS_DIR)
243
+ .option('--documents-dir <folder>', DOCUMENTS_DIR);
244
+ }
245
+ }
246
+ return { cli,
247
+ state };
248
+ }
249
+ /**
250
+ * Runs the `rq` command line and returns the exit code. `rq view`
251
+ * returns once the server listens, and adds it to `servers`; it serves until
252
+ * whoever passed `servers` closes it.
253
+ */
254
+ export async function runCli(args, io, servers = []) {
255
+ const { cli, state } = createCli(args, io, servers);
256
+ if (args.length === 0) {
257
+ cli.outputHelp();
258
+ return 0;
259
+ }
260
+ const logger = getLogger(io, 'rq');
261
+ logger.debug({ args }, 'command started');
262
+ takeWritten();
263
+ try {
264
+ await cli.parseAsync(args, { from: 'user' });
265
+ }
266
+ catch (error) {
267
+ if (error instanceof CommanderError) {
268
+ return error.exitCode;
269
+ }
270
+ io.stderr.write(`${error instanceof Error
271
+ ? error.message
272
+ : String(error)}\n`);
273
+ state.exitCode = 1;
274
+ }
275
+ try {
276
+ const processed = await postProcess({ ...state.io,
277
+ logger: getLogger(state.io, 'rq.post-process') }, takeWritten(), 'rq.json');
278
+ logger.debug({ exitCode: state.exitCode,
279
+ postProcessing: processed }, 'command finished');
280
+ return state.exitCode === 0
281
+ ? processed
282
+ : state.exitCode;
283
+ }
284
+ catch (error) {
285
+ io.stderr.write(`${error instanceof Error
286
+ ? error.message
287
+ : String(error)}\n`);
288
+ return 1;
289
+ }
290
+ }
291
+ function collect(value, previous) {
292
+ return [...previous,
293
+ value];
294
+ }
295
+ /**
296
+ * The `rq` executable: runs the command line with a logger provider made
297
+ * from `--loglevel`, `--logfile`, `--logformat` and the `RQ_LOG_`
298
+ * variables, and disposes it before it returns the exit code. A server that
299
+ * `view` started serves until SIGINT or SIGTERM first.
300
+ */
301
+ export async function main(args, io) {
302
+ const loggerProvider = createLoggerProvider('RQ_LOG_', readLoggerOptions(args));
303
+ const servers = [];
304
+ try {
305
+ const exitCode = await runCli(args, { ...io,
306
+ loggerProvider }, servers);
307
+ for (const server of servers) {
308
+ await untilStopped(server);
309
+ }
310
+ return exitCode;
311
+ }
312
+ finally {
313
+ await loggerProvider.dispose();
314
+ }
315
+ }