@descent-vtt/spec-brief 0.6.0 → 0.7.0
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/CHANGELOG.md +96 -0
- package/README.md +24 -5
- package/bin/spec-brief.js +8 -1
- package/dist/cli.js +92 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.js +5 -1
- package/dist/config.js.map +1 -1
- package/dist/engine.d.ts +7 -0
- package/dist/engine.js +28 -3
- package/dist/engine.js.map +1 -1
- package/dist/git.d.ts +8 -0
- package/dist/git.js +15 -4
- package/dist/git.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -11,6 +11,102 @@ release that can turn a run red. This is the family's policy, recorded in
|
|
|
11
11
|
releases up to 0.2.7 came before it, and several of their patches changed
|
|
12
12
|
what a run reports.
|
|
13
13
|
|
|
14
|
+
## 0.7.0
|
|
15
|
+
|
|
16
|
+
An input that is set and names nothing is refused, exit 2, by a line that
|
|
17
|
+
names it, where it was read as if it had not been given: a
|
|
18
|
+
`SOURCE_DATE_EPOCH` that is no date, an option given an empty value, an
|
|
19
|
+
argument a command does not take, a `--root` that is no directory, a list
|
|
20
|
+
that names no brief or has a place that names none.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **A `SOURCE_DATE_EPOCH` that is set and cannot be read as a date is
|
|
25
|
+
refused, not read as unset.** `new` and `archive` without `--date` took
|
|
26
|
+
`tomorrow`, `1.5`, `-1`, a value with a space beside its digits and an
|
|
27
|
+
empty one for no value at all, wrote the clock's date and exited 0. Each
|
|
28
|
+
now writes nothing and ends with exit 2 and a line that names the variable
|
|
29
|
+
and shows what it holds, and `today()` from the package throws for them
|
|
30
|
+
(ADR-0004, [Today's date](README.md#todays-date)).
|
|
31
|
+
Upgrading: set it to whole seconds in digits alone, as `date +%s` prints
|
|
32
|
+
them, no more than `253402300799` (9999-12-31), or pass `--date`, or leave
|
|
33
|
+
it unset; set and empty is not unset.
|
|
34
|
+
- **An option given an empty value is refused by its name, not read as the
|
|
35
|
+
option left out.** `new "T" --id ""` wrote `briefs/_t.md`, a file no
|
|
36
|
+
command reads again; `--root ""` ran in the current directory; `archive
|
|
37
|
+
--summary ""` wrote a banner without the sentence; `init --briefs " "`
|
|
38
|
+
made a directory named with a space. Every option that takes a value now
|
|
39
|
+
ends the run with exit 2 and `--id is "", which is no value` for one that
|
|
40
|
+
is empty or only space, nothing written; the ones refused already say the
|
|
41
|
+
same line (ADR-0001, [An input that names nothing](README.md#an-input-that-names-nothing)).
|
|
42
|
+
Upgrading: where a script may hand an option nothing, leave the option
|
|
43
|
+
out, as `${ID:+--id "$ID"}` does in a shell.
|
|
44
|
+
- **`new --depends-on` refuses a list that names no brief, and a place in
|
|
45
|
+
one that names none.** `--depends-on ,` scaffolded a brief with no
|
|
46
|
+
dependency, and `7,8,` or `7,,8` one that depends on 7 and 8, as `"$A,$B"`
|
|
47
|
+
with `$B` unset made one that depends on `$A` alone, each exit 0.
|
|
48
|
+
Upgrading: leave the option out for a brief that depends on nothing, and
|
|
49
|
+
write the list with one comma between each two ids and none at either end.
|
|
50
|
+
- **`init`, `list`, `matrix` and `schedule` refuse an argument, which they
|
|
51
|
+
do not take.** `list nonsense` listed every brief, `schedule later
|
|
52
|
+
--write` ran as `schedule --write`, and `init mydir` set up the current
|
|
53
|
+
directory, each exit 0; each is now exit 2 with the argument named and the
|
|
54
|
+
command's usage. Upgrading: drop the argument; `init --root mydir` sets up another
|
|
55
|
+
directory, and `--briefs mydir` names the one for the briefs.
|
|
56
|
+
- **A `--root` that is no directory is refused.** Given a file or a path
|
|
57
|
+
that does not exist, the search for a configuration went on from its
|
|
58
|
+
parent: `lint --root tpyo` linted the tree above, and `new --root tpyo`
|
|
59
|
+
wrote into it, exit 0. `init` still makes a root that is not there.
|
|
60
|
+
Upgrading: give `--root` a directory that exists.
|
|
61
|
+
|
|
62
|
+
### Fixed
|
|
63
|
+
|
|
64
|
+
- A terminal that says `TERM=dumb` gets no colour, unless `FORCE_COLOR` or
|
|
65
|
+
`--color` asks for it, as the family's other tools have it. It was sent
|
|
66
|
+
the escape sequences it had said it does not read.
|
|
67
|
+
- A `SOURCE_DATE_EPOCH` that cannot be read stops only a run that would
|
|
68
|
+
write its date. One of more seconds than any date has ended `lint`,
|
|
69
|
+
`list`, `matrix`, `schedule`, `init` and `unarchive`, and `new` and
|
|
70
|
+
`archive` given `--date`, with exit 2; they now run whatever it holds.
|
|
71
|
+
- A `SOURCE_DATE_EPOCH` in the years 10000 to 275760 is refused by name,
|
|
72
|
+
where `new` and `archive` said `"+010000-01" is not a date written
|
|
73
|
+
YYYY-MM-DD`. Exit 2, as before.
|
|
74
|
+
- The README says what `SOURCE_DATE_EPOCH` does, and `--help` names it
|
|
75
|
+
beside `--date`.
|
|
76
|
+
- A configuration that is a directory, named by `--config` or found as
|
|
77
|
+
`.spec-brief.json`, is said in one line, `adir: is a directory, not a
|
|
78
|
+
configuration file`, where the run ended on `unexpected error: Error:
|
|
79
|
+
EISDIR` and a stack. Exit 2, as before.
|
|
80
|
+
- A configuration that is not JSON is refused in one line whatever the file
|
|
81
|
+
holds. A line break the parser quoted from it made the refusal two lines,
|
|
82
|
+
and any other control character reached the terminal as it was; each is
|
|
83
|
+
now written as its escape. Exit 2, as before.
|
|
84
|
+
|
|
85
|
+
## 0.6.1
|
|
86
|
+
|
|
87
|
+
Four ways a run ended with `unexpected error:` and a stack for something
|
|
88
|
+
that was no defect of spec-brief's. Each is one line now; every exit code is
|
|
89
|
+
as it was.
|
|
90
|
+
|
|
91
|
+
### Fixed
|
|
92
|
+
|
|
93
|
+
- **A reader that closes the output is answered in one line, not a stack.**
|
|
94
|
+
`spec-brief list | head` could end with `spec-brief: unexpected error:
|
|
95
|
+
Error: EPIPE: broken pipe, write` and a stack once `head` had left; it now
|
|
96
|
+
prints `spec-brief: stdout was closed before all of the output was
|
|
97
|
+
written` on stderr, exit 2 as before
|
|
98
|
+
([the family contract](https://github.com/DescentVTT/spec-core/blob/main/docs/adr/0005-the-family-contract.md)).
|
|
99
|
+
- A `SOURCE_DATE_EPOCH` of more seconds than any date has is named, with
|
|
100
|
+
what to do about it, where the run ended on `RangeError: Invalid time
|
|
101
|
+
value` and a stack. Exit 2, as before.
|
|
102
|
+
- `archive` reports what git was not handed or could not do by its message
|
|
103
|
+
alone: `"-x" is not a revision` for a `--commit`, a `--base` or an
|
|
104
|
+
`archiving.base` that would be read as an option, and git's own words
|
|
105
|
+
when its status or its diff fails. Exit 2, as before.
|
|
106
|
+
- `init --briefs ../docs` and `--archive` likewise are refused as the
|
|
107
|
+
configuration refuses them, `--briefs must be a directory inside the
|
|
108
|
+
repository`, where the path's error came with a stack. Exit 2, as before.
|
|
109
|
+
|
|
14
110
|
## 0.6.0
|
|
15
111
|
|
|
16
112
|
An error spec-brief did not expect before a command started, or where
|
package/README.md
CHANGED
|
@@ -95,11 +95,11 @@ A brief is read as CommonMark reads it wherever that decides what is code or a c
|
|
|
95
95
|
|
|
96
96
|
### `spec-brief init`
|
|
97
97
|
|
|
98
|
-
Writes `.spec-brief.json` with every default spelled out, and creates the brief and archive directories. `--briefs <dir>` and `--archive <dir>` choose them.
|
|
98
|
+
Writes `.spec-brief.json` with every default spelled out, and creates the brief and archive directories. `--briefs <dir>` and `--archive <dir>` choose them, and `--root <dir>` the directory to set up, which `init` makes when it is not there. It takes no argument: `init mydir` is [refused](#an-input-that-names-nothing).
|
|
99
99
|
|
|
100
100
|
### `spec-brief new <title>`
|
|
101
101
|
|
|
102
|
-
Scaffolds a brief with the next free number - one more than the highest id, live or archived, since ids are never reused - and every required section, each holding its `hint` in a comment. A comment is not content, so a fresh brief reads as unwritten until someone writes it. `--id`, `--type`, `--wave`, `--depends-on 7,8` and `--date` fill the front matter.
|
|
102
|
+
Scaffolds a brief with the next free number - one more than the highest id, live or archived, since ids are never reused - and every required section, each holding its `hint` in a comment. A comment is not content, so a fresh brief reads as unwritten until someone writes it. `--id`, `--type`, `--wave`, `--depends-on 7,8` and `--date` fill the front matter; without `--date` the date is [today's](#todays-date).
|
|
103
103
|
|
|
104
104
|
### `spec-brief lint [brief...]`
|
|
105
105
|
|
|
@@ -230,6 +230,7 @@ With `archiving.rewriteLinks` off, no link is rewritten, and the links other liv
|
|
|
230
230
|
| `--base <rev>` | Measures from the merge base instead, for a branch of several commits. |
|
|
231
231
|
| `--pr <n>` | Links the pull request. |
|
|
232
232
|
| `--summary <text>` | What the round did, in the banner. |
|
|
233
|
+
| `--date <YYYY-MM-DD>` | The date in the banner, where it is not [today's](#todays-date). |
|
|
233
234
|
| `--dry-run` | Prints the plan and the banner, and writes nothing. |
|
|
234
235
|
|
|
235
236
|
spec-brief reads git and never writes it. Review the change and commit it with the round.
|
|
@@ -247,13 +248,13 @@ Reopens an archived brief: the banner and the hash come off, the status goes bac
|
|
|
247
248
|
|
|
248
249
|
| Flag | What it does |
|
|
249
250
|
| --- | --- |
|
|
250
|
-
| `--root <dir>` | Runs from another directory. |
|
|
251
|
+
| `--root <dir>` | Runs from another directory, which must be there: only `init` makes one that is not. |
|
|
251
252
|
| `--config <file>` | Names the configuration. |
|
|
252
253
|
| `--no-config` | Uses the defaults. |
|
|
253
254
|
| `--format <format>` | `pretty` by default, or `json`; `lint`, `matrix` and `schedule`, which report findings, also write `sarif`, `github` and `gitlab`, and `--help` lists each command's formats. |
|
|
254
255
|
| `--strict` | Makes warnings fail the run. |
|
|
255
256
|
| `--no-git` | Leaves git out even inside a repository: the tree is read from disk, and `archive` records no commit. |
|
|
256
|
-
| `--color`, `--no-color` | Override `NO_COLOR`, `FORCE_COLOR` and the terminal check. |
|
|
257
|
+
| `--color`, `--no-color` | Override `NO_COLOR`, `FORCE_COLOR`, `TERM=dumb` and the terminal check. |
|
|
257
258
|
| `--help`, `--version` | Do what they say. |
|
|
258
259
|
|
|
259
260
|
**Exit codes:**
|
|
@@ -262,10 +263,28 @@ Reopens an archived brief: the banner and the hash come off, the status goes bac
|
|
|
262
263
|
| --- | --- |
|
|
263
264
|
| `0` | Clean. |
|
|
264
265
|
| `1` | Findings, a collision, or a refused action. |
|
|
265
|
-
| `2` | The run could not be trusted - a bad flag, a configuration that does not load, a brief that does not exist, or an error spec-brief did not expect, reported on stderr as `spec-brief: unexpected error:` with its stack. |
|
|
266
|
+
| `2` | The run could not be trusted, or its answer did not arrive - a bad flag, [an input that names nothing](#an-input-that-names-nothing), a configuration that does not load, a brief that does not exist, a git command that failed, a `SOURCE_DATE_EPOCH` that [cannot be read as a date](#todays-date) where one is to be written, a stdout its reader closed before all of the output was written, as a pipeline into `head` does, each said in one line on stderr, or an error spec-brief did not expect, reported on stderr as `spec-brief: unexpected error:` with its stack. |
|
|
266
267
|
|
|
267
268
|
A run over a briefs directory that does not exist exits 2, because a check over nothing looks exactly like a clean one.
|
|
268
269
|
|
|
270
|
+
### An input that names nothing
|
|
271
|
+
|
|
272
|
+
An option, an argument or a path that is given and names nothing is refused: exit 2, one line that names it, nothing written. None is read as if it had not been given. `--root "$DIR"` where the variable is not set would otherwise run in the current directory, and the answer would look like the one that was asked for.
|
|
273
|
+
|
|
274
|
+
- **An option given nothing.** Every option that takes a value refuses one that is empty or only space, by the option's name: `--id is "", which is no value`. No option gives an empty value a meaning; leave the option out for what it does by default.
|
|
275
|
+
- **A list that names nothing.** `--depends-on ,` names no brief, and a brief that depends on nothing is one scaffolded without the option. A place in a list that names no brief, the comma too many in `7,8,` or the gap in `7,,8`, is refused as well: it is a brief left out, as `"$A,$B"` is with one of the two unset.
|
|
276
|
+
- **An argument a command does not take.** `init`, `list`, `matrix` and `schedule` take none and refuse one by name, with their usage. `init mydir` set up the current directory; `init --root mydir` is what sets up another.
|
|
277
|
+
- **A `--root` that is no directory.** A file, or a path that does not exist, is refused: the search for a configuration would go on from its parent, and the run would report on, or `new` write into, the tree above. A directory below the configuration's is a place to start from, as the current directory is.
|
|
278
|
+
- **A configuration that is a directory**, named by `--config` or found under the configuration's name, does not load, and is said so in a line, as one that is not JSON is.
|
|
279
|
+
|
|
280
|
+
### Today's date
|
|
281
|
+
|
|
282
|
+
`new` writes a date into the front matter and `archive` one into the banner: the one `--date` gives, or today's. Today is the clock's date in UTC, unless `SOURCE_DATE_EPOCH` is set, the variable a pipeline sets to have every run write the same bytes ([its specification](https://reproducible-builds.org/specs/source-date-epoch/)): then it is the date of that time, in UTC.
|
|
283
|
+
|
|
284
|
+
- **What it holds**: whole seconds since 1970-01-01 in digits alone, as `date +%s` prints them, from `0` to `253402300799`, the last second of 9999-12-31 and of the dates written `YYYY-MM-DD`.
|
|
285
|
+
- **Anything else is refused**, never read as unset: a word, `1.5`, `-1`, a space or a newline beside the digits, a later time, or nothing at all, which is what `SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)` leaves where git failed. `new` and `archive` then write nothing and exit 2 with a line that names the variable and shows what it holds. The clock's date, written where a pipeline asked for its own, would look like any other.
|
|
286
|
+
- **Only a run that would use it reads it**: `--date` gives the date instead, and `init`, `lint`, `list`, `matrix`, `schedule`, `unarchive`, `--version` and `--help` write none, so each runs whatever the variable holds.
|
|
287
|
+
|
|
269
288
|
## Configuration
|
|
270
289
|
|
|
271
290
|
`.spec-brief.json` (or `spec-brief.json`) at the root; the nearest one above the working directory is used, and its directory is the root. It is JSON, validated against [`schema.json`](schema.json): an unknown key, a misspelt rule or a value of the wrong type stops the run with exit 2 rather than falling back to defaults and reporting on the wrong rules.
|
package/bin/spec-brief.js
CHANGED
|
@@ -7,8 +7,15 @@
|
|
|
7
7
|
// main answers the errors it awaits with exit 2. What nothing awaits - a
|
|
8
8
|
// stream's error, a timer's, a promise nobody holds - Node ends with exit 1,
|
|
9
9
|
// which a script reads as findings: the same answer for those.
|
|
10
|
+
//
|
|
11
|
+
// A reader that closed stdout, as `| head` does, is no defect, and it arrives
|
|
12
|
+
// here, as the stream's error: one line, with no stack to send a person
|
|
13
|
+
// looking for a bug. The code alone names it, since stdout and stderr are the
|
|
14
|
+
// only pipes spec-brief writes to, and a line about a closed stderr reaches
|
|
15
|
+
// nobody.
|
|
10
16
|
process.on('uncaughtException', (error) => {
|
|
11
|
-
|
|
17
|
+
const said = error?.code === 'EPIPE' ? 'stdout was closed before all of the output was written' : `unexpected error: ${error?.stack ?? error}`;
|
|
18
|
+
process.stderr.write(`spec-brief: ${said}\n`);
|
|
12
19
|
process.exit(2);
|
|
13
20
|
});
|
|
14
21
|
|
package/dist/cli.js
CHANGED
|
@@ -6,13 +6,14 @@
|
|
|
6
6
|
* that does not load, a brief that does not exist. A run that checked nothing
|
|
7
7
|
* must never exit as though it found nothing.
|
|
8
8
|
*/
|
|
9
|
-
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
10
10
|
import { parseArgs } from 'node:util';
|
|
11
11
|
import { TransactionError, ConflictError } from './apply.js';
|
|
12
12
|
import { reported } from './collisions.js';
|
|
13
13
|
import { CONFIG_FILES, ConfigError, initialConfig } from './config.js';
|
|
14
14
|
import { BriefEngine, EngineError, today } from './engine.js';
|
|
15
15
|
import { NodeFileSystem } from './fs.js';
|
|
16
|
+
import { GitError } from './git.js';
|
|
16
17
|
import { failing, sortFindings, summarise } from './lint.js';
|
|
17
18
|
import { normalisePath } from './links.js';
|
|
18
19
|
import { briefJson, findingJson, FORMATS, githubCommands, gitlabCodeQuality, jsonDocument, matrixJson, planJson, prettyFindings, prettyList, prettyMatrix, prettyPlan, prettySchedule, sarif, scheduleJson, sectionsJson, summaryLine, } from './report.js';
|
|
@@ -38,7 +39,11 @@ const GLOBAL = {
|
|
|
38
39
|
version: { type: 'boolean', short: 'v' },
|
|
39
40
|
};
|
|
40
41
|
const TABLE = {
|
|
41
|
-
init: {
|
|
42
|
+
init: {
|
|
43
|
+
options: { briefs: { type: 'string' }, archive: { type: 'string' } },
|
|
44
|
+
formats: ['pretty', 'json'],
|
|
45
|
+
noArgument: '; --root <dir> names the directory it sets up, and --briefs <dir> the one for the briefs',
|
|
46
|
+
},
|
|
42
47
|
new: {
|
|
43
48
|
options: {
|
|
44
49
|
id: { type: 'string' },
|
|
@@ -50,9 +55,9 @@ const TABLE = {
|
|
|
50
55
|
formats: ['pretty', 'json'],
|
|
51
56
|
},
|
|
52
57
|
lint: { options: {}, formats: FORMATS },
|
|
53
|
-
list: { options: { ready: { type: 'boolean' }, archived: { type: 'boolean' } }, formats: ['pretty', 'json'] },
|
|
54
|
-
matrix: { options: { 'all-waves': { type: 'boolean' } }, formats: FORMATS },
|
|
55
|
-
schedule: { options: { write: { type: 'boolean' } }, formats: FORMATS },
|
|
58
|
+
list: { options: { ready: { type: 'boolean' }, archived: { type: 'boolean' } }, formats: ['pretty', 'json'], noArgument: '' },
|
|
59
|
+
matrix: { options: { 'all-waves': { type: 'boolean' } }, formats: FORMATS, noArgument: '' },
|
|
60
|
+
schedule: { options: { write: { type: 'boolean' } }, formats: FORMATS, noArgument: '' },
|
|
56
61
|
archive: {
|
|
57
62
|
options: {
|
|
58
63
|
pr: { type: 'string' },
|
|
@@ -104,7 +109,7 @@ Commands:
|
|
|
104
109
|
unarchive <brief> reopen an archived brief
|
|
105
110
|
|
|
106
111
|
Options for every command:
|
|
107
|
-
--root <dir> run from this directory instead of the current one
|
|
112
|
+
--root <dir> run from this directory instead of the current one; it must exist, unless init makes it
|
|
108
113
|
--config <file> use this configuration file
|
|
109
114
|
--no-config use the defaults, ignoring any configuration file
|
|
110
115
|
--format <format> the output format, pretty by default; each command's are below
|
|
@@ -126,7 +131,7 @@ new:
|
|
|
126
131
|
--type <type> a brief type from the configuration
|
|
127
132
|
--wave <n> the wave it runs in
|
|
128
133
|
--depends-on <ids> comma-separated ids it runs after
|
|
129
|
-
--date <YYYY-MM-DD> the date in its front matter; default today
|
|
134
|
+
--date <YYYY-MM-DD> the date in its front matter; default SOURCE_DATE_EPOCH's, or today's in UTC
|
|
130
135
|
|
|
131
136
|
list:
|
|
132
137
|
--ready only briefs that can run now: dependencies archived, not drafts or deferred
|
|
@@ -143,13 +148,16 @@ archive:
|
|
|
143
148
|
--commit <rev> the commit the round landed as
|
|
144
149
|
--base <rev> the branch it started from; the diff runs from the merge base
|
|
145
150
|
--summary <text> what the round did, for the banner
|
|
146
|
-
--date <YYYY-MM-DD> the archival date; default today
|
|
151
|
+
--date <YYYY-MM-DD> the archival date; default SOURCE_DATE_EPOCH's, or today's in UTC
|
|
147
152
|
--allow-dirty archive although the tree has uncommitted work
|
|
148
153
|
--dry-run print the plan and change nothing
|
|
149
154
|
|
|
150
155
|
unarchive:
|
|
151
156
|
--dry-run print the plan and change nothing
|
|
152
157
|
|
|
158
|
+
An option given an empty value, a list that names nothing and an argument a
|
|
159
|
+
command does not take are refused, not read as if they were not there.
|
|
160
|
+
|
|
153
161
|
Exit codes: 0 clean, 1 findings or refused, 2 the run could not be trusted.
|
|
154
162
|
`;
|
|
155
163
|
/** Global options that take a value, so the value is not mistaken for the command. */
|
|
@@ -174,18 +182,34 @@ export function parse(argv) {
|
|
|
174
182
|
if (command !== undefined && spec === undefined) {
|
|
175
183
|
throw new UsageError(`"${command}" is not a command; run spec-brief --help`);
|
|
176
184
|
}
|
|
185
|
+
let parsed;
|
|
177
186
|
try {
|
|
178
|
-
|
|
187
|
+
parsed = parseArgs({
|
|
179
188
|
args: [...argv],
|
|
180
189
|
options: { ...GLOBAL, ...(spec?.options ?? {}) },
|
|
181
190
|
allowPositionals: true,
|
|
182
191
|
strict: true,
|
|
183
192
|
});
|
|
184
|
-
return { command, values: values, positionals: positionals.slice(1) };
|
|
185
193
|
}
|
|
186
194
|
catch (error) {
|
|
187
195
|
throw new UsageError(error.message);
|
|
188
196
|
}
|
|
197
|
+
const positionals = parsed.positionals.slice(1);
|
|
198
|
+
// What the parser takes is not yet what names something. An option whose
|
|
199
|
+
// value is empty, as `--root "$DIR"` is where the variable is not set, was
|
|
200
|
+
// read as the option left out, and an argument no command asked for was
|
|
201
|
+
// dropped: either way the run answered a question nobody had put, exit 0.
|
|
202
|
+
for (const [name, value] of Object.entries(parsed.values)) {
|
|
203
|
+
if (typeof value === 'string' && value.trim() === '') {
|
|
204
|
+
// Shown as JSON, where a tab or a line break in it can be seen.
|
|
205
|
+
throw new UsageError(`--${name} is ${JSON.stringify(value)}, which is no value; give it one, or leave the option out`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (spec?.noArgument !== undefined && positionals.length > 0) {
|
|
209
|
+
const extra = positionals.map((argument) => `"${argument}"`).join(', ');
|
|
210
|
+
throw new UsageError(`${command} takes no argument, not ${extra}: spec-brief ${command}${spec.noArgument}`);
|
|
211
|
+
}
|
|
212
|
+
return { command, values: parsed.values, positionals };
|
|
189
213
|
}
|
|
190
214
|
/** The value of an option declared a string, which is all `parseArgs` gives one. */
|
|
191
215
|
function text(values, key) {
|
|
@@ -217,6 +241,10 @@ function useColor(values, io) {
|
|
|
217
241
|
const force = env['FORCE_COLOR'];
|
|
218
242
|
if (force !== undefined && force !== '')
|
|
219
243
|
return force !== '0';
|
|
244
|
+
// A terminal that says it is dumb has asked for no escape sequence, and
|
|
245
|
+
// Node itself counts one colour for it: the family's tools read it alike.
|
|
246
|
+
if (env['TERM'] === 'dumb')
|
|
247
|
+
return false;
|
|
220
248
|
return (io.stdout ?? process.stdout).isTTY === true;
|
|
221
249
|
}
|
|
222
250
|
async function openEngine(run) {
|
|
@@ -257,8 +285,18 @@ async function runInit(run) {
|
|
|
257
285
|
if (await fs.exists(name))
|
|
258
286
|
throw new UsageError(`${name} already exists here; edit it instead`);
|
|
259
287
|
}
|
|
260
|
-
|
|
261
|
-
|
|
288
|
+
// A directory above the root is refused as the configuration refuses one
|
|
289
|
+
// (config.ts), in the same words, before anything is written.
|
|
290
|
+
const inside = (option, given) => {
|
|
291
|
+
try {
|
|
292
|
+
return normalisePath(given);
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
throw new UsageError(`--${option} must be a directory inside the repository, not "${given}"`);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
const briefs = inside('briefs', text(run.values, 'briefs') ?? 'briefs');
|
|
299
|
+
const archive = inside('archive', text(run.values, 'archive') ?? `${briefs}/archive`);
|
|
262
300
|
if (briefs === '' || archive === '' || briefs === archive)
|
|
263
301
|
throw new UsageError('--briefs and --archive must be two directories');
|
|
264
302
|
const config = `${JSON.stringify(initialConfig(briefs, archive), null, 2)}\n`;
|
|
@@ -281,18 +319,26 @@ async function runNew(run) {
|
|
|
281
319
|
const title = run.positionals.join(' ').trim();
|
|
282
320
|
if (title === '')
|
|
283
321
|
throw new UsageError('new needs a title: spec-brief new "<title>"');
|
|
322
|
+
const listed = text(run.values, 'depends-on');
|
|
323
|
+
const dependsOn = listed?.split(',').map((d) => d.trim());
|
|
324
|
+
// A list that names no brief is not a brief that depends on none: written
|
|
325
|
+
// as given, "," scaffolded a brief with no dependency and said nothing.
|
|
326
|
+
if (dependsOn?.every((d) => d === '')) {
|
|
327
|
+
throw new UsageError(`--depends-on is ${JSON.stringify(listed)}, which names no brief; give it ids with commas between them, as in 7,8, or leave the option out`);
|
|
328
|
+
}
|
|
329
|
+
// And a place in it that names none is a brief left out: "$A,$B" with one
|
|
330
|
+
// of the two unset scaffolded a brief that depends on the other alone.
|
|
331
|
+
if (dependsOn?.includes('')) {
|
|
332
|
+
throw new UsageError(`--depends-on is ${JSON.stringify(listed)}, which names no brief in one of its places; give it ids with one comma between each two, as in 7,8, and none before or after`);
|
|
333
|
+
}
|
|
284
334
|
const engine = await openEngine(run);
|
|
285
|
-
const dependsOn = text(run.values, 'depends-on')
|
|
286
|
-
?.split(',')
|
|
287
|
-
.map((d) => d.trim())
|
|
288
|
-
.filter((d) => d !== '');
|
|
289
335
|
const created = await engine.create({
|
|
290
336
|
title,
|
|
291
337
|
id: text(run.values, 'id'),
|
|
292
338
|
type: text(run.values, 'type'),
|
|
293
339
|
wave: integer(run.values, 'wave', 0),
|
|
294
340
|
dependsOn,
|
|
295
|
-
date: text(run.values, 'date') ?? run.today,
|
|
341
|
+
date: text(run.values, 'date') ?? run.today(),
|
|
296
342
|
});
|
|
297
343
|
if (run.format === 'json')
|
|
298
344
|
run.out(jsonDocument('new', run.version, { ok: true, file: created.file }));
|
|
@@ -378,7 +424,7 @@ async function runTransition(run, action) {
|
|
|
378
424
|
commit: text(run.values, 'commit'),
|
|
379
425
|
base: text(run.values, 'base'),
|
|
380
426
|
summary: text(run.values, 'summary'),
|
|
381
|
-
date: text(run.values, 'date') ?? run.today,
|
|
427
|
+
date: text(run.values, 'date') ?? run.today(),
|
|
382
428
|
allowDirty: flag(run.values, 'allow-dirty'),
|
|
383
429
|
strict: run.strict,
|
|
384
430
|
noGit: flag(run.values, 'no-git'),
|
|
@@ -409,7 +455,7 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
409
455
|
}
|
|
410
456
|
const { command, values, positionals } = parsed;
|
|
411
457
|
// From here on, so that what comes before a command is covered as the
|
|
412
|
-
// command is: --version and --help, the package's own manifest, the
|
|
458
|
+
// command is: --version and --help, the package's own manifest, the colour.
|
|
413
459
|
// An error there left main as a rejection, which the launcher ended as
|
|
414
460
|
// Node's uncaught error with exit 1, "findings" to a script.
|
|
415
461
|
try {
|
|
@@ -428,8 +474,20 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
428
474
|
err(`--format for ${command} is one of ${spec.formats.join(', ')}, not "${format}"`);
|
|
429
475
|
return EXIT_ERROR;
|
|
430
476
|
}
|
|
477
|
+
// A file, or a name with nothing behind it, had the search for a
|
|
478
|
+
// configuration go on from its parent: the run reported on a tree nobody
|
|
479
|
+
// had named, and `new` wrote into it. A path that cannot be reached is
|
|
480
|
+
// not there to the run either. `init` alone is given a directory that
|
|
481
|
+
// does not exist yet, which it makes.
|
|
482
|
+
const root = text(values, 'root');
|
|
483
|
+
if (root !== undefined) {
|
|
484
|
+
const found = await stat(root).catch(() => null);
|
|
485
|
+
if (found === null ? command !== 'init' : !found.isDirectory()) {
|
|
486
|
+
throw new UsageError(`--root is "${root}", which is no directory; give it one, or leave the option out`);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
431
489
|
const run = {
|
|
432
|
-
today: today(io.env ?? process.env),
|
|
490
|
+
today: () => today(io.env ?? process.env),
|
|
433
491
|
out: (t) => {
|
|
434
492
|
stdout.write(t);
|
|
435
493
|
},
|
|
@@ -463,12 +521,24 @@ export async function main(argv = process.argv.slice(2), io = {}) {
|
|
|
463
521
|
}
|
|
464
522
|
}
|
|
465
523
|
catch (error) {
|
|
466
|
-
|
|
524
|
+
// What git refused or could not do is git's message, and no line here.
|
|
525
|
+
const expected = [ConfigError, UsageError, EngineError, ConflictError, TransactionError, GitError];
|
|
467
526
|
if (expected.some((kind) => error instanceof kind)) {
|
|
468
527
|
err(error.message);
|
|
469
528
|
return EXIT_ERROR;
|
|
470
529
|
}
|
|
471
|
-
|
|
530
|
+
// A write its reader had closed the pipe for is no defect either: the
|
|
531
|
+
// answer was not delivered, which is still 2, and no stack sends a person
|
|
532
|
+
// looking for a bug. The process's own streams report it as an event,
|
|
533
|
+
// which the launcher answers in the same words; here it is a caller's
|
|
534
|
+
// stream that throws it. It is read by its code: stdout and stderr are the
|
|
535
|
+
// only pipes spec-brief writes to.
|
|
536
|
+
if (!(error instanceof Error))
|
|
537
|
+
err(`unexpected error: ${String(error)}`);
|
|
538
|
+
else if (error.code === 'EPIPE')
|
|
539
|
+
err('stdout was closed before all of the output was written');
|
|
540
|
+
else
|
|
541
|
+
err(`unexpected error: ${error.stack ?? error.message}`);
|
|
472
542
|
return EXIT_ERROR;
|
|
473
543
|
}
|
|
474
544
|
}
|