launchprep 0.2.0 → 0.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/LICENSE ADDED
@@ -0,0 +1,31 @@
1
+ Launchprep Licence
2
+
3
+ Copyright (c) 2026 Bc. Bektur Aibekov, IČO 29522471, Prague, Czech Republic.
4
+ All rights reserved.
5
+
6
+ You may:
7
+
8
+ 1. Install and run this software to scan source code that you own or are
9
+ authorised to scan.
10
+ 2. Make copies as reasonably necessary for that use (for example, inside a
11
+ CI pipeline or a container image you operate).
12
+
13
+ You may not:
14
+
15
+ 3. Copy, redistribute, sublicense, sell, or publish this software or any
16
+ part of it, including the rule and check definitions it contains.
17
+ 4. Extract, reproduce, or adapt the rule and check definitions for use in
18
+ any other product or service, commercial or not.
19
+ 5. Remove or alter this notice.
20
+
21
+ Versions 0.1.0 and 0.2.0 of the "launchprep" npm package were published under
22
+ the MIT licence; that grant stands for those versions. This licence applies to
23
+ this version and all later ones.
24
+
25
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
26
+ IMPLIED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR
27
+ OTHER LIABILITY ARISING FROM THE USE OF THE SOFTWARE. Findings are produced
28
+ by automated analysis and may be incomplete or wrong; they are information,
29
+ not a guarantee of security or fitness for launch.
30
+
31
+ Questions and permissions: hello@launchprep.dev
package/README.md CHANGED
@@ -13,7 +13,7 @@ No signup. No dashboard. Nothing added to your repo.
13
13
 
14
14
  ## What it does
15
15
 
16
- 288 checks — but it works out what you built **before** running any of them, so
16
+ 289 checks — but it works out what you built **before** running any of them, so
17
17
  you are not told about problems you cannot have. A static site is asked 48
18
18
  questions; a multi-tenant SaaS with payments and AI is asked 158.
19
19
 
@@ -69,6 +69,19 @@ the report link keeps working, and that link expires.
69
69
 
70
70
  If there is no terminal — CI, a script — it refuses rather than assuming yes.
71
71
 
72
+ ## In CI
73
+
74
+ ```bash
75
+ npx launchprep . --fail-on critical # exit 1 if a critical is found
76
+ npx launchprep deep . --yes --fail-on high # the paid scan as a gate
77
+ ```
78
+
79
+ Without `--fail-on` the exit code stays 0 — a report, not a verdict. With it,
80
+ findings at or above the threshold (`critical`, `high`, `medium`, `low`, `any`)
81
+ fail the build. On a deep scan, checks that did not complete fail it too — a
82
+ check that could not run has not passed. A path that does not exist always
83
+ exits 2: a scan of nothing must never look like a clean scan of something.
84
+
72
85
  ## Free and paid
73
86
 
74
87
  Everything above is free, unlimited, forever — every check a program can make on
@@ -81,4 +94,6 @@ delete. See <https://launchprep.dev>.
81
94
 
82
95
  ## Licence
83
96
 
84
- MIT
97
+ Proprietary — see the LICENSE file. Run it on anything you are authorised to
98
+ scan; the rules and checks may not be copied into other products. Versions
99
+ 0.1.0 and 0.2.0 were published under MIT and remain so.
package/net/commands.mjs CHANGED
@@ -99,6 +99,12 @@ async function deep(args) {
99
99
  console.error(`\n ${C.red}Too many requests.${C.off} ${C.d}Try again in ${backoff(r)}. No scan was used.${C.off}\n`);
100
100
  process.exit(1);
101
101
  }
102
+ if (r.status === 402 && (r.data?.error === 'attempt_ceiling' || r.data?.error === 'spend_ceiling')) {
103
+ console.error(`\n ${C.red}This key has run too many scans that did not finish.${C.off}`);
104
+ console.error(` ${C.d}${r.data.attempts} attempts against a ${r.data.limit}-scan licence. Something is going`);
105
+ console.error(` wrong on our side rather than yours — mail ${BRAND.email} and we will sort it out.${C.off}\n`);
106
+ process.exit(1);
107
+ }
102
108
  if (r.status === 402) {
103
109
  console.error(`\n ${C.red}No deep scans left${C.off} ${C.d}(${r.data?.used}/${r.data?.limit} used)${C.off}\n`);
104
110
  process.exit(1);
@@ -127,12 +133,47 @@ async function deep(args) {
127
133
  console.log(` ${f.detail}`);
128
134
  console.log(` ${C.grn}Fix${C.off} ${f.fix}\n`);
129
135
  }
136
+ // Printed BEFORE the report link, and before the scans-left line, because a
137
+ // reader who stops at the findings must still have seen this. A check that
138
+ // did not run is a hole in the answer, not a footnote to it.
139
+ const missed = r.data.incomplete || [];
140
+ if (missed.length) {
141
+ console.log(`${C.red}${C.b}${missed.length} check${missed.length === 1 ? '' : 's'} did not complete${C.off}`);
142
+ console.log(` ${C.d}These are not a pass. Nothing below was ruled out.${C.off}`);
143
+ for (const m of missed.slice(0, 8)) {
144
+ console.log(` ${C.red}~${C.off} ${C.g}${m.id.padEnd(12)}${C.off}${C.d}${(m.title || '').slice(0, 58)}${C.off}`);
145
+ }
146
+ if (missed.length > 8) console.log(` ${C.d}and ${missed.length - 8} more${C.off}`);
147
+ if (r.data.coverage !== undefined) {
148
+ console.log(` ${C.d}${Math.round(r.data.coverage * 100)}% of the ${r.data.rulesAsked} checks in this scan answered.${C.off}`);
149
+ }
150
+ if (r.data.refunded) {
151
+ console.log(` ${C.grn}This scan did not finish, so it does not count against your five.${C.off}`);
152
+ }
153
+ console.log(` ${C.d}Run it again and they usually complete.${C.off}\n`);
154
+ }
155
+
130
156
  if (r.data.report?.url) {
131
157
  console.log(`${C.b}Your report${C.off}`);
132
158
  console.log(` ${C.o}${r.data.report.url}${C.off}`);
133
159
  console.log(` ${C.d}dated, shareable, and it expires in ${r.data.report.expiresInDays} days${C.off}\n`);
134
160
  }
135
161
  console.log(` ${C.d}${r.data.scansRemaining} deep scan${r.data.scansRemaining === 1 ? '' : 's'} left${C.off}\n`);
162
+
163
+ // --fail-on <severity>: the CI gate, same contract as the free scan. The
164
+ // human report above has fully printed; the pipeline now gets its verdict.
165
+ // A scan with holes trips the gate too — a check that did not run has not
166
+ // passed, and a gate that shrugs at that is a gate that waves through the
167
+ // exact case it exists for.
168
+ const failOn = args[args.indexOf('--fail-on') + 1];
169
+ if (args.includes('--fail-on')) {
170
+ if (!(failOn in rank) && failOn !== 'any') {
171
+ console.error(' --fail-on must be one of: critical, high, medium, low, any');
172
+ process.exit(2);
173
+ }
174
+ const limit = failOn === 'any' ? 3 : rank[failOn];
175
+ if (findings.some(f => rank[f.severity] <= limit) || missed.length) process.exit(1);
176
+ }
136
177
  }
137
178
 
138
179
  function help() {
@@ -145,8 +186,10 @@ ${C.b}${BRAND.name}${C.off} ${C.d}${BRAND.tagline}${C.off}
145
186
  ${C.b}npx ${BRAND.slug} logout${C.off} forget the key
146
187
  ${C.b}npx ${BRAND.slug} deep${C.off} ${C.d}[path]${C.off} the paid scan — asks before sending anything
147
188
 
148
- ${C.d}--yes${C.off} skip the upload confirmation ${C.d}(for CI)${C.off}
149
- ${C.d}--json${C.off} machine readable ${C.d}(free scan only)${C.off}
189
+ ${C.d}--yes${C.off} skip the upload confirmation ${C.d}(for CI)${C.off}
190
+ ${C.d}--json${C.off} machine readable ${C.d}(free scan only)${C.off}
191
+ ${C.d}--fail-on${C.off} ${C.d}<sev>${C.off} exit 1 if findings reach this severity — fails a CI build
192
+ ${C.d}critical | high | medium | low | any${C.off}
150
193
 
151
194
  ${C.d}https://${BRAND.slug}.dev${C.off}
152
195
  `);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "launchprep",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
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": {
@@ -11,12 +11,15 @@
11
11
  "net",
12
12
  "bin",
13
13
  "scripts/verify-readonly.mjs",
14
- "README.md"
14
+ "README.md",
15
+ "LICENSE"
15
16
  ],
16
17
  "scripts": {
17
18
  "verify": "node scripts/verify-readonly.mjs",
18
19
  "pretest": "node scripts/verify-readonly.mjs",
19
- "test": "node test/run.mjs"
20
+ "test": "node test/run.mjs && node test/gate.mjs",
21
+ "prepack": "node scripts/prepare-publish.mjs",
22
+ "postpack": "node scripts/restore-after-publish.mjs"
20
23
  },
21
24
  "keywords": [
22
25
  "security",
@@ -29,13 +32,11 @@
29
32
  "checklist"
30
33
  ],
31
34
  "homepage": "https://launchprep.dev",
32
- "repository": {
33
- "type": "git",
34
- "url": "git+https://github.com/312labs/launchprep-app.git",
35
- "directory": "cli"
36
- },
37
- "license": "MIT",
35
+ "license": "SEE LICENSE IN LICENSE",
38
36
  "engines": {
39
37
  "node": ">=18"
38
+ },
39
+ "bugs": {
40
+ "email": "hello@launchprep.dev"
40
41
  }
41
42
  }
package/src/index.mjs CHANGED
@@ -5,9 +5,38 @@ import { detectProfile, toGateProfile } from './detect.mjs';
5
5
  import { gate, missingFacts } from './gate.mjs';
6
6
  import { runChecks, runRootChecks } from './checks.mjs';
7
7
  import { render } from './report.mjs';
8
+ import { existsSync, statSync } from 'node:fs';
8
9
 
9
- const target = process.argv[2] || process.cwd();
10
- const asJson = process.argv.includes('--json');
10
+ const args = process.argv.slice(2);
11
+ const target = args.find(a => !a.startsWith('-')) || process.cwd();
12
+ const asJson = args.includes('--json');
13
+
14
+ // --fail-on <severity>: the CI gate. Findings at or above the threshold turn
15
+ // the exit code to 1 so a pipeline can stop the deploy. Opt-in on purpose —
16
+ // a person reading the report in a terminal gets information, a robot gets a
17
+ // verdict only when asked for one.
18
+ const RANK_GATE = { critical: 0, high: 1, medium: 2, low: 3, any: 3 };
19
+ let failOn = null;
20
+ {
21
+ const i = args.indexOf('--fail-on');
22
+ if (i !== -1) {
23
+ failOn = args[i + 1];
24
+ if (!(failOn in RANK_GATE)) {
25
+ process.stderr.write(`\n --fail-on must be one of: critical, high, medium, low, any\n\n`);
26
+ process.exit(2);
27
+ }
28
+ }
29
+ }
30
+
31
+ // A path that does not exist must be a loud error, never a clean report. A
32
+ // scan of nothing looks exactly like a scan that found nothing, and telling
33
+ // someone they are safe because they mistyped a folder name is the one thing
34
+ // this tool must never do.
35
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
36
+ process.stderr.write(`\n \x1b[31mThat path does not exist: ${target}\x1b[0m\n`);
37
+ process.stderr.write(` \x1b[2mNothing was scanned. Check the folder name and run it again.\x1b[0m\n\n`);
38
+ process.exit(2);
39
+ }
11
40
 
12
41
  const repo = scanRepo(target, { maxFiles: 12000 });
13
42
  const packages = splitWorkspaces(repo);
@@ -69,3 +98,9 @@ if (asJson) {
69
98
  } else {
70
99
  process.stdout.write(render({ repo: target, scanned, lead, findings, questions, shallow }));
71
100
  }
101
+
102
+ // The gate, after the full report has printed: the human still sees
103
+ // everything, and the pipeline sees the verdict it asked for.
104
+ if (failOn && findings.some(f => RANK[f.severity] <= RANK_GATE[failOn])) {
105
+ process.exit(1);
106
+ }