@edcalderon/versioning 1.1.0 ā 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 +56 -5
- package/dist/extensions/cleanup-repo-extension.d.ts +49 -0
- package/dist/extensions/cleanup-repo-extension.js +641 -0
- package/dist/extensions/reentry-status/config-manager.d.ts +7 -2
- package/dist/extensions/reentry-status/config-manager.js +73 -6
- 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 +8 -1
- package/dist/extensions/reentry-status/roadmap-renderer.js +29 -12
- package/dist/extensions/reentry-status-extension.js +302 -27
- package/package.json +2 -2
|
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
const commander_1 = require("commander");
|
|
37
37
|
const fs = __importStar(require("fs-extra"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
38
39
|
const config_manager_1 = require("./reentry-status/config-manager");
|
|
39
40
|
const constants_1 = require("./reentry-status/constants");
|
|
40
41
|
const file_manager_1 = require("./reentry-status/file-manager");
|
|
@@ -47,14 +48,14 @@ const roadmap_parser_1 = require("./reentry-status/roadmap-parser");
|
|
|
47
48
|
const roadmap_renderer_1 = require("./reentry-status/roadmap-renderer");
|
|
48
49
|
const status_renderer_1 = require("./reentry-status/status-renderer");
|
|
49
50
|
const reentry_status_manager_1 = require("./reentry-status/reentry-status-manager");
|
|
51
|
+
const git_context_1 = require("./reentry-status/git-context");
|
|
50
52
|
const extension = {
|
|
51
53
|
name: constants_1.REENTRY_EXTENSION_NAME,
|
|
52
54
|
description: 'Maintains canonical re-entry status and synchronizes to files, GitHub Issues, and Obsidian notes',
|
|
53
|
-
version: '1.
|
|
55
|
+
version: '1.2.0',
|
|
54
56
|
hooks: {
|
|
55
57
|
postVersion: async (type, version, options) => {
|
|
56
58
|
try {
|
|
57
|
-
// Extensions are loaded before the CLI reads config per-command; use global config snapshot.
|
|
58
59
|
const configPath = options?.config ?? 'versioning.config.json';
|
|
59
60
|
if (!(await fs.pathExists(configPath)))
|
|
60
61
|
return;
|
|
@@ -64,15 +65,42 @@ const extension = {
|
|
|
64
65
|
return;
|
|
65
66
|
if (reentryCfg.hooks?.postVersion === false)
|
|
66
67
|
return;
|
|
68
|
+
// Auto-collect real git context
|
|
69
|
+
const gitCtx = await (0, git_context_1.collectGitContext)();
|
|
67
70
|
const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager: new file_manager_1.FileManager() });
|
|
68
71
|
await manager.applyContext(cfg, {
|
|
69
72
|
trigger: 'postVersion',
|
|
70
73
|
command: 'versioning bump',
|
|
71
74
|
options,
|
|
72
|
-
gitInfo: {
|
|
75
|
+
gitInfo: {
|
|
76
|
+
branch: gitCtx.branch,
|
|
77
|
+
commit: gitCtx.commit,
|
|
78
|
+
author: gitCtx.author,
|
|
79
|
+
timestamp: gitCtx.timestamp,
|
|
80
|
+
},
|
|
73
81
|
versioningInfo: { versionType: type, oldVersion: undefined, newVersion: version }
|
|
74
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);
|
|
75
102
|
await manager.syncAll(cfg);
|
|
103
|
+
console.log(`š Re-entry auto-updated: phase=${phase}, next="${nextStep}"`);
|
|
76
104
|
}
|
|
77
105
|
catch (error) {
|
|
78
106
|
console.warn('ā ļø reentry-status postVersion hook failed:', error instanceof Error ? error.message : String(error));
|
|
@@ -89,12 +117,19 @@ const extension = {
|
|
|
89
117
|
return;
|
|
90
118
|
if (reentryCfg.hooks?.postRelease !== true)
|
|
91
119
|
return;
|
|
120
|
+
// Auto-collect real git context
|
|
121
|
+
const gitCtx = await (0, git_context_1.collectGitContext)();
|
|
92
122
|
const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager: new file_manager_1.FileManager() });
|
|
93
123
|
await manager.applyContext(cfg, {
|
|
94
124
|
trigger: 'postRelease',
|
|
95
125
|
command: 'versioning release',
|
|
96
126
|
options,
|
|
97
|
-
gitInfo: {
|
|
127
|
+
gitInfo: {
|
|
128
|
+
branch: gitCtx.branch,
|
|
129
|
+
commit: gitCtx.commit,
|
|
130
|
+
author: gitCtx.author,
|
|
131
|
+
timestamp: gitCtx.timestamp,
|
|
132
|
+
},
|
|
98
133
|
versioningInfo: { newVersion: version }
|
|
99
134
|
});
|
|
100
135
|
await manager.syncAll(cfg);
|
|
@@ -107,31 +142,122 @@ const extension = {
|
|
|
107
142
|
register: async (program, rootConfig) => {
|
|
108
143
|
const fileManager = new file_manager_1.FileManager();
|
|
109
144
|
const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager });
|
|
145
|
+
const discoverWorkspaceProjects = async (configPath) => {
|
|
146
|
+
const rootDir = path.dirname(configPath);
|
|
147
|
+
const slugs = new Set();
|
|
148
|
+
const names = new Set();
|
|
149
|
+
const considerPackageJson = async (packageJsonPath) => {
|
|
150
|
+
try {
|
|
151
|
+
if (!(await fs.pathExists(packageJsonPath)))
|
|
152
|
+
return;
|
|
153
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
154
|
+
const name = typeof pkg?.name === 'string' ? String(pkg.name).trim() : '';
|
|
155
|
+
if (!name)
|
|
156
|
+
return;
|
|
157
|
+
names.add(name);
|
|
158
|
+
const slug = name.includes('/') ? name.split('/').pop() : name;
|
|
159
|
+
if (slug)
|
|
160
|
+
slugs.add(String(slug));
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// ignore
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
const scanOneLevel = async (baseDir) => {
|
|
167
|
+
const abs = path.join(rootDir, baseDir);
|
|
168
|
+
if (!(await fs.pathExists(abs)))
|
|
169
|
+
return;
|
|
170
|
+
const entries = await fs.readdir(abs, { withFileTypes: true });
|
|
171
|
+
for (const entry of entries) {
|
|
172
|
+
if (!entry.isDirectory())
|
|
173
|
+
continue;
|
|
174
|
+
const dirName = entry.name;
|
|
175
|
+
if (dirName === 'node_modules' || dirName === 'dist' || dirName === '.git' || dirName === 'archive')
|
|
176
|
+
continue;
|
|
177
|
+
slugs.add(dirName);
|
|
178
|
+
await considerPackageJson(path.join(abs, dirName, 'package.json'));
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
const scanTwoLevelsUnderApps = async () => {
|
|
182
|
+
const absApps = path.join(rootDir, 'apps');
|
|
183
|
+
if (!(await fs.pathExists(absApps)))
|
|
184
|
+
return;
|
|
185
|
+
const entries = await fs.readdir(absApps, { withFileTypes: true });
|
|
186
|
+
for (const entry of entries) {
|
|
187
|
+
if (!entry.isDirectory())
|
|
188
|
+
continue;
|
|
189
|
+
const groupDir = path.join(absApps, entry.name);
|
|
190
|
+
if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git' || entry.name === 'archive')
|
|
191
|
+
continue;
|
|
192
|
+
const nested = await fs.readdir(groupDir, { withFileTypes: true });
|
|
193
|
+
for (const n of nested) {
|
|
194
|
+
if (!n.isDirectory())
|
|
195
|
+
continue;
|
|
196
|
+
if (n.name === 'node_modules' || n.name === 'dist' || n.name === '.git' || n.name === 'archive')
|
|
197
|
+
continue;
|
|
198
|
+
slugs.add(n.name);
|
|
199
|
+
await considerPackageJson(path.join(groupDir, n.name, 'package.json'));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
await scanOneLevel('apps');
|
|
204
|
+
await scanTwoLevelsUnderApps();
|
|
205
|
+
await scanOneLevel('packages');
|
|
206
|
+
return { slugs, names };
|
|
207
|
+
};
|
|
208
|
+
const validateProjectOption = async (configPath, project) => {
|
|
209
|
+
const canonical = (0, config_manager_1.canonicalProjectKey)(project);
|
|
210
|
+
if (!canonical)
|
|
211
|
+
return undefined;
|
|
212
|
+
const { slugs, names } = await discoverWorkspaceProjects(configPath);
|
|
213
|
+
const raw = String(project ?? '').trim();
|
|
214
|
+
const ok = slugs.has(canonical) || names.has(raw) || names.has(`@ed/${canonical}`) || names.has(`@edcalderon/${canonical}`);
|
|
215
|
+
if (!ok) {
|
|
216
|
+
const available = Array.from(slugs).sort().slice(0, 40);
|
|
217
|
+
const suffix = slugs.size > 40 ? 'ā¦' : '';
|
|
218
|
+
throw new Error(`Unknown project scope: '${raw}'. Expected an existing workspace app/package (try one of: ${available.join(', ')}${suffix}).`);
|
|
219
|
+
}
|
|
220
|
+
return canonical;
|
|
221
|
+
};
|
|
110
222
|
const loadRootConfigFile = async (configPath) => {
|
|
111
223
|
if (!(await fs.pathExists(configPath))) {
|
|
112
224
|
throw new Error(`Config file not found: ${configPath}. Run 'versioning init' to create one.`);
|
|
113
225
|
}
|
|
114
226
|
return await fs.readJson(configPath);
|
|
115
227
|
};
|
|
116
|
-
const ensureReentryInitialized = async (configPath, migrate) => {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
228
|
+
const ensureReentryInitialized = async (configPath, migrate, project) => {
|
|
229
|
+
const validatedProject = await validateProjectOption(configPath, project);
|
|
230
|
+
const rawCfg = await loadRootConfigFile(configPath);
|
|
231
|
+
const resolved = config_manager_1.ConfigManager.loadConfig(rawCfg, validatedProject);
|
|
232
|
+
const cfg = {
|
|
233
|
+
...rawCfg,
|
|
234
|
+
reentryStatus: {
|
|
235
|
+
...(rawCfg.reentryStatus ?? {}),
|
|
236
|
+
files: resolved.files,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg, validatedProject);
|
|
240
|
+
await fs.ensureDir(path.dirname(reentryCfg.files.jsonPath));
|
|
241
|
+
const defaultRoadmapPath = path.join(path.dirname(reentryCfg.files.jsonPath), constants_1.ROADMAP_MD_FILENAME);
|
|
120
242
|
const existingJson = await fileManager.readFileIfExists(reentryCfg.files.jsonPath);
|
|
121
243
|
if (existingJson) {
|
|
122
244
|
const parsed = status_renderer_1.StatusRenderer.parseJson(existingJson);
|
|
123
245
|
if (migrate && parsed.schemaVersion === '1.0') {
|
|
124
|
-
// Explicit migration: rewrite as 1.1 without changing semantics.
|
|
125
246
|
const migrated = {
|
|
126
247
|
...parsed,
|
|
127
248
|
schemaVersion: '1.1',
|
|
128
249
|
milestone: parsed.milestone ?? null,
|
|
129
|
-
roadmapFile:
|
|
250
|
+
roadmapFile: defaultRoadmapPath
|
|
130
251
|
};
|
|
131
252
|
await fileManager.writeStatusJson(cfg, migrated);
|
|
132
|
-
return migrated;
|
|
253
|
+
return { cfg, status: migrated };
|
|
133
254
|
}
|
|
134
|
-
|
|
255
|
+
const normalized = {
|
|
256
|
+
...parsed,
|
|
257
|
+
schemaVersion: '1.1',
|
|
258
|
+
roadmapFile: parsed.roadmapFile || defaultRoadmapPath,
|
|
259
|
+
};
|
|
260
|
+
return { cfg, status: normalized };
|
|
135
261
|
}
|
|
136
262
|
const initial = {
|
|
137
263
|
schemaVersion: '1.1',
|
|
@@ -144,7 +270,7 @@ const extension = {
|
|
|
144
270
|
versioningInfo: {}
|
|
145
271
|
},
|
|
146
272
|
milestone: null,
|
|
147
|
-
roadmapFile:
|
|
273
|
+
roadmapFile: defaultRoadmapPath,
|
|
148
274
|
currentPhase: 'planning',
|
|
149
275
|
milestones: [],
|
|
150
276
|
blockers: [],
|
|
@@ -162,17 +288,21 @@ const extension = {
|
|
|
162
288
|
}
|
|
163
289
|
};
|
|
164
290
|
await fileManager.writeStatusFiles(cfg, initial);
|
|
165
|
-
return initial;
|
|
291
|
+
return { cfg, status: initial };
|
|
166
292
|
};
|
|
293
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
294
|
+
// REENTRY COMMANDS
|
|
295
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
167
296
|
program
|
|
168
297
|
.command('reentry')
|
|
169
298
|
.description('Manage re-entry status (fast layer)')
|
|
170
299
|
.addCommand(new commander_1.Command('init')
|
|
171
300
|
.description('Initialize re-entry status files')
|
|
172
301
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
302
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
173
303
|
.option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
|
|
174
304
|
.action(async (options) => {
|
|
175
|
-
const status = await ensureReentryInitialized(options.config, Boolean(options.migrate));
|
|
305
|
+
const { status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
|
|
176
306
|
console.log(`ā
Initialized re-entry status (schema ${status.schemaVersion})`);
|
|
177
307
|
}))
|
|
178
308
|
.addCommand(new commander_1.Command('set')
|
|
@@ -180,10 +310,10 @@ const extension = {
|
|
|
180
310
|
.option('--phase <phase>', 'Set current phase')
|
|
181
311
|
.option('--next <text>', 'Set next micro-step (replaces first nextSteps entry)')
|
|
182
312
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
313
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
183
314
|
.option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
|
|
184
315
|
.action(async (options) => {
|
|
185
|
-
const cfg = await
|
|
186
|
-
const status = await ensureReentryInitialized(options.config, Boolean(options.migrate));
|
|
316
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
|
|
187
317
|
const nextStepText = typeof options.next === 'string' ? options.next.trim() : '';
|
|
188
318
|
const phase = typeof options.phase === 'string' ? options.phase.trim() : '';
|
|
189
319
|
const updated = {
|
|
@@ -197,15 +327,123 @@ const extension = {
|
|
|
197
327
|
};
|
|
198
328
|
await manager.updateStatus(cfg, () => updated);
|
|
199
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');
|
|
200
438
|
}))
|
|
201
439
|
.addCommand(new commander_1.Command('sync')
|
|
202
440
|
.description('Ensure generated status files exist and are up to date (idempotent)')
|
|
203
441
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
442
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
204
443
|
.option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
|
|
205
444
|
.action(async (options) => {
|
|
206
|
-
const cfg = await
|
|
207
|
-
const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg);
|
|
208
|
-
const status = await ensureReentryInitialized(options.config, Boolean(options.migrate));
|
|
445
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
|
|
446
|
+
const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg, options.project);
|
|
209
447
|
// Ensure ROADMAP exists (light touch: only managed block is updated).
|
|
210
448
|
const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
|
|
211
449
|
const existing = await fileManager.readFileIfExists(roadmapPath);
|
|
@@ -290,20 +528,28 @@ const extension = {
|
|
|
290
528
|
}
|
|
291
529
|
console.log('ā
Re-entry sync complete');
|
|
292
530
|
}));
|
|
531
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
532
|
+
// ROADMAP COMMANDS (expanded with project identification)
|
|
533
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
293
534
|
program
|
|
294
535
|
.command('roadmap')
|
|
295
536
|
.description('Manage roadmap/backlog (slow layer)')
|
|
296
537
|
.addCommand(new commander_1.Command('init')
|
|
297
538
|
.description(`Create ${constants_1.REENTRY_STATUS_DIRNAME}/${constants_1.ROADMAP_MD_FILENAME} if missing and ensure managed header block`)
|
|
298
539
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
540
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
299
541
|
.option('-t, --title <title>', 'project title for ROADMAP.md template', 'Untitled')
|
|
300
542
|
.action(async (options) => {
|
|
301
|
-
const
|
|
302
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
543
|
+
const projectKey = await validateProjectOption(options.config, options.project);
|
|
544
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, false, projectKey);
|
|
303
545
|
const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
|
|
546
|
+
// If a project is specified and title is left default, prefer a non-stale title.
|
|
547
|
+
const title = projectKey && String(options.title).trim() === 'Untitled'
|
|
548
|
+
? String(options.project ?? projectKey)
|
|
549
|
+
: String(options.title);
|
|
304
550
|
const existing = await fileManager.readFileIfExists(roadmapPath);
|
|
305
551
|
if (!existing) {
|
|
306
|
-
await fileManager.writeFileIfChanged(roadmapPath, roadmap_renderer_1.RoadmapRenderer.renderTemplate({ projectTitle:
|
|
552
|
+
await fileManager.writeFileIfChanged(roadmapPath, roadmap_renderer_1.RoadmapRenderer.renderTemplate({ projectTitle: title }, { milestone: status.milestone, roadmapFile: roadmapPath }));
|
|
307
553
|
console.log(`ā
Created ${roadmapPath}`);
|
|
308
554
|
return;
|
|
309
555
|
}
|
|
@@ -317,12 +563,40 @@ const extension = {
|
|
|
317
563
|
}
|
|
318
564
|
// Keep REENTRY.md consistent with roadmap references.
|
|
319
565
|
await fileManager.writeReentryMarkdown(cfg, status);
|
|
566
|
+
}))
|
|
567
|
+
.addCommand(new commander_1.Command('validate')
|
|
568
|
+
.description('Validate that project roadmaps correspond to existing workspaces (detect stale roadmaps)')
|
|
569
|
+
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
570
|
+
.action(async (options) => {
|
|
571
|
+
const { slugs } = await discoverWorkspaceProjects(String(options.config));
|
|
572
|
+
const projectsDir = path.join(path.dirname(String(options.config)), constants_1.REENTRY_STATUS_DIRNAME, 'projects');
|
|
573
|
+
if (!(await fs.pathExists(projectsDir))) {
|
|
574
|
+
console.log('ā
No project roadmaps found');
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const entries = await fs.readdir(projectsDir, { withFileTypes: true });
|
|
578
|
+
const stale = [];
|
|
579
|
+
for (const entry of entries) {
|
|
580
|
+
if (!entry.isDirectory())
|
|
581
|
+
continue;
|
|
582
|
+
const key = entry.name;
|
|
583
|
+
if (!slugs.has(key)) {
|
|
584
|
+
stale.push(key);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
if (stale.length === 0) {
|
|
588
|
+
console.log('ā
All project roadmaps match a workspace');
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
console.warn(`ā ļø Stale project roadmaps found (no matching workspace): ${stale.join(', ')}`);
|
|
592
|
+
process.exitCode = 1;
|
|
320
593
|
}))
|
|
321
594
|
.addCommand(new commander_1.Command('list')
|
|
322
595
|
.description('List roadmap milestones parsed from ROADMAP.md')
|
|
323
596
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
597
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
324
598
|
.action(async (options) => {
|
|
325
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
599
|
+
const { status } = await ensureReentryInitialized(options.config, false, options.project);
|
|
326
600
|
const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
|
|
327
601
|
const content = await fileManager.readFileIfExists(roadmapPath);
|
|
328
602
|
if (!content) {
|
|
@@ -346,9 +620,9 @@ const extension = {
|
|
|
346
620
|
.requiredOption('--id <id>', 'Milestone id (must match a [id] in ROADMAP.md)')
|
|
347
621
|
.requiredOption('--title <title>', 'Milestone title')
|
|
348
622
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
623
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
349
624
|
.action(async (options) => {
|
|
350
|
-
const cfg = await
|
|
351
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
625
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, false, options.project);
|
|
352
626
|
const next = {
|
|
353
627
|
...status,
|
|
354
628
|
schemaVersion: '1.1',
|
|
@@ -365,8 +639,9 @@ const extension = {
|
|
|
365
639
|
.requiredOption('--item <item>', 'Item text')
|
|
366
640
|
.option('--id <id>', 'Optional explicit id (e.g., now-02)')
|
|
367
641
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
642
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
368
643
|
.action(async (options) => {
|
|
369
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
644
|
+
const { status } = await ensureReentryInitialized(options.config, false, options.project);
|
|
370
645
|
const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
|
|
371
646
|
const content = await fileManager.readFileIfExists(roadmapPath);
|
|
372
647
|
if (!content) {
|
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
|
+
}
|