@lipter7/blueprint 2.0.0 → 2.0.1
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 +20 -16
- package/bin/install.js +717 -37
- package/bin/install.test.js +870 -0
- package/blueprint/bin/blueprint-tools.js +250 -0
- package/blueprint/bin/blueprint-tools.test.js +443 -0
- package/blueprint/references/planning-config.md +6 -6
- package/blueprint/references/verification-gates.md +346 -0
- package/blueprint/templates/codebase/architecture.md +2 -2
- package/blueprint/templates/codebase/structure.md +3 -3
- package/blueprint/templates/config.json +7 -1
- package/blueprint/templates/state.md +31 -0
- package/blueprint/workflows/complete-milestone.md +94 -5
- package/blueprint/workflows/discuss-phase.md +86 -21
- package/blueprint/workflows/map-codebase.md +26 -0
- package/blueprint/workflows/new-milestone.md +219 -1
- package/blueprint/workflows/new-project.md +407 -67
- package/blueprint/workflows/plan-phase.md +108 -4
- package/blueprint/workflows/progress.md +1 -1
- package/blueprint/workflows/research-phase.md +216 -1
- package/blueprint/workflows/settings-cursor.md +254 -0
- package/blueprint/workflows/settings.md +2 -2
- package/blueprint/workflows/verify-work.md +49 -1
- package/commands/bp/add-phase.md +1 -1
- package/commands/bp/add-todo.md +1 -1
- package/commands/bp/audit-milestone.md +1 -1
- package/commands/bp/check-todos.md +1 -1
- package/commands/bp/complete-milestone.md +1 -1
- package/commands/bp/debug.md +18 -1
- package/commands/bp/discuss-phase.md +1 -1
- package/commands/bp/execute-phase.md +1 -1
- package/commands/bp/help.md +1 -1
- package/commands/bp/insert-phase.md +1 -1
- package/commands/bp/join-discord.md +1 -1
- package/commands/bp/list-phase-assumptions.md +1 -1
- package/commands/bp/map-codebase.md +1 -1
- package/commands/bp/new-milestone.md +1 -1
- package/commands/bp/new-project.md +1 -1
- package/commands/bp/pause-work.md +1 -1
- package/commands/bp/plan-milestone-gaps.md +1 -1
- package/commands/bp/plan-phase.md +1 -1
- package/commands/bp/progress.md +1 -1
- package/commands/bp/quick.md +1 -1
- package/commands/bp/remove-phase.md +1 -1
- package/commands/bp/research-phase.md +1 -1
- package/commands/bp/resume-work.md +1 -1
- package/commands/bp/set-profile.md +1 -1
- package/commands/bp/settings.md +1 -1
- package/commands/bp/verify-work.md +1 -1
- package/package.json +1 -1
package/bin/install.js
CHANGED
|
@@ -23,6 +23,7 @@ const hasLocal = args.includes('--local') || args.includes('-l');
|
|
|
23
23
|
const hasOpencode = args.includes('--opencode');
|
|
24
24
|
const hasClaude = args.includes('--claude');
|
|
25
25
|
const hasGemini = args.includes('--gemini');
|
|
26
|
+
const hasCursor = args.includes('--cursor');
|
|
26
27
|
const hasBoth = args.includes('--both'); // Legacy flag, keeps working
|
|
27
28
|
const hasAll = args.includes('--all');
|
|
28
29
|
const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
@@ -30,19 +31,21 @@ const hasUninstall = args.includes('--uninstall') || args.includes('-u');
|
|
|
30
31
|
// Runtime selection - can be set by flags or interactive prompt
|
|
31
32
|
let selectedRuntimes = [];
|
|
32
33
|
if (hasAll) {
|
|
33
|
-
selectedRuntimes = ['claude', 'opencode', 'gemini'];
|
|
34
|
+
selectedRuntimes = ['claude', 'opencode', 'gemini', 'cursor'];
|
|
34
35
|
} else if (hasBoth) {
|
|
35
36
|
selectedRuntimes = ['claude', 'opencode'];
|
|
36
37
|
} else {
|
|
37
38
|
if (hasOpencode) selectedRuntimes.push('opencode');
|
|
38
39
|
if (hasClaude) selectedRuntimes.push('claude');
|
|
39
40
|
if (hasGemini) selectedRuntimes.push('gemini');
|
|
41
|
+
if (hasCursor) selectedRuntimes.push('cursor');
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// Helper to get directory name for a runtime (used for local/project installs)
|
|
43
45
|
function getDirName(runtime) {
|
|
44
46
|
if (runtime === 'opencode') return '.opencode';
|
|
45
47
|
if (runtime === 'gemini') return '.gemini';
|
|
48
|
+
if (runtime === 'cursor') return '.cursor';
|
|
46
49
|
return '.claude';
|
|
47
50
|
}
|
|
48
51
|
|
|
@@ -95,7 +98,18 @@ function getGlobalDir(runtime, explicitDir = null) {
|
|
|
95
98
|
}
|
|
96
99
|
return path.join(os.homedir(), '.gemini');
|
|
97
100
|
}
|
|
98
|
-
|
|
101
|
+
|
|
102
|
+
if (runtime === 'cursor') {
|
|
103
|
+
// Cursor: --config-dir > CURSOR_CONFIG_DIR > ~/.cursor
|
|
104
|
+
if (explicitDir) {
|
|
105
|
+
return expandTilde(explicitDir);
|
|
106
|
+
}
|
|
107
|
+
if (process.env.CURSOR_CONFIG_DIR) {
|
|
108
|
+
return expandTilde(process.env.CURSOR_CONFIG_DIR);
|
|
109
|
+
}
|
|
110
|
+
return path.join(os.homedir(), '.cursor');
|
|
111
|
+
}
|
|
112
|
+
|
|
99
113
|
// Claude Code: --config-dir > CLAUDE_CONFIG_DIR > ~/.claude
|
|
100
114
|
if (explicitDir) {
|
|
101
115
|
return expandTilde(explicitDir);
|
|
@@ -116,7 +130,7 @@ const banner = '\n' +
|
|
|
116
130
|
'\n' +
|
|
117
131
|
' Blueprint ' + dim + 'v' + pkg.version + reset + '\n' +
|
|
118
132
|
' A meta-prompting, context engineering and spec-driven\n' +
|
|
119
|
-
' development system for Claude Code, OpenCode, and
|
|
133
|
+
' development system for Claude Code, OpenCode, Gemini, and Cursor by TÂCHES.\n';
|
|
120
134
|
|
|
121
135
|
// Parse --config-dir argument
|
|
122
136
|
function parseConfigDirArg() {
|
|
@@ -150,7 +164,7 @@ console.log(banner);
|
|
|
150
164
|
|
|
151
165
|
// Show help if requested
|
|
152
166
|
if (hasHelp) {
|
|
153
|
-
console.log(` ${yellow}Usage:${reset} npx @lipter7/blueprint [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall Blueprint (remove all Blueprint files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx @lipter7/blueprint\n\n ${dim}# Install for Claude Code globally${reset}\n npx @lipter7/blueprint --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx @lipter7/blueprint --gemini --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx @lipter7/blueprint --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx @lipter7/blueprint --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx @lipter7/blueprint --claude --local\n\n ${dim}# Uninstall Blueprint from Claude Code globally${reset}\n npx @lipter7/blueprint --claude --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR environment variables.\n`);
|
|
167
|
+
console.log(` ${yellow}Usage:${reset} npx @lipter7/blueprint [options]\n\n ${yellow}Options:${reset}\n ${cyan}-g, --global${reset} Install globally (to config directory)\n ${cyan}-l, --local${reset} Install locally (to current directory)\n ${cyan}--claude${reset} Install for Claude Code only\n ${cyan}--opencode${reset} Install for OpenCode only\n ${cyan}--gemini${reset} Install for Gemini only\n ${cyan}--cursor${reset} Install for Cursor only\n ${cyan}--all${reset} Install for all runtimes\n ${cyan}-u, --uninstall${reset} Uninstall Blueprint (remove all Blueprint files)\n ${cyan}-c, --config-dir <path>${reset} Specify custom config directory\n ${cyan}-h, --help${reset} Show this help message\n ${cyan}--force-statusline${reset} Replace existing statusline config\n\n ${yellow}Examples:${reset}\n ${dim}# Interactive install (prompts for runtime and location)${reset}\n npx @lipter7/blueprint\n\n ${dim}# Install for Claude Code globally${reset}\n npx @lipter7/blueprint --claude --global\n\n ${dim}# Install for Gemini globally${reset}\n npx @lipter7/blueprint --gemini --global\n\n ${dim}# Install for Cursor globally${reset}\n npx @lipter7/blueprint --cursor --global\n\n ${dim}# Install for all runtimes globally${reset}\n npx @lipter7/blueprint --all --global\n\n ${dim}# Install to custom config directory${reset}\n npx @lipter7/blueprint --claude --global --config-dir ~/.claude-bc\n\n ${dim}# Install to current project only${reset}\n npx @lipter7/blueprint --claude --local\n\n ${dim}# Uninstall Blueprint from Claude Code globally${reset}\n npx @lipter7/blueprint --claude --global --uninstall\n\n ${yellow}Notes:${reset}\n The --config-dir option is useful when you have multiple configurations.\n It takes priority over CLAUDE_CONFIG_DIR / GEMINI_CONFIG_DIR / CURSOR_CONFIG_DIR environment variables.\n`);
|
|
154
168
|
process.exit(0);
|
|
155
169
|
}
|
|
156
170
|
|
|
@@ -224,6 +238,16 @@ function getCommitAttribution(runtime) {
|
|
|
224
238
|
} else {
|
|
225
239
|
result = settings.attribution.commit;
|
|
226
240
|
}
|
|
241
|
+
} else if (runtime === 'cursor') {
|
|
242
|
+
// Cursor: check cursor settings.json for attribution config
|
|
243
|
+
const settings = readSettings(path.join(getGlobalDir('cursor', explicitConfigDir), 'settings.json'));
|
|
244
|
+
if (!settings.attribution || settings.attribution.commit === undefined) {
|
|
245
|
+
result = undefined;
|
|
246
|
+
} else if (settings.attribution.commit === '') {
|
|
247
|
+
result = null;
|
|
248
|
+
} else {
|
|
249
|
+
result = settings.attribution.commit;
|
|
250
|
+
}
|
|
227
251
|
} else {
|
|
228
252
|
// Claude Code
|
|
229
253
|
const settings = readSettings(path.join(getGlobalDir('claude', explicitConfigDir), 'settings.json'));
|
|
@@ -308,6 +332,438 @@ const claudeToGeminiTools = {
|
|
|
308
332
|
AskUserQuestion: 'ask_user',
|
|
309
333
|
};
|
|
310
334
|
|
|
335
|
+
// Cursor skill ordering — numbered prefixes for palette sorting
|
|
336
|
+
const CURSOR_SKILL_ORDER = {
|
|
337
|
+
'map-codebase': 1,
|
|
338
|
+
'new-project': 2,
|
|
339
|
+
'new-milestone': 3,
|
|
340
|
+
'discuss-phase': 4,
|
|
341
|
+
'research-phase': 5,
|
|
342
|
+
'plan-phase': 6,
|
|
343
|
+
'execute-phase': 7,
|
|
344
|
+
'verify-work': 8,
|
|
345
|
+
'audit-milestone': 9,
|
|
346
|
+
'plan-milestone-gaps': 10,
|
|
347
|
+
'complete-milestone': 11,
|
|
348
|
+
'add-phase': 12,
|
|
349
|
+
'insert-phase': 13,
|
|
350
|
+
'remove-phase': 14,
|
|
351
|
+
'progress': 15,
|
|
352
|
+
'resume-work': 16,
|
|
353
|
+
'pause-work': 17,
|
|
354
|
+
'quick': 18,
|
|
355
|
+
'debug': 19,
|
|
356
|
+
'list-phase-assumptions': 20,
|
|
357
|
+
'add-todo': 21,
|
|
358
|
+
'check-todos': 22,
|
|
359
|
+
'settings': 23,
|
|
360
|
+
'set-profile': 24,
|
|
361
|
+
'update': 25,
|
|
362
|
+
'reapply-patches': 26,
|
|
363
|
+
'help': 27,
|
|
364
|
+
'join-discord': 28,
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const GATE_TEMPLATES = {
|
|
368
|
+
confidence_gate: `
|
|
369
|
+
<cursor_interaction type="confidence_gate" id="{gate_id}">
|
|
370
|
+
IMPORTANT: You MUST use the AskQuestion tool here. Do NOT proceed without user input.
|
|
371
|
+
|
|
372
|
+
Present the following to the user via AskQuestion:
|
|
373
|
+
- Context: {what_was_just_completed}
|
|
374
|
+
- Options: {confidence_options}
|
|
375
|
+
- Wait for response before continuing
|
|
376
|
+
|
|
377
|
+
Based on user's choice:
|
|
378
|
+
- If "{option_a}": {action_a}
|
|
379
|
+
- If "{option_b}": {action_b}
|
|
380
|
+
- If user provides custom input: incorporate their guidance and proceed accordingly
|
|
381
|
+
</cursor_interaction>`,
|
|
382
|
+
|
|
383
|
+
decision_gate: `
|
|
384
|
+
<cursor_interaction type="decision_gate" id="{gate_id}">
|
|
385
|
+
IMPORTANT: You MUST use the AskQuestion tool here. Do NOT proceed without user input.
|
|
386
|
+
|
|
387
|
+
Present the following choice to the user via AskQuestion:
|
|
388
|
+
- Context: {decision_context}
|
|
389
|
+
- Options:
|
|
390
|
+
1. {option_1} — {description_1}
|
|
391
|
+
2. {option_2} — {description_2}
|
|
392
|
+
{option_3_line}
|
|
393
|
+
- Wait for response before continuing
|
|
394
|
+
|
|
395
|
+
Route based on user's choice:
|
|
396
|
+
- "{option_1}": {action_1}
|
|
397
|
+
- "{option_2}": {action_2}
|
|
398
|
+
- Custom input: {fallback_action}
|
|
399
|
+
</cursor_interaction>`,
|
|
400
|
+
|
|
401
|
+
continuation_gate: `
|
|
402
|
+
<cursor_interaction type="continuation_gate" id="{gate_id}">
|
|
403
|
+
IMPORTANT: You MUST use the AskQuestion tool here. Do NOT proceed without user input.
|
|
404
|
+
|
|
405
|
+
Ask the user via AskQuestion:
|
|
406
|
+
- Context: {current_state_summary}
|
|
407
|
+
- Options:
|
|
408
|
+
1. {proceed_option} — {proceed_description}
|
|
409
|
+
2. {continue_option} — {continue_description}
|
|
410
|
+
- Wait for response before continuing
|
|
411
|
+
|
|
412
|
+
If "{continue_option}": loop back to {loop_target_step}
|
|
413
|
+
If "{proceed_option}": continue to next step
|
|
414
|
+
</cursor_interaction>`,
|
|
415
|
+
|
|
416
|
+
action_gate: `
|
|
417
|
+
<cursor_interaction type="action_gate" id="{gate_id}">
|
|
418
|
+
IMPORTANT: You MUST use the AskQuestion tool here. Do NOT proceed without user input.
|
|
419
|
+
|
|
420
|
+
Present available actions to the user via AskQuestion:
|
|
421
|
+
- Context: {action_context}
|
|
422
|
+
- Options:
|
|
423
|
+
1. {action_1} — {description_1}
|
|
424
|
+
2. {action_2} — {description_2}
|
|
425
|
+
3. {action_3} — {description_3}
|
|
426
|
+
{action_4_line}
|
|
427
|
+
- Wait for response before continuing
|
|
428
|
+
|
|
429
|
+
Execute the user's chosen action:
|
|
430
|
+
- "{action_1}": {execute_1}
|
|
431
|
+
- "{action_2}": {execute_2}
|
|
432
|
+
- "{action_3}": {execute_3}
|
|
433
|
+
- Custom input: {fallback_action}
|
|
434
|
+
</cursor_interaction>`
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const DISCUSS_DEEP_DIVE_BLOCK = `
|
|
438
|
+
<cursor_interaction type="deep_dive" id="discuss-area-questioning">
|
|
439
|
+
IMPORTANT: For EACH selected gray area, you MUST conduct a focused discussion using AskQuestion.
|
|
440
|
+
|
|
441
|
+
For each area in the selected list:
|
|
442
|
+
1. Ask 4 specific decision questions about this area using AskQuestion
|
|
443
|
+
- Each question should have 2-3 concrete options
|
|
444
|
+
- Include a "You decide" option when the decision is genuinely discretionary
|
|
445
|
+
- Wait for each response before asking the next question
|
|
446
|
+
2. After all 4 questions for an area, use AskQuestion to ask:
|
|
447
|
+
- "More questions about {area_name}?" / "Move to next area" / "Done discussing"
|
|
448
|
+
3. If "More questions": ask additional questions about this area
|
|
449
|
+
4. If "Move to next area": proceed to the next selected area
|
|
450
|
+
5. If "Done discussing": stop discussion, move to CONTEXT.md creation
|
|
451
|
+
|
|
452
|
+
Record every user decision. Each answer populates a decision record in CONTEXT.md.
|
|
453
|
+
Do NOT skip areas. Do NOT proceed to CONTEXT.md creation until the user says "Done" or all areas are covered.
|
|
454
|
+
</cursor_interaction>`;
|
|
455
|
+
|
|
456
|
+
const SETTINGS_CONFIG_BLOCK = `
|
|
457
|
+
<cursor_interaction type="configuration_chain" id="settings-configuration">
|
|
458
|
+
IMPORTANT: You MUST ask ALL configuration questions using AskQuestion, one at a time. Do NOT skip any.
|
|
459
|
+
|
|
460
|
+
Ask each question in sequence via AskQuestion. Wait for each response before asking the next.
|
|
461
|
+
|
|
462
|
+
1. Per-Agent Model Configuration:
|
|
463
|
+
For each of the 11 agent roles, present available models and let the user select:
|
|
464
|
+
- bp-planner, bp-executor, bp-verifier, bp-debugger, bp-codebase-mapper
|
|
465
|
+
- bp-phase-researcher, bp-project-researcher, bp-research-synthesizer
|
|
466
|
+
- bp-roadmapper, bp-plan-checker, bp-integration-checker
|
|
467
|
+
|
|
468
|
+
2. Plan Researcher (research before planning):
|
|
469
|
+
- "Yes" — Run phase researcher before planner
|
|
470
|
+
- "No" — Skip research, plan from existing context
|
|
471
|
+
|
|
472
|
+
3. Plan Checker (verify plans after creation):
|
|
473
|
+
- "Yes" — Run plan checker after planner
|
|
474
|
+
- "No" — Skip plan verification
|
|
475
|
+
|
|
476
|
+
4. Execution Verifier (verify after execution):
|
|
477
|
+
- "Yes" — Run verifier after executor
|
|
478
|
+
- "No" — Skip execution verification
|
|
479
|
+
|
|
480
|
+
5. Git Branching Strategy:
|
|
481
|
+
- "None (Recommended)" — All work on current branch
|
|
482
|
+
- "Per Phase" — Create branch per phase
|
|
483
|
+
- "Per Milestone" — Create branch per milestone
|
|
484
|
+
|
|
485
|
+
After all responses are collected, write the configuration to .blueprint/config.json with both agent_models and workflow settings.
|
|
486
|
+
</cursor_interaction>`;
|
|
487
|
+
|
|
488
|
+
const DEBUG_SYMPTOMS_BLOCK = `
|
|
489
|
+
<cursor_interaction type="symptom_gathering" id="debug-symptoms">
|
|
490
|
+
IMPORTANT: You MUST ask ALL 5 diagnostic questions using AskQuestion, one at a time. Do NOT skip any. Do NOT start debugging until all 5 are answered.
|
|
491
|
+
|
|
492
|
+
Ask each question in sequence via AskQuestion:
|
|
493
|
+
|
|
494
|
+
1. Expected behavior: "What should happen? Describe the correct behavior."
|
|
495
|
+
(Freeform response — no predefined options)
|
|
496
|
+
|
|
497
|
+
2. Actual behavior: "What happens instead? Describe what you observe."
|
|
498
|
+
(Freeform response)
|
|
499
|
+
|
|
500
|
+
3. Error messages: "Are there any error messages? Paste or describe them."
|
|
501
|
+
(Freeform response — "None" is valid)
|
|
502
|
+
|
|
503
|
+
4. Timeline: "When did this start? Has it ever worked correctly?"
|
|
504
|
+
(Freeform response)
|
|
505
|
+
|
|
506
|
+
5. Reproduction: "How do you trigger this issue? What steps reproduce it?"
|
|
507
|
+
(Freeform response)
|
|
508
|
+
|
|
509
|
+
After collecting all 5 responses, populate the debug session file at .blueprint/debug/{slug}.md with the gathered information, then proceed to hypothesis formation.
|
|
510
|
+
</cursor_interaction>`;
|
|
511
|
+
|
|
512
|
+
// Cursor interaction map — defines where AskUserQuestion blocks appear in workflow files
|
|
513
|
+
// and how to convert them to Cursor-compatible AskQuestion interaction blocks.
|
|
514
|
+
// Each entry has: id, type (gate template or bespoke), marker (regex matching the source block),
|
|
515
|
+
// and either params (for gate templates) or block (for bespoke replacements).
|
|
516
|
+
const CURSOR_INTERACTION_MAP = {
|
|
517
|
+
'workflows/discovery-phase.md': [
|
|
518
|
+
{
|
|
519
|
+
id: 'discovery-confidence',
|
|
520
|
+
type: 'confidence_gate',
|
|
521
|
+
marker: /If confidence is LOW:\nUse AskUserQuestion:[\s\S]*?- "Pause" - I need to think about this/,
|
|
522
|
+
params: {
|
|
523
|
+
gate_id: 'discovery-confidence',
|
|
524
|
+
what_was_just_completed: 'Discovery research completed but confidence is LOW',
|
|
525
|
+
confidence_options: '"Dig deeper" to do more research, or "Proceed anyway" to accept uncertainty',
|
|
526
|
+
option_a: 'Dig deeper',
|
|
527
|
+
action_a: 'Do more research before planning',
|
|
528
|
+
option_b: 'Proceed anyway',
|
|
529
|
+
action_b: 'Accept uncertainty, plan with caveats'
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
],
|
|
533
|
+
'workflows/discuss-phase.md': [
|
|
534
|
+
{
|
|
535
|
+
id: 'discuss-check-existing',
|
|
536
|
+
type: 'decision_gate',
|
|
537
|
+
marker: /\*\*If exists:\*\*\nUse AskUserQuestion:[\s\S]*?If "Skip": Exit workflow/,
|
|
538
|
+
params: {
|
|
539
|
+
gate_id: 'discuss-check-existing',
|
|
540
|
+
decision_context: 'Phase already has existing context (CONTEXT.md found)',
|
|
541
|
+
option_1: 'Update it',
|
|
542
|
+
description_1: 'Review and revise existing context',
|
|
543
|
+
option_2: 'View it',
|
|
544
|
+
description_2: 'Show me what\'s there',
|
|
545
|
+
option_3_line: '3. Skip — Use existing context as-is',
|
|
546
|
+
action_1: 'Load existing context, continue to analyze_phase',
|
|
547
|
+
action_2: 'Display CONTEXT.md, then offer update/skip',
|
|
548
|
+
fallback_action: 'Exit workflow (treat as skip)'
|
|
549
|
+
}
|
|
550
|
+
},
|
|
551
|
+
{
|
|
552
|
+
id: 'discuss-deep-dive',
|
|
553
|
+
type: 'bespoke',
|
|
554
|
+
marker: /\*\*Then use AskUserQuestion \(multiSelect: true\):\*\*[\s\S]*?Continue to discuss_areas with selected areas\.\n<\/step>\n\n<step name="discuss_areas">[\s\S]*?Track deferred ideas internally\.\n<\/step>/,
|
|
555
|
+
block: DISCUSS_DEEP_DIVE_BLOCK
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
id: 'discuss-verify-context',
|
|
559
|
+
type: 'confidence_gate',
|
|
560
|
+
marker: /AskUserQuestion:\n- header: "Context"\n- question: "Does this accurately capture what you described\?"\n- options:[\s\S]*?- "Review full file" — Show me the raw file first/,
|
|
561
|
+
params: {
|
|
562
|
+
gate_id: 'discuss-verify-context',
|
|
563
|
+
what_was_just_completed: 'CONTEXT.md has been created with your implementation decisions',
|
|
564
|
+
confidence_options: '"Approve" to proceed, "Corrections" to change things, or "Review full file" to see the raw file',
|
|
565
|
+
option_a: 'Approve',
|
|
566
|
+
action_a: 'Proceed to git commit',
|
|
567
|
+
option_b: 'Corrections',
|
|
568
|
+
action_b: 'Ask what to change, apply edits, re-present for approval'
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
],
|
|
572
|
+
'workflows/quick.md': [
|
|
573
|
+
{
|
|
574
|
+
id: 'quick-task-description',
|
|
575
|
+
type: 'action_gate',
|
|
576
|
+
marker: /AskUserQuestion\(\n\s*header: "Quick Task",\n\s*question: "What do you want to do\?",\n\s*followUp: null\n\)/,
|
|
577
|
+
params: {
|
|
578
|
+
gate_id: 'quick-task-description',
|
|
579
|
+
action_context: 'Starting a quick task — need task description',
|
|
580
|
+
action_1: 'Describe your task',
|
|
581
|
+
description_1: 'Type what you want to do (freeform)',
|
|
582
|
+
action_2: 'Cancel',
|
|
583
|
+
description_2: 'Exit quick task mode',
|
|
584
|
+
action_3: 'View recent tasks',
|
|
585
|
+
description_3: 'See previously completed quick tasks',
|
|
586
|
+
action_4_line: '',
|
|
587
|
+
execute_1: 'Store response as task description and proceed to initialization',
|
|
588
|
+
execute_2: 'Exit workflow',
|
|
589
|
+
execute_3: 'Show quick task history from STATE.md',
|
|
590
|
+
fallback_action: 'Use input as the task description'
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
],
|
|
594
|
+
'workflows/add-todo.md': [
|
|
595
|
+
{
|
|
596
|
+
id: 'add-todo-duplicate',
|
|
597
|
+
type: 'decision_gate',
|
|
598
|
+
marker: /If overlapping, use AskUserQuestion:[\s\S]*?- "Add anyway" — create as separate todo/,
|
|
599
|
+
params: {
|
|
600
|
+
gate_id: 'add-todo-duplicate',
|
|
601
|
+
decision_context: 'A similar todo already exists',
|
|
602
|
+
option_1: 'Skip',
|
|
603
|
+
description_1: 'Keep existing todo',
|
|
604
|
+
option_2: 'Replace',
|
|
605
|
+
description_2: 'Update existing with new context',
|
|
606
|
+
option_3_line: '3. Add anyway — Create as separate todo',
|
|
607
|
+
action_1: 'Keep existing todo, exit without creating new one',
|
|
608
|
+
action_2: 'Update existing todo file with new context',
|
|
609
|
+
fallback_action: 'Create as separate todo alongside existing one'
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
],
|
|
613
|
+
'workflows/settings.md': [
|
|
614
|
+
{
|
|
615
|
+
id: 'settings-configuration',
|
|
616
|
+
type: 'bespoke',
|
|
617
|
+
marker: /Use AskUserQuestion with current values pre-selected:[\s\S]*?\]\)\n```/,
|
|
618
|
+
block: SETTINGS_CONFIG_BLOCK
|
|
619
|
+
}
|
|
620
|
+
],
|
|
621
|
+
'workflows/complete-milestone.md': [
|
|
622
|
+
{
|
|
623
|
+
id: 'complete-milestone-branches',
|
|
624
|
+
type: 'action_gate',
|
|
625
|
+
marker: /AskUserQuestion with options: Squash merge[\s\S]*?Keep branches\./,
|
|
626
|
+
params: {
|
|
627
|
+
gate_id: 'complete-milestone-branches',
|
|
628
|
+
action_context: 'Git branches detected for completed milestone. Choose how to handle them.',
|
|
629
|
+
action_1: 'Squash merge (Recommended)',
|
|
630
|
+
description_1: 'Merge branches into main with a single squash commit',
|
|
631
|
+
action_2: 'Merge with history',
|
|
632
|
+
description_2: 'Merge branches preserving full commit history',
|
|
633
|
+
action_3: 'Delete without merging',
|
|
634
|
+
description_3: 'Remove branches (already merged or not needed)',
|
|
635
|
+
action_4_line: '4. Keep branches — Leave for manual handling',
|
|
636
|
+
execute_1: 'Squash merge each branch into main',
|
|
637
|
+
execute_2: 'Merge each branch with --no-ff into main',
|
|
638
|
+
execute_3: 'Delete the branch(es)',
|
|
639
|
+
fallback_action: 'Report "Branches preserved for manual handling"'
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
],
|
|
643
|
+
'workflows/new-project.md': [
|
|
644
|
+
{
|
|
645
|
+
id: 'new-project-brownfield',
|
|
646
|
+
type: 'decision_gate',
|
|
647
|
+
marker: /Use AskUserQuestion:\n- header: "Existing Code"[\s\S]*?- "Skip mapping" — Proceed with project initialization/,
|
|
648
|
+
params: {
|
|
649
|
+
gate_id: 'new-project-brownfield',
|
|
650
|
+
decision_context: 'Existing code detected in this directory but no codebase map exists',
|
|
651
|
+
option_1: 'Map codebase first',
|
|
652
|
+
description_1: 'Run /bp:map-codebase to understand existing architecture (Recommended)',
|
|
653
|
+
option_2: 'Skip mapping',
|
|
654
|
+
description_2: 'Proceed with project initialization',
|
|
655
|
+
option_3_line: '',
|
|
656
|
+
action_1: 'Exit and run /bp:map-codebase first, then return to /bp:new-project',
|
|
657
|
+
action_2: 'Continue with project initialization without codebase mapping',
|
|
658
|
+
fallback_action: 'Continue with project initialization'
|
|
659
|
+
}
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
id: 'new-project-ready',
|
|
663
|
+
type: 'continuation_gate',
|
|
664
|
+
marker: /When you could write a clear PROJECT\.md, use AskUserQuestion:[\s\S]*?- "Keep exploring" — I want to share more \/ ask me more/,
|
|
665
|
+
params: {
|
|
666
|
+
gate_id: 'new-project-ready',
|
|
667
|
+
current_state_summary: 'Deep questioning phase — enough context gathered to write PROJECT.md',
|
|
668
|
+
proceed_option: 'Create PROJECT.md',
|
|
669
|
+
proceed_description: 'Let\'s move forward with what we have',
|
|
670
|
+
continue_option: 'Keep exploring',
|
|
671
|
+
continue_description: 'I want to share more / ask me more',
|
|
672
|
+
loop_target_step: 'deep questioning (ask what they want to add, or identify gaps and probe naturally)'
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
],
|
|
676
|
+
'workflows/new-milestone.md': [
|
|
677
|
+
{
|
|
678
|
+
id: 'new-milestone-staleness',
|
|
679
|
+
type: 'decision_gate',
|
|
680
|
+
marker: /Present to the user via `AskUserQuestion`:[\s\S]*?\*\*Options:\*\*\n1\. \*\*Full remap\*\*[\s\S]*?2\. \*\*Skip\*\* — Continue with current codebase docs/,
|
|
681
|
+
params: {
|
|
682
|
+
gate_id: 'new-milestone-staleness',
|
|
683
|
+
decision_context: 'Codebase mapping may be stale — significant changes since last mapping',
|
|
684
|
+
option_1: 'Full remap',
|
|
685
|
+
description_1: 'Re-run all 4 mapping agents (recommended if significant structural changes)',
|
|
686
|
+
option_2: 'Skip',
|
|
687
|
+
description_2: 'Continue with current codebase docs',
|
|
688
|
+
option_3_line: '',
|
|
689
|
+
action_1: 'Spawn 4 bp-codebase-mapper agents in parallel and update mapping metadata',
|
|
690
|
+
action_2: 'Continue to the next step with existing codebase docs',
|
|
691
|
+
fallback_action: 'Continue with existing codebase docs'
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
],
|
|
695
|
+
'workflows/check-todos.md': [
|
|
696
|
+
{
|
|
697
|
+
id: 'check-todos-action',
|
|
698
|
+
type: 'action_gate',
|
|
699
|
+
marker: /Use AskUserQuestion:\n- header: "Action"\n- question: "This todo relates to Phase[\s\S]*?"Put it back" — return to list/,
|
|
700
|
+
params: {
|
|
701
|
+
gate_id: 'check-todos-action',
|
|
702
|
+
action_context: 'Todo selected — choose what to do with it',
|
|
703
|
+
action_1: 'Work on it now',
|
|
704
|
+
description_1: 'Move to done, start working',
|
|
705
|
+
action_2: 'Add to phase plan',
|
|
706
|
+
description_2: 'Include when planning the related phase',
|
|
707
|
+
action_3: 'Brainstorm approach',
|
|
708
|
+
description_3: 'Think through before deciding',
|
|
709
|
+
action_4_line: '4. Put it back — Return to list',
|
|
710
|
+
execute_1: 'Move todo to done/ directory, update STATE.md, begin work',
|
|
711
|
+
execute_2: 'Note todo reference in phase planning notes, keep in pending',
|
|
712
|
+
execute_3: 'Keep in pending, start discussion about problem and approaches',
|
|
713
|
+
fallback_action: 'Return to todo list'
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
],
|
|
717
|
+
'commands/bp/debug.md': [
|
|
718
|
+
{
|
|
719
|
+
id: 'debug-symptoms',
|
|
720
|
+
type: 'bespoke',
|
|
721
|
+
marker: /## 2\. Gather Symptoms \(if new issue\)\n\nUse AskUserQuestion for each:[\s\S]*?After all gathered, confirm ready to investigate\./,
|
|
722
|
+
block: DEBUG_SYMPTOMS_BLOCK
|
|
723
|
+
}
|
|
724
|
+
]
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
function applyInteractionConversions(content, relativePath) {
|
|
728
|
+
const normalizedPath = relativePath
|
|
729
|
+
.replace(/^blueprint\//, '')
|
|
730
|
+
.replace(/^commands\/bp\//, 'commands/bp/');
|
|
731
|
+
|
|
732
|
+
const interactions = CURSOR_INTERACTION_MAP[normalizedPath];
|
|
733
|
+
if (!interactions || interactions.length === 0) {
|
|
734
|
+
return content.replace(/\bAskUserQuestion\b/g, 'AskQuestion');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
let result = content;
|
|
738
|
+
|
|
739
|
+
for (const interaction of interactions) {
|
|
740
|
+
if (interaction.type === 'bespoke') {
|
|
741
|
+
result = result.replace(interaction.marker, interaction.block);
|
|
742
|
+
} else {
|
|
743
|
+
let filled = GATE_TEMPLATES[interaction.type];
|
|
744
|
+
for (const [key, value] of Object.entries(interaction.params)) {
|
|
745
|
+
filled = filled.replace(new RegExp(`\\{${key}\\}`, 'g'), value);
|
|
746
|
+
}
|
|
747
|
+
result = result.replace(interaction.marker, filled);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// Catch any remaining AskUserQuestion references
|
|
752
|
+
result = result.replace(/\bAskUserQuestion\b/g, 'AskQuestion');
|
|
753
|
+
|
|
754
|
+
return result;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function convertCommandReferences(content) {
|
|
758
|
+
return content.replace(/\/bp:([a-z-]+)/g, (match, name) => {
|
|
759
|
+
const num = CURSOR_SKILL_ORDER[name];
|
|
760
|
+
if (num != null) {
|
|
761
|
+
return `/bp-${String(num).padStart(2, '0')}-${name}`;
|
|
762
|
+
}
|
|
763
|
+
return match;
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
311
767
|
/**
|
|
312
768
|
* Convert a Claude Code tool name to OpenCode format
|
|
313
769
|
* - Applies special mappings (AskUserQuestion -> question, etc.)
|
|
@@ -583,6 +1039,169 @@ function convertClaudeToGeminiToml(content) {
|
|
|
583
1039
|
return toml;
|
|
584
1040
|
}
|
|
585
1041
|
|
|
1042
|
+
/**
|
|
1043
|
+
* Convert Claude Code command file to Cursor Skill format
|
|
1044
|
+
* Strips disallowed frontmatter fields and converts name format
|
|
1045
|
+
* @param {string} content - Markdown file content with YAML frontmatter
|
|
1046
|
+
* @param {string} pathPrefix - Path prefix to replace ~/.claude/ with
|
|
1047
|
+
* @returns {string} - Converted content
|
|
1048
|
+
*/
|
|
1049
|
+
function convertClaudeToCursorSkill(content, pathPrefix) {
|
|
1050
|
+
let converted = content;
|
|
1051
|
+
converted = converted.replace(/~\/\.claude\//g, pathPrefix);
|
|
1052
|
+
|
|
1053
|
+
if (!converted.startsWith('---')) return converted;
|
|
1054
|
+
|
|
1055
|
+
const endIndex = converted.indexOf('---', 3);
|
|
1056
|
+
if (endIndex === -1) return converted;
|
|
1057
|
+
|
|
1058
|
+
const frontmatter = converted.substring(3, endIndex).trim();
|
|
1059
|
+
const body = converted.substring(endIndex + 3);
|
|
1060
|
+
|
|
1061
|
+
const lines = frontmatter.split('\n');
|
|
1062
|
+
const newLines = [];
|
|
1063
|
+
let inAllowedTools = false;
|
|
1064
|
+
|
|
1065
|
+
for (const line of lines) {
|
|
1066
|
+
const trimmed = line.trim();
|
|
1067
|
+
|
|
1068
|
+
// Strip allowed-tools YAML array
|
|
1069
|
+
if (trimmed.startsWith('allowed-tools:')) { inAllowedTools = true; continue; }
|
|
1070
|
+
if (inAllowedTools) {
|
|
1071
|
+
if (trimmed.startsWith('- ')) continue;
|
|
1072
|
+
if (trimmed && !trimmed.startsWith('-')) inAllowedTools = false;
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// Strip argument-hint
|
|
1076
|
+
if (trimmed.startsWith('argument-hint:')) continue;
|
|
1077
|
+
|
|
1078
|
+
// Strip agent
|
|
1079
|
+
if (trimmed.startsWith('agent:')) continue;
|
|
1080
|
+
|
|
1081
|
+
// Strip tools (comma-separated string format)
|
|
1082
|
+
if (trimmed.startsWith('tools:')) continue;
|
|
1083
|
+
|
|
1084
|
+
// Convert name from bp:command to bp-command
|
|
1085
|
+
if (trimmed.startsWith('name:')) {
|
|
1086
|
+
const name = trimmed.substring(5).trim().replace(/^bp:/, 'bp-');
|
|
1087
|
+
newLines.push(`name: ${name}`);
|
|
1088
|
+
continue;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
if (!inAllowedTools) newLines.push(line);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Add disable-model-invocation
|
|
1095
|
+
newLines.push('disable-model-invocation: true');
|
|
1096
|
+
|
|
1097
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
1098
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Convert Claude Code agent file to Cursor agent format
|
|
1103
|
+
* Strips disallowed frontmatter fields and adds model: inherit
|
|
1104
|
+
* @param {string} content - Markdown file content with YAML frontmatter
|
|
1105
|
+
* @param {string} pathPrefix - Path prefix to replace ~/.claude/ with
|
|
1106
|
+
* @returns {string} - Converted content
|
|
1107
|
+
*/
|
|
1108
|
+
function convertClaudeToCursorAgent(content, pathPrefix) {
|
|
1109
|
+
let converted = content;
|
|
1110
|
+
converted = converted.replace(/~\/\.claude\//g, pathPrefix);
|
|
1111
|
+
|
|
1112
|
+
if (!converted.startsWith('---')) return converted;
|
|
1113
|
+
|
|
1114
|
+
const endIndex = converted.indexOf('---', 3);
|
|
1115
|
+
if (endIndex === -1) return converted;
|
|
1116
|
+
|
|
1117
|
+
const frontmatter = converted.substring(3, endIndex).trim();
|
|
1118
|
+
const body = converted.substring(endIndex + 3);
|
|
1119
|
+
|
|
1120
|
+
const lines = frontmatter.split('\n');
|
|
1121
|
+
const newLines = [];
|
|
1122
|
+
let inAllowedTools = false;
|
|
1123
|
+
|
|
1124
|
+
for (const line of lines) {
|
|
1125
|
+
const trimmed = line.trim();
|
|
1126
|
+
|
|
1127
|
+
// Strip allowed-tools array
|
|
1128
|
+
if (trimmed.startsWith('allowed-tools:')) { inAllowedTools = true; continue; }
|
|
1129
|
+
if (inAllowedTools) {
|
|
1130
|
+
if (trimmed.startsWith('- ')) continue;
|
|
1131
|
+
if (trimmed && !trimmed.startsWith('-')) inAllowedTools = false;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
// Strip tools (comma-separated)
|
|
1135
|
+
if (trimmed.startsWith('tools:')) {
|
|
1136
|
+
const toolsValue = trimmed.substring(6).trim();
|
|
1137
|
+
if (toolsValue) continue; // inline tools: value
|
|
1138
|
+
inAllowedTools = true; // tools: with YAML array following
|
|
1139
|
+
continue;
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// Strip color
|
|
1143
|
+
if (trimmed.startsWith('color:')) continue;
|
|
1144
|
+
|
|
1145
|
+
if (!inAllowedTools) newLines.push(line);
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// Add model: inherit
|
|
1149
|
+
newLines.push('model: inherit');
|
|
1150
|
+
|
|
1151
|
+
const newFrontmatter = newLines.join('\n').trim();
|
|
1152
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
/**
|
|
1156
|
+
* Copy commands to Cursor's nested skills directory structure
|
|
1157
|
+
* Cursor expects: skills/bp-01-commandname/SKILL.md
|
|
1158
|
+
* Source structure: commands/bp/commandname.md
|
|
1159
|
+
*
|
|
1160
|
+
* @param {string} srcDir - Source directory (e.g., commands/bp/)
|
|
1161
|
+
* @param {string} destDir - Destination directory (e.g., skills/)
|
|
1162
|
+
* @param {string} pathPrefix - Path prefix for file references
|
|
1163
|
+
* @param {string} runtime - Target runtime
|
|
1164
|
+
* @returns {number} - Number of skills installed
|
|
1165
|
+
*/
|
|
1166
|
+
function copySkillsFromCommands(srcDir, destDir, pathPrefix, runtime) {
|
|
1167
|
+
if (!fs.existsSync(srcDir)) return 0;
|
|
1168
|
+
|
|
1169
|
+
// Clean up old skills
|
|
1170
|
+
if (fs.existsSync(destDir)) {
|
|
1171
|
+
for (const dir of fs.readdirSync(destDir)) {
|
|
1172
|
+
if (dir.startsWith('bp-') && fs.statSync(path.join(destDir, dir)).isDirectory()) {
|
|
1173
|
+
fs.rmSync(path.join(destDir, dir), { recursive: true });
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
const entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
1179
|
+
let installedCount = 0;
|
|
1180
|
+
|
|
1181
|
+
for (const entry of entries) {
|
|
1182
|
+
if (!entry.isFile() || !entry.name.endsWith('.md')) continue;
|
|
1183
|
+
|
|
1184
|
+
const commandName = entry.name.replace('.md', '');
|
|
1185
|
+
const skillNumber = CURSOR_SKILL_ORDER[commandName];
|
|
1186
|
+
const skillDirName = skillNumber != null
|
|
1187
|
+
? `bp-${String(skillNumber).padStart(2, '0')}-${commandName}`
|
|
1188
|
+
: `bp-${commandName}`;
|
|
1189
|
+
|
|
1190
|
+
const skillDir = path.join(destDir, skillDirName);
|
|
1191
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
1192
|
+
|
|
1193
|
+
let content = fs.readFileSync(path.join(srcDir, entry.name), 'utf8');
|
|
1194
|
+
content = convertClaudeToCursorSkill(content, pathPrefix);
|
|
1195
|
+
content = applyInteractionConversions(content, `commands/bp/${entry.name}`);
|
|
1196
|
+
content = processAttribution(content, getCommitAttribution(runtime));
|
|
1197
|
+
|
|
1198
|
+
fs.writeFileSync(path.join(skillDir, 'SKILL.md'), content);
|
|
1199
|
+
installedCount++;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
return installedCount;
|
|
1203
|
+
}
|
|
1204
|
+
|
|
586
1205
|
/**
|
|
587
1206
|
* Copy commands to a flat structure for OpenCode
|
|
588
1207
|
* OpenCode expects: command/bp-help.md (invoked as /bp-help)
|
|
@@ -671,7 +1290,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
|
|
|
671
1290
|
content = content.replace(claudeDirRegex, pathPrefix);
|
|
672
1291
|
content = processAttribution(content, getCommitAttribution(runtime));
|
|
673
1292
|
|
|
674
|
-
// Convert
|
|
1293
|
+
// Convert for runtime compatibility
|
|
675
1294
|
if (isOpencode) {
|
|
676
1295
|
content = convertClaudeToOpencodeFrontmatter(content);
|
|
677
1296
|
fs.writeFileSync(destPath, content);
|
|
@@ -682,6 +1301,12 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime) {
|
|
|
682
1301
|
// Replace extension with .toml
|
|
683
1302
|
const tomlPath = destPath.replace(/\.md$/, '.toml');
|
|
684
1303
|
fs.writeFileSync(tomlPath, tomlContent);
|
|
1304
|
+
} else if (runtime === 'cursor') {
|
|
1305
|
+
// Apply Cursor interaction conversions and command reference conversions
|
|
1306
|
+
const relativePath = srcPath.replace(/^.*?\/blueprint\//, 'blueprint/').replace(/^.*?\/commands\//, 'commands/');
|
|
1307
|
+
content = applyInteractionConversions(content, relativePath);
|
|
1308
|
+
content = convertCommandReferences(content);
|
|
1309
|
+
fs.writeFileSync(destPath, content);
|
|
685
1310
|
} else {
|
|
686
1311
|
fs.writeFileSync(destPath, content);
|
|
687
1312
|
}
|
|
@@ -803,6 +1428,7 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
803
1428
|
let runtimeLabel = 'Claude Code';
|
|
804
1429
|
if (runtime === 'opencode') runtimeLabel = 'OpenCode';
|
|
805
1430
|
if (runtime === 'gemini') runtimeLabel = 'Gemini';
|
|
1431
|
+
if (runtime === 'cursor') runtimeLabel = 'Cursor';
|
|
806
1432
|
|
|
807
1433
|
console.log(` Uninstalling Blueprint from ${cyan}${runtimeLabel}${reset} at ${cyan}${locationLabel}${reset}\n`);
|
|
808
1434
|
|
|
@@ -815,8 +1441,27 @@ function uninstall(isGlobal, runtime = 'claude') {
|
|
|
815
1441
|
|
|
816
1442
|
let removedCount = 0;
|
|
817
1443
|
|
|
818
|
-
// 1. Remove Blueprint commands directory
|
|
819
|
-
|
|
1444
|
+
// 1. Remove Blueprint commands/skills directory
|
|
1445
|
+
const isCursor = runtime === 'cursor';
|
|
1446
|
+
if (isCursor) {
|
|
1447
|
+
// Cursor: remove skills/bp-*/ directories
|
|
1448
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
1449
|
+
if (fs.existsSync(skillsDir)) {
|
|
1450
|
+
const dirs = fs.readdirSync(skillsDir);
|
|
1451
|
+
let skillCount = 0;
|
|
1452
|
+
for (const dir of dirs) {
|
|
1453
|
+
const fullPath = path.join(skillsDir, dir);
|
|
1454
|
+
if (dir.startsWith('bp-') && fs.statSync(fullPath).isDirectory()) {
|
|
1455
|
+
fs.rmSync(fullPath, { recursive: true });
|
|
1456
|
+
skillCount++;
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
if (skillCount > 0) {
|
|
1460
|
+
removedCount++;
|
|
1461
|
+
console.log(` ${green}✓${reset} Removed ${skillCount} Blueprint skills`);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
} else if (isOpencode) {
|
|
820
1465
|
// OpenCode: remove command/bp-*.md files
|
|
821
1466
|
const commandDir = path.join(targetDir, 'command');
|
|
822
1467
|
if (fs.existsSync(commandDir)) {
|
|
@@ -1284,6 +1929,7 @@ function reportLocalPatches(configDir) {
|
|
|
1284
1929
|
function install(isGlobal, runtime = 'claude') {
|
|
1285
1930
|
const isOpencode = runtime === 'opencode';
|
|
1286
1931
|
const isGemini = runtime === 'gemini';
|
|
1932
|
+
const isCursor = runtime === 'cursor';
|
|
1287
1933
|
const dirName = getDirName(runtime);
|
|
1288
1934
|
const src = path.join(__dirname, '..');
|
|
1289
1935
|
|
|
@@ -1306,6 +1952,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1306
1952
|
let runtimeLabel = 'Claude Code';
|
|
1307
1953
|
if (isOpencode) runtimeLabel = 'OpenCode';
|
|
1308
1954
|
if (isGemini) runtimeLabel = 'Gemini';
|
|
1955
|
+
if (isCursor) runtimeLabel = 'Cursor';
|
|
1309
1956
|
|
|
1310
1957
|
console.log(` Installing for ${cyan}${runtimeLabel}${reset} to ${cyan}${locationLabel}${reset}\n`);
|
|
1311
1958
|
|
|
@@ -1318,13 +1965,24 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1318
1965
|
// Clean up orphaned files from previous versions
|
|
1319
1966
|
cleanupOrphanedFiles(targetDir);
|
|
1320
1967
|
|
|
1321
|
-
//
|
|
1322
|
-
|
|
1323
|
-
|
|
1968
|
+
// Runtime-specific command installation
|
|
1969
|
+
if (isCursor) {
|
|
1970
|
+
// Cursor: Skills in skills/ directory with nested structure
|
|
1971
|
+
const skillsDir = path.join(targetDir, 'skills');
|
|
1972
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
1973
|
+
|
|
1974
|
+
const bpSrc = path.join(src, 'commands', 'bp');
|
|
1975
|
+
const count = copySkillsFromCommands(bpSrc, skillsDir, pathPrefix, runtime);
|
|
1976
|
+
if (count > 0) {
|
|
1977
|
+
console.log(` ${green}✓${reset} Installed ${count} skills to skills/`);
|
|
1978
|
+
} else {
|
|
1979
|
+
failures.push('skills');
|
|
1980
|
+
}
|
|
1981
|
+
} else if (isOpencode) {
|
|
1324
1982
|
// OpenCode: flat structure in command/ directory
|
|
1325
1983
|
const commandDir = path.join(targetDir, 'command');
|
|
1326
1984
|
fs.mkdirSync(commandDir, { recursive: true });
|
|
1327
|
-
|
|
1985
|
+
|
|
1328
1986
|
// Copy commands/bp/*.md as command/bp-*.md (flatten structure)
|
|
1329
1987
|
const bpSrc = path.join(src, 'commands', 'bp');
|
|
1330
1988
|
copyFlattenedCommands(bpSrc, commandDir, 'bp', pathPrefix, runtime);
|
|
@@ -1338,7 +1996,7 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1338
1996
|
// Claude Code & Gemini: nested structure in commands/ directory
|
|
1339
1997
|
const commandsDir = path.join(targetDir, 'commands');
|
|
1340
1998
|
fs.mkdirSync(commandsDir, { recursive: true });
|
|
1341
|
-
|
|
1999
|
+
|
|
1342
2000
|
const bpSrc = path.join(src, 'commands', 'bp');
|
|
1343
2001
|
const bpDest = path.join(commandsDir, 'bp');
|
|
1344
2002
|
copyWithPathReplacement(bpSrc, bpDest, pathPrefix, runtime);
|
|
@@ -1388,6 +2046,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1388
2046
|
content = convertClaudeToOpencodeFrontmatter(content);
|
|
1389
2047
|
} else if (isGemini) {
|
|
1390
2048
|
content = convertClaudeToGeminiAgent(content);
|
|
2049
|
+
} else if (isCursor) {
|
|
2050
|
+
content = convertClaudeToCursorAgent(content, pathPrefix);
|
|
1391
2051
|
}
|
|
1392
2052
|
fs.writeFileSync(path.join(agentsDest, entry.name), content);
|
|
1393
2053
|
}
|
|
@@ -1421,22 +2081,25 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1421
2081
|
}
|
|
1422
2082
|
|
|
1423
2083
|
// Copy hooks from dist/ (bundled with dependencies)
|
|
1424
|
-
|
|
1425
|
-
if (
|
|
1426
|
-
const
|
|
1427
|
-
fs.
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1432
|
-
const
|
|
1433
|
-
fs.
|
|
2084
|
+
// Skip for Cursor — hooks deferred to v2
|
|
2085
|
+
if (!isCursor) {
|
|
2086
|
+
const hooksSrc = path.join(src, 'hooks', 'dist');
|
|
2087
|
+
if (fs.existsSync(hooksSrc)) {
|
|
2088
|
+
const hooksDest = path.join(targetDir, 'hooks');
|
|
2089
|
+
fs.mkdirSync(hooksDest, { recursive: true });
|
|
2090
|
+
const hookEntries = fs.readdirSync(hooksSrc);
|
|
2091
|
+
for (const entry of hookEntries) {
|
|
2092
|
+
const srcFile = path.join(hooksSrc, entry);
|
|
2093
|
+
if (fs.statSync(srcFile).isFile()) {
|
|
2094
|
+
const destFile = path.join(hooksDest, entry);
|
|
2095
|
+
fs.copyFileSync(srcFile, destFile);
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
if (verifyInstalled(hooksDest, 'hooks')) {
|
|
2099
|
+
console.log(` ${green}✓${reset} Installed hooks (bundled)`);
|
|
2100
|
+
} else {
|
|
2101
|
+
failures.push('hooks');
|
|
1434
2102
|
}
|
|
1435
|
-
}
|
|
1436
|
-
if (verifyInstalled(hooksDest, 'hooks')) {
|
|
1437
|
-
console.log(` ${green}✓${reset} Installed hooks (bundled)`);
|
|
1438
|
-
} else {
|
|
1439
|
-
failures.push('hooks');
|
|
1440
2103
|
}
|
|
1441
2104
|
}
|
|
1442
2105
|
|
|
@@ -1467,8 +2130,8 @@ function install(isGlobal, runtime = 'claude') {
|
|
|
1467
2130
|
}
|
|
1468
2131
|
}
|
|
1469
2132
|
|
|
1470
|
-
// Configure SessionStart hook for update checking (skip for opencode)
|
|
1471
|
-
if (!isOpencode) {
|
|
2133
|
+
// Configure SessionStart hook for update checking (skip for opencode and cursor)
|
|
2134
|
+
if (!isOpencode && !isCursor) {
|
|
1472
2135
|
if (!settings.hooks) {
|
|
1473
2136
|
settings.hooks = {};
|
|
1474
2137
|
}
|
|
@@ -1528,8 +2191,10 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
|
|
|
1528
2191
|
let program = 'Claude Code';
|
|
1529
2192
|
if (runtime === 'opencode') program = 'OpenCode';
|
|
1530
2193
|
if (runtime === 'gemini') program = 'Gemini';
|
|
2194
|
+
if (runtime === 'cursor') program = 'Cursor';
|
|
1531
2195
|
|
|
1532
|
-
const
|
|
2196
|
+
const isCursor = runtime === 'cursor';
|
|
2197
|
+
const command = isOpencode ? '/bp-help' : isCursor ? '/bp-27-help' : '/bp:help';
|
|
1533
2198
|
console.log(`
|
|
1534
2199
|
${green}Done!${reset} Launch ${program} and run ${cyan}${command}${reset}.
|
|
1535
2200
|
|
|
@@ -1610,15 +2275,18 @@ function promptRuntime(callback) {
|
|
|
1610
2275
|
console.log(` ${yellow}Which runtime(s) would you like to install for?${reset}\n\n ${cyan}1${reset}) Claude Code ${dim}(~/.claude)${reset}
|
|
1611
2276
|
${cyan}2${reset}) OpenCode ${dim}(~/.config/opencode)${reset} - open source, free models
|
|
1612
2277
|
${cyan}3${reset}) Gemini ${dim}(~/.gemini)${reset}
|
|
1613
|
-
${cyan}4${reset})
|
|
2278
|
+
${cyan}4${reset}) Cursor ${dim}(~/.cursor)${reset}
|
|
2279
|
+
${cyan}5${reset}) All
|
|
1614
2280
|
`);
|
|
1615
2281
|
|
|
1616
2282
|
rl.question(` Choice ${dim}[1]${reset}: `, (answer) => {
|
|
1617
2283
|
answered = true;
|
|
1618
2284
|
rl.close();
|
|
1619
2285
|
const choice = answer.trim() || '1';
|
|
1620
|
-
if (choice === '
|
|
1621
|
-
callback(['claude', 'opencode', 'gemini']);
|
|
2286
|
+
if (choice === '5') {
|
|
2287
|
+
callback(['claude', 'opencode', 'gemini', 'cursor']);
|
|
2288
|
+
} else if (choice === '4') {
|
|
2289
|
+
callback(['cursor']);
|
|
1622
2290
|
} else if (choice === '3') {
|
|
1623
2291
|
callback(['gemini']);
|
|
1624
2292
|
} else if (choice === '2') {
|
|
@@ -1703,16 +2371,28 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
|
|
|
1703
2371
|
if (geminiResult) {
|
|
1704
2372
|
finishInstall(geminiResult.settingsPath, geminiResult.settings, geminiResult.statuslineCommand, shouldInstallStatusline, 'gemini');
|
|
1705
2373
|
}
|
|
1706
|
-
|
|
2374
|
+
|
|
1707
2375
|
const opencodeResult = results.find(r => r.runtime === 'opencode');
|
|
1708
2376
|
if (opencodeResult) {
|
|
1709
2377
|
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
1710
2378
|
}
|
|
2379
|
+
|
|
2380
|
+
// Cursor doesn't use statusline — finishInstall with shouldInstallStatusline=false
|
|
2381
|
+
const cursorResult = results.find(r => r.runtime === 'cursor');
|
|
2382
|
+
if (cursorResult) {
|
|
2383
|
+
finishInstall(cursorResult.settingsPath, cursorResult.settings, cursorResult.statuslineCommand, false, 'cursor');
|
|
2384
|
+
}
|
|
1711
2385
|
});
|
|
1712
2386
|
} else {
|
|
1713
|
-
// Only OpenCode
|
|
1714
|
-
const opencodeResult = results
|
|
1715
|
-
|
|
2387
|
+
// Only runtimes without statusline (OpenCode and/or Cursor)
|
|
2388
|
+
const opencodeResult = results.find(r => r.runtime === 'opencode');
|
|
2389
|
+
if (opencodeResult) {
|
|
2390
|
+
finishInstall(opencodeResult.settingsPath, opencodeResult.settings, opencodeResult.statuslineCommand, false, 'opencode');
|
|
2391
|
+
}
|
|
2392
|
+
const cursorResult = results.find(r => r.runtime === 'cursor');
|
|
2393
|
+
if (cursorResult) {
|
|
2394
|
+
finishInstall(cursorResult.settingsPath, cursorResult.settings, cursorResult.statuslineCommand, false, 'cursor');
|
|
2395
|
+
}
|
|
1716
2396
|
}
|
|
1717
2397
|
}
|
|
1718
2398
|
|