@edcalderon/versioning 1.1.2 → 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.
Files changed (29) hide show
  1. package/README.md +83 -17
  2. package/dist/extensions/cleanup-repo/index.d.ts +49 -0
  3. package/dist/extensions/cleanup-repo/index.js +649 -0
  4. package/dist/extensions/lifecycle-hooks/index.d.ts +4 -0
  5. package/dist/extensions/{lifecycle-hooks.js → lifecycle-hooks/index.js} +1 -1
  6. package/dist/extensions/npm-publish/index.d.ts +4 -0
  7. package/dist/extensions/{npm-publish.js → npm-publish/index.js} +1 -1
  8. package/dist/extensions/reentry-status/config-manager.js +3 -1
  9. package/dist/extensions/reentry-status/constants.d.ts +1 -1
  10. package/dist/extensions/reentry-status/constants.js +1 -1
  11. package/dist/extensions/reentry-status/extension.d.ts +4 -0
  12. package/dist/extensions/{reentry-status-extension.js → reentry-status/extension.js} +165 -18
  13. package/dist/extensions/reentry-status/git-context.d.ts +27 -0
  14. package/dist/extensions/reentry-status/git-context.js +94 -0
  15. package/dist/extensions/reentry-status/index.d.ts +3 -0
  16. package/dist/extensions/reentry-status/index.js +6 -0
  17. package/dist/extensions/reentry-status/roadmap-renderer.d.ts +7 -0
  18. package/dist/extensions/reentry-status/roadmap-renderer.js +27 -10
  19. package/dist/extensions/sample-extension/index.d.ts +4 -0
  20. package/dist/extensions/{sample-extension.js → sample-extension/index.js} +1 -1
  21. package/dist/extensions/secrets-check/index.d.ts +16 -0
  22. package/dist/extensions/secrets-check/index.js +264 -0
  23. package/dist/extensions.js +27 -15
  24. package/dist/versioning.d.ts +1 -0
  25. package/package.json +2 -2
  26. package/dist/extensions/lifecycle-hooks.d.ts +0 -4
  27. package/dist/extensions/npm-publish.d.ts +0 -4
  28. package/dist/extensions/reentry-status-extension.d.ts +0 -4
  29. package/dist/extensions/sample-extension.d.ts +0 -4
@@ -0,0 +1,649 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.DEFAULT_ROUTES = exports.DEFAULT_EXTENSIONS = exports.BUILTIN_ALLOWLIST = void 0;
40
+ exports.loadCleanupConfig = loadCleanupConfig;
41
+ const commander_1 = require("commander");
42
+ const fs = __importStar(require("fs-extra"));
43
+ const path = __importStar(require("path"));
44
+ const simple_git_1 = __importDefault(require("simple-git"));
45
+ const CLEANUP_EXTENSION_NAME = 'cleanup-repo';
46
+ // ─────────────────────────────────────────────────────────────────
47
+ // BUILT-IN ESSENTIALS – these never get flagged
48
+ // ─────────────────────────────────────────────────────────────────
49
+ const BUILTIN_ALLOWLIST = new Set([
50
+ 'package.json',
51
+ 'pnpm-workspace.yaml',
52
+ 'pnpm-lock.yaml',
53
+ 'yarn.lock',
54
+ 'package-lock.json',
55
+ 'bun.lockb',
56
+ 'tsconfig.json',
57
+ 'tsconfig.base.json',
58
+ '.gitignore',
59
+ '.gitattributes',
60
+ '.npmrc',
61
+ '.nvmrc',
62
+ '.node-version',
63
+ '.editorconfig',
64
+ '.eslintrc.js',
65
+ '.eslintrc.json',
66
+ '.eslintrc.cjs',
67
+ '.eslintrc.yaml',
68
+ 'eslint.config.js',
69
+ 'eslint.config.mjs',
70
+ '.prettierrc',
71
+ '.prettierrc.json',
72
+ '.prettierrc.js',
73
+ '.prettierrc.yaml',
74
+ '.prettierignore',
75
+ '.env',
76
+ '.env.example',
77
+ '.env.local',
78
+ '.envrc',
79
+ 'LICENSE',
80
+ 'README.md',
81
+ 'Makefile',
82
+ 'Dockerfile',
83
+ 'docker-compose.yml',
84
+ 'docker-compose.yaml',
85
+ 'firebase.json',
86
+ 'firestore.rules',
87
+ 'versioning.config.json',
88
+ 'turbo.json',
89
+ 'nx.json',
90
+ 'lerna.json',
91
+ 'biome.json',
92
+ 'renovate.json',
93
+ '.releaserc.json',
94
+ 'jest.config.js',
95
+ 'jest.config.ts',
96
+ 'vitest.config.ts',
97
+ 'vitest.config.js',
98
+ 'tailwind.config.js',
99
+ 'tailwind.config.ts',
100
+ 'postcss.config.js',
101
+ 'postcss.config.mjs',
102
+ 'next.config.js',
103
+ 'next.config.mjs',
104
+ 'vite.config.ts',
105
+ 'vite.config.js',
106
+ ]);
107
+ exports.BUILTIN_ALLOWLIST = BUILTIN_ALLOWLIST;
108
+ // ─────────────────────────────────────────────────────────────────
109
+ // DEFAULTS
110
+ // ─────────────────────────────────────────────────────────────────
111
+ const DEFAULT_EXTENSIONS = ['.md', '.sh', '.json', '.yaml', '.yml', '.txt', '.log'];
112
+ exports.DEFAULT_EXTENSIONS = DEFAULT_EXTENSIONS;
113
+ const DEFAULT_ROUTES = {
114
+ '.md': 'docs',
115
+ '.sh': 'scripts',
116
+ '.json': 'config',
117
+ '.yaml': 'config',
118
+ '.yml': 'config',
119
+ '.txt': 'archive',
120
+ '.log': 'archive',
121
+ };
122
+ exports.DEFAULT_ROUTES = DEFAULT_ROUTES;
123
+ // ─────────────────────────────────────────────────────────────────
124
+ // CONFIG LOADER
125
+ // ─────────────────────────────────────────────────────────────────
126
+ function loadCleanupConfig(rootConfig) {
127
+ const raw = rootConfig?.extensionConfig?.['cleanup-repo'] ?? rootConfig?.cleanup ?? {};
128
+ return {
129
+ enabled: raw.enabled !== false,
130
+ defaultDestination: typeof raw.defaultDestination === 'string' ? raw.defaultDestination : 'docs',
131
+ allowlist: Array.isArray(raw.allowlist) ? raw.allowlist : [],
132
+ denylist: Array.isArray(raw.denylist) ? raw.denylist : [],
133
+ extensions: Array.isArray(raw.extensions) ? raw.extensions.map((e) => e.startsWith('.') ? e : `.${e}`) : DEFAULT_EXTENSIONS,
134
+ routes: typeof raw.routes === 'object' && raw.routes !== null ? raw.routes : DEFAULT_ROUTES,
135
+ husky: {
136
+ enabled: raw.husky?.enabled === true,
137
+ hook: typeof raw.husky?.hook === 'string' ? raw.husky.hook : 'pre-commit',
138
+ mode: raw.husky?.mode === 'enforce' ? 'enforce' : 'scan',
139
+ },
140
+ };
141
+ }
142
+ // ─────────────────────────────────────────────────────────────────
143
+ // MATCHING HELPERS
144
+ // ─────────────────────────────────────────────────────────────────
145
+ /** Simple glob matching: supports exact names and "*.ext" patterns. */
146
+ function matchesPattern(filename, pattern) {
147
+ if (pattern === filename)
148
+ return true;
149
+ if (pattern.startsWith('*.')) {
150
+ const ext = pattern.slice(1); // e.g. ".config.js"
151
+ return filename.endsWith(ext);
152
+ }
153
+ if (pattern.endsWith('*')) {
154
+ const prefix = pattern.slice(0, -1);
155
+ return filename.startsWith(prefix);
156
+ }
157
+ return false;
158
+ }
159
+ function isAllowed(filename, config) {
160
+ // Built-in essentials always allowed
161
+ if (BUILTIN_ALLOWLIST.has(filename))
162
+ return true;
163
+ // Check user allowlist from config
164
+ for (const pattern of config.allowlist) {
165
+ if (matchesPattern(filename, pattern))
166
+ return true;
167
+ }
168
+ return false;
169
+ }
170
+ function isDenied(filename, config) {
171
+ for (const pattern of config.denylist) {
172
+ if (matchesPattern(filename, pattern))
173
+ return true;
174
+ }
175
+ return false;
176
+ }
177
+ function getDestination(filename, ext, config) {
178
+ // Check extension routes from config
179
+ if (config.routes[ext])
180
+ return config.routes[ext];
181
+ return config.defaultDestination;
182
+ }
183
+ function getReason(filename, ext, dest) {
184
+ const typeMap = {
185
+ '.md': 'Markdown file',
186
+ '.sh': 'Shell script',
187
+ '.json': 'JSON config file',
188
+ '.yaml': 'YAML file',
189
+ '.yml': 'YAML file',
190
+ '.txt': 'Text file',
191
+ '.log': 'Log file',
192
+ };
193
+ const type = typeMap[ext] || 'File';
194
+ return `${type} "${filename}" should live in ${dest}/`;
195
+ }
196
+ async function scanRootForCleanup(rootDir, config) {
197
+ const candidates = [];
198
+ const extensionSet = new Set(config.extensions);
199
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
200
+ for (const entry of entries) {
201
+ if (entry.isDirectory())
202
+ continue;
203
+ const name = entry.name;
204
+ // Skip hidden dotfiles unless they match a cleanup extension
205
+ const ext = path.extname(name).toLowerCase();
206
+ if (name.startsWith('.') && !extensionSet.has(ext))
207
+ continue;
208
+ // Denylist check — always flag these, even if allowlisted
209
+ const denied = isDenied(name, config);
210
+ if (!denied) {
211
+ // Skip allowlisted files
212
+ if (isAllowed(name, config))
213
+ continue;
214
+ // Check if this extension is a cleanup candidate
215
+ if (!extensionSet.has(ext))
216
+ continue;
217
+ }
218
+ const filePath = path.join(rootDir, name);
219
+ const stat = await fs.stat(filePath);
220
+ const dest = getDestination(name, ext, config);
221
+ const reason = denied
222
+ ? `"${name}" is in the denylist → forced move to ${dest}/`
223
+ : getReason(name, ext, dest);
224
+ candidates.push({
225
+ file: name,
226
+ ext,
227
+ sizeBytes: stat.size,
228
+ reason,
229
+ destination: dest,
230
+ isDenied: denied,
231
+ });
232
+ }
233
+ return candidates;
234
+ }
235
+ // ─────────────────────────────────────────────────────────────────
236
+ // EXTENSION
237
+ // ─────────────────────────────────────────────────────────────────
238
+ const extension = {
239
+ name: CLEANUP_EXTENSION_NAME,
240
+ description: 'Config-driven repo root cleanup — keeps stray files out of root, configurable via versioning.config.json',
241
+ version: '1.1.0',
242
+ register: async (program, rootConfig) => {
243
+ const config = loadCleanupConfig(rootConfig);
244
+ const cleanupCmd = program
245
+ .command('cleanup')
246
+ .description('Repository root cleanup utilities (config: versioning.config.json → cleanup)');
247
+ // ── versioning cleanup scan ──────────────────────────────────
248
+ cleanupCmd.addCommand(new commander_1.Command('scan')
249
+ .description('Scan the repository root for files that should be moved')
250
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
251
+ .action(async (options) => {
252
+ // Reload config in case user passed a different config file
253
+ const cfg = await reloadConfig(options.config);
254
+ const rootDir = process.cwd();
255
+ if (!cfg.enabled) {
256
+ console.log('ℹ️ Cleanup is disabled in versioning.config.json (cleanup.enabled = false)');
257
+ return;
258
+ }
259
+ const candidates = await scanRootForCleanup(rootDir, cfg);
260
+ if (candidates.length === 0) {
261
+ console.log('\n✅ Repository root is clean! No files need to be moved.\n');
262
+ return;
263
+ }
264
+ console.log('\n🔍 Repository Root Cleanup Scan\n');
265
+ console.log(` Config: versioning.config.json → extensionConfig['cleanup-repo']`);
266
+ console.log(` Default destination: ${cfg.defaultDestination}/`);
267
+ console.log(` Extensions monitored: ${cfg.extensions.join(', ')}`);
268
+ console.log(` Allowlist (custom): ${cfg.allowlist.length > 0 ? cfg.allowlist.join(', ') : '—'}`);
269
+ console.log(` Denylist: ${cfg.denylist.length > 0 ? cfg.denylist.join(', ') : '—'}`);
270
+ console.log(`\nFound ${candidates.length} file(s) that should be moved:\n`);
271
+ for (const c of candidates) {
272
+ const size = c.sizeBytes > 1024 ? `${(c.sizeBytes / 1024).toFixed(1)}KB` : `${c.sizeBytes}B`;
273
+ const badge = c.isDenied ? ' 🚫 DENIED' : '';
274
+ console.log(` 📄 ${c.file} (${size})${badge}`);
275
+ console.log(` → ${c.destination}/${c.file}`);
276
+ console.log(` ℹ️ ${c.reason}\n`);
277
+ }
278
+ console.log(' Run `versioning cleanup move` to move these files.');
279
+ console.log(' Edit versioning.config.json → cleanup.allowlist to keep specific files in root.\n');
280
+ }));
281
+ // ── versioning cleanup move ──────────────────────────────────
282
+ cleanupCmd.addCommand(new commander_1.Command('move')
283
+ .description('Move stray files from root to their configured destinations')
284
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
285
+ .option('-d, --dest <path>', 'Override default destination directory')
286
+ .option('--force', 'Overwrite files at destination', false)
287
+ .option('--dry-run', 'Show what would happen without moving', false)
288
+ .option('--git-add', 'Stage moves with git after moving', false)
289
+ .action(async (options) => {
290
+ const cfg = await reloadConfig(options.config);
291
+ const rootDir = process.cwd();
292
+ if (!cfg.enabled) {
293
+ console.log('ℹ️ Cleanup is disabled in config.');
294
+ return;
295
+ }
296
+ // Override default destination if passed via CLI
297
+ if (options.dest) {
298
+ cfg.defaultDestination = options.dest;
299
+ }
300
+ const candidates = await scanRootForCleanup(rootDir, cfg);
301
+ if (candidates.length === 0) {
302
+ console.log('\n✅ Repository root is already clean!\n');
303
+ return;
304
+ }
305
+ console.log(`\n🧹 Moving ${candidates.length} file(s) from root…\n`);
306
+ const moved = [];
307
+ for (const c of candidates) {
308
+ const destDir = path.join(rootDir, c.destination);
309
+ const srcPath = path.join(rootDir, c.file);
310
+ const destPath = path.join(destDir, c.file);
311
+ if (options.dryRun) {
312
+ console.log(` [dry-run] ${c.file} → ${c.destination}/${c.file}`);
313
+ continue;
314
+ }
315
+ try {
316
+ await fs.ensureDir(destDir);
317
+ if (await fs.pathExists(destPath)) {
318
+ if (!options.force) {
319
+ console.log(` ⚠️ Skipped ${c.file} (already exists at ${c.destination}/${c.file})`);
320
+ continue;
321
+ }
322
+ }
323
+ await fs.move(srcPath, destPath, { overwrite: options.force });
324
+ moved.push(c.file);
325
+ console.log(` ✅ ${c.file} → ${c.destination}/${c.file}`);
326
+ }
327
+ catch (error) {
328
+ console.error(` ❌ Failed to move ${c.file}: ${error instanceof Error ? error.message : String(error)}`);
329
+ }
330
+ }
331
+ if (options.gitAdd && moved.length > 0 && !options.dryRun) {
332
+ try {
333
+ const git = (0, simple_git_1.default)();
334
+ await git.add('.');
335
+ console.log(`\n📦 Staged ${moved.length} moved file(s) with git.`);
336
+ }
337
+ catch (error) {
338
+ console.warn(`\n⚠️ Could not stage changes: ${error instanceof Error ? error.message : String(error)}`);
339
+ }
340
+ }
341
+ if (!options.dryRun) {
342
+ console.log(`\n✅ Moved ${moved.length}/${candidates.length} file(s). Root is cleaner!\n`);
343
+ }
344
+ else {
345
+ console.log(`\n📋 Dry run complete. ${candidates.length} file(s) would be moved.\n`);
346
+ }
347
+ }));
348
+ // ── versioning cleanup restore ───────────────────────────────
349
+ cleanupCmd.addCommand(new commander_1.Command('restore')
350
+ .description('Restore a previously moved file back to root')
351
+ .requiredOption('--file <name>', 'File name to restore')
352
+ .option('--from <path>', 'Source directory to restore from (auto-detects from routes if omitted)')
353
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
354
+ .action(async (options) => {
355
+ const cfg = await reloadConfig(options.config);
356
+ const rootDir = process.cwd();
357
+ // Auto-detect source from configured routes if --from not specified
358
+ let fromDir = options.from;
359
+ if (!fromDir) {
360
+ const ext = path.extname(options.file).toLowerCase();
361
+ fromDir = cfg.routes[ext] || cfg.defaultDestination;
362
+ }
363
+ const srcPath = path.join(rootDir, fromDir, options.file);
364
+ const destPath = path.join(rootDir, options.file);
365
+ if (!(await fs.pathExists(srcPath))) {
366
+ console.error(`❌ File not found: ${srcPath}`);
367
+ process.exit(1);
368
+ }
369
+ if (await fs.pathExists(destPath)) {
370
+ console.error(`❌ File already exists in root: ${options.file}`);
371
+ process.exit(1);
372
+ }
373
+ await fs.move(srcPath, destPath);
374
+ console.log(`✅ Restored ${fromDir}/${options.file} → ${options.file}`);
375
+ }));
376
+ // ── versioning cleanup config ────────────────────────────────
377
+ cleanupCmd.addCommand(new commander_1.Command('config')
378
+ .description('View or manage the cleanup configuration in versioning.config.json')
379
+ .option('--show', 'Show the full resolved cleanup config', false)
380
+ .option('--allow <file>', 'Add a file to the allowlist')
381
+ .option('--deny <file>', 'Add a file to the denylist')
382
+ .option('--unallow <file>', 'Remove a file from the allowlist')
383
+ .option('--undeny <file>', 'Remove a file from the denylist')
384
+ .option('--route <mapping>', 'Add extension route (format: ".ext=destination")')
385
+ .option('--set-dest <path>', 'Set the default destination directory')
386
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
387
+ .action(async (options) => {
388
+ const configPath = String(options.config);
389
+ let rawCfg = {};
390
+ if (await fs.pathExists(configPath)) {
391
+ rawCfg = await fs.readJson(configPath);
392
+ }
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];
408
+ let modified = false;
409
+ // ── --allow
410
+ if (options.allow) {
411
+ const file = String(options.allow).trim();
412
+ if (!extensionCfg.allowlist.includes(file)) {
413
+ extensionCfg.allowlist.push(file);
414
+ modified = true;
415
+ console.log(`✅ Added "${file}" to cleanup.allowlist`);
416
+ }
417
+ else {
418
+ console.log(`ℹ️ "${file}" is already in the allowlist.`);
419
+ }
420
+ }
421
+ // ── --deny
422
+ if (options.deny) {
423
+ const file = String(options.deny).trim();
424
+ if (!extensionCfg.denylist.includes(file)) {
425
+ extensionCfg.denylist.push(file);
426
+ modified = true;
427
+ console.log(`✅ Added "${file}" to cleanup.denylist`);
428
+ }
429
+ else {
430
+ console.log(`ℹ️ "${file}" is already in the denylist.`);
431
+ }
432
+ }
433
+ // ── --unallow
434
+ if (options.unallow) {
435
+ const file = String(options.unallow).trim();
436
+ const idx = extensionCfg.allowlist.indexOf(file);
437
+ if (idx !== -1) {
438
+ extensionCfg.allowlist.splice(idx, 1);
439
+ modified = true;
440
+ console.log(`✅ Removed "${file}" from cleanup.allowlist`);
441
+ }
442
+ else {
443
+ console.log(`ℹ️ "${file}" is not in the allowlist.`);
444
+ }
445
+ }
446
+ // ── --undeny
447
+ if (options.undeny) {
448
+ const file = String(options.undeny).trim();
449
+ const idx = extensionCfg.denylist.indexOf(file);
450
+ if (idx !== -1) {
451
+ extensionCfg.denylist.splice(idx, 1);
452
+ modified = true;
453
+ console.log(`✅ Removed "${file}" from cleanup.denylist`);
454
+ }
455
+ else {
456
+ console.log(`ℹ️ "${file}" is not in the denylist.`);
457
+ }
458
+ }
459
+ // ── --route
460
+ if (options.route) {
461
+ const mapping = String(options.route).trim();
462
+ const eqIdx = mapping.indexOf('=');
463
+ if (eqIdx === -1) {
464
+ console.error('❌ Route format must be ".ext=destination" (e.g. ".md=docs")');
465
+ process.exit(1);
466
+ }
467
+ let ext = mapping.slice(0, eqIdx).trim();
468
+ const dest = mapping.slice(eqIdx + 1).trim();
469
+ if (!ext.startsWith('.'))
470
+ ext = `.${ext}`;
471
+ extensionCfg.routes[ext] = dest;
472
+ modified = true;
473
+ console.log(`✅ Added route: ${ext} → ${dest}/`);
474
+ }
475
+ // ── --set-dest
476
+ if (options.setDest) {
477
+ extensionCfg.defaultDestination = String(options.setDest).trim();
478
+ modified = true;
479
+ console.log(`✅ Default destination set to "${extensionCfg.defaultDestination}"`);
480
+ }
481
+ // Write config if modified
482
+ if (modified) {
483
+ await fs.writeJson(configPath, rawCfg, { spaces: 2 });
484
+ console.log(`\n📝 Updated ${configPath}`);
485
+ }
486
+ // ── --show or no flags
487
+ if (options.show || (!options.allow && !options.deny && !options.unallow && !options.undeny && !options.route && !options.setDest)) {
488
+ const resolved = loadCleanupConfig(rawCfg);
489
+ console.log('\n┌─────────────────────────────────────────────────┐');
490
+ console.log('│ 🧹 Cleanup-Repo Configuration │');
491
+ console.log('├─────────────────────────────────────────────────┤');
492
+ console.log(`│ Enabled: ${String(resolved.enabled).padEnd(28)}│`);
493
+ console.log(`│ Default dest: ${resolved.defaultDestination.padEnd(28)}│`);
494
+ console.log('│ │');
495
+ console.log('│ 📋 Extensions monitored: │');
496
+ for (const ext of resolved.extensions) {
497
+ const route = resolved.routes[ext] || resolved.defaultDestination;
498
+ console.log(`│ ${ext.padEnd(8)} → ${route.padEnd(34)}│`);
499
+ }
500
+ console.log('│ │');
501
+ console.log('│ ✅ Allowlist (custom): │');
502
+ if (resolved.allowlist.length === 0) {
503
+ console.log('│ (none) │');
504
+ }
505
+ else {
506
+ for (const f of resolved.allowlist) {
507
+ console.log(`│ + ${f.padEnd(42)}│`);
508
+ }
509
+ }
510
+ console.log('│ │');
511
+ console.log('│ 🚫 Denylist (force-move): │');
512
+ if (resolved.denylist.length === 0) {
513
+ console.log('│ (none) │');
514
+ }
515
+ else {
516
+ for (const f of resolved.denylist) {
517
+ console.log(`│ - ${f.padEnd(42)}│`);
518
+ }
519
+ }
520
+ console.log('│ │');
521
+ console.log(`│ 🔗 Husky integration: ${String(resolved.husky.enabled).padEnd(25)}│`);
522
+ if (resolved.husky.enabled) {
523
+ console.log(`│ Hook: ${resolved.husky.hook.padEnd(39)}│`);
524
+ console.log(`│ Mode: ${resolved.husky.mode.padEnd(39)}│`);
525
+ }
526
+ console.log('│ │');
527
+ console.log('│ 📦 Built-in essentials (always kept): │');
528
+ const builtinArr = Array.from(BUILTIN_ALLOWLIST).sort().slice(0, 8);
529
+ for (const f of builtinArr) {
530
+ console.log(`│ ✓ ${f.padEnd(42)}│`);
531
+ }
532
+ console.log(`│ … and ${BUILTIN_ALLOWLIST.size - builtinArr.length} more │`);
533
+ console.log('└─────────────────────────────────────────────────┘\n');
534
+ }
535
+ }));
536
+ // ── versioning cleanup husky ─────────────────────────────────
537
+ cleanupCmd.addCommand(new commander_1.Command('husky')
538
+ .description('Set up Husky hooks to auto-run cleanup on each commit')
539
+ .option('--remove', 'Remove cleanup from Husky hook', false)
540
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
541
+ .action(async (options) => {
542
+ const cfg = await reloadConfig(options.config);
543
+ const rootDir = process.cwd();
544
+ const huskyDir = path.join(rootDir, '.husky');
545
+ const hookName = cfg.husky.hook;
546
+ const hookPath = path.join(huskyDir, hookName);
547
+ const CLEANUP_MARKER_START = '# === cleanup-repo: start ===';
548
+ const CLEANUP_MARKER_END = '# === cleanup-repo: end ===';
549
+ if (options.remove) {
550
+ if (!(await fs.pathExists(hookPath))) {
551
+ console.log(`ℹ️ Hook ${hookName} does not exist.`);
552
+ return;
553
+ }
554
+ const content = await fs.readFile(hookPath, 'utf8');
555
+ const startIdx = content.indexOf(CLEANUP_MARKER_START);
556
+ const endIdx = content.indexOf(CLEANUP_MARKER_END);
557
+ if (startIdx === -1 || endIdx === -1) {
558
+ console.log(`ℹ️ No cleanup block found in ${hookName}.`);
559
+ return;
560
+ }
561
+ const before = content.slice(0, startIdx).trimEnd();
562
+ const after = content.slice(endIdx + CLEANUP_MARKER_END.length).trimStart();
563
+ const updated = [before, '', after].join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
564
+ await fs.writeFile(hookPath, updated, 'utf8');
565
+ // Also update config
566
+ await updateHuskyConfig(options.config, false);
567
+ console.log(`✅ Removed cleanup block from .husky/${hookName}`);
568
+ return;
569
+ }
570
+ // Add cleanup block to hook
571
+ await fs.ensureDir(huskyDir);
572
+ const cleanupBlock = cfg.husky.mode === 'enforce'
573
+ ? [
574
+ '',
575
+ CLEANUP_MARKER_START,
576
+ 'echo "🧹 Running root cleanup (enforce mode)…"',
577
+ 'npx versioning cleanup move --force --git-add 2>/dev/null || true',
578
+ CLEANUP_MARKER_END,
579
+ ''
580
+ ].join('\n')
581
+ : [
582
+ '',
583
+ CLEANUP_MARKER_START,
584
+ 'echo "🔍 Running root cleanup scan…"',
585
+ 'npx versioning cleanup scan 2>/dev/null || true',
586
+ CLEANUP_MARKER_END,
587
+ ''
588
+ ].join('\n');
589
+ if (await fs.pathExists(hookPath)) {
590
+ const existing = await fs.readFile(hookPath, 'utf8');
591
+ if (existing.includes(CLEANUP_MARKER_START)) {
592
+ console.log(`ℹ️ Cleanup is already integrated in .husky/${hookName}.`);
593
+ console.log(' Use --remove to uninstall, then re-add.');
594
+ return;
595
+ }
596
+ const updated = existing.trimEnd() + '\n' + cleanupBlock;
597
+ await fs.writeFile(hookPath, updated, 'utf8');
598
+ }
599
+ else {
600
+ const content = [
601
+ '#!/bin/sh',
602
+ '. "$(dirname "$0")/_/husky.sh"',
603
+ '',
604
+ cleanupBlock
605
+ ].join('\n');
606
+ await fs.writeFile(hookPath, content, { mode: 0o755 });
607
+ }
608
+ // Also update config
609
+ await updateHuskyConfig(options.config, true, hookName, cfg.husky.mode);
610
+ const mode = cfg.husky.mode === 'enforce' ? 'enforce (auto-move + git-add)' : 'scan (warning only)';
611
+ console.log(`✅ Cleanup integrated into .husky/${hookName} [${mode}]`);
612
+ console.log(` Configure mode in versioning.config.json → cleanup.husky.mode`);
613
+ }));
614
+ }
615
+ };
616
+ // ─────────────────────────────────────────────────────────────────
617
+ // HELPER: reload config from disk
618
+ // ─────────────────────────────────────────────────────────────────
619
+ async function reloadConfig(configPath) {
620
+ const fullPath = String(configPath);
621
+ if (await fs.pathExists(fullPath)) {
622
+ const raw = await fs.readJson(fullPath);
623
+ return loadCleanupConfig(raw);
624
+ }
625
+ return loadCleanupConfig({});
626
+ }
627
+ async function updateHuskyConfig(configPath, enabled, hook, mode) {
628
+ const fullPath = String(configPath);
629
+ let rawCfg = {};
630
+ if (await fs.pathExists(fullPath)) {
631
+ rawCfg = await fs.readJson(fullPath);
632
+ }
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;
642
+ if (hook)
643
+ extensionCfg.husky.hook = hook;
644
+ if (mode)
645
+ extensionCfg.husky.mode = mode;
646
+ await fs.writeJson(fullPath, rawCfg, { spaces: 2 });
647
+ }
648
+ exports.default = extension;
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