@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 +43 -15
- package/dist/extensions/cleanup-repo-extension.d.ts +49 -0
- package/dist/extensions/cleanup-repo-extension.js +641 -0
- 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 +1 -0
- package/dist/extensions/reentry-status/index.js +1 -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/reentry-status-extension.js +152 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -113,7 +113,7 @@ Features:
|
|
|
113
113
|
- Local registry support
|
|
114
114
|
- Dry-run mode
|
|
115
115
|
|
|
116
|
-
|
|
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
|
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
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
|
|
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
|
-
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
#### Cleanup Repo Extension
|
|
152
159
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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.
|
|
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: {
|
|
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: {
|
|
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.
|
|
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
|
+
}
|