secure-husky-setup 1.0.12 → 1.0.13

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/bin/index.js CHANGED
@@ -3,7 +3,6 @@ const { installHusky } = require('../lib/husky');
3
3
  const { installGitleaks } = require('../lib/gitleaks');
4
4
  const { installSonarScanner, setupSonarProperties } = require('../lib/sonarqube');
5
5
  const { setupPreCommitHook } = require('../lib/hooks');
6
- // Added by Arjun — import CI setup functions from the new lib/ci.js module
7
6
  const { setupPrePushHook, setupCIScript, setupCIWorkflow, validateProject, ensurePackageLock } = require('../lib/ci');
8
7
  const { isGitRepo } = require('../lib/git');
9
8
  const { logInfo, logError, logSuccess } = require('../lib/logger');
@@ -19,41 +18,38 @@ const command = process.argv[2];
19
18
  try {
20
19
  logInfo("Initializing secure git hooks...");
21
20
 
22
- if (!await isGitRepo()) {
23
- throw new Error("Not inside a git repository.");
21
+ const { found, gitRoot, projectRoot } = await isGitRepo();
22
+
23
+ if (!found) {
24
+ throw new Error("Not inside a git repository. Please run 'git init' first.");
25
+ }
26
+
27
+ if (gitRoot !== projectRoot) {
28
+ logInfo(`Git root detected at: ${gitRoot}`);
29
+ logInfo(`Project root (package.json): ${projectRoot}`);
30
+ logInfo(`Monorepo/subfolder setup detected — hooks installed at git root, config at project root.`);
24
31
  }
25
32
 
26
- // ── Existing steps — pre-commit hooks (no changes made here) ─────────────
27
- await installHusky();
33
+ // ── Pre-commit hooks ──────────────────────────────────────────────────────
34
+ await installHusky(gitRoot);
28
35
  await installGitleaks();
29
36
  await installSonarScanner();
30
37
  await setupSonarProperties();
31
- await setupPreCommitHook();
38
+ await setupPreCommitHook(gitRoot);
32
39
 
33
40
  logSuccess("Secure Husky + Gitleaks + SonarQube setup completed.");
34
41
  logInfo("Next step: edit sonar-project.properties and set sonar.host.url and sonar.token.");
35
42
 
36
- // Added by Arjun — pre-push hook + GitHub Actions CI workflow setup ───────
37
- // Runs Newman API tests and smoke tests automatically on every git push
43
+ // ── Pre-push hook + GitHub Actions CI workflow ────────────────────────────
38
44
  logInfo("Setting up Newman & Smoke Test CI workflow...");
39
45
 
40
- // Added by Arjun — ensure package-lock.json exists (required by npm ci in workflow)
41
46
  await ensurePackageLock();
42
-
43
- // Added by Arjun — validate package.json has "start" and "test" scripts
44
47
  await validateProject();
45
-
46
- // Added by Arjun — write standalone scripts/run-ci-checks.sh (all test logic lives here)
47
- await setupCIScript();
48
-
49
- // Added by Arjun — copy ci-tests.yml into .github/workflows/
48
+ await setupCIScript(gitRoot);
50
49
  await setupCIWorkflow();
51
-
52
- // Added by Arjun — create .husky/pre-push hook (thin wrapper that calls run-ci-checks.sh)
53
- await setupPrePushHook();
50
+ await setupPrePushHook(gitRoot);
54
51
 
55
52
  logSuccess("Newman + Smoke Test pre-push hook and GitHub Actions workflow setup completed.");
56
- // ── End of Arjun's additions ──────────────────────────────────────────────
57
53
 
58
54
  } catch (err) {
59
55
  logError(err.message);
package/lib/ci.js CHANGED
@@ -1,12 +1,5 @@
1
1
  'use strict';
2
2
 
3
- // ─────────────────────────────────────────────────────────────────────────────
4
- // Added by Arjun
5
- // File: lib/ci.js
6
- // Purpose: Sets up Newman API tests + Smoke Tests as a pre-push git hook
7
- // and copies the GitHub Actions CI workflow into the project.
8
- // ─────────────────────────────────────────────────────────────────────────────
9
-
10
3
  const fs = require('fs-extra');
11
4
  const path = require('path');
12
5
  const { execSync } = require('child_process');
@@ -14,7 +7,8 @@ const { logInfo, logSuccess, logError } = require('./logger');
14
7
 
15
8
  const TEMPLATE_PATH = path.resolve(__dirname, '../templates/ci-tests.yml');
16
9
 
17
- exports.setupCIScript = async () => {
10
+ exports.setupCIScript = async (gitRoot) => {
11
+ const projectDir = path.relative(gitRoot, process.cwd()) || '.';
18
12
  const scriptsDir = path.join(process.cwd(), 'scripts');
19
13
  const scriptPath = path.join(scriptsDir, 'run-ci-checks.sh');
20
14
 
@@ -26,14 +20,14 @@ exports.setupCIScript = async () => {
26
20
  logInfo("Creating scripts/run-ci-checks.sh...");
27
21
  }
28
22
 
29
- await fs.writeFile(scriptPath, buildCIScript());
23
+ await fs.writeFile(scriptPath, buildCIScript(projectDir));
30
24
  await fs.chmod(scriptPath, 0o755);
31
25
  logSuccess("scripts/run-ci-checks.sh created.");
32
26
  logInfo("To move tests to pre-commit in future: add './scripts/run-ci-checks.sh' to .husky/pre-commit.");
33
27
  };
34
28
 
35
- exports.setupPrePushHook = async () => {
36
- const huskyDir = path.join(process.cwd(), '.husky');
29
+ exports.setupPrePushHook = async (gitRoot) => {
30
+ const huskyDir = path.join(gitRoot, '.husky');
37
31
  const hookPath = path.join(huskyDir, 'pre-push');
38
32
 
39
33
  if (!await fs.pathExists(huskyDir)) {
@@ -41,13 +35,15 @@ exports.setupPrePushHook = async () => {
41
35
  return;
42
36
  }
43
37
 
38
+ const projectDir = path.relative(gitRoot, process.cwd()) || '.';
39
+
44
40
  if (await fs.pathExists(hookPath)) {
45
41
  logInfo("Pre-push hook already configured. Overwriting with latest setup...");
46
42
  } else {
47
43
  logInfo("Creating new pre-push hook...");
48
44
  }
49
45
 
50
- await fs.writeFile(hookPath, buildPrePushHook());
46
+ await fs.writeFile(hookPath, buildPrePushHook(projectDir));
51
47
  await fs.chmod(hookPath, 0o755);
52
48
  logSuccess("Pre-push hook created — calls scripts/run-ci-checks.sh.");
53
49
  };
@@ -85,12 +81,12 @@ exports.validateProject = async () => {
85
81
  const scripts = pkg.scripts || {};
86
82
 
87
83
  if (!scripts.start) {
88
- logError('No "start" script in package.json — CI server boot will fail.');
84
+ logError('No "start" script in package.json — smoke tests will be skipped.');
89
85
  logInfo('Add: "start": "node index.js"');
90
86
  }
91
87
 
92
88
  if (!scripts.test) {
93
- logError('No "test" script in package.json — smoke tests will fail.');
89
+ logError('No "test" script in package.json — smoke tests will be skipped.');
94
90
  logInfo('Add: "test": "jest" (or your test runner)');
95
91
  }
96
92
 
@@ -117,19 +113,17 @@ exports.ensurePackageLock = async () => {
117
113
  }
118
114
  };
119
115
 
120
- function buildPrePushHook() {
116
+ function buildPrePushHook(projectDir) {
117
+ const cdLine = projectDir !== '.' ? `cd "${projectDir}"` : '';
121
118
  return `#!/bin/sh
122
119
 
123
- # ---------------------------------------------------------------
124
120
  # Pre-push hook — Newman + Smoke Tests
125
- # Delegates all logic to scripts/run-ci-checks.sh
126
- # ---------------------------------------------------------------
127
-
121
+ ${cdLine ? cdLine + '\n' : ''}
128
122
  ./scripts/run-ci-checks.sh
129
123
  `;
130
124
  }
131
125
 
132
- function buildCIScript() {
126
+ function buildCIScript(projectDir) {
133
127
  return `#!/bin/sh
134
128
 
135
129
  # ---------------------------------------------------------------
@@ -166,17 +160,17 @@ echo ""
166
160
  echo "[CI Checks] Starting checks..."
167
161
 
168
162
  # ---------------------------------------------------------------
169
- # Check if start script exists
163
+ # Check if start and test scripts exist
170
164
  # ---------------------------------------------------------------
171
- START_SCRIPT=$(node -e "const p=require('./package.json'); console.log(p.scripts&&p.scripts.start?'yes':'no')" 2>/dev/null)
172
- TEST_SCRIPT=$(node -e "const p=require('./package.json'); console.log(p.scripts&&p.scripts.test?'yes':'no')" 2>/dev/null)
165
+ START_SCRIPT=$(node -e "try{const p=require('./package.json');console.log(p.scripts&&p.scripts.start?'yes':'no')}catch(e){console.log('no')}" 2>/dev/null)
166
+ TEST_SCRIPT=$(node -e "try{const p=require('./package.json');console.log(p.scripts&&p.scripts.test?'yes':'no')}catch(e){console.log('no')}" 2>/dev/null)
173
167
 
174
168
  if [ "$START_SCRIPT" = "no" ]; then
175
169
  echo "[Smoke Tests] No start script in package.json — skipping smoke tests."
176
170
  else
177
171
 
178
172
  # ---------------------------------------------------------------
179
- # Step 1: Smoke Tests
173
+ # Step 1: Smoke Tests — start server + run tests
180
174
  # ---------------------------------------------------------------
181
175
  echo ""
182
176
  echo "[Smoke Tests] Starting server..."
@@ -184,10 +178,10 @@ else
184
178
  npm start &
185
179
  SERVER_PID=\$!
186
180
 
187
- # Auto-detect port — tries common ports
181
+ # Auto-detect port — tries all common ports
188
182
  SERVER_UP=0
189
183
  for i in \$(seq 1 30); do
190
- for PORT_TRY in 3000 5000 8000 8080 4000 4200 3001; do
184
+ for PORT_TRY in 3000 5000 8000 8080 4000 4200 3001 8081 1337 9000; do
191
185
  if curl -sf http://localhost:\$PORT_TRY > /dev/null 2>&1; then
192
186
  PORT=\$PORT_TRY
193
187
  SERVER_UP=1
@@ -221,7 +215,7 @@ else
221
215
  fi
222
216
 
223
217
  # ---------------------------------------------------------------
224
- # Step 2: Newman
218
+ # Step 2: Newman API Tests
225
219
  # ---------------------------------------------------------------
226
220
  echo ""
227
221
  echo "[Newman] Looking for Postman collections..."
@@ -241,7 +235,7 @@ else
241
235
 
242
236
  if ! command -v newman > /dev/null 2>&1; then
243
237
  echo "[Newman] Installing newman globally..."
244
- npm install -g newman newman-reporter-htmlextra
238
+ npm install -g newman newman-reporter-htmlextra 2>/dev/null || true
245
239
  fi
246
240
 
247
241
  mkdir -p newman-reports
package/lib/git.js CHANGED
@@ -1,19 +1,20 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
 
4
+ // Returns { found, gitRoot, projectRoot }
5
+ // gitRoot = where .git folder is (where husky installs)
6
+ // projectRoot = where package.json is (where scripts/sonar/gitleaks live)
4
7
  exports.isGitRepo = async () => {
5
- // Walk up directory tree to find .git folder
6
- // This handles postinstall where cwd() is inside node_modules/package-name
7
- let dir = process.cwd();
8
+ const projectRoot = process.cwd();
9
+ let dir = projectRoot;
8
10
 
9
11
  while (true) {
10
12
  if (fs.existsSync(path.join(dir, '.git'))) {
11
- return true;
13
+ return { found: true, gitRoot: dir, projectRoot };
12
14
  }
13
15
  const parent = path.dirname(dir);
14
- if (parent === dir) break; // reached filesystem root
16
+ if (parent === dir) break;
15
17
  dir = parent;
16
18
  }
17
-
18
- return false;
19
+ return { found: false, gitRoot: null, projectRoot };
19
20
  };
package/lib/hooks.js CHANGED
@@ -2,8 +2,8 @@ const fs = require('fs-extra');
2
2
  const path = require('path');
3
3
  const { logInfo, logSuccess } = require('./logger');
4
4
 
5
- exports.setupPreCommitHook = async () => {
6
- const huskyDir = path.join(process.cwd(), '.husky');
5
+ exports.setupPreCommitHook = async (gitRoot) => {
6
+ const huskyDir = path.join(gitRoot, '.husky');
7
7
  const hookPath = path.join(huskyDir, 'pre-commit');
8
8
 
9
9
  if (!await fs.pathExists(huskyDir)) {
@@ -11,7 +11,11 @@ exports.setupPreCommitHook = async () => {
11
11
  return;
12
12
  }
13
13
 
14
- const hookContent = buildHookScript();
14
+ // projectDir = where package.json, sonar-project.properties, .tools/ live
15
+ // This is relative path from gitRoot to project (e.g. "server" or ".")
16
+ const projectDir = path.relative(gitRoot, process.cwd()) || '.';
17
+
18
+ const hookContent = buildHookScript(projectDir);
15
19
 
16
20
  if (await fs.pathExists(hookPath)) {
17
21
  logInfo("Pre-commit hook already configured. Overwriting with latest setup...");
@@ -22,6 +26,7 @@ exports.setupPreCommitHook = async () => {
22
26
  await fs.writeFile(hookPath, hookContent);
23
27
  await fs.chmod(hookPath, 0o755);
24
28
 
29
+ // .gitleaksignore goes in the project dir, not git root
25
30
  const gitleaksIgnorePath = path.join(process.cwd(), '.gitleaksignore');
26
31
  await fs.writeFile(gitleaksIgnorePath, '.tools/\nsonar-project.properties\n');
27
32
  logInfo(".gitleaksignore created — excluding .tools/ and sonar-project.properties.");
@@ -29,9 +34,23 @@ exports.setupPreCommitHook = async () => {
29
34
  logSuccess("Pre-commit hook created with Gitleaks + SonarQube (git diff only).");
30
35
  };
31
36
 
32
- function buildHookScript() {
37
+ function buildHookScript(projectDir) {
38
+ // If project is in a subfolder, all tool paths must be prefixed
39
+ const cdLine = projectDir !== '.' ? `cd "${projectDir}"` : '';
40
+ const gitleaksBin = projectDir !== '.'
41
+ ? `./${projectDir}/.tools/gitleaks/gitleaks`
42
+ : `./.tools/gitleaks/gitleaks`;
43
+ const sonarBin = projectDir !== '.'
44
+ ? `./${projectDir}/node_modules/.bin/sonar-scanner`
45
+ : `./node_modules/.bin/sonar-scanner`;
46
+ const sonarProps = projectDir !== '.'
47
+ ? `./${projectDir}/sonar-project.properties`
48
+ : `./sonar-project.properties`;
49
+
33
50
  return `#!/bin/sh
34
51
 
52
+ # Move to project directory if in monorepo/subfolder setup
53
+ ${cdLine ? cdLine + '\n' : ''}
35
54
  STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM)
36
55
 
37
56
  if [ -z "$STAGED_FILES" ]; then
@@ -47,7 +66,7 @@ done
47
66
  echo ""
48
67
  echo "[Gitleaks] Scanning changed files for secrets..."
49
68
 
50
- GITLEAKS_BIN="./.tools/gitleaks/gitleaks"
69
+ GITLEAKS_BIN="${gitleaksBin}"
51
70
 
52
71
  if [ ! -f "$GITLEAKS_BIN" ]; then
53
72
  echo "[Gitleaks] Binary not found. Skipping."
@@ -58,6 +77,7 @@ else
58
77
  case "$FILE" in
59
78
  sonar-project.properties) ;;
60
79
  .tools/*) ;;
80
+ ${projectDir !== '.' ? `${projectDir}/.tools/*) ;;` : ''}
61
81
  *)
62
82
  if [ -f "$FILE" ]; then
63
83
  DEST="$GITLEAKS_TMPDIR/$FILE"
@@ -83,15 +103,16 @@ fi
83
103
  echo ""
84
104
  echo "[SonarQube] Scanning changed files..."
85
105
 
86
- SONAR_BIN="./node_modules/.bin/sonar-scanner"
106
+ SONAR_BIN="${sonarBin}"
107
+ SONAR_PROPS="${sonarProps}"
87
108
 
88
109
  if [ ! -f "$SONAR_BIN" ]; then
89
110
  echo "[SonarQube] sonar-scanner not found. Skipping."
90
111
  else
91
- if [ ! -f "sonar-project.properties" ]; then
112
+ if [ ! -f "$SONAR_PROPS" ]; then
92
113
  echo "[SonarQube] sonar-project.properties not found. Skipping."
93
114
  else
94
- SONAR_HOST=$(grep "^sonar.host.url=" sonar-project.properties | cut -d'=' -f2 | tr -d '[:space:]')
115
+ SONAR_HOST=$(grep "^sonar.host.url=" "$SONAR_PROPS" | cut -d'=' -f2 | tr -d '[:space:]')
95
116
  SONAR_DOMAIN=$(echo "$SONAR_HOST" | sed 's|https://||' | sed 's|http://||' | cut -d'/' -f1 | cut -d':' -f1)
96
117
  SONAR_PORT=$(echo "$SONAR_HOST" | grep -o ':[0-9]*$' | tr -d ':')
97
118
  SONAR_PORT=\${SONAR_PORT:-9000}
@@ -99,15 +120,15 @@ else
99
120
  if ! nc -z -w3 "$SONAR_DOMAIN" "$SONAR_PORT" 2>/dev/null; then
100
121
  echo "[SonarQube] Server unreachable — skipping analysis."
101
122
  else
102
- SONAR_INCLUSIONS=$(echo "$STAGED_FILES" | tr '\n' ',' | sed 's/,$//')
123
+ SONAR_INCLUSIONS=$(echo "$STAGED_FILES" | tr '\\n' ',' | sed 's/,$//')
103
124
  echo "[SonarQube] Scanning: $SONAR_INCLUSIONS"
104
125
 
105
- $SONAR_BIN -Dsonar.inclusions="$SONAR_INCLUSIONS" -Dsonar.qualitygate.wait=true
126
+ $SONAR_BIN -Dproject.settings="$SONAR_PROPS" -Dsonar.inclusions="$SONAR_INCLUSIONS" -Dsonar.qualitygate.wait=true
106
127
  SONAR_EXIT=$?
107
128
 
108
129
  if [ $SONAR_EXIT -ne 0 ]; then
109
130
  echo "[SonarQube] Quality Gate FAILED. Commit blocked."
110
- echo "[SonarQube] Fix issues at: $SONAR_HOST/dashboard?id=$(grep 'sonar.projectKey' sonar-project.properties | cut -d'=' -f2)"
131
+ echo "[SonarQube] Fix issues at: $SONAR_HOST"
111
132
  exit 1
112
133
  fi
113
134
 
package/lib/husky.js CHANGED
@@ -1,12 +1,17 @@
1
- const { fileExists, readJSON, writeJSON } = require('./utils');
1
+ const { readJSON, writeJSON } = require('./utils');
2
2
  const { installDevDependency } = require('./packageManager');
3
3
  const execa = require('execa');
4
4
  const path = require('path');
5
5
  const fs = require('fs-extra');
6
- const { logInfo, logSuccess } = require('./logger');
6
+ const { logInfo, logSuccess, logError } = require('./logger');
7
7
 
8
- exports.installHusky = async () => {
8
+ exports.installHusky = async (gitRoot) => {
9
9
  const pkgPath = path.join(process.cwd(), 'package.json');
10
+
11
+ if (!await fs.pathExists(pkgPath)) {
12
+ throw new Error(`No package.json found in ${process.cwd()}. Please run from your project root.`);
13
+ }
14
+
10
15
  const pkg = await readJSON(pkgPath);
11
16
 
12
17
  if (!pkg.devDependencies || !pkg.devDependencies.husky) {
@@ -15,13 +20,23 @@ exports.installHusky = async () => {
15
20
  }
16
21
 
17
22
  logInfo("Initializing Husky...");
18
- await execa('npx', ['husky', 'install'], { stdio: 'inherit' });
19
23
 
20
- if (!pkg.scripts) pkg.scripts = {};
24
+ // Always run husky install from the git root — this is where .husky/ is created
25
+ await execa('npx', ['husky', 'install'], { stdio: 'inherit', cwd: gitRoot });
21
26
 
27
+ // Add prepare script to the project's package.json
28
+ // If project is a subfolder, prepare script needs to reference path to git root
29
+ if (!pkg.scripts) pkg.scripts = {};
22
30
  if (!pkg.scripts.prepare) {
23
- pkg.scripts.prepare = "husky install";
31
+ const isSubfolder = gitRoot !== process.cwd();
32
+ if (isSubfolder) {
33
+ // Calculate relative path from project root to git root for husky install
34
+ const relPath = path.relative(process.cwd(), gitRoot);
35
+ pkg.scripts.prepare = `cd ${relPath} && husky install ${path.relative(gitRoot, process.cwd())}/.husky || true`;
36
+ } else {
37
+ pkg.scripts.prepare = "husky install";
38
+ }
24
39
  await writeJSON(pkgPath, pkg);
25
40
  logSuccess("Added prepare script.");
26
41
  }
27
- };
42
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "secure-husky-setup",
3
- "version": "1.0.12",
3
+ "version": "1.0.13",
4
4
  "description": "Automatic Husky + Gitleaks setup for any JS project",
5
5
  "main": "bin/index.js",
6
6
  "bin": {