roadmapsmith 0.9.14 → 0.9.16
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 +71 -16
- package/bin/cli.js +340 -93
- package/package.json +2 -2
- package/src/config.js +33 -2
- package/src/generator/index.js +31 -4
- package/src/host.js +982 -0
- package/src/index.js +3 -0
- package/src/slash.js +226 -0
- package/src/zero.js +129 -0
package/bin/cli.js
CHANGED
|
@@ -3,23 +3,34 @@
|
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const path = require('path');
|
|
6
|
+
const readline = require('node:readline/promises');
|
|
6
7
|
const { parseArgv } = require('../src/utils');
|
|
7
|
-
const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins } = require('../src/config');
|
|
8
|
+
const { loadConfig, resolveRoadmapFile, resolveAgentsFile, loadPlugins, readUserConfig, resolveConfigPath } = require('../src/config');
|
|
8
9
|
const { readTextIfExists, writeText, printDryRunDiff } = require('../src/io');
|
|
10
|
+
const { buildSetupFiles, applySetupFiles, inspectHostSetup, parseHosts, assertSupportedEditor } = require('../src/host');
|
|
11
|
+
const { getSlashAction, renderSlashPalette, resolveSlashInvocation } = require('../src/slash');
|
|
9
12
|
const { renderRoadmapTemplate, renderAgentsTemplate } = require('../src/templates');
|
|
10
13
|
const { generateRoadmapDocument } = require('../src/generator');
|
|
11
14
|
const { parseRoadmap } = require('../src/parser');
|
|
12
15
|
const { buildValidationContext, validateTasks, auditValidation, CONFIDENCE_RANK, applyMinimumConfidence } = require('../src/validator');
|
|
13
16
|
const { applySync } = require('../src/sync');
|
|
17
|
+
const { buildZeroModeConfigPatch, buildZeroModeDefaults, collectZeroModeAnswers, isInteractiveTerminal } = require('../src/zero');
|
|
14
18
|
|
|
15
19
|
function printHelp() {
|
|
16
20
|
console.log([
|
|
17
21
|
'Usage:',
|
|
22
|
+
' roadmapsmith zero [--project-root <path>] [--config <path>]',
|
|
23
|
+
' roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file <path>]',
|
|
24
|
+
' roadmapsmith /road',
|
|
25
|
+
' roadmapsmith /road <action>',
|
|
26
|
+
' roadmapsmith /roadmap-sync <action>',
|
|
27
|
+
' roadmapsmith /zero | /maintain | /status | /init | /generate | /validate | /sync | /audit | /setup',
|
|
18
28
|
' roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]',
|
|
29
|
+
' roadmapsmith setup [--project-root <path>] [--config <path>] [--editor vscode] [--hosts <codex,claude>] [--dry-run]',
|
|
19
30
|
' roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]',
|
|
20
31
|
' roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]',
|
|
21
32
|
' roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]',
|
|
22
|
-
' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>]'
|
|
33
|
+
' roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]'
|
|
23
34
|
].join('\n'));
|
|
24
35
|
}
|
|
25
36
|
|
|
@@ -68,10 +79,194 @@ function printAudit(audit) {
|
|
|
68
79
|
}
|
|
69
80
|
}
|
|
70
81
|
|
|
82
|
+
function formatSetupVerb(result, dryRun) {
|
|
83
|
+
if (dryRun) {
|
|
84
|
+
return result.before == null ? 'Would create' : 'Would update';
|
|
85
|
+
}
|
|
86
|
+
return result.before == null ? 'Created' : 'Updated';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function runInitCommand(projectRoot, config, flags) {
|
|
90
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
91
|
+
const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
|
|
92
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
93
|
+
|
|
94
|
+
const roadmapExists = fs.existsSync(roadmapFile);
|
|
95
|
+
const agentsExists = fs.existsSync(agentsFile);
|
|
96
|
+
|
|
97
|
+
if (!roadmapExists) {
|
|
98
|
+
const roadmap = renderRoadmapTemplate();
|
|
99
|
+
const result = writeText(roadmapFile, roadmap, { dryRun });
|
|
100
|
+
if (dryRun && result.changed) {
|
|
101
|
+
printDryRunDiff(roadmapFile, result.before, result.after);
|
|
102
|
+
}
|
|
103
|
+
console.log(`${dryRun ? 'Would create' : 'Created'} ${roadmapFile}`);
|
|
104
|
+
} else {
|
|
105
|
+
console.log(`Skipped existing ${roadmapFile}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!agentsExists) {
|
|
109
|
+
const agents = renderAgentsTemplate({ roadmapPath: path.basename(roadmapFile) });
|
|
110
|
+
const result = writeText(agentsFile, agents, { dryRun });
|
|
111
|
+
if (dryRun && result.changed) {
|
|
112
|
+
printDryRunDiff(agentsFile, result.before, result.after);
|
|
113
|
+
}
|
|
114
|
+
console.log(`${dryRun ? 'Would create' : 'Created'} ${agentsFile}`);
|
|
115
|
+
} else {
|
|
116
|
+
console.log(`Skipped existing ${agentsFile}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function runGenerateCommand(projectRoot, config, flags, options = {}) {
|
|
121
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
122
|
+
const plugins = loadPlugins(projectRoot, config.plugins);
|
|
123
|
+
const existingContent = readTextIfExists(roadmapFile) || '';
|
|
124
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
125
|
+
|
|
126
|
+
const document = generateRoadmapDocument({
|
|
127
|
+
projectRoot,
|
|
128
|
+
roadmapPath: roadmapFile,
|
|
129
|
+
existingContent,
|
|
130
|
+
config,
|
|
131
|
+
plugins
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const writeResult = writeText(roadmapFile, document, { dryRun });
|
|
135
|
+
if (dryRun) {
|
|
136
|
+
if (writeResult.changed) {
|
|
137
|
+
printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
|
|
138
|
+
} else {
|
|
139
|
+
console.log(`No changes for ${roadmapFile}`);
|
|
140
|
+
}
|
|
141
|
+
} else {
|
|
142
|
+
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (options.audit || isEnabled(flags.audit)) {
|
|
146
|
+
const parsedRoadmap = parseRoadmap(document);
|
|
147
|
+
const validationContext = buildValidationContext(projectRoot, config, plugins);
|
|
148
|
+
const results = validateTasks(parsedRoadmap.tasks, validationContext, config, plugins);
|
|
149
|
+
const audit = auditValidation(parsedRoadmap.tasks, results);
|
|
150
|
+
printAudit(audit);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function runSyncCommand(projectRoot, config, flags, options = {}) {
|
|
155
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
156
|
+
const content = readTextIfExists(roadmapFile);
|
|
157
|
+
if (content == null) {
|
|
158
|
+
throw new Error(`Roadmap not found: ${roadmapFile}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const parsedRoadmap = parseRoadmap(content);
|
|
162
|
+
const syncTasks = tasksInManagedBlock(parsedRoadmap);
|
|
163
|
+
const validationContext = buildValidationContext(projectRoot, config, loadPlugins(projectRoot, config.plugins));
|
|
164
|
+
const results = validateTasks(syncTasks, validationContext, config, validationContext.plugins);
|
|
165
|
+
applyMinimumConfidence(results, config.validation?.minimumConfidence);
|
|
166
|
+
const next = applySync(content, syncTasks, results);
|
|
167
|
+
const dryRun = isEnabled(flags['dry-run']);
|
|
168
|
+
const writeResult = writeText(roadmapFile, next, { dryRun });
|
|
169
|
+
|
|
170
|
+
if (dryRun) {
|
|
171
|
+
if (writeResult.changed) {
|
|
172
|
+
printDryRunDiff(roadmapFile, writeResult.before, writeResult.after);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(`No changes for ${roadmapFile}`);
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (options.audit || isEnabled(flags.audit)) {
|
|
181
|
+
const audit = auditValidation(syncTasks, results);
|
|
182
|
+
printAudit(audit);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function printHumanStatus(payload) {
|
|
187
|
+
console.log('RoadmapSmith status\n');
|
|
188
|
+
console.log(`Project root: ${payload.projectRoot}`);
|
|
189
|
+
console.log(`CLI resolution: ${payload.cli.kind}${payload.cli.path ? ` (${payload.cli.path})` : ''}${payload.cli.ready ? '' : ' [missing]'}`);
|
|
190
|
+
console.log(`Roadmap file: ${payload.roadmap.exists ? 'ready' : 'missing'} (${payload.roadmap.path})`);
|
|
191
|
+
console.log(`Agent rules: ${payload.agents.exists ? 'ready' : 'missing'} (${payload.agents.path})`);
|
|
192
|
+
console.log(`VS Code launcher: ${payload.vscode.launcher.exists ? 'ready' : 'missing'} (${payload.vscode.launcher.path})`);
|
|
193
|
+
console.log(`VS Code task wrappers: ${payload.vscode.wrappers.ready ? 'ready' : 'incomplete'} (${payload.vscode.wrappers.presentCount}/${payload.vscode.wrappers.expectedCount} files)`);
|
|
194
|
+
console.log(`VS Code tasks: ${payload.vscode.tasks.ready ? 'ready' : 'incomplete'} (${payload.vscode.tasks.presentLabels.length}/${payload.vscode.tasks.expectedLabels.length} tasks)`);
|
|
195
|
+
console.log(`Node runtime: ${payload.runtime.ready ? `ready (${payload.runtime.kind}${payload.runtime.path ? `: ${payload.runtime.path}` : ''})` : 'missing'}`);
|
|
196
|
+
if (!payload.vscode.tasks.ready && payload.vscode.tasks.missingLabels.length > 0) {
|
|
197
|
+
console.log(`Missing VS Code tasks: ${payload.vscode.tasks.missingLabels.join(', ')}`);
|
|
198
|
+
}
|
|
199
|
+
if (!payload.vscode.wrappers.ready) {
|
|
200
|
+
console.log(`Missing task wrapper files: ${payload.vscode.wrappers.missingPaths.join(', ')}`);
|
|
201
|
+
}
|
|
202
|
+
console.log(`Codex readiness: ${payload.hosts.codex.ready ? 'ready' : 'needs setup'} (${payload.hosts.codex.message})`);
|
|
203
|
+
console.log(`Claude readiness: ${payload.hosts.claude.ready ? 'ready' : 'needs setup'} (${payload.hosts.claude.message})`);
|
|
204
|
+
console.log('\nRecommended entrypoints: roadmapsmith zero (empty repo), roadmapsmith maintain (existing repo).');
|
|
205
|
+
if (!payload.cli.ready) {
|
|
206
|
+
console.log('\nInstalling the skill alone does not expose the CLI in VS Code. Install the CLI and rerun roadmapsmith setup.');
|
|
207
|
+
}
|
|
208
|
+
if (!payload.runtime.ready) {
|
|
209
|
+
console.log('\nThe VS Code task runtime is missing. Install Node.js or set ROADMAPSMITH_NODE, then rerun RoadmapSmith: Status.');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function runStatusCommand(projectRoot, config, flags, options = {}) {
|
|
214
|
+
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
215
|
+
const agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
|
|
216
|
+
const payload = inspectHostSetup(projectRoot, { roadmapFile, agentsFile });
|
|
217
|
+
|
|
218
|
+
if (options.json) {
|
|
219
|
+
process.stdout.write(JSON.stringify(payload, null, 2) + '\n');
|
|
220
|
+
} else {
|
|
221
|
+
printHumanStatus(payload);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const ready = payload.cli.ready && payload.roadmap.exists && payload.agents.exists && payload.vscode.tasks.ready && payload.runtime.ready && payload.claude.ready;
|
|
225
|
+
if (!ready) {
|
|
226
|
+
process.exitCode = 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function runZeroCommand(projectRoot, flags) {
|
|
231
|
+
const configPath = resolveConfigPath({ projectRoot, configPath: flags.config });
|
|
232
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
233
|
+
if (!isInteractiveTerminal(process.stdin, process.stdout)) {
|
|
234
|
+
throw new Error('Zero Mode requires an interactive terminal. Run roadmapsmith zero from a terminal session, or add a config/brief workflow before retrying in non-interactive mode.');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const defaults = buildZeroModeDefaults(projectRoot, config);
|
|
238
|
+
const rl = readline.createInterface({
|
|
239
|
+
input: process.stdin,
|
|
240
|
+
output: process.stdout
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
console.log('RoadmapSmith Zero Mode');
|
|
245
|
+
console.log('Answer the discovery interview to generate the first roadmap.\n');
|
|
246
|
+
const answers = await collectZeroModeAnswers((prompt) => rl.question(prompt), defaults);
|
|
247
|
+
const existingUserConfig = readUserConfig({ projectRoot, configPath: flags.config });
|
|
248
|
+
const nextUserConfig = buildZeroModeConfigPatch(projectRoot, existingUserConfig, answers);
|
|
249
|
+
writeText(configPath, JSON.stringify(nextUserConfig, null, 2));
|
|
250
|
+
console.log(`Updated ${configPath}`);
|
|
251
|
+
const nextConfig = loadConfig({ projectRoot, configPath: flags.config });
|
|
252
|
+
runInitCommand(projectRoot, nextConfig, flags);
|
|
253
|
+
runGenerateCommand(projectRoot, nextConfig, flags);
|
|
254
|
+
} finally {
|
|
255
|
+
rl.close();
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function runMaintainCommand(projectRoot, flags) {
|
|
260
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
261
|
+
runGenerateCommand(projectRoot, config, flags);
|
|
262
|
+
runSyncCommand(projectRoot, config, { ...flags, audit: true }, { audit: true });
|
|
263
|
+
}
|
|
264
|
+
|
|
71
265
|
async function run() {
|
|
72
266
|
const parsed = parseArgv(process.argv.slice(2));
|
|
73
267
|
const command = parsed.command;
|
|
74
268
|
const flags = parsed.flags;
|
|
269
|
+
let effectiveCommand = command;
|
|
75
270
|
|
|
76
271
|
if (isEnabled(flags.version) || isEnabled(flags.v)) {
|
|
77
272
|
const pkg = require(path.join(__dirname, '..', 'package.json'));
|
|
@@ -84,113 +279,91 @@ async function run() {
|
|
|
84
279
|
return;
|
|
85
280
|
}
|
|
86
281
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
282
|
+
const slashInvocation = resolveSlashInvocation(command, parsed.args);
|
|
283
|
+
if (slashInvocation) {
|
|
284
|
+
if (slashInvocation.kind === 'palette') {
|
|
285
|
+
process.stdout.write(renderSlashPalette(slashInvocation) + '\n');
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
93
288
|
|
|
94
|
-
const
|
|
95
|
-
|
|
289
|
+
const slashAction = getSlashAction(slashInvocation.actionId);
|
|
290
|
+
if (!slashAction) {
|
|
291
|
+
process.stdout.write(renderSlashPalette(slashInvocation) + '\n');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
96
294
|
|
|
97
|
-
if (
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
console.log(`${dryRun ? 'Would create' : 'Created'} ${roadmapFile}`);
|
|
104
|
-
} else {
|
|
105
|
-
console.log(`Skipped existing ${roadmapFile}`);
|
|
295
|
+
if (slashAction.id === 'status') {
|
|
296
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
297
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
298
|
+
runStatusCommand(projectRoot, config, flags, { json: isEnabled(flags.json) });
|
|
299
|
+
return;
|
|
106
300
|
}
|
|
107
301
|
|
|
108
|
-
if (
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (dryRun && result.changed) {
|
|
112
|
-
printDryRunDiff(agentsFile, result.before, result.after);
|
|
113
|
-
}
|
|
114
|
-
console.log(`${dryRun ? 'Would create' : 'Created'} ${agentsFile}`);
|
|
302
|
+
if (slashAction.id === 'audit') {
|
|
303
|
+
flags.audit = true;
|
|
304
|
+
effectiveCommand = 'sync';
|
|
115
305
|
} else {
|
|
116
|
-
|
|
306
|
+
effectiveCommand = slashAction.id;
|
|
117
307
|
}
|
|
118
|
-
return;
|
|
119
308
|
}
|
|
120
309
|
|
|
121
|
-
if (
|
|
310
|
+
if (effectiveCommand === 'zero') {
|
|
122
311
|
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
const existingContent = readTextIfExists(roadmapFile) || '';
|
|
127
|
-
const dryRun = isEnabled(flags['dry-run']);
|
|
128
|
-
|
|
129
|
-
const document = generateRoadmapDocument({
|
|
130
|
-
projectRoot,
|
|
131
|
-
roadmapPath: roadmapFile,
|
|
132
|
-
existingContent,
|
|
133
|
-
config,
|
|
134
|
-
plugins
|
|
135
|
-
});
|
|
312
|
+
await runZeroCommand(projectRoot, flags);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
136
315
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
console.log(`No changes for ${roadmapFile}`);
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
console.log(writeResult.changed ? `Updated ${roadmapFile}` : `No changes for ${roadmapFile}`);
|
|
146
|
-
}
|
|
316
|
+
if (effectiveCommand === 'maintain') {
|
|
317
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
318
|
+
runMaintainCommand(projectRoot, flags);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
147
321
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
const audit = auditValidation(parsedRoadmap.tasks, results);
|
|
153
|
-
printAudit(audit);
|
|
154
|
-
}
|
|
322
|
+
if (effectiveCommand === 'init') {
|
|
323
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
324
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
325
|
+
runInitCommand(projectRoot, config, flags);
|
|
155
326
|
return;
|
|
156
327
|
}
|
|
157
328
|
|
|
158
|
-
if (
|
|
329
|
+
if (effectiveCommand === 'generate') {
|
|
159
330
|
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
160
331
|
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
throw new Error(`Roadmap not found: ${roadmapFile}`);
|
|
165
|
-
}
|
|
332
|
+
runGenerateCommand(projectRoot, config, flags);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
166
335
|
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
const next = applySync(content, syncTasks, results);
|
|
336
|
+
if (effectiveCommand === 'setup') {
|
|
337
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
338
|
+
loadConfig({ projectRoot, configPath: flags.config });
|
|
339
|
+
const editor = assertSupportedEditor(flags.editor || 'vscode');
|
|
340
|
+
const hosts = parseHosts(flags.hosts || 'codex,claude');
|
|
173
341
|
const dryRun = isEnabled(flags['dry-run']);
|
|
174
|
-
const
|
|
342
|
+
const setupPlan = buildSetupFiles(projectRoot, { editor, hosts });
|
|
343
|
+
const results = applySetupFiles(setupPlan, { dryRun });
|
|
175
344
|
|
|
176
|
-
|
|
177
|
-
if (
|
|
178
|
-
printDryRunDiff(
|
|
179
|
-
} else {
|
|
180
|
-
console.log(`No changes for ${roadmapFile}`);
|
|
345
|
+
results.forEach((result) => {
|
|
346
|
+
if (dryRun && result.changed) {
|
|
347
|
+
printDryRunDiff(result.path, result.before, result.after);
|
|
181
348
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
349
|
+
if (!result.changed) {
|
|
350
|
+
console.log(`No changes for ${result.path}`);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
console.log(`${formatSetupVerb(result, dryRun)} ${result.path}`);
|
|
354
|
+
});
|
|
185
355
|
|
|
186
|
-
if (isEnabled(flags.audit)) {
|
|
187
|
-
const audit = auditValidation(syncTasks, results);
|
|
188
|
-
printAudit(audit);
|
|
189
|
-
}
|
|
190
356
|
return;
|
|
191
357
|
}
|
|
192
358
|
|
|
193
|
-
if (
|
|
359
|
+
if (effectiveCommand === 'sync') {
|
|
360
|
+
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
361
|
+
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
362
|
+
runSyncCommand(projectRoot, config, flags);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (effectiveCommand === 'validate') {
|
|
194
367
|
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
195
368
|
const config = loadConfig({ projectRoot, configPath: flags.config });
|
|
196
369
|
const roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
@@ -223,38 +396,112 @@ async function run() {
|
|
|
223
396
|
return;
|
|
224
397
|
}
|
|
225
398
|
|
|
226
|
-
if (
|
|
399
|
+
if (effectiveCommand === 'doctor') {
|
|
227
400
|
const projectRoot = path.resolve(String(flags['project-root'] || process.cwd()));
|
|
228
401
|
let ok = true;
|
|
402
|
+
const jsonMode = isEnabled(flags.json);
|
|
403
|
+
const log = jsonMode ? () => {} : console.log;
|
|
404
|
+
const logError = jsonMode ? () => {} : console.error;
|
|
229
405
|
|
|
230
406
|
let config;
|
|
407
|
+
let roadmapFile = null;
|
|
408
|
+
let agentsFile = null;
|
|
231
409
|
try {
|
|
232
410
|
config = loadConfig({ projectRoot, configPath: flags.config });
|
|
233
|
-
|
|
411
|
+
log('[ok] Config loaded without errors');
|
|
234
412
|
} catch (error) {
|
|
235
|
-
|
|
413
|
+
logError(`[fail] Config error: ${error.message}`);
|
|
236
414
|
ok = false;
|
|
237
415
|
}
|
|
238
416
|
|
|
239
417
|
if (config) {
|
|
240
|
-
|
|
418
|
+
roadmapFile = resolveRoadmapFile(projectRoot, config, flags['roadmap-file']);
|
|
241
419
|
if (fs.existsSync(roadmapFile)) {
|
|
242
|
-
|
|
420
|
+
log(`[ok] ROADMAP file found: ${roadmapFile}`);
|
|
243
421
|
} else {
|
|
244
|
-
|
|
422
|
+
logError(`[fail] ROADMAP file not found: ${roadmapFile}`);
|
|
245
423
|
ok = false;
|
|
246
424
|
}
|
|
425
|
+
|
|
426
|
+
agentsFile = resolveAgentsFile(projectRoot, config, flags['agents-file']);
|
|
427
|
+
if (fs.existsSync(agentsFile)) {
|
|
428
|
+
log(`[ok] Agent rules file found: ${agentsFile}`);
|
|
429
|
+
} else {
|
|
430
|
+
logError(`[fail] Agent rules file not found: ${agentsFile}`);
|
|
431
|
+
ok = false;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
let hostStatus = null;
|
|
436
|
+
if (config) {
|
|
437
|
+
try {
|
|
438
|
+
hostStatus = inspectHostSetup(projectRoot, { roadmapFile, agentsFile });
|
|
439
|
+
} catch (error) {
|
|
440
|
+
logError(`[fail] Host integration error: ${error.message}`);
|
|
441
|
+
ok = false;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (hostStatus) {
|
|
446
|
+
if (hostStatus.cli.ready) {
|
|
447
|
+
log(`[ok] CLI resolution: ${hostStatus.cli.kind}${hostStatus.cli.path ? ` (${hostStatus.cli.path})` : ''}`);
|
|
448
|
+
} else {
|
|
449
|
+
logError('[fail] CLI resolution: missing local package and global command');
|
|
450
|
+
ok = false;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (hostStatus.vscode.launcher.exists) {
|
|
454
|
+
log(`[ok] VS Code launcher found: ${hostStatus.vscode.launcher.path}`);
|
|
455
|
+
} else {
|
|
456
|
+
logError(`[fail] VS Code launcher missing: ${hostStatus.vscode.launcher.path}`);
|
|
457
|
+
ok = false;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (hostStatus.vscode.wrappers.ready) {
|
|
461
|
+
log(`[ok] VS Code task wrappers ready: ${hostStatus.vscode.wrappers.presentCount}/${hostStatus.vscode.wrappers.expectedCount} files`);
|
|
462
|
+
} else {
|
|
463
|
+
logError(`[fail] VS Code task wrappers incomplete: missing ${hostStatus.vscode.wrappers.missingPaths.join(', ') || 'wrapper files'}`);
|
|
464
|
+
ok = false;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (hostStatus.vscode.tasks.ready) {
|
|
468
|
+
log(`[ok] VS Code tasks ready: ${hostStatus.vscode.tasks.presentLabels.length}/${hostStatus.vscode.tasks.expectedLabels.length} tasks`);
|
|
469
|
+
} else {
|
|
470
|
+
logError(`[fail] VS Code tasks incomplete: missing ${hostStatus.vscode.tasks.missingLabels.join(', ') || 'managed labels'}`);
|
|
471
|
+
ok = false;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (hostStatus.runtime.ready) {
|
|
475
|
+
log(`[ok] Node runtime: ${hostStatus.runtime.kind}${hostStatus.runtime.path ? ` (${hostStatus.runtime.path})` : ''}`);
|
|
476
|
+
} else {
|
|
477
|
+
logError('[fail] Node runtime missing for VS Code task execution');
|
|
478
|
+
ok = false;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (hostStatus.claude.ready) {
|
|
482
|
+
log(`[ok] Claude hook ready: ${hostStatus.claude.hookFile.path}`);
|
|
483
|
+
} else {
|
|
484
|
+
logError(`[fail] Claude hook incomplete: ${hostStatus.hosts.claude.message}`);
|
|
485
|
+
ok = false;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (jsonMode) {
|
|
490
|
+
process.stdout.write(JSON.stringify(hostStatus || {
|
|
491
|
+
projectRoot,
|
|
492
|
+
error: 'doctor failed before host inspection'
|
|
493
|
+
}, null, 2) + '\n');
|
|
247
494
|
}
|
|
248
495
|
|
|
249
496
|
if (!ok) {
|
|
250
497
|
process.exitCode = 1;
|
|
251
498
|
return;
|
|
252
499
|
}
|
|
253
|
-
|
|
500
|
+
log('doctor: all checks passed');
|
|
254
501
|
return;
|
|
255
502
|
}
|
|
256
503
|
|
|
257
|
-
throw new Error(`Unknown command: ${
|
|
504
|
+
throw new Error(`Unknown command: ${effectiveCommand}`);
|
|
258
505
|
}
|
|
259
506
|
|
|
260
507
|
run().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "roadmapsmith",
|
|
3
|
-
"version": "0.9.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.9.16",
|
|
4
|
+
"description": "One-command evidence-backed ROADMAP.md generator and sync tool for AI coding agents.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"roadmapsmith": "bin/cli.js"
|
package/src/config.js
CHANGED
|
@@ -24,6 +24,12 @@ const DEFAULT_CONFIG = {
|
|
|
24
24
|
steps: [],
|
|
25
25
|
phases: []
|
|
26
26
|
},
|
|
27
|
+
zeroMode: {
|
|
28
|
+
problemStatement: '',
|
|
29
|
+
preferredStack: '',
|
|
30
|
+
constraints: [],
|
|
31
|
+
doneCriteria: []
|
|
32
|
+
},
|
|
27
33
|
validation: {
|
|
28
34
|
minimumConfidence: 'low'
|
|
29
35
|
},
|
|
@@ -77,6 +83,16 @@ function mergeConfig(userConfig) {
|
|
|
77
83
|
? userConfig.product.phases
|
|
78
84
|
: DEFAULT_CONFIG.product.phases
|
|
79
85
|
},
|
|
86
|
+
zeroMode: {
|
|
87
|
+
...DEFAULT_CONFIG.zeroMode,
|
|
88
|
+
...((userConfig && userConfig.zeroMode) || {}),
|
|
89
|
+
constraints: (userConfig && userConfig.zeroMode && Array.isArray(userConfig.zeroMode.constraints))
|
|
90
|
+
? userConfig.zeroMode.constraints
|
|
91
|
+
: DEFAULT_CONFIG.zeroMode.constraints,
|
|
92
|
+
doneCriteria: (userConfig && userConfig.zeroMode && Array.isArray(userConfig.zeroMode.doneCriteria))
|
|
93
|
+
? userConfig.zeroMode.doneCriteria
|
|
94
|
+
: DEFAULT_CONFIG.zeroMode.doneCriteria
|
|
95
|
+
},
|
|
80
96
|
validation: {
|
|
81
97
|
...DEFAULT_CONFIG.validation,
|
|
82
98
|
...((userConfig && userConfig.validation) || {})
|
|
@@ -84,11 +100,15 @@ function mergeConfig(userConfig) {
|
|
|
84
100
|
};
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
function
|
|
103
|
+
function resolveConfigPath(options = {}) {
|
|
88
104
|
const projectRoot = path.resolve(options.projectRoot || process.cwd());
|
|
89
|
-
|
|
105
|
+
return options.configPath
|
|
90
106
|
? path.resolve(projectRoot, String(options.configPath))
|
|
91
107
|
: path.resolve(projectRoot, 'roadmap-skill.config.json');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function loadConfig(options = {}) {
|
|
111
|
+
const resolvedConfigPath = resolveConfigPath(options);
|
|
92
112
|
|
|
93
113
|
const content = readTextIfExists(resolvedConfigPath);
|
|
94
114
|
let userConfig = {};
|
|
@@ -114,6 +134,15 @@ function loadConfig(options = {}) {
|
|
|
114
134
|
return merged;
|
|
115
135
|
}
|
|
116
136
|
|
|
137
|
+
function readUserConfig(options = {}) {
|
|
138
|
+
const resolvedConfigPath = resolveConfigPath(options);
|
|
139
|
+
const content = readTextIfExists(resolvedConfigPath);
|
|
140
|
+
if (!content) {
|
|
141
|
+
return {};
|
|
142
|
+
}
|
|
143
|
+
return safeParseJson(content, resolvedConfigPath);
|
|
144
|
+
}
|
|
145
|
+
|
|
117
146
|
function resolveRoadmapFile(projectRoot, config, overridePath) {
|
|
118
147
|
if (overridePath) {
|
|
119
148
|
return path.resolve(projectRoot, overridePath);
|
|
@@ -214,6 +243,8 @@ module.exports = {
|
|
|
214
243
|
collectPluginContributions,
|
|
215
244
|
loadConfig,
|
|
216
245
|
loadPlugins,
|
|
246
|
+
readUserConfig,
|
|
217
247
|
resolveAgentsFile,
|
|
248
|
+
resolveConfigPath,
|
|
218
249
|
resolveRoadmapFile
|
|
219
250
|
};
|