roadmapsmith 0.9.14 → 0.9.15
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 +50 -14
- package/bin/cli.js +340 -93
- package/package.json +1 -1
- 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/README.md
CHANGED
|
@@ -12,15 +12,22 @@ Production-grade roadmap generator and sync tool for agent-driven projects.
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npm install -g roadmapsmith
|
|
15
|
+
roadmapsmith setup
|
|
16
|
+
roadmapsmith zero
|
|
17
|
+
roadmapsmith maintain
|
|
15
18
|
```
|
|
16
19
|
|
|
20
|
+
Slash entrypoints are also supported from the CLI and launcher, for example: `roadmapsmith /road`, `roadmapsmith /zero`, `roadmapsmith /maintain`, and `roadmapsmith /roadmap-sync maintain`.
|
|
21
|
+
The generated VS Code task layer now resolves Node automatically where possible; if it cannot, RoadmapSmith prints a readable runtime diagnostic instead of a dead task.
|
|
22
|
+
`RoadmapSmith: Status` now treats "ready" as runnable task UX, not merely generated files.
|
|
23
|
+
|
|
17
24
|
### Agent Skill
|
|
18
25
|
|
|
19
26
|
```bash
|
|
20
27
|
npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
|
|
21
28
|
```
|
|
22
29
|
|
|
23
|
-
This adds the `roadmap-sync` agent skill. It does not install the CLI
|
|
30
|
+
This adds the `roadmap-sync` agent skill only. It does not install the CLI and it does not create visible VS Code actions by itself.
|
|
24
31
|
|
|
25
32
|
## Updating
|
|
26
33
|
|
|
@@ -37,12 +44,14 @@ npm install roadmapsmith@latest
|
|
|
37
44
|
npx roadmapsmith@latest validate --json
|
|
38
45
|
```
|
|
39
46
|
|
|
40
|
-
The `roadmap-sync` agent skill is separate from the CLI. Re-running the skills install updates the agent instructions, but it does not update the `roadmapsmith` npm binary:
|
|
47
|
+
The `roadmap-sync` agent skill is separate from the CLI. Re-running the skills install updates the agent instructions, but it does not update the `roadmapsmith` npm binary or the generated VS Code host files:
|
|
41
48
|
|
|
42
49
|
```bash
|
|
43
50
|
npx skills add PapiScholz/roadmapsmith --skill roadmap-sync
|
|
44
51
|
```
|
|
45
52
|
|
|
53
|
+
After updating the CLI, rerun `roadmapsmith setup` in repositories where you want the latest VS Code tasks, task wrappers, launcher behavior, or Claude hook template.
|
|
54
|
+
|
|
46
55
|
Fixes are available through `@latest` only after a new npm package version has been published. Before publication, install from a local checkout or a packed tarball for testing.
|
|
47
56
|
|
|
48
57
|
## Operating Modes
|
|
@@ -51,11 +60,11 @@ Fixes are available through `@latest` only after a new npm package version has b
|
|
|
51
60
|
|
|
52
61
|
Agent-guided discovery for empty or low-context repositories. The developer has a product idea but no implementation files, no stack decision, and no ROADMAP.md yet.
|
|
53
62
|
|
|
54
|
-
|
|
63
|
+
Run `roadmapsmith setup` first if you want visible VS Code tasks. `roadmapsmith zero` is the one-command entrypoint: it runs the terminal interview, creates governance files when needed, and generates the first roadmap.
|
|
55
64
|
|
|
56
65
|
```bash
|
|
57
|
-
roadmapsmith
|
|
58
|
-
roadmapsmith
|
|
66
|
+
roadmapsmith setup
|
|
67
|
+
roadmapsmith zero
|
|
59
68
|
```
|
|
60
69
|
|
|
61
70
|
### Sync/Audit Mode
|
|
@@ -63,30 +72,50 @@ roadmapsmith generate --project-root .
|
|
|
63
72
|
Repository-backed roadmap generation, validation, and synchronization. Use when the repository already has code, tests, docs, TODOs, or an existing ROADMAP.md.
|
|
64
73
|
|
|
65
74
|
```bash
|
|
66
|
-
roadmapsmith
|
|
67
|
-
roadmapsmith
|
|
68
|
-
|
|
69
|
-
|
|
75
|
+
roadmapsmith setup
|
|
76
|
+
roadmapsmith maintain
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Recommended Daily Flow
|
|
80
|
+
|
|
81
|
+
Use the public entrypoints first:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
roadmapsmith setup
|
|
85
|
+
roadmapsmith zero # empty repo
|
|
86
|
+
roadmapsmith maintain # existing repo
|
|
70
87
|
```
|
|
71
88
|
|
|
89
|
+
Use the lower-level commands only when you want manual control over generation, validation, or sync.
|
|
90
|
+
|
|
72
91
|
## Host Support Today
|
|
73
92
|
|
|
74
93
|
| Host | Current support |
|
|
75
94
|
|---|---|
|
|
76
|
-
| Claude Code |
|
|
77
|
-
| Codex / Codex CLI |
|
|
95
|
+
| Claude Code | Supported through `roadmapsmith setup`: visible VS Code tasks, slash-capable launcher UX, and optional repo-local Claude hook wiring. |
|
|
96
|
+
| Codex / Codex CLI | Supported through a visible VS Code task workflow and slash-capable launcher UX after `roadmapsmith setup`. Codex chat itself remains unchanged unless the host exposes native slash registration. |
|
|
78
97
|
| CI | Use disposable checkouts if you run `sync --audit`, because it still mutates the roadmap today. |
|
|
79
98
|
| Other hosts | Use the skill plus manual CLI commands. |
|
|
80
99
|
|
|
100
|
+
If Node is installed outside PATH, set `ROADMAPSMITH_NODE` to a working `node` executable before using the generated VS Code tasks.
|
|
101
|
+
|
|
81
102
|
---
|
|
82
103
|
|
|
83
104
|
## Commands
|
|
84
105
|
|
|
85
106
|
```bash
|
|
107
|
+
roadmapsmith /road
|
|
108
|
+
roadmapsmith /zero
|
|
109
|
+
roadmapsmith /maintain
|
|
110
|
+
roadmapsmith /roadmap-sync maintain
|
|
111
|
+
roadmapsmith setup [--project-root <path>] [--config <path>] [--editor vscode] [--hosts <codex,claude>] [--dry-run]
|
|
112
|
+
roadmapsmith zero [--project-root <path>] [--config <path>]
|
|
113
|
+
roadmapsmith maintain [--project-root <path>] [--config <path>] [--roadmap-file <path>]
|
|
86
114
|
roadmapsmith init [--roadmap-file <path>] [--agents-file <path>] [--dry-run]
|
|
87
115
|
roadmapsmith generate [--project-root <path>] [--config <path>] [--roadmap-file <path>] [--dry-run] [--audit]
|
|
88
116
|
roadmapsmith sync [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--dry-run] [--audit]
|
|
89
117
|
roadmapsmith validate [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--task <id|text>] [--json]
|
|
118
|
+
roadmapsmith doctor [--roadmap-file <path>] [--project-root <path>] [--config <path>] [--json]
|
|
90
119
|
```
|
|
91
120
|
|
|
92
121
|
## Behavior
|
|
@@ -241,10 +270,9 @@ module.exports = {
|
|
|
241
270
|
## Example Usage
|
|
242
271
|
|
|
243
272
|
```bash
|
|
244
|
-
roadmapsmith
|
|
245
|
-
roadmapsmith
|
|
273
|
+
roadmapsmith zero
|
|
274
|
+
roadmapsmith maintain
|
|
246
275
|
roadmapsmith validate --json
|
|
247
|
-
roadmapsmith sync
|
|
248
276
|
roadmapsmith sync --dry-run
|
|
249
277
|
```
|
|
250
278
|
|
|
@@ -261,6 +289,8 @@ roadmapsmith sync --dry-run
|
|
|
261
289
|
npm test
|
|
262
290
|
```
|
|
263
291
|
|
|
292
|
+
If `npm test` fails in your shell with "`node` is not recognized", treat that as a local PATH/runtime issue first and rerun the suite with an explicit Node executable.
|
|
293
|
+
|
|
264
294
|
## Publishing
|
|
265
295
|
|
|
266
296
|
```bash
|
|
@@ -270,6 +300,12 @@ npm publish --access public
|
|
|
270
300
|
git push origin main --follow-tags
|
|
271
301
|
```
|
|
272
302
|
|
|
303
|
+
Repository-specific release note:
|
|
304
|
+
|
|
305
|
+
- The canonical release automation lives in `.github/workflows/ci.yml`.
|
|
306
|
+
- This repository publishes from GitHub Actions on `main`; local `npm publish` is a maintainer workflow, not the default repo release path.
|
|
307
|
+
- Before publishing, verify the UX/release gate in `docs/release-ux-gate.md` and update `CHANGELOG.md` with the user-visible behavior changes.
|
|
308
|
+
|
|
273
309
|
## Versioning Strategy
|
|
274
310
|
|
|
275
311
|
- `patch`: bug fixes and non-breaking validation/generation improvements.
|
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) => {
|