shipready 1.0.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.
@@ -0,0 +1,100 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import fg from "fast-glob";
4
+ /** Glob patterns that are always ignored when scanning. */
5
+ export const IGNORE_PATTERNS = [
6
+ "node_modules/**",
7
+ ".git/**",
8
+ "dist/**",
9
+ "build/**",
10
+ ".next/**",
11
+ "out/**",
12
+ "coverage/**",
13
+ ".cache/**",
14
+ ];
15
+ /** Extensions we treat as scannable text/code files. */
16
+ export const CODE_EXTENSIONS = [
17
+ "js",
18
+ "jsx",
19
+ "ts",
20
+ "tsx",
21
+ "mjs",
22
+ "cjs",
23
+ "mts",
24
+ "cts",
25
+ "vue",
26
+ "svelte",
27
+ "py",
28
+ "rb",
29
+ "go",
30
+ "rs",
31
+ "java",
32
+ "php",
33
+ "json",
34
+ "yml",
35
+ "yaml",
36
+ "toml",
37
+ "env",
38
+ "sh",
39
+ ];
40
+ /** Returns true if the file exists (relative to root or absolute). */
41
+ export function fileExists(root, rel) {
42
+ return fs.existsSync(path.join(root, rel));
43
+ }
44
+ /** Reads a text file safely; returns null on failure. */
45
+ export function readTextFile(root, rel) {
46
+ try {
47
+ return fs.readFileSync(path.join(root, rel), "utf8");
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ /** Reads and parses JSON safely; returns null on failure. */
54
+ export function readJsonFile(root, rel) {
55
+ const raw = readTextFile(root, rel);
56
+ if (raw === null)
57
+ return null;
58
+ try {
59
+ return JSON.parse(raw);
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ /** Simple binary sniff: checks for NUL bytes in the first chunk. */
66
+ export function isProbablyBinary(root, rel) {
67
+ try {
68
+ const fd = fs.openSync(path.join(root, rel), "r");
69
+ const buf = Buffer.alloc(512);
70
+ const bytes = fs.readSync(fd, buf, 0, 512, 0);
71
+ fs.closeSync(fd);
72
+ for (let i = 0; i < bytes; i++) {
73
+ if (buf[i] === 0)
74
+ return true;
75
+ }
76
+ return false;
77
+ }
78
+ catch {
79
+ return true;
80
+ }
81
+ }
82
+ /** Finds all scannable source files under root, honoring ignore patterns. */
83
+ export async function findSourceFiles(root, extraIgnore = []) {
84
+ const pattern = `**/*.{${CODE_EXTENSIONS.join(",")}}`;
85
+ const files = await fg([pattern, ".env*", "**/.env*"], {
86
+ cwd: root,
87
+ ignore: [...IGNORE_PATTERNS, ...extraIgnore],
88
+ dot: true,
89
+ onlyFiles: true,
90
+ followSymbolicLinks: false,
91
+ unique: true,
92
+ });
93
+ return files.sort();
94
+ }
95
+ /** Writes a text file, creating parent directories as needed. */
96
+ export function writeTextFile(root, rel, content) {
97
+ const abs = path.join(root, rel);
98
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
99
+ fs.writeFileSync(abs, content, "utf8");
100
+ }
@@ -0,0 +1,68 @@
1
+ import { fileExists } from "./files.js";
2
+ /** Detects the package manager based on lockfiles. */
3
+ export function detectPackageManager(root) {
4
+ if (fileExists(root, "pnpm-lock.yaml"))
5
+ return "pnpm";
6
+ if (fileExists(root, "yarn.lock"))
7
+ return "yarn";
8
+ if (fileExists(root, "bun.lockb") || fileExists(root, "bun.lock"))
9
+ return "bun";
10
+ if (fileExists(root, "package-lock.json"))
11
+ return "npm";
12
+ return "unknown";
13
+ }
14
+ /** Detects the framework from package.json dependencies. */
15
+ export function detectFramework(pkg) {
16
+ if (!pkg)
17
+ return "unknown";
18
+ const deps = {
19
+ ...pkg.dependencies,
20
+ ...pkg.devDependencies,
21
+ };
22
+ // Order matters: meta-frameworks before their underlying libraries.
23
+ if (deps["next"])
24
+ return "Next.js";
25
+ if (deps["@nestjs/core"])
26
+ return "NestJS";
27
+ if (deps["nuxt"] || deps["vue"])
28
+ return "Vue";
29
+ if (deps["svelte"] || deps["@sveltejs/kit"])
30
+ return "Svelte";
31
+ if (deps["vite"] && deps["react"])
32
+ return "Vite";
33
+ if (deps["vite"])
34
+ return "Vite";
35
+ if (deps["react"])
36
+ return "React";
37
+ if (deps["express"])
38
+ return "Express";
39
+ return "Node.js";
40
+ }
41
+ /** Returns the install command for a given package manager. */
42
+ export function installCommand(pm) {
43
+ switch (pm) {
44
+ case "pnpm":
45
+ return "pnpm install";
46
+ case "yarn":
47
+ return "yarn install";
48
+ case "bun":
49
+ return "bun install";
50
+ case "npm":
51
+ case "unknown":
52
+ return "npm install";
53
+ }
54
+ }
55
+ /** Returns the run command prefix for a given package manager. */
56
+ export function runCommand(pm, script) {
57
+ switch (pm) {
58
+ case "pnpm":
59
+ return `pnpm ${script}`;
60
+ case "yarn":
61
+ return `yarn ${script}`;
62
+ case "bun":
63
+ return `bun run ${script}`;
64
+ case "npm":
65
+ case "unknown":
66
+ return `npm run ${script}`;
67
+ }
68
+ }
@@ -0,0 +1,108 @@
1
+ import pc from "picocolors";
2
+ const ICONS = {
3
+ success: pc.green("\u2713"),
4
+ error: pc.red("\u2717"),
5
+ warning: pc.yellow("\u26a0"),
6
+ info: pc.cyan("\u2139"),
7
+ };
8
+ function colorFor(severity, text) {
9
+ switch (severity) {
10
+ case "success":
11
+ return pc.green(text);
12
+ case "error":
13
+ return pc.red(text);
14
+ case "warning":
15
+ return pc.yellow(text);
16
+ case "info":
17
+ return pc.cyan(text);
18
+ }
19
+ }
20
+ function scoreColor(score) {
21
+ const label = `${score}/100`;
22
+ if (score >= 85)
23
+ return pc.green(label);
24
+ if (score >= 60)
25
+ return pc.yellow(label);
26
+ return pc.red(label);
27
+ }
28
+ /** Builds recommended next steps from findings, highest severity first. */
29
+ export function nextSteps(report) {
30
+ const steps = [];
31
+ const all = report.results.flatMap((r) => r.findings);
32
+ const has = (rule) => all.some((f) => f.rule === rule);
33
+ if (has("package-json.missing"))
34
+ steps.push("Add a package.json (npm init -y)");
35
+ if (has("secrets.detected"))
36
+ steps.push("Rotate and remove hardcoded secrets immediately");
37
+ if (has("env.not-ignored"))
38
+ steps.push("Add .env to .gitignore");
39
+ if (has("env.example-missing"))
40
+ steps.push("Create .env.example (run: shipready fix)");
41
+ if (has("env.example-incomplete"))
42
+ steps.push("Add missing variables to .env.example");
43
+ if (has("readme.missing"))
44
+ steps.push("Write a README with setup instructions");
45
+ if (has("readme.weak"))
46
+ steps.push("Expand README with installation and usage sections");
47
+ if (has("gitignore.missing"))
48
+ steps.push("Create a .gitignore (run: shipready fix)");
49
+ if (has("gitignore.incomplete"))
50
+ steps.push("Add missing entries to .gitignore (run: shipready fix)");
51
+ if (has("scripts.missing"))
52
+ steps.push("Add missing package.json scripts (build/test/lint)");
53
+ if (has("todos.debug"))
54
+ steps.push("Remove debug logs and debugger statements before shipping");
55
+ if (has("todos.markers"))
56
+ steps.push("Resolve TODO/FIXME comments or track them as issues");
57
+ return steps;
58
+ }
59
+ /** Renders the full report to a printable string. */
60
+ export function renderReport(report, verbose = false) {
61
+ const lines = [];
62
+ const { project } = report;
63
+ lines.push("");
64
+ lines.push(pc.bold(pc.magenta("shipready report")));
65
+ lines.push("");
66
+ lines.push(`${pc.dim("Project:")} ${project.framework}`);
67
+ lines.push(`${pc.dim("Package manager:")} ${project.packageManager}`);
68
+ lines.push("");
69
+ lines.push(pc.bold("Summary:"));
70
+ for (const result of report.results) {
71
+ for (const finding of summarize(result.findings)) {
72
+ lines.push(`${ICONS[finding.severity]} ${finding.message}`);
73
+ }
74
+ }
75
+ // Detailed findings with file locations
76
+ const located = report.results
77
+ .flatMap((r) => r.findings)
78
+ .filter((f) => f.file && f.severity !== "success");
79
+ if (located.length > 0 && verbose) {
80
+ lines.push("");
81
+ lines.push(pc.bold("Details:"));
82
+ for (const f of located) {
83
+ const loc = f.line ? `${f.file}:${f.line}` : f.file;
84
+ lines.push(` ${ICONS[f.severity]} ${pc.dim(loc ?? "")} ${colorFor(f.severity, f.message)}`);
85
+ }
86
+ }
87
+ lines.push("");
88
+ lines.push(`${pc.bold("Score:")} ${scoreColor(report.score)}`);
89
+ const steps = nextSteps(report);
90
+ if (steps.length > 0) {
91
+ lines.push("");
92
+ lines.push(pc.bold("Recommended next steps:"));
93
+ steps.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
94
+ }
95
+ if (located.length > 0 && !verbose) {
96
+ lines.push("");
97
+ lines.push(pc.dim("Run with --verbose to see file locations."));
98
+ }
99
+ lines.push("");
100
+ return lines.join("\n");
101
+ }
102
+ /**
103
+ * Collapses per-file findings into summary lines while keeping
104
+ * top-level findings (those without a file) as-is.
105
+ */
106
+ function summarize(findings) {
107
+ return findings.filter((f) => !f.file);
108
+ }
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "shipready",
3
+ "version": "1.0.0",
4
+ "description": "Pre-flight check for your repo before shipping. Scans AI-coded projects for secrets, env issues, debug leftovers, and generates AI-agent instruction files.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "shipready contributors",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/formalness/shipready.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/formalness/shipready/issues"
14
+ },
15
+ "homepage": "https://github.com/formalness/shipready#readme",
16
+ "bin": {
17
+ "shipready": "./dist/index.js"
18
+ },
19
+ "main": "./dist/index.js",
20
+ "exports": {
21
+ ".": "./dist/index.js"
22
+ },
23
+ "files": [
24
+ "dist",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "scripts": {
35
+ "dev": "tsx src/index.ts",
36
+ "build": "tsc",
37
+ "test": "vitest run",
38
+ "test:watch": "vitest",
39
+ "lint": "eslint .",
40
+ "prepublishOnly": "npm run build && npm run test"
41
+ },
42
+ "keywords": [
43
+ "cli",
44
+ "security",
45
+ "secrets",
46
+ "secret-scanning",
47
+ "ai",
48
+ "vibe-coding",
49
+ "pre-flight",
50
+ "ship",
51
+ "lint",
52
+ "env",
53
+ "gitignore",
54
+ "agents-md"
55
+ ],
56
+ "dependencies": {
57
+ "commander": "^12.1.0",
58
+ "fast-glob": "^3.3.2",
59
+ "picocolors": "^1.1.1"
60
+ },
61
+ "devDependencies": {
62
+ "@types/node": "^22.10.2",
63
+ "tsx": "^4.19.2",
64
+ "typescript": "^5.7.2",
65
+ "vitest": "^2.1.8"
66
+ }
67
+ }