@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.
@@ -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.1.0',
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: { 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
+ },
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: { 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
+ },
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 cfg = await loadRootConfigFile(configPath);
118
- const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg);
119
- await fs.ensureDir(constants_1.REENTRY_STATUS_DIRNAME);
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: parsed.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath()
250
+ roadmapFile: defaultRoadmapPath
130
251
  };
131
252
  await fileManager.writeStatusJson(cfg, migrated);
132
- return migrated;
253
+ return { cfg, status: migrated };
133
254
  }
134
- return parsed;
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: roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath(),
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 loadRootConfigFile(options.config);
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 loadRootConfigFile(options.config);
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 cfg = await loadRootConfigFile(options.config);
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: options.title }, { milestone: status.milestone, roadmapFile: roadmapPath }));
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 loadRootConfigFile(options.config);
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.1.0",
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
+ }