Files
gh-duongdev-ccpm/commands/spec:migrate.md
2025-11-29 18:24:24 +08:00

809 lines
23 KiB
Markdown

---
description: Migrate existing markdown specs from .claude/ to Linear Documents
allowed-tools: [LinearMCP, Read, Glob, Bash, AskUserQuestion]
argument-hint: <project-path> [category]
---
# Migrate Specs to Linear: $1
## 🚨 CRITICAL: Safety Rules
**READ FIRST**: ``$CCPM_COMMANDS_DIR/SAFETY_RULES.md``
**NEVER** submit, post, or update anything to Jira, Confluence, BitBucket, or Slack without explicit user confirmation, even in bypass permission mode.
---
## Arguments
- **$1** - Project path:
- `.` - Use current directory
- Absolute path (e.g., `~/projects/my-app`)
- Relative path (e.g., `../other-project`)
- **$2** - (Optional) Category to migrate: `docs`, `plans`, `enhancements`, `tasks`, `all`
Default category: `all`
**Note**: Using `.` will scan current working directory for `.claude/` folder.
## Workflow
### Step 1: Resolve Project Path
```javascript
// Resolve project path
let projectPath = $1
if (projectPath === '.') {
// Use current working directory
projectPath = process.cwd()
}
// Resolve to absolute path
projectPath = path.resolve(projectPath)
// Check if .claude/ exists
const claudePath = path.join(projectPath, '.claude')
if (!fs.existsSync(claudePath)) {
// Error: No .claude/ directory found
// Suggest: Check path or create .claude/ first
}
```
### Step 2: Discover Existing Specs
Scan project `.claude/` directory for markdown files:
```javascript
const categories = {
docs: '.claude/docs/',
plans: '.claude/plans/',
enhancements: '.claude/enhancements/',
tasks: '.claude/tasks/',
research: '.claude/research/',
analysis: '.claude/analysis/',
qa: '.claude/qa/',
security: '.claude/security/'
}
// Use Glob to find all .md files in each category
const files = {}
for (const [category, path] of Object.entries(categories)) {
const pattern = `${projectPath}/${path}**/*.md`
files[category] = await glob(pattern)
}
```
**Filter by category** if `$2` provided.
### Step 2: Categorize Files by Type
**Analyze each file to determine type:**
```javascript
function categorizeFile(filePath, content) {
// Check filename patterns
const fileName = path.basename(filePath)
// Plans with checklists → Features
if (fileName.includes('plan') && hasChecklist(content)) {
return { type: 'feature', confidence: 'high' }
}
// Enhancements → Features
if (filePath.includes('/enhancements/')) {
return { type: 'feature', confidence: 'high' }
}
// Tasks → Tasks
if (filePath.includes('/tasks/') && hasImplementationDetails(content)) {
return { type: 'task', confidence: 'high' }
}
// Docs/guides → Documentation (not migrated, or linked)
if (filePath.includes('/docs/') || fileName.includes('guide')) {
return { type: 'documentation', confidence: 'high' }
}
// Research → Link as reference
if (filePath.includes('/research/')) {
return { type: 'reference', confidence: 'high' }
}
// Large multi-section files → Epics
if (hasSections(content) > 5 && hasFeatureBreakdown(content)) {
return { type: 'epic', confidence: 'medium' }
}
// Default: Feature
return { type: 'feature', confidence: 'low' }
}
function hasChecklist(content) {
return /- \[ \]/.test(content)
}
function hasImplementationDetails(content) {
return content.includes('## What Was Implemented') ||
content.includes('## Implementation') ||
content.includes('Status:** ✅')
}
function hasFeatureBreakdown(content) {
return /## Features/i.test(content) ||
/## Phases/i.test(content)
}
```
### Step 3: Ask What to Migrate
**FIRST: Let user select which categories to migrate.**
Use **AskUserQuestion**:
```javascript
{
questions: [{
question: "Which categories would you like to migrate?",
header: "Select Categories",
multiSelect: true, // Allow multiple selections
options: [
{
label: "Epics (2 files)",
description: `${epics.length} epic specs found in plans/`
},
{
label: "Features (12 files)",
description: `${features.length} features found in enhancements/`
},
{
label: "Tasks (25 files)",
description: `${tasks.length} tasks found in tasks/`
},
{
label: "Documentation (6 files)",
description: `${docs.length} docs/guides found in docs/`
},
{
label: "Research (8 files)",
description: `${research.length} research docs found in research/`
}
]
}]
}
```
**User can select**:
- Single category (e.g., only "Features")
- Multiple categories (e.g., "Epics" + "Features")
- All categories (select all)
**After selection**, filter files to only selected categories.
### Step 4: Show Migration Preview for Selected Categories
Display categorized files to user:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📦 Migration Preview: $1
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📂 Category: ${$2 || 'all'}
📁 Project Path: $1/.claude/
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Discovered Files
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📚 EPICS (Will create Initiatives + Spec Docs):
1. production-deployment-plan.md → Epic: "Production Deployment Plan"
2. [...]
🎨 FEATURES (Will create Features + Design Docs):
1. 20251031-posthog-observability-implementation.md → Feature: "PostHog Observability"
2. 20251024-120100-search-and-filter-system.md → Feature: "Search & Filter System"
3. [...]
✅ TASKS (Will create Tasks):
1. 20251030-130330-implement-task-comments-phase2.md → Task: "Task Comments Phase 2"
2. 20251107-095033-fix-posthog-provider-crash.md → Task: "Fix PostHog Provider Crash"
3. [...]
📖 DOCUMENTATION (Will link as references):
1. feature-flags-guide.md
2. ota-updates-guide.md
3. [...]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📈 Migration Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Files: 45
- Epics: 2
- Features: 12
- Tasks: 25
- Documentation: 6
Estimated Time: ~15 minutes
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⚠️ Migration Rules
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Original .md files will NOT be deleted (safe migration)
✅ Files will be moved to .claude/migrated/ after successful migration
✅ Linear issues will include link to original file
✅ You can review and edit in Linear after migration
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Step 4: Show Detailed Preview (MANDATORY)
**CRITICAL: ALWAYS show full preview before ANY migration.**
For EACH file, display:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📄 File #1: production-deployment-plan.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 Original Path: .claude/plans/production-deployment-plan.md
📊 Type: Epic (detected)
📝 Size: 57KB
Will Create in Linear:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Epic/Initiative:
- Title: "Production Deployment Plan"
- Team: Personal
- Project: Personal Project
- Labels: ["epic", "migrated", "spec:draft"]
- Status: Planned
2. Linear Document:
- Title: "Epic Spec: Production Deployment Plan"
- Content: Full markdown (57KB)
- Linked to Epic above
3. Extracted Metadata:
- Created: 2025-11-01
- Status: Planning
- Version: v1.0.0
4. Post-Migration:
- Original file moved to: .claude/migrated/plans/
- Breadcrumb created: .claude/plans/production-deployment-plan.md.migrated.txt
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📄 File #2: 20251031-posthog-observability-implementation.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 Original Path: .claude/enhancements/20251031-posthog-observability-implementation.md
📊 Type: Feature (detected)
📝 Size: 35KB
📋 Checklist Found: 3 subtasks detected
Will Create in Linear:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Feature (Parent Issue):
- Title: "PostHog Observability Implementation"
- Team: Personal
- Project: Personal Project
- Labels: ["feature", "migrated", "spec:draft"]
- Priority: High (detected)
2. Linear Document:
- Title: "Feature Design: PostHog Observability Implementation"
- Content: Full markdown (35KB)
- Linked to Feature above
3. Sub-Tasks (from checklist):
- Task 1: "Setup PostHog Integration" (Est: 2h)
- Task 2: "Configure Event Tracking" (Est: 4h)
- Task 3: "Add Custom Properties" (Est: 3h)
4. Post-Migration:
- Original file moved to: .claude/migrated/enhancements/
- Breadcrumb created with Linear links
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[... show ALL files ...]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Step 5: Confirm Migration (REQUIRED)
**NEVER migrate without explicit confirmation.**
Use **AskUserQuestion**:
```javascript
{
questions: [{
question: "⚠️ REVIEW COMPLETE. Proceed with creating these items in Linear?",
header: "Confirm",
multiSelect: false,
options: [
{
label: "✅ Yes, Migrate All",
description: "Create all items in Linear as shown above"
},
{
label: "🔍 Select Specific Files",
description: "I want to choose which files to migrate"
},
{
label: "❌ Cancel",
description: "Don't migrate anything"
}
]
}]
}
```
**If "Select Specific Files":**
- Show numbered list of all files
- Ask user to specify indices (e.g., "1,3,5-8,12")
- Show preview AGAIN for selected files only
- Ask confirmation AGAIN before migrating
### Step 5: Migrate Each File
For each file:
#### Step 5.1: Read and Parse File
```javascript
const content = await readFile(filePath)
const metadata = extractMetadata(content)
// Extracts:
// - Title (from # heading or filename)
// - Created date (from file or "Created:" field)
// - Status (from "Status:" field)
// - Related docs (from "Related:" or links)
```
#### Step 5.2: Transform Content for Linear
**Keep most content as-is, but:**
1. **Remove file-specific headers:**
```markdown
# Task Comments Phase 2: Photo Attachments Implementation
**Date:** 2025-10-30 - 2025-10-31
**Status:** ✅ COMPLETED
**Related Task:** `.claude/tasks/20251030-130330-implement-task-comments.md`
```
Becomes Linear issue fields:
- Title: "Task Comments Phase 2: Photo Attachments"
- Status: "Done"
- Description: (rest of content)
2. **Convert local file links to references:**
```markdown
**Related Task:** `.claude/tasks/20251030-130330-implement-task-comments.md`
```
Becomes:
```markdown
**Related Task:** [Will be migrated as WORK-XXX] or [Original file: `.claude/tasks/...`]
```
3. **Preserve all other content:**
- Code blocks → Keep as-is
- Tables → Keep as-is
- Images → Keep as-is (if hosted)
- Checklists → Keep as-is
- Headers, formatting → Keep as-is
#### Step 5.3: Create Linear Entities
**For Epic:**
```javascript
// 1. Create Initiative in Linear
const epic = await createLinearInitiative({
name: metadata.title,
description: `Original file: \`${relativePath}\`\n\nMigrated from: ${filePath}`,
team: detectTeam(projectPath),
targetDate: metadata.targetDate
})
// 2. Create Linear Document for Epic Spec
const doc = await createLinearDocument({
title: `Epic Spec: ${metadata.title}`,
content: transformedContent, // Full markdown content
projectId: epic.id
})
// 3. Update Initiative description to link to spec doc
await updateLinearInitiative(epic.id, {
description: `
## 📄 Specification
**Spec Document**: [Epic Spec: ${metadata.title}](${doc.url})
**Original File**: \`${relativePath}\`
**Migrated**: ${new Date().toISOString().split('T')[0]}
---
${epic.description}
`
})
```
**For Feature:**
```javascript
// 1. Create Feature (Parent Issue)
const feature = await createLinearIssue({
title: metadata.title,
team: detectTeam(projectPath),
project: detectProject(projectPath),
labels: ['feature', 'migrated', metadata.status ? `status:${metadata.status}` : 'spec:draft'],
priority: metadata.priority || 0,
description: `Original file: \`${relativePath}\`\n\nMigrated from: ${filePath}`
})
// 2. Create Linear Document for Feature Design
const doc = await createLinearDocument({
title: `Feature Design: ${metadata.title}`,
content: transformedContent,
projectId: feature.projectId
})
// 3. Link doc to feature
await updateLinearIssue(feature.id, {
description: `
## 📄 Specification
**Design Doc**: [Feature Design: ${metadata.title}](${doc.url})
**Original File**: \`${relativePath}\`
**Migrated**: ${new Date().toISOString().split('T')[0]}
**Original Status**: ${metadata.status || 'N/A'}
---
${feature.description}
`
})
// 4. If file has checklist, extract and add as sub-tasks
if (hasChecklist(content)) {
const tasks = extractChecklist(content)
for (const task of tasks) {
await createLinearIssue({
title: task.title,
team: feature.team,
project: feature.project,
parent: feature.id, // Sub-issue
labels: ['task', 'migrated'],
description: task.description || ''
})
}
}
```
**For Task:**
```javascript
// Create Task (regular issue, may or may not be sub-issue)
const task = await createLinearIssue({
title: metadata.title,
team: detectTeam(projectPath),
project: detectProject(projectPath),
labels: ['task', 'migrated', metadata.status === '✅ COMPLETED' ? 'status:done' : 'planning'],
status: metadata.status === '✅ COMPLETED' ? 'Done' : 'Planning',
description: `
## 📄 Original Implementation Notes
**Original File**: \`${relativePath}\`
**Migrated**: ${new Date().toISOString().split('T')[0]}
**Original Status**: ${metadata.status || 'N/A'}
---
${transformedContent}
`
})
```
**For Documentation:**
```javascript
// Don't create Linear issue, just create reference document
const doc = await createLinearDocument({
title: `Reference: ${metadata.title}`,
content: `
# ${metadata.title}
**Source**: \`${relativePath}\`
**Type**: Documentation/Guide
---
${content}
`
})
// Optionally create "Documentation" parent issue to group all docs
```
#### Step 5.4: Move Original File
After successful migration:
```bash
# Create migrated directory
mkdir -p ${projectPath}/.claude/migrated/${category}
# Move original file
mv ${filePath} ${projectPath}/.claude/migrated/${category}/
# Create breadcrumb file
echo "Migrated to Linear: [WORK-123](url)" > ${filePath}.migrated.txt
```
### Step 6: Track Progress
Show progress during migration:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🔄 Migration in Progress...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
[████████████░░░░░░░░] 12/45 (26%)
Current: 20251031-posthog-observability-implementation.md
Status: Creating feature... ✅
Creating design doc... ✅
Extracting subtasks... ✅ (3 tasks created)
Moving to migrated/... ✅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Step 7: Generate Migration Summary
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ Migration Complete!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Migration Results
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total Files Migrated: 45
- Epics Created: 2
- Features Created: 12
- Tasks Created: 25
- Documentation: 6
Total Linear Issues Created: 39
Total Linear Documents Created: 14
Total Sub-Tasks Created: 18
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📋 Created Items Summary
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Epics:
✅ WORK-100: Production Deployment Plan
✅ WORK-101: Feature Roadmap V1
Features:
✅ WORK-102: PostHog Observability (+ 3 subtasks)
✅ WORK-103: Search & Filter System (+ 4 subtasks)
✅ WORK-104: Task Comment Enhancements (+ 2 subtasks)
... [show first 10, then "and X more"]
Tasks:
✅ WORK-120: Fix PostHog Provider Crash
✅ WORK-121: Implement Additional Task Flags
... [show first 10, then "and X more"]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📁 Original Files
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Original files moved to:
${projectPath}/.claude/migrated/
Breadcrumb files created with Linear links.
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
💡 Next Steps
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Review migrated items in Linear
2. Update status labels if needed
3. Link related features/tasks
4. Run /ccpm:utils:report to see project overview
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
### Step 8: Create Migration Log
Create a migration log file:
```markdown
# Migration Log - ${new Date().toISOString().split('T')[0]}
## Summary
- Total Files: 45
- Epics: 2
- Features: 12
- Tasks: 25
- Documentation: 6
## Migrated Files
| Original File | Type | Linear ID | Linear URL | Status |
|---------------|------|-----------|------------|--------|
| production-deployment-plan.md | Epic | WORK-100 | [Link](url) | ✅ |
| 20251031-posthog-observability-implementation.md | Feature | WORK-102 | [Link](url) | ✅ |
| ... | ... | ... | ... | ... |
## Errors
[None / List any files that failed to migrate]
## Notes
- Original files moved to `.claude/migrated/`
- All Linear issues labeled with `migrated`
- Spec documents created for Epics and Features
- Subtasks extracted from checklists
```
Save to: `${projectPath}/.claude/migration-log-${timestamp}.md`
### Step 9: Interactive Next Actions
```javascript
{
questions: [{
question: "Migration complete! What would you like to do next?",
header: "Next Step",
multiSelect: false,
options: [
{
label: "View Project Report",
description: "See all migrated items organized (/ccpm:utils:report)"
},
{
label: "Review in Linear",
description: "Open Linear to review migrated items"
},
{
label: "View Migration Log",
description: "Open detailed migration log file"
},
{
label: "Done",
description: "Finish migration"
}
]
}]
}
```
## Helper Functions
```javascript
function detectTeamAndProject(projectPath) {
// Load CCPM configuration
const config = loadCCPMConfig() // from ~/.claude/ccpm-config.yaml
// Try auto-detection from config patterns
for (const [projectId, projectConfig] of Object.entries(config.projects)) {
const patterns = config.context.detection.patterns || []
// Check if path matches any detection pattern
for (const pattern of patterns) {
if (pattern.project === projectId) {
const regex = new RegExp(pattern.pattern.replace('*', '.*'))
if (regex.test(projectPath)) {
return {
team: projectConfig.linear.team,
project: projectConfig.linear.project,
projectId: projectId
}
}
}
}
// Also check repository URL match
if (projectConfig.repository?.url && projectPath.includes(projectConfig.repository.url)) {
return {
team: projectConfig.linear.team,
project: projectConfig.linear.project,
projectId: projectId
}
}
}
// If no match, use active project or prompt
const activeProject = config.context.current_project
if (activeProject && config.projects[activeProject]) {
return {
team: config.projects[activeProject].linear.team,
project: config.projects[activeProject].linear.project,
projectId: activeProject
}
}
// Last resort: prompt user to select from available projects
return promptForProject(config.projects)
}
function loadCCPMConfig() {
// Load from ~/.claude/ccpm-config.yaml
const configPath = path.join(process.env.HOME, '.claude', 'ccpm-config.yaml')
return yaml.parse(fs.readFileSync(configPath, 'utf8'))
}
function extractMetadata(content) {
const metadata = {}
// Extract title (first # heading)
const titleMatch = content.match(/^#\s+(.+)$/m)
metadata.title = titleMatch ? titleMatch[1] : 'Untitled'
// Extract created date
const createdMatch = content.match(/\*\*Created[:\s]+\*\*\s*(\d{4}-\d{2}-\d{2})/i)
metadata.created = createdMatch ? createdMatch[1] : null
// Extract status
const statusMatch = content.match(/\*\*Status[:\s]+\*\*\s*(.+?)(?:\n|$)/i)
metadata.status = statusMatch ? statusMatch[1].trim() : null
// Extract priority
const priorityMatch = content.match(/\*\*Priority[:\s]+\*\*\s*(.+?)(?:\n|$)/i)
metadata.priority = priorityMatch ? mapPriority(priorityMatch[1].trim()) : null
return metadata
}
function extractChecklist(content) {
const tasks = []
const checklistRegex = /- \[ \] \*\*(.+?)\*\*[:\s]+(.+?)(?:\(Est: (.+?)\))?$/gm
let match
while ((match = checklistRegex.exec(content)) !== null) {
tasks.push({
title: match[1].trim(),
description: match[2].trim(),
estimate: match[3] ? match[3].trim() : null
})
}
return tasks
}
```
## Notes
- **Safe Migration**: Original files are moved, not deleted
- **Preserves History**: Original file path saved in Linear description
- **Full Content**: Entire markdown content migrated to Linear Documents
- **Relationships**: Checklists → Sub-tasks automatically
- **Status Mapping**: Original status preserved as label
- **Breadcrumbs**: .migrated.txt files created with Linear links
- **Rollback**: Can restore from `.claude/migrated/` if needed