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/src/host.js ADDED
@@ -0,0 +1,982 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { readTextIfExists, writeText } = require('./io');
6
+ const { getSlashActionSpecs } = require('./slash');
7
+
8
+ const SUPPORTED_EDITORS = new Set(['vscode']);
9
+ const SUPPORTED_HOSTS = new Set(['codex', 'claude']);
10
+ const VSCODE_LAUNCHER_RELATIVE_PATH = '.vscode/roadmapsmith-launcher.js';
11
+ const WINDOWS_TASK_WRAPPER_RELATIVE_PATH = '.vscode/roadmapsmith-task.cmd';
12
+ const POSIX_TASK_WRAPPER_RELATIVE_PATH = '.vscode/roadmapsmith-task.sh';
13
+ const ROADMAPSMITH_TASK_LABELS = [
14
+ 'RoadmapSmith: Zero Mode',
15
+ 'RoadmapSmith: Maintain',
16
+ 'RoadmapSmith: Status',
17
+ 'RoadmapSmith: Explain Workflow',
18
+ 'RoadmapSmith: Init',
19
+ 'RoadmapSmith: Generate',
20
+ 'RoadmapSmith: Validate',
21
+ 'RoadmapSmith: Sync',
22
+ 'RoadmapSmith: Sync Dry Run',
23
+ 'RoadmapSmith: Sync Audit',
24
+ 'RoadmapSmith: Refresh Setup'
25
+ ];
26
+ const CLAUDE_HOOK_COMMAND = 'node .claude/hooks/roadmap-sync.js';
27
+ const CLAUDE_HOOK_RELATIVE_PATH = '.claude/hooks/roadmap-sync.js';
28
+
29
+ function removeJsonComments(content) {
30
+ let result = '';
31
+ let inString = false;
32
+ let escaped = false;
33
+ let lineComment = false;
34
+ let blockComment = false;
35
+
36
+ for (let i = 0; i < content.length; i += 1) {
37
+ const current = content[i];
38
+ const next = content[i + 1];
39
+
40
+ if (lineComment) {
41
+ if (current === '\n') {
42
+ lineComment = false;
43
+ result += current;
44
+ }
45
+ continue;
46
+ }
47
+
48
+ if (blockComment) {
49
+ if (current === '*' && next === '/') {
50
+ blockComment = false;
51
+ i += 1;
52
+ }
53
+ continue;
54
+ }
55
+
56
+ if (inString) {
57
+ result += current;
58
+ if (escaped) {
59
+ escaped = false;
60
+ } else if (current === '\\') {
61
+ escaped = true;
62
+ } else if (current === '"') {
63
+ inString = false;
64
+ }
65
+ continue;
66
+ }
67
+
68
+ if (current === '"') {
69
+ inString = true;
70
+ result += current;
71
+ continue;
72
+ }
73
+
74
+ if (current === '/' && next === '/') {
75
+ lineComment = true;
76
+ i += 1;
77
+ continue;
78
+ }
79
+
80
+ if (current === '/' && next === '*') {
81
+ blockComment = true;
82
+ i += 1;
83
+ continue;
84
+ }
85
+
86
+ result += current;
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function removeTrailingCommas(content) {
93
+ let result = '';
94
+ let inString = false;
95
+ let escaped = false;
96
+
97
+ for (let i = 0; i < content.length; i += 1) {
98
+ const current = content[i];
99
+
100
+ if (inString) {
101
+ result += current;
102
+ if (escaped) {
103
+ escaped = false;
104
+ } else if (current === '\\') {
105
+ escaped = true;
106
+ } else if (current === '"') {
107
+ inString = false;
108
+ }
109
+ continue;
110
+ }
111
+
112
+ if (current === '"') {
113
+ inString = true;
114
+ result += current;
115
+ continue;
116
+ }
117
+
118
+ if (current === ',') {
119
+ let cursor = i + 1;
120
+ while (cursor < content.length && /\s/.test(content[cursor])) {
121
+ cursor += 1;
122
+ }
123
+ if (content[cursor] === '}' || content[cursor] === ']') {
124
+ continue;
125
+ }
126
+ }
127
+
128
+ result += current;
129
+ }
130
+
131
+ return result;
132
+ }
133
+
134
+ function parseJsonc(content, filePath) {
135
+ const sanitized = removeTrailingCommas(removeJsonComments(String(content || '').replace(/^\uFEFF/, '')));
136
+ try {
137
+ return JSON.parse(sanitized);
138
+ } catch (error) {
139
+ throw new Error(`Invalid JSON in ${filePath}: ${error.message}`);
140
+ }
141
+ }
142
+
143
+ function readJsoncObject(filePath) {
144
+ const content = readTextIfExists(filePath);
145
+ if (content == null) {
146
+ return null;
147
+ }
148
+ const parsed = parseJsonc(content, filePath);
149
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
150
+ throw new Error(`Expected a JSON object in ${filePath}`);
151
+ }
152
+ return parsed;
153
+ }
154
+
155
+ function stringifyJson(value) {
156
+ return JSON.stringify(value, null, 2);
157
+ }
158
+
159
+ function normalizeHostValue(host) {
160
+ return String(host || '').trim().toLowerCase();
161
+ }
162
+
163
+ function parseHosts(hostValue) {
164
+ const rawHosts = Array.isArray(hostValue)
165
+ ? hostValue.flatMap((entry) => String(entry).split(','))
166
+ : String(hostValue || 'codex,claude').split(',');
167
+ const result = [];
168
+ const seen = new Set();
169
+
170
+ for (const rawHost of rawHosts) {
171
+ const host = normalizeHostValue(rawHost);
172
+ if (!host) {
173
+ continue;
174
+ }
175
+ if (!SUPPORTED_HOSTS.has(host)) {
176
+ throw new Error(`Unsupported host "${host}". Supported hosts: codex, claude`);
177
+ }
178
+ if (seen.has(host)) {
179
+ continue;
180
+ }
181
+ seen.add(host);
182
+ result.push(host);
183
+ }
184
+
185
+ if (result.length === 0) {
186
+ throw new Error('At least one host must be selected for setup');
187
+ }
188
+
189
+ return result;
190
+ }
191
+
192
+ function assertSupportedEditor(editorValue) {
193
+ const editor = String(editorValue || 'vscode').trim().toLowerCase();
194
+ if (!SUPPORTED_EDITORS.has(editor)) {
195
+ throw new Error(`Unsupported editor "${editor}". Supported editors: vscode`);
196
+ }
197
+ return editor;
198
+ }
199
+
200
+ function createTask(action, label, detail) {
201
+ return {
202
+ label,
203
+ type: 'process',
204
+ command: 'sh',
205
+ args: [POSIX_TASK_WRAPPER_RELATIVE_PATH, action],
206
+ windows: {
207
+ command: 'cmd.exe',
208
+ args: ['/d', '/c', WINDOWS_TASK_WRAPPER_RELATIVE_PATH.replace(/\//g, '\\'), action]
209
+ },
210
+ options: {
211
+ cwd: '${workspaceFolder}'
212
+ },
213
+ problemMatcher: [],
214
+ presentation: {
215
+ reveal: 'always',
216
+ panel: 'shared',
217
+ clear: true
218
+ },
219
+ detail
220
+ };
221
+ }
222
+
223
+ function createManagedTasks() {
224
+ return [
225
+ createTask('zero', 'RoadmapSmith: Zero Mode', 'Run the Zero Mode interview and generate the first roadmap in one command.'),
226
+ createTask('maintain', 'RoadmapSmith: Maintain', 'Run the default existing-repo flow: generate, sync, and audit in one command.'),
227
+ createTask('status', 'RoadmapSmith: Status', 'Inspect readiness and learn the slash entrypoints like /road and /roadmap-sync <action>.'),
228
+ createTask('explain', 'RoadmapSmith: Explain Workflow', 'Explain how zero, maintain, the skill, setup, slash routing, and VS Code tasks work together.'),
229
+ createTask('init', 'RoadmapSmith: Init', 'Create ROADMAP.md and AGENTS.md when they are missing.'),
230
+ createTask('generate', 'RoadmapSmith: Generate', 'Rebuild the managed roadmap block from repository context.'),
231
+ createTask('validate', 'RoadmapSmith: Validate', 'Inspect per-task evidence status as JSON.'),
232
+ createTask('sync', 'RoadmapSmith: Sync', 'Apply evidence-backed checklist sync to ROADMAP.md.'),
233
+ createTask('sync-dry-run', 'RoadmapSmith: Sync Dry Run', 'Preview the next roadmap sync without writing files.'),
234
+ createTask('sync-audit', 'RoadmapSmith: Sync Audit', 'Run sync and print the post-sync mismatch summary.'),
235
+ createTask('setup', 'RoadmapSmith: Refresh Setup', 'Reapply RoadmapSmith VS Code and host integration files.')
236
+ ];
237
+ }
238
+
239
+ function mergeVsCodeTasks(existingConfig) {
240
+ const config = existingConfig ? { ...existingConfig } : {};
241
+ const existingTasks = Array.isArray(config.tasks) ? config.tasks.slice() : [];
242
+ const managedLabels = new Set(ROADMAPSMITH_TASK_LABELS);
243
+ const unmanagedTasks = existingTasks.filter((task) => !managedLabels.has(task && task.label));
244
+
245
+ return {
246
+ ...config,
247
+ version: typeof config.version === 'string' ? config.version : '2.0.0',
248
+ tasks: [...createManagedTasks(), ...unmanagedTasks]
249
+ };
250
+ }
251
+
252
+ function createClaudeHookEntry() {
253
+ return {
254
+ matcher: 'Write|Edit|MultiEdit',
255
+ hooks: [
256
+ {
257
+ type: 'command',
258
+ command: CLAUDE_HOOK_COMMAND,
259
+ timeout: 30
260
+ }
261
+ ]
262
+ };
263
+ }
264
+
265
+ function isRoadmapSmithHookEntry(entry) {
266
+ if (!entry || !Array.isArray(entry.hooks)) {
267
+ return false;
268
+ }
269
+ return entry.hooks.some((hook) => hook && hook.command === CLAUDE_HOOK_COMMAND);
270
+ }
271
+
272
+ function mergeClaudeSettings(existingConfig) {
273
+ const config = existingConfig ? { ...existingConfig } : {};
274
+ const hooks = config.hooks && typeof config.hooks === 'object' && !Array.isArray(config.hooks)
275
+ ? { ...config.hooks }
276
+ : {};
277
+ const postToolUse = Array.isArray(hooks.PostToolUse) ? hooks.PostToolUse.slice() : [];
278
+ const withoutManagedEntry = postToolUse.filter((entry) => !isRoadmapSmithHookEntry(entry));
279
+
280
+ hooks.PostToolUse = [createClaudeHookEntry(), ...withoutManagedEntry];
281
+
282
+ return {
283
+ ...config,
284
+ hooks
285
+ };
286
+ }
287
+
288
+ function renderClaudeHookScript() {
289
+ return [
290
+ '#!/usr/bin/env node',
291
+ '\'use strict\';',
292
+ '',
293
+ 'const path = require(\'path\');',
294
+ 'const fs = require(\'fs\');',
295
+ 'const { execFileSync } = require(\'child_process\');',
296
+ '',
297
+ '// .claude/hooks/ -> project root (two levels up)',
298
+ 'const PROJECT_ROOT = path.resolve(__dirname, \'../..\');',
299
+ 'const CLI = path.join(PROJECT_ROOT, \'roadmap-skill\', \'bin\', \'cli.js\');',
300
+ 'const LOCK_FILE = path.join(__dirname, \'.sync.lock\');',
301
+ '',
302
+ 'let data = \'\';',
303
+ 'process.stdin.setEncoding(\'utf8\');',
304
+ 'process.stdin.on(\'data\', chunk => { data += chunk; });',
305
+ 'process.stdin.on(\'end\', () => {',
306
+ ' let filePath = \'\';',
307
+ ' try {',
308
+ ' const parsed = JSON.parse(data);',
309
+ ' filePath = (parsed && parsed.tool_input && parsed.tool_input.file_path) || \'\';',
310
+ ' } catch (_) {',
311
+ ' process.exit(0);',
312
+ ' }',
313
+ '',
314
+ ' const normalised = filePath.replace(/\\\\/g, \'/\');',
315
+ ' if (!normalised || normalised.endsWith(\'/ROADMAP.md\')) {',
316
+ ' process.exit(0);',
317
+ ' }',
318
+ '',
319
+ ' if (fs.existsSync(LOCK_FILE)) {',
320
+ ' process.exit(0);',
321
+ ' }',
322
+ '',
323
+ ' try {',
324
+ ' fs.writeFileSync(LOCK_FILE, String(process.pid));',
325
+ ' execFileSync(process.execPath, [CLI, \'sync\', \'--project-root\', PROJECT_ROOT], {',
326
+ ' stdio: \'inherit\'',
327
+ ' });',
328
+ ' } catch (err) {',
329
+ ' process.stderr.write(\'roadmapsmith sync failed: \' + (err.message || String(err)) + \'\\n\');',
330
+ ' } finally {',
331
+ ' try { fs.unlinkSync(LOCK_FILE); } catch (_) {}',
332
+ ' }',
333
+ '});',
334
+ ''
335
+ ].join('\n');
336
+ }
337
+
338
+ function renderWindowsTaskWrapper() {
339
+ return [
340
+ '@echo off',
341
+ 'setlocal',
342
+ 'set "SCRIPT_DIR=%~dp0"',
343
+ 'set "ACTION=%~1"',
344
+ 'if not defined ACTION set "ACTION=explain"',
345
+ 'call :resolve_node',
346
+ 'if defined ROADMAPSMITH_NODE_RESOLVED goto run_launcher',
347
+ 'echo RoadmapSmith VS Code task runtime error',
348
+ 'echo.',
349
+ 'echo VS Code tasks are installed, but the Node runtime needed to start RoadmapSmith could not be resolved.',
350
+ 'echo RoadmapSmith itself may still be installed and the CLI may still be available.',
351
+ 'echo Missing piece: the Node runtime used to start .vscode\\roadmapsmith-launcher.js',
352
+ 'echo Recovery: install Node.js or set ROADMAPSMITH_NODE to a working node executable path, then rerun RoadmapSmith: Status.',
353
+ 'if /I "%ACTION%"=="status" exit /b 0',
354
+ 'if /I "%ACTION%"=="explain" exit /b 0',
355
+ 'exit /b 1',
356
+ '',
357
+ ':run_launcher',
358
+ '"%ROADMAPSMITH_NODE_RESOLVED%" "%SCRIPT_DIR%roadmapsmith-launcher.js" %*',
359
+ 'exit /b %ERRORLEVEL%',
360
+ '',
361
+ ':resolve_node',
362
+ 'set "ROADMAPSMITH_NODE_RESOLVED="',
363
+ 'if defined ROADMAPSMITH_NODE if exist "%ROADMAPSMITH_NODE%" set "ROADMAPSMITH_NODE_RESOLVED=%ROADMAPSMITH_NODE%"',
364
+ 'if defined ROADMAPSMITH_NODE if not defined ROADMAPSMITH_NODE_RESOLVED call :resolve_command "%ROADMAPSMITH_NODE%"',
365
+ 'if defined ROADMAPSMITH_NODE_RESOLVED exit /b 0',
366
+ 'for /f "delims=" %%I in (\'where node 2^>nul\') do (',
367
+ ' set "ROADMAPSMITH_NODE_RESOLVED=%%~fI"',
368
+ ' goto :node_resolved',
369
+ ')',
370
+ 'if not defined ROADMAPSMITH_NODE_RESOLVED if defined ProgramFiles if exist "%ProgramFiles%\\nodejs\\node.exe" set "ROADMAPSMITH_NODE_RESOLVED=%ProgramFiles%\\nodejs\\node.exe"',
371
+ 'if not defined ROADMAPSMITH_NODE_RESOLVED if defined ProgramFiles(x86) if exist "%ProgramFiles(x86)%\\nodejs\\node.exe" set "ROADMAPSMITH_NODE_RESOLVED=%ProgramFiles(x86)%\\nodejs\\node.exe"',
372
+ 'if not defined ROADMAPSMITH_NODE_RESOLVED if defined LocalAppData if exist "%LocalAppData%\\Programs\\nodejs\\node.exe" set "ROADMAPSMITH_NODE_RESOLVED=%LocalAppData%\\Programs\\nodejs\\node.exe"',
373
+ 'if not defined ROADMAPSMITH_NODE_RESOLVED if defined LocalAppData if exist "%LocalAppData%\\Volta\\bin\\node.exe" set "ROADMAPSMITH_NODE_RESOLVED=%LocalAppData%\\Volta\\bin\\node.exe"',
374
+ ':node_resolved',
375
+ 'exit /b 0',
376
+ '',
377
+ ':resolve_command',
378
+ 'for /f "delims=" %%I in (\'where %~1 2^>nul\') do (',
379
+ ' set "ROADMAPSMITH_NODE_RESOLVED=%%~fI"',
380
+ ' goto :eof',
381
+ ')',
382
+ 'exit /b 0'
383
+ ].join('\n');
384
+ }
385
+
386
+ function renderPosixTaskWrapper() {
387
+ return [
388
+ '#!/bin/sh',
389
+ 'set -eu',
390
+ 'SCRIPT_PATH="$0"',
391
+ 'case "${SCRIPT_PATH}" in',
392
+ ' */*) ;;',
393
+ ' *) SCRIPT_PATH="./${SCRIPT_PATH}" ;;',
394
+ 'esac',
395
+ 'SCRIPT_DIR=$(CDPATH= cd -- "${SCRIPT_PATH%/*}" && pwd)',
396
+ 'ACTION="${1:-explain}"',
397
+ 'ROADMAPSMITH_NODE_RESOLVED=""',
398
+ 'if [ -n "${ROADMAPSMITH_NODE:-}" ]; then',
399
+ ' if [ -x "${ROADMAPSMITH_NODE}" ]; then',
400
+ ' ROADMAPSMITH_NODE_RESOLVED="${ROADMAPSMITH_NODE}"',
401
+ ' elif command -v -- "${ROADMAPSMITH_NODE}" >/dev/null 2>&1; then',
402
+ ' ROADMAPSMITH_NODE_RESOLVED=$(command -v -- "${ROADMAPSMITH_NODE}")',
403
+ ' fi',
404
+ 'fi',
405
+ 'if [ -z "${ROADMAPSMITH_NODE_RESOLVED}" ] && command -v node >/dev/null 2>&1; then',
406
+ ' ROADMAPSMITH_NODE_RESOLVED=$(command -v node)',
407
+ 'fi',
408
+ 'if [ -z "${ROADMAPSMITH_NODE_RESOLVED}" ]; then',
409
+ ' echo "RoadmapSmith VS Code task runtime error"',
410
+ ' echo',
411
+ ' echo "VS Code tasks are installed, but the Node runtime needed to start RoadmapSmith could not be resolved."',
412
+ ' echo "RoadmapSmith itself may still be installed and the CLI may still be available."',
413
+ ' echo "Missing piece: the Node runtime used to start .vscode/roadmapsmith-launcher.js"',
414
+ ' echo "Recovery: install Node.js or set ROADMAPSMITH_NODE to a working node executable path, then rerun RoadmapSmith: Status."',
415
+ ' case "${ACTION}" in',
416
+ ' status|explain) exit 0 ;;',
417
+ ' *) exit 1 ;;',
418
+ ' esac',
419
+ 'fi',
420
+ 'exec "${ROADMAPSMITH_NODE_RESOLVED}" "${SCRIPT_DIR}/roadmapsmith-launcher.js" "$@"'
421
+ ].join('\n');
422
+ }
423
+
424
+ function findCommandPath(commandName, env = process.env) {
425
+ const probe = process.platform === 'win32'
426
+ ? require('child_process').spawnSync('where', [commandName], { encoding: 'utf8', env })
427
+ : require('child_process').spawnSync('which', [commandName], { encoding: 'utf8', env });
428
+ if (probe.status !== 0 || !probe.stdout) {
429
+ return null;
430
+ }
431
+ return probe.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || null;
432
+ }
433
+
434
+ function detectNodeRuntime(env = process.env) {
435
+ const override = String((env && env.ROADMAPSMITH_NODE) || '').trim();
436
+ if (override) {
437
+ if (fs.existsSync(override)) {
438
+ return { ready: true, kind: 'env-override', path: override };
439
+ }
440
+ const overrideCommandPath = findCommandPath(override, env);
441
+ if (overrideCommandPath) {
442
+ return { ready: true, kind: 'env-override', path: overrideCommandPath };
443
+ }
444
+ }
445
+
446
+ const pathNode = findCommandPath('node', env);
447
+ if (pathNode) {
448
+ return { ready: true, kind: 'path', path: pathNode };
449
+ }
450
+
451
+ if (process.platform === 'win32') {
452
+ const candidateSpecs = [
453
+ { kind: 'program-files', path: env && env.ProgramFiles ? path.join(env.ProgramFiles, 'nodejs', 'node.exe') : null },
454
+ { kind: 'program-files-x86', path: env && env['ProgramFiles(x86)'] ? path.join(env['ProgramFiles(x86)'], 'nodejs', 'node.exe') : null },
455
+ { kind: 'local-app-data', path: env && env.LocalAppData ? path.join(env.LocalAppData, 'Programs', 'nodejs', 'node.exe') : null },
456
+ { kind: 'volta', path: env && env.LocalAppData ? path.join(env.LocalAppData, 'Volta', 'bin', 'node.exe') : null }
457
+ ];
458
+
459
+ for (const candidate of candidateSpecs) {
460
+ if (candidate.path && fs.existsSync(candidate.path)) {
461
+ return { ready: true, kind: candidate.kind, path: candidate.path };
462
+ }
463
+ }
464
+ }
465
+
466
+ return {
467
+ ready: false,
468
+ kind: 'missing',
469
+ path: null
470
+ };
471
+ }
472
+
473
+ function renderVsCodeLauncher() {
474
+ const slashSpecsJson = JSON.stringify(getSlashActionSpecs());
475
+ return [
476
+ '#!/usr/bin/env node',
477
+ '\'use strict\';',
478
+ '',
479
+ 'const fs = require(\'fs\');',
480
+ 'const path = require(\'path\');',
481
+ 'const { spawnSync } = require(\'child_process\');',
482
+ '',
483
+ 'const PROJECT_ROOT = path.resolve(__dirname, \'..\');',
484
+ 'const RAW_ARGS = process.argv.slice(2);',
485
+ 'const ACTION = RAW_ARGS[0] || \'explain\';',
486
+ `const SLASH_ACTIONS = ${slashSpecsJson};`,
487
+ 'const SLASH_ROOT_ALIASES = new Set([\'/road\', \'/roadmap-sync\']);',
488
+ 'const DIRECT_SLASH_ALIAS_TO_ACTION = {',
489
+ ' \'/zero\': \'zero\',',
490
+ ' \'/maintain\': \'maintain\',',
491
+ ' \'/status\': \'status\',',
492
+ ' \'/init\': \'init\',',
493
+ ' \'/generate\': \'generate\',',
494
+ ' \'/validate\': \'validate\',',
495
+ ' \'/sync\': \'sync\',',
496
+ ' \'/audit\': \'audit\',',
497
+ ' \'/setup\': \'setup\'',
498
+ '};',
499
+ 'const LOCAL_DEV_CLI = path.join(PROJECT_ROOT, \'roadmap-skill\', \'bin\', \'cli.js\');',
500
+ 'const LOCAL_PACKAGE_CLI = path.join(PROJECT_ROOT, \'node_modules\', \'roadmapsmith\', \'bin\', \'cli.js\');',
501
+ '',
502
+ 'function candidate(kind, cliPath) {',
503
+ ' return { kind, execPath: process.execPath, prefixArgs: [cliPath], shell: false, displayPath: cliPath };',
504
+ '}',
505
+ '',
506
+ 'function findGlobalCommandPath() {',
507
+ ' const probe = process.platform === \'win32\'',
508
+ ' ? spawnSync(\'where\', [\'roadmapsmith\'], { encoding: \'utf8\' })',
509
+ ' : spawnSync(\'which\', [\'roadmapsmith\'], { encoding: \'utf8\' });',
510
+ ' if (probe.status !== 0 || !probe.stdout) {',
511
+ ' return null;',
512
+ ' }',
513
+ ' const firstLine = probe.stdout.split(/\\r?\\n/).map((line) => line.trim()).find(Boolean);',
514
+ ' return firstLine || null;',
515
+ '}',
516
+ '',
517
+ 'function resolveCli() {',
518
+ ' if (fs.existsSync(LOCAL_DEV_CLI)) {',
519
+ ' return candidate(\'workspace-dev-copy\', LOCAL_DEV_CLI);',
520
+ ' }',
521
+ ' if (fs.existsSync(LOCAL_PACKAGE_CLI)) {',
522
+ ' return candidate(\'workspace-dependency\', LOCAL_PACKAGE_CLI);',
523
+ ' }',
524
+ ' const globalPath = findGlobalCommandPath();',
525
+ ' if (globalPath) {',
526
+ ' return { kind: \'global\', execPath: globalPath, prefixArgs: [], shell: process.platform === \'win32\', displayPath: globalPath };',
527
+ ' }',
528
+ ' return null;',
529
+ '}',
530
+ '',
531
+ 'function normalizeActionId(value) {',
532
+ ' return String(value || \'\').trim().toLowerCase().replace(/^\\/+/g, \'\');',
533
+ '}',
534
+ '',
535
+ 'function getSlashSuggestions(query) {',
536
+ ' const normalized = normalizeActionId(query);',
537
+ ' if (!normalized) {',
538
+ ' return SLASH_ACTIONS.slice();',
539
+ ' }',
540
+ ' const startsWithMatches = SLASH_ACTIONS.filter((action) => action.id.startsWith(normalized));',
541
+ ' const containsMatches = SLASH_ACTIONS.filter((action) => !action.id.startsWith(normalized) && action.id.includes(normalized));',
542
+ ' return [...startsWithMatches, ...containsMatches];',
543
+ '}',
544
+ '',
545
+ 'function renderSlashPalette(route) {',
546
+ ' const source = route && route.source ? route.source : \'/road\';',
547
+ ' const query = normalizeActionId(route && route.query);',
548
+ ' const suggestions = route && Array.isArray(route.suggestions) ? route.suggestions : getSlashSuggestions(query);',
549
+ ' const lines = [];',
550
+ ' lines.push(\'RoadmapSmith slash palette\');',
551
+ ' lines.push(\'\');',
552
+ ' if (query) {',
553
+ ' lines.push(`Input: ${source} ${query}`);',
554
+ ' lines.push(suggestions.length > 0 ? \'No exact slash match was executed. Related actions:\' : \'No exact slash match was executed.\');',
555
+ ' } else {',
556
+ ' lines.push(`Entry point: ${source}`);',
557
+ ' lines.push(\'Use an exact slash action to execute work. Incomplete or ambiguous input only shows suggestions.\');',
558
+ ' }',
559
+ ' lines.push(\'\');',
560
+ ' if (suggestions.length === 0) {',
561
+ ' lines.push(\'No related slash actions found.\');',
562
+ ' } else {',
563
+ ' suggestions.forEach((action) => {',
564
+ ' lines.push(`- /${action.id}: ${action.description}`);',
565
+ ' lines.push(` Classic CLI: ${action.classicCliExample}`);',
566
+ ' lines.push(` Skill form: /roadmap-sync ${action.id}`);',
567
+ ' lines.push(` VS Code task: ${action.taskLabel}`);',
568
+ ' });',
569
+ ' }',
570
+ ' lines.push(\'\');',
571
+ ' lines.push(\'Examples:\');',
572
+ ' lines.push(\'- roadmapsmith zero\');',
573
+ ' lines.push(\'- roadmapsmith maintain\');',
574
+ ' lines.push(\'- roadmapsmith /road\');',
575
+ ' lines.push(\'- roadmapsmith /maintain\');',
576
+ ' lines.push(\'- roadmapsmith /roadmap-sync maintain\');',
577
+ ' lines.push(\'\');',
578
+ ' lines.push(\'Installing the skill alone does not expose CLI behavior in VS Code. Use roadmapsmith setup for the visible task/launcher layer.\');',
579
+ ' return lines.join(\'\\n\');',
580
+ '}',
581
+ '',
582
+ 'function resolveSlashInvocation(command, args) {',
583
+ ' if (typeof command !== \'string\' || !command.trim().startsWith(\'/\')) {',
584
+ ' return null;',
585
+ ' }',
586
+ ' const normalizedCommand = command.trim().toLowerCase();',
587
+ ' if (Object.prototype.hasOwnProperty.call(DIRECT_SLASH_ALIAS_TO_ACTION, normalizedCommand)) {',
588
+ ' return {',
589
+ ' kind: \'execute\',',
590
+ ' actionId: DIRECT_SLASH_ALIAS_TO_ACTION[normalizedCommand],',
591
+ ' query: normalizeActionId(normalizedCommand),',
592
+ ' source: normalizedCommand,',
593
+ ' suggestions: getSlashSuggestions(normalizedCommand)',
594
+ ' };',
595
+ ' }',
596
+ ' if (SLASH_ROOT_ALIASES.has(normalizedCommand)) {',
597
+ ' const queryToken = args.length > 0 ? normalizeActionId(args[0]) : \'\';',
598
+ ' if (!queryToken) {',
599
+ ' return { kind: \'palette\', query: \'\', source: normalizedCommand, suggestions: getSlashSuggestions(\'\') };',
600
+ ' }',
601
+ ' const exactAction = SLASH_ACTIONS.find((action) => action.id === queryToken);',
602
+ ' if (exactAction) {',
603
+ ' return { kind: \'execute\', actionId: exactAction.id, query: queryToken, source: normalizedCommand, suggestions: getSlashSuggestions(queryToken) };',
604
+ ' }',
605
+ ' return { kind: \'palette\', query: queryToken, source: normalizedCommand, suggestions: getSlashSuggestions(queryToken) };',
606
+ ' }',
607
+ ' return { kind: \'palette\', query: normalizeActionId(normalizedCommand), source: normalizedCommand, suggestions: getSlashSuggestions(normalizedCommand) };',
608
+ '}',
609
+ '',
610
+ 'function explain() {',
611
+ ' console.log(\'RoadmapSmith layers:\\n\');',
612
+ ' console.log(\'1. The roadmap-sync skill guides the agent. It does not add VS Code buttons or install the CLI.\');',
613
+ ' console.log(\'2. The roadmapsmith CLI executes zero/maintain plus the lower-level init/generate/validate/sync/setup/doctor commands.\');',
614
+ ' console.log(\'3. roadmapsmith setup makes the CLI visible in VS Code through tasks and optional Claude hook wiring.\\n\');',
615
+ ' console.log(\'Typical VS Code workflow:\');',
616
+ ' console.log(\'- Run "RoadmapSmith: Status" to inspect readiness.\');',
617
+ ' console.log(\'- For empty repos, run "RoadmapSmith: Zero Mode" or use "/road zero".\');',
618
+ ' console.log(\'- For existing repos, run "RoadmapSmith: Maintain" or use "/road maintain".\');',
619
+ ' console.log(\'- Use the lower-level Init, Generate, Validate, and Sync tasks only when you want manual control.\\n\');',
620
+ ' console.log(\'If you installed only the skill, install the CLI as well and then run "RoadmapSmith: Refresh Setup".\');',
621
+ '}',
622
+ '',
623
+ 'function printStatusFromDoctor(payload) {',
624
+ ' console.log(\'RoadmapSmith status\\n\');',
625
+ ' if (!payload || !payload.cli || !payload.vscode || !payload.hosts) {',
626
+ ' console.log(\'Doctor could not inspect the full host setup. Raw payload follows:\\n\');',
627
+ ' console.log(JSON.stringify(payload, null, 2));',
628
+ ' return;',
629
+ ' }',
630
+ ' console.log(`Project root: ${payload.projectRoot}`);',
631
+ ' console.log(`CLI resolution: ${payload.cli.kind}${payload.cli.path ? ` (${payload.cli.path})` : \'\'}${payload.cli.ready ? \'\' : \' [missing]\'}`);',
632
+ ' console.log(`Roadmap file: ${payload.roadmap.exists ? \'ready\' : \'missing\'} (${payload.roadmap.path})`);',
633
+ ' console.log(`Agent rules: ${payload.agents.exists ? \'ready\' : \'missing\'} (${payload.agents.path})`);',
634
+ ' console.log(`VS Code launcher: ${payload.vscode.launcher.exists ? \'ready\' : \'missing\'} (${payload.vscode.launcher.path})`);',
635
+ ' console.log(`VS Code task wrappers: ${payload.vscode.wrappers.ready ? \'ready\' : \'incomplete\'} (${payload.vscode.wrappers.presentCount}/${payload.vscode.wrappers.expectedCount} files)`);',
636
+ ' console.log(`VS Code tasks: ${payload.vscode.tasks.ready ? \'ready\' : \'incomplete\'} (${payload.vscode.tasks.presentLabels.length}/${payload.vscode.tasks.expectedLabels.length} tasks)`);',
637
+ ' console.log(`Node runtime: ${payload.runtime.ready ? `ready (${payload.runtime.kind}${payload.runtime.path ? `: ${payload.runtime.path}` : \'\'})` : \'missing\'}`);',
638
+ ' if (!payload.vscode.tasks.ready && payload.vscode.tasks.missingLabels.length > 0) {',
639
+ ' console.log(`Missing VS Code tasks: ${payload.vscode.tasks.missingLabels.join(\', \')}`);',
640
+ ' }',
641
+ ' if (!payload.vscode.wrappers.ready) {',
642
+ ' console.log(`Missing task wrapper files: ${payload.vscode.wrappers.missingPaths.join(\', \')}`);',
643
+ ' }',
644
+ ' console.log(`Codex readiness: ${payload.hosts.codex.ready ? \'ready\' : \'needs setup\'} (${payload.hosts.codex.message})`);',
645
+ ' console.log(`Claude readiness: ${payload.hosts.claude.ready ? \'ready\' : \'needs setup\'} (${payload.hosts.claude.message})`);',
646
+ ' if (!payload.cli.ready) {',
647
+ ' console.log(\'\\nThe CLI is missing. Installing the skill alone does not expose RoadmapSmith actions in VS Code.\');',
648
+ ' console.log(\'Install the CLI, then run "RoadmapSmith: Refresh Setup".\');',
649
+ ' }',
650
+ ' if (!payload.runtime.ready) {',
651
+ ' console.log(\'\\nThe VS Code task runtime is missing. Install Node.js or set ROADMAPSMITH_NODE, then rerun "RoadmapSmith: Status".\');',
652
+ ' }',
653
+ ' console.log(\'\\nRecommended entrypoints: roadmapsmith zero, roadmapsmith maintain\');',
654
+ ' console.log(\'Slash entrypoints: /road, /zero, /maintain, /status, /generate, /validate, /sync, /audit, /setup, /roadmap-sync <action>\');',
655
+ '}',
656
+ '',
657
+ 'function printMissingCliStatus() {',
658
+ ' console.log(\'RoadmapSmith status\\n\');',
659
+ ' console.log(`Project root: ${PROJECT_ROOT}`);',
660
+ ' console.log(\'CLI resolution: missing\');',
661
+ ' console.log(\'VS Code tasks are visible because setup generated this launcher, but no RoadmapSmith CLI could be resolved.\');',
662
+ ' console.log(\'Installing the skill alone does not expose the CLI in VS Code.\');',
663
+ ' console.log(\'Install the roadmapsmith package, then run "RoadmapSmith: Refresh Setup".\');',
664
+ ' console.log(\'The launcher looks for, in order: workspace dev copy, workspace dependency, global command.\');',
665
+ ' console.log(\'Slash discovery still works here: try /road for the local palette.\');',
666
+ '}',
667
+ '',
668
+ 'function runCli(args, options = {}) {',
669
+ ' const resolution = resolveCli();',
670
+ ' if (!resolution) {',
671
+ ' if (options.allowMissingCli) {',
672
+ ' return { status: 0, stdout: \'\', stderr: \'\', missingCli: true };',
673
+ ' }',
674
+ ' console.error(\'RoadmapSmith CLI not found. Install the CLI and rerun setup.\');',
675
+ ' process.exitCode = 1;',
676
+ ' return null;',
677
+ ' }',
678
+ ' const result = spawnSync(resolution.execPath, [...resolution.prefixArgs, ...args], {',
679
+ ' cwd: PROJECT_ROOT,',
680
+ ' encoding: \'utf8\',',
681
+ ' shell: resolution.shell,',
682
+ ' stdio: options.capture ? \'pipe\' : \'inherit\'',
683
+ ' });',
684
+ ' return { ...result, resolution };',
685
+ '}',
686
+ '',
687
+ 'function forwardResult(result) {',
688
+ ' if (!result) {',
689
+ ' return;',
690
+ ' }',
691
+ ' if (typeof result.status === \'number\' && result.status !== 0) {',
692
+ ' process.exitCode = result.status;',
693
+ ' }',
694
+ '}',
695
+ '',
696
+ 'function status() {',
697
+ ' const result = runCli([\'doctor\', \'--project-root\', PROJECT_ROOT, \'--json\'], { capture: true, allowMissingCli: true });',
698
+ ' if (!result || result.missingCli) {',
699
+ ' printMissingCliStatus();',
700
+ ' return;',
701
+ ' }',
702
+ ' if (result.stdout) {',
703
+ ' try {',
704
+ ' printStatusFromDoctor(JSON.parse(result.stdout));',
705
+ ' return;',
706
+ ' } catch (_) {}',
707
+ ' }',
708
+ ' if (result.stdout) process.stdout.write(result.stdout);',
709
+ ' if (result.stderr) process.stderr.write(result.stderr);',
710
+ ' process.exitCode = 0;',
711
+ '}',
712
+ '',
713
+ 'const actionToCliArgs = {',
714
+ ' zero: [\'zero\', \'--project-root\', PROJECT_ROOT],',
715
+ ' maintain: [\'maintain\', \'--project-root\', PROJECT_ROOT],',
716
+ ' init: [\'init\'],',
717
+ ' generate: [\'generate\', \'--project-root\', PROJECT_ROOT],',
718
+ ' validate: [\'validate\', \'--json\', \'--project-root\', PROJECT_ROOT],',
719
+ ' sync: [\'sync\', \'--project-root\', PROJECT_ROOT],',
720
+ ' audit: [\'sync\', \'--audit\', \'--project-root\', PROJECT_ROOT],',
721
+ ' \'sync-dry-run\': [\'sync\', \'--dry-run\', \'--project-root\', PROJECT_ROOT],',
722
+ ' \'sync-audit\': [\'sync\', \'--audit\', \'--project-root\', PROJECT_ROOT],',
723
+ ' setup: [\'setup\', \'--project-root\', PROJECT_ROOT]',
724
+ '};',
725
+ '',
726
+ 'const slashInvocation = resolveSlashInvocation(ACTION, RAW_ARGS.slice(1));',
727
+ '',
728
+ 'if (slashInvocation) {',
729
+ ' if (slashInvocation.kind === \'palette\') {',
730
+ ' console.log(renderSlashPalette(slashInvocation));',
731
+ ' } else if (slashInvocation.actionId === \'status\') {',
732
+ ' status();',
733
+ ' } else if (Object.prototype.hasOwnProperty.call(actionToCliArgs, slashInvocation.actionId)) {',
734
+ ' const result = runCli(actionToCliArgs[slashInvocation.actionId]);',
735
+ ' forwardResult(result);',
736
+ ' } else {',
737
+ ' console.log(renderSlashPalette(slashInvocation));',
738
+ ' }',
739
+ '} else if (ACTION === \'explain\') {',
740
+ ' explain();',
741
+ '} else if (ACTION === \'status\') {',
742
+ ' status();',
743
+ '} else if (Object.prototype.hasOwnProperty.call(actionToCliArgs, ACTION)) {',
744
+ ' const result = runCli(actionToCliArgs[ACTION]);',
745
+ ' forwardResult(result);',
746
+ '} else {',
747
+ ' console.error(`Unknown RoadmapSmith launcher action: ${ACTION}`);',
748
+ ' process.exitCode = 1;',
749
+ '}',
750
+ ''
751
+ ].join('\n');
752
+ }
753
+
754
+ function buildSetupFiles(projectRoot, options = {}) {
755
+ const editor = assertSupportedEditor(options.editor);
756
+ const hosts = parseHosts(options.hosts);
757
+ if (editor !== 'vscode') {
758
+ throw new Error(`Unsupported editor "${editor}"`);
759
+ }
760
+
761
+ const vscodeTasksPath = path.join(projectRoot, '.vscode', 'tasks.json');
762
+ const vscodeLauncherPath = path.join(projectRoot, VSCODE_LAUNCHER_RELATIVE_PATH);
763
+ const windowsTaskWrapperPath = path.join(projectRoot, WINDOWS_TASK_WRAPPER_RELATIVE_PATH);
764
+ const posixTaskWrapperPath = path.join(projectRoot, POSIX_TASK_WRAPPER_RELATIVE_PATH);
765
+ const files = [
766
+ {
767
+ path: vscodeTasksPath,
768
+ content: stringifyJson(mergeVsCodeTasks(readJsoncObject(vscodeTasksPath)))
769
+ },
770
+ {
771
+ path: vscodeLauncherPath,
772
+ content: renderVsCodeLauncher()
773
+ },
774
+ {
775
+ path: windowsTaskWrapperPath,
776
+ content: renderWindowsTaskWrapper()
777
+ },
778
+ {
779
+ path: posixTaskWrapperPath,
780
+ content: renderPosixTaskWrapper()
781
+ }
782
+ ];
783
+
784
+ if (hosts.includes('claude')) {
785
+ const claudeSettingsPath = path.join(projectRoot, '.claude', 'settings.json');
786
+ const claudeHookPath = path.join(projectRoot, CLAUDE_HOOK_RELATIVE_PATH);
787
+ files.push({
788
+ path: claudeSettingsPath,
789
+ content: stringifyJson(mergeClaudeSettings(readJsoncObject(claudeSettingsPath)))
790
+ });
791
+ files.push({
792
+ path: claudeHookPath,
793
+ content: renderClaudeHookScript()
794
+ });
795
+ }
796
+
797
+ return {
798
+ editor,
799
+ hosts,
800
+ files
801
+ };
802
+ }
803
+
804
+ function applySetupFiles(setupPlan, options = {}) {
805
+ return setupPlan.files.map((file) => {
806
+ return writeText(file.path, file.content, { dryRun: options.dryRun });
807
+ });
808
+ }
809
+
810
+ function findGlobalRoadmapsmith() {
811
+ const probe = process.platform === 'win32'
812
+ ? require('child_process').spawnSync('where', ['roadmapsmith'], { encoding: 'utf8' })
813
+ : require('child_process').spawnSync('which', ['roadmapsmith'], { encoding: 'utf8' });
814
+ if (probe.status !== 0 || !probe.stdout) {
815
+ return null;
816
+ }
817
+ return probe.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || null;
818
+ }
819
+
820
+ function detectCliResolution(projectRoot) {
821
+ const workspaceDevCli = path.join(projectRoot, 'roadmap-skill', 'bin', 'cli.js');
822
+ if (fs.existsSync(workspaceDevCli)) {
823
+ return {
824
+ ready: true,
825
+ kind: 'workspace-dev-copy',
826
+ path: workspaceDevCli
827
+ };
828
+ }
829
+
830
+ const workspaceDependencyCli = path.join(projectRoot, 'node_modules', 'roadmapsmith', 'bin', 'cli.js');
831
+ if (fs.existsSync(workspaceDependencyCli)) {
832
+ return {
833
+ ready: true,
834
+ kind: 'workspace-dependency',
835
+ path: workspaceDependencyCli
836
+ };
837
+ }
838
+
839
+ const globalCommandPath = findGlobalRoadmapsmith();
840
+ if (globalCommandPath) {
841
+ return {
842
+ ready: true,
843
+ kind: 'global',
844
+ path: globalCommandPath
845
+ };
846
+ }
847
+
848
+ return {
849
+ ready: false,
850
+ kind: 'missing',
851
+ path: null
852
+ };
853
+ }
854
+
855
+ function inspectVsCodeTasks(projectRoot) {
856
+ const tasksPath = path.join(projectRoot, '.vscode', 'tasks.json');
857
+ const launcherPath = path.join(projectRoot, VSCODE_LAUNCHER_RELATIVE_PATH);
858
+ const windowsWrapperPath = path.join(projectRoot, WINDOWS_TASK_WRAPPER_RELATIVE_PATH);
859
+ const posixWrapperPath = path.join(projectRoot, POSIX_TASK_WRAPPER_RELATIVE_PATH);
860
+ const tasksConfig = readJsoncObject(tasksPath);
861
+ const tasks = Array.isArray(tasksConfig && tasksConfig.tasks) ? tasksConfig.tasks : [];
862
+ const presentLabels = tasks.map((task) => task && task.label).filter(Boolean);
863
+ const presentManagedLabels = ROADMAPSMITH_TASK_LABELS.filter((label) => presentLabels.includes(label));
864
+ const wrapperEntries = [
865
+ { path: windowsWrapperPath, exists: fs.existsSync(windowsWrapperPath) },
866
+ { path: posixWrapperPath, exists: fs.existsSync(posixWrapperPath) }
867
+ ];
868
+ const presentWrappers = wrapperEntries.filter((entry) => entry.exists);
869
+
870
+ return {
871
+ launcher: {
872
+ path: launcherPath,
873
+ exists: fs.existsSync(launcherPath)
874
+ },
875
+ wrappers: {
876
+ expectedCount: wrapperEntries.length,
877
+ presentCount: presentWrappers.length,
878
+ ready: wrapperEntries.every((entry) => entry.exists),
879
+ windows: wrapperEntries[0],
880
+ posix: wrapperEntries[1],
881
+ missingPaths: wrapperEntries.filter((entry) => !entry.exists).map((entry) => entry.path)
882
+ },
883
+ tasks: {
884
+ path: tasksPath,
885
+ exists: tasksConfig != null,
886
+ ready: ROADMAPSMITH_TASK_LABELS.every((label) => presentLabels.includes(label)) && fs.existsSync(launcherPath) && wrapperEntries.every((entry) => entry.exists),
887
+ expectedLabels: ROADMAPSMITH_TASK_LABELS.slice(),
888
+ presentLabels: presentManagedLabels,
889
+ missingLabels: ROADMAPSMITH_TASK_LABELS.filter((label) => !presentLabels.includes(label))
890
+ }
891
+ };
892
+ }
893
+
894
+ function inspectClaudeSetup(projectRoot) {
895
+ const settingsPath = path.join(projectRoot, '.claude', 'settings.json');
896
+ const hookPath = path.join(projectRoot, CLAUDE_HOOK_RELATIVE_PATH);
897
+ const settings = readJsoncObject(settingsPath);
898
+ const postToolUse = Array.isArray(settings && settings.hooks && settings.hooks.PostToolUse)
899
+ ? settings.hooks.PostToolUse
900
+ : [];
901
+ const configured = postToolUse.some((entry) => isRoadmapSmithHookEntry(entry));
902
+
903
+ return {
904
+ settings: {
905
+ path: settingsPath,
906
+ exists: settings != null
907
+ },
908
+ hookFile: {
909
+ path: hookPath,
910
+ exists: fs.existsSync(hookPath)
911
+ },
912
+ configured,
913
+ ready: configured && fs.existsSync(hookPath)
914
+ };
915
+ }
916
+
917
+ function inspectHostSetup(projectRoot, options = {}) {
918
+ const roadmapFile = options.roadmapFile;
919
+ const agentsFile = options.agentsFile;
920
+ const runtime = detectNodeRuntime(options.env || process.env);
921
+ const cli = detectCliResolution(projectRoot);
922
+ const vscode = inspectVsCodeTasks(projectRoot);
923
+ const claude = inspectClaudeSetup(projectRoot);
924
+ const codexReady = cli.ready && vscode.tasks.ready && runtime.ready;
925
+ let codexMessage = 'VS Code tasks are ready for Codex/manual host workflows';
926
+ if (!cli.ready) {
927
+ codexMessage = 'Install the RoadmapSmith CLI and rerun setup';
928
+ } else if (!vscode.tasks.ready) {
929
+ codexMessage = 'Run roadmapsmith setup to regenerate the VS Code task surface';
930
+ } else if (!runtime.ready) {
931
+ codexMessage = 'Install Node.js or set ROADMAPSMITH_NODE so VS Code tasks can launch the wrapper';
932
+ }
933
+
934
+ return {
935
+ projectRoot,
936
+ cli,
937
+ runtime,
938
+ roadmap: {
939
+ path: roadmapFile,
940
+ exists: roadmapFile ? fs.existsSync(roadmapFile) : false
941
+ },
942
+ agents: {
943
+ path: agentsFile,
944
+ exists: agentsFile ? fs.existsSync(agentsFile) : false
945
+ },
946
+ vscode,
947
+ claude,
948
+ hosts: {
949
+ codex: {
950
+ ready: codexReady,
951
+ message: codexMessage
952
+ },
953
+ claude: {
954
+ ready: cli.ready && claude.ready,
955
+ message: cli.ready && claude.ready
956
+ ? 'Claude PostToolUse hook is configured'
957
+ : 'Run roadmapsmith setup with the claude host and verify node is available to Claude'
958
+ }
959
+ }
960
+ };
961
+ }
962
+
963
+ module.exports = {
964
+ CLAUDE_HOOK_COMMAND,
965
+ ROADMAPSMITH_TASK_LABELS,
966
+ applySetupFiles,
967
+ assertSupportedEditor,
968
+ buildSetupFiles,
969
+ detectNodeRuntime,
970
+ detectCliResolution,
971
+ inspectHostSetup,
972
+ mergeClaudeSettings,
973
+ mergeVsCodeTasks,
974
+ parseHosts,
975
+ parseJsonc,
976
+ readJsoncObject,
977
+ renderClaudeHookScript,
978
+ renderPosixTaskWrapper,
979
+ renderVsCodeLauncher,
980
+ renderWindowsTaskWrapper,
981
+ stringifyJson
982
+ };