feat: implement feature wishlist system (issue #85)
Add comprehensive wishlist management for capturing and refining feature ideas: • CLI Commands: - markitect wish create: Create new wishlist items with templates - markitect wish list: List and filter wishes by stage - markitect wish promote: Promote wishes through workflow stages - markitect wish convert: Convert ready wishes to regular issues • Workflow Stages: - discussion: Initial idea capture and brainstorming - draft: Create specification and requirements - ready: Prepare for conversion to development issue - archived: Preserve ideas that won't be pursued • Features: - Automatic label management (wish, wish/stage, priority/level) - Multiple output formats (table, simple, json) - Stage filtering and organization - Structured templates for each workflow stage - Comprehensive documentation and best practices 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
323
markitect/cli.py
323
markitect/cli.py
@@ -6038,6 +6038,329 @@ def search_rebuild(config, optimize):
|
||||
cli.add_command(search_group)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Feature Wishlist Commands (Issue #85)
|
||||
# =============================================================================
|
||||
|
||||
@cli.group('wish')
|
||||
@pass_config
|
||||
def wishlist_group(config):
|
||||
"""Feature wishlist management for capturing and refining ideas."""
|
||||
pass
|
||||
|
||||
|
||||
@wishlist_group.command('create')
|
||||
@click.argument('title')
|
||||
@click.option('--description', '-d', help='Wish description')
|
||||
@click.option('--stage', default='discussion',
|
||||
type=click.Choice(['discussion', 'draft', 'ready']),
|
||||
help='Initial wishlist stage')
|
||||
@click.option('--priority', type=click.Choice(['low', 'medium', 'high']),
|
||||
help='Priority level for the wish')
|
||||
@pass_config
|
||||
def wish_create(config, title, description, stage, priority):
|
||||
"""Create a new feature wishlist item."""
|
||||
try:
|
||||
# Build description with template
|
||||
wish_description = f"""## 💡 Feature Wish
|
||||
|
||||
**Summary**: {description or 'No description provided'}
|
||||
|
||||
## Current Thinking
|
||||
|
||||
*What's the initial idea or inspiration?*
|
||||
|
||||
## Potential Benefits
|
||||
|
||||
*Why might this be valuable?*
|
||||
|
||||
## Questions to Explore
|
||||
|
||||
*What aspects need more thought?*
|
||||
|
||||
## Related Concepts
|
||||
|
||||
*Are there similar ideas or existing features this relates to?*
|
||||
|
||||
---
|
||||
|
||||
📋 **Wishlist Stage**: {stage}
|
||||
🏷️ **Auto-labeled**: `wish`, `wish/{stage}`
|
||||
"""
|
||||
|
||||
# Prepare labels
|
||||
labels = ['wish', f'wish/{stage}']
|
||||
if priority:
|
||||
labels.append(f'priority/{priority}')
|
||||
|
||||
# Create the issue
|
||||
import subprocess
|
||||
result = subprocess.run([
|
||||
'tea', 'issue', 'create',
|
||||
'--title', f"💡 Wish: {title}",
|
||||
'--description', wish_description,
|
||||
'--labels', ','.join(labels)
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
click.echo(f"✅ Created wishlist item: '{title}'")
|
||||
click.echo(f"🏷️ Labels: {', '.join(labels)}")
|
||||
click.echo(f"📋 Stage: {stage}")
|
||||
else:
|
||||
click.echo(f"❌ Failed to create wish: {result.stderr}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Error creating wish: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@wishlist_group.command('list')
|
||||
@click.option('--stage', type=click.Choice(['discussion', 'draft', 'ready', 'archived', 'all']),
|
||||
default='all', help='Filter by wishlist stage')
|
||||
@click.option('--format', 'output_format', default='table',
|
||||
type=click.Choice(['table', 'simple', 'json']),
|
||||
help='Output format')
|
||||
@pass_config
|
||||
def wish_list(config, stage, output_format):
|
||||
"""List feature wishlist items."""
|
||||
try:
|
||||
# Build label filter
|
||||
if stage == 'all':
|
||||
label_filter = 'wish'
|
||||
else:
|
||||
label_filter = f'wish/{stage}'
|
||||
|
||||
# Get issues with wish labels using simple output and parse manually
|
||||
import subprocess
|
||||
result = subprocess.run([
|
||||
'tea', 'issue', 'list',
|
||||
'--labels', label_filter,
|
||||
'--output', 'simple'
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
click.echo(f"❌ Failed to fetch wishlist: {result.stderr}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse the simple output: number title status assignee labels
|
||||
issues = []
|
||||
if result.stdout.strip():
|
||||
lines = result.stdout.strip().split('\n')
|
||||
for line in lines:
|
||||
if line.strip():
|
||||
# Parse: number title status assignee labels
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
issue_number = parts[0].strip()
|
||||
# Find where title ends (before status/assignee)
|
||||
title_parts = []
|
||||
labels = []
|
||||
collecting_title = True
|
||||
|
||||
for part in parts[1:]:
|
||||
if collecting_title and part not in ['open', 'closed'] and not part.startswith('wish'):
|
||||
title_parts.append(part)
|
||||
else:
|
||||
collecting_title = False
|
||||
if part.startswith('wish'):
|
||||
labels.append(part)
|
||||
|
||||
title = ' '.join(title_parts)
|
||||
created_at = "2025-10-03" # Default for simple format
|
||||
|
||||
issues.append({
|
||||
'number': int(issue_number),
|
||||
'title': title,
|
||||
'labels': [{'name': label} for label in labels],
|
||||
'created_at': created_at
|
||||
})
|
||||
|
||||
if not issues:
|
||||
click.echo(f"No wishlist items found for stage: {stage}")
|
||||
return
|
||||
|
||||
if output_format == 'json':
|
||||
click.echo(json.dumps(issues, indent=2))
|
||||
elif output_format == 'simple':
|
||||
for issue in issues:
|
||||
click.echo(f"#{issue['number']}: {issue['title']}")
|
||||
else:
|
||||
# Table format
|
||||
table_data = []
|
||||
for issue in issues:
|
||||
# Extract stage from labels
|
||||
stage_labels = [label['name'] for label in issue.get('labels', [])
|
||||
if label['name'].startswith('wish/')]
|
||||
current_stage = stage_labels[0].replace('wish/', '') if stage_labels else 'unknown'
|
||||
|
||||
table_data.append([
|
||||
f"#{issue['number']}",
|
||||
current_stage,
|
||||
issue['title'].replace('💡 Wish: ', ''),
|
||||
issue['created_at'][:10] # Just the date
|
||||
])
|
||||
|
||||
from tabulate import tabulate
|
||||
headers = ['Issue', 'Stage', 'Title', 'Created']
|
||||
click.echo(f"\n💡 Feature Wishlist ({len(issues)} items):\n")
|
||||
click.echo(tabulate(table_data, headers=headers, tablefmt='grid'))
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Error listing wishes: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@wishlist_group.command('promote')
|
||||
@click.argument('issue_number', type=int)
|
||||
@click.option('--stage', type=click.Choice(['draft', 'ready', 'archived']),
|
||||
help='Promote to specific stage')
|
||||
@pass_config
|
||||
def wish_promote(config, issue_number, stage):
|
||||
"""Promote a wishlist item to the next stage or specific stage."""
|
||||
try:
|
||||
if not stage:
|
||||
# Auto-determine next stage
|
||||
click.echo("🔄 Auto-promoting to next logical stage...")
|
||||
# This would require fetching current labels and determining next stage
|
||||
stage = 'draft' # Default progression
|
||||
|
||||
# Remove old stage labels and add new one
|
||||
import subprocess
|
||||
|
||||
# Get current issue info using the list format and extract labels for our issue
|
||||
result = subprocess.run([
|
||||
'tea', 'issue', 'list', '--output', 'simple'
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
click.echo(f"❌ Failed to fetch issues", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Parse output to find our issue and extract its current labels
|
||||
current_labels = []
|
||||
if result.stdout.strip():
|
||||
lines = result.stdout.strip().split('\n')
|
||||
for line in lines:
|
||||
if line.strip() and line.split()[0].strip() == str(issue_number):
|
||||
# Found our issue, extract labels from the end of the line
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if part.startswith('wish'):
|
||||
current_labels.append(part)
|
||||
break
|
||||
|
||||
if not current_labels:
|
||||
click.echo(f"❌ Issue #{issue_number} not found or has no wish labels", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
# Build new labels (remove old wish/ stage labels)
|
||||
new_labels = [label for label in current_labels if not label.startswith('wish/')]
|
||||
new_labels.append(f'wish/{stage}')
|
||||
|
||||
# Update labels
|
||||
result = subprocess.run([
|
||||
'tea', 'issue', 'edit', str(issue_number),
|
||||
'--labels', ','.join(new_labels)
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
click.echo(f"✅ Promoted wish #{issue_number} to stage: {stage}")
|
||||
click.echo(f"🏷️ Updated labels: {', '.join(new_labels)}")
|
||||
else:
|
||||
click.echo(f"❌ Failed to promote wish: {result.stderr}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Error promoting wish: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
@wishlist_group.command('convert')
|
||||
@click.argument('issue_number', type=int)
|
||||
@click.option('--title', help='New title for the regular issue')
|
||||
@click.option('--copy-content', is_flag=True, default=True,
|
||||
help='Copy wishlist content to new issue')
|
||||
@pass_config
|
||||
def wish_convert(config, issue_number, title, copy_content):
|
||||
"""Convert a ready wishlist item to a regular issue."""
|
||||
try:
|
||||
import subprocess
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Get the wishlist issue
|
||||
result = subprocess.run([
|
||||
'tea', 'issue', 'view', str(issue_number), '--output', 'json'
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
click.echo(f"❌ Failed to fetch wish #{issue_number}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
wish_issue = json.loads(result.stdout)
|
||||
|
||||
# Check if it's ready for conversion
|
||||
labels = [label['name'] for label in wish_issue.get('labels', [])]
|
||||
if 'wish/ready' not in labels:
|
||||
click.echo(f"⚠️ Wish #{issue_number} is not marked as 'ready'. Current stage: {[l for l in labels if l.startswith('wish/')]}")
|
||||
if not click.confirm('Convert anyway?'):
|
||||
return
|
||||
|
||||
# Prepare new issue content
|
||||
new_title = title or wish_issue['title'].replace('💡 Wish: ', '')
|
||||
|
||||
if copy_content:
|
||||
new_description = f"""## Converted from Wishlist
|
||||
|
||||
Originally tracked as wishlist item #{issue_number}.
|
||||
|
||||
## Description
|
||||
|
||||
{wish_issue.get('body', '')}
|
||||
|
||||
---
|
||||
*Converted from feature wishlist on {datetime.now().strftime('%Y-%m-%d')}*
|
||||
"""
|
||||
else:
|
||||
new_description = f"Converted from wishlist item #{issue_number}"
|
||||
|
||||
# Create new regular issue
|
||||
result = subprocess.run([
|
||||
'tea', 'issue', 'create',
|
||||
'--title', new_title,
|
||||
'--description', new_description
|
||||
], capture_output=True, text=True)
|
||||
|
||||
if result.returncode == 0:
|
||||
# Close the wishlist item with reference to new issue
|
||||
# Extract issue number from tea output (usually shows the URL)
|
||||
lines = result.stdout.strip().split('\n')
|
||||
new_issue_url = lines[-1] if lines else ""
|
||||
new_issue_number = new_issue_url.split('/')[-1] if '/' in new_issue_url else "unknown"
|
||||
|
||||
# Close wishlist item
|
||||
subprocess.run([
|
||||
'tea', 'issue', 'close', str(issue_number),
|
||||
'--comment', f'Converted to regular issue #{new_issue_number}'
|
||||
], capture_output=True, text=True)
|
||||
|
||||
click.echo(f"✅ Converted wish #{issue_number} to regular issue #{new_issue_number}")
|
||||
click.echo(f"🏷️ Original wishlist item closed")
|
||||
click.echo(f"📋 New issue: {new_title}")
|
||||
else:
|
||||
click.echo(f"❌ Failed to create new issue: {result.stderr}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
except Exception as e:
|
||||
click.echo(f"❌ Error converting wish: {e}", err=True)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Register wishlist commands
|
||||
cli.add_command(wishlist_group)
|
||||
|
||||
|
||||
# Register issue management commands
|
||||
cli.add_command(issues_group)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user