@edcalderon/versioning 1.1.0 → 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.
- package/README.md +24 -1
- 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/roadmap-renderer.d.ts +1 -1
- package/dist/extensions/reentry-status/roadmap-renderer.js +2 -2
- package/dist/extensions/reentry-status-extension.js +151 -23
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -118,10 +118,21 @@ Features:
|
|
|
118
118
|
Maintains a fast **re-entry** layer (current state + next micro-step) and a slow **roadmap/backlog** layer (long-term plan).
|
|
119
119
|
|
|
120
120
|
Canonical files:
|
|
121
|
+
|
|
122
|
+
Single-project (default):
|
|
121
123
|
- `.versioning/reentry.status.json` (machine)
|
|
122
124
|
- `.versioning/REENTRY.md` (generated, minimal diffs)
|
|
123
125
|
- `.versioning/ROADMAP.md` (human-first; only a small managed header block is auto-updated)
|
|
124
126
|
|
|
127
|
+
Multi-project (scoped by `--project <name>`):
|
|
128
|
+
- `.versioning/projects/<project>/reentry.status.json`
|
|
129
|
+
- `.versioning/projects/<project>/REENTRY.md`
|
|
130
|
+
- `.versioning/projects/<project>/ROADMAP.md`
|
|
131
|
+
|
|
132
|
+
Notes:
|
|
133
|
+
- `--project` must match an existing workspace app/package (it can be a slug like `trader`, a scoped package name like `@ed/trader`, or a path like `apps/trader`).
|
|
134
|
+
- The canonical project key is the last path segment (e.g. `@ed/trader` → `trader`).
|
|
135
|
+
|
|
125
136
|
Commands:
|
|
126
137
|
|
|
127
138
|
```bash
|
|
@@ -129,11 +140,23 @@ Commands:
|
|
|
129
140
|
versioning reentry init
|
|
130
141
|
versioning reentry sync
|
|
131
142
|
|
|
143
|
+
# Fast layer (scoped)
|
|
144
|
+
versioning reentry init --project trader
|
|
145
|
+
versioning reentry sync --project trader
|
|
146
|
+
|
|
132
147
|
# Slow layer
|
|
133
148
|
versioning roadmap init --title "My Project"
|
|
134
149
|
versioning roadmap list
|
|
135
150
|
versioning roadmap set-milestone --id "now-01" --title "Ship X"
|
|
136
|
-
versioning roadmap add --section
|
|
151
|
+
versioning roadmap add --section Now --id "now-02" --item "Add observability"
|
|
152
|
+
|
|
153
|
+
# Slow layer (scoped)
|
|
154
|
+
versioning roadmap init --project trader --title "Trader"
|
|
155
|
+
versioning roadmap list --project trader
|
|
156
|
+
versioning roadmap add --project trader --section Now --item "Wire user-data ORDER_* events"
|
|
157
|
+
|
|
158
|
+
# Detect stale/mismatched scoped roadmaps
|
|
159
|
+
versioning roadmap validate
|
|
137
160
|
```
|
|
138
161
|
|
|
139
162
|
Backward compatibility:
|
|
@@ -3,10 +3,15 @@ export interface ValidationResult {
|
|
|
3
3
|
valid: boolean;
|
|
4
4
|
errors: string[];
|
|
5
5
|
}
|
|
6
|
+
type DeepPartial<T> = {
|
|
7
|
+
[K in keyof T]?: T[K] extends Array<infer U> ? Array<DeepPartial<U>> : T[K] extends object ? DeepPartial<T[K]> : T[K];
|
|
8
|
+
};
|
|
9
|
+
export declare function canonicalProjectKey(project?: string): string | undefined;
|
|
6
10
|
export declare class ConfigManager {
|
|
7
|
-
static loadConfig(rootConfig: any): ReentryStatusConfig;
|
|
8
|
-
static mergeWithDefaults(partial:
|
|
11
|
+
static loadConfig(rootConfig: any, project?: string): ReentryStatusConfig;
|
|
12
|
+
static mergeWithDefaults(partial: DeepPartial<ReentryStatusConfig>, project?: string): ReentryStatusConfig;
|
|
9
13
|
static validateConfig(config: ReentryStatusConfig): ValidationResult;
|
|
10
14
|
static getSyncTargets(config: ReentryStatusConfig): SyncTarget[];
|
|
11
15
|
}
|
|
16
|
+
export {};
|
|
12
17
|
//# sourceMappingURL=config-manager.d.ts.map
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.ConfigManager = void 0;
|
|
37
|
+
exports.canonicalProjectKey = canonicalProjectKey;
|
|
37
38
|
const path = __importStar(require("path"));
|
|
38
39
|
const constants_1 = require("./constants");
|
|
39
40
|
function isNonEmptyString(value) {
|
|
@@ -45,12 +46,78 @@ function defaultFilesConfig() {
|
|
|
45
46
|
markdownPath: path.join(constants_1.REENTRY_STATUS_DIRNAME, constants_1.REENTRY_STATUS_MD_FILENAME)
|
|
46
47
|
};
|
|
47
48
|
}
|
|
49
|
+
function canonicalProjectKey(project) {
|
|
50
|
+
const raw = typeof project === 'string' ? project.trim() : '';
|
|
51
|
+
if (!raw)
|
|
52
|
+
return undefined;
|
|
53
|
+
// Prefer the last segment for scoped package names (e.g. "@ed/trader" -> "trader").
|
|
54
|
+
if (raw.startsWith('@') && raw.includes('/')) {
|
|
55
|
+
const parts = raw.split('/').filter(Boolean);
|
|
56
|
+
const last = parts[parts.length - 1]?.trim();
|
|
57
|
+
return last || undefined;
|
|
58
|
+
}
|
|
59
|
+
// Also handle path-like inputs (e.g. "apps/trader" -> "trader").
|
|
60
|
+
if (raw.includes('/')) {
|
|
61
|
+
const parts = raw.split('/').filter(Boolean);
|
|
62
|
+
const last = parts[parts.length - 1]?.trim();
|
|
63
|
+
return last || undefined;
|
|
64
|
+
}
|
|
65
|
+
return raw;
|
|
66
|
+
}
|
|
67
|
+
function toProjectDir(project) {
|
|
68
|
+
const raw = canonicalProjectKey(project) ?? '';
|
|
69
|
+
if (!raw)
|
|
70
|
+
return constants_1.REENTRY_STATUS_DIRNAME;
|
|
71
|
+
const safe = raw
|
|
72
|
+
.toLowerCase()
|
|
73
|
+
.replace(/[^a-z0-9._-]+/g, '-')
|
|
74
|
+
.replace(/^-+|-+$/g, '');
|
|
75
|
+
return path.join(constants_1.REENTRY_STATUS_DIRNAME, 'projects', safe || 'project');
|
|
76
|
+
}
|
|
77
|
+
function defaultFilesConfigForProject(project) {
|
|
78
|
+
const dir = toProjectDir(project);
|
|
79
|
+
return {
|
|
80
|
+
jsonPath: path.join(dir, constants_1.REENTRY_STATUS_JSON_FILENAME),
|
|
81
|
+
markdownPath: path.join(dir, constants_1.REENTRY_STATUS_MD_FILENAME),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
48
84
|
class ConfigManager {
|
|
49
|
-
static loadConfig(rootConfig) {
|
|
50
|
-
const
|
|
51
|
-
|
|
85
|
+
static loadConfig(rootConfig, project) {
|
|
86
|
+
const canonicalProject = canonicalProjectKey(project);
|
|
87
|
+
const raw = (rootConfig && typeof rootConfig === 'object' ? rootConfig.reentryStatus : undefined);
|
|
88
|
+
const basePartial = { ...(raw ?? {}) };
|
|
89
|
+
delete basePartial.projects;
|
|
90
|
+
const projectPartial = canonicalProject && raw && typeof raw === 'object' && raw.projects && typeof raw.projects === 'object'
|
|
91
|
+
? raw.projects[String(canonicalProject)] ?? raw.projects[String(project)]
|
|
92
|
+
: undefined;
|
|
93
|
+
if (!projectPartial) {
|
|
94
|
+
return ConfigManager.mergeWithDefaults(basePartial, canonicalProject);
|
|
95
|
+
}
|
|
96
|
+
const merged = {
|
|
97
|
+
...basePartial,
|
|
98
|
+
...projectPartial,
|
|
99
|
+
hooks: {
|
|
100
|
+
...basePartial.hooks,
|
|
101
|
+
...projectPartial.hooks,
|
|
102
|
+
},
|
|
103
|
+
files: {
|
|
104
|
+
...basePartial.files,
|
|
105
|
+
...projectPartial.files,
|
|
106
|
+
},
|
|
107
|
+
template: projectPartial.template
|
|
108
|
+
? {
|
|
109
|
+
includeSections: projectPartial.template.includeSections ?? basePartial.template?.includeSections ?? [],
|
|
110
|
+
excludeSections: projectPartial.template.excludeSections ?? basePartial.template?.excludeSections ?? [],
|
|
111
|
+
customSections: projectPartial.template.customSections ?? basePartial.template?.customSections,
|
|
112
|
+
}
|
|
113
|
+
: basePartial.template,
|
|
114
|
+
github: projectPartial.github ?? basePartial.github,
|
|
115
|
+
obsidian: projectPartial.obsidian ?? basePartial.obsidian,
|
|
116
|
+
};
|
|
117
|
+
return ConfigManager.mergeWithDefaults(merged, canonicalProject);
|
|
52
118
|
}
|
|
53
|
-
static mergeWithDefaults(partial) {
|
|
119
|
+
static mergeWithDefaults(partial, project) {
|
|
120
|
+
const defaults = defaultFilesConfigForProject(project);
|
|
54
121
|
const merged = {
|
|
55
122
|
enabled: partial.enabled ?? true,
|
|
56
123
|
autoSync: partial.autoSync ?? true,
|
|
@@ -60,8 +127,8 @@ class ConfigManager {
|
|
|
60
127
|
postRelease: partial.hooks?.postRelease ?? false
|
|
61
128
|
},
|
|
62
129
|
files: {
|
|
63
|
-
jsonPath: partial.files?.jsonPath ??
|
|
64
|
-
markdownPath: partial.files?.markdownPath ??
|
|
130
|
+
jsonPath: partial.files?.jsonPath ?? defaults.jsonPath,
|
|
131
|
+
markdownPath: partial.files?.markdownPath ?? defaults.markdownPath
|
|
65
132
|
},
|
|
66
133
|
github: partial.github,
|
|
67
134
|
obsidian: partial.obsidian,
|
|
@@ -5,7 +5,7 @@ export interface RoadmapRenderOptions {
|
|
|
5
5
|
projectTitle?: string;
|
|
6
6
|
}
|
|
7
7
|
export declare class RoadmapRenderer {
|
|
8
|
-
static defaultRoadmapPath(): string;
|
|
8
|
+
static defaultRoadmapPath(baseDir?: string): string;
|
|
9
9
|
static renderManagedBlock(status?: Pick<ReentryStatus, 'milestone' | 'roadmapFile'>): string;
|
|
10
10
|
static renderTemplate(options?: RoadmapRenderOptions, status?: Pick<ReentryStatus, 'milestone' | 'roadmapFile'>): string;
|
|
11
11
|
/**
|
|
@@ -5,8 +5,8 @@ const constants_1 = require("./constants");
|
|
|
5
5
|
exports.ROADMAP_MANAGED_START = '<!-- roadmap:managed:start -->';
|
|
6
6
|
exports.ROADMAP_MANAGED_END = '<!-- roadmap:managed:end -->';
|
|
7
7
|
class RoadmapRenderer {
|
|
8
|
-
static defaultRoadmapPath() {
|
|
9
|
-
return `${
|
|
8
|
+
static defaultRoadmapPath(baseDir = constants_1.REENTRY_STATUS_DIRNAME) {
|
|
9
|
+
return `${baseDir}/${constants_1.ROADMAP_MD_FILENAME}`;
|
|
10
10
|
}
|
|
11
11
|
static renderManagedBlock(status) {
|
|
12
12
|
const milestoneText = status?.milestone
|
|
@@ -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");
|
|
@@ -50,7 +51,7 @@ const reentry_status_manager_1 = require("./reentry-status/reentry-status-manage
|
|
|
50
51
|
const extension = {
|
|
51
52
|
name: constants_1.REENTRY_EXTENSION_NAME,
|
|
52
53
|
description: 'Maintains canonical re-entry status and synchronizes to files, GitHub Issues, and Obsidian notes',
|
|
53
|
-
version: '1.1.
|
|
54
|
+
version: '1.1.2',
|
|
54
55
|
hooks: {
|
|
55
56
|
postVersion: async (type, version, options) => {
|
|
56
57
|
try {
|
|
@@ -107,16 +108,103 @@ const extension = {
|
|
|
107
108
|
register: async (program, rootConfig) => {
|
|
108
109
|
const fileManager = new file_manager_1.FileManager();
|
|
109
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
|
+
};
|
|
110
188
|
const loadRootConfigFile = async (configPath) => {
|
|
111
189
|
if (!(await fs.pathExists(configPath))) {
|
|
112
190
|
throw new Error(`Config file not found: ${configPath}. Run 'versioning init' to create one.`);
|
|
113
191
|
}
|
|
114
192
|
return await fs.readJson(configPath);
|
|
115
193
|
};
|
|
116
|
-
const ensureReentryInitialized = async (configPath, migrate) => {
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
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);
|
|
120
208
|
const existingJson = await fileManager.readFileIfExists(reentryCfg.files.jsonPath);
|
|
121
209
|
if (existingJson) {
|
|
122
210
|
const parsed = status_renderer_1.StatusRenderer.parseJson(existingJson);
|
|
@@ -126,12 +214,17 @@ const extension = {
|
|
|
126
214
|
...parsed,
|
|
127
215
|
schemaVersion: '1.1',
|
|
128
216
|
milestone: parsed.milestone ?? null,
|
|
129
|
-
roadmapFile:
|
|
217
|
+
roadmapFile: defaultRoadmapPath
|
|
130
218
|
};
|
|
131
219
|
await fileManager.writeStatusJson(cfg, migrated);
|
|
132
|
-
return migrated;
|
|
220
|
+
return { cfg, status: migrated };
|
|
133
221
|
}
|
|
134
|
-
|
|
222
|
+
const normalized = {
|
|
223
|
+
...parsed,
|
|
224
|
+
schemaVersion: '1.1',
|
|
225
|
+
roadmapFile: parsed.roadmapFile || defaultRoadmapPath,
|
|
226
|
+
};
|
|
227
|
+
return { cfg, status: normalized };
|
|
135
228
|
}
|
|
136
229
|
const initial = {
|
|
137
230
|
schemaVersion: '1.1',
|
|
@@ -144,7 +237,7 @@ const extension = {
|
|
|
144
237
|
versioningInfo: {}
|
|
145
238
|
},
|
|
146
239
|
milestone: null,
|
|
147
|
-
roadmapFile:
|
|
240
|
+
roadmapFile: defaultRoadmapPath,
|
|
148
241
|
currentPhase: 'planning',
|
|
149
242
|
milestones: [],
|
|
150
243
|
blockers: [],
|
|
@@ -162,7 +255,7 @@ const extension = {
|
|
|
162
255
|
}
|
|
163
256
|
};
|
|
164
257
|
await fileManager.writeStatusFiles(cfg, initial);
|
|
165
|
-
return initial;
|
|
258
|
+
return { cfg, status: initial };
|
|
166
259
|
};
|
|
167
260
|
program
|
|
168
261
|
.command('reentry')
|
|
@@ -170,9 +263,10 @@ const extension = {
|
|
|
170
263
|
.addCommand(new commander_1.Command('init')
|
|
171
264
|
.description('Initialize re-entry status files')
|
|
172
265
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
266
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
173
267
|
.option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
|
|
174
268
|
.action(async (options) => {
|
|
175
|
-
const status = await ensureReentryInitialized(options.config, Boolean(options.migrate));
|
|
269
|
+
const { status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
|
|
176
270
|
console.log(`✅ Initialized re-entry status (schema ${status.schemaVersion})`);
|
|
177
271
|
}))
|
|
178
272
|
.addCommand(new commander_1.Command('set')
|
|
@@ -180,10 +274,10 @@ const extension = {
|
|
|
180
274
|
.option('--phase <phase>', 'Set current phase')
|
|
181
275
|
.option('--next <text>', 'Set next micro-step (replaces first nextSteps entry)')
|
|
182
276
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
277
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
183
278
|
.option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
|
|
184
279
|
.action(async (options) => {
|
|
185
|
-
const cfg = await
|
|
186
|
-
const status = await ensureReentryInitialized(options.config, Boolean(options.migrate));
|
|
280
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
|
|
187
281
|
const nextStepText = typeof options.next === 'string' ? options.next.trim() : '';
|
|
188
282
|
const phase = typeof options.phase === 'string' ? options.phase.trim() : '';
|
|
189
283
|
const updated = {
|
|
@@ -201,11 +295,11 @@ const extension = {
|
|
|
201
295
|
.addCommand(new commander_1.Command('sync')
|
|
202
296
|
.description('Ensure generated status files exist and are up to date (idempotent)')
|
|
203
297
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
298
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
204
299
|
.option('--migrate', 'rewrite v1.0 schema to v1.1 (no semantic changes)', false)
|
|
205
300
|
.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));
|
|
301
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, Boolean(options.migrate), options.project);
|
|
302
|
+
const reentryCfg = config_manager_1.ConfigManager.loadConfig(cfg, options.project);
|
|
209
303
|
// Ensure ROADMAP exists (light touch: only managed block is updated).
|
|
210
304
|
const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
|
|
211
305
|
const existing = await fileManager.readFileIfExists(roadmapPath);
|
|
@@ -296,14 +390,19 @@ const extension = {
|
|
|
296
390
|
.addCommand(new commander_1.Command('init')
|
|
297
391
|
.description(`Create ${constants_1.REENTRY_STATUS_DIRNAME}/${constants_1.ROADMAP_MD_FILENAME} if missing and ensure managed header block`)
|
|
298
392
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
393
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
299
394
|
.option('-t, --title <title>', 'project title for ROADMAP.md template', 'Untitled')
|
|
300
395
|
.action(async (options) => {
|
|
301
|
-
const
|
|
302
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
396
|
+
const projectKey = await validateProjectOption(options.config, options.project);
|
|
397
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, false, projectKey);
|
|
303
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);
|
|
304
403
|
const existing = await fileManager.readFileIfExists(roadmapPath);
|
|
305
404
|
if (!existing) {
|
|
306
|
-
await fileManager.writeFileIfChanged(roadmapPath, roadmap_renderer_1.RoadmapRenderer.renderTemplate({ projectTitle:
|
|
405
|
+
await fileManager.writeFileIfChanged(roadmapPath, roadmap_renderer_1.RoadmapRenderer.renderTemplate({ projectTitle: title }, { milestone: status.milestone, roadmapFile: roadmapPath }));
|
|
307
406
|
console.log(`✅ Created ${roadmapPath}`);
|
|
308
407
|
return;
|
|
309
408
|
}
|
|
@@ -317,12 +416,40 @@ const extension = {
|
|
|
317
416
|
}
|
|
318
417
|
// Keep REENTRY.md consistent with roadmap references.
|
|
319
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;
|
|
320
446
|
}))
|
|
321
447
|
.addCommand(new commander_1.Command('list')
|
|
322
448
|
.description('List roadmap milestones parsed from ROADMAP.md')
|
|
323
449
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
450
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
324
451
|
.action(async (options) => {
|
|
325
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
452
|
+
const { status } = await ensureReentryInitialized(options.config, false, options.project);
|
|
326
453
|
const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
|
|
327
454
|
const content = await fileManager.readFileIfExists(roadmapPath);
|
|
328
455
|
if (!content) {
|
|
@@ -346,9 +473,9 @@ const extension = {
|
|
|
346
473
|
.requiredOption('--id <id>', 'Milestone id (must match a [id] in ROADMAP.md)')
|
|
347
474
|
.requiredOption('--title <title>', 'Milestone title')
|
|
348
475
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
476
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
349
477
|
.action(async (options) => {
|
|
350
|
-
const cfg = await
|
|
351
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
478
|
+
const { cfg, status } = await ensureReentryInitialized(options.config, false, options.project);
|
|
352
479
|
const next = {
|
|
353
480
|
...status,
|
|
354
481
|
schemaVersion: '1.1',
|
|
@@ -365,8 +492,9 @@ const extension = {
|
|
|
365
492
|
.requiredOption('--item <item>', 'Item text')
|
|
366
493
|
.option('--id <id>', 'Optional explicit id (e.g., now-02)')
|
|
367
494
|
.option('-c, --config <file>', 'config file path', 'versioning.config.json')
|
|
495
|
+
.option('-p, --project <name>', 'project scope (separate ROADMAP/REENTRY/status per project)')
|
|
368
496
|
.action(async (options) => {
|
|
369
|
-
const status = await ensureReentryInitialized(options.config, false);
|
|
497
|
+
const { status } = await ensureReentryInitialized(options.config, false, options.project);
|
|
370
498
|
const roadmapPath = status.roadmapFile || roadmap_renderer_1.RoadmapRenderer.defaultRoadmapPath();
|
|
371
499
|
const content = await fileManager.readFileIfExists(roadmapPath);
|
|
372
500
|
if (!content) {
|