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 +51 -0
- package/README.md +224 -2
- package/bin/rq.js +12 -0
- package/dist/cli.d.ts +38 -0
- package/dist/cli.js +315 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.js +78 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/docs/Changing the graph.md +108 -0
- package/docs/Querying the graph.md +71 -0
- package/docs/rq agents.md +29 -0
- package/docs/rq check.md +32 -0
- package/docs/rq config.md +60 -0
- package/docs/rq coverage.md +80 -0
- package/docs/rq test.md +124 -0
- package/package.json +71 -4
- package/skills/requirements-authoring.md +82 -0
- package/skills/requirements-change.md +51 -0
- package/skills/requirements-coverage.md +48 -0
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
|
-
#
|
|
1
|
+
# rq
|
|
2
2
|
|
|
3
|
-
|
|
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
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
|
+
}
|