launchprep 0.5.2 → 0.5.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/package.json +1 -1
- package/src/index.mjs +30 -22
- package/src/interactive.mjs +1 -13
- package/src/report.mjs +4 -4
package/package.json
CHANGED
package/src/index.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { detectProfile, toGateProfile } from './detect.mjs';
|
|
|
5
5
|
import { gate, missingFacts } from './gate.mjs';
|
|
6
6
|
import { runChecks, runRootChecks, CHECKS } from './checks.mjs';
|
|
7
7
|
import { render } from './report.mjs';
|
|
8
|
-
import { askQuestions
|
|
8
|
+
import { askQuestions } from './interactive.mjs';
|
|
9
9
|
import { questionFor } from './questions.mjs';
|
|
10
10
|
import { existsSync, statSync } from 'node:fs';
|
|
11
11
|
|
|
@@ -153,28 +153,36 @@ if (asJson) {
|
|
|
153
153
|
shallow: r.shallow.map(s => ({ id: s.id, title: s.title, severity: s.severity })),
|
|
154
154
|
}, null, 2));
|
|
155
155
|
} else {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
156
|
+
// Will we ask the questions in the terminal? Only in a real TTY, only when
|
|
157
|
+
// there is something to unlock, and never on --json / --yes / --no-interactive
|
|
158
|
+
// or a pipe. Decided BEFORE rendering so the report can drop the "write
|
|
159
|
+
// launchprep.json" instructions it would otherwise duplicate.
|
|
160
|
+
const willPrompt = !noInteractive && process.stdin.isTTY && process.stdout.isTTY && r.questions.length > 0;
|
|
161
|
+
|
|
162
|
+
process.stdout.write(render({ repo: target, ...r, stated, interactive: willPrompt }));
|
|
163
|
+
|
|
164
|
+
if (willPrompt) {
|
|
165
|
+
// No yes/no gate: a user typed their answers into the old [Y/n] and it read
|
|
166
|
+
// the whole sentence as "no". Go straight to the questions instead, each one
|
|
167
|
+
// clearly skippable with Enter, so there is only ever one thing to type.
|
|
162
168
|
const unlocks = r.questions.reduce((n, [, c]) => n + c, 0);
|
|
163
|
-
|
|
164
|
-
`\n \x1b[
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
169
|
+
process.stdout.write(
|
|
170
|
+
`\n \x1b[36m${r.questions.length} question${r.questions.length === 1 ? '' : 's'} unlock up to ` +
|
|
171
|
+
`${unlocks} more checks\x1b[0m \x1b[2m— they can't be read from your code. ` +
|
|
172
|
+
`Type a number for each, or press Enter to skip.\x1b[0m\n`);
|
|
173
|
+
|
|
174
|
+
const answers = await askQuestions(r.questions.map(([fact]) => fact));
|
|
175
|
+
if (Object.keys(answers).length) {
|
|
176
|
+
const before = new Set(r.findings.map(keyOf));
|
|
177
|
+
const ranBefore = r.coverage.ran;
|
|
178
|
+
stated = { ...(fileStated || {}), ...answers };
|
|
179
|
+
r = analyze(stated);
|
|
180
|
+
const fresh = r.findings.filter(f => !before.has(keyOf(f)));
|
|
181
|
+
printDelta(fresh, r.coverage.ran - ranBefore);
|
|
182
|
+
} else {
|
|
183
|
+
process.stdout.write(
|
|
184
|
+
`\n \x1b[2mSkipped — the report above is unchanged. You can also put the answers in\x1b[0m` +
|
|
185
|
+
` launchprep.json \x1b[2mand run again.\x1b[0m\n\n`);
|
|
178
186
|
}
|
|
179
187
|
}
|
|
180
188
|
}
|
package/src/interactive.mjs
CHANGED
|
@@ -72,7 +72,7 @@ export async function askQuestions(facts, { input = process.stdin, output = proc
|
|
|
72
72
|
// is worse than being asked again.
|
|
73
73
|
let value = null;
|
|
74
74
|
while (value === null) {
|
|
75
|
-
const raw = (await ask(` ${C.b}›${C.o} `)).trim().toLowerCase();
|
|
75
|
+
const raw = (await ask(` ${C.d}number, or Enter to skip${C.o} ${C.b}›${C.o} `)).trim().toLowerCase();
|
|
76
76
|
if (raw === '') break; // skip this one
|
|
77
77
|
const n = Number(raw);
|
|
78
78
|
if (Number.isInteger(n) && n >= 1 && n <= shape.choices.length) value = shape.choices[n - 1];
|
|
@@ -86,15 +86,3 @@ export async function askQuestions(facts, { input = process.stdin, output = proc
|
|
|
86
86
|
}
|
|
87
87
|
return stated;
|
|
88
88
|
}
|
|
89
|
-
|
|
90
|
-
// The yes/no gate before any of it. Returns true only on an explicit yes.
|
|
91
|
-
export async function confirm(prompt, { input = process.stdin, output = process.stdout, readLine } = {}) {
|
|
92
|
-
const rl = readLine ? null : createInterface({ input, output });
|
|
93
|
-
const ask = readLine || ((q) => new Promise(res => rl.question(q, res)));
|
|
94
|
-
try {
|
|
95
|
-
const raw = (await ask(prompt)).trim().toLowerCase();
|
|
96
|
-
return raw === '' || raw === 'y' || raw === 'yes'; // default yes
|
|
97
|
-
} finally {
|
|
98
|
-
rl?.close();
|
|
99
|
-
}
|
|
100
|
-
}
|
package/src/report.mjs
CHANGED
|
@@ -8,7 +8,7 @@ const SEV = { critical:[C.red,'CRITICAL'], high:[C.yel,'HIGH'], medium:[C.blu,'M
|
|
|
8
8
|
// The wording lives in questions.mjs — 33 facts, not the 5 that had text.
|
|
9
9
|
// A customer used to read "? llm_tools_enabled" as the last line of a scan.
|
|
10
10
|
|
|
11
|
-
export function render({ repo, scanned, lead, findings, questions, shallow = [], coverage = null, stated = null }) {
|
|
11
|
+
export function render({ repo, scanned, lead, findings, questions, shallow = [], coverage = null, stated = null, interactive = false }) {
|
|
12
12
|
const L = [];
|
|
13
13
|
const p = (s = '') => L.push(s);
|
|
14
14
|
const pr = lead.profile;
|
|
@@ -130,7 +130,7 @@ export function render({ repo, scanned, lead, findings, questions, shallow = [],
|
|
|
130
130
|
p(` ${C.dim}${String(n).padStart(3)} because ${f} is ${JSON.stringify(lead.profile[f] ?? lead.profile.stack?.[f.split('.').pop()])}${C.off}`);
|
|
131
131
|
|
|
132
132
|
// ---------- the three questions ----------
|
|
133
|
-
if (questions.length) {
|
|
133
|
+
if (questions.length && !interactive) {
|
|
134
134
|
p(`\n${C.bold}${g.unknown.length} more checks need ${questions.length} answer${questions.length === 1 ? '' : 's'}${C.off}`);
|
|
135
135
|
for (const [factName, n] of questions) {
|
|
136
136
|
const q = questionFor(factName);
|
|
@@ -142,9 +142,9 @@ export function render({ repo, scanned, lead, findings, questions, shallow = [],
|
|
|
142
142
|
// This used to say "Correct it and the checks adjust" with no way on earth to
|
|
143
143
|
// do so — no flag, no file, no prompt. It was the last line of every scan and
|
|
144
144
|
// it invited an action that did not exist. Now it names the file that works.
|
|
145
|
-
if (questions.length)
|
|
145
|
+
if (questions.length && !interactive)
|
|
146
146
|
p(`\n ${C.dim}Put those in ${C.off}launchprep.json${C.dim} at the top of your project and run again — the answers unlock the checks above.${C.off}\n`);
|
|
147
|
-
else
|
|
147
|
+
else if (!interactive)
|
|
148
148
|
p(`\n ${C.dim}Wrong about your app? Correct it in ${C.off}launchprep.json${C.dim} and run again.${C.off}\n`);
|
|
149
149
|
return L.join('\n');
|
|
150
150
|
}
|