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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/checks/env.js +156 -0
- package/dist/checks/gitignore.js +56 -0
- package/dist/checks/packageJson.js +34 -0
- package/dist/checks/readme.js +47 -0
- package/dist/checks/secrets.js +105 -0
- package/dist/checks/todos.js +72 -0
- package/dist/cli.js +151 -0
- package/dist/config.js +59 -0
- package/dist/fixers/agentFiles.js +23 -0
- package/dist/fixers/envExample.js +27 -0
- package/dist/fixers/gitignore.js +29 -0
- package/dist/generators/agentsMd.js +66 -0
- package/dist/generators/claudeMd.js +35 -0
- package/dist/generators/cursorRules.js +21 -0
- package/dist/index.js +6 -0
- package/dist/scanner.js +116 -0
- package/dist/types.js +1 -0
- package/dist/utils/files.js +100 -0
- package/dist/utils/framework.js +68 -0
- package/dist/utils/report.js +108 -0
- package/package.json +67 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const MARKER_RE = /\b(TODO|FIXME|HACK|XXX)\b/;
|
|
2
|
+
const DEBUG_PATTERNS = [
|
|
3
|
+
{ label: "console.log", re: /\bconsole\.log\s*\(/ },
|
|
4
|
+
{ label: "debugger", re: /^\s*debugger\b/ },
|
|
5
|
+
{
|
|
6
|
+
label: "not implemented",
|
|
7
|
+
re: /throw new Error\((?:"Not implemented"|'Not implemented')\)/,
|
|
8
|
+
},
|
|
9
|
+
];
|
|
10
|
+
/** Scans a file's content for TODO/debug markers. */
|
|
11
|
+
export function scanContentForTodos(content, file) {
|
|
12
|
+
const found = [];
|
|
13
|
+
const lines = content.split("\n");
|
|
14
|
+
for (let i = 0; i < lines.length; i++) {
|
|
15
|
+
const line = lines[i];
|
|
16
|
+
const marker = line.match(MARKER_RE);
|
|
17
|
+
if (marker) {
|
|
18
|
+
found.push({ kind: "marker", label: marker[1], file, line: i + 1 });
|
|
19
|
+
}
|
|
20
|
+
for (const { label, re } of DEBUG_PATTERNS) {
|
|
21
|
+
if (re.test(line)) {
|
|
22
|
+
found.push({ kind: "debug", label, file, line: i + 1 });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
/** Builds the CheckResult from all todo/debug findings. */
|
|
29
|
+
export function checkTodos(todoFindings) {
|
|
30
|
+
const findings = [];
|
|
31
|
+
const markers = todoFindings.filter((f) => f.kind === "marker");
|
|
32
|
+
const debug = todoFindings.filter((f) => f.kind === "debug");
|
|
33
|
+
if (markers.length === 0 && debug.length === 0) {
|
|
34
|
+
findings.push({
|
|
35
|
+
severity: "success",
|
|
36
|
+
rule: "todos.clean",
|
|
37
|
+
message: "No TODO/FIXME or debug leftovers found",
|
|
38
|
+
});
|
|
39
|
+
return { name: "todos", findings };
|
|
40
|
+
}
|
|
41
|
+
if (markers.length > 0) {
|
|
42
|
+
findings.push({
|
|
43
|
+
severity: "warning",
|
|
44
|
+
rule: "todos.markers",
|
|
45
|
+
message: `${markers.length} TODO/FIXME comment${markers.length > 1 ? "s" : ""} found`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
if (debug.length > 0) {
|
|
49
|
+
const logs = debug.filter((d) => d.label === "console.log").length;
|
|
50
|
+
const other = debug.length - logs;
|
|
51
|
+
const parts = [];
|
|
52
|
+
if (logs > 0)
|
|
53
|
+
parts.push(`${logs} console.log call${logs > 1 ? "s" : ""}`);
|
|
54
|
+
if (other > 0)
|
|
55
|
+
parts.push(`${other} other debug marker${other > 1 ? "s" : ""}`);
|
|
56
|
+
findings.push({
|
|
57
|
+
severity: "warning",
|
|
58
|
+
rule: "todos.debug",
|
|
59
|
+
message: `${parts.join(" and ")} found`,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
for (const f of todoFindings) {
|
|
63
|
+
findings.push({
|
|
64
|
+
severity: "info",
|
|
65
|
+
rule: "todos.item",
|
|
66
|
+
message: f.label,
|
|
67
|
+
file: f.file,
|
|
68
|
+
line: f.line,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return { name: "todos", findings };
|
|
72
|
+
}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { detectProject, runScan, scanFiles } from "./scanner.js";
|
|
6
|
+
import { loadConfig } from "./config.js";
|
|
7
|
+
import { fixAgentFiles } from "./fixers/agentFiles.js";
|
|
8
|
+
import { fixEnvExample } from "./fixers/envExample.js";
|
|
9
|
+
import { fixGitignore } from "./fixers/gitignore.js";
|
|
10
|
+
import { renderReport } from "./utils/report.js";
|
|
11
|
+
function printFixResults(results) {
|
|
12
|
+
for (const r of results) {
|
|
13
|
+
if (r.action === "created") {
|
|
14
|
+
console.log(`${pc.green("+")} created ${pc.bold(r.file)}`);
|
|
15
|
+
}
|
|
16
|
+
else if (r.action === "updated") {
|
|
17
|
+
console.log(`${pc.yellow("~")} updated ${pc.bold(r.file)}`);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
console.log(`${pc.dim("-")} skipped ${r.file}${r.reason ? pc.dim(` (${r.reason})`) : ""}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Resolves and validates the target directory argument. */
|
|
25
|
+
function resolveRoot(dir) {
|
|
26
|
+
const root = path.resolve(dir ?? process.cwd());
|
|
27
|
+
if (!fs.existsSync(root)) {
|
|
28
|
+
throw new Error(`directory not found: ${root}`);
|
|
29
|
+
}
|
|
30
|
+
if (!fs.statSync(root).isDirectory()) {
|
|
31
|
+
throw new Error(`not a directory: ${root}`);
|
|
32
|
+
}
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
/** Applies all safe fixes for the given root; returns the results. */
|
|
36
|
+
async function applyFixes(root, force) {
|
|
37
|
+
const config = loadConfig(root);
|
|
38
|
+
const project = await detectProject(root, config);
|
|
39
|
+
const { envUsages } = scanFiles(root, project.sourceFiles, config.secretAllowlist);
|
|
40
|
+
return [
|
|
41
|
+
fixEnvExample(root, envUsages, force),
|
|
42
|
+
fixGitignore(root),
|
|
43
|
+
...fixAgentFiles(root, project, force),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
46
|
+
/** Builds the commander program. Exported for testing. */
|
|
47
|
+
export function buildProgram() {
|
|
48
|
+
const program = new Command();
|
|
49
|
+
program
|
|
50
|
+
.name("shipready")
|
|
51
|
+
.description("Pre-flight check for your repo before shipping.")
|
|
52
|
+
.version("1.0.0");
|
|
53
|
+
program
|
|
54
|
+
.command("check")
|
|
55
|
+
.description("Scan a project for shipping blockers")
|
|
56
|
+
.argument("[path]", "project directory to scan (defaults to current directory)")
|
|
57
|
+
.option("-v, --verbose", "show file locations for every finding")
|
|
58
|
+
.option("--json", "output the raw report as JSON")
|
|
59
|
+
.option("--fix", "apply safe fixes, then re-scan and show the improved report")
|
|
60
|
+
.action(async (dir, opts) => {
|
|
61
|
+
try {
|
|
62
|
+
const root = resolveRoot(dir);
|
|
63
|
+
let report = await runScan(root);
|
|
64
|
+
if (opts.fix) {
|
|
65
|
+
const before = report.score;
|
|
66
|
+
const results = await applyFixes(root, false);
|
|
67
|
+
report = await runScan(root);
|
|
68
|
+
if (!opts.json) {
|
|
69
|
+
console.log("");
|
|
70
|
+
console.log(pc.bold(pc.magenta("shipready check --fix")));
|
|
71
|
+
console.log("");
|
|
72
|
+
printFixResults(results);
|
|
73
|
+
console.log("");
|
|
74
|
+
const delta = report.score - before;
|
|
75
|
+
console.log(`Score: ${pc.bold(String(before))} -> ${pc.bold(String(report.score))}` +
|
|
76
|
+
(delta > 0 ? pc.green(` (+${delta})`) : pc.dim(" (no change)")));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (opts.json) {
|
|
80
|
+
console.log(JSON.stringify(report, null, 2));
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
console.log(renderReport(report, opts.verbose ?? false));
|
|
84
|
+
}
|
|
85
|
+
// Non-zero exit when errors are present, useful for CI.
|
|
86
|
+
const hasErrors = report.results.some((r) => r.findings.some((f) => f.severity === "error"));
|
|
87
|
+
process.exitCode = hasErrors ? 1 : 0;
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
console.error(pc.red(`shipready check failed: ${err.message}`));
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
program
|
|
95
|
+
.command("init")
|
|
96
|
+
.description("Generate AI-agent instruction files (AGENTS.md, CLAUDE.md, Cursor rules)")
|
|
97
|
+
.argument("[path]", "project directory (defaults to current directory)")
|
|
98
|
+
.option("-f, --force", "overwrite existing files")
|
|
99
|
+
.action(async (dir, opts) => {
|
|
100
|
+
try {
|
|
101
|
+
const root = resolveRoot(dir);
|
|
102
|
+
const project = await detectProject(root, loadConfig(root));
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(pc.bold(pc.magenta("shipready init")));
|
|
105
|
+
console.log("");
|
|
106
|
+
const results = fixAgentFiles(root, project, opts.force ?? false);
|
|
107
|
+
printFixResults(results);
|
|
108
|
+
const skipped = results.filter((r) => r.action === "skipped").length;
|
|
109
|
+
if (skipped > 0 && !opts.force) {
|
|
110
|
+
console.log("");
|
|
111
|
+
console.log(pc.dim("Some files already exist. Re-run with --force to overwrite."));
|
|
112
|
+
}
|
|
113
|
+
console.log("");
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
console.error(pc.red(`shipready init failed: ${err.message}`));
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
program
|
|
121
|
+
.command("fix")
|
|
122
|
+
.description("Apply safe automatic fixes (.env.example, .gitignore, agent files)")
|
|
123
|
+
.argument("[path]", "project directory (defaults to current directory)")
|
|
124
|
+
.option("-f, --force", "overwrite existing files")
|
|
125
|
+
.action(async (dir, opts) => {
|
|
126
|
+
try {
|
|
127
|
+
const root = resolveRoot(dir);
|
|
128
|
+
console.log("");
|
|
129
|
+
console.log(pc.bold(pc.magenta("shipready fix")));
|
|
130
|
+
console.log("");
|
|
131
|
+
const results = await applyFixes(root, opts.force ?? false);
|
|
132
|
+
printFixResults(results);
|
|
133
|
+
const changed = results.filter((r) => r.action !== "skipped").length;
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log(changed > 0
|
|
136
|
+
? pc.green(`${changed} file${changed > 1 ? "s" : ""} changed.`)
|
|
137
|
+
: pc.dim("Nothing to fix - everything already in place."));
|
|
138
|
+
console.log("");
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error(pc.red(`shipready fix failed: ${err.message}`));
|
|
142
|
+
process.exitCode = 1;
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return program;
|
|
146
|
+
}
|
|
147
|
+
/** CLI entry: parses argv and runs the matching command. */
|
|
148
|
+
export async function run(argv) {
|
|
149
|
+
const program = buildProgram();
|
|
150
|
+
await program.parseAsync(argv);
|
|
151
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const CONFIG_FILE = "shipready.config.json";
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
ignore: [],
|
|
6
|
+
disableRules: [],
|
|
7
|
+
secretAllowlist: [],
|
|
8
|
+
};
|
|
9
|
+
function assertStringArray(value, field) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return [];
|
|
12
|
+
if (!Array.isArray(value) || value.some((v) => typeof v !== "string")) {
|
|
13
|
+
throw new Error(`Invalid ${CONFIG_FILE}: "${field}" must be an array of strings`);
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Loads shipready.config.json from the project root.
|
|
19
|
+
* Returns defaults when the file does not exist.
|
|
20
|
+
* Throws a readable error for malformed JSON or wrong field types.
|
|
21
|
+
*/
|
|
22
|
+
export function loadConfig(root) {
|
|
23
|
+
const abs = path.join(root, CONFIG_FILE);
|
|
24
|
+
if (!fs.existsSync(abs))
|
|
25
|
+
return { ...DEFAULT_CONFIG };
|
|
26
|
+
let raw;
|
|
27
|
+
try {
|
|
28
|
+
raw = fs.readFileSync(abs, "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return { ...DEFAULT_CONFIG };
|
|
32
|
+
}
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(raw);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`Invalid ${CONFIG_FILE}: file is not valid JSON`);
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
41
|
+
throw new Error(`Invalid ${CONFIG_FILE}: expected a JSON object`);
|
|
42
|
+
}
|
|
43
|
+
const obj = parsed;
|
|
44
|
+
const known = new Set(["ignore", "disableRules", "secretAllowlist"]);
|
|
45
|
+
for (const key of Object.keys(obj)) {
|
|
46
|
+
if (!known.has(key)) {
|
|
47
|
+
throw new Error(`Invalid ${CONFIG_FILE}: unknown field "${key}" (allowed: ignore, disableRules, secretAllowlist)`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
ignore: assertStringArray(obj.ignore, "ignore"),
|
|
52
|
+
disableRules: assertStringArray(obj.disableRules, "disableRules"),
|
|
53
|
+
secretAllowlist: assertStringArray(obj.secretAllowlist, "secretAllowlist"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** Returns true when the rule is disabled by config (exact id or check prefix). */
|
|
57
|
+
export function isRuleDisabled(rule, disableRules) {
|
|
58
|
+
return disableRules.some((d) => d === rule || rule === d || rule.startsWith(`${d}.`));
|
|
59
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { generateAgentsMd } from "../generators/agentsMd.js";
|
|
2
|
+
import { generateClaudeMd } from "../generators/claudeMd.js";
|
|
3
|
+
import { generateCursorRules } from "../generators/cursorRules.js";
|
|
4
|
+
import { fileExists, writeTextFile } from "../utils/files.js";
|
|
5
|
+
const TARGETS = [
|
|
6
|
+
{ file: "AGENTS.md", generate: generateAgentsMd },
|
|
7
|
+
{ file: "CLAUDE.md", generate: generateClaudeMd },
|
|
8
|
+
{ file: ".cursor/rules/shipready.md", generate: generateCursorRules },
|
|
9
|
+
];
|
|
10
|
+
/** Generates all agent instruction files, honoring --force. */
|
|
11
|
+
export function fixAgentFiles(root, project, force) {
|
|
12
|
+
const results = [];
|
|
13
|
+
for (const { file, generate } of TARGETS) {
|
|
14
|
+
if (fileExists(root, file) && !force) {
|
|
15
|
+
results.push({ file, action: "skipped", reason: "already exists (use --force)" });
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const existed = fileExists(root, file);
|
|
19
|
+
writeTextFile(root, file, generate(project));
|
|
20
|
+
results.push({ file, action: existed ? "updated" : "created" });
|
|
21
|
+
}
|
|
22
|
+
return results;
|
|
23
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { fileExists, writeTextFile } from "../utils/files.js";
|
|
2
|
+
/** Builds .env.example content from detected env var usages. */
|
|
3
|
+
export function buildEnvExample(usages) {
|
|
4
|
+
const names = [...new Set(usages.map((u) => u.name))].sort();
|
|
5
|
+
const lines = [
|
|
6
|
+
"# Environment variables used by this project.",
|
|
7
|
+
"# Copy to .env and fill in real values. Do not commit .env.",
|
|
8
|
+
"",
|
|
9
|
+
...names.map((n) => `${n}=`),
|
|
10
|
+
"",
|
|
11
|
+
];
|
|
12
|
+
return lines.join("\n");
|
|
13
|
+
}
|
|
14
|
+
/** Creates .env.example if missing (or with force). */
|
|
15
|
+
export function fixEnvExample(root, usages, force) {
|
|
16
|
+
const file = ".env.example";
|
|
17
|
+
const names = [...new Set(usages.map((u) => u.name))];
|
|
18
|
+
if (names.length === 0) {
|
|
19
|
+
return { file, action: "skipped", reason: "no env vars detected in code" };
|
|
20
|
+
}
|
|
21
|
+
if (fileExists(root, file) && !force) {
|
|
22
|
+
return { file, action: "skipped", reason: "already exists (use --force)" };
|
|
23
|
+
}
|
|
24
|
+
const existed = fileExists(root, file);
|
|
25
|
+
writeTextFile(root, file, buildEnvExample(usages));
|
|
26
|
+
return { file, action: existed ? "updated" : "created" };
|
|
27
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { missingIgnoreEntries } from "../checks/gitignore.js";
|
|
2
|
+
import { readTextFile, writeTextFile } from "../utils/files.js";
|
|
3
|
+
/** Adds missing important entries to .gitignore (creates it if absent). */
|
|
4
|
+
export function fixGitignore(root) {
|
|
5
|
+
const file = ".gitignore";
|
|
6
|
+
const existing = readTextFile(root, file);
|
|
7
|
+
if (existing === null) {
|
|
8
|
+
const content = [
|
|
9
|
+
"# Added by shipready",
|
|
10
|
+
".env",
|
|
11
|
+
".env.local",
|
|
12
|
+
"node_modules",
|
|
13
|
+
"dist",
|
|
14
|
+
"build",
|
|
15
|
+
".next",
|
|
16
|
+
"",
|
|
17
|
+
].join("\n");
|
|
18
|
+
writeTextFile(root, file, content);
|
|
19
|
+
return { file, action: "created" };
|
|
20
|
+
}
|
|
21
|
+
const missing = missingIgnoreEntries(existing);
|
|
22
|
+
if (missing.length === 0) {
|
|
23
|
+
return { file, action: "skipped", reason: "already complete" };
|
|
24
|
+
}
|
|
25
|
+
const suffix = existing.endsWith("\n") ? "" : "\n";
|
|
26
|
+
const addition = `${suffix}\n# Added by shipready\n${missing.join("\n")}\n`;
|
|
27
|
+
writeTextFile(root, file, existing + addition);
|
|
28
|
+
return { file, action: "updated" };
|
|
29
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { installCommand, runCommand } from "../utils/framework.js";
|
|
2
|
+
/** Shared list of rules for AI agents working in the repo. */
|
|
3
|
+
export const AGENT_RULES = [
|
|
4
|
+
"Do not edit .env files directly.",
|
|
5
|
+
"Do not commit secrets or API keys.",
|
|
6
|
+
"Run the build command before finalizing major changes.",
|
|
7
|
+
"Keep changes small and focused.",
|
|
8
|
+
"Prefer existing project patterns over introducing new libraries.",
|
|
9
|
+
"Update README when setup steps change.",
|
|
10
|
+
];
|
|
11
|
+
/** Builds the shared command table used by all generated files. */
|
|
12
|
+
export function buildCommandSection(project) {
|
|
13
|
+
const pm = project.packageManager;
|
|
14
|
+
const s = project.scripts;
|
|
15
|
+
const rows = [`- Install: \`${installCommand(pm)}\``];
|
|
16
|
+
if (s.dev)
|
|
17
|
+
rows.push(`- Dev: \`${runCommand(pm, "dev")}\``);
|
|
18
|
+
if (s.build)
|
|
19
|
+
rows.push(`- Build: \`${runCommand(pm, "build")}\``);
|
|
20
|
+
if (s.test)
|
|
21
|
+
rows.push(`- Test: \`${runCommand(pm, "test")}\``);
|
|
22
|
+
if (s.lint)
|
|
23
|
+
rows.push(`- Lint: \`${runCommand(pm, "lint")}\``);
|
|
24
|
+
return rows.join("\n");
|
|
25
|
+
}
|
|
26
|
+
/** Builds a short project structure summary from scanned files. */
|
|
27
|
+
export function buildStructureSummary(project) {
|
|
28
|
+
const dirs = new Set();
|
|
29
|
+
for (const f of project.sourceFiles) {
|
|
30
|
+
const top = f.split("/")[0];
|
|
31
|
+
if (top && !top.startsWith(".") && f.includes("/"))
|
|
32
|
+
dirs.add(top);
|
|
33
|
+
}
|
|
34
|
+
const list = [...dirs].sort().slice(0, 12);
|
|
35
|
+
if (list.length === 0)
|
|
36
|
+
return "- Flat project (no top-level source directories detected)";
|
|
37
|
+
return list.map((d) => `- \`${d}/\``).join("\n");
|
|
38
|
+
}
|
|
39
|
+
/** Generates AGENTS.md content. */
|
|
40
|
+
export function generateAgentsMd(project) {
|
|
41
|
+
const name = project.packageJson?.name ?? "this project";
|
|
42
|
+
return `# AGENTS.md
|
|
43
|
+
|
|
44
|
+
Instructions for AI coding agents working on ${name}.
|
|
45
|
+
|
|
46
|
+
## Project
|
|
47
|
+
|
|
48
|
+
- Framework: ${project.framework}
|
|
49
|
+
- Package manager: ${project.packageManager}
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
${buildCommandSection(project)}
|
|
54
|
+
|
|
55
|
+
## Project structure
|
|
56
|
+
|
|
57
|
+
${buildStructureSummary(project)}
|
|
58
|
+
|
|
59
|
+
## Rules
|
|
60
|
+
|
|
61
|
+
${AGENT_RULES.map((r) => `- ${r}`).join("\n")}
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
Generated by [shipready](https://www.npmjs.com/package/shipready).
|
|
65
|
+
`;
|
|
66
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { AGENT_RULES, buildCommandSection, buildStructureSummary, } from "./agentsMd.js";
|
|
2
|
+
/** Generates CLAUDE.md content. */
|
|
3
|
+
export function generateClaudeMd(project) {
|
|
4
|
+
const name = project.packageJson?.name ?? "this project";
|
|
5
|
+
return `# CLAUDE.md
|
|
6
|
+
|
|
7
|
+
Guidance for Claude when working on ${name}.
|
|
8
|
+
|
|
9
|
+
## Project
|
|
10
|
+
|
|
11
|
+
- Framework: ${project.framework}
|
|
12
|
+
- Package manager: ${project.packageManager}
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
${buildCommandSection(project)}
|
|
17
|
+
|
|
18
|
+
## Project structure
|
|
19
|
+
|
|
20
|
+
${buildStructureSummary(project)}
|
|
21
|
+
|
|
22
|
+
## Rules
|
|
23
|
+
|
|
24
|
+
${AGENT_RULES.map((r) => `- ${r}`).join("\n")}
|
|
25
|
+
|
|
26
|
+
## Workflow
|
|
27
|
+
|
|
28
|
+
- Read existing code before writing new code.
|
|
29
|
+
- Verify changes compile/pass tests before declaring them done.
|
|
30
|
+
- Ask before making sweeping refactors.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
Generated by [shipready](https://www.npmjs.com/package/shipready).
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { AGENT_RULES, buildCommandSection } from "./agentsMd.js";
|
|
2
|
+
/** Generates .cursor/rules/shipready.md content. */
|
|
3
|
+
export function generateCursorRules(project) {
|
|
4
|
+
return `# shipready rules
|
|
5
|
+
|
|
6
|
+
Project: ${project.framework} (${project.packageManager})
|
|
7
|
+
|
|
8
|
+
## Commands
|
|
9
|
+
|
|
10
|
+
${buildCommandSection(project)}
|
|
11
|
+
|
|
12
|
+
## Rules for AI edits
|
|
13
|
+
|
|
14
|
+
${AGENT_RULES.map((r) => `- ${r}`).join("\n")}
|
|
15
|
+
- Mask or omit any secret values when displaying file contents.
|
|
16
|
+
- Never write real credentials into example or config files.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
Generated by shipready.
|
|
20
|
+
`;
|
|
21
|
+
}
|
package/dist/index.js
ADDED
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { checkEnv, extractEnvUsages } from "./checks/env.js";
|
|
3
|
+
import { checkGitignore } from "./checks/gitignore.js";
|
|
4
|
+
import { checkPackageJson } from "./checks/packageJson.js";
|
|
5
|
+
import { checkReadme } from "./checks/readme.js";
|
|
6
|
+
import { checkSecrets, scanContentForSecrets } from "./checks/secrets.js";
|
|
7
|
+
import { checkTodos, scanContentForTodos } from "./checks/todos.js";
|
|
8
|
+
import { fileExists, findSourceFiles, isProbablyBinary, readJsonFile, readTextFile, } from "./utils/files.js";
|
|
9
|
+
import { detectFramework, detectPackageManager } from "./utils/framework.js";
|
|
10
|
+
import { isRuleDisabled, loadConfig } from "./config.js";
|
|
11
|
+
/** File extensions considered code for content scanning (not config/data). */
|
|
12
|
+
const CODE_ONLY = new Set([
|
|
13
|
+
".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs", ".mts", ".cts",
|
|
14
|
+
".vue", ".svelte", ".py", ".rb", ".go", ".rs", ".java", ".php",
|
|
15
|
+
]);
|
|
16
|
+
/** Gathers structured project info from the given root. */
|
|
17
|
+
export async function detectProject(root, config) {
|
|
18
|
+
const pkg = readJsonFile(root, "package.json");
|
|
19
|
+
const sourceFiles = await findSourceFiles(root, config?.ignore ?? []);
|
|
20
|
+
return {
|
|
21
|
+
root,
|
|
22
|
+
hasPackageJson: fileExists(root, "package.json"),
|
|
23
|
+
packageJson: pkg,
|
|
24
|
+
packageManager: detectPackageManager(root),
|
|
25
|
+
framework: detectFramework(pkg),
|
|
26
|
+
scripts: pkg?.scripts ?? {},
|
|
27
|
+
sourceFiles,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/** Extracts env usages, secrets, and todos from all source files. */
|
|
31
|
+
export function scanFiles(root, files, secretAllowlist = []) {
|
|
32
|
+
const envUsages = [];
|
|
33
|
+
const secrets = [];
|
|
34
|
+
const todos = [];
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const base = path.basename(file);
|
|
37
|
+
// Never flag .env files themselves for secrets/todos; they are expected
|
|
38
|
+
// to contain real values and are handled by the env check.
|
|
39
|
+
const isEnvFile = base.startsWith(".env");
|
|
40
|
+
const ext = path.extname(file).toLowerCase();
|
|
41
|
+
const isCode = CODE_ONLY.has(ext);
|
|
42
|
+
if (isProbablyBinary(root, file))
|
|
43
|
+
continue;
|
|
44
|
+
const content = readTextFile(root, file);
|
|
45
|
+
if (content === null)
|
|
46
|
+
continue;
|
|
47
|
+
if (isCode) {
|
|
48
|
+
envUsages.push(...extractEnvUsages(content, file));
|
|
49
|
+
todos.push(...scanContentForTodos(content, file));
|
|
50
|
+
}
|
|
51
|
+
if (!isEnvFile && base !== ".env.example" && base !== ".env.sample") {
|
|
52
|
+
secrets.push(...scanContentForSecrets(content, file, secretAllowlist));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { envUsages, secrets, todos };
|
|
56
|
+
}
|
|
57
|
+
/** Score deductions per the shipready scoring model. */
|
|
58
|
+
export function calculateScore(results) {
|
|
59
|
+
let score = 100;
|
|
60
|
+
const all = results.flatMap((r) => r.findings);
|
|
61
|
+
const has = (rule) => all.some((f) => f.rule === rule);
|
|
62
|
+
const count = (rule) => all.filter((f) => f.rule === rule).length;
|
|
63
|
+
if (has("package-json.missing"))
|
|
64
|
+
score -= 20;
|
|
65
|
+
if (has("readme.missing"))
|
|
66
|
+
score -= 15;
|
|
67
|
+
if (has("readme.weak"))
|
|
68
|
+
score -= 8;
|
|
69
|
+
// Missing scripts: look at message to determine which ones
|
|
70
|
+
const scriptsFinding = all.find((f) => f.rule === "scripts.missing");
|
|
71
|
+
if (scriptsFinding) {
|
|
72
|
+
if (scriptsFinding.message.includes("build"))
|
|
73
|
+
score -= 8;
|
|
74
|
+
if (scriptsFinding.message.includes("test"))
|
|
75
|
+
score -= 6;
|
|
76
|
+
}
|
|
77
|
+
if (has("env.example-missing"))
|
|
78
|
+
score -= 10;
|
|
79
|
+
if (has("env.not-ignored"))
|
|
80
|
+
score -= 15;
|
|
81
|
+
const secretCount = count("secrets.detected-item");
|
|
82
|
+
score -= Math.min(secretCount * 25, 50);
|
|
83
|
+
const todoCount = count("todos.item");
|
|
84
|
+
score -= Math.min(todoCount * 2, 15);
|
|
85
|
+
return Math.max(0, score);
|
|
86
|
+
}
|
|
87
|
+
/** Removes findings whose rules are disabled by config. */
|
|
88
|
+
function applyDisabledRules(results, disableRules) {
|
|
89
|
+
if (disableRules.length === 0)
|
|
90
|
+
return results;
|
|
91
|
+
return results.map((r) => ({
|
|
92
|
+
...r,
|
|
93
|
+
findings: r.findings.filter((f) => !isRuleDisabled(f.rule, disableRules) && !isRuleDisabled(r.name, disableRules)),
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
/** Runs the full scan and returns a structured report. */
|
|
97
|
+
export async function runScan(root) {
|
|
98
|
+
const config = loadConfig(root);
|
|
99
|
+
const project = await detectProject(root, config);
|
|
100
|
+
const { envUsages, secrets, todos } = scanFiles(root, project.sourceFiles, config.secretAllowlist);
|
|
101
|
+
const rawResults = [
|
|
102
|
+
checkPackageJson(project),
|
|
103
|
+
checkReadme(root),
|
|
104
|
+
checkEnv(root, envUsages),
|
|
105
|
+
checkSecrets(secrets),
|
|
106
|
+
checkTodos(todos),
|
|
107
|
+
checkGitignore(root),
|
|
108
|
+
];
|
|
109
|
+
// Disabled rules are removed before scoring so they don't affect the score.
|
|
110
|
+
const results = applyDisabledRules(rawResults, config.disableRules);
|
|
111
|
+
return {
|
|
112
|
+
project,
|
|
113
|
+
results,
|
|
114
|
+
score: calculateScore(results),
|
|
115
|
+
};
|
|
116
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|