doit-toolkit-cli 0.1.9__py3-none-any.whl
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.
- doit_cli/__init__.py +1356 -0
- doit_cli/cli/__init__.py +26 -0
- doit_cli/cli/analytics_command.py +616 -0
- doit_cli/cli/context_command.py +213 -0
- doit_cli/cli/diagram_command.py +304 -0
- doit_cli/cli/fixit_command.py +641 -0
- doit_cli/cli/hooks_command.py +211 -0
- doit_cli/cli/init_command.py +613 -0
- doit_cli/cli/memory_command.py +293 -0
- doit_cli/cli/status_command.py +117 -0
- doit_cli/cli/sync_prompts_command.py +248 -0
- doit_cli/cli/validate_command.py +196 -0
- doit_cli/cli/verify_command.py +204 -0
- doit_cli/cli/workflow_mixin.py +224 -0
- doit_cli/cli/xref_command.py +555 -0
- doit_cli/formatters/__init__.py +8 -0
- doit_cli/formatters/base.py +38 -0
- doit_cli/formatters/json_formatter.py +126 -0
- doit_cli/formatters/markdown_formatter.py +97 -0
- doit_cli/formatters/rich_formatter.py +257 -0
- doit_cli/main.py +49 -0
- doit_cli/models/__init__.py +139 -0
- doit_cli/models/agent.py +74 -0
- doit_cli/models/analytics_models.py +384 -0
- doit_cli/models/context_config.py +464 -0
- doit_cli/models/crossref_models.py +182 -0
- doit_cli/models/diagram_models.py +363 -0
- doit_cli/models/fixit_models.py +355 -0
- doit_cli/models/hook_config.py +125 -0
- doit_cli/models/project.py +91 -0
- doit_cli/models/results.py +121 -0
- doit_cli/models/search_models.py +228 -0
- doit_cli/models/status_models.py +195 -0
- doit_cli/models/sync_models.py +146 -0
- doit_cli/models/template.py +77 -0
- doit_cli/models/validation_models.py +175 -0
- doit_cli/models/workflow_models.py +319 -0
- doit_cli/prompts/__init__.py +5 -0
- doit_cli/prompts/fixit_prompts.py +344 -0
- doit_cli/prompts/interactive.py +390 -0
- doit_cli/rules/__init__.py +5 -0
- doit_cli/rules/builtin_rules.py +160 -0
- doit_cli/services/__init__.py +79 -0
- doit_cli/services/agent_detector.py +168 -0
- doit_cli/services/analytics_service.py +218 -0
- doit_cli/services/architecture_generator.py +290 -0
- doit_cli/services/backup_service.py +204 -0
- doit_cli/services/config_loader.py +113 -0
- doit_cli/services/context_loader.py +1121 -0
- doit_cli/services/coverage_calculator.py +142 -0
- doit_cli/services/crossref_service.py +237 -0
- doit_cli/services/cycle_time_calculator.py +134 -0
- doit_cli/services/date_inferrer.py +349 -0
- doit_cli/services/diagram_service.py +337 -0
- doit_cli/services/drift_detector.py +109 -0
- doit_cli/services/entity_parser.py +301 -0
- doit_cli/services/er_diagram_generator.py +197 -0
- doit_cli/services/fixit_service.py +699 -0
- doit_cli/services/github_service.py +192 -0
- doit_cli/services/hook_manager.py +258 -0
- doit_cli/services/hook_validator.py +528 -0
- doit_cli/services/input_validator.py +322 -0
- doit_cli/services/memory_search.py +527 -0
- doit_cli/services/mermaid_validator.py +334 -0
- doit_cli/services/prompt_transformer.py +91 -0
- doit_cli/services/prompt_writer.py +133 -0
- doit_cli/services/query_interpreter.py +428 -0
- doit_cli/services/report_exporter.py +219 -0
- doit_cli/services/report_generator.py +256 -0
- doit_cli/services/requirement_parser.py +112 -0
- doit_cli/services/roadmap_summarizer.py +209 -0
- doit_cli/services/rule_engine.py +443 -0
- doit_cli/services/scaffolder.py +215 -0
- doit_cli/services/score_calculator.py +172 -0
- doit_cli/services/section_parser.py +204 -0
- doit_cli/services/spec_scanner.py +327 -0
- doit_cli/services/state_manager.py +355 -0
- doit_cli/services/status_reporter.py +143 -0
- doit_cli/services/task_parser.py +347 -0
- doit_cli/services/template_manager.py +710 -0
- doit_cli/services/template_reader.py +158 -0
- doit_cli/services/user_journey_generator.py +214 -0
- doit_cli/services/user_story_parser.py +232 -0
- doit_cli/services/validation_service.py +188 -0
- doit_cli/services/validator.py +232 -0
- doit_cli/services/velocity_tracker.py +173 -0
- doit_cli/services/workflow_engine.py +405 -0
- doit_cli/templates/agent-file-template.md +28 -0
- doit_cli/templates/checklist-template.md +39 -0
- doit_cli/templates/commands/doit.checkin.md +363 -0
- doit_cli/templates/commands/doit.constitution.md +187 -0
- doit_cli/templates/commands/doit.documentit.md +485 -0
- doit_cli/templates/commands/doit.fixit.md +181 -0
- doit_cli/templates/commands/doit.implementit.md +265 -0
- doit_cli/templates/commands/doit.planit.md +262 -0
- doit_cli/templates/commands/doit.reviewit.md +355 -0
- doit_cli/templates/commands/doit.roadmapit.md +368 -0
- doit_cli/templates/commands/doit.scaffoldit.md +458 -0
- doit_cli/templates/commands/doit.specit.md +521 -0
- doit_cli/templates/commands/doit.taskit.md +304 -0
- doit_cli/templates/commands/doit.testit.md +277 -0
- doit_cli/templates/config/context.yaml +134 -0
- doit_cli/templates/config/hooks.yaml +93 -0
- doit_cli/templates/config/validation-rules.yaml +64 -0
- doit_cli/templates/github-issue-templates/epic.yml +78 -0
- doit_cli/templates/github-issue-templates/feature.yml +116 -0
- doit_cli/templates/github-issue-templates/task.yml +129 -0
- doit_cli/templates/hooks/.gitkeep +0 -0
- doit_cli/templates/hooks/post-commit.sh +25 -0
- doit_cli/templates/hooks/post-merge.sh +75 -0
- doit_cli/templates/hooks/pre-commit.sh +17 -0
- doit_cli/templates/hooks/pre-push.sh +18 -0
- doit_cli/templates/memory/completed_roadmap.md +50 -0
- doit_cli/templates/memory/constitution.md +125 -0
- doit_cli/templates/memory/roadmap.md +61 -0
- doit_cli/templates/plan-template.md +146 -0
- doit_cli/templates/scripts/bash/check-prerequisites.sh +166 -0
- doit_cli/templates/scripts/bash/common.sh +156 -0
- doit_cli/templates/scripts/bash/create-new-feature.sh +297 -0
- doit_cli/templates/scripts/bash/setup-plan.sh +61 -0
- doit_cli/templates/scripts/bash/update-agent-context.sh +675 -0
- doit_cli/templates/scripts/powershell/check-prerequisites.ps1 +148 -0
- doit_cli/templates/scripts/powershell/common.ps1 +137 -0
- doit_cli/templates/scripts/powershell/create-new-feature.ps1 +283 -0
- doit_cli/templates/scripts/powershell/setup-plan.ps1 +61 -0
- doit_cli/templates/scripts/powershell/update-agent-context.ps1 +406 -0
- doit_cli/templates/spec-template.md +159 -0
- doit_cli/templates/tasks-template.md +313 -0
- doit_cli/templates/vscode-settings.json +14 -0
- doit_toolkit_cli-0.1.9.dist-info/METADATA +324 -0
- doit_toolkit_cli-0.1.9.dist-info/RECORD +134 -0
- doit_toolkit_cli-0.1.9.dist-info/WHEEL +4 -0
- doit_toolkit_cli-0.1.9.dist-info/entry_points.txt +2 -0
- doit_toolkit_cli-0.1.9.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
|
|
3
|
+
# Consolidated prerequisite checking script (PowerShell)
|
|
4
|
+
#
|
|
5
|
+
# This script provides unified prerequisite checking for Spec-Driven Development workflow.
|
|
6
|
+
# It replaces the functionality previously spread across multiple scripts.
|
|
7
|
+
#
|
|
8
|
+
# Usage: ./check-prerequisites.ps1 [OPTIONS]
|
|
9
|
+
#
|
|
10
|
+
# OPTIONS:
|
|
11
|
+
# -Json Output in JSON format
|
|
12
|
+
# -RequireTasks Require tasks.md to exist (for implementation phase)
|
|
13
|
+
# -IncludeTasks Include tasks.md in AVAILABLE_DOCS list
|
|
14
|
+
# -PathsOnly Only output path variables (no validation)
|
|
15
|
+
# -Help, -h Show help message
|
|
16
|
+
|
|
17
|
+
[CmdletBinding()]
|
|
18
|
+
param(
|
|
19
|
+
[switch]$Json,
|
|
20
|
+
[switch]$RequireTasks,
|
|
21
|
+
[switch]$IncludeTasks,
|
|
22
|
+
[switch]$PathsOnly,
|
|
23
|
+
[switch]$Help
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
$ErrorActionPreference = 'Stop'
|
|
27
|
+
|
|
28
|
+
# Show help if requested
|
|
29
|
+
if ($Help) {
|
|
30
|
+
Write-Output @"
|
|
31
|
+
Usage: check-prerequisites.ps1 [OPTIONS]
|
|
32
|
+
|
|
33
|
+
Consolidated prerequisite checking for Spec-Driven Development workflow.
|
|
34
|
+
|
|
35
|
+
OPTIONS:
|
|
36
|
+
-Json Output in JSON format
|
|
37
|
+
-RequireTasks Require tasks.md to exist (for implementation phase)
|
|
38
|
+
-IncludeTasks Include tasks.md in AVAILABLE_DOCS list
|
|
39
|
+
-PathsOnly Only output path variables (no prerequisite validation)
|
|
40
|
+
-Help, -h Show this help message
|
|
41
|
+
|
|
42
|
+
EXAMPLES:
|
|
43
|
+
# Check task prerequisites (plan.md required)
|
|
44
|
+
.\check-prerequisites.ps1 -Json
|
|
45
|
+
|
|
46
|
+
# Check implementation prerequisites (plan.md + tasks.md required)
|
|
47
|
+
.\check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks
|
|
48
|
+
|
|
49
|
+
# Get feature paths only (no validation)
|
|
50
|
+
.\check-prerequisites.ps1 -PathsOnly
|
|
51
|
+
|
|
52
|
+
"@
|
|
53
|
+
exit 0
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# Source common functions
|
|
57
|
+
. "$PSScriptRoot/common.ps1"
|
|
58
|
+
|
|
59
|
+
# Get feature paths and validate branch
|
|
60
|
+
$paths = Get-FeaturePathsEnv
|
|
61
|
+
|
|
62
|
+
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit:$paths.HAS_GIT)) {
|
|
63
|
+
exit 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# If paths-only mode, output paths and exit (support combined -Json -PathsOnly)
|
|
67
|
+
if ($PathsOnly) {
|
|
68
|
+
if ($Json) {
|
|
69
|
+
[PSCustomObject]@{
|
|
70
|
+
REPO_ROOT = $paths.REPO_ROOT
|
|
71
|
+
BRANCH = $paths.CURRENT_BRANCH
|
|
72
|
+
FEATURE_DIR = $paths.FEATURE_DIR
|
|
73
|
+
FEATURE_SPEC = $paths.FEATURE_SPEC
|
|
74
|
+
IMPL_PLAN = $paths.IMPL_PLAN
|
|
75
|
+
TASKS = $paths.TASKS
|
|
76
|
+
} | ConvertTo-Json -Compress
|
|
77
|
+
} else {
|
|
78
|
+
Write-Output "REPO_ROOT: $($paths.REPO_ROOT)"
|
|
79
|
+
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
|
80
|
+
Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)"
|
|
81
|
+
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
|
|
82
|
+
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
|
|
83
|
+
Write-Output "TASKS: $($paths.TASKS)"
|
|
84
|
+
}
|
|
85
|
+
exit 0
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# Validate required directories and files
|
|
89
|
+
if (-not (Test-Path $paths.FEATURE_DIR -PathType Container)) {
|
|
90
|
+
Write-Output "ERROR: Feature directory not found: $($paths.FEATURE_DIR)"
|
|
91
|
+
Write-Output "Run /speckit.doit first to create the feature structure."
|
|
92
|
+
exit 1
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (-not (Test-Path $paths.IMPL_PLAN -PathType Leaf)) {
|
|
96
|
+
Write-Output "ERROR: plan.md not found in $($paths.FEATURE_DIR)"
|
|
97
|
+
Write-Output "Run /speckit.plan first to create the implementation plan."
|
|
98
|
+
exit 1
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Check for tasks.md if required
|
|
102
|
+
if ($RequireTasks -and -not (Test-Path $paths.TASKS -PathType Leaf)) {
|
|
103
|
+
Write-Output "ERROR: tasks.md not found in $($paths.FEATURE_DIR)"
|
|
104
|
+
Write-Output "Run /speckit.tasks first to create the task list."
|
|
105
|
+
exit 1
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Build list of available documents
|
|
109
|
+
$docs = @()
|
|
110
|
+
|
|
111
|
+
# Always check these optional docs
|
|
112
|
+
if (Test-Path $paths.RESEARCH) { $docs += 'research.md' }
|
|
113
|
+
if (Test-Path $paths.DATA_MODEL) { $docs += 'data-model.md' }
|
|
114
|
+
|
|
115
|
+
# Check contracts directory (only if it exists and has files)
|
|
116
|
+
if ((Test-Path $paths.CONTRACTS_DIR) -and (Get-ChildItem -Path $paths.CONTRACTS_DIR -ErrorAction SilentlyContinue | Select-Object -First 1)) {
|
|
117
|
+
$docs += 'contracts/'
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (Test-Path $paths.QUICKSTART) { $docs += 'quickstart.md' }
|
|
121
|
+
|
|
122
|
+
# Include tasks.md if requested and it exists
|
|
123
|
+
if ($IncludeTasks -and (Test-Path $paths.TASKS)) {
|
|
124
|
+
$docs += 'tasks.md'
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
# Output results
|
|
128
|
+
if ($Json) {
|
|
129
|
+
# JSON output
|
|
130
|
+
[PSCustomObject]@{
|
|
131
|
+
FEATURE_DIR = $paths.FEATURE_DIR
|
|
132
|
+
AVAILABLE_DOCS = $docs
|
|
133
|
+
} | ConvertTo-Json -Compress
|
|
134
|
+
} else {
|
|
135
|
+
# Text output
|
|
136
|
+
Write-Output "FEATURE_DIR:$($paths.FEATURE_DIR)"
|
|
137
|
+
Write-Output "AVAILABLE_DOCS:"
|
|
138
|
+
|
|
139
|
+
# Show status of each potential document
|
|
140
|
+
Test-FileExists -Path $paths.RESEARCH -Description 'research.md' | Out-Null
|
|
141
|
+
Test-FileExists -Path $paths.DATA_MODEL -Description 'data-model.md' | Out-Null
|
|
142
|
+
Test-DirHasFiles -Path $paths.CONTRACTS_DIR -Description 'contracts/' | Out-Null
|
|
143
|
+
Test-FileExists -Path $paths.QUICKSTART -Description 'quickstart.md' | Out-Null
|
|
144
|
+
|
|
145
|
+
if ($IncludeTasks) {
|
|
146
|
+
Test-FileExists -Path $paths.TASKS -Description 'tasks.md' | Out-Null
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Common PowerShell functions analogous to common.sh
|
|
3
|
+
|
|
4
|
+
function Get-RepoRoot {
|
|
5
|
+
try {
|
|
6
|
+
$result = git rev-parse --show-toplevel 2>$null
|
|
7
|
+
if ($LASTEXITCODE -eq 0) {
|
|
8
|
+
return $result
|
|
9
|
+
}
|
|
10
|
+
} catch {
|
|
11
|
+
# Git command failed
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# Fall back to script location for non-git repos
|
|
15
|
+
return (Resolve-Path (Join-Path $PSScriptRoot "../../..")).Path
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function Get-CurrentBranch {
|
|
19
|
+
# First check if SPECIFY_FEATURE environment variable is set
|
|
20
|
+
if ($env:SPECIFY_FEATURE) {
|
|
21
|
+
return $env:SPECIFY_FEATURE
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Then check git if available
|
|
25
|
+
try {
|
|
26
|
+
$result = git rev-parse --abbrev-ref HEAD 2>$null
|
|
27
|
+
if ($LASTEXITCODE -eq 0) {
|
|
28
|
+
return $result
|
|
29
|
+
}
|
|
30
|
+
} catch {
|
|
31
|
+
# Git command failed
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# For non-git repos, try to find the latest feature directory
|
|
35
|
+
$repoRoot = Get-RepoRoot
|
|
36
|
+
$specsDir = Join-Path $repoRoot "specs"
|
|
37
|
+
|
|
38
|
+
if (Test-Path $specsDir) {
|
|
39
|
+
$latestFeature = ""
|
|
40
|
+
$highest = 0
|
|
41
|
+
|
|
42
|
+
Get-ChildItem -Path $specsDir -Directory | ForEach-Object {
|
|
43
|
+
if ($_.Name -match '^(\d{3})-') {
|
|
44
|
+
$num = [int]$matches[1]
|
|
45
|
+
if ($num -gt $highest) {
|
|
46
|
+
$highest = $num
|
|
47
|
+
$latestFeature = $_.Name
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ($latestFeature) {
|
|
53
|
+
return $latestFeature
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Final fallback
|
|
58
|
+
return "main"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function Test-HasGit {
|
|
62
|
+
try {
|
|
63
|
+
git rev-parse --show-toplevel 2>$null | Out-Null
|
|
64
|
+
return ($LASTEXITCODE -eq 0)
|
|
65
|
+
} catch {
|
|
66
|
+
return $false
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Test-FeatureBranch {
|
|
71
|
+
param(
|
|
72
|
+
[string]$Branch,
|
|
73
|
+
[bool]$HasGit = $true
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# For non-git repos, we can't enforce branch naming but still provide output
|
|
77
|
+
if (-not $HasGit) {
|
|
78
|
+
Write-Warning "[specify] Warning: Git repository not detected; skipped branch validation"
|
|
79
|
+
return $true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if ($Branch -notmatch '^[0-9]{3}-') {
|
|
83
|
+
Write-Output "ERROR: Not on a feature branch. Current branch: $Branch"
|
|
84
|
+
Write-Output "Feature branches should be named like: 001-feature-name"
|
|
85
|
+
return $false
|
|
86
|
+
}
|
|
87
|
+
return $true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function Get-FeatureDir {
|
|
91
|
+
param([string]$RepoRoot, [string]$Branch)
|
|
92
|
+
Join-Path $RepoRoot "specs/$Branch"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function Get-FeaturePathsEnv {
|
|
96
|
+
$repoRoot = Get-RepoRoot
|
|
97
|
+
$currentBranch = Get-CurrentBranch
|
|
98
|
+
$hasGit = Test-HasGit
|
|
99
|
+
$featureDir = Get-FeatureDir -RepoRoot $repoRoot -Branch $currentBranch
|
|
100
|
+
|
|
101
|
+
[PSCustomObject]@{
|
|
102
|
+
REPO_ROOT = $repoRoot
|
|
103
|
+
CURRENT_BRANCH = $currentBranch
|
|
104
|
+
HAS_GIT = $hasGit
|
|
105
|
+
FEATURE_DIR = $featureDir
|
|
106
|
+
FEATURE_SPEC = Join-Path $featureDir 'spec.md'
|
|
107
|
+
IMPL_PLAN = Join-Path $featureDir 'plan.md'
|
|
108
|
+
TASKS = Join-Path $featureDir 'tasks.md'
|
|
109
|
+
RESEARCH = Join-Path $featureDir 'research.md'
|
|
110
|
+
DATA_MODEL = Join-Path $featureDir 'data-model.md'
|
|
111
|
+
QUICKSTART = Join-Path $featureDir 'quickstart.md'
|
|
112
|
+
CONTRACTS_DIR = Join-Path $featureDir 'contracts'
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function Test-FileExists {
|
|
117
|
+
param([string]$Path, [string]$Description)
|
|
118
|
+
if (Test-Path -Path $Path -PathType Leaf) {
|
|
119
|
+
Write-Output " ✓ $Description"
|
|
120
|
+
return $true
|
|
121
|
+
} else {
|
|
122
|
+
Write-Output " ✗ $Description"
|
|
123
|
+
return $false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Test-DirHasFiles {
|
|
128
|
+
param([string]$Path, [string]$Description)
|
|
129
|
+
if ((Test-Path -Path $Path -PathType Container) -and (Get-ChildItem -Path $Path -ErrorAction SilentlyContinue | Where-Object { -not $_.PSIsContainer } | Select-Object -First 1)) {
|
|
130
|
+
Write-Output " ✓ $Description"
|
|
131
|
+
return $true
|
|
132
|
+
} else {
|
|
133
|
+
Write-Output " ✗ $Description"
|
|
134
|
+
return $false
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Create a new feature
|
|
3
|
+
[CmdletBinding()]
|
|
4
|
+
param(
|
|
5
|
+
[switch]$Json,
|
|
6
|
+
[string]$ShortName,
|
|
7
|
+
[int]$Number = 0,
|
|
8
|
+
[switch]$Help,
|
|
9
|
+
[Parameter(ValueFromRemainingArguments = $true)]
|
|
10
|
+
[string[]]$FeatureDescription
|
|
11
|
+
)
|
|
12
|
+
$ErrorActionPreference = 'Stop'
|
|
13
|
+
|
|
14
|
+
# Show help if requested
|
|
15
|
+
if ($Help) {
|
|
16
|
+
Write-Host "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] [-Number N] <feature description>"
|
|
17
|
+
Write-Host ""
|
|
18
|
+
Write-Host "Options:"
|
|
19
|
+
Write-Host " -Json Output in JSON format"
|
|
20
|
+
Write-Host " -ShortName <name> Provide a custom short name (2-4 words) for the branch"
|
|
21
|
+
Write-Host " -Number N Specify branch number manually (overrides auto-detection)"
|
|
22
|
+
Write-Host " -Help Show this help message"
|
|
23
|
+
Write-Host ""
|
|
24
|
+
Write-Host "Examples:"
|
|
25
|
+
Write-Host " ./create-new-feature.ps1 'Add user authentication system' -ShortName 'user-auth'"
|
|
26
|
+
Write-Host " ./create-new-feature.ps1 'Implement OAuth2 integration for API'"
|
|
27
|
+
exit 0
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Check if feature description provided
|
|
31
|
+
if (-not $FeatureDescription -or $FeatureDescription.Count -eq 0) {
|
|
32
|
+
Write-Error "Usage: ./create-new-feature.ps1 [-Json] [-ShortName <name>] <feature description>"
|
|
33
|
+
exit 1
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
$featureDesc = ($FeatureDescription -join ' ').Trim()
|
|
37
|
+
|
|
38
|
+
# Resolve repository root. Prefer git information when available, but fall back
|
|
39
|
+
# to searching for repository markers so the workflow still functions in repositories that
|
|
40
|
+
# were initialized with --no-git.
|
|
41
|
+
function Find-RepositoryRoot {
|
|
42
|
+
param(
|
|
43
|
+
[string]$StartDir,
|
|
44
|
+
[string[]]$Markers = @('.git', '.doit')
|
|
45
|
+
)
|
|
46
|
+
$current = Resolve-Path $StartDir
|
|
47
|
+
while ($true) {
|
|
48
|
+
foreach ($marker in $Markers) {
|
|
49
|
+
if (Test-Path (Join-Path $current $marker)) {
|
|
50
|
+
return $current
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
$parent = Split-Path $current -Parent
|
|
54
|
+
if ($parent -eq $current) {
|
|
55
|
+
# Reached filesystem root without finding markers
|
|
56
|
+
return $null
|
|
57
|
+
}
|
|
58
|
+
$current = $parent
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Get-HighestNumberFromSpecs {
|
|
63
|
+
param([string]$SpecsDir)
|
|
64
|
+
|
|
65
|
+
$highest = 0
|
|
66
|
+
if (Test-Path $SpecsDir) {
|
|
67
|
+
Get-ChildItem -Path $SpecsDir -Directory | ForEach-Object {
|
|
68
|
+
if ($_.Name -match '^(\d+)') {
|
|
69
|
+
$num = [int]$matches[1]
|
|
70
|
+
if ($num -gt $highest) { $highest = $num }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return $highest
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Get-HighestNumberFromBranches {
|
|
78
|
+
param()
|
|
79
|
+
|
|
80
|
+
$highest = 0
|
|
81
|
+
try {
|
|
82
|
+
$branches = git branch -a 2>$null
|
|
83
|
+
if ($LASTEXITCODE -eq 0) {
|
|
84
|
+
foreach ($branch in $branches) {
|
|
85
|
+
# Clean branch name: remove leading markers and remote prefixes
|
|
86
|
+
$cleanBranch = $branch.Trim() -replace '^\*?\s+', '' -replace '^remotes/[^/]+/', ''
|
|
87
|
+
|
|
88
|
+
# Extract feature number if branch matches pattern ###-*
|
|
89
|
+
if ($cleanBranch -match '^(\d+)-') {
|
|
90
|
+
$num = [int]$matches[1]
|
|
91
|
+
if ($num -gt $highest) { $highest = $num }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
# If git command fails, return 0
|
|
97
|
+
Write-Verbose "Could not check Git branches: $_"
|
|
98
|
+
}
|
|
99
|
+
return $highest
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function Get-NextBranchNumber {
|
|
103
|
+
param(
|
|
104
|
+
[string]$SpecsDir
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# Fetch all remotes to get latest branch info (suppress errors if no remotes)
|
|
108
|
+
try {
|
|
109
|
+
git fetch --all --prune 2>$null | Out-Null
|
|
110
|
+
} catch {
|
|
111
|
+
# Ignore fetch errors
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
# Get highest number from ALL branches (not just matching short name)
|
|
115
|
+
$highestBranch = Get-HighestNumberFromBranches
|
|
116
|
+
|
|
117
|
+
# Get highest number from ALL specs (not just matching short name)
|
|
118
|
+
$highestSpec = Get-HighestNumberFromSpecs -SpecsDir $SpecsDir
|
|
119
|
+
|
|
120
|
+
# Take the maximum of both
|
|
121
|
+
$maxNum = [Math]::Max($highestBranch, $highestSpec)
|
|
122
|
+
|
|
123
|
+
# Return next number
|
|
124
|
+
return $maxNum + 1
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ConvertTo-CleanBranchName {
|
|
128
|
+
param([string]$Name)
|
|
129
|
+
|
|
130
|
+
return $Name.ToLower() -replace '[^a-z0-9]', '-' -replace '-{2,}', '-' -replace '^-', '' -replace '-$', ''
|
|
131
|
+
}
|
|
132
|
+
$fallbackRoot = (Find-RepositoryRoot -StartDir $PSScriptRoot)
|
|
133
|
+
if (-not $fallbackRoot) {
|
|
134
|
+
Write-Error "Error: Could not determine repository root. Please run this script from within the repository."
|
|
135
|
+
exit 1
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
$repoRoot = git rev-parse --show-toplevel 2>$null
|
|
140
|
+
if ($LASTEXITCODE -eq 0) {
|
|
141
|
+
$hasGit = $true
|
|
142
|
+
} else {
|
|
143
|
+
throw "Git not available"
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
$repoRoot = $fallbackRoot
|
|
147
|
+
$hasGit = $false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
Set-Location $repoRoot
|
|
151
|
+
|
|
152
|
+
$specsDir = Join-Path $repoRoot 'specs'
|
|
153
|
+
New-Item -ItemType Directory -Path $specsDir -Force | Out-Null
|
|
154
|
+
|
|
155
|
+
# Function to generate branch name with stop word filtering and length filtering
|
|
156
|
+
function Get-BranchName {
|
|
157
|
+
param([string]$Description)
|
|
158
|
+
|
|
159
|
+
# Common stop words to filter out
|
|
160
|
+
$stopWords = @(
|
|
161
|
+
'i', 'a', 'an', 'the', 'to', 'for', 'of', 'in', 'on', 'at', 'by', 'with', 'from',
|
|
162
|
+
'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
|
|
163
|
+
'do', 'does', 'did', 'will', 'would', 'should', 'could', 'can', 'may', 'might', 'must', 'shall',
|
|
164
|
+
'this', 'that', 'these', 'those', 'my', 'your', 'our', 'their',
|
|
165
|
+
'want', 'need', 'add', 'get', 'set'
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# Convert to lowercase and extract words (alphanumeric only)
|
|
169
|
+
$cleanName = $Description.ToLower() -replace '[^a-z0-9\s]', ' '
|
|
170
|
+
$words = $cleanName -split '\s+' | Where-Object { $_ }
|
|
171
|
+
|
|
172
|
+
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
|
173
|
+
$meaningfulWords = @()
|
|
174
|
+
foreach ($word in $words) {
|
|
175
|
+
# Skip stop words
|
|
176
|
+
if ($stopWords -contains $word) { continue }
|
|
177
|
+
|
|
178
|
+
# Keep words that are length >= 3 OR appear as uppercase in original (likely acronyms)
|
|
179
|
+
if ($word.Length -ge 3) {
|
|
180
|
+
$meaningfulWords += $word
|
|
181
|
+
} elseif ($Description -match "\b$($word.ToUpper())\b") {
|
|
182
|
+
# Keep short words if they appear as uppercase in original (likely acronyms)
|
|
183
|
+
$meaningfulWords += $word
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
# If we have meaningful words, use first 3-4 of them
|
|
188
|
+
if ($meaningfulWords.Count -gt 0) {
|
|
189
|
+
$maxWords = if ($meaningfulWords.Count -eq 4) { 4 } else { 3 }
|
|
190
|
+
$result = ($meaningfulWords | Select-Object -First $maxWords) -join '-'
|
|
191
|
+
return $result
|
|
192
|
+
} else {
|
|
193
|
+
# Fallback to original logic if no meaningful words found
|
|
194
|
+
$result = ConvertTo-CleanBranchName -Name $Description
|
|
195
|
+
$fallbackWords = ($result -split '-') | Where-Object { $_ } | Select-Object -First 3
|
|
196
|
+
return [string]::Join('-', $fallbackWords)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
# Generate branch name
|
|
201
|
+
if ($ShortName) {
|
|
202
|
+
# Use provided short name, just clean it up
|
|
203
|
+
$branchSuffix = ConvertTo-CleanBranchName -Name $ShortName
|
|
204
|
+
} else {
|
|
205
|
+
# Generate from description with smart filtering
|
|
206
|
+
$branchSuffix = Get-BranchName -Description $featureDesc
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
# Determine branch number
|
|
210
|
+
if ($Number -eq 0) {
|
|
211
|
+
if ($hasGit) {
|
|
212
|
+
# Check existing branches on remotes
|
|
213
|
+
$Number = Get-NextBranchNumber -SpecsDir $specsDir
|
|
214
|
+
} else {
|
|
215
|
+
# Fall back to local directory check
|
|
216
|
+
$Number = (Get-HighestNumberFromSpecs -SpecsDir $specsDir) + 1
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
$featureNum = ('{0:000}' -f $Number)
|
|
221
|
+
$branchName = "$featureNum-$branchSuffix"
|
|
222
|
+
|
|
223
|
+
# GitHub enforces a 244-byte limit on branch names
|
|
224
|
+
# Validate and truncate if necessary
|
|
225
|
+
$maxBranchLength = 244
|
|
226
|
+
if ($branchName.Length -gt $maxBranchLength) {
|
|
227
|
+
# Calculate how much we need to trim from suffix
|
|
228
|
+
# Account for: feature number (3) + hyphen (1) = 4 chars
|
|
229
|
+
$maxSuffixLength = $maxBranchLength - 4
|
|
230
|
+
|
|
231
|
+
# Truncate suffix
|
|
232
|
+
$truncatedSuffix = $branchSuffix.Substring(0, [Math]::Min($branchSuffix.Length, $maxSuffixLength))
|
|
233
|
+
# Remove trailing hyphen if truncation created one
|
|
234
|
+
$truncatedSuffix = $truncatedSuffix -replace '-$', ''
|
|
235
|
+
|
|
236
|
+
$originalBranchName = $branchName
|
|
237
|
+
$branchName = "$featureNum-$truncatedSuffix"
|
|
238
|
+
|
|
239
|
+
Write-Warning "[specify] Branch name exceeded GitHub's 244-byte limit"
|
|
240
|
+
Write-Warning "[specify] Original: $originalBranchName ($($originalBranchName.Length) bytes)"
|
|
241
|
+
Write-Warning "[specify] Truncated to: $branchName ($($branchName.Length) bytes)"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if ($hasGit) {
|
|
245
|
+
try {
|
|
246
|
+
git checkout -b $branchName | Out-Null
|
|
247
|
+
} catch {
|
|
248
|
+
Write-Warning "Failed to create git branch: $branchName"
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
Write-Warning "[specify] Warning: Git repository not detected; skipped branch creation for $branchName"
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
$featureDir = Join-Path $specsDir $branchName
|
|
255
|
+
New-Item -ItemType Directory -Path $featureDir -Force | Out-Null
|
|
256
|
+
|
|
257
|
+
$template = Join-Path $repoRoot '.doit/templates/spec-template.md'
|
|
258
|
+
$specFile = Join-Path $featureDir 'spec.md'
|
|
259
|
+
if (Test-Path $template) {
|
|
260
|
+
Copy-Item $template $specFile -Force
|
|
261
|
+
} else {
|
|
262
|
+
New-Item -ItemType File -Path $specFile | Out-Null
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
# Set the SPECIFY_FEATURE environment variable for the current session
|
|
266
|
+
$env:SPECIFY_FEATURE = $branchName
|
|
267
|
+
|
|
268
|
+
if ($Json) {
|
|
269
|
+
$obj = [PSCustomObject]@{
|
|
270
|
+
BRANCH_NAME = $branchName
|
|
271
|
+
SPEC_FILE = $specFile
|
|
272
|
+
FEATURE_NUM = $featureNum
|
|
273
|
+
HAS_GIT = $hasGit
|
|
274
|
+
}
|
|
275
|
+
$obj | ConvertTo-Json -Compress
|
|
276
|
+
} else {
|
|
277
|
+
Write-Output "BRANCH_NAME: $branchName"
|
|
278
|
+
Write-Output "SPEC_FILE: $specFile"
|
|
279
|
+
Write-Output "FEATURE_NUM: $featureNum"
|
|
280
|
+
Write-Output "HAS_GIT: $hasGit"
|
|
281
|
+
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
|
282
|
+
}
|
|
283
|
+
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
# Setup implementation plan for a feature
|
|
3
|
+
|
|
4
|
+
[CmdletBinding()]
|
|
5
|
+
param(
|
|
6
|
+
[switch]$Json,
|
|
7
|
+
[switch]$Help
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
$ErrorActionPreference = 'Stop'
|
|
11
|
+
|
|
12
|
+
# Show help if requested
|
|
13
|
+
if ($Help) {
|
|
14
|
+
Write-Output "Usage: ./setup-plan.ps1 [-Json] [-Help]"
|
|
15
|
+
Write-Output " -Json Output results in JSON format"
|
|
16
|
+
Write-Output " -Help Show this help message"
|
|
17
|
+
exit 0
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
# Load common functions
|
|
21
|
+
. "$PSScriptRoot/common.ps1"
|
|
22
|
+
|
|
23
|
+
# Get all paths and variables from common functions
|
|
24
|
+
$paths = Get-FeaturePathsEnv
|
|
25
|
+
|
|
26
|
+
# Check if we're on a proper feature branch (only for git repos)
|
|
27
|
+
if (-not (Test-FeatureBranch -Branch $paths.CURRENT_BRANCH -HasGit $paths.HAS_GIT)) {
|
|
28
|
+
exit 1
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Ensure the feature directory exists
|
|
32
|
+
New-Item -ItemType Directory -Path $paths.FEATURE_DIR -Force | Out-Null
|
|
33
|
+
|
|
34
|
+
# Copy plan template if it exists, otherwise note it or create empty file
|
|
35
|
+
$template = Join-Path $paths.REPO_ROOT '.doit/templates/plan-template.md'
|
|
36
|
+
if (Test-Path $template) {
|
|
37
|
+
Copy-Item $template $paths.IMPL_PLAN -Force
|
|
38
|
+
Write-Output "Copied plan template to $($paths.IMPL_PLAN)"
|
|
39
|
+
} else {
|
|
40
|
+
Write-Warning "Plan template not found at $template"
|
|
41
|
+
# Create a basic plan file if template doesn't exist
|
|
42
|
+
New-Item -ItemType File -Path $paths.IMPL_PLAN -Force | Out-Null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Output results
|
|
46
|
+
if ($Json) {
|
|
47
|
+
$result = [PSCustomObject]@{
|
|
48
|
+
FEATURE_SPEC = $paths.FEATURE_SPEC
|
|
49
|
+
IMPL_PLAN = $paths.IMPL_PLAN
|
|
50
|
+
SPECS_DIR = $paths.FEATURE_DIR
|
|
51
|
+
BRANCH = $paths.CURRENT_BRANCH
|
|
52
|
+
HAS_GIT = $paths.HAS_GIT
|
|
53
|
+
}
|
|
54
|
+
$result | ConvertTo-Json -Compress
|
|
55
|
+
} else {
|
|
56
|
+
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
|
|
57
|
+
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
|
|
58
|
+
Write-Output "SPECS_DIR: $($paths.FEATURE_DIR)"
|
|
59
|
+
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
|
60
|
+
Write-Output "HAS_GIT: $($paths.HAS_GIT)"
|
|
61
|
+
}
|