asljs-rq-cli 0.0.0-stage → 0.1.2

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,51 @@
1
+ # Permissive No-Derivatives License 1.0
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 (the "Software") to:
7
+
8
+ 1. Use the Software for any purpose, including commercial purposes.
9
+ 2. Copy and redistribute the Software, including as part of another application,
10
+ library, or package.
11
+ 3. Modify the Software for private or internal use.
12
+
13
+ Subject to the following conditions:
14
+
15
+ ## 1. No Distribution of Modified Versions
16
+
17
+ Modified versions of the Software, or substantial portions thereof, must not be
18
+ distributed without prior written permission from the copyright holder.
19
+
20
+ Routine build transformations, including minification, transpilation, and
21
+ bundling, are permitted for distribution, provided they do not intentionally
22
+ alter the Software's functionality.
23
+
24
+ ## 2. Licence and Copyright Preservation
25
+
26
+ All distributed copies of the Software must retain this copyright notice and
27
+ licence.
28
+
29
+ The Software itself, including copies incorporated into other applications,
30
+ libraries, or packages, remains subject to this licence.
31
+
32
+ The Software must not be sublicensed or redistributed under a different licence.
33
+
34
+ ## 3. Use in Other Software
35
+
36
+ Applications and libraries that merely use, import, link to, or bundle the
37
+ Software are not considered modified versions of the Software and may be
38
+ distributed under any licence.
39
+
40
+ Such use, incorporation, or bundling does not grant permission to relicense the
41
+ Software or distribute modified versions of it.
42
+
43
+ ### 4. No Warranty
44
+
45
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
46
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
47
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
48
+
49
+ IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR
50
+ OTHER LIABILITY ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE OR ITS
51
+ USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,225 @@
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][MCP] server, `asljs-rq-mcp`,
53
+ serves the same commands as MCP tools, and `rq view` runs the web server of
54
+ [asljs-rq-srv][SRV]. An application that wants the functions without a command
55
+ line imports `asljs-rq` instead.
56
+
57
+ ## Scope
58
+
59
+ - Requirements and tests are plain markdown files, kept with the code.
60
+ - The graph holds requirements, `R<n> <name>.md`, and tests, `T<n> <name>.md`,
61
+ and nothing else; its edges are the links in each requirement's `##
62
+ Implementation` list. Other documents are not part of the graph, though `rq
63
+ remove` and `rq move` fix the links to what they remove or move in every `.md`
64
+ file of the documents folder.
65
+ - `rq.json` or `--requirements-dir` and `--documents-dir` set where the
66
+ requirements are and where every document is; by default both are the working
67
+ folder.
68
+ - Steps run in order, in the test's folder, until the first that fails. An
69
+ instruction step needs an AI agent, `claude` or `copilot`; the first one
70
+ installed is used unless `--ai` names one.
71
+ - The view draws the graph with `asljs-ui-diagram`, served by `rq view` itself,
72
+ so it needs no network; it has a search over the nodes' titles.
73
+
74
+ ## Installation
75
+
76
+ ```bash
77
+ npm install asljs-rq-cli
78
+ ```
79
+
80
+ ## Usage
81
+
82
+ Every command takes `--loglevel <level>`, `--logfile <target>` and `--logformat
83
+ <format>`, or `RQ_LOG_LEVEL`, `RQ_LOG_FILE` and `RQ_LOG_FORMAT`, to log what it
84
+ does, e.g. `--loglevel debug --logfile stderr`; nothing is logged without them.
85
+
86
+ Build the graph with the commands, and write the statements, descriptions and
87
+ steps as text:
88
+
89
+ ```bash
90
+ npx rq add requirement "requirements/R1 Export.md" "CSV export" --statement "The report exports as CSV."
91
+ 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"'
92
+ ```
93
+
94
+ The parent, `requirements/R1 Export.md`, now lists them:
95
+
96
+ ```markdown
97
+ # R1 Export
98
+
99
+ The application exports a report as CSV and as PDF.
100
+
101
+ ## Implementation
102
+
103
+ - [R2 CSV export](<R2 CSV export.md>)
104
+ - [T1 PDF export](<tests/T1 PDF export.md>)
105
+ ```
106
+
107
+ Query the structure:
108
+
109
+ ```bash
110
+ npx rq links "requirements/R1 Export.md" --json
111
+ npx rq backlinks "requirements/tests/T1 PDF export.md"
112
+ npx rq tojson requirements
113
+ ```
114
+
115
+ Check it, then run the test:
116
+
117
+ ```bash
118
+ npx rq check requirements
119
+ npx rq test requirements
120
+ ```
121
+
122
+ ```text
123
+ PASS requirements/tests/T1 PDF export.md - 1 step
124
+ FAIL requirements/R1 Export.md - 1 of 2 links failed: R2 CSV export: links to no requirement or test
125
+ FAIL requirements/R2 CSV export.md - links to no requirement or test
126
+ Results .rq/E1 requirements.md
127
+ ```
128
+
129
+ The execution file records the run and the working directory, then each test:
130
+
131
+ ````markdown
132
+ # E1 requirements
133
+
134
+ - Date: 2026-10-08T09:30:00.000Z
135
+ - Command: `rq test requirements`
136
+ - Result: PASS - 1 of 1 tests passed
137
+ - Commit: 3f2a9c1d0b8e7f6a5c4d3e2f1a0b9c8d7e6f5a4b
138
+ - Branch: main
139
+ - Changed files: none
140
+
141
+ ## T1 PDF export
142
+
143
+ - File: <requirements/tests/T1 PDF export.md>
144
+ - Result: PASS - 1 step
145
+
146
+ ```text
147
+ [step 1] Step 1
148
+ $ npm test -- --test-name-pattern="PDF export"
149
+ ...
150
+ ```
151
+ ````
152
+
153
+ Run a single requirement and everything below it by id, from any folder:
154
+
155
+ ```bash
156
+ npx rq test R1 --recurse --working-dir path/to/project
157
+ ```
158
+
159
+ Browse the graph and the documents:
160
+
161
+ ```bash
162
+ npx rq view requirements --port 8080
163
+ ```
164
+
165
+ With `rq.json` at the project root, the commands default to the requirements
166
+ folder, and the view serves the whole project:
167
+
168
+ ```json
169
+ {
170
+ "requirementsDir": "requirements",
171
+ "documentsDir": "."
172
+ }
173
+ ```
174
+
175
+ ```bash
176
+ npx rq view
177
+ npx rq config
178
+ ```
179
+
180
+ ## Further reading
181
+
182
+ - [Requirement files][RF] - the formats of requirements, tests, `## Status`
183
+ sections and execution files, and the rules every change keeps.
184
+ - [Requirements][RM] - how `rq` reads and writes them, and how the graph is
185
+ built.
186
+ - [Querying the graph][QG] and [Changing the graph][CG] - the structural
187
+ commands.
188
+ - [rq check][CK], [rq test][TS], [rq coverage][CV], [rq view][VW] and
189
+ [rq config][CF] - checking, testing, checking coverage, viewing and the
190
+ configuration.
191
+ - [rq agents][AG] - the instructions for an AI agent that edits the files
192
+ without `rq`.
193
+ - [rq-mcp][MC] - the MCP server with a tool per command.
194
+ - [Skills][SK] - the AI skills for managing requirements.
195
+
196
+ Questions and bugs: [asljs/issues][IS].
197
+
198
+ ## Related packages
199
+
200
+ - [rq library][RQL] - the requirements model and the commands as functions.
201
+ - [asljs-rq-mcp][MCP] - the `rq-mcp` MCP server.
202
+ - [asljs-rq-srv][SRV] - the `rq view` web server.
203
+ - `asljs-part` defines project artefacts in markdown and checks them with rules.
204
+
205
+ ## License
206
+
207
+ MIT
208
+
209
+ [#1]: https://github.com/AlexandriteSoftware/asljs
210
+ [AG]: <docs/rq agents.md>
211
+ [CF]: <docs/rq config.md>
212
+ [CG]: <docs/Changing the graph.md>
213
+ [CK]: <docs/rq check.md>
214
+ [CV]: <docs/rq coverage.md>
215
+ [IS]: https://github.com/AlexandriteSoftware/asljs/issues
216
+ [MC]: ../rq-mcp/docs/rq-mcp.md
217
+ [MCP]: ../rq-mcp/README.md
218
+ [QG]: <docs/Querying the graph.md>
219
+ [RF]: <../rq/docs/Requirement files.md>
220
+ [RM]: ../rq/docs/Requirements.md
221
+ [RQL]: ../rq/README.md
222
+ [SK]: skills
223
+ [SRV]: ../rq-srv/README.md
224
+ [TS]: <docs/rq test.md>
225
+ [VW]: <../rq-srv/docs/rq view.md>
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 { execView } from 'asljs-rq-srv';
6
+ import { Command, CommanderError } from 'commander';
7
+ import path from 'node:path';
8
+ import { execConfig } from './config.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
+ }