@edcalderon/versioning 1.2.0 → 1.3.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/README.md CHANGED
@@ -11,7 +11,9 @@ A comprehensive versioning and changelog management tool designed for monorepos
11
11
  - 📦 NPM publishable
12
12
  - 🏷️ Git tagging and committing
13
13
  - ✅ Validation of version sync
14
- - 🔌 **Extensible plugin system** for custom business logic
14
+ - 🔌 **Extensible plugin system** for subdirectory-based extensions
15
+ - 🔒 **Security Checks** with automatic Husky integration
16
+ - 🧹 **Repository Cleanup** to keep root directory organized
15
17
 
16
18
  ## Installation
17
19
 
@@ -33,7 +35,7 @@ The versioning tool supports a **composable extension system** that allows you t
33
35
  - Implement custom versioning strategies
34
36
 
35
37
  Extensions are loaded automatically from:
36
- - Built-in extensions in the `src/extensions/` directory
38
+ - Built-in extensions in subdirectories of `src/extensions/` (e.g. `src/extensions/reentry-status/index.ts`)
37
39
  - External packages listed in `versioning.config.json`
38
40
 
39
41
  ### Creating Extensions
@@ -172,20 +174,56 @@ versioning cleanup scan # Dry-run scan of root
172
174
  versioning cleanup move # Move files to configured destinations
173
175
  versioning cleanup restore # Restore a moved file
174
176
  versioning cleanup config # View/manage configuration
175
- versioning cleanup husky # Setup git hook
177
+ versioning cleanup husky # Setup git hook (scan-only by default)
176
178
  ```
177
179
 
178
180
  Configuration (`versioning.config.json`):
179
181
 
180
182
  ```json
181
183
  {
182
- "cleanup": {
183
- "enabled": true,
184
- "defaultDestination": "docs",
185
- "allowlist": ["CHANGELOG.md"],
186
- "routes": {
187
- ".sh": "scripts",
188
- ".json": "config"
184
+ "extensionConfig": {
185
+ "cleanup-repo": {
186
+ "enabled": true,
187
+ "defaultDestination": "docs",
188
+ "allowlist": ["CHANGELOG.md"],
189
+ "routes": {
190
+ ".sh": "scripts",
191
+ ".json": "config"
192
+ }
193
+ }
194
+ }
195
+ }
196
+ ```
197
+
198
+ #### Secrets Check Extension
199
+
200
+ Prevents sensitive data (private keys, tokens, mnemonics) from being committed to the repository.
201
+
202
+ Features:
203
+ - **Pre-defined Patterns:** Detects AWS, GitHub, NPM tokens, private keys, and EVM mnemonics.
204
+ - **Allowlist:** Ignore false positives or specific test keys.
205
+ - **Husky Integration:** Easy CLI setup to block commits containing secrets.
206
+
207
+ Commands:
208
+
209
+ ```bash
210
+ versioning check-secrets # Scan staged files for secrets
211
+ versioning check-secrets husky # Add blocking secrets check to pre-commit hook
212
+ ```
213
+
214
+ Configuration (`versioning.config.json`):
215
+
216
+ ```json
217
+ {
218
+ "extensionConfig": {
219
+ "secrets-check": {
220
+ "enabled": true,
221
+ "patterns": [
222
+ "CUSTOM_API_KEY=[0-9a-f]{32}"
223
+ ],
224
+ "allowlist": [
225
+ "ETHERSCAN_API_KEY=YOUR_KEY"
226
+ ]
189
227
  }
190
228
  }
191
229
  }
@@ -1,4 +1,4 @@
1
- import { VersioningExtension } from '../extensions';
1
+ import { VersioningExtension } from '../../extensions';
2
2
  /**
3
3
  * The canonical cleanup configuration schema.
4
4
  * Everything the plugin needs lives here.
@@ -46,4 +46,4 @@ declare function loadCleanupConfig(rootConfig: any): CleanupRepoConfig;
46
46
  declare const extension: VersioningExtension;
47
47
  export { loadCleanupConfig, BUILTIN_ALLOWLIST, DEFAULT_EXTENSIONS, DEFAULT_ROUTES };
48
48
  export default extension;
49
- //# sourceMappingURL=cleanup-repo-extension.d.ts.map
49
+ //# sourceMappingURL=index.d.ts.map
@@ -124,7 +124,7 @@ exports.DEFAULT_ROUTES = DEFAULT_ROUTES;
124
124
  // CONFIG LOADER
125
125
  // ─────────────────────────────────────────────────────────────────
126
126
  function loadCleanupConfig(rootConfig) {
127
- const raw = rootConfig?.cleanup ?? {};
127
+ const raw = rootConfig?.extensionConfig?.['cleanup-repo'] ?? rootConfig?.cleanup ?? {};
128
128
  return {
129
129
  enabled: raw.enabled !== false,
130
130
  defaultDestination: typeof raw.defaultDestination === 'string' ? raw.defaultDestination : 'docs',
@@ -262,7 +262,7 @@ const extension = {
262
262
  return;
263
263
  }
264
264
  console.log('\n🔍 Repository Root Cleanup Scan\n');
265
- console.log(` Config: versioning.config.json → cleanup`);
265
+ console.log(` Config: versioning.config.json → extensionConfig['cleanup-repo']`);
266
266
  console.log(` Default destination: ${cfg.defaultDestination}/`);
267
267
  console.log(` Extensions monitored: ${cfg.extensions.join(', ')}`);
268
268
  console.log(` Allowlist (custom): ${cfg.allowlist.length > 0 ? cfg.allowlist.join(', ') : '—'}`);
@@ -390,23 +390,27 @@ const extension = {
390
390
  if (await fs.pathExists(configPath)) {
391
391
  rawCfg = await fs.readJson(configPath);
392
392
  }
393
- // Ensure cleanup section exists
394
- if (!rawCfg.cleanup)
395
- rawCfg.cleanup = {};
396
- if (!Array.isArray(rawCfg.cleanup.allowlist))
397
- rawCfg.cleanup.allowlist = [];
398
- if (!Array.isArray(rawCfg.cleanup.denylist))
399
- rawCfg.cleanup.denylist = [];
400
- if (!rawCfg.cleanup.routes)
401
- rawCfg.cleanup.routes = { ...DEFAULT_ROUTES };
402
- if (!rawCfg.cleanup.extensions)
403
- rawCfg.cleanup.extensions = [...DEFAULT_EXTENSIONS];
393
+ // Ensure extensionConfig section exists
394
+ if (!rawCfg.extensionConfig)
395
+ rawCfg.extensionConfig = {};
396
+ if (!rawCfg.extensionConfig['cleanup-repo']) {
397
+ rawCfg.extensionConfig['cleanup-repo'] = rawCfg.cleanup || {};
398
+ }
399
+ const extensionCfg = rawCfg.extensionConfig['cleanup-repo'];
400
+ if (!Array.isArray(extensionCfg.allowlist))
401
+ extensionCfg.allowlist = [];
402
+ if (!Array.isArray(extensionCfg.denylist))
403
+ extensionCfg.denylist = [];
404
+ if (!extensionCfg.routes)
405
+ extensionCfg.routes = { ...DEFAULT_ROUTES };
406
+ if (!extensionCfg.extensions)
407
+ extensionCfg.extensions = [...DEFAULT_EXTENSIONS];
404
408
  let modified = false;
405
409
  // ── --allow
406
410
  if (options.allow) {
407
411
  const file = String(options.allow).trim();
408
- if (!rawCfg.cleanup.allowlist.includes(file)) {
409
- rawCfg.cleanup.allowlist.push(file);
412
+ if (!extensionCfg.allowlist.includes(file)) {
413
+ extensionCfg.allowlist.push(file);
410
414
  modified = true;
411
415
  console.log(`✅ Added "${file}" to cleanup.allowlist`);
412
416
  }
@@ -417,8 +421,8 @@ const extension = {
417
421
  // ── --deny
418
422
  if (options.deny) {
419
423
  const file = String(options.deny).trim();
420
- if (!rawCfg.cleanup.denylist.includes(file)) {
421
- rawCfg.cleanup.denylist.push(file);
424
+ if (!extensionCfg.denylist.includes(file)) {
425
+ extensionCfg.denylist.push(file);
422
426
  modified = true;
423
427
  console.log(`✅ Added "${file}" to cleanup.denylist`);
424
428
  }
@@ -429,9 +433,9 @@ const extension = {
429
433
  // ── --unallow
430
434
  if (options.unallow) {
431
435
  const file = String(options.unallow).trim();
432
- const idx = rawCfg.cleanup.allowlist.indexOf(file);
436
+ const idx = extensionCfg.allowlist.indexOf(file);
433
437
  if (idx !== -1) {
434
- rawCfg.cleanup.allowlist.splice(idx, 1);
438
+ extensionCfg.allowlist.splice(idx, 1);
435
439
  modified = true;
436
440
  console.log(`✅ Removed "${file}" from cleanup.allowlist`);
437
441
  }
@@ -442,9 +446,9 @@ const extension = {
442
446
  // ── --undeny
443
447
  if (options.undeny) {
444
448
  const file = String(options.undeny).trim();
445
- const idx = rawCfg.cleanup.denylist.indexOf(file);
449
+ const idx = extensionCfg.denylist.indexOf(file);
446
450
  if (idx !== -1) {
447
- rawCfg.cleanup.denylist.splice(idx, 1);
451
+ extensionCfg.denylist.splice(idx, 1);
448
452
  modified = true;
449
453
  console.log(`✅ Removed "${file}" from cleanup.denylist`);
450
454
  }
@@ -464,15 +468,15 @@ const extension = {
464
468
  const dest = mapping.slice(eqIdx + 1).trim();
465
469
  if (!ext.startsWith('.'))
466
470
  ext = `.${ext}`;
467
- rawCfg.cleanup.routes[ext] = dest;
471
+ extensionCfg.routes[ext] = dest;
468
472
  modified = true;
469
473
  console.log(`✅ Added route: ${ext} → ${dest}/`);
470
474
  }
471
475
  // ── --set-dest
472
476
  if (options.setDest) {
473
- rawCfg.cleanup.defaultDestination = String(options.setDest).trim();
477
+ extensionCfg.defaultDestination = String(options.setDest).trim();
474
478
  modified = true;
475
- console.log(`✅ Default destination set to "${rawCfg.cleanup.defaultDestination}"`);
479
+ console.log(`✅ Default destination set to "${extensionCfg.defaultDestination}"`);
476
480
  }
477
481
  // Write config if modified
478
482
  if (modified) {
@@ -626,16 +630,20 @@ async function updateHuskyConfig(configPath, enabled, hook, mode) {
626
630
  if (await fs.pathExists(fullPath)) {
627
631
  rawCfg = await fs.readJson(fullPath);
628
632
  }
629
- if (!rawCfg.cleanup)
630
- rawCfg.cleanup = {};
631
- if (!rawCfg.cleanup.husky)
632
- rawCfg.cleanup.husky = {};
633
- rawCfg.cleanup.husky.enabled = enabled;
633
+ if (!rawCfg.extensionConfig)
634
+ rawCfg.extensionConfig = {};
635
+ if (!rawCfg.extensionConfig['cleanup-repo']) {
636
+ rawCfg.extensionConfig['cleanup-repo'] = rawCfg.cleanup || {};
637
+ }
638
+ const extensionCfg = rawCfg.extensionConfig['cleanup-repo'];
639
+ if (!extensionCfg.husky)
640
+ extensionCfg.husky = {};
641
+ extensionCfg.husky.enabled = enabled;
634
642
  if (hook)
635
- rawCfg.cleanup.husky.hook = hook;
643
+ extensionCfg.husky.hook = hook;
636
644
  if (mode)
637
- rawCfg.cleanup.husky.mode = mode;
645
+ extensionCfg.husky.mode = mode;
638
646
  await fs.writeJson(fullPath, rawCfg, { spaces: 2 });
639
647
  }
640
648
  exports.default = extension;
641
- //# sourceMappingURL=cleanup-repo-extension.js.map
649
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ import { VersioningExtension } from '../../extensions';
2
+ declare const extension: VersioningExtension;
3
+ export default extension;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -65,4 +65,4 @@ const extension = {
65
65
  }
66
66
  };
67
67
  exports.default = extension;
68
- //# sourceMappingURL=lifecycle-hooks.js.map
68
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ import { VersioningExtension } from '../../extensions';
2
+ declare const extension: VersioningExtension;
3
+ export default extension;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -178,4 +178,4 @@ async function publishToLocalRegistry(options) {
178
178
  console.log(`✅ Published to local registry: ${registry}`);
179
179
  }
180
180
  exports.default = extension;
181
- //# sourceMappingURL=npm-publish.js.map
181
+ //# sourceMappingURL=index.js.map
@@ -84,7 +84,9 @@ function defaultFilesConfigForProject(project) {
84
84
  class ConfigManager {
85
85
  static loadConfig(rootConfig, project) {
86
86
  const canonicalProject = canonicalProjectKey(project);
87
- const raw = (rootConfig && typeof rootConfig === 'object' ? rootConfig.reentryStatus : undefined);
87
+ // Try new extensionConfig location first, then fallback to root-level property
88
+ const extensionConfig = rootConfig?.extensionConfig?.['reentry-status'];
89
+ const raw = (extensionConfig ?? (rootConfig && typeof rootConfig === 'object' ? rootConfig.reentryStatus : undefined));
88
90
  const basePartial = { ...(raw ?? {}) };
89
91
  delete basePartial.projects;
90
92
  const projectPartial = canonicalProject && raw && typeof raw === 'object' && raw.projects && typeof raw.projects === 'object'
@@ -1,4 +1,4 @@
1
- export declare const REENTRY_EXTENSION_NAME = "reentry-status-extension";
1
+ export declare const REENTRY_EXTENSION_NAME = "reentry-status";
2
2
  export declare const REENTRY_STATUS_DIRNAME = ".versioning";
3
3
  export declare const REENTRY_STATUS_JSON_FILENAME = "reentry.status.json";
4
4
  export declare const REENTRY_STATUS_MD_FILENAME = "REENTRY.md";
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ROADMAP_MD_FILENAME = exports.REENTRY_STATUS_MD_FILENAME = exports.REENTRY_STATUS_JSON_FILENAME = exports.REENTRY_STATUS_DIRNAME = exports.REENTRY_EXTENSION_NAME = void 0;
4
- exports.REENTRY_EXTENSION_NAME = 'reentry-status-extension';
4
+ exports.REENTRY_EXTENSION_NAME = 'reentry-status';
5
5
  exports.REENTRY_STATUS_DIRNAME = '.versioning';
6
6
  exports.REENTRY_STATUS_JSON_FILENAME = 'reentry.status.json';
7
7
  exports.REENTRY_STATUS_MD_FILENAME = 'REENTRY.md';
@@ -0,0 +1,4 @@
1
+ import { VersioningExtension } from '../../extensions';
2
+ declare const extension: VersioningExtension;
3
+ export default extension;
4
+ //# sourceMappingURL=extension.d.ts.map
@@ -36,19 +36,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const commander_1 = require("commander");
37
37
  const fs = __importStar(require("fs-extra"));
38
38
  const path = __importStar(require("path"));
39
- const config_manager_1 = require("./reentry-status/config-manager");
40
- const constants_1 = require("./reentry-status/constants");
41
- const file_manager_1 = require("./reentry-status/file-manager");
42
- const dirty_detection_1 = require("./reentry-status/dirty-detection");
43
- const github_rest_client_1 = require("./reentry-status/github-rest-client");
44
- const github_sync_adapter_1 = require("./reentry-status/github-sync-adapter");
45
- const obsidian_cli_client_1 = require("./reentry-status/obsidian-cli-client");
46
- const obsidian_sync_adapter_1 = require("./reentry-status/obsidian-sync-adapter");
47
- const roadmap_parser_1 = require("./reentry-status/roadmap-parser");
48
- const roadmap_renderer_1 = require("./reentry-status/roadmap-renderer");
49
- const status_renderer_1 = require("./reentry-status/status-renderer");
50
- const reentry_status_manager_1 = require("./reentry-status/reentry-status-manager");
51
- const git_context_1 = require("./reentry-status/git-context");
39
+ const config_manager_1 = require("./config-manager");
40
+ const constants_1 = require("./constants");
41
+ const file_manager_1 = require("./file-manager");
42
+ const dirty_detection_1 = require("./dirty-detection");
43
+ const github_rest_client_1 = require("./github-rest-client");
44
+ const github_sync_adapter_1 = require("./github-sync-adapter");
45
+ const obsidian_cli_client_1 = require("./obsidian-cli-client");
46
+ const obsidian_sync_adapter_1 = require("./obsidian-sync-adapter");
47
+ const roadmap_parser_1 = require("./roadmap-parser");
48
+ const roadmap_renderer_1 = require("./roadmap-renderer");
49
+ const status_renderer_1 = require("./status-renderer");
50
+ const reentry_status_manager_1 = require("./reentry-status-manager");
51
+ const git_context_1 = require("./git-context");
52
52
  const extension = {
53
53
  name: constants_1.REENTRY_EXTENSION_NAME,
54
54
  description: 'Maintains canonical re-entry status and synchronizes to files, GitHub Issues, and Obsidian notes',
@@ -672,4 +672,4 @@ const extension = {
672
672
  }
673
673
  };
674
674
  exports.default = extension;
675
- //# sourceMappingURL=reentry-status-extension.js.map
675
+ //# sourceMappingURL=extension.js.map
@@ -12,4 +12,6 @@ export * from './roadmap-renderer';
12
12
  export * from './git-context';
13
13
  export * from './reentry-status-manager';
14
14
  export * from './status-renderer';
15
+ import extension from './extension';
16
+ export default extension;
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -13,6 +13,9 @@ var __createBinding = (this && this.__createBinding) || (Object.create ? (functi
13
13
  var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
14
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
15
  };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
16
19
  Object.defineProperty(exports, "__esModule", { value: true });
17
20
  __exportStar(require("./constants"), exports);
18
21
  __exportStar(require("./config-manager"), exports);
@@ -28,4 +31,6 @@ __exportStar(require("./roadmap-renderer"), exports);
28
31
  __exportStar(require("./git-context"), exports);
29
32
  __exportStar(require("./reentry-status-manager"), exports);
30
33
  __exportStar(require("./status-renderer"), exports);
34
+ const extension_1 = __importDefault(require("./extension"));
35
+ exports.default = extension_1.default;
31
36
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,4 @@
1
+ import { VersioningExtension } from '../../extensions';
2
+ declare const extension: VersioningExtension;
3
+ export default extension;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -51,4 +51,4 @@ const extension = {
51
51
  }
52
52
  };
53
53
  exports.default = extension;
54
- //# sourceMappingURL=sample-extension.js.map
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,16 @@
1
+ import { VersioningExtension } from '../../extensions';
2
+ export interface SecretCheckResult {
3
+ file: string;
4
+ line: number;
5
+ content: string;
6
+ pattern: string;
7
+ }
8
+ export interface SecretsConfig {
9
+ enabled?: boolean;
10
+ patterns?: string[];
11
+ allowlist?: string[];
12
+ }
13
+ export declare function checkContentForSecrets(content: string, patterns: RegExp[], allowlist: string[], filename: string): SecretCheckResult[];
14
+ declare const extension: VersioningExtension;
15
+ export default extension;
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,264 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.checkContentForSecrets = checkContentForSecrets;
37
+ const child_process_1 = require("child_process");
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ // Definition of patterns to search for
41
+ // Each pattern is a regex
42
+ const DEFAULT_PATTERNS = [
43
+ // PEM / JSON private keys
44
+ new RegExp('-----BEGIN ' + 'PRIVATE KEY-----'),
45
+ new RegExp('-----BEGIN ' + 'RSA PRIVATE KEY-----'),
46
+ /"private_key":\s*"/,
47
+ /"private_key_id":\s*"/,
48
+ // Cloud / API tokens
49
+ /AIza[0-9A-Za-z\-_]{35}/,
50
+ /ghp_[0-9A-Za-z]{36}/,
51
+ /npm_[0-9A-Za-z]{36}/,
52
+ // Ethereum / EVM hex private keys
53
+ /PRIVATE_DEPLOYER=[0-9a-fA-F]{40,}/,
54
+ /DEPLOYER_PRIVATE_KEY=0x[0-9a-fA-F]{40,}/,
55
+ /PRIVATE_KEY=[0-9a-fA-F]{64}/,
56
+ /_KEY=0x[0-9a-fA-F]{64}/,
57
+ /cast wallet address 0x[0-9a-fA-F]{64}/,
58
+ // Seed phrases
59
+ /MNEMONIC=.{20,}/
60
+ ];
61
+ // Allowlist patterns that are safe
62
+ const DEFAULT_ALLOWLIST = [
63
+ "POOL_MANAGER_ADDRESS=",
64
+ "HOOK_OWNER=",
65
+ "NEXT_PUBLIC_",
66
+ "ETHERSCAN_API_KEY=YOUR_",
67
+ "RPC_URL=",
68
+ "YOUR_DEPLOYER_PRIVATE_KEY",
69
+ "YOUR_LOCAL_PRIVATE_KEY",
70
+ "YOUR_TESTNET_PRIVATE_KEY",
71
+ "your_private_key_here"
72
+ ];
73
+ function checkContentForSecrets(content, patterns, allowlist, filename) {
74
+ const results = [];
75
+ const lines = content.split('\n');
76
+ for (let i = 0; i < lines.length; i++) {
77
+ const line = lines[i];
78
+ for (const pattern of patterns) {
79
+ if (pattern.test(line)) {
80
+ // Check allowance
81
+ let isAllowed = false;
82
+ for (const allow of allowlist) {
83
+ if (line.includes(allow)) {
84
+ isAllowed = true;
85
+ break;
86
+ }
87
+ }
88
+ if (!isAllowed) {
89
+ results.push({
90
+ file: filename,
91
+ line: i + 1,
92
+ content: line.trim(),
93
+ pattern: pattern.toString()
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+ return results;
100
+ }
101
+ const extension = {
102
+ name: 'secrets-check',
103
+ description: 'Checks for hardcoded secrets and private keys in staged files',
104
+ version: '1.1.0',
105
+ register: async (program, config) => {
106
+ // Try to get config from extensionConfig first, fallback to top-level secrets for backcompat
107
+ const extensionConfig = config.extensionConfig?.['secrets-check'];
108
+ const secretsConfig = extensionConfig || config.secrets || {};
109
+ if (secretsConfig.enabled === false) {
110
+ return;
111
+ }
112
+ // Merge patterns
113
+ const patterns = [...DEFAULT_PATTERNS];
114
+ if (secretsConfig.patterns) {
115
+ for (const p of secretsConfig.patterns) {
116
+ try {
117
+ patterns.push(new RegExp(p));
118
+ }
119
+ catch (e) {
120
+ console.warn(`⚠️ Invalid regex pattern in config: ${p}`);
121
+ }
122
+ }
123
+ }
124
+ // Merge allowlist
125
+ const allowlist = [...DEFAULT_ALLOWLIST];
126
+ if (secretsConfig.allowlist) {
127
+ allowlist.push(...secretsConfig.allowlist);
128
+ }
129
+ const secretsCmd = program
130
+ .command('check-secrets')
131
+ .description('Scan staged files for potential secrets')
132
+ .action(async () => {
133
+ await runSecretsCheck();
134
+ });
135
+ secretsCmd
136
+ .command('husky')
137
+ .description('Add secrets check to Husky pre-commit hook')
138
+ .option('--remove', 'Remove secrets check from Husky hook', false)
139
+ .action(async (options) => {
140
+ const rootDir = process.cwd();
141
+ const huskyDir = path.join(rootDir, '.husky');
142
+ const hookPath = path.join(huskyDir, 'pre-commit');
143
+ const MARKER_START = '# === secrets-check: start ===';
144
+ const MARKER_END = '# === secrets-check: end ===';
145
+ if (options.remove) {
146
+ if (!fs.existsSync(hookPath)) {
147
+ console.log('ℹ️ Hook pre-commit does not exist.');
148
+ return;
149
+ }
150
+ const content = fs.readFileSync(hookPath, 'utf8');
151
+ const startIdx = content.indexOf(MARKER_START);
152
+ const endIdx = content.indexOf(MARKER_END);
153
+ if (startIdx === -1 || endIdx === -1) {
154
+ console.log('ℹ️ No secrets-check block found in pre-commit.');
155
+ return;
156
+ }
157
+ const before = content.slice(0, startIdx).trimEnd();
158
+ const after = content.slice(endIdx + MARKER_END.length).trimStart();
159
+ const updated = [before, '', after].join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
160
+ fs.writeFileSync(hookPath, updated, 'utf8');
161
+ console.log('✅ Removed secrets check from .husky/pre-commit');
162
+ return;
163
+ }
164
+ // Add cleanup block to hook
165
+ if (!fs.existsSync(huskyDir)) {
166
+ fs.mkdirSync(huskyDir, { recursive: true });
167
+ }
168
+ const block = [
169
+ '',
170
+ MARKER_START,
171
+ 'echo "🔒 Running secrets check…"',
172
+ 'npx versioning check-secrets 2>/dev/null',
173
+ MARKER_END,
174
+ ''
175
+ ].join('\n');
176
+ if (fs.existsSync(hookPath)) {
177
+ const existing = fs.readFileSync(hookPath, 'utf8');
178
+ if (existing.includes(MARKER_START)) {
179
+ console.log('ℹ️ Secrets check is already integrated in .husky/pre-commit.');
180
+ return;
181
+ }
182
+ const updated = existing.trimEnd() + '\n' + block;
183
+ fs.writeFileSync(hookPath, updated, 'utf8');
184
+ }
185
+ else {
186
+ const content = [
187
+ '#!/bin/sh',
188
+ '. "$(dirname "$0")/_/husky.sh"',
189
+ '',
190
+ block
191
+ ].join('\n');
192
+ fs.writeFileSync(hookPath, content, { mode: 0o755 });
193
+ }
194
+ console.log('✅ Secrets check integrated into .husky/pre-commit');
195
+ });
196
+ async function runSecretsCheck() {
197
+ try {
198
+ console.log("🔒 Scanning for secrets...");
199
+ let failed = false;
200
+ // Get list of staged files
201
+ let output = '';
202
+ try {
203
+ output = (0, child_process_1.execSync)('git diff --cached --name-only --diff-filter=ACMR', { encoding: 'utf-8' });
204
+ }
205
+ catch (e) {
206
+ // If not in a git repo or no staged files, just return
207
+ console.log("⚠️ Not a git repository or git error.");
208
+ return;
209
+ }
210
+ const files = output.split('\n').filter(f => f.trim() !== '');
211
+ if (files.length === 0) {
212
+ console.log("✅ No staged files to check.");
213
+ return;
214
+ }
215
+ for (const relativePath of files) {
216
+ const filePath = path.resolve(process.cwd(), relativePath);
217
+ // Skip check if file doesn't exist (deleted)
218
+ if (!fs.existsSync(filePath)) {
219
+ continue;
220
+ }
221
+ // Skip lock files
222
+ if (relativePath.includes('lock.yaml') || relativePath.includes('lock.json')) {
223
+ continue;
224
+ }
225
+ // Read file content
226
+ let content = '';
227
+ try {
228
+ content = fs.readFileSync(filePath, 'utf-8');
229
+ }
230
+ catch (e) {
231
+ console.warn(`⚠️ Could not read file ${relativePath}:`, e);
232
+ continue;
233
+ }
234
+ const findings = checkContentForSecrets(content, patterns, allowlist, relativePath);
235
+ if (findings.length > 0) {
236
+ failed = true;
237
+ for (const finding of findings) {
238
+ console.log(`❌ POTENTIAL SECRET FOUND in ${finding.file}:${finding.line}`);
239
+ console.log(` ${finding.content}`);
240
+ console.log(` (matched pattern: ${finding.pattern})`);
241
+ }
242
+ }
243
+ }
244
+ if (failed) {
245
+ console.log("----------------------------------------------------");
246
+ console.log("⛔ COMMIT REJECTED");
247
+ console.log("Sensitive data was found in the staged files.");
248
+ console.log("Please remove the secrets before committing.");
249
+ console.log("----------------------------------------------------");
250
+ process.exit(1);
251
+ }
252
+ else {
253
+ console.log("✅ No secrets found.");
254
+ }
255
+ }
256
+ catch (error) {
257
+ console.error('❌ Check failed:', error instanceof Error ? error.message : String(error));
258
+ process.exit(1);
259
+ }
260
+ }
261
+ }
262
+ };
263
+ exports.default = extension;
264
+ //# sourceMappingURL=index.js.map
@@ -81,24 +81,36 @@ async function loadExtensions(program) {
81
81
  if (await fs.pathExists(extensionsDir)) {
82
82
  const extensionFiles = await fs.readdir(extensionsDir);
83
83
  for (const file of extensionFiles) {
84
- if (file.endsWith('.js')) {
85
- try {
86
- const extensionPath = path.join(extensionsDir, file);
87
- const extensionModule = require(extensionPath);
88
- const extension = extensionModule.default || extensionModule;
89
- if (extension && typeof extension.register === 'function') {
90
- await extension.register(program, config);
91
- // Register hooks if available
92
- if (extension.hooks) {
93
- context.hooks.push(extension.hooks);
94
- }
95
- console.log(`✅ Loaded extension: ${extension.name}@${extension.version}`);
96
- }
84
+ let extensionPath = path.join(extensionsDir, file);
85
+ const stats = await fs.stat(extensionPath);
86
+ if (stats.isDirectory()) {
87
+ // Check for index.js in directory
88
+ const indexPath = path.join(extensionPath, 'index.js');
89
+ if (await fs.pathExists(indexPath)) {
90
+ extensionPath = indexPath;
97
91
  }
98
- catch (error) {
99
- console.warn(`⚠️ Failed to load extension ${file}:`, error instanceof Error ? error.message : String(error));
92
+ else {
93
+ continue;
100
94
  }
101
95
  }
96
+ else if (!file.endsWith('.js')) {
97
+ continue;
98
+ }
99
+ try {
100
+ const extensionModule = require(extensionPath);
101
+ const extension = extensionModule.default || extensionModule;
102
+ if (extension && typeof extension.register === 'function') {
103
+ await extension.register(program, config);
104
+ // Register hooks if available
105
+ if (extension.hooks) {
106
+ context.hooks.push(extension.hooks);
107
+ }
108
+ console.log(`✅ Loaded extension: ${extension.name}@${extension.version}`);
109
+ }
110
+ }
111
+ catch (error) {
112
+ console.warn(`⚠️ Failed to load extension ${file}:`, error instanceof Error ? error.message : String(error));
113
+ }
102
114
  }
103
115
  }
104
116
  // Note: External extensions from config.extensions are not yet implemented
@@ -4,6 +4,7 @@ export interface VersionConfig {
4
4
  packages: string[];
5
5
  changelogFile?: string;
6
6
  conventionalCommits?: boolean;
7
+ extensionConfig?: Record<string, any>;
7
8
  }
8
9
  export declare class VersionManager {
9
10
  private config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edcalderon/versioning",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "A comprehensive versioning and changelog management tool for monorepos",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=lifecycle-hooks.d.ts.map
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=npm-publish.d.ts.map
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=reentry-status-extension.d.ts.map
@@ -1,4 +0,0 @@
1
- import { VersioningExtension } from '../extensions';
2
- declare const extension: VersioningExtension;
3
- export default extension;
4
- //# sourceMappingURL=sample-extension.d.ts.map