asljs-rq-cli 0.1.1 → 0.1.3

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/bin/rq-mcp.js DELETED
@@ -1,22 +0,0 @@
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/dist/mcp.d.ts DELETED
@@ -1,28 +0,0 @@
1
- import { type McpServerInfo, type McpTool } from 'asljs-mcp';
2
- import { Io } from 'asljs-rq';
3
- import { Server } from 'node:http';
4
- import { Readable } from 'node:stream';
5
- export declare const SERVER_NAME = "asljs-rq";
6
- /**
7
- * The tools of the MCP server: a tool per `rq` command, run as that command
8
- * line in `io.cwd`, or in its `workingDir`, with its output as the result.
9
- */
10
- export declare function createTools(io: Io, servers?: Server[]): McpTool[];
11
- /**
12
- * Serves the `rq` commands as MCP tools over a line-delimited JSON-RPC
13
- * stream, e.g. standard input and output. Resolves when the input ends,
14
- * and closes the servers `view` started.
15
- */
16
- export declare function runMcpServer(io: Io, input: Readable, write: (line: string) => void): Promise<void>;
17
- /**
18
- * The `rq-mcp` executable: serves the tools with a logger provider made
19
- * from `--loglevel`, `--logfile`, `--logformat` and the `RQ_LOG_`
20
- * variables, and disposes it once the input ends. Standard output carries the
21
- * protocol, so a log level without a log file other than stdout throws here,
22
- * before the first request is read.
23
- */
24
- export declare function main(args: string[], io: Io, input: Readable, write: (line: string) => void): Promise<void>;
25
- /**
26
- * The name and the version of the installed package.
27
- */
28
- export declare function serverInfo(): McpServerInfo;
package/dist/mcp.js DELETED
@@ -1,70 +0,0 @@
1
- import { createLoggerProvider, readLoggerOptions } from 'asljs-logging';
2
- import { commandTools, serveLines } from 'asljs-mcp';
3
- import { getLogger } from 'asljs-rq';
4
- import { createRequire } from 'node:module';
5
- import { createCli, runCli } from './cli.js';
6
- export const SERVER_NAME = 'asljs-rq';
7
- /**
8
- * The tools of the MCP server: a tool per `rq` command, run as that command
9
- * line in `io.cwd`, or in its `workingDir`, with its output as the result.
10
- */
11
- export function createTools(io, servers = []) {
12
- return commandTools(createCli([], io).cli, async (args) => {
13
- let stdout = '';
14
- let stderr = '';
15
- const exitCode = await runCli(args, { ...io,
16
- stdout: { write: (text) => {
17
- stdout += text;
18
- } },
19
- stderr: { write: (text) => {
20
- stderr += text;
21
- } } }, servers);
22
- return { exitCode,
23
- stdout,
24
- stderr };
25
- });
26
- }
27
- /**
28
- * Serves the `rq` commands as MCP tools over a line-delimited JSON-RPC
29
- * stream, e.g. standard input and output. Resolves when the input ends,
30
- * and closes the servers `view` started.
31
- */
32
- export async function runMcpServer(io, input, write) {
33
- const servers = [];
34
- try {
35
- await serveLines(input, write, createTools(io, servers), serverInfo(), { logger: getLogger(io, 'rq.mcp') });
36
- }
37
- finally {
38
- for (const server of servers) {
39
- await new Promise((resolve) => {
40
- server.close(resolve);
41
- server.closeAllConnections();
42
- });
43
- }
44
- }
45
- }
46
- /**
47
- * The `rq-mcp` executable: serves the tools with a logger provider made
48
- * from `--loglevel`, `--logfile`, `--logformat` and the `RQ_LOG_`
49
- * variables, and disposes it once the input ends. Standard output carries the
50
- * protocol, so a log level without a log file other than stdout throws here,
51
- * before the first request is read.
52
- */
53
- export async function main(args, io, input, write) {
54
- const loggerProvider = createLoggerProvider('RQ_LOG_', readLoggerOptions(args), { allowStdout: false });
55
- try {
56
- await runMcpServer({ ...io,
57
- loggerProvider }, input, write);
58
- }
59
- finally {
60
- await loggerProvider.dispose();
61
- }
62
- }
63
- /**
64
- * The name and the version of the installed package.
65
- */
66
- export function serverInfo() {
67
- const { version } = createRequire(import.meta.url)('../package.json');
68
- return { name: SERVER_NAME,
69
- version };
70
- }
@@ -1,41 +0,0 @@
1
- /**
2
- * The part of the diagram the window shows, in diagram coordinates: the SVG's
3
- * `viewBox`.
4
- */
5
- export interface ViewBox {
6
- x: number;
7
- y: number;
8
- width: number;
9
- height: number;
10
- }
11
- export interface Point {
12
- x: number;
13
- y: number;
14
- }
15
- /**
16
- * The view box zoomed by a wheel turn of `deltaY` around `point`, which stays
17
- * where it is on the screen: a turn down, a positive `deltaY`, zooms out.
18
- *
19
- * Its source is sent to the browser as part of the index page, so it uses
20
- * nothing but its arguments.
21
- */
22
- export declare function zoomAt(box: ViewBox, point: Point, deltaY: number): ViewBox;
23
- /**
24
- * The view box moved so that `grabbed`, the diagram point where a drag
25
- * started, is under the pointer, now over `point`.
26
- *
27
- * Its source is sent to the browser as part of the index page, so it uses
28
- * nothing but its arguments.
29
- */
30
- export declare function panTo(box: ViewBox, grabbed: Point, point: Point): ViewBox;
31
- /**
32
- * Whether pressing `button` on `target` starts a pan: the main button on
33
- * empty space, not on a node or a link, so that a click on a node still opens
34
- * its document.
35
- *
36
- * Its source is sent to the browser as part of the index page, so it uses
37
- * nothing but its arguments.
38
- */
39
- export declare function startsPan(button: number, target: {
40
- closest(selector: string): unknown;
41
- }): boolean;
@@ -1,39 +0,0 @@
1
- /**
2
- * The view box zoomed by a wheel turn of `deltaY` around `point`, which stays
3
- * where it is on the screen: a turn down, a positive `deltaY`, zooms out.
4
- *
5
- * Its source is sent to the browser as part of the index page, so it uses
6
- * nothing but its arguments.
7
- */
8
- export function zoomAt(box, point, deltaY) {
9
- const factor = Math.exp(deltaY * 0.0015);
10
- return { x: point.x - (point.x - box.x) * factor,
11
- y: point.y - (point.y - box.y) * factor,
12
- width: box.width * factor,
13
- height: box.height * factor };
14
- }
15
- /**
16
- * The view box moved so that `grabbed`, the diagram point where a drag
17
- * started, is under the pointer, now over `point`.
18
- *
19
- * Its source is sent to the browser as part of the index page, so it uses
20
- * nothing but its arguments.
21
- */
22
- export function panTo(box, grabbed, point) {
23
- return { x: box.x - (point.x - grabbed.x),
24
- y: box.y - (point.y - grabbed.y),
25
- width: box.width,
26
- height: box.height };
27
- }
28
- /**
29
- * Whether pressing `button` on `target` starts a pan: the main button on
30
- * empty space, not on a node or a link, so that a click on a node still opens
31
- * its document.
32
- *
33
- * Its source is sent to the browser as part of the index page, so it uses
34
- * nothing but its arguments.
35
- */
36
- export function startsPan(button, target) {
37
- return button === 0
38
- && !target.closest('.node, a');
39
- }
package/dist/view.d.ts DELETED
@@ -1,39 +0,0 @@
1
- import { type PostReply } from 'asljs-mdcli';
2
- import { Io } from 'asljs-rq';
3
- import { Server } from 'node:http';
4
- export { DEFAULT_PORT } from 'asljs-mdcli';
5
- export interface ViewOptions {
6
- /**
7
- * The requirement file or folder to view, relative to the working
8
- * directory, or an id or `.md` name; the requirements folder when absent.
9
- */
10
- target?: string;
11
- /**
12
- * The port to listen on, exactly; 0 picks a free one. When absent, the
13
- * first free port from `DEFAULT_PORT` on.
14
- */
15
- port?: number;
16
- host?: string;
17
- }
18
- /**
19
- * The body of a rendered document: a requirement or test starts with its
20
- * full path, which a click copies; any other document is its text alone.
21
- */
22
- export declare function renderDocument(file: string): Promise<string>;
23
- /**
24
- * Saves what the editor of a requirement or test sends to the document at
25
- * the URL's path in `folder`: its title and its text, unless the document
26
- * changed since the editor opened it; the ids of the requirements folder in
27
- * the text become links. Then the document is post-processed, as after a
28
- * command.
29
- */
30
- export declare function saveDocument(io: Io, folder: string, url: URL, body: unknown): Promise<PostReply>;
31
- /**
32
- * Starts a web server for a requirement file or folder. It serves the
33
- * documents folder, or the folder of the graph when none is given, which must
34
- * contain the graph's folder. `/` shows the graph, whose nodes open the
35
- * documents; a `.md` file of the served folder is shown rendered and any other
36
- * file as it is, so a document a requirement links to opens wherever it is in
37
- * the folder. The graph is reloaded on every request for `/`.
38
- */
39
- export declare function execView(io: Io, options: ViewOptions): Promise<Server>;
package/dist/view.js DELETED
@@ -1,321 +0,0 @@
1
- import { escapeHtml, markdownToHtml, page, postProcess, serverUrl, startServer, takeWritten, writeMarkdown } from 'asljs-mdcli';
2
- import { findTarget, getLogger, getNodeKind, getRequirementsDir, getResults, getVersion, isFile, isInside, loadGraph, readEditable, readIds, readImplementation, renderImplementationTable, toMermaid, writeEdited } from 'asljs-rq';
3
- import { readFile } from 'node:fs/promises';
4
- import path from 'node:path';
5
- import { panTo, startsPan, zoomAt } from './view-script.js';
6
- export { DEFAULT_PORT } from 'asljs-mdcli';
7
- const MERMAID_MODULE = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
8
- /**
9
- * The full path above a requirement or test, small and grey; a click copies
10
- * it.
11
- */
12
- const DOCUMENT_STYLE = `.rq-head { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; margin: 0 0 0.5rem; }
13
- .rq-path { margin: 0; font-family: ui-monospace, monospace; font-size: 0.8rem; color: #757575; cursor: copy; overflow-wrap: anywhere; }
14
- .rq-path.copied { color: #2e7d32; }
15
- .rq-title { display: flex; align-items: baseline; gap: 0.5rem; margin: 0.67em 0; font-size: 2em; font-weight: bold; }
16
- .rq-id { color: #757575; }
17
- #rq-title { flex: 1; min-width: 0; font: inherit; padding: 0 0.25rem; }
18
- #rq-editor { border: 1px solid #ddd; min-height: 300px; }
19
- .rq-note, #rq-message { color: #757575; font-size: 0.85rem; }
20
- .rq-implementation { border-collapse: collapse; }
21
- .rq-implementation th, .rq-implementation td { border-bottom: 1px solid #ddd; padding: 0.25rem 0.75rem 0.25rem 0; text-align: left; }
22
- .rq-pass { color: #2e7d32; }
23
- .rq-fail, .rq-missing { color: #c62828; }
24
- .rq-not-run { color: #757575; }`;
25
- /**
26
- * Stands for a requirement's Implementation list until the rest is rendered.
27
- */
28
- const TABLE_PLACEHOLDER = 'RQIMPLEMENTATIONTABLE';
29
- /**
30
- * The Milkdown editor, loaded when Edit is pressed: one module with its
31
- * dependencies, since separate modules can load ProseMirror twice, and its
32
- * theme.
33
- */
34
- const CREPE_MODULE = 'https://esm.sh/@milkdown/crepe@7.22.3?bundle';
35
- const CREPE_THEME = 'https://cdn.jsdelivr.net/npm/@milkdown/crepe@7.22.3/lib/theme';
36
- /**
37
- * Edit replaces the rendered document with a field for the title and a
38
- * Milkdown editor for the text, loaded only then; Save, or Ctrl+S, sends
39
- * both back with the version the page was made from, and reloads the page
40
- * once saved.
41
- */
42
- const EDIT_SCRIPT = `<script type="module">
43
- const data = JSON.parse(document.getElementById('rq-edit-data').textContent);
44
- const button = document.getElementById('rq-edit');
45
- const form = document.getElementById('rq-form');
46
- const title = document.getElementById('rq-title');
47
- const message = document.getElementById('rq-message');
48
- let crepe = null;
49
- button.addEventListener('click', async () => {
50
- button.disabled = true;
51
- button.textContent = 'Loading…';
52
- for (const sheet of [ 'common', 'frame' ]) {
53
- const link = document.createElement('link');
54
- link.rel = 'stylesheet';
55
- link.href = '${CREPE_THEME}/' + sheet + '/style.css';
56
- document.head.append(link);
57
- }
58
- try {
59
- const { Crepe } = await import('${CREPE_MODULE}');
60
- crepe = new Crepe({ root: document.getElementById('rq-editor'), defaultValue: data.body });
61
- await crepe.create();
62
- } catch (error) {
63
- button.disabled = false;
64
- button.textContent = 'Edit';
65
- message.textContent = 'The editor did not load: ' + error.message;
66
- return;
67
- }
68
- document.getElementById('rq-view').hidden = true;
69
- button.hidden = true;
70
- form.hidden = false;
71
- title.value = data.title;
72
- title.focus();
73
- });
74
- form.addEventListener('submit', async (event) => {
75
- event.preventDefault();
76
- message.textContent = 'Saving…';
77
- const response = await fetch(location.pathname, {
78
- method: 'POST',
79
- headers: { 'content-type': 'application/json' },
80
- body: JSON.stringify({ version: data.version, title: title.value, body: crepe.getMarkdown() })
81
- });
82
- const reply = await response.json().catch(() => ({ error: response.statusText }));
83
- if (response.ok) {
84
- location.reload();
85
- } else {
86
- message.textContent = reply.error;
87
- }
88
- });
89
- document.addEventListener('keydown', (event) => {
90
- if (!form.hidden && (event.ctrlKey || event.metaKey) && event.key === 's') {
91
- event.preventDefault();
92
- form.requestSubmit();
93
- }
94
- });
95
- document.getElementById('rq-cancel').addEventListener('click', () => location.reload());
96
- </script>`;
97
- /**
98
- * Copies the path a `.rq-path` holds when it is clicked, and shows it was
99
- * copied for a moment.
100
- */
101
- const COPY_SCRIPT = `<script>
102
- document.querySelector('.rq-path').addEventListener('click', async (event) => {
103
- const element = event.currentTarget;
104
- await navigator.clipboard.writeText(element.dataset.path);
105
- element.classList.add('copied');
106
- setTimeout(() => element.classList.remove('copied'), 1000);
107
- });
108
- </script>`;
109
- /**
110
- * The body of a rendered document: a requirement or test starts with its
111
- * full path, which a click copies; any other document is its text alone.
112
- */
113
- export async function renderDocument(file) {
114
- const text = await readFile(file, 'utf8');
115
- const kind = getNodeKind(file);
116
- if (kind === null) {
117
- return markdownToHtml(text);
118
- }
119
- // A requirement's Implementation list is shown as a table of what it
120
- // links to, with their coverage and result.
121
- const implementation = kind === 'requirement'
122
- ? await readImplementation(file, text, TABLE_PLACEHOLDER)
123
- : null;
124
- const html = implementation === null
125
- ? markdownToHtml(text)
126
- : markdownToHtml(implementation.text)
127
- .replace(`<p>${TABLE_PLACEHOLDER}</p>`, renderImplementationTable(implementation.rows));
128
- const editable = readEditable(file, text);
129
- // The editor's data, safe inside a script element.
130
- const data = JSON.stringify(editable)
131
- .replace(/</g, '\\u003c');
132
- return `<div class="rq-head">
133
- <p class="rq-path" data-path="${escapeHtml(file)}" title="Copy the path">${escapeHtml(file)}</p>
134
- <p><span id="rq-message"></span> <button type="button" id="rq-edit">Edit</button></p>
135
- </div>
136
- <div id="rq-view">
137
- ${html}
138
- </div>
139
- <form id="rq-form" hidden>
140
- <p class="rq-title"><span class="rq-id">${escapeHtml(editable.id)}</span><input id="rq-title" aria-label="Title"></p>
141
- <div id="rq-editor"></div>
142
- <p class="rq-note">Implementation, Coverage and Status are kept as they are, after the text. Ids such as R2 or T3 become links when saved.</p>
143
- <p><button type="submit">Save</button> <button type="button" id="rq-cancel">Cancel</button></p>
144
- </form>
145
- <script type="application/json" id="rq-edit-data">${data}</script>
146
- ${COPY_SCRIPT}
147
- ${EDIT_SCRIPT}`;
148
- }
149
- /**
150
- * Saves what the editor of a requirement or test sends to the document at
151
- * the URL's path in `folder`: its title and its text, unless the document
152
- * changed since the editor opened it; the ids of the requirements folder in
153
- * the text become links. Then the document is post-processed, as after a
154
- * command.
155
- */
156
- export async function saveDocument(io, folder, url, body) {
157
- const pathname = decodeURIComponent(url.pathname);
158
- const file = path.resolve(folder, `.${pathname}`);
159
- if (!isInside(folder, file)
160
- || getNodeKind(file)
161
- === null
162
- || !await isFile(file)) {
163
- return { status: 404,
164
- body: { error: `No requirement or test at ${pathname}.` } };
165
- }
166
- if (!isEdit(body)) {
167
- return { status: 400,
168
- body: { error: 'Send the version, the title and the text.' } };
169
- }
170
- const text = await readFile(file, 'utf8');
171
- if (getVersion(text)
172
- !== body.version) {
173
- return { status: 409,
174
- body: { error: 'The document changed since the editor opened it; copy your text, reload the page and edit again.' } };
175
- }
176
- await writeMarkdown(file, writeEdited(file, text, body, await readIds(getRequirementsDir(io))));
177
- const processed = await postProcess({ ...io,
178
- logger: getLogger(io, 'rq.post-process') }, takeWritten(), 'rq.json');
179
- return { status: 200,
180
- body: processed === 0
181
- ? { saved: true }
182
- : { saved: true,
183
- warning: 'markdownPostProcessing failed; see the server output.' } };
184
- }
185
- function isEdit(body) {
186
- return typeof body === 'object'
187
- && body !== null
188
- && typeof body.version === 'string'
189
- && typeof body.title === 'string'
190
- && typeof body.body === 'string';
191
- }
192
- /**
193
- * The index is the diagram alone, filling the window.
194
- */
195
- const INDEX_STYLE = `html, body { height: 100%; }
196
- body { max-width: none; margin: 0; padding: 0; overflow: hidden; }
197
- #diagram { position: fixed; inset: 0; cursor: grab; touch-action: none; user-select: none; -webkit-user-select: none; }
198
- #diagram.panning { cursor: grabbing; }
199
- #diagram > pre.mermaid { margin: 0; width: 100%; height: 100%; }
200
- #diagram svg { display: block; width: 100%; height: 100%; max-width: none !important; }
201
- #diagram .node { cursor: pointer; }
202
- #problems { position: fixed; top: 0.5rem; right: 0.5rem; max-width: 40%; max-height: 40%; overflow: auto; background: #fff8e1; border: 1px solid #ef8f00; padding: 0 0.75rem; font-size: 0.875rem; }
203
- #problems h2 { font-size: 1rem; }`;
204
- /**
205
- * Draws the diagram, then zooms it with the mouse wheel around the pointer and
206
- * pans it by dragging empty space. A click on a node opens its document, through
207
- * the `click` links of the diagram. The geometry is `zoomAt`, `panTo` and
208
- * `startsPan`, sent as their own source.
209
- */
210
- const SCRIPT = `import mermaid from '${MERMAID_MODULE}';
211
- ${zoomAt.toString()}
212
- ${panTo.toString()}
213
- ${startsPan.toString()}
214
- mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
215
- await mermaid.run({ querySelector: '#diagram .mermaid' });
216
- const box = document.getElementById('diagram');
217
- const svg = box.querySelector('svg');
218
- svg.removeAttribute('width');
219
- svg.removeAttribute('height');
220
- const { x, y, width, height } = svg.viewBox.baseVal;
221
- let view = { x, y, width, height };
222
- const show = () =>
223
- svg.setAttribute('viewBox', \`\${view.x} \${view.y} \${view.width} \${view.height}\`);
224
- const toDiagram = (event) =>
225
- new DOMPoint(event.clientX, event.clientY).matrixTransform(svg.getScreenCTM().inverse());
226
- box.addEventListener('wheel', (event) => {
227
- event.preventDefault();
228
- view = zoomAt(view, toDiagram(event), event.deltaY);
229
- show();
230
- }, { passive: false });
231
- let drag = null;
232
- box.addEventListener('pointerdown', (event) => {
233
- if (!startsPan(event.button, event.target)) {
234
- return;
235
- }
236
- // Without this, the browser starts selecting the diagram's text.
237
- event.preventDefault();
238
- drag = { pointer: event.pointerId, grabbed: toDiagram(event) };
239
- box.setPointerCapture(event.pointerId);
240
- box.classList.add('panning');
241
- });
242
- box.addEventListener('pointermove', (event) => {
243
- if (drag?.pointer !== event.pointerId) {
244
- return;
245
- }
246
- view = panTo(view, drag.grabbed, toDiagram(event));
247
- show();
248
- });
249
- const stop = (event) => {
250
- if (drag?.pointer === event.pointerId) {
251
- drag = null;
252
- box.classList.remove('panning');
253
- }
254
- };
255
- box.addEventListener('pointerup', stop);
256
- box.addEventListener('pointercancel', stop);`;
257
- /**
258
- * Starts a web server for a requirement file or folder. It serves the
259
- * documents folder, or the folder of the graph when none is given, which must
260
- * contain the graph's folder. `/` shows the graph, whose nodes open the
261
- * documents; a `.md` file of the served folder is shown rendered and any other
262
- * file as it is, so a document a requirement links to opens wherever it is in
263
- * the folder. The graph is reloaded on every request for `/`.
264
- */
265
- export async function execView(io, options) {
266
- const target = await findTarget(io, options.target);
267
- const graphFolder = (await loadGraph(target)).folder;
268
- const folder = io.documentsDir ?? graphFolder;
269
- if (!isInside(folder, graphFolder)) {
270
- throw new Error(`${graphFolder} is not inside the documents folder ${folder}.`);
271
- }
272
- const server = await startServer({ folder,
273
- index: async () => {
274
- const graph = await loadGraph(target);
275
- return renderIndex(folder, graph, getResults(graph));
276
- },
277
- home: 'Requirements',
278
- style: DOCUMENT_STYLE,
279
- render: renderDocument,
280
- post: async (url, body) => saveDocument(io, folder, url, body),
281
- port: options.port,
282
- host: options.host,
283
- logger: getLogger(io, 'rq.view') });
284
- io.stdout.write(`Serving ${folder === graphFolder
285
- ? folder
286
- : `${graphFolder} in ${folder}`} at ${serverUrl(server)}\n`);
287
- return server;
288
- }
289
- function renderIndex(folder, graph, results) {
290
- const href = (file) => {
291
- if (!isInside(folder, file)) {
292
- return null;
293
- }
294
- return `/${encodeURI(path.relative(folder, file)
295
- .split(path.sep)
296
- .join('/'))}`;
297
- };
298
- const title = (graph.roots.length === 1
299
- ? graph.nodes.get(graph.roots[0])?.title
300
- : null)
301
- ?? 'Requirements';
302
- const problems = graph.errors.length === 0
303
- ? ''
304
- : `<aside id="problems">
305
- <h2>Problems</h2>
306
- <ul>
307
- ${graph.errors
308
- .map(error => `<li>${escapeHtml(error)}</li>`)
309
- .join('\n')}
310
- </ul>
311
- </aside>
312
- `;
313
- return page(title, `<main id="diagram">
314
- <pre class="mermaid">
315
- ${escapeHtml(toMermaid(graph, href, results))}
316
- </pre>
317
- </main>
318
- ${problems}<script type="module">
319
- ${SCRIPT}
320
- </script>`, INDEX_STYLE);
321
- }
package/docs/rq view.md DELETED
@@ -1,101 +0,0 @@
1
- # rq view
2
-
3
- Starts a web server for a requirement file or folder, and runs until stopped, by
4
- SIGINT (Ctrl+C) or SIGTERM.
5
-
6
- ```text
7
- rq view [path] [--port <port>] [--working-dir <folder>]
8
- [--requirements-dir <folder>] [--documents-dir <folder>]
9
- ```
10
-
11
- `[path]` is a requirement file, a folder, a `.md` name or an id; the
12
- requirements folder by default. The index shows its graph.
13
-
14
- The server serves the documents folder; see
15
- [Requirements and documents folders][FD]. When none is set, it serves the folder
16
- of the graph: `[path]` when it is a folder, the file's folder otherwise. The
17
- graph's folder must be inside the served folder; `rq view` reports an error
18
- otherwise. It listens on `127.0.0.1`.
19
-
20
- ```text
21
- rq view requirements --documents-dir .
22
- ```
23
-
24
- serves the whole project, so a requirement's link to `docs/design.md` opens
25
- rendered, and the index still shows the graph of `requirements`.
26
-
27
- - `/` - the index: the graph as a left-to-right Mermaid diagram of the
28
- requirements and tests and their `## Implementation` links, filling the
29
- window, and nothing else; the structure errors, when there are any, are listed
30
- in a box over its corner. Other documents are not drawn. Each node's border
31
- shows its state, from its `## Status` section:
32
- - colour, from `Result`, or for a requirement that records none the result
33
- that follows from its links - neutral grey for `NOT RUN`, green for `PASS`,
34
- red for a test that is `FAIL`, amber for a requirement that is `FAIL`
35
- because of what it links to;
36
- - style, from `Coverage`, which `rq coverage` writes - solid for a test and
37
- for a requirement that is `COMPLETE`, dashed for one that is `INCOMPLETE`,
38
- dotted for one without `Coverage`.
39
-
40
- The mouse wheel zooms in and out around the pointer, and dragging empty space
41
- pans. Clicking a node opens its document, at its path in the served folder,
42
- e.g. `/requirements/R1 Export.md`. All roots are in the one diagram; the page
43
- is titled with the root's heading when there is one root, `Requirements`
44
- otherwise. The graph is read again on every request, so a reload shows the
45
- current files.
46
- - `/<file>.md` - the document rendered as HTML. Its links to other `.md` files
47
- open them rendered. A requirement or test starts with its full path, small and
48
- grey; a click copies it to the clipboard. A requirement's `## Implementation`
49
- list is shown as a table of what it links to: each one's id and title, linking
50
- to it, the `Coverage` its `## Status` records, as Complete or Partial, and its
51
- `Result`, `NOT RUN` when it records none; an item whose document is missing
52
- says so.
53
- - any other file of the served folder is served as it is, e.g. an image a
54
- document shows.
55
-
56
- The server is for local use: it serves every file inside the served folder,
57
- including hidden ones, and renders the documents' HTML as it is. It takes a save
58
- only as JSON, sent to `localhost` or its address from its own pages, so another
59
- site cannot write through it.
60
-
61
- Nodes outside the served folder are drawn but do not open. The diagram is drawn
62
- by Mermaid, loaded from `cdn.jsdelivr.net`; without it the index shows the
63
- diagram's Mermaid text.
64
-
65
- ## Editing
66
-
67
- A requirement or test has an Edit button next to its path. It opens:
68
-
69
- - the title, in a field after the id, which stays as it is; the file keeps its
70
- name, which [`rq move`][MV] changes;
71
- - the rest of the text in a [Milkdown][MK] editor, without `## Implementation`,
72
- `## Coverage` and `## Status`, which the commands own. A link to a requirement
73
- or test by its id shows as the id alone, e.g. `R2`, and any other reference
74
- link as an inline link.
75
-
76
- Save, or Ctrl+S, writes the heading, the id and the title, then the text, then
77
- the definitions the owned sections use and the owned sections as they were. Each
78
- id of a requirement or test of the requirements folder in the text, outside
79
- code, becomes a reference link to it again, e.g. `[R2][]`, with its definition
80
- after the text. The document is then post-processed with
81
- `markdownPostProcessing` of `rq.json`, as after a command. A save is refused
82
- when the file changed since the editor opened it: reload the page and edit
83
- again. Cancel reloads the page. Milkdown is loaded from `esm.sh`, and its theme
84
- from `cdn.jsdelivr.net`, when Edit is pressed.
85
-
86
- ## Options
87
-
88
- - `--port <port>` - the port, exactly: one in use is an error, and 0 picks any
89
- free one. Without it, the first free port from 3000 on, up to 3099. The
90
- address is printed when the server starts.
91
- - `--working-dir <folder>` - the working folder, whose `.rq` folder holds the
92
- results; see [Working folder][WF]. The results are read again on every request
93
- for `/`.
94
- - `--requirements-dir <folder>`, `--documents-dir <folder>` - the requirements
95
- folder, the default `[path]`, and the documents folder, the served one; see
96
- [Requirements and documents folders][FD].
97
-
98
- [FD]: ../../../libs/rq/docs/Requirements.md#requirements-and-documents-folders
99
- [WF]: ../../../libs/rq/docs/Requirements.md#working-folder
100
- [MK]: https://milkdown.dev
101
- [MV]: <Changing the graph.md#rq-move>