riskrails 4.2.0 → 4.3.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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +25 -15
- package/agents/ai-reviewer.md.tmpl +14 -1
- package/ai/agents/implementer.md +6 -0
- package/ai/agents/reviewer.md +8 -1
- package/bin/ansi.js +12 -0
- package/bin/menu.js +71 -12
- package/bin/riskrails.js +240 -14
- package/bin/uninstall.js +115 -0
- package/bin/wizard.js +72 -0
- package/hooks/context-guard.py +11 -1
- package/hooks/dialog-gate.py +3 -2
- package/hooks/guard/decision.py +4 -3
- package/hooks/guard/dialog.py +12 -2
- package/hooks/guard/ere.py +29 -10
- package/hooks/guard/git.py +2 -1
- package/hooks/guard/paths_base.py +7 -0
- package/hooks/guard/paths_bash.py +3 -3
- package/hooks/runtime-gate.py +87 -0
- package/hooks/update_notice.py +23 -8
- package/package.json +1 -1
- package/skills/ai-init/templates/.ai/policies/coding.md +9 -0
- package/skills/ai-task/rr_state/dialog.py +32 -7
- package/skills/ai-task/rr_state/dialog_cmd.py +38 -5
- package/skills/ai-task/rr_state/gate.py +10 -4
- package/skills/ai-task/rr_state/labels.py +33 -4
- package/skills/ai-task/rr_state/questions_cmd.py +5 -4
- package/skills/ai-task/rr_state/report.py +73 -10
- package/skills/ai-task/rr_state/report_cmd.py +2 -0
- package/skills/ai-task/rr_state/review.py +18 -14
- package/skills/ai-task/rr_state/start.py +12 -9
- package/skills/ai-task/rr_state/steps.py +16 -7
- package/skills/ai-task/rr_state/verify.py +17 -12
- package/skills/project-update/history/blobs/dac90781793b8a1c8e5f3e0307665b6394d922140c3831c5508d019f79e1ed0a +41 -0
- package/skills/project-update/history/index.json +6 -3
- package/skills/rr-build/SKILL.md +5 -2
- package/skills/rr-ship/SKILL.md +2 -2
- package/skills/rr-spec/SKILL.md +6 -0
- package/skills/rr-status/SKILL.md +2 -0
- package/skills/rr-status/status.py +5 -3
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
|
|
8
|
-
"version": "4.
|
|
8
|
+
"version": "4.3.0"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"source": "url",
|
|
15
15
|
"url": "https://github.com/vanssata/claude-agentic-sdlc-module.git"
|
|
16
16
|
},
|
|
17
|
-
"version": "4.
|
|
17
|
+
"version": "4.3.0",
|
|
18
18
|
"strict": true
|
|
19
19
|
}
|
|
20
20
|
]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riskrails",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ivan Kakurov"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "riskrails",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"description": "A weight-based engineering pipeline for Claude Code and Codex: each change is weighed on six axes, runs light, guarded or full, edits only the files its plan names, and ends at human approval — enforced by hooks, reviewed adversarially, routed to the cheap model tier by default.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Ivan Kakurov"
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# RiskRails
|
|
2
2
|
|
|
3
|
-
[](https://github.com/vanssata/claude-agentic-sdlc-module/releases/tag/v4.3.0)
|
|
4
4
|
[](docs/routing.md)
|
|
5
5
|
[](docs/known-risks.md)
|
|
6
6
|
[](LICENSE)
|
|
@@ -18,7 +18,7 @@ approval. Risky actions are stopped by a hook, not by a polite request in a
|
|
|
18
18
|
prompt.
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npx riskrails@4.
|
|
21
|
+
npx riskrails@4.3.0 # detects Claude Code, Codex or both (Node 18+, Python 3.11+)
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
Then, in any project: `/rr-setup` once, and `/rr-build <what you want>` for
|
|
@@ -50,7 +50,7 @@ Tuned for **one developer who knows the codebase**, on a Claude Pro, Max or
|
|
|
50
50
|
Team plan, or a ChatGPT Plus or Pro plan, following the
|
|
51
51
|
[AI-native SDLC playbook](https://claude.com/blog/the-ai-native-sdlc-playbook).
|
|
52
52
|
|
|
53
|
-
**Version 4.
|
|
53
|
+
**Version 4.3.0.** The guards have limits: read [Known risks](docs/known-risks.md)
|
|
54
54
|
before relying on them. What is decided but not built is in [ROADMAP.md](ROADMAP.md).
|
|
55
55
|
|
|
56
56
|
## Upgrade to 4.0
|
|
@@ -64,8 +64,10 @@ the plugin. Step by step, in this order:
|
|
|
64
64
|
without the conversions it missed. So a project last updated by an older
|
|
65
65
|
riskrails goes through the version that still converts it: install it
|
|
66
66
|
(`npx riskrails@3.1.0`), run `/rr-setup --update` with it, then install 4.0.
|
|
67
|
-
2. **Commit every project's `.ai/`** first; `git restore` is the rollback.
|
|
68
|
-
|
|
67
|
+
2. **Commit every project's `.ai/`** first; `git restore` is the rollback. From
|
|
68
|
+
4.2 that commit holds the configuration: `.ai/memory/` and `.ai/reports/` are
|
|
69
|
+
git-ignored, so copy them aside if you want them back.
|
|
70
|
+
3. **Update the home** on every machine, for every runtime: `npx riskrails@4.3.0`
|
|
69
71
|
(the plugin route and the npm route are separate installs: reinstall both
|
|
70
72
|
if you use both). Restart the runtime. In Codex, open `/hooks` and trust the
|
|
71
73
|
changed entries.
|
|
@@ -84,13 +86,13 @@ are removed; use the `/rr-*` commands. Details are in
|
|
|
84
86
|
## Install
|
|
85
87
|
|
|
86
88
|
```bash
|
|
87
|
-
npx riskrails@4.
|
|
88
|
-
npx riskrails@4.
|
|
89
|
-
npx riskrails@4.
|
|
90
|
-
npx riskrails@4.
|
|
91
|
-
npx riskrails@4.
|
|
92
|
-
npx riskrails@4.
|
|
93
|
-
npx riskrails@4.
|
|
89
|
+
npx riskrails@4.3.0 # detects Claude Code, Codex, or both; proposes the detected plan
|
|
90
|
+
npx riskrails@4.3.0 --target codex # auto | claude | codex | both
|
|
91
|
+
npx riskrails@4.3.0 --plan max --fable no # pro | team-pro | team-max | max | max20 | balanced-max
|
|
92
|
+
npx riskrails@4.3.0 --codex-plan plus # plus | pro | balanced-max
|
|
93
|
+
npx riskrails@4.3.0 --context economy # the plan's compaction window; the context guard also blocks
|
|
94
|
+
npx riskrails@4.3.0 --statusline own # keep your own statusline command (from 3.2.1; default: the plugin's two-line statusline)
|
|
95
|
+
npx riskrails@4.3.0 --dry-run # print what would be written, write nothing
|
|
94
96
|
npx -y riskrails@latest --yes # update in place, keeping the recorded plan
|
|
95
97
|
```
|
|
96
98
|
|
|
@@ -102,6 +104,9 @@ stop with nothing written. Enter through every menu installs what detection
|
|
|
102
104
|
proposes. A flag skips its menu; `--yes` or no terminal asks nothing, and
|
|
103
105
|
`RISKRAILS_MENU=off` (or `TERM=dumb`) brings back the typed plan question.
|
|
104
106
|
|
|
107
|
+
<!-- wizard recording: placeholder until the human records docs/media/wizard.tape (`vhs docs/media/wizard.tape`) and puts the GIF here -->
|
|
108
|
+
*A recording of the installer wizard goes here.*
|
|
109
|
+
|
|
105
110
|
Pin the version: the hooks it installs see every command an agent runs. It
|
|
106
111
|
installs the managed block, the plan's settings and limits, the five agents
|
|
107
112
|
with their tiers baked in, the hooks, the skills, the shared tree at
|
|
@@ -172,11 +177,14 @@ until it passes. After three failed review passes the next verdict is yours.
|
|
|
172
177
|
|
|
173
178
|
### The record
|
|
174
179
|
|
|
175
|
-
A closed task's who, what, how and why
|
|
180
|
+
A closed task's who, what, how and why are kept as a compact record: `state.py
|
|
176
181
|
memory enable` (yours, in a terminal, or as your pick in the `/rr-setup`
|
|
177
182
|
dialog) switches a project to packages mode, in
|
|
178
183
|
which `close` appends the task's entries to a per-developer batch under
|
|
179
|
-
`.ai/memory
|
|
184
|
+
`.ai/memory/`. From 4.2 the record is local by default (`.ai/memory/` and
|
|
185
|
+
`.ai/reports/` are git-ignored); `state.py memory enable --share git`, in your
|
|
186
|
+
own terminal, shares it through git and installs a pre-push hook that validates
|
|
187
|
+
it, and `--share local` takes it back. `state.py recall --files …`
|
|
180
188
|
gives an agent what the record holds about the files it is about to touch,
|
|
181
189
|
within a token budget. Task ids are `T-YYYY-MM-DD-HHMM-<slug>`
|
|
182
190
|
([docs/local-memory.md](docs/local-memory.md)).
|
|
@@ -243,7 +251,7 @@ python3 -I -m unittest discover -s tests -p 'test_*.py' # CI's serial oracl
|
|
|
243
251
|
AI_E2E=1 python3 -I -m unittest discover -s tests -p test_end_to_end.py
|
|
244
252
|
```
|
|
245
253
|
|
|
246
|
-
|
|
254
|
+
Seventy-eight suites, 1569 tests, about 85 seconds in parallel. The guards against JSON fixtures for both runtimes and a
|
|
247
255
|
golden of 1 341 recorded decisions, byte for byte, with timed cases that hold
|
|
248
256
|
the hot path under its budget; the state machine, the weight, the batch loop,
|
|
249
257
|
the dialog contract and the documents rule; the compact record and its
|
|
@@ -272,6 +280,8 @@ the developer's real `~/.claude` or `~/.codex`.
|
|
|
272
280
|
| [docs/local-memory.md](docs/local-memory.md) | the compact record: task ids, packages, recall, the pre-push check, migration |
|
|
273
281
|
| [docs/known-risks.md](docs/known-risks.md) | what the guards do not protect against, and what to do about it |
|
|
274
282
|
| [docs/faq.md](docs/faq.md) | why something was blocked, and how to change it |
|
|
283
|
+
| [docs/glossary.md](docs/glossary.md) | the words the dialog, the statusline and `/rr-status` use, and what still needs a terminal |
|
|
284
|
+
| [docs/dialog-probe.md](docs/dialog-probe.md) | the checklist for Remote Control, the desktop app and the web — not yet filled |
|
|
275
285
|
| [CHANGELOG.md](CHANGELOG.md) | what changed in each release, and the upgrade notes |
|
|
276
286
|
| [docs/sdlc/README.md](docs/sdlc/README.md) | the spec → plan → build artefacts in a project |
|
|
277
287
|
| [ROADMAP.md](ROADMAP.md) | what is decided but not built; the open rows |
|
|
@@ -76,7 +76,12 @@ previous version · state-machine transitions now reachable that should not be
|
|
|
76
76
|
API and event contract changes · security problems · missing logging on a
|
|
77
77
|
critical path · a rollback that would not actually work · scope creep and
|
|
78
78
|
refactoring nobody asked for · a test weakened or skipped, or a static-analysis
|
|
79
|
-
level lowered, to make CI pass
|
|
79
|
+
level lowered, to make CI pass · a new dependency without its line in the plan,
|
|
80
|
+
or one the standard library or a short function would replace · padding, last
|
|
81
|
+
and on a first pass only, in the lines this change added: an abstraction with
|
|
82
|
+
one implementation, an option nobody sets, a block or a test that repeats
|
|
83
|
+
another, lines no requirement and no test asks for — MEDIUM at most, three
|
|
84
|
+
findings at most, each titled `padding:` and naming what to delete.
|
|
80
85
|
|
|
81
86
|
## Focus: security
|
|
82
87
|
|
|
@@ -117,6 +122,14 @@ verdict: pass | pass_with_findings | blockers_open
|
|
|
117
122
|
scope_assessment: <did the change stay inside the approved plan>
|
|
118
123
|
```
|
|
119
124
|
|
|
125
|
+
In full mode only, after VERDICT:
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
## READ FIRST
|
|
129
|
+
- <file:line> — <why a human reads this before approving> # at most five
|
|
130
|
+
unverified: <what the review could not check, and what would check it> # or: none
|
|
131
|
+
```
|
|
132
|
+
|
|
120
133
|
Label each finding with its severity's letter and a number, counted per
|
|
121
134
|
severity from 1; a re-review continues the numbers already in
|
|
122
135
|
`.ai/reports/<task-id>/task.md` §Review, when it has a pass.
|
package/ai/agents/implementer.md
CHANGED
|
@@ -37,6 +37,12 @@ do not rename anything to make the change fit.
|
|
|
37
37
|
`project/known-risks.md`.
|
|
38
38
|
- Do not reformat lines you did not change — it hides the real diff from the
|
|
39
39
|
reviewer.
|
|
40
|
+
- Write the simplest version that passes the step's tests. A branch, an option,
|
|
41
|
+
a parameter or a layer is added only when a requirement or a failing test
|
|
42
|
+
asks for it.
|
|
43
|
+
- A new dependency is not yours to add. Ask first whether the standard library
|
|
44
|
+
or one function of about fifty lines does the job; when it does not, return
|
|
45
|
+
`SCOPE_CHANGE_REQUIRED` naming the manifest and the reason.
|
|
40
46
|
|
|
41
47
|
## Output
|
|
42
48
|
|
package/ai/agents/reviewer.md
CHANGED
|
@@ -20,7 +20,14 @@ usually a review that read the diff instead of the system.
|
|
|
20
20
|
- missing logging and observability on a critical path;
|
|
21
21
|
- a rollback that would not actually work;
|
|
22
22
|
- scope creep and refactoring that nobody asked for;
|
|
23
|
-
- a test weakened, skipped, or a static-analysis level lowered to make CI pass
|
|
23
|
+
- a test weakened, skipped, or a static-analysis level lowered to make CI pass;
|
|
24
|
+
- a new dependency without its line in the plan, or one the standard library
|
|
25
|
+
or a short function would replace;
|
|
26
|
+
- padding, last and on a first pass only, in the lines this change added: an
|
|
27
|
+
abstraction with one implementation, an option nobody sets, a block or a test
|
|
28
|
+
that repeats another, lines no requirement and no test asks for — MEDIUM at
|
|
29
|
+
most, three findings at most, each titled `padding:` and naming what to
|
|
30
|
+
delete.
|
|
24
31
|
|
|
25
32
|
## Boundary
|
|
26
33
|
|
package/bin/ansi.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The installer's colour gate and the few styles the menu uses. Pure: the stream's isTTY and the
|
|
2
|
+
// environment come in as parameters. Colour goes to stderr only (spec ui-first-approvals R14).
|
|
3
|
+
'use strict';
|
|
4
|
+
|
|
5
|
+
// Colour only on a terminal, with NO_COLOR empty or unset and TERM not dumb.
|
|
6
|
+
const useColor = (isTTY, env) => isTTY === true && !env.NO_COLOR && env.TERM !== 'dumb';
|
|
7
|
+
|
|
8
|
+
const CODES = { bold: '1', dim: '2', inverse: '7' };
|
|
9
|
+
// text in the style when on is true, else unchanged.
|
|
10
|
+
const paint = (on, style, text) => (on ? `\x1b[${CODES[style]}m${text}\x1b[0m` : text);
|
|
11
|
+
|
|
12
|
+
module.exports = { useColor, paint };
|
package/bin/menu.js
CHANGED
|
@@ -11,10 +11,25 @@
|
|
|
11
11
|
// Ctrl-C/Esc/q erases the menu, exit 130
|
|
12
12
|
// no terminal exit 3, nothing drawn: the parent falls back to its typed prompt
|
|
13
13
|
// Keys: ↑/k ↓/j (stop at the ends), Home/End, 1–9 jump to that row, Enter picks.
|
|
14
|
+
//
|
|
15
|
+
// A spec with any of the keys below is drawn as a wizard screen; without them it is the 4.2.0 menu.
|
|
16
|
+
// step {"n": 3, "names": ["runtimes", "plan", "context", …]}: the stepper line, step n of names
|
|
17
|
+
// help the help line, replacing the default
|
|
18
|
+
// back true: ←, h or Backspace exit 4, nothing drawn, nothing on stdout
|
|
19
|
+
// multi true: Space toggles a row, Enter confirms the set; stdout is the checked values in
|
|
20
|
+
// item order joined by "," ("" when none is checked)
|
|
21
|
+
// checked the values checked at the start (multi)
|
|
22
|
+
// color true: styles on stderr, when ansi.useColor allows it
|
|
23
|
+
// Items may carry "desc", shown under the list while the item is highlighted. → also confirms.
|
|
24
|
+
// Exit codes: 0 pick, 3 no terminal, 4 back, 130 abort.
|
|
14
25
|
'use strict';
|
|
26
|
+
const { useColor, paint } = require('./ansi');
|
|
27
|
+
|
|
28
|
+
const WIZARD_KEYS = ['help', 'multi', 'checked', 'step', 'back', 'color'];
|
|
29
|
+
const isWizard = (spec) => WIZARD_KEYS.some((k) => spec[k] !== undefined);
|
|
15
30
|
|
|
16
31
|
// The key, as readline's keypress event gives it, as one of the reducer's tokens (or null).
|
|
17
|
-
function token(key) {
|
|
32
|
+
function token(key, wizard) {
|
|
18
33
|
if (!key) return null;
|
|
19
34
|
if (key.ctrl && key.name === 'c') return 'abort';
|
|
20
35
|
if (key.ctrl || key.meta) return null;
|
|
@@ -24,11 +39,17 @@ function token(key) {
|
|
|
24
39
|
if (name === 'home' || name === 'end') return name;
|
|
25
40
|
if (name === 'return' || name === 'enter') return 'enter';
|
|
26
41
|
if (name === 'escape' || name === 'q') return 'abort';
|
|
42
|
+
if (wizard) {
|
|
43
|
+
if (name === 'left' || name === 'h' || name === 'backspace') return 'back';
|
|
44
|
+
if (name === 'right') return 'enter';
|
|
45
|
+
if (name === 'space' || name === ' ') return 'space';
|
|
46
|
+
}
|
|
27
47
|
if (/^[1-9]$/.test(String(name))) return String(name);
|
|
28
48
|
return null;
|
|
29
49
|
}
|
|
30
50
|
|
|
31
|
-
// The pure step: {index, count, done} and a token → the next state. done is null, 'pick'
|
|
51
|
+
// The pure step: {index, count, done, checked?} and a token → the next state. done is null, 'pick',
|
|
52
|
+
// 'back' or 'abort'. checked, an array of booleans, is there only for a multi-select.
|
|
32
53
|
function reduce(state, tok) {
|
|
33
54
|
const last = state.count - 1;
|
|
34
55
|
switch (tok) {
|
|
@@ -38,6 +59,9 @@ function reduce(state, tok) {
|
|
|
38
59
|
case 'end': return { ...state, index: last };
|
|
39
60
|
case 'enter': return { ...state, done: 'pick' };
|
|
40
61
|
case 'abort': return { ...state, done: 'abort' };
|
|
62
|
+
case 'back': return { ...state, done: 'back' };
|
|
63
|
+
case 'space': return state.checked
|
|
64
|
+
? { ...state, checked: state.checked.map((c, i) => (i === state.index ? !c : c)) } : state;
|
|
41
65
|
default: {
|
|
42
66
|
const n = /^[1-9]$/.test(String(tok)) ? Number(tok) : 0;
|
|
43
67
|
return n >= 1 && n <= state.count ? { ...state, index: n - 1 } : state;
|
|
@@ -57,6 +81,28 @@ function lines(spec, index, columns) {
|
|
|
57
81
|
})];
|
|
58
82
|
}
|
|
59
83
|
|
|
84
|
+
// The wizard screen: the stepper, a blank line, the title, the rows, a blank line, the highlighted
|
|
85
|
+
// option's description and the help line. Cut to the terminal's width like lines().
|
|
86
|
+
function wizardLines(spec, state, columns, color) {
|
|
87
|
+
const fit = (t) => (columns > 1 && [...t].length > columns - 1 ? [...t].slice(0, columns - 2).join('') + '…' : t);
|
|
88
|
+
const out = [];
|
|
89
|
+
if (spec.step) {
|
|
90
|
+
const names = spec.step.names.map((nm, i) => (i + 1 === spec.step.n ? paint(color, 'bold', `[${nm}]`) : nm));
|
|
91
|
+
out.push(fit(` step ${spec.step.n} of ${spec.step.names.length} · ${names.join(' › ')}`), '');
|
|
92
|
+
}
|
|
93
|
+
out.push(fit(` ${paint(color, 'bold', spec.title)}${spec.why ? ` (${spec.why})` : ''}`));
|
|
94
|
+
const width = Math.max(...spec.items.map((it) => String(it.label).length));
|
|
95
|
+
spec.items.forEach((it, i) => {
|
|
96
|
+
const box = state.checked ? `[${state.checked[i] ? 'x' : ' '}] ` : '';
|
|
97
|
+
const row = fit(` ${i === state.index ? '❯' : ' '} ${box}${i + 1}. ${String(it.label).padEnd(width)}${it.note ? ` ${it.note}` : ''}`);
|
|
98
|
+
out.push(i === state.index ? paint(color, 'inverse', row) : row);
|
|
99
|
+
});
|
|
100
|
+
const desc = spec.items[state.index].desc;
|
|
101
|
+
const keys = spec.help || `↑/↓ move ${spec.multi ? 'Space toggle ' : ''}${spec.back ? '← back ' : ''}Enter next Esc cancel`;
|
|
102
|
+
out.push('', ...(desc ? [fit(` ${paint(color, 'dim', desc)}`)] : []), fit(` ${paint(color, 'dim', keys)}`));
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
60
106
|
function main() {
|
|
61
107
|
let spec;
|
|
62
108
|
try { spec = JSON.parse(process.env.RISKRAILS_MENU_SPEC || ''); } catch (e) { process.exit(3); }
|
|
@@ -68,9 +114,12 @@ function main() {
|
|
|
68
114
|
const write = (s) => process.stderr.write(s);
|
|
69
115
|
let drawn = 0;
|
|
70
116
|
const erase = () => { if (drawn) write(`\x1b[${drawn}A\r\x1b[J`); drawn = 0; };
|
|
71
|
-
const
|
|
117
|
+
const wizard = isWizard(spec);
|
|
118
|
+
const color = spec.color === true && useColor(process.stderr.isTTY, process.env);
|
|
119
|
+
const draw = () => {
|
|
72
120
|
erase();
|
|
73
|
-
const shown =
|
|
121
|
+
const shown = wizard ? wizardLines(spec, state, process.stderr.columns || 0, color)
|
|
122
|
+
: lines(spec, state.index, process.stderr.columns || 0);
|
|
74
123
|
write(shown.join('\n') + '\n');
|
|
75
124
|
drawn = shown.length;
|
|
76
125
|
};
|
|
@@ -83,8 +132,8 @@ function main() {
|
|
|
83
132
|
erase();
|
|
84
133
|
restore();
|
|
85
134
|
if (code === 0) {
|
|
86
|
-
const why = state.index === initial && spec.why ? ` (${spec.why})` : '';
|
|
87
|
-
write(`${spec.title}: ${value}${why}\n`);
|
|
135
|
+
const why = !spec.multi && state.index === initial && spec.why ? ` (${spec.why})` : '';
|
|
136
|
+
write(`${spec.title}: ${spec.multi ? value.split(',').join(', ') : value}${why}\n`);
|
|
88
137
|
process.stdout.write(value, () => process.exit(0));
|
|
89
138
|
} else {
|
|
90
139
|
process.exit(code);
|
|
@@ -95,22 +144,32 @@ function main() {
|
|
|
95
144
|
const initial = Number.isInteger(spec.initial) && spec.initial >= 0 && spec.initial < spec.items.length
|
|
96
145
|
? spec.initial : 0;
|
|
97
146
|
let state = { index: initial, count: spec.items.length, done: null };
|
|
147
|
+
if (spec.multi) {
|
|
148
|
+
const on = new Set((Array.isArray(spec.checked) ? spec.checked : []).map(String));
|
|
149
|
+
state.checked = spec.items.map((it) => on.has(String(it.value)));
|
|
150
|
+
}
|
|
98
151
|
// A lone Esc: emitKeypressEvents without a readline interface has no escape timeout and would
|
|
99
152
|
// wait for the rest of a sequence. An arrow key arrives as one chunk (ESC [ B), never as ESC alone.
|
|
100
153
|
stdin.on('data', (chunk) => { if (chunk.length === 1 && chunk[0] === 0x1b) finish(130); });
|
|
101
154
|
require('readline').emitKeypressEvents(stdin);
|
|
102
155
|
write('\x1b[?25l');
|
|
103
|
-
draw(
|
|
156
|
+
draw();
|
|
104
157
|
stdin.on('keypress', (_str, key) => {
|
|
105
|
-
const tok = token(key);
|
|
106
|
-
if (tok === null) return;
|
|
158
|
+
const tok = token(key, wizard);
|
|
159
|
+
if (tok === null || (tok === 'back' && !spec.back)) return;
|
|
107
160
|
const next = reduce(state, tok);
|
|
108
|
-
if (next.done === 'pick') {
|
|
161
|
+
if (next.done === 'pick') {
|
|
162
|
+
state = next;
|
|
163
|
+
const set = spec.items.filter((_it, i) => state.checked && state.checked[i]).map((it) => String(it.value));
|
|
164
|
+
finish(0, state.checked ? set.join(',') : String(spec.items[state.index].value));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
109
167
|
if (next.done === 'abort') { finish(130); return; }
|
|
110
|
-
if (next.
|
|
168
|
+
if (next.done === 'back') { finish(4); return; }
|
|
169
|
+
if (next.index !== state.index || next.checked !== state.checked) { state = next; draw(); }
|
|
111
170
|
});
|
|
112
171
|
stdin.resume();
|
|
113
172
|
}
|
|
114
173
|
|
|
115
|
-
module.exports = { reduce, token, lines };
|
|
174
|
+
module.exports = { reduce, token, lines, wizardLines };
|
|
116
175
|
if (require.main === module) main();
|