asljs-rq-srv 0.1.1 → 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/README.md CHANGED
@@ -5,33 +5,50 @@
5
5
 
6
6
  ## Overview
7
7
 
8
- `asljs-rq-srv` is the `rq view` web server: an editor with the requirements
9
- graph as a diagram, the documents rendered, and an editor that saves them. `rq
10
- view` of `asljs-rq-cli` runs it.
8
+ `asljs-rq-srv` is the `rq-srv` web server: an editor with the requirements graph
9
+ as a diagram and the documents, rendered and edited in the browser from the data
10
+ the server sends.
11
11
 
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- npm install asljs-rq-srv
15
+ npm install --global asljs-rq-srv
16
16
  ```
17
17
 
18
18
  ## Usage
19
19
 
20
+ ```bash
21
+ rq-srv
22
+ ```
23
+
24
+ serves every project of the current folder, each folder with an `rq.json`, in
25
+ one editor with a list box to switch between them, until stopped with Ctrl+C.
26
+
27
+ ```bash
28
+ rq-srv --working-dir ~/work --port 8080
29
+ ```
30
+
31
+ serves the projects of `~/work` on port 8080. Each project's folders come from
32
+ its own `rq.json`; `rq-srv` takes no folder option of `rq`.
33
+
20
34
  ```ts
21
- import { execView } from 'asljs-rq-srv';
35
+ import { createApp } from 'asljs-rq-srv';
22
36
  ```
23
37
 
24
- `execView(io, options)` takes the `Io` of `asljs-rq` and the view options, among
25
- them the graph's path and the port, starts the server and returns it once it
26
- listens.
38
+ `createApp(context, options?)` takes an `EditorAppContext` of `asljs-appbase`,
39
+ `{ cwd, base, env, loggerProvider, files? }`, whose `cwd` the projects are
40
+ looked for in, and `{ stderr }`, where a failed post-processing is reported, and
41
+ returns the `EditorApp` whose `handler` serves the editor below `base`, for a
42
+ server of its own or one that serves several applications.
27
43
 
28
44
  ## Further reading
29
45
 
30
- - [rq view][DOC] - what the server serves, and editing.
46
+ - [rq-srv][DOC] - the command, what the server serves, and editing.
31
47
 
32
48
  ## Related packages
33
49
 
34
50
  - [asljs-rq-cli][CLI] - the `rq` command line.
51
+ - [asljs-rq-mcp][MCP] - the `rq-mcp` MCP server.
35
52
  - [asljs-rq][RQL] - the requirements model and the commands as functions.
36
53
 
37
54
  ## License
@@ -40,6 +57,7 @@ See [LICENSE][LIC] for details.
40
57
 
41
58
  [#1]: https://github.com/AlexandriteSoftware/asljs
42
59
  [CLI]: ../rq-cli/README.md
43
- [DOC]: <docs/rq view.md>
60
+ [DOC]: docs/rq-srv.md
44
61
  [LIC]: LICENSE.md
62
+ [MCP]: ../rq-mcp/README.md
45
63
  [RQL]: ../rq/README.md
package/bin/rq-srv.js ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { main }
4
+ from '../dist/main.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 });
@@ -0,0 +1,54 @@
1
+ import { type GraphDiagram, type ImplementationRow } from 'asljs-rq';
2
+ /**
3
+ * What `_/data.json` of a project answers: the title of the graph, its
4
+ * structure errors, and the diagram, whose nodes link to their documents by
5
+ * paths relative to the project's base.
6
+ */
7
+ export interface GraphReply {
8
+ title: string;
9
+ problems: string[];
10
+ diagram: GraphDiagram;
11
+ }
12
+ /**
13
+ * What the editor of a requirement or test edits: the id, which stays, the
14
+ * title after it, and the text without the sections the commands own; a save
15
+ * sends `version` back.
16
+ */
17
+ export interface EditableReply {
18
+ id: string;
19
+ title: string;
20
+ body: string;
21
+ version: string;
22
+ }
23
+ /**
24
+ * A requirement's `## Implementation` list: the rows of the documents it
25
+ * links to, shown as a table where the paragraph `placeholder` stands in the
26
+ * text.
27
+ */
28
+ export interface ImplementationReply {
29
+ placeholder: string;
30
+ rows: ImplementationRow[];
31
+ }
32
+ /**
33
+ * What `_/document.json?path=<path>` answers for a markdown document of the
34
+ * served folder: its path as asked, its full path, and its text. A
35
+ * requirement or test also has `editable`; a requirement with an
36
+ * `## Implementation` list has `implementation`, its list replaced in `text`
37
+ * by the placeholder.
38
+ */
39
+ export interface DocumentReply {
40
+ path: string;
41
+ file: string;
42
+ text: string;
43
+ editable?: EditableReply;
44
+ implementation?: ImplementationReply;
45
+ }
46
+ /**
47
+ * What a `POST` of `{ path, version, title, body }` to `_/document.json`
48
+ * answers when it saved the document: a warning when the post-processing
49
+ * failed.
50
+ */
51
+ export interface SaveReply {
52
+ saved: true;
53
+ warning?: string;
54
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { type ImplementationRow } from 'asljs-rq';
2
+ import { type DocumentReply } from './data.js';
3
+ /**
4
+ * The rendered text of a document of `_/document.json`: its markdown, with
5
+ * the paragraph that stands for a requirement's Implementation list replaced
6
+ * by the table of its rows.
7
+ */
8
+ export declare function renderText(reply: DocumentReply): DocumentFragment;
9
+ /**
10
+ * The table of a requirement's Implementation list: each linked document's id
11
+ * and title, linking to it by its path from the requirement, the coverage and
12
+ * the result it records; a missing document is marked missing, and an item
13
+ * that links to no requirement or test shows its text.
14
+ */
15
+ export declare function implementationTable(rows: readonly ImplementationRow[]): HTMLTableElement;
16
+ /**
17
+ * The full path of a requirement or test, which a click copies, showing it
18
+ * was copied for a moment.
19
+ */
20
+ export declare function pathLine(file: string): HTMLElement;
@@ -0,0 +1,96 @@
1
+ import { element } from 'asljs-ui-editor';
2
+ import { markdownToHtml } from 'asljs-ui-markdown';
3
+ /**
4
+ * How long the path line shows that it was copied, in milliseconds.
5
+ */
6
+ const COPIED_MS = 1000;
7
+ /**
8
+ * The rendered text of a document of `_/document.json`: its markdown, with
9
+ * the paragraph that stands for a requirement's Implementation list replaced
10
+ * by the table of its rows.
11
+ */
12
+ export function renderText(reply) {
13
+ const template = element('template');
14
+ template.innerHTML =
15
+ markdownToHtml(reply.text);
16
+ const content = template.content;
17
+ if (reply.implementation
18
+ !== undefined) {
19
+ const { placeholder, rows } = reply.implementation;
20
+ for (const paragraph of content.querySelectorAll('p')) {
21
+ if (paragraph.textContent
22
+ === placeholder) {
23
+ paragraph.replaceWith(implementationTable(rows));
24
+ }
25
+ }
26
+ }
27
+ return content;
28
+ }
29
+ /**
30
+ * The table of a requirement's Implementation list: each linked document's id
31
+ * and title, linking to it by its path from the requirement, the coverage and
32
+ * the result it records; a missing document is marked missing, and an item
33
+ * that links to no requirement or test shows its text.
34
+ */
35
+ export function implementationTable(rows) {
36
+ const table = element('table', 'rq-implementation');
37
+ const headings = element('tr');
38
+ for (const heading of ['ID',
39
+ 'Title',
40
+ 'Coverage',
41
+ 'Result']) {
42
+ headings.append(element('th', undefined, heading));
43
+ }
44
+ const head = element('thead');
45
+ head.append(headings);
46
+ const body = element('tbody');
47
+ for (const row of rows) {
48
+ body.append(implementationRow(row));
49
+ }
50
+ table.append(head, body);
51
+ return table;
52
+ }
53
+ function implementationRow(row) {
54
+ const cell = (content, className) => {
55
+ const made = element('td', className);
56
+ made.append(content);
57
+ return made;
58
+ };
59
+ const link = (text) => {
60
+ if (row.href === null
61
+ || text === '') {
62
+ return text;
63
+ }
64
+ const anchor = element('a', undefined, text);
65
+ anchor.setAttribute('href', encodeURI(row.href));
66
+ return anchor;
67
+ };
68
+ const tr = element('tr', row.missing
69
+ ? 'rq-missing'
70
+ : undefined);
71
+ tr.append(cell(link(row.id)), cell(row.missing
72
+ ? 'missing'
73
+ : link(row.title)), cell(row.coverage), cell(row.result, row.result === ''
74
+ ? undefined
75
+ : `rq-${row.result
76
+ .toLowerCase()
77
+ .replace(' ', '-')}`));
78
+ return tr;
79
+ }
80
+ /**
81
+ * The full path of a requirement or test, which a click copies, showing it
82
+ * was copied for a moment.
83
+ */
84
+ export function pathLine(file) {
85
+ const line = element('p', 'rq-path', file);
86
+ line.title = 'Copy the path';
87
+ line.addEventListener('click', () => {
88
+ void navigator.clipboard
89
+ .writeText(file)
90
+ .then(() => {
91
+ line.classList.add('copied');
92
+ setTimeout(() => line.classList.remove('copied'), COPIED_MS);
93
+ });
94
+ });
95
+ return line;
96
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The icon of the application, drawn in the text colour.
3
+ */
4
+ export declare const ICONS: {
5
+ graph: string;
6
+ };
@@ -0,0 +1,4 @@
1
+ /**
2
+ * The icon of the application, drawn in the text colour.
3
+ */
4
+ export const ICONS = { graph: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="6" cy="12" r="2.5"/><circle cx="18" cy="6" r="2.5"/><circle cx="18" cy="18" r="2.5"/><path d="m8.2 10.9 7.6-3.8M8.2 13.1l7.6 3.8"/></svg>' };
@@ -9,9 +9,6 @@ export interface OutlineItem {
9
9
  * each followed by what it links to, in link order. A node linked from several,
10
10
  * such as a test, is listed under the first that reaches it; a node no root
11
11
  * reaches, as in a cycle, is listed at the top.
12
- *
13
- * Its source is sent to the browser as part of the index page, so it uses
14
- * nothing but its arguments.
15
12
  */
16
13
  export declare function buildOutline(nodes: readonly {
17
14
  id: string;
@@ -3,9 +3,6 @@
3
3
  * each followed by what it links to, in link order. A node linked from several,
4
4
  * such as a test, is listed under the first that reaches it; a node no root
5
5
  * reaches, as in a cycle, is listed at the top.
6
- *
7
- * Its source is sent to the browser as part of the index page, so it uses
8
- * nothing but its arguments.
9
6
  */
10
7
  export function buildOutline(nodes, links) {
11
8
  const labels = new Map(nodes.map(node => [node.id,
@@ -0,0 +1,21 @@
1
+ import { type EditorPluginFactory } from 'asljs-ui-editor';
2
+ /**
3
+ * The plugin of a project, the browser module of `asljs-ui-editor` the
4
+ * server serves. It loads the graph from `_/data.json` of the project's
5
+ * `base`. The graph is the first document, `rq:graph`, and stays open; a
6
+ * double click on a node opens its document as another tab,
7
+ * `rq:doc:<path>`. The diagram's selection is the editor's: a click on a node
8
+ * selects it, and so does choosing it in the Nodes list, which also moves the
9
+ * view to it. The graph's preview, on its right, views or edits the selected
10
+ * node, as the View and Edit tabs of its action bar choose. The diagram is
11
+ * read-only: nodes move, but delete and resize do nothing. The search finds
12
+ * the nodes by their titles, and opens their documents.
13
+ *
14
+ * A document is rendered here from `_/document.json`: a requirement's
15
+ * Implementation list as a table, a requirement or test as a tab with its
16
+ * full path, which a click copies, above it. A link to a markdown document of
17
+ * the project opens it as another tab. Edit edits the title and the text of a
18
+ * requirement or test, saved with a `POST` to `_/document.json`.
19
+ */
20
+ declare const createPlugin: EditorPluginFactory;
21
+ export default createPlugin;
@@ -0,0 +1,278 @@
1
+ import { createModelFromDocument, createPreviewModel, searchElements } from 'asljs-ui-diagram';
2
+ import { element, fetchJson, ICONS as EDITOR_ICONS, note, pathHref, projectPath, searchMatcher, trackActive } from 'asljs-ui-editor';
3
+ import { markdownDocument } from 'asljs-ui-markdown';
4
+ import { pathLine, renderText } from './document.js';
5
+ import { ICONS } from './icons.js';
6
+ import { buildOutline } from './outline.js';
7
+ /**
8
+ * Where the browser keeps the state of the graph's preview.
9
+ */
10
+ const LAYOUT_KEY = 'rq-view-layout';
11
+ /**
12
+ * What the editor says under the text of a requirement or test.
13
+ */
14
+ const EDIT_NOTE = 'Implementation, Coverage and Status are kept as they are, after the text. Ids such as R2 or T3 become links when saved.';
15
+ /**
16
+ * The plugin of a project, the browser module of `asljs-ui-editor` the
17
+ * server serves. It loads the graph from `_/data.json` of the project's
18
+ * `base`. The graph is the first document, `rq:graph`, and stays open; a
19
+ * double click on a node opens its document as another tab,
20
+ * `rq:doc:<path>`. The diagram's selection is the editor's: a click on a node
21
+ * selects it, and so does choosing it in the Nodes list, which also moves the
22
+ * view to it. The graph's preview, on its right, views or edits the selected
23
+ * node, as the View and Edit tabs of its action bar choose. The diagram is
24
+ * read-only: nodes move, but delete and resize do nothing. The search finds
25
+ * the nodes by their titles, and opens their documents.
26
+ *
27
+ * A document is rendered here from `_/document.json`: a requirement's
28
+ * Implementation list as a table, a requirement or test as a tab with its
29
+ * full path, which a click copies, above it. A link to a markdown document of
30
+ * the project opens it as another tab. Edit edits the title and the text of a
31
+ * requirement or test, saved with a `POST` to `_/document.json`.
32
+ */
33
+ const createPlugin = async ({ id, base, model }) => {
34
+ const { title, problems, diagram: { document: data, hrefs } } = await fetchJson(base, '_/data.json');
35
+ const graphId = `${id}:graph`;
36
+ const labels = new Map(data.nodes.map(node => [node.id,
37
+ node.label]));
38
+ // The path of each node's document, relative to the project; none for a
39
+ // node outside the served folder.
40
+ const paths = new Map(Object.entries(hrefs)
41
+ .map(([node, href]) => [node,
42
+ decodeURI(href)]));
43
+ const titles = new Map([...paths]
44
+ .map(([node, file]) => [file,
45
+ labels.get(node) ?? file]));
46
+ const graph = createModelFromDocument(data);
47
+ graph.deleteElements = () => { };
48
+ graph.resizeElement = () => false;
49
+ const toDocument = (file) => ({ id: `${id}:doc:${file}`,
50
+ title: titles.get(file)
51
+ ?? file
52
+ .split('/')
53
+ .at(-1)
54
+ .replace(/\.md$/i, ''),
55
+ data: { path: file } });
56
+ const nodeDocument = (node) => {
57
+ const file = paths.get(node);
58
+ return file === undefined
59
+ ? null
60
+ : toDocument(file);
61
+ };
62
+ const selectNode = (node) => {
63
+ model.activate(graphId);
64
+ graph.select([node]);
65
+ requestAnimationFrame(() => graph.reveal(node));
66
+ };
67
+ const openNode = (node) => {
68
+ const opened = nodeDocument(node);
69
+ if (opened !== null) {
70
+ model.open(opened);
71
+ }
72
+ };
73
+ // The editor's selection is the graph's, which the Nodes list marks.
74
+ graph.on('change', () => {
75
+ const ids = graph.selection.filter(node => labels.has(node));
76
+ const node = ids.length === 1
77
+ ? ids[0]
78
+ : null;
79
+ if ((model.selection?.id
80
+ ?? null)
81
+ !== node) {
82
+ model.select(node === null
83
+ ? null
84
+ : { id: node,
85
+ title: labels.get(node),
86
+ data: { path: paths.get(node) ?? null } });
87
+ }
88
+ });
89
+ // A document of the project: rendered from what `_/document.json`
90
+ // answers, its links to markdown documents opened as tabs.
91
+ const documentView = (file, options) => {
92
+ let reply = null;
93
+ const head = element('div');
94
+ return markdownDocument({ url: pathHref(base, file),
95
+ load: async () => {
96
+ const loaded = await fetchJson(base, `_/document.json?path=${encodeURIComponent(file)}`);
97
+ reply = loaded;
98
+ head.replaceChildren(...loaded.editable === undefined
99
+ ? []
100
+ : [pathLine(loaded.file)]);
101
+ return loaded.editable === undefined
102
+ ? { body: loaded.text }
103
+ : { body: loaded.editable.body,
104
+ version: loaded.editable.version,
105
+ title: { label: loaded.editable.id,
106
+ value: loaded.editable.title } };
107
+ },
108
+ render: () => renderText(reply),
109
+ save: options.save === true
110
+ ? async (edit) => {
111
+ const saved = await fetchJson(base, '_/document.json', { path: file,
112
+ version: edit.version,
113
+ title: edit.title ?? '',
114
+ body: edit.body });
115
+ return saved.warning;
116
+ }
117
+ : undefined,
118
+ head: options.head === true
119
+ ? () => head
120
+ : undefined,
121
+ note: EDIT_NOTE,
122
+ editing: options.editing,
123
+ openLink: (url) => {
124
+ const target = projectPath(base, url);
125
+ if (target === null
126
+ || !/\.md$/i.test(target)) {
127
+ return false;
128
+ }
129
+ model.open(toDocument(target));
130
+ return true;
131
+ },
132
+ onInput: options.keep === undefined
133
+ ? undefined
134
+ : () => model.keep(options.keep) });
135
+ };
136
+ const nodeButton = (node, label) => {
137
+ const button = element('button', 'asljs-plugin-item', label);
138
+ button.type = 'button';
139
+ button.dataset.node = node;
140
+ button.title = label;
141
+ button.addEventListener('click', () => selectNode(node));
142
+ button.addEventListener('dblclick', () => openNode(node));
143
+ return button;
144
+ };
145
+ // The graph's preview: View shows the selected node's document, Edit
146
+ // edits it; its state is kept in this browser.
147
+ let stored = {};
148
+ try {
149
+ stored =
150
+ JSON.parse(localStorage.getItem(LAYOUT_KEY) ?? '{}');
151
+ }
152
+ catch {
153
+ // Without storage, the preview starts open, viewing.
154
+ }
155
+ const preview = createPreviewModel(stored.preview
156
+ ?? { open: true,
157
+ mode: 'view' });
158
+ preview.on('change', () => {
159
+ try {
160
+ localStorage.setItem(LAYOUT_KEY, JSON.stringify({ preview: { open: preview.open,
161
+ mode: preview.mode,
162
+ width: preview.width } }));
163
+ }
164
+ catch {
165
+ // Without storage, the preview lasts until the page is reloaded.
166
+ }
167
+ });
168
+ const previewed = (selection) => selection.length === 1
169
+ ? paths.get(selection[0]) ?? null
170
+ : null;
171
+ const previewModes = [{ id: 'view',
172
+ title: 'View',
173
+ isEnabled: selection => previewed(selection) !== null,
174
+ render: selection => documentView(previewed(selection), {}) },
175
+ { id: 'edit',
176
+ title: 'Edit',
177
+ isEnabled: selection => previewed(selection) !== null,
178
+ render: selection => documentView(previewed(selection), { editing: true,
179
+ save: true }) }];
180
+ const graphView = () => {
181
+ const view = element('div', 'rq-graph');
182
+ const diagram = document.createElement('asljs-diagram');
183
+ diagram.tabIndex = 0;
184
+ diagram.model = graph;
185
+ diagram.addEventListener('dblclick', (event) => {
186
+ const node = event.target
187
+ ?.closest?.('g[data-id]')
188
+ ?.getAttribute('data-id');
189
+ if (node) {
190
+ openNode(node);
191
+ }
192
+ });
193
+ const panel = document.createElement('asljs-diagram-preview');
194
+ panel.placeholder =
195
+ 'Select a requirement or test to preview it.';
196
+ panel.modes = previewModes;
197
+ panel.model = graph;
198
+ panel.preview = preview;
199
+ const stage = element('div', 'rq-stage');
200
+ stage.append(diagram, panel);
201
+ const zoom = document.createElement('asljs-diagram-zoom');
202
+ zoom.viewport = graph.viewport;
203
+ const tabs = document.createElement('asljs-diagram-preview-tabs');
204
+ tabs.modes = previewModes;
205
+ tabs.preview = preview;
206
+ const bar = element('div', 'rq-actionbar');
207
+ bar.append(zoom, tabs);
208
+ view.append(stage, bar);
209
+ requestAnimationFrame(() => graph.viewport.fit());
210
+ return view;
211
+ };
212
+ const outline = (items) => {
213
+ const list = element('ul', 'asljs-plugin-list');
214
+ for (const item of items) {
215
+ const entry = element('li');
216
+ entry.append(nodeButton(item.id, item.label));
217
+ if (item.children.length > 0) {
218
+ entry.append(outline(item.children));
219
+ }
220
+ list.append(entry);
221
+ }
222
+ return list;
223
+ };
224
+ // The nodes whose title matches: as searchElements finds them, or, with a
225
+ // regular expression or the case matched, as searchMatcher does.
226
+ const matching = (query) => {
227
+ if (!query.regex
228
+ && !query.matchCase) {
229
+ return searchElements(graph.diagram.elements, query.text)
230
+ .map(item => String(item.value));
231
+ }
232
+ const matches = searchMatcher(query);
233
+ return data.nodes
234
+ .filter(node => matches(node.label))
235
+ .map(node => node.id);
236
+ };
237
+ return { id,
238
+ title: 'Requirements',
239
+ icon: ICONS.graph,
240
+ documents: [{ id: graphId,
241
+ title,
242
+ closable: false }],
243
+ operations: [{ id: 'nodes',
244
+ title: 'Nodes',
245
+ icon: EDITOR_ICONS.list,
246
+ render: () => {
247
+ const list = outline(buildOutline(data.nodes, data.links));
248
+ trackActive(model, list, { attribute: 'node',
249
+ current: () => model.selection?.id ?? null });
250
+ return list;
251
+ } },
252
+ ...problems.length === 0
253
+ ? []
254
+ : [{ id: 'problems',
255
+ title: `Problems (${problems.length})`,
256
+ icon: EDITOR_ICONS.warning,
257
+ render: () => {
258
+ const list = element('div');
259
+ list.append(...problems.map(note));
260
+ return list;
261
+ } }]],
262
+ renderDocument: (opened) => opened.id === graphId
263
+ ? graphView()
264
+ : documentView(opened.data.path, { head: true,
265
+ save: true,
266
+ keep: opened.id }),
267
+ search: async (query) => matching(query)
268
+ .filter(node => labels.has(node))
269
+ .flatMap((node) => {
270
+ const found = nodeDocument(node);
271
+ return found === null
272
+ ? []
273
+ : [{ document: found,
274
+ title: labels.get(node) }];
275
+ }),
276
+ dispose: () => model.select(null) };
277
+ };
278
+ export default createPlugin;
package/dist/index.d.ts CHANGED
@@ -1 +1,2 @@
1
- export { execView, type ViewOptions } from './view.js';
1
+ export { main } from './main.js';
2
+ export { createApp, type RqAppOptions } from './view.js';
package/dist/index.js CHANGED
@@ -1 +1,2 @@
1
- export { execView } from './view.js';
1
+ export { main } from './main.js';
2
+ export { createApp } from './view.js';
package/dist/main.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Io } from 'asljs-rq';
2
+ /**
3
+ * The `rq-srv` executable: serves the projects of the working folder, on the
4
+ * port of `--port`, else of the nearest `rq.json`, until SIGINT or SIGTERM,
5
+ * then closes the application and returns the exit code. Its logger provider
6
+ * is made from `--loglevel`, `--logfile`, `--logformat`, the `RQ_LOG_`
7
+ * variables and `rq.json` (`runMain`); help and the version answer 0, a
8
+ * wrong argument or a server that cannot start 1, with the reason on
9
+ * `io.stderr`.
10
+ */
11
+ export declare function main(args: string[], io: Io): Promise<number>;
package/dist/main.js ADDED
@@ -0,0 +1,40 @@
1
+ import { commandLine, configPort, loadConfig, packageVersion, parsePort, runMain, serveApp } from 'asljs-appbase';
2
+ import { CONFIG_FILE } from 'asljs-rq';
3
+ import { Command } from 'commander';
4
+ import path from 'node:path';
5
+ import { createApp } from './view.js';
6
+ const WORKING_DIR = 'The folder the projects are looked for in: its rq.json, else each rq.json of its subfolders; the current directory by default. The port and the logging come from the nearest rq.json of it or a parent';
7
+ /**
8
+ * The `rq-srv` executable: serves the projects of the working folder, on the
9
+ * port of `--port`, else of the nearest `rq.json`, until SIGINT or SIGTERM,
10
+ * then closes the application and returns the exit code. Its logger provider
11
+ * is made from `--loglevel`, `--logfile`, `--logformat`, the `RQ_LOG_`
12
+ * variables and `rq.json` (`runMain`); help and the version answer 0, a
13
+ * wrong argument or a server that cannot start 1, with the reason on
14
+ * `io.stderr`.
15
+ */
16
+ export async function main(args, io) {
17
+ return await runMain(args, io, { prefix: 'RQ_LOG_',
18
+ configFile: CONFIG_FILE }, async (loggerProvider) => {
19
+ const cli = commandLine(new Command('rq-srv'), io, { workingDir: WORKING_DIR });
20
+ cli
21
+ .description('Serve a requirements graph and its documents over HTTP: the graph in an editor, and the documents rendered and editable.')
22
+ .version(packageVersion(import.meta.url))
23
+ .helpCommand(false)
24
+ .option('--port <port>', 'Port to listen on, exactly; 0 picks a free one. By default port of rq.json, else the first free port from 3000 on')
25
+ .action(async (options) => {
26
+ const cwd = path.resolve(io.cwd, options.workingDir ?? '.');
27
+ const port = parsePort(options.port)
28
+ ?? configPort(await loadConfig(cwd, CONFIG_FILE));
29
+ await serveApp(await createApp({ cwd,
30
+ base: '/',
31
+ env: io.env,
32
+ loggerProvider }, { stderr: io.stderr }), { io,
33
+ cwd,
34
+ port,
35
+ loggerProvider });
36
+ });
37
+ await cli.parseAsync(args, { from: 'user' });
38
+ return 0;
39
+ });
40
+ }