@edcalderon/versioning 1.0.11 → 1.1.2

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +50 -0
  3. package/dist/cli.js +3 -1
  4. package/dist/extensions/reentry-status/config-manager.d.ts +17 -0
  5. package/dist/extensions/reentry-status/config-manager.js +187 -0
  6. package/dist/extensions/reentry-status/constants.d.ts +6 -0
  7. package/dist/extensions/reentry-status/constants.js +9 -0
  8. package/dist/extensions/reentry-status/dirty-detection.d.ts +4 -0
  9. package/dist/extensions/reentry-status/dirty-detection.js +18 -0
  10. package/dist/extensions/reentry-status/file-manager.d.ts +29 -0
  11. package/dist/extensions/reentry-status/file-manager.js +144 -0
  12. package/dist/extensions/reentry-status/github-rest-client.d.ts +29 -0
  13. package/dist/extensions/reentry-status/github-rest-client.js +72 -0
  14. package/dist/extensions/reentry-status/github-sync-adapter.d.ts +39 -0
  15. package/dist/extensions/reentry-status/github-sync-adapter.js +80 -0
  16. package/dist/extensions/reentry-status/index.d.ts +14 -0
  17. package/dist/extensions/reentry-status/index.js +30 -0
  18. package/dist/extensions/reentry-status/models.d.ts +159 -0
  19. package/dist/extensions/reentry-status/models.js +3 -0
  20. package/dist/extensions/reentry-status/obsidian-cli-client.d.ts +15 -0
  21. package/dist/extensions/reentry-status/obsidian-cli-client.js +96 -0
  22. package/dist/extensions/reentry-status/obsidian-sync-adapter.d.ts +24 -0
  23. package/dist/extensions/reentry-status/obsidian-sync-adapter.js +80 -0
  24. package/dist/extensions/reentry-status/reentry-status-manager.d.ts +30 -0
  25. package/dist/extensions/reentry-status/reentry-status-manager.js +193 -0
  26. package/dist/extensions/reentry-status/roadmap-parser.d.ts +13 -0
  27. package/dist/extensions/reentry-status/roadmap-parser.js +32 -0
  28. package/dist/extensions/reentry-status/roadmap-renderer.d.ts +19 -0
  29. package/dist/extensions/reentry-status/roadmap-renderer.js +92 -0
  30. package/dist/extensions/reentry-status/status-renderer.d.ts +10 -0
  31. package/dist/extensions/reentry-status/status-renderer.js +176 -0
  32. package/dist/extensions/reentry-status-extension.d.ts +4 -0
  33. package/dist/extensions/reentry-status-extension.js +528 -0
  34. package/examples/reentry-status/README.md +18 -0
  35. package/examples/reentry-status/REENTRY.md.example +14 -0
  36. package/examples/reentry-status/ROADMAP.md.example +25 -0
  37. package/examples/reentry-status/reentry.status.v1.1.example.json +42 -0
  38. package/examples/reentry-status/versioning.config.reentryStatus.example.json +44 -0
  39. package/package.json +2 -1
@@ -0,0 +1,528 @@
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
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const commander_1 = require("commander");
37
+ const fs = __importStar(require("fs-extra"));
38
+ const path = __importStar(require("path"));
39
+ const config_manager_1 = require("./reentry-status/config-manager");
40
+ const constants_1 = require("./reentry-status/constants");
41
+ const file_manager_1 = require("./reentry-status/file-manager");
42
+ const dirty_detection_1 = require("./reentry-status/dirty-detection");
43
+ const github_rest_client_1 = require("./reentry-status/github-rest-client");
44
+ const github_sync_adapter_1 = require("./reentry-status/github-sync-adapter");
45
+ const obsidian_cli_client_1 = require("./reentry-status/obsidian-cli-client");
46
+ const obsidian_sync_adapter_1 = require("./reentry-status/obsidian-sync-adapter");
47
+ const roadmap_parser_1 = require("./reentry-status/roadmap-parser");
48
+ const roadmap_renderer_1 = require("./reentry-status/roadmap-renderer");
49
+ const status_renderer_1 = require("./reentry-status/status-renderer");
50
+ const reentry_status_manager_1 = require("./reentry-status/reentry-status-manager");
51
+ const extension = {
52
+ name: constants_1.REENTRY_EXTENSION_NAME,
53
+ description: 'Maintains canonical re-entry status and synchronizes to files, GitHub Issues, and Obsidian notes',
54
+ version: '1.1.2',
55
+ hooks: {
56
+ postVersion: async (type, version, options) => {
57
+ try {
58
+ // Extensions are loaded before the CLI reads config per-command; use global config snapshot.
59
+ const configPath = options?.config ?? 'versioning.config.json';
60
+ if (!(await fs.pathExists(configPath)))
61
+ return;
62
+ const cfg = await fs.readJson(configPath);
63
+ const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg);
64
+ if (!reentryCfg.enabled || !reentryCfg.autoSync)
65
+ return;
66
+ if (reentryCfg.hooks?.postVersion === false)
67
+ return;
68
+ const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager: new file_manager_1.FileManager() });
69
+ await manager.applyContext(cfg, {
70
+ trigger: 'postVersion',
71
+ command: 'versioning bump',
72
+ options,
73
+ gitInfo: { branch: '', commit: '', author: '', timestamp: new Date().toISOString() },
74
+ versioningInfo: { versionType: type, oldVersion: undefined, newVersion: version }
75
+ });
76
+ await manager.syncAll(cfg);
77
+ }
78
+ catch (error) {
79
+ console.warn('⚠️ reentry-status postVersion hook failed:', error instanceof Error ? error.message : String(error));
80
+ }
81
+ },
82
+ postRelease: async (version, options) => {
83
+ try {
84
+ const configPath = options?.config ?? 'versioning.config.json';
85
+ if (!(await fs.pathExists(configPath)))
86
+ return;
87
+ const cfg = await fs.readJson(configPath);
88
+ const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg);
89
+ if (!reentryCfg.enabled || !reentryCfg.autoSync)
90
+ return;
91
+ if (reentryCfg.hooks?.postRelease !== true)
92
+ return;
93
+ const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager: new file_manager_1.FileManager() });
94
+ await manager.applyContext(cfg, {
95
+ trigger: 'postRelease',
96
+ command: 'versioning release',
97
+ options,
98
+ gitInfo: { branch: '', commit: '', author: '', timestamp: new Date().toISOString() },
99
+ versioningInfo: { newVersion: version }
100
+ });
101
+ await manager.syncAll(cfg);
102
+ }
103
+ catch (error) {
104
+ console.warn('⚠️ reentry-status postRelease hook failed:', error instanceof Error ? error.message : String(error));
105
+ }
106
+ }
107
+ },
108
+ register: async (program, rootConfig) => {
109
+ const fileManager = new file_manager_1.FileManager();
110
+ const manager = new reentry_status_manager_1.ReentryStatusManager({ fileManager });
111
+ const discoverWorkspaceProjects = async (configPath) => {
112
+ const rootDir = path.dirname(configPath);
113
+ const slugs = new Set();
114
+ const names = new Set();
115
+ const considerPackageJson = async (packageJsonPath) => {
116
+ try {
117
+ if (!(await fs.pathExists(packageJsonPath)))
118
+ return;
119
+ const pkg = await fs.readJson(packageJsonPath);
120
+ const name = typeof pkg?.name === 'string' ? String(pkg.name).trim() : '';
121
+ if (!name)
122
+ return;
123
+ names.add(name);
124
+ const slug = name.includes('/') ? name.split('/').pop() : name;
125
+ if (slug)
126
+ slugs.add(String(slug));
127
+ }
128
+ catch {
129
+ // ignore
130
+ }
131
+ };
132
+ const scanOneLevel = async (baseDir) => {
133
+ const abs = path.join(rootDir, baseDir);
134
+ if (!(await fs.pathExists(abs)))
135
+ return;
136
+ const entries = await fs.readdir(abs, { withFileTypes: true });
137
+ for (const entry of entries) {
138
+ if (!entry.isDirectory())
139
+ continue;
140
+ const dirName = entry.name;
141
+ if (dirName === 'node_modules' || dirName === 'dist' || dirName === '.git' || dirName === 'archive')
142
+ continue;
143
+ slugs.add(dirName);
144
+ await considerPackageJson(path.join(abs, dirName, 'package.json'));
145
+ }
146
+ };
147
+ const scanTwoLevelsUnderApps = async () => {
148
+ const absApps = path.join(rootDir, 'apps');
149
+ if (!(await fs.pathExists(absApps)))
150
+ return;
151
+ const entries = await fs.readdir(absApps, { withFileTypes: true });
152
+ for (const entry of entries) {
153
+ if (!entry.isDirectory())
154
+ continue;
155
+ const groupDir = path.join(absApps, entry.name);
156
+ if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git' || entry.name === 'archive')
157
+ continue;
158
+ const nested = await fs.readdir(groupDir, { withFileTypes: true });
159
+ for (const n of nested) {
160
+ if (!n.isDirectory())
161
+ continue;
162
+ if (n.name === 'node_modules' || n.name === 'dist' || n.name === '.git' || n.name === 'archive')
163
+ continue;
164
+ slugs.add(n.name);
165
+ await considerPackageJson(path.join(groupDir, n.name, 'package.json'));
166
+ }
167
+ }
168
+ };
169
+ await scanOneLevel('apps');
170
+ await scanTwoLevelsUnderApps();
171
+ await scanOneLevel('packages');
172
+ return { slugs, names };
173
+ };
174
+ const validateProjectOption = async (configPath, project) => {
175
+ const canonical = (0, config_manager_1.canonicalProjectKey)(project);
176
+ if (!canonical)
177
+ return undefined;
178
+ const { slugs, names } = await discoverWorkspaceProjects(configPath);
179
+ const raw = String(project ?? '').trim();
180
+ const ok = slugs.has(canonical) || names.has(raw) || names.has(`@ed/${canonical}`) || names.has(`@edcalderon/${canonical}`);
181
+ if (!ok) {
182
+ const available = Array.from(slugs).sort().slice(0, 40);
183
+ const suffix = slugs.size > 40 ? '…' : '';
184
+ throw new Error(`Unknown project scope: '${raw}'. Expected an existing workspace app/package (try one of: ${available.join(', ')}${suffix}).`);
185
+ }
186
+ return canonical;
187
+ };
188
+ const loadRootConfigFile = async (configPath) => {
189
+ if (!(await fs.pathExists(configPath))) {
190
+ throw new Error(`Config file not found: ${configPath}. Run 'versioning init' to create one.`);
191
+ }
192
+ return await fs.readJson(configPath);
193
+ };
194
+ const ensureReentryInitialized = async (configPath, migrate, project) => {
195
+ const validatedProject = await validateProjectOption(configPath, project);
196
+ const rawCfg = await loadRootConfigFile(configPath);
197
+ const resolved = config_manager_1.ConfigManager.loadConfig(rawCfg, validatedProject);
198
+ const cfg = {
199
+ ...rawCfg,
200
+ reentryStatus: {
201
+ ...(rawCfg.reentryStatus ?? {}),
202
+ files: resolved.files,
203
+ },
204
+ };
205
+ const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg, validatedProject);
206
+ await fs.ensureDir(path.dirname(reentryCfg.files.jsonPath));
207
+ const defaultRoadmapPath = path.join(path.dirname(reentryCfg.files.jsonPath), constants_1.ROADMAP_MD_FILENAME);
208
+ const existingJson = await fileManager.readFileIfExists(reentryCfg.files.jsonPath);
209
+ if (existingJson) {
210
+ const parsed = status_renderer_1.StatusRenderer.parseJson(existingJson);
211
+ if (migrate && parsed.schemaVersion === '1.0') {
212
+ // Explicit migration: rewrite as 1.1 without changing semantics.
213
+ const migrated = {
214
+ ...parsed,
215
+ schemaVersion: '1.1',
216
+ milestone: parsed.milestone ?? null,
217
+ roadmapFile: defaultRoadmapPath
218
+ };
219
+ await fileManager.writeStatusJson(cfg, migrated);
220
+ return { cfg, status: migrated };
221
+ }
222
+ const normalized = {
223
+ ...parsed,
224
+ schemaVersion: '1.1',
225
+ roadmapFile: parsed.roadmapFile || defaultRoadmapPath,
226
+ };
227
+ return { cfg, status: normalized };
228
+ }
229
+ const initial = {
230
+ schemaVersion: '1.1',
231
+ version: '0.0.0',
232
+ lastUpdated: new Date(0).toISOString(),
233
+ updatedBy: 'unknown',
234
+ context: {
235
+ trigger: 'manual',
236
+ gitInfo: { branch: '', commit: '', author: '', timestamp: new Date(0).toISOString() },
237
+ versioningInfo: {}
238
+ },
239
+ milestone: null,
240
+ roadmapFile: defaultRoadmapPath,
241
+ currentPhase: 'planning',
242
+ milestones: [],
243
+ blockers: [],
244
+ nextSteps: [],
245
+ risks: [],
246
+ dependencies: [],
247
+ versioning: {
248
+ currentVersion: '0.0.0',
249
+ previousVersion: '0.0.0',
250
+ versionType: 'patch'
251
+ },
252
+ syncMetadata: {
253
+ lastSyncAttempt: new Date(0).toISOString(),
254
+ lastSuccessfulSync: new Date(0).toISOString()
255
+ }
256
+ };
257
+ await fileManager.writeStatusFiles(cfg, initial);
258
+ return { cfg, status: initial };
259
+ };
260
+ program
261
+ .command('reentry')
262
+ .description('Manage re-entry status (fast layer)')
263
+ .addCommand(new commander_1.Command('init')
264
+ .description('Initialize re-entry status files')
265
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
266
+ .option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
267
+ .option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
268
+ .action(async (options) => {
269
+ const { status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
270
+ console.log(`✅ Initialized re-entry status (schema ${status.schemaVersion})`);
271
+ }))
272
+ .addCommand(new commander_1.Command('set')
273
+ .description('Update re-entry status fields (fast layer)')
274
+ .option('--phase <phase>', 'Set current phase')
275
+ .option('--next <text>', 'Set next micro-step (replaces first nextSteps entry)')
276
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
277
+ .option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
278
+ .option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
279
+ .action(async (options) => {
280
+ const { cfg, status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
281
+ const nextStepText = typeof options.next === 'string' ? options.next.trim() : '';
282
+ const phase = typeof options.phase === 'string' ? options.phase.trim() : '';
283
+ const updated = {
284
+ ...status,
285
+ schemaVersion: '1.1',
286
+ currentPhase: phase ? phase : status.currentPhase,
287
+ nextSteps: nextStepText
288
+ ? [{ id: 'next', description: nextStepText, priority: 1 }]
289
+ : status.nextSteps,
290
+ lastUpdated: new Date().toISOString()
291
+ };
292
+ await manager.updateStatus(cfg, () => updated);
293
+ console.log('✅ Re-entry status updated');
294
+ }))
295
+ .addCommand(new commander_1.Command('sync')
296
+ .description('Ensure generated status files exist and are up to date (idempotent)')
297
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
298
+ .option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
299
+ .option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
300
+ .action(async (options) => {
301
+ const { cfg, status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
302
+ const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg, options.project);
303
+ // Ensure ROADMAP exists (light touch: only managed block is updated).
304
+ const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
305
+ const existing = await fileManager.readFileIfExists(roadmapPath);
306
+ if (!existing) {
307
+ await fileManager.writeFileIfChanged(roadmapPath, roadmap_renderer_1.RoadmapRenderer.renderTemplate({}, { milestone: status.milestone, roadmapFile: roadmapPath }));
308
+ }
309
+ else {
310
+ const upserted = roadmap_renderer_1.RoadmapRenderer.upsertManagedBlock(existing, { milestone: status.milestone, roadmapFile: roadmapPath });
311
+ if (upserted.changed) {
312
+ await fileManager.writeFileIfChanged(roadmapPath, upserted.content);
313
+ }
314
+ }
315
+ // Always ensure JSON + REENTRY.md are consistent.
316
+ await fileManager.writeStatusFiles(cfg, status);
317
+ const reentryMarkdown = status_renderer_1.StatusRenderer.renderMarkdown(status);
318
+ // Optional external sync targets (fail-soft by default).
319
+ let nextStatus = status;
320
+ if (reentryCfg.github?.enabled) {
321
+ try {
322
+ const client = new github_rest_client_1.GitHubRestClient(reentryCfg.github.auth.token);
323
+ const adapter = new github_sync_adapter_1.GitHubSyncAdapter(reentryCfg.github, client);
324
+ const result = await adapter.sync(nextStatus, reentryMarkdown);
325
+ const body = adapter.renderIssueBody(nextStatus, reentryMarkdown);
326
+ const bodyHash = (0, dirty_detection_1.sha256)(body);
327
+ if (result.details?.created || result.details?.updated) {
328
+ nextStatus = {
329
+ ...nextStatus,
330
+ schemaVersion: '1.1',
331
+ syncMetadata: {
332
+ ...nextStatus.syncMetadata,
333
+ githubIssueId: result.details.issueId ?? nextStatus.syncMetadata.githubIssueId,
334
+ githubIssueUrl: result.details.url ?? nextStatus.syncMetadata.githubIssueUrl,
335
+ published: {
336
+ ...(nextStatus.syncMetadata.published ?? {}),
337
+ githubIssueBodySha256: bodyHash
338
+ }
339
+ }
340
+ };
341
+ await fileManager.writeStatusFiles(cfg, nextStatus);
342
+ }
343
+ }
344
+ catch (error) {
345
+ const message = error instanceof Error ? error.message : String(error);
346
+ if (reentryCfg.failHard)
347
+ throw error;
348
+ console.warn(`⚠️ GitHub sync skipped: ${message}`);
349
+ }
350
+ }
351
+ if (reentryCfg.obsidian?.enabled) {
352
+ try {
353
+ const available = await obsidian_cli_client_1.ObsidianCliClient.isAvailable();
354
+ if (!available) {
355
+ throw new Error('obsidian CLI not available (enable it in Obsidian Settings → General → Command line interface)');
356
+ }
357
+ const client = new obsidian_cli_client_1.ObsidianCliClient();
358
+ const adapter = new obsidian_sync_adapter_1.ObsidianSyncAdapter(reentryCfg.obsidian, client);
359
+ const result = await adapter.sync(nextStatus, reentryMarkdown);
360
+ const content = adapter.renderNoteContent(nextStatus, reentryMarkdown);
361
+ const contentHash = (0, dirty_detection_1.sha256)(content);
362
+ if (result.details?.updated) {
363
+ nextStatus = {
364
+ ...nextStatus,
365
+ schemaVersion: '1.1',
366
+ syncMetadata: {
367
+ ...nextStatus.syncMetadata,
368
+ obsidianNotePath: reentryCfg.obsidian.notePath,
369
+ published: {
370
+ ...(nextStatus.syncMetadata.published ?? {}),
371
+ obsidianNoteBodySha256: contentHash
372
+ }
373
+ }
374
+ };
375
+ await fileManager.writeStatusFiles(cfg, nextStatus);
376
+ }
377
+ }
378
+ catch (error) {
379
+ const message = error instanceof Error ? error.message : String(error);
380
+ if (reentryCfg.failHard)
381
+ throw error;
382
+ console.warn(`⚠️ Obsidian sync skipped: ${message}`);
383
+ }
384
+ }
385
+ console.log('✅ Re-entry sync complete');
386
+ }));
387
+ program
388
+ .command('roadmap')
389
+ .description('Manage roadmap/backlog (slow layer)')
390
+ .addCommand(new commander_1.Command('init')
391
+ .description(`Create ${constants_1.REENTRY_STATUS_DIRNAME}/${constants_1.ROADMAP_MD_FILENAME} if missing and ensure managed header block`)
392
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
393
+ .option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
394
+ .option('-t, --title <title>', 'project title for ROADMAP.md template', 'Untitled')
395
+ .action(async (options) => {
396
+ const projectKey = await validateProjectOption(options.config, options.project);
397
+ const { cfg, status } = await ensureReentryInitialized(options.config, false, projectKey);
398
+ const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
399
+ // If a project is specified and title is left default, prefer a non-stale title.
400
+ const title = projectKey && String(options.title).trim() === 'Untitled'
401
+ ? String(options.project ?? projectKey)
402
+ : String(options.title);
403
+ const existing = await fileManager.readFileIfExists(roadmapPath);
404
+ if (!existing) {
405
+ await fileManager.writeFileIfChanged(roadmapPath, roadmap_renderer_1.RoadmapRenderer.renderTemplate({ projectTitle: title }, { milestone: status.milestone, roadmapFile: roadmapPath }));
406
+ console.log(`✅ Created ${roadmapPath}`);
407
+ return;
408
+ }
409
+ const upserted = roadmap_renderer_1.RoadmapRenderer.upsertManagedBlock(existing, { milestone: status.milestone, roadmapFile: roadmapPath });
410
+ if (upserted.changed) {
411
+ await fileManager.writeFileIfChanged(roadmapPath, upserted.content);
412
+ console.log(`✅ Updated managed block in ${roadmapPath}`);
413
+ }
414
+ else {
415
+ console.log(`✅ ${roadmapPath} already initialized`);
416
+ }
417
+ // Keep REENTRY.md consistent with roadmap references.
418
+ await fileManager.writeReentryMarkdown(cfg, status);
419
+ }))
420
+ .addCommand(new commander_1.Command('validate')
421
+ .description('Validate that project roadmaps correspond to existing workspaces (detect stale roadmaps)')
422
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
423
+ .action(async (options) => {
424
+ const { slugs } = await discoverWorkspaceProjects(String(options.config));
425
+ const projectsDir = path.join(path.dirname(String(options.config)), constants_1.REENTRY_STATUS_DIRNAME, 'projects');
426
+ if (!(await fs.pathExists(projectsDir))) {
427
+ console.log('✅ No project roadmaps found');
428
+ return;
429
+ }
430
+ const entries = await fs.readdir(projectsDir, { withFileTypes: true });
431
+ const stale = [];
432
+ for (const entry of entries) {
433
+ if (!entry.isDirectory())
434
+ continue;
435
+ const key = entry.name;
436
+ if (!slugs.has(key)) {
437
+ stale.push(key);
438
+ }
439
+ }
440
+ if (stale.length === 0) {
441
+ console.log('✅ All project roadmaps match a workspace');
442
+ return;
443
+ }
444
+ console.warn(`⚠️ Stale project roadmaps found (no matching workspace): ${stale.join(', ')}`);
445
+ process.exitCode = 1;
446
+ }))
447
+ .addCommand(new commander_1.Command('list')
448
+ .description('List roadmap milestones parsed from ROADMAP.md')
449
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
450
+ .option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
451
+ .action(async (options) => {
452
+ const { status } = await ensureReentryInitialized(options.config, false, options.project);
453
+ const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
454
+ const content = await fileManager.readFileIfExists(roadmapPath);
455
+ if (!content) {
456
+ console.warn(`⚠️ ${roadmapPath} not found. Run 'versioning roadmap init' first.`);
457
+ return;
458
+ }
459
+ const parsed = (0, roadmap_parser_1.parseRoadmapMilestones)(content);
460
+ for (const w of parsed.warnings)
461
+ console.warn(`⚠️ ${w}`);
462
+ if (parsed.items.length === 0) {
463
+ console.log('— No milestones found');
464
+ return;
465
+ }
466
+ for (const item of parsed.items) {
467
+ const section = item.section ? ` [${item.section}]` : '';
468
+ console.log(`- ${item.id}: ${item.title}${section}`);
469
+ }
470
+ }))
471
+ .addCommand(new commander_1.Command('set-milestone')
472
+ .description('Set active milestone link in reentry.status.json')
473
+ .requiredOption('--id <id>', 'Milestone id (must match a [id] in ROADMAP.md)')
474
+ .requiredOption('--title <title>', 'Milestone title')
475
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
476
+ .option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
477
+ .action(async (options) => {
478
+ const { cfg, status } = await ensureReentryInitialized(options.config, false, options.project);
479
+ const next = {
480
+ ...status,
481
+ schemaVersion: '1.1',
482
+ milestone: { id: String(options.id), title: String(options.title) },
483
+ roadmapFile: status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath()
484
+ };
485
+ await fileManager.writeStatusJson(cfg, next);
486
+ await fileManager.writeReentryMarkdown(cfg, next);
487
+ console.log(`✅ Active milestone set to ${String(options.title)} (${String(options.id)})`);
488
+ }))
489
+ .addCommand(new commander_1.Command('add')
490
+ .description('Add a milestone item to ROADMAP.md under a section')
491
+ .requiredOption('--section <section>', 'Now|Next|Later')
492
+ .requiredOption('--item <item>', 'Item text')
493
+ .option('--id <id>', 'Optional explicit id (e.g., now-02)')
494
+ .option('-c, --config <file>', 'config file path', 'versioning.config.json')
495
+ .option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
496
+ .action(async (options) => {
497
+ const { status } = await ensureReentryInitialized(options.config, false, options.project);
498
+ const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
499
+ const content = await fileManager.readFileIfExists(roadmapPath);
500
+ if (!content) {
501
+ console.warn(`⚠️ ${roadmapPath} not found. Run 'versioning roadmap init' first.`);
502
+ return;
503
+ }
504
+ const normalized = content.replace(/\r\n/g, '\n');
505
+ const lines = normalized.split('\n');
506
+ const sectionName = String(options.section).trim();
507
+ const header = `## ${sectionName}`;
508
+ const headerIndex = lines.findIndex((l) => l.trim() === header);
509
+ if (headerIndex === -1) {
510
+ console.warn(`⚠️ Section not found: ${header}.`);
511
+ return;
512
+ }
513
+ const bulletId = options.id ? String(options.id).trim() : undefined;
514
+ const itemText = String(options.item).trim();
515
+ const bullet = bulletId ? `- [${bulletId}] ${itemText}` : `- ${itemText}`;
516
+ // Insert after the header and the following blank line (if any).
517
+ let insertAt = headerIndex + 1;
518
+ if (lines[insertAt] === '')
519
+ insertAt += 1;
520
+ lines.splice(insertAt, 0, bullet);
521
+ const next = `${lines.join('\n')}\n`;
522
+ await fileManager.writeFileIfChanged(roadmapPath, next);
523
+ console.log(`✅ Added item under ${header}`);
524
+ }));
525
+ }
526
+ };
527
+ exports.default = extension;
528
+ //# sourceMappingURL=reentry-status-extension.js.map
@@ -0,0 +1,18 @@
1
+ # Re-entry Status + Roadmap examples
2
+
3
+ Files in this folder are copy/paste starting points.
4
+
5
+ - `versioning.config.reentryStatus.example.json`: example config with reentryStatus + roadmap + (optional) GitHub/Obsidian.
6
+ - `reentry.status.v1.1.example.json`: example canonical status (JSON).
7
+ - `REENTRY.md.example`: example generated REENTRY markdown.
8
+ - `ROADMAP.md.example`: example roadmap/backlog template with managed block markers.
9
+
10
+ Quick start:
11
+
12
+ ```bash
13
+ # In your repo root:
14
+ cp packages/versioning/examples/reentry-status/versioning.config.reentryStatus.example.json versioning.config.json
15
+ versioning reentry init
16
+ versioning roadmap init --title "My Project"
17
+ versioning roadmap list
18
+ ```
@@ -0,0 +1,14 @@
1
+ # Re-entry Status
2
+
3
+ Schema: 1.1
4
+ Version: 0.1.0
5
+ Phase: development
6
+
7
+ Next micro-step: Run sync + verify ROADMAP managed block
8
+
9
+ Milestone: Ship stable integration (id: now-01)
10
+ Roadmap: .versioning/ROADMAP.md
11
+
12
+ ## Notes
13
+
14
+ - This file is generated for stable diffs. Edit ROADMAP.md for long-term planning.
@@ -0,0 +1,25 @@
1
+ # Project Roadmap – Untitled
2
+
3
+ <!-- roadmap:managed:start -->
4
+ > Managed by `@edcalderon/versioning` reentry-status-extension.
5
+ > Canonical roadmap file: .versioning/ROADMAP.md
6
+ > Active milestone: Ship stable integration (id: now-01)
7
+ >
8
+ > Everything outside this block is user-editable.
9
+ <!-- roadmap:managed:end -->
10
+
11
+ ## North Star
12
+
13
+ - Describe the long-term outcome this project is aiming for.
14
+
15
+ ## Now (1–2 weeks)
16
+
17
+ - [now-01] Ship stable integration
18
+
19
+ ## Next (4–8 weeks)
20
+
21
+ - [next-01] Improve reliability
22
+
23
+ ## Later
24
+
25
+ - [later-01] Optional refactor
@@ -0,0 +1,42 @@
1
+ {
2
+ "schemaVersion": "1.1",
3
+ "version": "0.1.0",
4
+ "lastUpdated": "2026-02-11T00:00:00.000Z",
5
+ "updatedBy": "dev",
6
+ "context": {
7
+ "trigger": "manual",
8
+ "gitInfo": {
9
+ "branch": "main",
10
+ "commit": "abc123",
11
+ "author": "dev",
12
+ "timestamp": "2026-02-11T00:00:00.000Z"
13
+ },
14
+ "versioningInfo": {}
15
+ },
16
+ "milestone": {
17
+ "id": "now-01",
18
+ "title": "Ship stable integration"
19
+ },
20
+ "roadmapFile": ".versioning/ROADMAP.md",
21
+ "currentPhase": "development",
22
+ "milestones": [],
23
+ "blockers": [],
24
+ "nextSteps": [
25
+ {
26
+ "id": "next",
27
+ "description": "Run sync + verify ROADMAP managed block",
28
+ "priority": 1
29
+ }
30
+ ],
31
+ "risks": [],
32
+ "dependencies": [],
33
+ "versioning": {
34
+ "currentVersion": "0.1.0",
35
+ "previousVersion": "0.0.9",
36
+ "versionType": "patch"
37
+ },
38
+ "syncMetadata": {
39
+ "lastSyncAttempt": "2026-02-11T00:00:00.000Z",
40
+ "lastSuccessfulSync": "2026-02-11T00:00:00.000Z"
41
+ }
42
+ }