shippingszn 0.1.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/dist/index.js ADDED
@@ -0,0 +1,194 @@
1
+ #!/usr/bin/env node
2
+ import * as path from "node:path";
3
+ import * as process from "node:process";
4
+ import { ALL_CHECKS } from "./checks.js";
5
+ import { listFiles } from "./scan.js";
6
+ import { CHECKLIST_ITEMS, permalinkFor } from "./items.js";
7
+ const DEFAULT_BASE_URL = "https://shippingszn.com";
8
+ const PKG_VERSION = "0.1.0";
9
+ function parseArgs(argv) {
10
+ const opts = {
11
+ cwd: process.cwd(),
12
+ json: false,
13
+ baseUrl: process.env.VIBE_LAUNCH_CHECK_BASE_URL ?? DEFAULT_BASE_URL,
14
+ help: false,
15
+ version: false,
16
+ noColor: !!process.env.NO_COLOR,
17
+ };
18
+ for (let i = 0; i < argv.length; i++) {
19
+ const a = argv[i];
20
+ if (a === "--help" || a === "-h")
21
+ opts.help = true;
22
+ else if (a === "--version" || a === "-v")
23
+ opts.version = true;
24
+ else if (a === "--json")
25
+ opts.json = true;
26
+ else if (a === "--no-color")
27
+ opts.noColor = true;
28
+ else if (a === "--base-url")
29
+ opts.baseUrl = argv[++i] ?? opts.baseUrl;
30
+ else if (a === "--cwd")
31
+ opts.cwd = path.resolve(argv[++i] ?? opts.cwd);
32
+ else if (!a.startsWith("-"))
33
+ opts.cwd = path.resolve(a);
34
+ }
35
+ return opts;
36
+ }
37
+ function color(enabled) {
38
+ const wrap = (codes) => (s) => enabled ? `\x1b[${codes}m${s}\x1b[0m` : s;
39
+ return {
40
+ bold: wrap("1"),
41
+ dim: wrap("2"),
42
+ red: wrap("31"),
43
+ yellow: wrap("33"),
44
+ blue: wrap("34"),
45
+ cyan: wrap("36"),
46
+ green: wrap("32"),
47
+ magenta: wrap("35"),
48
+ gray: wrap("90"),
49
+ };
50
+ }
51
+ const SEVERITY_ORDER = ["critical", "high", "medium", "lower"];
52
+ const SEVERITY_LABEL = {
53
+ critical: "CRITICAL",
54
+ high: "HIGH",
55
+ medium: "MEDIUM",
56
+ lower: "LOWER",
57
+ };
58
+ function printHelp() {
59
+ process.stdout.write(`shippingszn v${PKG_VERSION}
60
+
61
+ Read-only scanner that checks the current project against a small set of
62
+ high-signal items from the Vibe Coder Launch Checklist.
63
+
64
+ Usage:
65
+ npx shippingszn [path] [options]
66
+
67
+ Options:
68
+ --json Output a machine-readable JSON report.
69
+ --base-url <url> Base URL used to build links back to checklist items.
70
+ (default: ${DEFAULT_BASE_URL})
71
+ --cwd <path> Directory to scan. Default: current working directory.
72
+ --no-color Disable ANSI colors in the human-readable report.
73
+ -h, --help Show this help.
74
+ -v, --version Print version.
75
+
76
+ The scanner only reads files. It never writes, modifies, or deletes anything.
77
+ Exit code is non-zero if any Critical findings are detected.
78
+ `);
79
+ }
80
+ async function run() {
81
+ const opts = parseArgs(process.argv.slice(2));
82
+ if (opts.help) {
83
+ printHelp();
84
+ return 0;
85
+ }
86
+ if (opts.version) {
87
+ process.stdout.write(`${PKG_VERSION}\n`);
88
+ return 0;
89
+ }
90
+ const c = color(!opts.noColor && process.stdout.isTTY === true && !opts.json);
91
+ const files = await listFiles(opts.cwd);
92
+ const ctx = { rootDir: opts.cwd, files };
93
+ const all = [];
94
+ for (const check of ALL_CHECKS) {
95
+ try {
96
+ const out = await check.run(ctx);
97
+ all.push(...out);
98
+ }
99
+ catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ all.push({
102
+ checkId: `${check.id}:error`,
103
+ itemId: "ai-audit",
104
+ severity: "lower",
105
+ message: `Check ${check.id} crashed: ${msg}`,
106
+ });
107
+ }
108
+ }
109
+ const enriched = all.map((f) => {
110
+ const item = CHECKLIST_ITEMS[f.itemId];
111
+ return {
112
+ ...f,
113
+ itemTitle: item?.title ?? f.itemId,
114
+ permalink: permalinkFor(f.itemId, opts.baseUrl),
115
+ };
116
+ });
117
+ enriched.sort((a, b) => {
118
+ const sa = SEVERITY_ORDER.indexOf(a.severity);
119
+ const sb = SEVERITY_ORDER.indexOf(b.severity);
120
+ if (sa !== sb)
121
+ return sa - sb;
122
+ if (a.itemId !== b.itemId)
123
+ return a.itemId.localeCompare(b.itemId);
124
+ return a.checkId.localeCompare(b.checkId);
125
+ });
126
+ const totals = {
127
+ critical: 0,
128
+ high: 0,
129
+ medium: 0,
130
+ lower: 0,
131
+ };
132
+ for (const f of enriched)
133
+ totals[f.severity]++;
134
+ const report = {
135
+ generatedAt: new Date().toISOString(),
136
+ baseUrl: opts.baseUrl,
137
+ cwd: opts.cwd,
138
+ filesScanned: files.length,
139
+ totals,
140
+ findings: enriched,
141
+ };
142
+ if (opts.json) {
143
+ process.stdout.write(JSON.stringify(report, null, 2) + "\n");
144
+ return totals.critical > 0 ? 1 : 0;
145
+ }
146
+ // Human-readable report
147
+ const sevColor = (s) => {
148
+ if (s === "critical")
149
+ return c.red;
150
+ if (s === "high")
151
+ return c.yellow;
152
+ if (s === "medium")
153
+ return c.blue;
154
+ return c.gray;
155
+ };
156
+ process.stdout.write(`\n${c.bold("shippingszn")} ${c.dim(`v${PKG_VERSION}`)}\n`);
157
+ process.stdout.write(c.dim(`Scanned ${files.length} files in ${opts.cwd}\n\n`));
158
+ if (enriched.length === 0) {
159
+ process.stdout.write(c.green("✓ No findings. Nice work — still walk through the full checklist before launch.\n\n"));
160
+ return 0;
161
+ }
162
+ // Strip ASCII control characters (including ESC) so a maliciously-named
163
+ // file or matched secret slice cannot inject ANSI escape sequences into
164
+ // the operator's terminal.
165
+ const safe = (s) => s.replace(/[\x00-\x1f\x7f]/g, "?");
166
+ for (const sev of SEVERITY_ORDER) {
167
+ const group = enriched.filter((f) => f.severity === sev);
168
+ if (group.length === 0)
169
+ continue;
170
+ process.stdout.write(`${sevColor(sev)(c.bold(`${SEVERITY_LABEL[sev]} (${group.length})`))}\n`);
171
+ for (const f of group) {
172
+ const loc = f.file
173
+ ? ` ${c.dim(`— ${safe(f.file)}${f.line ? `:${f.line}` : ""}`)}`
174
+ : "";
175
+ process.stdout.write(` ${c.bold("•")} ${safe(f.message)}${loc}\n`);
176
+ if (f.evidence) {
177
+ process.stdout.write(` ${c.dim(`evidence: ${safe(f.evidence)}`)}\n`);
178
+ }
179
+ process.stdout.write(` ${c.cyan(`→ ${safe(f.itemTitle)}`)} ${c.dim(f.permalink)}\n`);
180
+ }
181
+ process.stdout.write("\n");
182
+ }
183
+ process.stdout.write(`${c.bold("Summary:")} ${c.red(`${totals.critical} critical`)}, ${c.yellow(`${totals.high} high`)}, ${c.blue(`${totals.medium} medium`)}, ${c.gray(`${totals.lower} lower`)}\n`);
184
+ if (totals.critical > 0) {
185
+ process.stdout.write(c.red("\nCritical findings detected. Exiting with code 1.\n"));
186
+ return 1;
187
+ }
188
+ process.stdout.write(c.dim("\nNo critical findings. Open the linked checklist items to dig deeper.\n"));
189
+ return 0;
190
+ }
191
+ run().then((code) => process.exit(code), (err) => {
192
+ process.stderr.write(`shippingszn failed: ${err instanceof Error ? err.message : String(err)}\n`);
193
+ process.exit(2);
194
+ });
package/dist/items.js ADDED
@@ -0,0 +1,46 @@
1
+ export const CHECKLIST_ITEMS = {
2
+ secrets: {
3
+ id: "secrets",
4
+ title: "Lock up your API keys and passwords",
5
+ priority: "critical",
6
+ },
7
+ "common-attacks": {
8
+ id: "common-attacks",
9
+ title: "Block the most common automated attacks",
10
+ priority: "critical",
11
+ },
12
+ "https-headers": {
13
+ id: "https-headers",
14
+ title: "Force HTTPS and add browser-level defenses",
15
+ priority: "critical",
16
+ },
17
+ "dev-prod-data": {
18
+ id: "dev-prod-data",
19
+ title: "Keep your test data away from real users",
20
+ priority: "critical",
21
+ },
22
+ github: {
23
+ id: "github",
24
+ title: "Get your code into GitHub safely",
25
+ priority: "high",
26
+ },
27
+ seo: {
28
+ id: "seo",
29
+ title: "Make sure search engines and link previews work",
30
+ priority: "medium",
31
+ },
32
+ "launch-polish": {
33
+ id: "launch-polish",
34
+ title: "Last-mile launch polish",
35
+ priority: "medium",
36
+ },
37
+ "ai-audit": {
38
+ id: "ai-audit",
39
+ title: "Audit what your AI builder actually shipped",
40
+ priority: "critical",
41
+ },
42
+ };
43
+ export function permalinkFor(itemId, baseUrl) {
44
+ const trimmed = baseUrl.replace(/\/+$/, "");
45
+ return `${trimmed}/i/${itemId}`;
46
+ }
package/dist/scan.js ADDED
@@ -0,0 +1,171 @@
1
+ import { promises as fs } from "node:fs";
2
+ import * as path from "node:path";
3
+ const DEFAULT_IGNORES = new Set([
4
+ "node_modules",
5
+ ".git",
6
+ "dist",
7
+ "build",
8
+ ".next",
9
+ ".nuxt",
10
+ ".turbo",
11
+ ".cache",
12
+ ".vercel",
13
+ ".netlify",
14
+ "out",
15
+ "coverage",
16
+ ".pnpm-store",
17
+ ".yarn",
18
+ ".expo",
19
+ ".local",
20
+ "attached_assets",
21
+ "vendor",
22
+ ]);
23
+ const TEXT_EXT = new Set([
24
+ ".ts",
25
+ ".tsx",
26
+ ".js",
27
+ ".jsx",
28
+ ".mjs",
29
+ ".cjs",
30
+ ".json",
31
+ ".md",
32
+ ".mdx",
33
+ ".html",
34
+ ".htm",
35
+ ".css",
36
+ ".scss",
37
+ ".vue",
38
+ ".svelte",
39
+ ".astro",
40
+ ".py",
41
+ ".rb",
42
+ ".go",
43
+ ".rs",
44
+ ".java",
45
+ ".kt",
46
+ ".swift",
47
+ ".php",
48
+ ".cs",
49
+ ".env",
50
+ ".example",
51
+ ".sample",
52
+ ".local",
53
+ ".yaml",
54
+ ".yml",
55
+ ".toml",
56
+ ".ini",
57
+ ".conf",
58
+ ".sh",
59
+ ]);
60
+ const MAX_FILE_BYTES = 512 * 1024;
61
+ const MAX_DEPTH = 24;
62
+ const MAX_FILES = 50_000;
63
+ export async function listFiles(rootDir) {
64
+ const out = [];
65
+ const visited = new Set();
66
+ const rootResolved = path.resolve(rootDir);
67
+ async function walk(dir, depth) {
68
+ if (depth > MAX_DEPTH)
69
+ return;
70
+ if (out.length >= MAX_FILES)
71
+ return;
72
+ let entries;
73
+ try {
74
+ entries = await fs.readdir(dir, { withFileTypes: true });
75
+ }
76
+ catch {
77
+ return;
78
+ }
79
+ for (const entry of entries) {
80
+ if (out.length >= MAX_FILES)
81
+ return;
82
+ if (entry.name.startsWith(".") && DEFAULT_IGNORES.has(entry.name))
83
+ continue;
84
+ if (DEFAULT_IGNORES.has(entry.name))
85
+ continue;
86
+ const abs = path.join(dir, entry.name);
87
+ // Refuse to follow symlinks: prevents arbitrary file reads outside the
88
+ // target tree and protects against symlink loops causing exhaustion.
89
+ if (entry.isSymbolicLink())
90
+ continue;
91
+ if (entry.isDirectory()) {
92
+ // Skip test fixture directories: these intentionally contain
93
+ // bad/insecure inputs (fake secrets, missing files, etc.) used to
94
+ // exercise the checks themselves. Counting them in a scan of a
95
+ // consumer repo would produce noisy false positives and would also
96
+ // break CI for this repo's own PR scan.
97
+ if (entry.name === "fixtures") {
98
+ const parentBase = path.basename(dir);
99
+ if (parentBase === "test" ||
100
+ parentBase === "tests" ||
101
+ parentBase === "__tests__") {
102
+ continue;
103
+ }
104
+ }
105
+ let real;
106
+ try {
107
+ real = await fs.realpath(abs);
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ // Stay within the original root and avoid revisiting the same
113
+ // directory through hard-linked or aliased paths. Use path.relative
114
+ // (separator-aware) so that a sibling like "/root-sibling" can never
115
+ // sneak past a naive prefix check on "/root".
116
+ const relFromRoot = path.relative(rootResolved, real);
117
+ if (relFromRoot.startsWith("..") || path.isAbsolute(relFromRoot)) {
118
+ continue;
119
+ }
120
+ if (visited.has(real))
121
+ continue;
122
+ visited.add(real);
123
+ await walk(abs, depth + 1);
124
+ }
125
+ else if (entry.isFile()) {
126
+ const rel = path.relative(rootDir, abs);
127
+ let size = 0;
128
+ try {
129
+ const st = await fs.stat(abs);
130
+ size = st.size;
131
+ }
132
+ catch {
133
+ continue;
134
+ }
135
+ out.push({ absPath: abs, relPath: rel, size });
136
+ }
137
+ }
138
+ }
139
+ await walk(rootDir, 0);
140
+ return out;
141
+ }
142
+ export function isTextFile(file) {
143
+ const base = path.basename(file.relPath).toLowerCase();
144
+ if (base.startsWith(".env"))
145
+ return true;
146
+ if (base === "dockerfile" || base === "makefile" || base === "procfile")
147
+ return true;
148
+ const ext = path.extname(file.relPath).toLowerCase();
149
+ if (TEXT_EXT.has(ext))
150
+ return true;
151
+ return false;
152
+ }
153
+ export async function readFileSafe(file) {
154
+ if (file.size > MAX_FILE_BYTES)
155
+ return null;
156
+ try {
157
+ return await fs.readFile(file.absPath, "utf8");
158
+ }
159
+ catch {
160
+ return null;
161
+ }
162
+ }
163
+ export async function fileExists(p) {
164
+ try {
165
+ await fs.stat(p);
166
+ return true;
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "shippingszn",
3
+ "version": "0.1.0",
4
+ "description": "Read-only CLI scanner that checks a project for common pre-launch issues from the shippingszn.com launch checklist.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "shippingszn": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
15
+ "keywords": [
16
+ "cli",
17
+ "launch",
18
+ "checklist",
19
+ "preflight",
20
+ "security",
21
+ "audit",
22
+ "scanner",
23
+ "ship",
24
+ "shippingszn",
25
+ "vibe-coder"
26
+ ],
27
+ "homepage": "https://shippingszn.com",
28
+ "bugs": {
29
+ "url": "https://github.com/keeptahoeblueish/shippingszn-cli/issues"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/keeptahoeblueish/shippingszn-cli.git"
34
+ },
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "dev": "tsx ./src/index.ts",
41
+ "start": "node ./dist/index.js",
42
+ "test": "tsx --test ./test/*.test.ts",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "clean": "rm -rf dist",
45
+ "prepublishOnly": "npm run clean && npm run build && npm test"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "^25.3.3",
49
+ "tsx": "4.21.0",
50
+ "typescript": "^5.6.3"
51
+ },
52
+ "publishConfig": {
53
+ "access": "public",
54
+ "registry": "https://registry.npmjs.org/"
55
+ }
56
+ }