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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchprep",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Launch readiness checker. Reads your code and tells you what will break before your users find out.",
5
5
  "type": "module",
6
6
  "bin": {
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, confirm } from './interactive.mjs';
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
- process.stdout.write(render({ repo: target, ...r, stated }));
157
-
158
- // The Q&A. Only in a real terminal, only when there is something to unlock,
159
- // and never when the caller opted out - so a pipe, --json or a CI job is
160
- // untouched. Answers are held in memory and merged; no file is written.
161
- if (!noInteractive && process.stdin.isTTY && process.stdout.isTTY && r.questions.length) {
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
- const yes = await confirm(
164
- `\n \x1b[36mAnswer ${r.questions.length} quick question${r.questions.length === 1 ? '' : 's'}` +
165
- ` to unlock up to ${unlocks} more checks?\x1b[0m \x1b[2m[Y/n]\x1b[0m `);
166
- if (yes) {
167
- const answers = await askQuestions(r.questions.map(([fact]) => fact));
168
- if (Object.keys(answers).length) {
169
- const before = new Set(r.findings.map(keyOf));
170
- const ranBefore = r.coverage.ran;
171
- stated = { ...(fileStated || {}), ...answers };
172
- r = analyze(stated);
173
- const fresh = r.findings.filter(f => !before.has(keyOf(f)));
174
- printDelta(fresh, r.coverage.ran - ranBefore);
175
- } else {
176
- process.stdout.write(`\n \x1b[2mNothing answered — the report above is unchanged.\x1b[0m\n\n`);
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
  }
@@ -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
  }