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/src/index.js
CHANGED
|
@@ -6,6 +6,9 @@ module.exports = {
|
|
|
6
6
|
sync: require('./sync'),
|
|
7
7
|
validator: require('./validator'),
|
|
8
8
|
config: require('./config'),
|
|
9
|
+
host: require('./host'),
|
|
10
|
+
slash: require('./slash'),
|
|
11
|
+
zero: require('./zero'),
|
|
9
12
|
model: require('./model'),
|
|
10
13
|
renderer: require('./renderer')
|
|
11
14
|
};
|
package/src/slash.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SLASH_ACTIONS = [
|
|
4
|
+
{
|
|
5
|
+
id: 'zero',
|
|
6
|
+
description: 'Interview the developer in terminal and generate the first roadmap for an empty or low-context repo.',
|
|
7
|
+
classicCliExample: 'roadmapsmith zero',
|
|
8
|
+
slashExamples: ['/zero', '/road zero', '/roadmap-sync zero'],
|
|
9
|
+
taskLabel: 'RoadmapSmith: Zero Mode'
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
id: 'maintain',
|
|
13
|
+
description: 'Regenerate, sync, and audit the roadmap for an existing repository.',
|
|
14
|
+
classicCliExample: 'roadmapsmith maintain',
|
|
15
|
+
slashExamples: ['/maintain', '/road maintain', '/roadmap-sync maintain'],
|
|
16
|
+
taskLabel: 'RoadmapSmith: Maintain'
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'status',
|
|
20
|
+
description: 'Inspect CLI, roadmap, VS Code task, and Claude hook readiness.',
|
|
21
|
+
classicCliExample: 'roadmapsmith doctor --json',
|
|
22
|
+
slashExamples: ['/status', '/road status', '/roadmap-sync status'],
|
|
23
|
+
taskLabel: 'RoadmapSmith: Status'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'init',
|
|
27
|
+
description: 'Create ROADMAP.md and AGENTS.md when they are missing.',
|
|
28
|
+
classicCliExample: 'roadmapsmith init',
|
|
29
|
+
slashExamples: ['/init', '/road init', '/roadmap-sync init'],
|
|
30
|
+
taskLabel: 'RoadmapSmith: Init'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'generate',
|
|
34
|
+
description: 'Rebuild the managed roadmap block from repository context.',
|
|
35
|
+
classicCliExample: 'roadmapsmith generate --project-root .',
|
|
36
|
+
slashExamples: ['/generate', '/road generate', '/roadmap-sync generate'],
|
|
37
|
+
taskLabel: 'RoadmapSmith: Generate'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'validate',
|
|
41
|
+
description: 'Inspect per-task evidence status as JSON.',
|
|
42
|
+
classicCliExample: 'roadmapsmith validate --json --project-root .',
|
|
43
|
+
slashExamples: ['/validate', '/road validate', '/roadmap-sync validate'],
|
|
44
|
+
taskLabel: 'RoadmapSmith: Validate'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 'sync',
|
|
48
|
+
description: 'Apply evidence-backed checklist sync to ROADMAP.md.',
|
|
49
|
+
classicCliExample: 'roadmapsmith sync --project-root .',
|
|
50
|
+
slashExamples: ['/sync', '/road sync', '/roadmap-sync sync'],
|
|
51
|
+
taskLabel: 'RoadmapSmith: Sync'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
id: 'audit',
|
|
55
|
+
description: 'Run sync and print the post-sync mismatch summary.',
|
|
56
|
+
classicCliExample: 'roadmapsmith sync --audit --project-root .',
|
|
57
|
+
slashExamples: ['/audit', '/road audit', '/roadmap-sync audit'],
|
|
58
|
+
taskLabel: 'RoadmapSmith: Sync Audit'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: 'setup',
|
|
62
|
+
description: 'Generate visible VS Code tasks and optional Claude hook wiring.',
|
|
63
|
+
classicCliExample: 'roadmapsmith setup',
|
|
64
|
+
slashExamples: ['/setup', '/road setup', '/roadmap-sync setup'],
|
|
65
|
+
taskLabel: 'RoadmapSmith: Refresh Setup'
|
|
66
|
+
}
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const SLASH_ROOT_ALIASES = new Set(['/road', '/roadmap-sync']);
|
|
70
|
+
|
|
71
|
+
const DIRECT_SLASH_ALIAS_TO_ACTION = Object.freeze({
|
|
72
|
+
'/zero': 'zero',
|
|
73
|
+
'/maintain': 'maintain',
|
|
74
|
+
'/status': 'status',
|
|
75
|
+
'/init': 'init',
|
|
76
|
+
'/generate': 'generate',
|
|
77
|
+
'/validate': 'validate',
|
|
78
|
+
'/sync': 'sync',
|
|
79
|
+
'/audit': 'audit',
|
|
80
|
+
'/setup': 'setup'
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
function normalizeActionId(value) {
|
|
84
|
+
return String(value || '').trim().toLowerCase().replace(/^\/+/, '');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isSlashToken(value) {
|
|
88
|
+
return typeof value === 'string' && value.trim().startsWith('/');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getSlashAction(actionId) {
|
|
92
|
+
const normalized = normalizeActionId(actionId);
|
|
93
|
+
return SLASH_ACTIONS.find((action) => action.id === normalized) || null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getSlashActionSpecs() {
|
|
97
|
+
return SLASH_ACTIONS.map((action) => ({ ...action }));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getSlashSuggestions(query) {
|
|
101
|
+
const normalized = normalizeActionId(query);
|
|
102
|
+
if (!normalized) {
|
|
103
|
+
return getSlashActionSpecs();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const startsWithMatches = SLASH_ACTIONS.filter((action) => action.id.startsWith(normalized));
|
|
107
|
+
const containsMatches = SLASH_ACTIONS.filter((action) => {
|
|
108
|
+
return !action.id.startsWith(normalized) && action.id.includes(normalized);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
return [...startsWithMatches, ...containsMatches].map((action) => ({ ...action }));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resolveSlashInvocation(command, args = []) {
|
|
115
|
+
if (!isSlashToken(command)) {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const normalizedCommand = String(command).trim().toLowerCase();
|
|
120
|
+
|
|
121
|
+
if (Object.prototype.hasOwnProperty.call(DIRECT_SLASH_ALIAS_TO_ACTION, normalizedCommand)) {
|
|
122
|
+
return {
|
|
123
|
+
kind: 'execute',
|
|
124
|
+
actionId: DIRECT_SLASH_ALIAS_TO_ACTION[normalizedCommand],
|
|
125
|
+
query: normalizeActionId(normalizedCommand),
|
|
126
|
+
source: normalizedCommand,
|
|
127
|
+
suggestions: getSlashSuggestions(normalizedCommand)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (SLASH_ROOT_ALIASES.has(normalizedCommand)) {
|
|
132
|
+
const queryToken = args.length > 0 ? normalizeActionId(args[0]) : '';
|
|
133
|
+
if (!queryToken) {
|
|
134
|
+
return {
|
|
135
|
+
kind: 'palette',
|
|
136
|
+
query: '',
|
|
137
|
+
source: normalizedCommand,
|
|
138
|
+
suggestions: getSlashSuggestions('')
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const exactAction = getSlashAction(queryToken);
|
|
143
|
+
if (exactAction) {
|
|
144
|
+
return {
|
|
145
|
+
kind: 'execute',
|
|
146
|
+
actionId: exactAction.id,
|
|
147
|
+
query: queryToken,
|
|
148
|
+
source: normalizedCommand,
|
|
149
|
+
suggestions: getSlashSuggestions(queryToken)
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
kind: 'palette',
|
|
155
|
+
query: queryToken,
|
|
156
|
+
source: normalizedCommand,
|
|
157
|
+
suggestions: getSlashSuggestions(queryToken)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
kind: 'palette',
|
|
163
|
+
query: normalizeActionId(normalizedCommand),
|
|
164
|
+
source: normalizedCommand,
|
|
165
|
+
suggestions: getSlashSuggestions(normalizedCommand)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function renderSlashPalette(options = {}) {
|
|
170
|
+
const source = options.source || '/road';
|
|
171
|
+
const query = normalizeActionId(options.query);
|
|
172
|
+
const suggestions = Array.isArray(options.suggestions) ? options.suggestions : getSlashSuggestions(query);
|
|
173
|
+
const lines = [];
|
|
174
|
+
|
|
175
|
+
lines.push('RoadmapSmith slash palette');
|
|
176
|
+
lines.push('');
|
|
177
|
+
|
|
178
|
+
if (query) {
|
|
179
|
+
lines.push(`Input: ${source} ${query}`);
|
|
180
|
+
if (suggestions.length > 0) {
|
|
181
|
+
lines.push('No exact slash match was executed. Related actions:');
|
|
182
|
+
} else {
|
|
183
|
+
lines.push('No exact slash match was executed.');
|
|
184
|
+
}
|
|
185
|
+
} else {
|
|
186
|
+
lines.push(`Entry point: ${source}`);
|
|
187
|
+
lines.push('Use an exact slash action to execute work. Incomplete or ambiguous input only shows suggestions.');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
lines.push('');
|
|
191
|
+
|
|
192
|
+
if (suggestions.length === 0) {
|
|
193
|
+
lines.push('No related slash actions found.');
|
|
194
|
+
} else {
|
|
195
|
+
suggestions.forEach((action) => {
|
|
196
|
+
lines.push(`- /${action.id}: ${action.description}`);
|
|
197
|
+
lines.push(` Classic CLI: ${action.classicCliExample}`);
|
|
198
|
+
lines.push(` Skill form: /roadmap-sync ${action.id}`);
|
|
199
|
+
lines.push(` VS Code task: ${action.taskLabel}`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
lines.push('');
|
|
204
|
+
lines.push('Examples:');
|
|
205
|
+
lines.push('- roadmapsmith zero');
|
|
206
|
+
lines.push('- roadmapsmith maintain');
|
|
207
|
+
lines.push('- roadmapsmith /road');
|
|
208
|
+
lines.push('- roadmapsmith /maintain');
|
|
209
|
+
lines.push('- roadmapsmith /roadmap-sync maintain');
|
|
210
|
+
lines.push('');
|
|
211
|
+
lines.push('Installing the skill alone does not expose CLI behavior in VS Code. Use roadmapsmith setup for the visible task/launcher layer.');
|
|
212
|
+
|
|
213
|
+
return lines.join('\n');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
DIRECT_SLASH_ALIAS_TO_ACTION,
|
|
218
|
+
SLASH_ROOT_ALIASES,
|
|
219
|
+
getSlashAction,
|
|
220
|
+
getSlashActionSpecs,
|
|
221
|
+
getSlashSuggestions,
|
|
222
|
+
isSlashToken,
|
|
223
|
+
normalizeActionId,
|
|
224
|
+
renderSlashPalette,
|
|
225
|
+
resolveSlashInvocation
|
|
226
|
+
};
|
package/src/zero.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const ZERO_MODE_QUESTIONS = [
|
|
6
|
+
{ id: 'productName', prompt: '1. What product are we building?' },
|
|
7
|
+
{ id: 'primaryUser', prompt: '2. Who is the target user?' },
|
|
8
|
+
{ id: 'problemStatement', prompt: '3. What problem does it solve?' },
|
|
9
|
+
{ id: 'targetOutcome', prompt: '4. What is the desired v1.0 outcome?' },
|
|
10
|
+
{ id: 'antiGoals', prompt: '5. What is explicitly out of scope? Separate multiple items with ;' },
|
|
11
|
+
{ id: 'preferredStack', prompt: '6. What stack do you prefer, if any?' },
|
|
12
|
+
{ id: 'constraints', prompt: '7. What constraints exist? Separate multiple items with ;' },
|
|
13
|
+
{ id: 'doneCriteria', prompt: '8. What does "done" mean for the first usable version? Separate multiple items with ;' }
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
function splitListAnswer(value) {
|
|
17
|
+
return String(value || '')
|
|
18
|
+
.split(/(?:\r?\n|;)+/)
|
|
19
|
+
.map((item) => item.trim())
|
|
20
|
+
.filter(Boolean);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function joinListDefault(values) {
|
|
24
|
+
return Array.isArray(values) && values.length > 0 ? values.join('; ') : '';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function deriveDefaultProblemStatement(config) {
|
|
28
|
+
const explicit = config.zeroMode && config.zeroMode.problemStatement;
|
|
29
|
+
if (explicit) {
|
|
30
|
+
return explicit;
|
|
31
|
+
}
|
|
32
|
+
const positioning = String((config.product && config.product.positioning) || '').trim();
|
|
33
|
+
const prefix = 'Core problem: ';
|
|
34
|
+
if (positioning.startsWith(prefix)) {
|
|
35
|
+
return positioning.slice(prefix.length).trim();
|
|
36
|
+
}
|
|
37
|
+
return '';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function buildZeroModeDefaults(projectRoot, config) {
|
|
41
|
+
return {
|
|
42
|
+
productName: (config.product && config.product.name) || path.basename(projectRoot),
|
|
43
|
+
primaryUser: (config.product && config.product.primaryUser) || '',
|
|
44
|
+
problemStatement: deriveDefaultProblemStatement(config),
|
|
45
|
+
targetOutcome: (config.product && config.product.targetOutcome) || '',
|
|
46
|
+
antiGoals: joinListDefault(config.product && config.product.antiGoals),
|
|
47
|
+
preferredStack: (config.zeroMode && config.zeroMode.preferredStack) || '',
|
|
48
|
+
constraints: joinListDefault(config.zeroMode && config.zeroMode.constraints),
|
|
49
|
+
doneCriteria: joinListDefault(
|
|
50
|
+
(config.zeroMode && config.zeroMode.doneCriteria && config.zeroMode.doneCriteria.length > 0)
|
|
51
|
+
? config.zeroMode.doneCriteria
|
|
52
|
+
: (config.product && config.product.successCriteria)
|
|
53
|
+
)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function collectZeroModeAnswers(ask, defaults = {}) {
|
|
58
|
+
const answers = {};
|
|
59
|
+
for (const question of ZERO_MODE_QUESTIONS) {
|
|
60
|
+
const fallback = String(defaults[question.id] || '').trim();
|
|
61
|
+
const suffix = fallback ? ` [${fallback}]` : '';
|
|
62
|
+
const response = await ask(`${question.prompt}${suffix}: `);
|
|
63
|
+
const normalized = String(response || '').trim();
|
|
64
|
+
answers[question.id] = normalized || fallback;
|
|
65
|
+
}
|
|
66
|
+
return answers;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function deriveNorthStar(answers) {
|
|
70
|
+
const productName = String(answers.productName || '').trim();
|
|
71
|
+
const primaryUser = String(answers.primaryUser || '').trim();
|
|
72
|
+
const targetOutcome = String(answers.targetOutcome || '').trim();
|
|
73
|
+
if (productName && primaryUser && targetOutcome) {
|
|
74
|
+
return `${productName} helps ${primaryUser} achieve ${targetOutcome}.`;
|
|
75
|
+
}
|
|
76
|
+
if (productName && targetOutcome) {
|
|
77
|
+
return `${productName} exists to deliver ${targetOutcome}.`;
|
|
78
|
+
}
|
|
79
|
+
if (productName) {
|
|
80
|
+
return `Ship the first usable version of ${productName}.`;
|
|
81
|
+
}
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function buildZeroModeConfigPatch(projectRoot, existingUserConfig, answers) {
|
|
86
|
+
const productName = String(answers.productName || '').trim() || path.basename(projectRoot);
|
|
87
|
+
const primaryUser = String(answers.primaryUser || '').trim();
|
|
88
|
+
const problemStatement = String(answers.problemStatement || '').trim();
|
|
89
|
+
const targetOutcome = String(answers.targetOutcome || '').trim();
|
|
90
|
+
const antiGoals = splitListAnswer(answers.antiGoals);
|
|
91
|
+
const constraints = splitListAnswer(answers.constraints);
|
|
92
|
+
const doneCriteria = splitListAnswer(answers.doneCriteria);
|
|
93
|
+
const preferredStack = String(answers.preferredStack || '').trim();
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
...existingUserConfig,
|
|
97
|
+
product: {
|
|
98
|
+
...((existingUserConfig && existingUserConfig.product) || {}),
|
|
99
|
+
name: productName,
|
|
100
|
+
northStar: deriveNorthStar({ productName, primaryUser, targetOutcome }) || (((existingUserConfig || {}).product || {}).northStar || ''),
|
|
101
|
+
positioning: problemStatement ? `Core problem: ${problemStatement}` : ((((existingUserConfig || {}).product || {}).positioning) || ''),
|
|
102
|
+
primaryUser,
|
|
103
|
+
targetOutcome,
|
|
104
|
+
antiGoals,
|
|
105
|
+
risks: constraints.map((constraint) => `Constraint: ${constraint}`),
|
|
106
|
+
successCriteria: doneCriteria
|
|
107
|
+
},
|
|
108
|
+
zeroMode: {
|
|
109
|
+
...((existingUserConfig && existingUserConfig.zeroMode) || {}),
|
|
110
|
+
problemStatement,
|
|
111
|
+
preferredStack,
|
|
112
|
+
constraints,
|
|
113
|
+
doneCriteria
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isInteractiveTerminal(input = process.stdin, output = process.stdout) {
|
|
119
|
+
return Boolean(input && input.isTTY && output && output.isTTY);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
ZERO_MODE_QUESTIONS,
|
|
124
|
+
buildZeroModeConfigPatch,
|
|
125
|
+
buildZeroModeDefaults,
|
|
126
|
+
collectZeroModeAnswers,
|
|
127
|
+
isInteractiveTerminal,
|
|
128
|
+
splitListAnswer
|
|
129
|
+
};
|