@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.
- package/README.md +83 -17
- package/dist/extensions/cleanup-repo/index.d.ts +49 -0
- package/dist/extensions/cleanup-repo/index.js +649 -0
- package/dist/extensions/lifecycle-hooks/index.d.ts +4 -0
- package/dist/extensions/{lifecycle-hooks.js → lifecycle-hooks/index.js} +1 -1
- package/dist/extensions/npm-publish/index.d.ts +4 -0
- package/dist/extensions/{npm-publish.js → npm-publish/index.js} +1 -1
- package/dist/extensions/reentry-status/config-manager.js +3 -1
- package/dist/extensions/reentry-status/constants.d.ts +1 -1
- package/dist/extensions/reentry-status/constants.js +1 -1
- package/dist/extensions/reentry-status/extension.d.ts +4 -0
- package/dist/extensions/{reentry-status-extension.js → reentry-status/extension.js} +165 -18
- package/dist/extensions/reentry-status/git-context.d.ts +27 -0
- package/dist/extensions/reentry-status/git-context.js +94 -0
- package/dist/extensions/reentry-status/index.d.ts +3 -0
- package/dist/extensions/reentry-status/index.js +6 -0
- package/dist/extensions/reentry-status/roadmap-renderer.d.ts +7 -0
- package/dist/extensions/reentry-status/roadmap-renderer.js +27 -10
- package/dist/extensions/sample-extension/index.d.ts +4 -0
- package/dist/extensions/{sample-extension.js → sample-extension/index.js} +1 -1
- package/dist/extensions/secrets-check/index.d.ts +16 -0
- package/dist/extensions/secrets-check/index.js +264 -0
- package/dist/extensions.js +27 -15
- package/dist/versioning.d.ts +1 -0
- package/package.json +2 -2
- package/dist/extensions/lifecycle-hooks.d.ts +0 -4
- package/dist/extensions/npm-publish.d.ts +0 -4
- package/dist/extensions/reentry-status-extension.d.ts +0 -4
- 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
|