@edcalderon/versioning 1.1.2 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,7 +113,7 @@ Features:
113
113
  - Local registry support
114
114
  - Dry-run mode
115
115
 
116
- #### Re-entry Status + Roadmap Extension
116
+ ### Re-entry Status + Roadmap Extension
117
117
 
118
118
  Maintains a fast **re-entry** layer (current state + next micro-step) and a slow **roadmap/backlog** layer (long-term plan).
119
119
 
@@ -129,39 +129,67 @@ Multi-project (scoped by `--project <name>`):
129
129
  - `.versioning/projects/<project>/REENTRY.md`
130
130
  - `.versioning/projects/<project>/ROADMAP.md`
131
131
 
132
- Notes:
133
- - `--project` must match an existing workspace app/package (it can be a slug like `trader`, a scoped package name like `@ed/trader`, or a path like `apps/trader`).
134
- - The canonical project key is the last path segment (e.g. `@ed/trader` → `trader`).
132
+ **Smart Features:**
133
+ - **Auto-Update:** `versioning reentry update` infers the project phase and next step from your last git commit.
134
+ - `feat: ...` **Phase: development**
135
+ - `fix: ...` → **Phase: maintenance**
136
+ - **Git Context:** Automatically links status to the latest branch, commit, and author.
135
137
 
136
138
  Commands:
137
139
 
138
140
  ```bash
139
141
  # Fast layer
140
142
  versioning reentry init
143
+ versioning reentry update # Auto-update status from git commit
144
+ versioning reentry show # Show current status summary
141
145
  versioning reentry sync
142
146
 
143
147
  # Fast layer (scoped)
144
148
  versioning reentry init --project trader
145
- versioning reentry sync --project trader
149
+ versioning reentry update --project trader
150
+ versioning reentry show --project trader
146
151
 
147
152
  # Slow layer
148
153
  versioning roadmap init --title "My Project"
149
154
  versioning roadmap list
150
155
  versioning roadmap set-milestone --id "now-01" --title "Ship X"
151
- versioning roadmap add --section Now --id "now-02" --item "Add observability"
156
+ ```
157
+
158
+ #### Cleanup Repo Extension
152
159
 
153
- # Slow layer (scoped)
154
- versioning roadmap init --project trader --title "Trader"
155
- versioning roadmap list --project trader
156
- versioning roadmap add --project trader --section Now --item "Wire user-data ORDER_* events"
160
+ Keeps your repository root clean by organizing stray files into appropriate directories (docs, scripts, config, archive).
161
+
162
+ Features:
163
+ - **Smart Scanning:** Identifies files that don't belong in the root.
164
+ - **Configurable Routes:** Map extensions to folders (e.g. `.sh` → `scripts/`).
165
+ - **Safety:** Allowlist for root files and Denylist for forced moves.
166
+ - **Husky Integration:** Auto-scan or auto-move on commit.
157
167
 
158
- # Detect stale/mismatched scoped roadmaps
159
- versioning roadmap validate
168
+ Commands:
169
+
170
+ ```bash
171
+ versioning cleanup scan # Dry-run scan of root
172
+ versioning cleanup move # Move files to configured destinations
173
+ versioning cleanup restore # Restore a moved file
174
+ versioning cleanup config # View/manage configuration
175
+ versioning cleanup husky # Setup git hook
160
176
  ```
161
177
 
162
- Backward compatibility:
163
- - v1.0 status files load safely.
164
- - Schema migrates to v1.1 only when you actually modify status, or explicitly via `versioning reentry sync --migrate`.
178
+ Configuration (`versioning.config.json`):
179
+
180
+ ```json
181
+ {
182
+ "cleanup": {
183
+ "enabled": true,
184
+ "defaultDestination": "docs",
185
+ "allowlist": ["CHANGELOG.md"],
186
+ "routes": {
187
+ ".sh": "scripts",
188
+ ".json": "config"
189
+ }
190
+ }
191
+ }
192
+ ```
165
193
 
166
194
  ### External Extensions
167
195
 
@@ -0,0 +1,49 @@
1
+ import { VersioningExtension } from '../extensions';
2
+ /**
3
+ * The canonical cleanup configuration schema.
4
+ * Everything the plugin needs lives here.
5
+ */
6
+ export interface CleanupRepoConfig {
7
+ /** Master switch. Default: true */
8
+ enabled: boolean;
9
+ /** Default destination for files without a specific route. Default: "docs" */
10
+ defaultDestination: string;
11
+ /**
12
+ * Files explicitly allowed to stay in root (on top of built-in essentials).
13
+ * Supports exact filenames and glob-like patterns (e.g. "*.config.js").
14
+ */
15
+ allowlist: string[];
16
+ /**
17
+ * Files that should ALWAYS be moved, even if they match the allowlist.
18
+ * Useful for forcing cleanup of specific known offenders.
19
+ */
20
+ denylist: string[];
21
+ /**
22
+ * File extensions to consider for cleanup (with leading dot).
23
+ * Default: [".md", ".sh", ".json", ".yaml", ".yml", ".txt", ".log"]
24
+ */
25
+ extensions: string[];
26
+ /**
27
+ * Mapping of file extension → destination directory.
28
+ * Overrides defaultDestination per extension.
29
+ */
30
+ routes: Record<string, string>;
31
+ /**
32
+ * Husky integration settings.
33
+ */
34
+ husky: {
35
+ enabled: boolean;
36
+ /** Which husky hook to attach to. Default: "pre-commit" */
37
+ hook: string;
38
+ /** "scan" = warning only, "enforce" = auto-move + git add. Default: "scan" */
39
+ mode: 'scan' | 'enforce';
40
+ };
41
+ }
42
+ declare const BUILTIN_ALLOWLIST: Set<string>;
43
+ declare const DEFAULT_EXTENSIONS: string[];
44
+ declare const DEFAULT_ROUTES: Record<string, string>;
45
+ declare function loadCleanupConfig(rootConfig: any): CleanupRepoConfig;
46
+ declare const extension: VersioningExtension;
47
+ export { loadCleanupConfig, BUILTIN_ALLOWLIST, DEFAULT_EXTENSIONS, DEFAULT_ROUTES };
48
+ export default extension;
49
+ //# sourceMappingURL=cleanup-repo-extension.d.ts.map
@@ -0,0 +1,641 @@
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?.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 → cleanup`);
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 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];
404
+ let modified = false;
405
+ // ── --allow
406
+ if (options.allow) {
407
+ const file = String(options.allow).trim();
408
+ if (!rawCfg.cleanup.allowlist.includes(file)) {
409
+ rawCfg.cleanup.allowlist.push(file);
410
+ modified = true;
411
+ console.log(`✅ Added "${file}" to cleanup.allowlist`);
412
+ }
413
+ else {
414
+ console.log(`ℹ️ "${file}" is already in the allowlist.`);
415
+ }
416
+ }
417
+ // ── --deny
418
+ if (options.deny) {
419
+ const file = String(options.deny).trim();
420
+ if (!rawCfg.cleanup.denylist.includes(file)) {
421
+ rawCfg.cleanup.denylist.push(file);
422
+ modified = true;
423
+ console.log(`✅ Added "${file}" to cleanup.denylist`);
424
+ }
425
+ else {
426
+ console.log(`ℹ️ "${file}" is already in the denylist.`);
427
+ }
428
+ }
429
+ // ── --unallow
430
+ if (options.unallow) {
431
+ const file = String(options.unallow).trim();
432
+ const idx = rawCfg.cleanup.allowlist.indexOf(file);
433
+ if (idx !== -1) {
434
+ rawCfg.cleanup.allowlist.splice(idx, 1);
435
+ modified = true;
436
+ console.log(`✅ Removed "${file}" from cleanup.allowlist`);
437
+ }
438
+ else {
439
+ console.log(`ℹ️ "${file}" is not in the allowlist.`);
440
+ }
441
+ }
442
+ // ── --undeny
443
+ if (options.undeny) {
444
+ const file = String(options.undeny).trim();
445
+ const idx = rawCfg.cleanup.denylist.indexOf(file);
446
+ if (idx !== -1) {
447
+ rawCfg.cleanup.denylist.splice(idx, 1);
448
+ modified = true;
449
+ console.log(`✅ Removed "${file}" from cleanup.denylist`);
450
+ }
451
+ else {
452
+ console.log(`ℹ️ "${file}" is not in the denylist.`);
453
+ }
454
+ }
455
+ // ── --route
456
+ if (options.route) {
457
+ const mapping = String(options.route).trim();
458
+ const eqIdx = mapping.indexOf('=');
459
+ if (eqIdx === -1) {
460
+ console.error('❌ Route format must be ".ext=destination" (e.g. ".md=docs")');
461
+ process.exit(1);
462
+ }
463
+ let ext = mapping.slice(0, eqIdx).trim();
464
+ const dest = mapping.slice(eqIdx + 1).trim();
465
+ if (!ext.startsWith('.'))
466
+ ext = `.${ext}`;
467
+ rawCfg.cleanup.routes[ext] = dest;
468
+ modified = true;
469
+ console.log(`✅ Added route: ${ext} → ${dest}/`);
470
+ }
471
+ // ── --set-dest
472
+ if (options.setDest) {
473
+ rawCfg.cleanup.defaultDestination = String(options.setDest).trim();
474
+ modified = true;
475
+ console.log(`✅ Default destination set to "${rawCfg.cleanup.defaultDestination}"`);
476
+ }
477
+ // Write config if modified
478
+ if (modified) {
479
+ await fs.writeJson(configPath, rawCfg, { spaces: 2 });
480
+ console.log(`\n📝 Updated ${configPath}`);
481
+ }
482
+ // ── --show or no flags
483
+ if (options.show || (!options.allow && !options.deny && !options.unallow && !options.undeny && !options.route && !options.setDest)) {
484
+ const resolved = loadCleanupConfig(rawCfg);
485
+ console.log('\n┌─────────────────────────────────────────────────┐');
486
+ console.log('│ 🧹 Cleanup-Repo Configuration │');
487
+ console.log('├─────────────────────────────────────────────────┤');
488
+ console.log(`│ Enabled: ${String(resolved.enabled).padEnd(28)}│`);
489
+ console.log(`│ Default dest: ${resolved.defaultDestination.padEnd(28)}│`);
490
+ console.log('│ │');
491
+ console.log('│ 📋 Extensions monitored: │');
492
+ for (const ext of resolved.extensions) {
493
+ const route = resolved.routes[ext] || resolved.defaultDestination;
494
+ console.log(`│ ${ext.padEnd(8)} → ${route.padEnd(34)}│`);
495
+ }
496
+ console.log('│ │');
497
+ console.log('│ ✅ Allowlist (custom): │');
498
+ if (resolved.allowlist.length === 0) {
499
+ console.log('│ (none) │');
500
+ }
501
+ else {
502
+ for (const f of resolved.allowlist) {
503
+ console.log(`│ + ${f.padEnd(42)}│`);
504
+ }
505
+ }
506
+ console.log('│ │');
507
+ console.log('│ 🚫 Denylist (force-move): │');
508
+ if (resolved.denylist.length === 0) {
509
+ console.log('│ (none) │');
510
+ }
511
+ else {
512
+ for (const f of resolved.denylist) {
513
+ console.log(`│ - ${f.padEnd(42)}│`);
514
+ }
515
+ }
516
+ console.log('│ │');
517
+ console.log(`│ 🔗 Husky integration: ${String(resolved.husky.enabled).padEnd(25)}│`);
518
+ if (resolved.husky.enabled) {
519
+ console.log(`│ Hook: ${resolved.husky.hook.padEnd(39)}│`);
520
+ console.log(`│ Mode: ${resolved.husky.mode.padEnd(39)}│`);
521
+ }
522
+ console.log('│ │');
523
+ console.log('│ 📦 Built-in essentials (always kept): │');
524
+ const builtinArr = Array.from(BUILTIN_ALLOWLIST).sort().slice(0, 8);
525
+ for (const f of builtinArr) {
526
+ console.log(`│ ✓ ${f.padEnd(42)}│`);
527
+ }
528
+ console.log(`│ … and ${BUILTIN_ALLOWLIST.size - builtinArr.length} more │`);
529
+ console.log('└─────────────────────────────────────────────────┘\n');
530
+ }
531
+ }));
532
+ // ── versioning cleanup husky ─────────────────────────────────
533
+ cleanupCmd.addCommand(new commander_1.Command('husky')
534
+ .description('Set up Husky hooks to auto-run cleanup on each commit')
535
+ .option('--remove', 'Remove cleanup from Husky hook', false)
536
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
537
+ .action(async (options) => {
538
+ const cfg = await reloadConfig(options.config);
539
+ const rootDir = process.cwd();
540
+ const huskyDir = path.join(rootDir, '.husky');
541
+ const hookName = cfg.husky.hook;
542
+ const hookPath = path.join(huskyDir, hookName);
543
+ const CLEANUP_MARKER_START = '# === cleanup-repo: start ===';
544
+ const CLEANUP_MARKER_END = '# === cleanup-repo: end ===';
545
+ if (options.remove) {
546
+ if (!(await fs.pathExists(hookPath))) {
547
+ console.log(`ℹ️ Hook ${hookName} does not exist.`);
548
+ return;
549
+ }
550
+ const content = await fs.readFile(hookPath, 'utf8');
551
+ const startIdx = content.indexOf(CLEANUP_MARKER_START);
552
+ const endIdx = content.indexOf(CLEANUP_MARKER_END);
553
+ if (startIdx === -1 || endIdx === -1) {
554
+ console.log(`ℹ️ No cleanup block found in ${hookName}.`);
555
+ return;
556
+ }
557
+ const before = content.slice(0, startIdx).trimEnd();
558
+ const after = content.slice(endIdx + CLEANUP_MARKER_END.length).trimStart();
559
+ const updated = [before, '', after].join('\n').replace(/\n{3,}/g, '\n\n').trim() + '\n';
560
+ await fs.writeFile(hookPath, updated, 'utf8');
561
+ // Also update config
562
+ await updateHuskyConfig(options.config, false);
563
+ console.log(`✅ Removed cleanup block from .husky/${hookName}`);
564
+ return;
565
+ }
566
+ // Add cleanup block to hook
567
+ await fs.ensureDir(huskyDir);
568
+ const cleanupBlock = cfg.husky.mode === 'enforce'
569
+ ? [
570
+ '',
571
+ CLEANUP_MARKER_START,
572
+ 'echo "🧹 Running root cleanup (enforce mode)…"',
573
+ 'npx versioning cleanup move --force --git-add 2>/dev/null || true',
574
+ CLEANUP_MARKER_END,
575
+ ''
576
+ ].join('\n')
577
+ : [
578
+ '',
579
+ CLEANUP_MARKER_START,
580
+ 'echo "🔍 Running root cleanup scan…"',
581
+ 'npx versioning cleanup scan 2>/dev/null || true',
582
+ CLEANUP_MARKER_END,
583
+ ''
584
+ ].join('\n');
585
+ if (await fs.pathExists(hookPath)) {
586
+ const existing = await fs.readFile(hookPath, 'utf8');
587
+ if (existing.includes(CLEANUP_MARKER_START)) {
588
+ console.log(`ℹ️ Cleanup is already integrated in .husky/${hookName}.`);
589
+ console.log(' Use --remove to uninstall, then re-add.');
590
+ return;
591
+ }
592
+ const updated = existing.trimEnd() + '\n' + cleanupBlock;
593
+ await fs.writeFile(hookPath, updated, 'utf8');
594
+ }
595
+ else {
596
+ const content = [
597
+ '#!/bin/sh',
598
+ '. "$(dirname "$0")/_/husky.sh"',
599
+ '',
600
+ cleanupBlock
601
+ ].join('\n');
602
+ await fs.writeFile(hookPath, content, { mode: 0o755 });
603
+ }
604
+ // Also update config
605
+ await updateHuskyConfig(options.config, true, hookName, cfg.husky.mode);
606
+ const mode = cfg.husky.mode === 'enforce' ? 'enforce (auto-move + git-add)' : 'scan (warning only)';
607
+ console.log(`✅ Cleanup integrated into .husky/${hookName} [${mode}]`);
608
+ console.log(` Configure mode in versioning.config.json → cleanup.husky.mode`);
609
+ }));
610
+ }
611
+ };
612
+ // ─────────────────────────────────────────────────────────────────
613
+ // HELPER: reload config from disk
614
+ // ─────────────────────────────────────────────────────────────────
615
+ async function reloadConfig(configPath) {
616
+ const fullPath = String(configPath);
617
+ if (await fs.pathExists(fullPath)) {
618
+ const raw = await fs.readJson(fullPath);
619
+ return loadCleanupConfig(raw);
620
+ }
621
+ return loadCleanupConfig({});
622
+ }
623
+ async function updateHuskyConfig(configPath, enabled, hook, mode) {
624
+ const fullPath = String(configPath);
625
+ let rawCfg = {};
626
+ if (await fs.pathExists(fullPath)) {
627
+ rawCfg = await fs.readJson(fullPath);
628
+ }
629
+ if (!rawCfg.cleanup)
630
+ rawCfg.cleanup = {};
631
+ if (!rawCfg.cleanup.husky)
632
+ rawCfg.cleanup.husky = {};
633
+ rawCfg.cleanup.husky.enabled = enabled;
634
+ if (hook)
635
+ rawCfg.cleanup.husky.hook = hook;
636
+ if (mode)
637
+ rawCfg.cleanup.husky.mode = mode;
638
+ await fs.writeJson(fullPath, rawCfg, { spaces: 2 });
639
+ }
640
+ exports.default = extension;
641
+ //# sourceMappingURL=cleanup-repo-extension.js.map
@@ -0,0 +1,27 @@
1
+ export interface GitContextInfo {
2
+ branch: string;
3
+ commit: string;
4
+ commitMessage: string;
5
+ author: string;
6
+ timestamp: string;
7
+ changedFiles: string[];
8
+ diffSummary: {
9
+ insertions: number;
10
+ deletions: number;
11
+ filesChanged: number;
12
+ };
13
+ }
14
+ /**
15
+ * Collects the current git context automatically from the working directory.
16
+ * This is used to auto-fill the reentry status with real git data.
17
+ */
18
+ export declare function collectGitContext(): Promise<GitContextInfo>;
19
+ /**
20
+ * Infer the current project phase based on git data and recent changes.
21
+ */
22
+ export declare function inferPhase(context: GitContextInfo, currentVersion: string): string;
23
+ /**
24
+ * Generate a suggested next step based on the last commit context.
25
+ */
26
+ export declare function suggestNextStep(context: GitContextInfo): string;
27
+ //# sourceMappingURL=git-context.d.ts.map
@@ -0,0 +1,94 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.collectGitContext = collectGitContext;
7
+ exports.inferPhase = inferPhase;
8
+ exports.suggestNextStep = suggestNextStep;
9
+ const simple_git_1 = __importDefault(require("simple-git"));
10
+ /**
11
+ * Collects the current git context automatically from the working directory.
12
+ * This is used to auto-fill the reentry status with real git data.
13
+ */
14
+ async function collectGitContext() {
15
+ const git = (0, simple_git_1.default)();
16
+ try {
17
+ const [log, branch, diff] = await Promise.all([
18
+ git.log({ maxCount: 1 }),
19
+ git.branch(),
20
+ git.diffSummary(['HEAD~1', 'HEAD']).catch(() => null),
21
+ ]);
22
+ const latest = log.latest;
23
+ const statusResult = await git.status();
24
+ return {
25
+ branch: branch.current || '',
26
+ commit: latest?.hash?.substring(0, 7) || '',
27
+ commitMessage: latest?.message || '',
28
+ author: latest?.author_name || '',
29
+ timestamp: latest?.date || new Date().toISOString(),
30
+ changedFiles: statusResult.files.map(f => f.path),
31
+ diffSummary: {
32
+ insertions: diff?.insertions ?? 0,
33
+ deletions: diff?.deletions ?? 0,
34
+ filesChanged: diff?.changed ?? 0,
35
+ },
36
+ };
37
+ }
38
+ catch {
39
+ return {
40
+ branch: '',
41
+ commit: '',
42
+ commitMessage: '',
43
+ author: '',
44
+ timestamp: new Date().toISOString(),
45
+ changedFiles: [],
46
+ diffSummary: { insertions: 0, deletions: 0, filesChanged: 0 },
47
+ };
48
+ }
49
+ }
50
+ /**
51
+ * Infer the current project phase based on git data and recent changes.
52
+ */
53
+ function inferPhase(context, currentVersion) {
54
+ const msg = context.commitMessage.toLowerCase();
55
+ if (msg.startsWith('fix:') || msg.startsWith('hotfix:') || msg.includes('bugfix')) {
56
+ return 'maintenance';
57
+ }
58
+ if (msg.startsWith('test:') || msg.includes('test')) {
59
+ return 'testing';
60
+ }
61
+ if (msg.startsWith('feat:') || msg.startsWith('feature:')) {
62
+ return 'development';
63
+ }
64
+ if (msg.startsWith('chore: release') || msg.includes('deploy') || msg.includes('staging')) {
65
+ return 'staging';
66
+ }
67
+ if (msg.startsWith('docs:') || msg.startsWith('chore:')) {
68
+ return 'maintenance';
69
+ }
70
+ return 'development';
71
+ }
72
+ /**
73
+ * Generate a suggested next step based on the last commit context.
74
+ */
75
+ function suggestNextStep(context) {
76
+ const msg = context.commitMessage.toLowerCase();
77
+ if (msg.startsWith('feat:')) {
78
+ return `Write tests for: ${context.commitMessage.replace(/^feat:\s*/i, '').substring(0, 60)}`;
79
+ }
80
+ if (msg.startsWith('fix:')) {
81
+ return `Verify fix and add regression test for: ${context.commitMessage.replace(/^fix:\s*/i, '').substring(0, 50)}`;
82
+ }
83
+ if (msg.startsWith('test:')) {
84
+ return 'Review test coverage and consider edge cases';
85
+ }
86
+ if (msg.startsWith('chore: release')) {
87
+ return 'Verify deployment and update documentation';
88
+ }
89
+ if (msg.startsWith('docs:')) {
90
+ return 'Continue with next feature or bugfix';
91
+ }
92
+ return `Review changes from: ${context.commitMessage.substring(0, 60)}`;
93
+ }
94
+ //# sourceMappingURL=git-context.js.map
@@ -9,6 +9,7 @@ export * from './obsidian-sync-adapter';
9
9
  export * from './obsidian-cli-client';
10
10
  export * from './roadmap-parser';
11
11
  export * from './roadmap-renderer';
12
+ export * from './git-context';
12
13
  export * from './reentry-status-manager';
13
14
  export * from './status-renderer';
14
15
  //# sourceMappingURL=index.d.ts.map
@@ -25,6 +25,7 @@ __exportStar(require("./obsidian-sync-adapter"), exports);
25
25
  __exportStar(require("./obsidian-cli-client"), exports);
26
26
  __exportStar(require("./roadmap-parser"), exports);
27
27
  __exportStar(require("./roadmap-renderer"), exports);
28
+ __exportStar(require("./git-context"), exports);
28
29
  __exportStar(require("./reentry-status-manager"), exports);
29
30
  __exportStar(require("./status-renderer"), exports);
30
31
  //# sourceMappingURL=index.js.map
@@ -3,9 +3,16 @@ export declare const ROADMAP_MANAGED_START = "<!-- roadmap:managed:start -->";
3
3
  export declare const ROADMAP_MANAGED_END = "<!-- roadmap:managed:end -->";
4
4
  export interface RoadmapRenderOptions {
5
5
  projectTitle?: string;
6
+ projectSlug?: string;
7
+ monorepoName?: string;
6
8
  }
7
9
  export declare class RoadmapRenderer {
8
10
  static defaultRoadmapPath(baseDir?: string): string;
11
+ /**
12
+ * Extract the project name from the roadmap file path for auto-identification.
13
+ * e.g. ".versioning/projects/trader/ROADMAP.md" → "trader"
14
+ */
15
+ static extractProjectFromPath(roadmapFile: string): string | null;
9
16
  static renderManagedBlock(status?: Pick<ReentryStatus, 'milestone' | 'roadmapFile'>): string;
10
17
  static renderTemplate(options?: RoadmapRenderOptions, status?: Pick<ReentryStatus, 'milestone' | 'roadmapFile'>): string;
11
18
  /**
@@ -8,28 +8,45 @@ class RoadmapRenderer {
8
8
  static defaultRoadmapPath(baseDir = constants_1.REENTRY_STATUS_DIRNAME) {
9
9
  return `${baseDir}/${constants_1.ROADMAP_MD_FILENAME}`;
10
10
  }
11
+ /**
12
+ * Extract the project name from the roadmap file path for auto-identification.
13
+ * e.g. ".versioning/projects/trader/ROADMAP.md" → "trader"
14
+ */
15
+ static extractProjectFromPath(roadmapFile) {
16
+ const normalized = roadmapFile.replace(/\\/g, '/');
17
+ const match = /\.versioning\/projects\/([^/]+)\/ROADMAP\.md$/.exec(normalized);
18
+ return match ? match[1] : null;
19
+ }
11
20
  static renderManagedBlock(status) {
12
21
  const milestoneText = status?.milestone
13
22
  ? `${status.milestone.title} (id: ${status.milestone.id})`
14
23
  : '—';
15
24
  const roadmapFile = status?.roadmapFile ?? RoadmapRenderer.defaultRoadmapPath();
16
- // Keep this block stable: no timestamps.
17
- return [
25
+ const projectSlug = RoadmapRenderer.extractProjectFromPath(roadmapFile);
26
+ // Build the managed block with project identification
27
+ const lines = [
18
28
  exports.ROADMAP_MANAGED_START,
19
29
  '> Managed by `@edcalderon/versioning` reentry-status-extension.',
20
30
  `> Canonical roadmap file: ${roadmapFile}`,
21
- `> Active milestone: ${milestoneText}`,
22
- '> ',
23
- '> Everything outside this block is user-editable.',
24
- exports.ROADMAP_MANAGED_END,
25
- ''
26
- ].join('\n');
31
+ ];
32
+ if (projectSlug) {
33
+ lines.push(`> Project: **${projectSlug}**`);
34
+ }
35
+ lines.push(`> Active milestone: ${milestoneText}`, '> ', '> Everything outside this block is user-editable.', exports.ROADMAP_MANAGED_END, '');
36
+ return lines.join('\n');
27
37
  }
28
38
  static renderTemplate(options = {}, status) {
29
39
  const title = options.projectTitle?.trim() ? options.projectTitle.trim() : 'Untitled';
40
+ const roadmapFile = status?.roadmapFile ?? RoadmapRenderer.defaultRoadmapPath();
41
+ const projectSlug = options.projectSlug || RoadmapRenderer.extractProjectFromPath(roadmapFile);
42
+ const monorepo = options.monorepoName || '@ed/monorepo';
43
+ const headerLines = [`# Project Roadmap – ${title}`, ''];
44
+ // Add project metadata section
45
+ if (projectSlug) {
46
+ headerLines.push(`> 📦 **Project:** \`${projectSlug}\` | **Monorepo:** \`${monorepo}\``, '');
47
+ }
30
48
  return [
31
- `# Project Roadmap – ${title}`,
32
- '',
49
+ ...headerLines,
33
50
  RoadmapRenderer.renderManagedBlock(status),
34
51
  '## North Star',
35
52
  '',
@@ -48,14 +48,14 @@ const roadmap_parser_1 = require("./reentry-status/roadmap-parser");
48
48
  const roadmap_renderer_1 = require("./reentry-status/roadmap-renderer");
49
49
  const status_renderer_1 = require("./reentry-status/status-renderer");
50
50
  const reentry_status_manager_1 = require("./reentry-status/reentry-status-manager");
51
+ const git_context_1 = require("./reentry-status/git-context");
51
52
  const extension = {
52
53
  name: constants_1.REENTRY_EXTENSION_NAME,
53
54
  description: 'Maintains canonical re-entry status and synchronizes to files, GitHub Issues, and Obsidian notes',
54
- version: '1.1.2',
55
+ version: '1.2.0',
55
56
  hooks: {
56
57
  postVersion: async (type, version, options) => {
57
58
  try {
58
- // Extensions are loaded before the CLI reads config per-command; use global config snapshot.
59
59
  const configPath = options?.config ?? 'versioning.config.json';
60
60
  if (!(await fs.pathExists(configPath)))
61
61
  return;
@@ -65,15 +65,42 @@ const extension = {
65
65
  return;
66
66
  if (reentryCfg.hooks?.postVersion === false)
67
67
  return;
68
+ // Auto-collect real git context
69
+ const gitCtx = await (0, git_context_1.collectGitContext)();
68
70
  const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager: new file_manager_1.FileManager() });
69
71
  await manager.applyContext(cfg, {
70
72
  trigger: 'postVersion',
71
73
  command: 'versioning bump',
72
74
  options,
73
- gitInfo: { branch: '', commit: '', author: '', timestamp: new Date().toISOString() },
75
+ gitInfo: {
76
+ branch: gitCtx.branch,
77
+ commit: gitCtx.commit,
78
+ author: gitCtx.author,
79
+ timestamp: gitCtx.timestamp,
80
+ },
74
81
  versioningInfo: { versionType: type, oldVersion: undefined, newVersion: version }
75
82
  });
83
+ // Auto-update phase and suggest next step
84
+ const current = await manager.loadOrInit(cfg);
85
+ const phase = (0, git_context_1.inferPhase)(gitCtx, version);
86
+ const nextStep = (0, git_context_1.suggestNextStep)(gitCtx);
87
+ const updated = {
88
+ ...current,
89
+ schemaVersion: '1.1',
90
+ currentPhase: phase,
91
+ nextSteps: [{ id: 'next', description: nextStep, priority: 1 }],
92
+ version: version,
93
+ versioning: {
94
+ ...current.versioning,
95
+ currentVersion: version,
96
+ previousVersion: current.versioning.currentVersion,
97
+ versionType: type,
98
+ },
99
+ lastUpdated: new Date().toISOString(),
100
+ };
101
+ await manager.updateStatus(cfg, () => updated);
76
102
  await manager.syncAll(cfg);
103
+ console.log(`📋 Re-entry auto-updated: phase=${phase}, next="${nextStep}"`);
77
104
  }
78
105
  catch (error) {
79
106
  console.warn('⚠️ reentry-status postVersion hook failed:', error instanceof Error ? error.message : String(error));
@@ -90,12 +117,19 @@ const extension = {
90
117
  return;
91
118
  if (reentryCfg.hooks?.postRelease !== true)
92
119
  return;
120
+ // Auto-collect real git context
121
+ const gitCtx = await (0, git_context_1.collectGitContext)();
93
122
  const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager: new file_manager_1.FileManager() });
94
123
  await manager.applyContext(cfg, {
95
124
  trigger: 'postRelease',
96
125
  command: 'versioning release',
97
126
  options,
98
- gitInfo: { branch: '', commit: '', author: '', timestamp: new Date().toISOString() },
127
+ gitInfo: {
128
+ branch: gitCtx.branch,
129
+ commit: gitCtx.commit,
130
+ author: gitCtx.author,
131
+ timestamp: gitCtx.timestamp,
132
+ },
99
133
  versioningInfo: { newVersion: version }
100
134
  });
101
135
  await manager.syncAll(cfg);
@@ -209,7 +243,6 @@ const extension = {
209
243
  if (existingJson) {
210
244
  const parsed = status_renderer_1.StatusRenderer.parseJson(existingJson);
211
245
  if (migrate && parsed.schemaVersion === '1.0') {
212
- // Explicit migration: rewrite as 1.1 without changing semantics.
213
246
  const migrated = {
214
247
  ...parsed,
215
248
  schemaVersion: '1.1',
@@ -257,6 +290,9 @@ const extension = {
257
290
  await fileManager.writeStatusFiles(cfg, initial);
258
291
  return { cfg, status: initial };
259
292
  };
293
+ // ─────────────────────────────────────────────
294
+ // REENTRY COMMANDS
295
+ // ─────────────────────────────────────────────
260
296
  program
261
297
  .command('reentry')
262
298
  .description('Manage re-entry status (fast layer)')
@@ -291,6 +327,114 @@ const extension = {
291
327
  };
292
328
  await manager.updateStatus(cfg, () => updated);
293
329
  console.log('✅ Re-entry status updated');
330
+ }))
331
+ .addCommand(new commander_1.Command('update')
332
+ .description('Auto-fill re-entry status from last commit and current version (smart reentry)')
333
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
334
+ .option('-p, --project <name>', 'project scope')
335
+ .option('--phase <phase>', 'Override inferred phase')
336
+ .option('--next <text>', 'Override suggested next step')
337
+ .option('--dry-run', 'Show what would be updated without writing', false)
338
+ .action(async (options) => {
339
+ const { cfg, status } = await ensureReentryInitialized(options.config, false, options.project);
340
+ // Auto-collect git context
341
+ const gitCtx = await (0, git_context_1.collectGitContext)();
342
+ // Read current version from package.json
343
+ let currentVersion = status.versioning.currentVersion;
344
+ try {
345
+ const rootPkg = await fs.readJson('package.json');
346
+ currentVersion = rootPkg.version || currentVersion;
347
+ }
348
+ catch { /* keep existing */ }
349
+ // Infer phase or use override
350
+ const phase = options.phase || (0, git_context_1.inferPhase)(gitCtx, currentVersion);
351
+ // Suggest next step or use override
352
+ const nextStep = options.next || (0, git_context_1.suggestNextStep)(gitCtx);
353
+ const updated = {
354
+ ...status,
355
+ schemaVersion: '1.1',
356
+ version: currentVersion,
357
+ currentPhase: phase,
358
+ nextSteps: [{ id: 'next', description: nextStep, priority: 1 }],
359
+ context: {
360
+ trigger: 'auto',
361
+ command: 'versioning reentry update',
362
+ gitInfo: {
363
+ branch: gitCtx.branch,
364
+ commit: gitCtx.commit,
365
+ author: gitCtx.author,
366
+ timestamp: gitCtx.timestamp,
367
+ },
368
+ versioningInfo: {
369
+ newVersion: currentVersion,
370
+ },
371
+ },
372
+ versioning: {
373
+ ...status.versioning,
374
+ currentVersion: currentVersion,
375
+ previousVersion: status.versioning.currentVersion !== currentVersion
376
+ ? status.versioning.currentVersion
377
+ : status.versioning.previousVersion,
378
+ },
379
+ lastUpdated: new Date().toISOString(),
380
+ updatedBy: gitCtx.author || 'auto',
381
+ };
382
+ if (options.dryRun) {
383
+ console.log('\n📋 Re-entry Update Preview (dry-run)\n');
384
+ console.log(` Branch: ${gitCtx.branch}`);
385
+ console.log(` Commit: ${gitCtx.commit}`);
386
+ console.log(` Message: ${gitCtx.commitMessage}`);
387
+ console.log(` Author: ${gitCtx.author}`);
388
+ console.log(` Version: ${currentVersion}`);
389
+ console.log(` Phase: ${phase}`);
390
+ console.log(` Next step: ${nextStep}`);
391
+ console.log(` Files changed: ${gitCtx.diffSummary.filesChanged} (+${gitCtx.diffSummary.insertions}/-${gitCtx.diffSummary.deletions})`);
392
+ console.log('\n Use without --dry-run to apply.\n');
393
+ return;
394
+ }
395
+ await manager.updateStatus(cfg, () => updated);
396
+ console.log('\n📋 Re-entry Status Auto-Updated\n');
397
+ console.log(` ├─ Branch: ${gitCtx.branch}`);
398
+ console.log(` ├─ Commit: ${gitCtx.commit} — ${gitCtx.commitMessage}`);
399
+ console.log(` ├─ Version: ${currentVersion}`);
400
+ console.log(` ├─ Phase: ${phase}`);
401
+ console.log(` ├─ Next step: ${nextStep}`);
402
+ console.log(` └─ Updated by: ${gitCtx.author || 'auto'}\n`);
403
+ console.log(' 🔜 Suggested workflow:');
404
+ console.log(' 1. Review next step above');
405
+ console.log(' 2. Work on the task');
406
+ console.log(' 3. Commit & push');
407
+ console.log(' 4. Run `versioning reentry update` again\n');
408
+ }))
409
+ .addCommand(new commander_1.Command('show')
410
+ .description('Show current re-entry status summary')
411
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
412
+ .option('-p, --project <name>', 'project scope')
413
+ .option('--json', 'Output as JSON', false)
414
+ .action(async (options) => {
415
+ const { status } = await ensureReentryInitialized(options.config, false, options.project);
416
+ if (options.json) {
417
+ console.log(JSON.stringify(status, null, 2));
418
+ return;
419
+ }
420
+ const milestoneText = status.milestone
421
+ ? `${status.milestone.title} (${status.milestone.id})`
422
+ : '—';
423
+ const nextStep = status.nextSteps?.[0]?.description ?? '—';
424
+ const gitCommit = status.context?.gitInfo?.commit || '—';
425
+ const gitBranch = status.context?.gitInfo?.branch || '—';
426
+ console.log('\n┌───────────────────────────────────────────┐');
427
+ console.log('│ 📋 Re-entry Status Summary │');
428
+ console.log('├───────────────────────────────────────────┤');
429
+ console.log(`│ Version: ${status.version.padEnd(28)}│`);
430
+ console.log(`│ Phase: ${status.currentPhase.padEnd(28)}│`);
431
+ console.log(`│ Branch: ${gitBranch.padEnd(28)}│`);
432
+ console.log(`│ Commit: ${gitCommit.padEnd(28)}│`);
433
+ console.log(`│ Milestone: ${milestoneText.padEnd(28).substring(0, 28)}│`);
434
+ console.log(`│ Next step: ${nextStep.padEnd(28).substring(0, 28)}│`);
435
+ console.log(`│ Updated: ${status.lastUpdated.substring(0, 19).padEnd(28)}│`);
436
+ console.log(`│ Roadmap: ${status.roadmapFile.padEnd(28).substring(0, 28)}│`);
437
+ console.log('└───────────────────────────────────────────┘\n');
294
438
  }))
295
439
  .addCommand(new commander_1.Command('sync')
296
440
  .description('Ensure generated status files exist and are up to date (idempotent)')
@@ -384,6 +528,9 @@ const extension = {
384
528
  }
385
529
  console.log('✅ Re-entry sync complete');
386
530
  }));
531
+ // ─────────────────────────────────────────────
532
+ // ROADMAP COMMANDS (expanded with project identification)
533
+ // ─────────────────────────────────────────────
387
534
  program
388
535
  .command('roadmap')
389
536
  .description('Manage roadmap/backlog (slow layer)')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edcalderon/versioning",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "A comprehensive versioning and changelog management tool for monorepos",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -78,4 +78,4 @@
78
78
  "url": "git+https://github.com/edcalderon/my-second-brain.git",
79
79
  "directory": "packages/versioning"
80
80
  }
81
- }
81
+ }