ship18ion 1.1.0 → 1.2.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,60 @@
1
+ # Contributing to ship18ion
2
+
3
+ First off, thanks for taking the time to contribute! ❤️
4
+
5
+ All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions.
6
+
7
+ ## Table of Contents
8
+
9
+ - [I Have a Question](#i-have-a-question)
10
+ - [I Want To Contribute](#i-want-to-contribute)
11
+ - [Reporting Bugs](#reporting-bugs)
12
+ - [Suggesting Enhancements](#suggesting-enhancements)
13
+ - [Your First Code Contribution](#your-first-code-contribution)
14
+
15
+ ## I Have a Question
16
+
17
+ > If you want to ask a question, we assume that you have read the available [Documentation](README.md).
18
+
19
+ Before you ask a question, it is best to search for existing [Issues](/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
20
+
21
+ ## I Want To Contribute
22
+
23
+ ### Reporting Bugs
24
+
25
+ - Make sure that you are using the latest version.
26
+ - Read the documentation carefully and find out if the functionality is already covered, maybe by an individual configuration.
27
+ - Perform a search to see if the problem has already been reported. If it has, add a comment to the existing issue instead of opening a new one.
28
+
29
+ ### Suggesting Enhancements
30
+
31
+ - Open a new issue and use the **Feature Request** template.
32
+ - Explain why this enhancement would be useful to most users.
33
+
34
+ ## Styleguides
35
+
36
+ ### Commit Messages
37
+
38
+ - Use [Conventional Commits](https://www.conventionalcommits.org/).
39
+
40
+ ### Code Style
41
+
42
+ - Keep code clean and use the existing ESLint/Prettier configs (if available).
43
+ - Add tests for new features.
44
+
45
+ ## Development Workflow
46
+
47
+ To test your changes locally on another project without publishing:
48
+
49
+ 1. **Build and Link**:
50
+ ```bash
51
+ npm run build
52
+ npm link
53
+ ```
54
+ 2. **Use in Target Project**:
55
+ ```bash
56
+ npm link ship18ion
57
+ npx ship18ion
58
+ ```
59
+
60
+ Thank you!
package/README.md CHANGED
@@ -31,6 +31,17 @@ Think of it as `eslint` but for **deployability**.
31
31
  - Ensures `.env` files are not bundled into build output directories.
32
32
  - Checks for dev dependencies (like `eslint`) accidentally listed in `dependencies`.
33
33
 
34
+ - **🧹 Code Hygiene (New)**:
35
+ - Warns on leftover `console.log()` calls.
36
+ - Flags `FIXME` comments that need resolution.
37
+
38
+ - **📦 Dependencies (New)**:
39
+ - Checks for duplicate packages (listed in both `dependencies` and `devDependencies`).
40
+
41
+ - **🐙 Git Safety (New)**:
42
+ - Ensures critical files (`node_modules`, `.env`) and framework artifacts (`.next`) are git-ignored.
43
+ - **Security**: alerts if dangerous keys (e.g. `serviceAccountKey.json`) exist but are *not* ignored.
44
+
34
45
  - **👷 CI/CD Ready**:
35
46
  - Zero config by default.
36
47
  - Returns exit code `1` on failure to block bad builds.
@@ -95,6 +106,11 @@ npx ship18ion check --ci
95
106
  | **Next.js** | `nextjs-public-secret` | High-entropy string found in `NEXT_PUBLIC_` variable. |
96
107
  | **Security** | `security-cors` | Detects wildcard `Access-Control-Allow-Origin`. |
97
108
  | **Git** | `git-dirty` | Warns if deploying with uncommitted changes. |
109
+ | **Git** | `git-ignore-missing` | Warns if `.gitignore` is missing critical entries (`node_modules`, `.env`). |
110
+ | **Git** | `git-ignore-auth` | **Critical**: Fails if `serviceAccountKey.json` etc are not ignored. |
111
+ | **Hygiene** | `hygiene-console-log` | Warns on `console.log` in production code. |
112
+ | **Hygiene** | `hygiene-fixme` | Warns on leftover `FIXME` comments. |
113
+ | **Package** | `package-duplicate` | Warns if a package is in both dependency lists. |
98
114
 
99
115
  ## 🤝 Contributing
100
116
 
package/dist/cli/index.js CHANGED
@@ -27,11 +27,16 @@ program
27
27
  const config = await (0, config_1.loadConfig)(cwd);
28
28
  const spinner = (0, ora_1.default)('Initializing...').start();
29
29
  try {
30
+ let framework = 'unknown';
30
31
  if (!options.ci) {
31
- const framework = await (0, detector_1.detectFramework)(cwd);
32
+ framework = await (0, detector_1.detectFramework)(cwd);
32
33
  spinner.text = `Detected Framework: ${chalk_1.default.cyan(framework.toUpperCase())}`;
33
34
  await new Promise(r => setTimeout(r, 800)); // Brief pause to show framework
34
35
  }
36
+ else {
37
+ // Even in CI, simple detection is useful for reporting if needed, or we just skip
38
+ framework = await (0, detector_1.detectFramework)(cwd);
39
+ }
35
40
  const results = await (0, runner_1.runChecks)(config, cwd, (stage) => {
36
41
  if (!options.ci)
37
42
  spinner.text = stage;
@@ -39,7 +44,7 @@ program
39
44
  spinner.succeed(chalk_1.default.green('Checks completed!'));
40
45
  console.log('');
41
46
  // Uses console reporter for both normal and CI for now (it handles exit codes)
42
- (0, console_1.reportConsole)(results, cwd);
47
+ (0, console_1.reportConsole)(results, cwd, framework);
43
48
  }
44
49
  catch (e) {
45
50
  spinner.fail(chalk_1.default.red('Error running checks'));
@@ -6,6 +6,8 @@ const env_1 = require("../rules/env");
6
6
  const secrets_1 = require("../rules/secrets");
7
7
  const security_1 = require("../rules/security");
8
8
  const build_1 = require("../rules/build");
9
+ const hygiene_1 = require("../rules/hygiene");
10
+ const packages_1 = require("../rules/packages");
9
11
  const nextjs_1 = require("../rules/frameworks/nextjs");
10
12
  const git_1 = require("../rules/git");
11
13
  const detector_1 = require("./detector");
@@ -13,7 +15,9 @@ async function runChecks(config, cwd, onProgress) {
13
15
  if (onProgress)
14
16
  onProgress('Scanning files...');
15
17
  const files = await (0, scanner_1.scanFiles)(cwd, config.ignore);
16
- const ctx = { config, files, cwd };
18
+ // Framework detection
19
+ const framework = await (0, detector_1.detectFramework)(cwd);
20
+ const ctx = { config, files, cwd, framework };
17
21
  const results = [];
18
22
  // Run all checks
19
23
  if (onProgress)
@@ -31,8 +35,14 @@ async function runChecks(config, cwd, onProgress) {
31
35
  if (onProgress)
32
36
  onProgress('Inspecting build artifacts...');
33
37
  results.push(...await (0, build_1.checkBuild)(ctx));
34
- // Framework detection
35
- const framework = await (0, detector_1.detectFramework)(cwd);
38
+ // New Rules
39
+ if (onProgress)
40
+ onProgress('Checking code hygiene...');
41
+ results.push(...await (0, hygiene_1.checkHygiene)(ctx));
42
+ if (onProgress)
43
+ onProgress('Validating packages...');
44
+ results.push(...await (0, packages_1.checkPackages)(ctx));
45
+ // Framework specific checks
36
46
  if (framework === 'nextjs') {
37
47
  if (onProgress)
38
48
  onProgress('Running Next.js specific checks...');
@@ -12,12 +12,18 @@ const CATEGORIES = {
12
12
  'security': { icon: '⚠️', label: 'Security' },
13
13
  'dep': { icon: '📦', label: 'Dependency & Build' },
14
14
  'build': { icon: '📦', label: 'Dependency & Build' },
15
+ 'git': { icon: '🐙', label: 'Git & Repo' },
16
+ 'hygiene': { icon: '🧹', label: 'Code Hygiene' },
17
+ 'package': { icon: '📦', label: 'Packages' },
15
18
  };
16
19
  function getCategory(ruleId) {
17
20
  const prefix = ruleId.split('-')[0];
18
21
  return CATEGORIES[prefix] || { icon: '❓', label: 'Other' };
19
22
  }
20
- function reportConsole(results, cwd) {
23
+ function reportConsole(results, cwd, framework) {
24
+ if (framework) {
25
+ console.log(chalk_1.default.blue(`ℹ️ Framework: ${framework.toUpperCase()}`));
26
+ }
21
27
  if (results.length === 0) {
22
28
  console.log(chalk_1.default.green('\n✅ Production Readiness Check Passed!\n'));
23
29
  return;
@@ -46,12 +46,12 @@ async function checkBuild(ctx) {
46
46
  // We search inside the found build directories
47
47
  for (const dir of foundBuildDirs) {
48
48
  const mapFiles = await (0, glob_1.glob)(`${dir}/**/*.map`, { cwd: ctx.cwd, absolute: true });
49
- for (const file of mapFiles) {
49
+ if (mapFiles.length > 0) {
50
50
  results.push({
51
51
  status: 'warn',
52
- message: `Source map found in build output (${dir}) (information leak)`,
52
+ message: `Found ${mapFiles.length} source map files in '${dir}' (e.g. ${path_1.default.basename(mapFiles[0])}). Ensure these are not exposed publicly.`,
53
53
  ruleId: 'build-source-map',
54
- file
54
+ file: dir // Point to the directory itself
55
55
  });
56
56
  }
57
57
  // 2. Check for .env files in build output
package/dist/rules/git.js CHANGED
@@ -1,10 +1,16 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.checkGit = checkGit;
4
7
  const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
5
10
  async function checkGit(ctx) {
6
11
  const results = [];
7
12
  try {
13
+ // ... (Existing git checks) ...
8
14
  // Check for uncommitted changes
9
15
  const status = (0, child_process_1.execSync)('git status --porcelain', { cwd: ctx.cwd, encoding: 'utf-8' });
10
16
  if (status.trim().length > 0) {
@@ -18,17 +24,67 @@ async function checkGit(ctx) {
18
24
  const branch = (0, child_process_1.execSync)('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf-8' }).trim();
19
25
  const allowedBranches = ['main', 'master', 'staging', 'production', 'prod'];
20
26
  if (!allowedBranches.includes(branch)) {
27
+ // Warn, but maybe less aggressively? Keeping as warn.
21
28
  results.push({
22
29
  status: 'warn',
23
30
  message: `You are on branch '${branch}'. Production builds typically come from main/master.`,
24
31
  ruleId: 'git-branch',
25
32
  });
26
33
  }
34
+ // --- New: .gitignore Check ---
35
+ const gitignorePath = path_1.default.join(ctx.cwd, '.gitignore');
36
+ if (fs_1.default.existsSync(gitignorePath)) {
37
+ const content = fs_1.default.readFileSync(gitignorePath, 'utf-8');
38
+ const lines = content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
39
+ // Helper to check if item is ignored (naive grep)
40
+ const isIgnored = (item) => lines.some(l => l.includes(item));
41
+ const requiredIgnores = ['node_modules', '.env'];
42
+ if (ctx.framework === 'nextjs') {
43
+ requiredIgnores.push('.next');
44
+ }
45
+ else if (ctx.framework !== 'unknown') {
46
+ // For other frameworks, maybe 'dist' or 'build'
47
+ if (!isIgnored('dist') && !isIgnored('build')) {
48
+ // We can't strictly require one, but warn if NEITHER is found?
49
+ // Let's stick to safe defaults.
50
+ }
51
+ }
52
+ for (const item of requiredIgnores) {
53
+ if (!isIgnored(item)) {
54
+ results.push({
55
+ status: 'warn',
56
+ message: `.gitignore is missing '${item}'. This is critical for security and repo size.`,
57
+ ruleId: 'git-ignore-missing',
58
+ file: gitignorePath
59
+ });
60
+ }
61
+ }
62
+ // Check for specific dangerous files not being ignored
63
+ const dangerousPatterns = ['firebase.json', 'serviceAccountKey.json', '*.pem', '*.key'];
64
+ // This is tricky because firebase.json CAN be committed. serviceAccountKey.json should NOT.
65
+ if (!isIgnored('serviceAccountKey.json')) {
66
+ // Only warn if the FILE actually exists? Or just warn generic?
67
+ // Best to warn if the file exists AND isn't ignored.
68
+ if (fs_1.default.existsSync(path_1.default.join(ctx.cwd, 'serviceAccountKey.json'))) {
69
+ results.push({
70
+ status: 'fail',
71
+ message: 'serviceAccountKey.json exists but is NOT in .gitignore!',
72
+ ruleId: 'git-ignore-auth',
73
+ file: gitignorePath
74
+ });
75
+ }
76
+ }
77
+ }
78
+ else {
79
+ results.push({
80
+ status: 'warn',
81
+ message: 'No .gitignore file found! node_modules and secrets might be committed.',
82
+ ruleId: 'git-no-ignore',
83
+ });
84
+ }
27
85
  }
28
86
  catch (e) {
29
87
  // Not a git repo or git not found
30
- // Silently fail or warn?
31
- // results.push({ status: 'warn', message: 'Not a git repository or git command failed.', ruleId: 'git-error' });
32
88
  }
33
89
  return results;
34
90
  }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkHygiene = checkHygiene;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ async function checkHygiene(ctx) {
9
+ const results = [];
10
+ const codeFiles = ctx.files.filter(f => f.match(/\.(js|ts|jsx|tsx)$/) &&
11
+ !f.includes('.test.') &&
12
+ !f.includes('.spec.'));
13
+ for (const file of codeFiles) {
14
+ const content = fs_1.default.readFileSync(file, 'utf-8');
15
+ const lines = content.split('\n');
16
+ lines.forEach((line, index) => {
17
+ const lineNum = index + 1;
18
+ // 1. Console Log Check
19
+ // Allow console.error and console.warn, but warn on console.log
20
+ if (line.includes('console.log(')) {
21
+ // Ignore if commented out
22
+ if (!line.trim().startsWith('//') && !line.trim().startsWith('*')) {
23
+ results.push({
24
+ status: 'warn',
25
+ message: 'Leftover console.log() call detected.',
26
+ ruleId: 'hygiene-console-log',
27
+ file,
28
+ line: lineNum
29
+ });
30
+ }
31
+ }
32
+ // 2. TODO / FIXME Check
33
+ if (line.match(/\/\/\s*(TODO|FIXME):/i)) {
34
+ if (line.match(/FIXME/i)) {
35
+ results.push({
36
+ status: 'warn',
37
+ message: 'FIXME comment found. Resolve before shipping.',
38
+ ruleId: 'hygiene-fixme',
39
+ file,
40
+ line: lineNum
41
+ });
42
+ }
43
+ }
44
+ });
45
+ }
46
+ return results;
47
+ }
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.checkPackages = checkPackages;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ async function checkPackages(ctx) {
9
+ const results = [];
10
+ const packageJsons = ctx.files.filter(f => f.endsWith('package.json') && !f.includes('node_modules'));
11
+ for (const pkgFile of packageJsons) {
12
+ try {
13
+ const content = JSON.parse(fs_1.default.readFileSync(pkgFile, 'utf-8'));
14
+ const deps = Object.keys(content.dependencies || {});
15
+ const devDeps = Object.keys(content.devDependencies || {});
16
+ // Find intersection
17
+ const duplicates = deps.filter(d => devDeps.includes(d));
18
+ for (const dup of duplicates) {
19
+ results.push({
20
+ status: 'warn',
21
+ message: `Package '${dup}' is listed in both 'dependencies' and 'devDependencies'.`,
22
+ ruleId: 'package-duplicate',
23
+ file: pkgFile
24
+ });
25
+ }
26
+ }
27
+ catch (e) {
28
+ // ignore malformed json
29
+ }
30
+ }
31
+ return results;
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship18ion",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "",
5
5
  "main": "dist/cli/index.js",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -27,10 +27,14 @@ program
27
27
  const spinner = ora('Initializing...').start();
28
28
 
29
29
  try {
30
+ let framework: string = 'unknown';
30
31
  if (!options.ci) {
31
- const framework = await detectFramework(cwd);
32
+ framework = await detectFramework(cwd);
32
33
  spinner.text = `Detected Framework: ${chalk.cyan(framework.toUpperCase())}`;
33
34
  await new Promise(r => setTimeout(r, 800)); // Brief pause to show framework
35
+ } else {
36
+ // Even in CI, simple detection is useful for reporting if needed, or we just skip
37
+ framework = await detectFramework(cwd);
34
38
  }
35
39
 
36
40
  const results = await runChecks(config, cwd, (stage) => {
@@ -41,7 +45,7 @@ program
41
45
  console.log('');
42
46
 
43
47
  // Uses console reporter for both normal and CI for now (it handles exit codes)
44
- reportConsole(results, cwd);
48
+ reportConsole(results, cwd, framework);
45
49
  } catch (e) {
46
50
  spinner.fail(chalk.red('Error running checks'));
47
51
  console.error(e);
@@ -5,6 +5,8 @@ import { checkEnvVars } from '../rules/env';
5
5
  import { checkSecrets } from '../rules/secrets';
6
6
  import { checkSecurity } from '../rules/security';
7
7
  import { checkDependencies, checkBuild } from '../rules/build';
8
+ import { checkHygiene } from '../rules/hygiene';
9
+ import { checkPackages } from '../rules/packages';
8
10
  import { checkNextJs } from '../rules/frameworks/nextjs';
9
11
  import { checkGit } from '../rules/git';
10
12
 
@@ -17,7 +19,10 @@ export async function runChecks(
17
19
  ): Promise<RuleResult[]> {
18
20
  if (onProgress) onProgress('Scanning files...');
19
21
  const files = await scanFiles(cwd, config.ignore);
20
- const ctx: RuleContext = { config, files, cwd };
22
+ // Framework detection
23
+ const framework = await detectFramework(cwd);
24
+
25
+ const ctx: RuleContext = { config, files, cwd, framework };
21
26
 
22
27
  const results: RuleResult[] = [];
23
28
 
@@ -37,8 +42,14 @@ export async function runChecks(
37
42
  if (onProgress) onProgress('Inspecting build artifacts...');
38
43
  results.push(...await checkBuild(ctx));
39
44
 
40
- // Framework detection
41
- const framework = await detectFramework(cwd);
45
+ // New Rules
46
+ if (onProgress) onProgress('Checking code hygiene...');
47
+ results.push(...await checkHygiene(ctx));
48
+
49
+ if (onProgress) onProgress('Validating packages...');
50
+ results.push(...await checkPackages(ctx));
51
+
52
+ // Framework specific checks
42
53
  if (framework === 'nextjs') {
43
54
  if (onProgress) onProgress('Running Next.js specific checks...');
44
55
  results.push(...await checkNextJs(ctx));
@@ -1,4 +1,5 @@
1
1
  import { Ship18ionConfig } from './config';
2
+ import { FrameworkType } from './detector';
2
3
 
3
4
  export interface RuleResult {
4
5
  status: 'pass' | 'fail' | 'warn';
@@ -12,4 +13,5 @@ export interface RuleContext {
12
13
  config: Ship18ionConfig;
13
14
  files: string[];
14
15
  cwd: string;
16
+ framework: FrameworkType;
15
17
  }
@@ -8,6 +8,9 @@ const CATEGORIES: Record<string, { icon: string; label: string }> = {
8
8
  'security': { icon: '⚠️', label: 'Security' },
9
9
  'dep': { icon: '📦', label: 'Dependency & Build' },
10
10
  'build': { icon: '📦', label: 'Dependency & Build' },
11
+ 'git': { icon: '🐙', label: 'Git & Repo' },
12
+ 'hygiene': { icon: '🧹', label: 'Code Hygiene' },
13
+ 'package': { icon: '📦', label: 'Packages' },
11
14
  };
12
15
 
13
16
  function getCategory(ruleId: string) {
@@ -15,7 +18,11 @@ function getCategory(ruleId: string) {
15
18
  return CATEGORIES[prefix] || { icon: '❓', label: 'Other' };
16
19
  }
17
20
 
18
- export function reportConsole(results: RuleResult[], cwd: string) {
21
+ export function reportConsole(results: RuleResult[], cwd: string, framework?: string) {
22
+ if (framework) {
23
+ console.log(chalk.blue(`ℹ️ Framework: ${framework.toUpperCase()}`));
24
+ }
25
+
19
26
  if (results.length === 0) {
20
27
  console.log(chalk.green('\n✅ Production Readiness Check Passed!\n'));
21
28
  return;
@@ -51,12 +51,12 @@ export async function checkBuild(ctx: RuleContext): Promise<RuleResult[]> {
51
51
  for (const dir of foundBuildDirs) {
52
52
  const mapFiles = await glob(`${dir}/**/*.map`, { cwd: ctx.cwd, absolute: true });
53
53
 
54
- for (const file of mapFiles) {
54
+ if (mapFiles.length > 0) {
55
55
  results.push({
56
56
  status: 'warn',
57
- message: `Source map found in build output (${dir}) (information leak)`,
57
+ message: `Found ${mapFiles.length} source map files in '${dir}' (e.g. ${path.basename(mapFiles[0])}). Ensure these are not exposed publicly.`,
58
58
  ruleId: 'build-source-map',
59
- file
59
+ file: dir // Point to the directory itself
60
60
  });
61
61
  }
62
62
 
package/src/rules/git.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import { execSync } from 'child_process';
2
2
  import { RuleContext, RuleResult } from '../engine/types';
3
3
 
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+
4
7
  export async function checkGit(ctx: RuleContext): Promise<RuleResult[]> {
5
8
  const results: RuleResult[] = [];
6
9
 
7
10
  try {
11
+ // ... (Existing git checks) ...
8
12
  // Check for uncommitted changes
9
13
  const status = execSync('git status --porcelain', { cwd: ctx.cwd, encoding: 'utf-8' });
10
14
  if (status.trim().length > 0) {
@@ -19,6 +23,7 @@ export async function checkGit(ctx: RuleContext): Promise<RuleResult[]> {
19
23
  const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ctx.cwd, encoding: 'utf-8' }).trim();
20
24
  const allowedBranches = ['main', 'master', 'staging', 'production', 'prod'];
21
25
  if (!allowedBranches.includes(branch)) {
26
+ // Warn, but maybe less aggressively? Keeping as warn.
22
27
  results.push({
23
28
  status: 'warn',
24
29
  message: `You are on branch '${branch}'. Production builds typically come from main/master.`,
@@ -26,10 +31,64 @@ export async function checkGit(ctx: RuleContext): Promise<RuleResult[]> {
26
31
  });
27
32
  }
28
33
 
34
+ // --- New: .gitignore Check ---
35
+ const gitignorePath = path.join(ctx.cwd, '.gitignore');
36
+ if (fs.existsSync(gitignorePath)) {
37
+ const content = fs.readFileSync(gitignorePath, 'utf-8');
38
+ const lines = content.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'));
39
+
40
+ // Helper to check if item is ignored (naive grep)
41
+ const isIgnored = (item: string) => lines.some(l => l.includes(item));
42
+
43
+ const requiredIgnores = ['node_modules', '.env'];
44
+ if (ctx.framework === 'nextjs') {
45
+ requiredIgnores.push('.next');
46
+ } else if (ctx.framework !== 'unknown') {
47
+ // For other frameworks, maybe 'dist' or 'build'
48
+ if (!isIgnored('dist') && !isIgnored('build')) {
49
+ // We can't strictly require one, but warn if NEITHER is found?
50
+ // Let's stick to safe defaults.
51
+ }
52
+ }
53
+
54
+ for (const item of requiredIgnores) {
55
+ if (!isIgnored(item)) {
56
+ results.push({
57
+ status: 'warn',
58
+ message: `.gitignore is missing '${item}'. This is critical for security and repo size.`,
59
+ ruleId: 'git-ignore-missing',
60
+ file: gitignorePath
61
+ });
62
+ }
63
+ }
64
+
65
+ // Check for specific dangerous files not being ignored
66
+ const dangerousPatterns = ['firebase.json', 'serviceAccountKey.json', '*.pem', '*.key'];
67
+ // This is tricky because firebase.json CAN be committed. serviceAccountKey.json should NOT.
68
+
69
+ if (!isIgnored('serviceAccountKey.json')) {
70
+ // Only warn if the FILE actually exists? Or just warn generic?
71
+ // Best to warn if the file exists AND isn't ignored.
72
+ if (fs.existsSync(path.join(ctx.cwd, 'serviceAccountKey.json'))) {
73
+ results.push({
74
+ status: 'fail',
75
+ message: 'serviceAccountKey.json exists but is NOT in .gitignore!',
76
+ ruleId: 'git-ignore-auth',
77
+ file: gitignorePath
78
+ });
79
+ }
80
+ }
81
+
82
+ } else {
83
+ results.push({
84
+ status: 'warn',
85
+ message: 'No .gitignore file found! node_modules and secrets might be committed.',
86
+ ruleId: 'git-no-ignore',
87
+ });
88
+ }
89
+
29
90
  } catch (e) {
30
91
  // Not a git repo or git not found
31
- // Silently fail or warn?
32
- // results.push({ status: 'warn', message: 'Not a git repository or git command failed.', ruleId: 'git-error' });
33
92
  }
34
93
 
35
94
  return results;
@@ -0,0 +1,52 @@
1
+ import fs from 'fs';
2
+ import { RuleContext, RuleResult } from '../engine/types';
3
+
4
+ export async function checkHygiene(ctx: RuleContext): Promise<RuleResult[]> {
5
+ const results: RuleResult[] = [];
6
+
7
+ const codeFiles = ctx.files.filter(f =>
8
+ f.match(/\.(js|ts|jsx|tsx)$/) &&
9
+ !f.includes('.test.') &&
10
+ !f.includes('.spec.')
11
+ );
12
+
13
+ for (const file of codeFiles) {
14
+ const content = fs.readFileSync(file, 'utf-8');
15
+ const lines = content.split('\n');
16
+
17
+ lines.forEach((line, index) => {
18
+ const lineNum = index + 1;
19
+
20
+ // 1. Console Log Check
21
+ // Allow console.error and console.warn, but warn on console.log
22
+ if (line.includes('console.log(')) {
23
+ // Ignore if commented out
24
+ if (!line.trim().startsWith('//') && !line.trim().startsWith('*')) {
25
+ results.push({
26
+ status: 'warn',
27
+ message: 'Leftover console.log() call detected.',
28
+ ruleId: 'hygiene-console-log',
29
+ file,
30
+ line: lineNum
31
+ });
32
+ }
33
+ }
34
+
35
+ // 2. TODO / FIXME Check
36
+ if (line.match(/\/\/\s*(TODO|FIXME):/i)) {
37
+
38
+ if (line.match(/FIXME/i)) {
39
+ results.push({
40
+ status: 'warn',
41
+ message: 'FIXME comment found. Resolve before shipping.',
42
+ ruleId: 'hygiene-fixme',
43
+ file,
44
+ line: lineNum
45
+ });
46
+ }
47
+ }
48
+ });
49
+ }
50
+
51
+ return results;
52
+ }
@@ -0,0 +1,33 @@
1
+ import fs from 'fs';
2
+ import { RuleContext, RuleResult } from '../engine/types';
3
+
4
+ export async function checkPackages(ctx: RuleContext): Promise<RuleResult[]> {
5
+ const results: RuleResult[] = [];
6
+
7
+ const packageJsons = ctx.files.filter(f => f.endsWith('package.json') && !f.includes('node_modules'));
8
+
9
+ for (const pkgFile of packageJsons) {
10
+ try {
11
+ const content = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
12
+ const deps = Object.keys(content.dependencies || {});
13
+ const devDeps = Object.keys(content.devDependencies || {});
14
+
15
+ // Find intersection
16
+ const duplicates = deps.filter(d => devDeps.includes(d));
17
+
18
+ for (const dup of duplicates) {
19
+ results.push({
20
+ status: 'warn',
21
+ message: `Package '${dup}' is listed in both 'dependencies' and 'devDependencies'.`,
22
+ ruleId: 'package-duplicate',
23
+ file: pkgFile
24
+ });
25
+ }
26
+
27
+ } catch (e) {
28
+ // ignore malformed json
29
+ }
30
+ }
31
+
32
+ return results;
33
+ }
package/SHIPPING.md DELETED
@@ -1,57 +0,0 @@
1
- # Shipping ship18ion
2
-
3
- ## 1. Local Testing (Link)
4
- To test `ship18ion` on your other local projects without publishing, use `npm link`.
5
-
6
- 1. **In this directory (ship18ion):**
7
- ```bash
8
- npm run build
9
- npm link
10
- ```
11
- This creates a global symlink to your local version.
12
-
13
- 2. **In your target project directory:**
14
- ```bash
15
- npm link ship18ion
16
- ```
17
- Now you can run:
18
- ```bash
19
- npx ship18ion check
20
- ```
21
-
22
- 3. **To unlink:**
23
- ```bash
24
- # In target project
25
- npm unlink -g ship18ion
26
-
27
- # In ship18ion directory
28
- npm unlink -g
29
- ```
30
-
31
- ## 2. Publishing to NPM
32
- To share this tool with the world.
33
-
34
- 1. **Login to NPM:**
35
- ```bash
36
- npm login
37
- ```
38
-
39
- 2. **Prepare:**
40
- - Ensure `package.json` has the correct version.
41
- - Run tests: `npm test` (or verifying script).
42
-
43
- 3. **Publish:**
44
- ```bash
45
- npm publish
46
- ```
47
-
48
- If you have a scoped package (e.g. `@yourname/ship18ion`), use:
49
- ```bash
50
- npm publish --access public
51
- ```
52
-
53
- ## 3. Usage for Users
54
- Once published, anyone can use it without installation:
55
- ```bash
56
- npx ship18ion@latest
57
- ```
package/walkthrough.md DELETED
@@ -1,51 +0,0 @@
1
- ship18ion - Production Readiness Inspector
2
- I have successfully built ship18ion, a CLI tool to check for production readiness.
3
-
4
- Features Implemented
5
- 1. Environment Variable Safety
6
- Unused Variable Detection: Scans
7
-
8
- .env
9
- files and code to find variables defined but never used.
10
- Missing Variable Detection: Identifies process.env.VAR usages that lack a corresponding definition in
11
-
12
- .env
13
- (or config).
14
- Format Support: Supports
15
-
16
- .env
17
- , .env.production files.
18
- Robust AST Parsing: Correctly detects process.env.VAR, import.meta.env.VAR (Vite), and process.env["VAR"].
19
- 2. Secrets Detection
20
- Pattern Matching: Detects AWS Keys, Google API Keys, Stripe Keys, and generic private keys.
21
- Entropy Heuristics: Detects potential high-entropy strings assigned to "secret" or "key" variables.
22
- 3. Framework & Security Checks
23
- Next.js Safety: Scans for NEXT_PUBLIC_ variables that appear to contain secrets (e.g. NEXT_PUBLIC_SECRET_KEY).
24
- Git Safety: Warns if deploying from a dirty working directory or a non-production branch.
25
- Debug Mode: Alerts on debug: true.
26
- CORS Wildcards: Fails if origin: '*' is detected.
27
- Database Credentials: Detects hardcoded connection strings.
28
- 4. Dependency & Build Safety
29
- Dev Dependencies: Warns if eslint or other dev tools are in dependencies.
30
- Build Artifacts: Alerts if source maps (.map) or
31
-
32
- .env
33
- files are found in build directories.
34
- Usage
35
- # In your project root
36
- npx ship18ion check
37
- # CI Mode (minimal output)
38
- npx ship18ion check --ci
39
- How to Ship & Share
40
- See
41
-
42
- SHIPPING.md
43
- for detailed instructions on:
44
-
45
- Local Testing: Using npm link to test on your other projects instantly.
46
- Publishing: Pushing to NPM so anyone can use npx ship18ion.
47
- Architecture
48
- CLI: Built with commander.
49
- Engine: TypeScript-based rule engine.
50
- Parsing: Babel-based AST parsing.
51
- Config: ship18ion.config.json support.