specpro-cli 0.1.0__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.
- specpro_cli/__init__.py +16 -0
- specpro_cli/assets/commands/specpro.analyze.md +1102 -0
- specpro_cli/assets/commands/specpro.checklist.md +335 -0
- specpro_cli/assets/commands/specpro.clarify.md +581 -0
- specpro_cli/assets/commands/specpro.constitution.md +488 -0
- specpro_cli/assets/commands/specpro.feature.md +115 -0
- specpro_cli/assets/commands/specpro.implement.md +1881 -0
- specpro_cli/assets/commands/specpro.manual-test.md +206 -0
- specpro_cli/assets/commands/specpro.plan.md +3284 -0
- specpro_cli/assets/commands/specpro.qc.md +1489 -0
- specpro_cli/assets/commands/specpro.scenarios.md +154 -0
- specpro_cli/assets/commands/specpro.specify.md +1449 -0
- specpro_cli/assets/commands/specpro.status.md +863 -0
- specpro_cli/assets/commands/specpro.tasks.md +1207 -0
- specpro_cli/assets/commands/specpro.test-implement.md +462 -0
- specpro_cli/assets/commands/specpro.test-plan.md +383 -0
- specpro_cli/assets/commands/specpro.user-manual.md +178 -0
- specpro_cli/assets/scripts/bash/check-anti-coupling.sh +293 -0
- specpro_cli/assets/scripts/bash/check-prerequisites.sh +176 -0
- specpro_cli/assets/scripts/bash/common.sh +88 -0
- specpro_cli/assets/scripts/bash/create-new-feature.sh +336 -0
- specpro_cli/assets/scripts/bash/qc-auto-fix.sh +121 -0
- specpro_cli/assets/scripts/bash/setup-plan.sh +60 -0
- specpro_cli/assets/scripts/bash/verify-cumulative-records.sh +203 -0
- specpro_cli/assets/scripts/bash/verify-deliverables-tracked.sh +147 -0
- specpro_cli/assets/scripts/bash/verify-deployment.sh +239 -0
- specpro_cli/assets/scripts/bash/verify-frontmatter-yaml.sh +63 -0
- specpro_cli/assets/scripts/bash/verify-ledger.sh +376 -0
- specpro_cli/assets/scripts/bash/verify-shapes.sh +1082 -0
- specpro_cli/assets/scripts/git-hooks/pre-commit +243 -0
- specpro_cli/assets/scripts/install-git-hooks.sh +67 -0
- specpro_cli/assets/scripts/powershell/check-anti-coupling.ps1 +249 -0
- specpro_cli/assets/scripts/powershell/check-prerequisites.ps1 +148 -0
- specpro_cli/assets/scripts/powershell/common.ps1 +95 -0
- specpro_cli/assets/scripts/powershell/create-new-feature.ps1 +229 -0
- specpro_cli/assets/scripts/powershell/qc-auto-fix.ps1 +110 -0
- specpro_cli/assets/scripts/powershell/setup-plan.ps1 +61 -0
- specpro_cli/assets/scripts/powershell/verify-cumulative-records.ps1 +133 -0
- specpro_cli/assets/scripts/powershell/verify-deliverables-tracked.ps1 +112 -0
- specpro_cli/assets/scripts/powershell/verify-deployment.ps1 +278 -0
- specpro_cli/assets/scripts/powershell/verify-frontmatter-yaml.ps1 +56 -0
- specpro_cli/assets/scripts/powershell/verify-ledger.ps1 +383 -0
- specpro_cli/assets/scripts/powershell/verify-shapes.ps1 +978 -0
- specpro_cli/assets/templates/agent-context-template.md +49 -0
- specpro_cli/assets/templates/assumptions-template.md +248 -0
- specpro_cli/assets/templates/checklist-template.md +40 -0
- specpro_cli/assets/templates/clarifications-template.md +155 -0
- specpro_cli/assets/templates/constitution-template.md +50 -0
- specpro_cli/assets/templates/feature-spec-template.md +66 -0
- specpro_cli/assets/templates/plan-overview-template.md +150 -0
- specpro_cli/assets/templates/plan-template.md +387 -0
- specpro_cli/assets/templates/protocol-golden-bytes-guide.md +195 -0
- specpro_cli/assets/templates/requirements-template.md +356 -0
- specpro_cli/assets/templates/spec-template.md +267 -0
- specpro_cli/assets/templates/tasks-template.md +252 -0
- specpro_cli/assets/templates/test-tasks-template.md +174 -0
- specpro_cli/cli/__init__.py +5 -0
- specpro_cli/cli/cmd_init.py +416 -0
- specpro_cli/cli/cmd_remove.py +122 -0
- specpro_cli/cli/entry.py +181 -0
- specpro_cli/integrations/__init__.py +36 -0
- specpro_cli/integrations/base.py +601 -0
- specpro_cli/integrations/claude/__init__.py +101 -0
- specpro_cli/integrations/copilot/__init__.py +153 -0
- specpro_cli/integrations/cursor_agent/__init__.py +51 -0
- specpro_cli/integrations/gemini/__init__.py +44 -0
- specpro_cli/integrations/opencode/__init__.py +48 -0
- specpro_cli/integrations/qodercli/__init__.py +54 -0
- specpro_cli/integrations/registry.py +88 -0
- specpro_cli/packaged/__init__.py +5 -0
- specpro_cli/packaged/sync.py +106 -0
- specpro_cli-0.1.0.dist-info/METADATA +117 -0
- specpro_cli-0.1.0.dist-info/RECORD +76 -0
- specpro_cli-0.1.0.dist-info/WHEEL +4 -0
- specpro_cli-0.1.0.dist-info/entry_points.txt +2 -0
- specpro_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Consolidated prerequisite checking for the Spec-Driven Development workflow.
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Contract-equivalent to `scripts/bash/check-prerequisites.sh`: the same options, the
|
|
7
|
+
same two output modes, and the same exit codes.
|
|
8
|
+
|
|
9
|
+
⚠️ Arguments are parsed with a manual loop rather than a `param()` block, and that
|
|
10
|
+
is deliberate: callers are command documents that invoke the Bash side as
|
|
11
|
+
`check-prerequisites.sh --json`, and a `param()` block would reject `--json`
|
|
12
|
+
(PowerShell binds `-Json`). Accepting both spellings keeps one call site valid on
|
|
13
|
+
both platforms — the alternative is two call conventions that will drift.
|
|
14
|
+
|
|
15
|
+
.EXAMPLE
|
|
16
|
+
./check-prerequisites.ps1 --json --require-tasks --include-tasks
|
|
17
|
+
./check-prerequisites.ps1 --paths-only
|
|
18
|
+
#>
|
|
19
|
+
Set-StrictMode -Version Latest
|
|
20
|
+
$ErrorActionPreference = 'Stop'
|
|
21
|
+
|
|
22
|
+
. (Join-Path $PSScriptRoot 'common.ps1')
|
|
23
|
+
|
|
24
|
+
$jsonMode = $false
|
|
25
|
+
$requireTasks = $false
|
|
26
|
+
$includeTasks = $false
|
|
27
|
+
$pathsOnly = $false
|
|
28
|
+
|
|
29
|
+
foreach ($arg in $args) {
|
|
30
|
+
switch -Regex ($arg) {
|
|
31
|
+
'^--?json$' { $jsonMode = $true; continue }
|
|
32
|
+
'^--?require-tasks$' { $requireTasks = $true; continue }
|
|
33
|
+
'^--?include-tasks$' { $includeTasks = $true; continue }
|
|
34
|
+
'^--?paths-only$' { $pathsOnly = $true; continue }
|
|
35
|
+
'^--?help$|^-h$' {
|
|
36
|
+
# ⚠️ Reproduces the Bash help text byte-for-byte EXCEPT the script's own
|
|
37
|
+
# name: a usage message that named the other platform's script would send
|
|
38
|
+
# the reader looking for a file that is not there. Script name is the ONE
|
|
39
|
+
# intentional difference; the ` ` (two-space) lines below are not blanks
|
|
40
|
+
# by accident — they are what the Bash heredoc emits.
|
|
41
|
+
@'
|
|
42
|
+
Usage: check-prerequisites.ps1 [OPTIONS]
|
|
43
|
+
|
|
44
|
+
Consolidated prerequisite checking for Spec-Driven Development workflow.
|
|
45
|
+
|
|
46
|
+
OPTIONS:
|
|
47
|
+
--json Output in JSON format
|
|
48
|
+
--require-tasks Require tasks.md to exist (for implementation phase)
|
|
49
|
+
--include-tasks Include tasks.md in AVAILABLE_DOCS list
|
|
50
|
+
--paths-only Only output path variables (no prerequisite validation)
|
|
51
|
+
--help, -h Show this help message
|
|
52
|
+
|
|
53
|
+
EXAMPLES:
|
|
54
|
+
# Check task prerequisites (plan.md required)
|
|
55
|
+
./check-prerequisites.ps1 --json
|
|
56
|
+
|
|
57
|
+
# Check implementation prerequisites (plan.md + tasks.md required)
|
|
58
|
+
./check-prerequisites.ps1 --json --require-tasks --include-tasks
|
|
59
|
+
|
|
60
|
+
# Get feature paths only (no validation)
|
|
61
|
+
./check-prerequisites.ps1 --paths-only
|
|
62
|
+
|
|
63
|
+
'@ | Write-Output
|
|
64
|
+
exit 0
|
|
65
|
+
}
|
|
66
|
+
default {
|
|
67
|
+
[Console]::Error.WriteLine("ERROR: Unknown option '$arg'. Use --help for usage information.")
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
$paths = Get-FeaturePaths
|
|
74
|
+
$featureDir = $paths.FEATURE_DIR
|
|
75
|
+
$implPlan = $paths.IMPL_PLAN
|
|
76
|
+
$featureSpec = $paths.FEATURE_SPEC
|
|
77
|
+
$tasks = $paths.TASKS
|
|
78
|
+
|
|
79
|
+
# --- paths-only: emit and exit (no validation) ----------------------------------------
|
|
80
|
+
if ($pathsOnly) {
|
|
81
|
+
if ($jsonMode) {
|
|
82
|
+
# Minimal JSON paths payload. Hand-built to keep the key order and the exact
|
|
83
|
+
# byte shape the Bash side prints.
|
|
84
|
+
# ⚠️ Literal braces in a `-f` format string MUST be doubled: a bare `{` opens a
|
|
85
|
+
# format item. The Bash side needs no escape, which is why the two expressions
|
|
86
|
+
# look alike and are not — the parity check is what catches a missed doubling.
|
|
87
|
+
$json = '{{"REPO_ROOT":"{0}","BRANCH":"{1}","FEATURE_SCOPE":"{2}","FEATURE_DIR":"{3}","FEATURE_SPEC":"{4}","IMPL_PLAN":"{5}","TASKS":"{6}"}}' -f `
|
|
88
|
+
$paths.REPO_ROOT, $paths.CURRENT_BRANCH, $paths.FEATURE_SCOPE, $paths.FEATURE_DIR, $paths.FEATURE_SPEC, $paths.IMPL_PLAN, $paths.TASKS
|
|
89
|
+
Write-Output $json
|
|
90
|
+
} else {
|
|
91
|
+
Write-Output "REPO_ROOT: $($paths.REPO_ROOT)"
|
|
92
|
+
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
|
93
|
+
Write-Output "FEATURE_SCOPE: $($paths.FEATURE_SCOPE)"
|
|
94
|
+
Write-Output "FEATURE_DIR: $($paths.FEATURE_DIR)"
|
|
95
|
+
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
|
|
96
|
+
Write-Output "IMPL_PLAN: $($paths.IMPL_PLAN)"
|
|
97
|
+
Write-Output "TASKS: $($paths.TASKS)"
|
|
98
|
+
}
|
|
99
|
+
exit 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# --- validation ------------------------------------------------------------------------
|
|
103
|
+
if (-not (Test-Path -LiteralPath $featureDir -PathType Container)) {
|
|
104
|
+
[Console]::Error.WriteLine("ERROR: Feature directory not found: $featureDir")
|
|
105
|
+
[Console]::Error.WriteLine('Run /specpro-specify first to create the feature structure.')
|
|
106
|
+
exit 1
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (-not (Test-Path -LiteralPath $implPlan -PathType Leaf)) {
|
|
110
|
+
[Console]::Error.WriteLine("ERROR: plan.md not found in $featureDir")
|
|
111
|
+
[Console]::Error.WriteLine('Run /specpro-plan first to create the implementation plan.')
|
|
112
|
+
exit 1
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ($requireTasks -and -not (Test-Path -LiteralPath $tasks -PathType Leaf)) {
|
|
116
|
+
[Console]::Error.WriteLine("ERROR: tasks.md not found in $featureDir")
|
|
117
|
+
[Console]::Error.WriteLine('Run /specpro-tasks first to create the task list.')
|
|
118
|
+
exit 1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
# --- available documents ---------------------------------------------------------------
|
|
122
|
+
$docs = [System.Collections.Generic.List[string]]::new()
|
|
123
|
+
if (Test-Path -LiteralPath $featureSpec -PathType Leaf) { $docs.Add('spec.md') }
|
|
124
|
+
if (Test-Path -LiteralPath $paths.RESEARCH -PathType Leaf) { $docs.Add('research.md') }
|
|
125
|
+
if (Test-Path -LiteralPath $paths.DATA_MODEL -PathType Leaf) { $docs.Add('data-model.md') }
|
|
126
|
+
if ((Test-Path -LiteralPath $paths.CONTRACTS_DIR -PathType Container) -and
|
|
127
|
+
((Get-ChildItem -LiteralPath $paths.CONTRACTS_DIR -Force | Measure-Object).Count -gt 0)) {
|
|
128
|
+
$docs.Add('contracts/')
|
|
129
|
+
}
|
|
130
|
+
if (Test-Path -LiteralPath $paths.QUICKSTART -PathType Leaf) { $docs.Add('quickstart.md') }
|
|
131
|
+
if ($includeTasks -and (Test-Path -LiteralPath $tasks -PathType Leaf)) { $docs.Add('tasks.md') }
|
|
132
|
+
# Optional high-level test plan — never required.
|
|
133
|
+
if (Test-Path -LiteralPath $paths.TEST_TASKS -PathType Leaf) { $docs.Add('test-tasks.md') }
|
|
134
|
+
|
|
135
|
+
if ($jsonMode) {
|
|
136
|
+
$jsonDocs = '[' + (($docs | ForEach-Object { '"' + $_ + '"' }) -join ',') + ']'
|
|
137
|
+
Write-Output ('{{"FEATURE_DIR":"{0}","FEATURE_SCOPE":"{1}","AVAILABLE_DOCS":{2}}}' -f $featureDir, $paths.FEATURE_SCOPE, $jsonDocs)
|
|
138
|
+
} else {
|
|
139
|
+
Write-Output "FEATURE_DIR:$featureDir"
|
|
140
|
+
Write-Output 'AVAILABLE_DOCS:'
|
|
141
|
+
Write-CheckFile $featureSpec 'spec.md'
|
|
142
|
+
Write-CheckFile $paths.RESEARCH 'research.md'
|
|
143
|
+
Write-CheckFile $paths.DATA_MODEL 'data-model.md'
|
|
144
|
+
Write-CheckDir $paths.CONTRACTS_DIR 'contracts/'
|
|
145
|
+
Write-CheckFile $paths.QUICKSTART 'quickstart.md'
|
|
146
|
+
if ($includeTasks) { Write-CheckFile $tasks 'tasks.md' }
|
|
147
|
+
Write-CheckFile $paths.TEST_TASKS 'test-tasks.md'
|
|
148
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Common functions for all specpro support scripts (PowerShell).
|
|
2
|
+
#
|
|
3
|
+
# Contract-equivalent to `scripts/bash/common.sh`. "Contract" here means what callers
|
|
4
|
+
# depend on: the same three queries, the same returned path set, and the same fallbacks
|
|
5
|
+
# — NOT the same function names (PowerShell casing is the convention on this side) and
|
|
6
|
+
# not the same implementation.
|
|
7
|
+
#
|
|
8
|
+
# Dot-source this file; it defines functions and has no side effects.
|
|
9
|
+
|
|
10
|
+
Set-StrictMode -Version Latest
|
|
11
|
+
|
|
12
|
+
# Repository root, with a fallback for non-git repositories.
|
|
13
|
+
function Get-RepoRoot {
|
|
14
|
+
if (Test-HasGit) {
|
|
15
|
+
# `git rev-parse --show-toplevel` already returns an absolute, forward-slash path.
|
|
16
|
+
$root = & git rev-parse --show-toplevel 2>$null
|
|
17
|
+
if ($LASTEXITCODE -eq 0 -and $root) { return $root.Trim() }
|
|
18
|
+
}
|
|
19
|
+
# Fall back to the script location: <repo>/scripts/powershell/common.ps1 -> <repo>
|
|
20
|
+
return (Resolve-Path (Join-Path $PSScriptRoot '..' '..')).Path
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Current branch — informational only. Branch information is NOT used for directory
|
|
24
|
+
# structure; every spec file lives directly under specs/.
|
|
25
|
+
function Get-CurrentBranch {
|
|
26
|
+
if (Test-HasGit) {
|
|
27
|
+
$branch = & git rev-parse --abbrev-ref HEAD 2>$null
|
|
28
|
+
if ($LASTEXITCODE -eq 0 -and $branch) { return $branch.Trim() }
|
|
29
|
+
}
|
|
30
|
+
return 'unknown'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# Is git available and is this a repository?
|
|
34
|
+
function Test-HasGit {
|
|
35
|
+
& git rev-parse --show-toplevel *> $null
|
|
36
|
+
return ($LASTEXITCODE -eq 0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Every path a caller needs. Mirrors `get_feature_paths` one key at a time: a caller
|
|
40
|
+
# that reads a key the Bash side emits must find it here too.
|
|
41
|
+
function Get-FeaturePaths {
|
|
42
|
+
$repoRoot = Get-RepoRoot
|
|
43
|
+
$specsDir = Join-Path $repoRoot 'specs'
|
|
44
|
+
# ── Feature-scope detection (FR-063 / T050 · T255, v0.23) ──
|
|
45
|
+
# cwd inside specs/fNNN-简称/ ⇒ feature scope: spec/plan/tasks/test-tasks resolve
|
|
46
|
+
# to THAT directory; constitution/research stay mother-side (living-reference
|
|
47
|
+
# inheritance, FR-014). Greppable anchors: 「feature 作用域」/「母作用域」.
|
|
48
|
+
# FEATURE_SCOPE is an ASCII value ('true'/'false') — the same decision that keeps
|
|
49
|
+
# the Bash side safe through eval word-splitting.
|
|
50
|
+
$scopeDir = [System.IO.Path]::GetFullPath((Get-Location).ProviderPath)
|
|
51
|
+
$inFeature = 'false'
|
|
52
|
+
if ($scopeDir -match '/specs/f[0-9][0-9][0-9]-') { $inFeature = 'true' }
|
|
53
|
+
if ($inFeature -eq 'true') {
|
|
54
|
+
$fd = $scopeDir
|
|
55
|
+
$implan = Join-Path $scopeDir 'plan.md'
|
|
56
|
+
$tasksPath = Join-Path $scopeDir 'tasks.md'
|
|
57
|
+
$tt = Join-Path $scopeDir 'test-tasks.md'
|
|
58
|
+
} else {
|
|
59
|
+
$fd = $specsDir
|
|
60
|
+
$implan = Join-Path $specsDir 'plan.md'
|
|
61
|
+
$tasksPath = Join-Path $specsDir 'tasks.md'
|
|
62
|
+
$tt = Join-Path $specsDir 'test-tasks.md'
|
|
63
|
+
}
|
|
64
|
+
[ordered]@{
|
|
65
|
+
REPO_ROOT = $repoRoot
|
|
66
|
+
CURRENT_BRANCH = Get-CurrentBranch
|
|
67
|
+
HAS_GIT = if (Test-HasGit) { 'true' } else { 'false' }
|
|
68
|
+
FEATURE_SCOPE = $inFeature
|
|
69
|
+
FEATURE_DIR = $fd
|
|
70
|
+
FEATURE_SPEC = Join-Path $fd 'spec.md'
|
|
71
|
+
IMPL_PLAN = $implan
|
|
72
|
+
TASKS = $tasksPath
|
|
73
|
+
RESEARCH = Join-Path $specsDir 'research.md'
|
|
74
|
+
DATA_MODEL = Join-Path $specsDir 'data-model.md'
|
|
75
|
+
QUICKSTART = Join-Path $specsDir 'quickstart.md'
|
|
76
|
+
TEST_TASKS = $tt
|
|
77
|
+
CONTRACTS_DIR = Join-Path $fd 'contracts'
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# The two status lines the text output mode prints. Same glyphs and same leading
|
|
82
|
+
# spacing as the Bash side — these are read by humans comparing the two outputs.
|
|
83
|
+
function Write-CheckFile {
|
|
84
|
+
param([string]$Path, [string]$Label)
|
|
85
|
+
if (Test-Path -LiteralPath $Path -PathType Leaf) { Write-Output " ✓ $Label" }
|
|
86
|
+
else { Write-Output " ✗ $Label" }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Write-CheckDir {
|
|
90
|
+
param([string]$Path, [string]$Label)
|
|
91
|
+
$hasContent = (Test-Path -LiteralPath $Path -PathType Container) -and
|
|
92
|
+
((Get-ChildItem -LiteralPath $Path -Force | Measure-Object).Count -gt 0)
|
|
93
|
+
if ($hasContent) { Write-Output " ✓ $Label" }
|
|
94
|
+
else { Write-Output " ✗ $Label" }
|
|
95
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Ensure specs/spec.md exists, seeding it from the template, and report the feature number.
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Contract-equivalent to `scripts/bash/create-new-feature.sh`.
|
|
7
|
+
|
|
8
|
+
⚠️ Despite that script's filename and its own header calling it legacy, what it
|
|
9
|
+
actually does is the **current** layout: it writes `specs/spec.md` directly and
|
|
10
|
+
creates no branch subdirectory. Its header describes what it used to do. The port
|
|
11
|
+
follows the behaviour, which is what the callers depend on — see ISS-50.
|
|
12
|
+
|
|
13
|
+
Branch names are computed and reported but **do not direct any directory**; every
|
|
14
|
+
spec file lives directly under specs/.
|
|
15
|
+
|
|
16
|
+
.EXAMPLE
|
|
17
|
+
./create-new-feature.ps1 --json "Add multi-platform install support"
|
|
18
|
+
./create-new-feature.ps1 --json --short-name "plugin-arch" --number 7
|
|
19
|
+
#>
|
|
20
|
+
Set-StrictMode -Version Latest
|
|
21
|
+
$ErrorActionPreference = 'Stop'
|
|
22
|
+
|
|
23
|
+
. (Join-Path $PSScriptRoot 'common.ps1')
|
|
24
|
+
|
|
25
|
+
$jsonMode = $false
|
|
26
|
+
$shortName = ''
|
|
27
|
+
$branchNumber = ''
|
|
28
|
+
$positional = @()
|
|
29
|
+
|
|
30
|
+
for ($i = 0; $i -lt $args.Count; $i++) {
|
|
31
|
+
$arg = $args[$i]
|
|
32
|
+
switch -Regex ($arg) {
|
|
33
|
+
'^--?json$' { $jsonMode = $true; continue }
|
|
34
|
+
'^--?short-name$' {
|
|
35
|
+
if ($i + 1 -ge $args.Count -or $args[$i + 1] -like '-*') {
|
|
36
|
+
Write-Error 'Error: --short-name requires a value'
|
|
37
|
+
exit 1
|
|
38
|
+
}
|
|
39
|
+
$shortName = $args[++$i]; continue
|
|
40
|
+
}
|
|
41
|
+
'^--?number$' {
|
|
42
|
+
if ($i + 1 -ge $args.Count -or $args[$i + 1] -like '-*') {
|
|
43
|
+
Write-Error 'Error: --number requires a value'
|
|
44
|
+
exit 1
|
|
45
|
+
}
|
|
46
|
+
$branchNumber = $args[++$i]; continue
|
|
47
|
+
}
|
|
48
|
+
'^--?help$|^-h$' {
|
|
49
|
+
Write-Output 'Usage: create-new-feature.ps1 [OPTIONS] [FEATURE_DESCRIPTION]'
|
|
50
|
+
Write-Output ' --json Output in JSON format'
|
|
51
|
+
Write-Output ' --short-name NAME Use NAME as the branch suffix'
|
|
52
|
+
Write-Output ' --number N Use N as the feature number'
|
|
53
|
+
exit 0
|
|
54
|
+
}
|
|
55
|
+
default { $positional += $arg; continue }
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
$featureDescription = ($positional -join ' ')
|
|
59
|
+
|
|
60
|
+
# A description is REQUIRED — the options modify how one is turned into a number and a
|
|
61
|
+
# slug, they do not replace it. Without this check `--short-name x` alone would succeed
|
|
62
|
+
# and produce a branch name with no feature behind it, where the Bash side exits 1.
|
|
63
|
+
if ([string]::IsNullOrEmpty($featureDescription)) {
|
|
64
|
+
# `$PSCommandPath` is the running script — the ONE intentional difference from the
|
|
65
|
+
# Bash side, which prints `$0`. Naming the other platform's file would send the
|
|
66
|
+
# reader looking for something that is not there.
|
|
67
|
+
[Console]::Error.WriteLine("Usage: scripts/powershell/create-new-feature.ps1 [--json] [--short-name <name>] [--number N] <feature_description>")
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# --- repository root -------------------------------------------------------------------
|
|
72
|
+
# Prefer git; fall back to walking up for a repository marker, so the workflow still
|
|
73
|
+
# functions in repositories initialised with --no-git.
|
|
74
|
+
function Find-RepoRoot {
|
|
75
|
+
param([string]$Start)
|
|
76
|
+
$dir = $Start
|
|
77
|
+
while ($dir -and $dir -ne [System.IO.Path]::GetPathRoot($dir)) {
|
|
78
|
+
if ((Test-Path (Join-Path $dir '.git')) -or (Test-Path (Join-Path $dir '.specpro'))) {
|
|
79
|
+
return $dir
|
|
80
|
+
}
|
|
81
|
+
$dir = Split-Path $dir -Parent
|
|
82
|
+
}
|
|
83
|
+
return $null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (Test-HasGit) {
|
|
87
|
+
$repoRoot = (& git rev-parse --show-toplevel).Trim()
|
|
88
|
+
$hasGit = $true
|
|
89
|
+
} else {
|
|
90
|
+
$repoRoot = Find-RepoRoot $PSScriptRoot
|
|
91
|
+
if (-not $repoRoot) {
|
|
92
|
+
Write-Error 'Error: Could not determine repository root. Please run this script from within the repository.'
|
|
93
|
+
exit 1
|
|
94
|
+
}
|
|
95
|
+
$hasGit = $false
|
|
96
|
+
}
|
|
97
|
+
Set-Location $repoRoot
|
|
98
|
+
$specsDir = Join-Path $repoRoot 'specs'
|
|
99
|
+
New-Item -ItemType Directory -Force -Path $specsDir | Out-Null
|
|
100
|
+
|
|
101
|
+
# --- numbering helpers -----------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
function Get-HighestFromSpecs {
|
|
104
|
+
param([string]$SpecsDir)
|
|
105
|
+
$highest = 0
|
|
106
|
+
if (Test-Path -LiteralPath $SpecsDir -PathType Container) {
|
|
107
|
+
foreach ($d in Get-ChildItem -LiteralPath $SpecsDir -Directory -Force) {
|
|
108
|
+
if ($d.Name -match '^([0-9]+)') {
|
|
109
|
+
# Base-10 explicitly: a leading zero would otherwise read as octal.
|
|
110
|
+
$n = [int]$Matches[1]
|
|
111
|
+
if ($n -gt $highest) { $highest = $n }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return $highest
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Get-HighestFromBranches {
|
|
119
|
+
$highest = 0
|
|
120
|
+
$branches = & git branch -a 2>$null
|
|
121
|
+
if ($LASTEXITCODE -ne 0 -or -not $branches) { return 0 }
|
|
122
|
+
foreach ($b in $branches) {
|
|
123
|
+
$clean = ($b -replace '^[* ]*', '') -replace '^remotes/[^/]+/', ''
|
|
124
|
+
if ($clean -match '^([0-9]{3})-') {
|
|
125
|
+
$n = [int]$Matches[1]
|
|
126
|
+
if ($n -gt $highest) { $highest = $n }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return $highest
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Get-NextBranchNumber {
|
|
133
|
+
param([string]$SpecsDir)
|
|
134
|
+
& git fetch --all --prune *> $null # failures are expected with no remote
|
|
135
|
+
$fromBranches = Get-HighestFromBranches
|
|
136
|
+
$fromSpecs = Get-HighestFromSpecs $SpecsDir
|
|
137
|
+
return ([Math]::Max($fromBranches, $fromSpecs) + 1)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function Get-CleanBranchName {
|
|
141
|
+
param([string]$Name)
|
|
142
|
+
$s = $Name.ToLowerInvariant() -replace '[^a-z0-9]', '-'
|
|
143
|
+
$s = $s -replace '-+', '-'
|
|
144
|
+
return $s.Trim('-')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function Get-GeneratedBranchName {
|
|
148
|
+
param([string]$Description)
|
|
149
|
+
$stopWords = '^(i|a|an|the|to|for|of|in|on|at|by|with|from|is|are|was|were|be|been|being|have|has|had|do|does|did|will|would|should|could|can|may|might|must|shall|this|that|these|those|my|your|our|their|want|need|add|get|set)$'
|
|
150
|
+
|
|
151
|
+
$words = ($Description.ToLowerInvariant() -replace '[^a-z0-9]', ' ') -split '\s+'
|
|
152
|
+
$meaningful = @()
|
|
153
|
+
foreach ($w in $words) {
|
|
154
|
+
if ([string]::IsNullOrEmpty($w)) { continue }
|
|
155
|
+
if ($w -match $stopWords) { continue }
|
|
156
|
+
if ($w.Length -ge 3) { $meaningful += $w; continue }
|
|
157
|
+
# Short words survive only when the ORIGINAL text has them uppercased — the
|
|
158
|
+
# usual signal for an acronym the description cares about.
|
|
159
|
+
#
|
|
160
|
+
# ⚠️ **This is the one place the two implementations deliberately differ.** The
|
|
161
|
+
# Bash side writes `${word^^}` for the uppercasing, which is a Bash 4.0+ expansion
|
|
162
|
+
# — on the Bash 3.2 that macOS ships it fails with "bad substitution", the grep
|
|
163
|
+
# gets a broken pattern, and the short word is silently dropped. So Bash's output
|
|
164
|
+
# here is not a contract: it differs between Bash 3.2 and Bash 4+ on the same
|
|
165
|
+
# input. This side implements the documented INTENT. See ISS-51.
|
|
166
|
+
if ($Description -cmatch "\b$([regex]::Escape($w.ToUpperInvariant()))\b") { $meaningful += $w }
|
|
167
|
+
}
|
|
168
|
+
if ($meaningful.Count -gt 0) {
|
|
169
|
+
$max = if ($meaningful.Count -eq 4) { 4 } else { 3 }
|
|
170
|
+
$take = $meaningful | Select-Object -First $max
|
|
171
|
+
return ($take -join '-')
|
|
172
|
+
}
|
|
173
|
+
$cleaned = Get-CleanBranchName $Description
|
|
174
|
+
$parts = @($cleaned -split '-' | Where-Object { $_ -ne '' } | Select-Object -First 3)
|
|
175
|
+
return ($parts -join '-')
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# --- compose the branch name ------------------------------------------------------------
|
|
179
|
+
$branchSuffix = if ($shortName) { Get-CleanBranchName $shortName } else { Get-GeneratedBranchName $featureDescription }
|
|
180
|
+
|
|
181
|
+
if (-not $branchNumber) {
|
|
182
|
+
$branchNumber = if ($hasGit) { Get-NextBranchNumber $specsDir } else { (Get-HighestFromSpecs $specsDir) + 1 }
|
|
183
|
+
}
|
|
184
|
+
$featureNum = '{0:d3}' -f [int]$branchNumber
|
|
185
|
+
$branchName = "$featureNum-$branchSuffix"
|
|
186
|
+
|
|
187
|
+
# GitHub enforces a 244-byte limit on branch names.
|
|
188
|
+
$maxBranchLength = 244
|
|
189
|
+
if ($branchName.Length -gt $maxBranchLength) {
|
|
190
|
+
$maxSuffixLength = $maxBranchLength - 4 # number (3) + hyphen (1)
|
|
191
|
+
$truncated = $branchSuffix.Substring(0, [Math]::Min($maxSuffixLength, $branchSuffix.Length)).TrimEnd('-')
|
|
192
|
+
$original = $branchName
|
|
193
|
+
$branchName = "$featureNum-$truncated"
|
|
194
|
+
[Console]::Error.WriteLine("[specify] Warning: Branch name exceeded GitHub's 244-byte limit")
|
|
195
|
+
[Console]::Error.WriteLine("[specify] Original: $original ($($original.Length) bytes)")
|
|
196
|
+
[Console]::Error.WriteLine("[specify] Truncated to: $branchName ($($branchName.Length) bytes)")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
# --- produce the spec file --------------------------------------------------------------
|
|
200
|
+
# No Git branch is created for the specification: one project has one spec.md that
|
|
201
|
+
# evolves. Branch information is reported, never used for directory structure.
|
|
202
|
+
$featureDir = $specsDir
|
|
203
|
+
New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
|
|
204
|
+
$specFile = Join-Path $featureDir 'spec.md'
|
|
205
|
+
|
|
206
|
+
if (-not (Test-Path -LiteralPath $specFile -PathType Leaf)) {
|
|
207
|
+
$template = Join-Path $repoRoot '.specpro' 'templates' 'spec-template.md'
|
|
208
|
+
if (Test-Path -LiteralPath $template -PathType Leaf) {
|
|
209
|
+
Copy-Item -LiteralPath $template -Destination $specFile
|
|
210
|
+
[Console]::Error.WriteLine("[specify] Created initial specification: $specFile")
|
|
211
|
+
} else {
|
|
212
|
+
New-Item -ItemType File -Force -Path $specFile | Out-Null
|
|
213
|
+
[Console]::Error.WriteLine("[specify] Created empty specification: $specFile")
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
[Console]::Error.WriteLine("[specify] Specification already exists: $specFile")
|
|
217
|
+
[Console]::Error.WriteLine('[specify] Use /specpro-specify with update intent to evolve the existing spec')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
$env:SPECIFY_FEATURE = $branchName
|
|
221
|
+
|
|
222
|
+
if ($jsonMode) {
|
|
223
|
+
Write-Output ('{{"BRANCH_NAME":"{0}","SPEC_FILE":"{1}","FEATURE_NUM":"{2}"}}' -f $branchName, $specFile, $featureNum)
|
|
224
|
+
} else {
|
|
225
|
+
Write-Output "BRANCH_NAME: $branchName"
|
|
226
|
+
Write-Output "SPEC_FILE: $specFile"
|
|
227
|
+
Write-Output "FEATURE_NUM: $featureNum"
|
|
228
|
+
Write-Output "SPECIFY_FEATURE environment variable set to: $branchName"
|
|
229
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
QC Auto-Fix — the write-back implementation for `/specpro-qc`.
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Contract-equivalent to `scripts/bash/qc-auto-fix.sh`. The contract is deliberately
|
|
7
|
+
strong here: **for the same inputs, both implementations print byte-identical output
|
|
8
|
+
and return the same exit code** (FR-008) — this script is invoked from a command
|
|
9
|
+
document, and a pair that disagrees across platforms is worse than one that is merely
|
|
10
|
+
missing, because each side looks authoritative.
|
|
11
|
+
|
|
12
|
+
Why this exists (T186 / ISS-143, settled 2026-09-19): these bodies used to live inside
|
|
13
|
+
`commands/specpro.qc.md` as bash fences. That put an executable implementation in a
|
|
14
|
+
Markdown document, which the landing policy (`plan.md` -> 落点政策 §1) forbids: an
|
|
15
|
+
implementation that lives in prose can only be verified by EXTRACTING it, and that
|
|
16
|
+
extraction depends on the Markdown's shape and returns an empty string — silently —
|
|
17
|
+
the moment the shape changes. T164 verified its own guard exactly that way.
|
|
18
|
+
|
|
19
|
+
What the move must not lose: `apply_fix` is the ONE write-back path, and its value is
|
|
20
|
+
entirely in three guards added the hard way (T164) —
|
|
21
|
+
|
|
22
|
+
1. the destination is a REQUIRED argument (an omitted target ABORTS, it does not
|
|
23
|
+
guess: a defaulted parameter once delivered the CHECKLIST's content into
|
|
24
|
+
`spec.md`, observed as a 202-byte spec.md replaced by a 100-byte checklist)
|
|
25
|
+
2. a non-zero transformer status aborts
|
|
26
|
+
3. an empty result aborts
|
|
27
|
+
|
|
28
|
+
…and then exactly ONE move.
|
|
29
|
+
|
|
30
|
+
.PARAMETER Command
|
|
31
|
+
`apply-fix` — and that is the whole surface. ⚠️ Two more subcommands were removed
|
|
32
|
+
on 2026-09-19 (`ISS-186` / `T220`): the one that rewrote `spec.md` prose, and the
|
|
33
|
+
one that authorised repairing another command's artifact. The ruling made
|
|
34
|
+
`/specpro-qc` a checker that **does not repair** (`FR-058`), so there is nothing
|
|
35
|
+
left for an authorisation guard to guard — see the Bash twin's header for the full
|
|
36
|
+
reasoning, which is kept in one place on purpose.
|
|
37
|
+
.PARAMETER Args
|
|
38
|
+
apply-fix: <tmp> <gen-status> <dest>
|
|
39
|
+
.OUTPUTS
|
|
40
|
+
Exit 0 = the write happened; 1 = aborted, nothing written.
|
|
41
|
+
#>
|
|
42
|
+
|
|
43
|
+
param(
|
|
44
|
+
[Parameter(Position = 0)][string]$Command,
|
|
45
|
+
[Parameter(Position = 1, ValueFromRemainingArguments = $true)][string[]]$Args
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
$ErrorActionPreference = 'Stop'
|
|
49
|
+
|
|
50
|
+
function Write-Usage {
|
|
51
|
+
# ⚠️ The usage names the COMMAND, not this file — same reason as the Bash twin's note.
|
|
52
|
+
[Console]::Error.WriteLine('usage: qc-auto-fix apply-fix <tmp> <gen-status> <dest>')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# =======================================================================================
|
|
56
|
+
# Invoke-ApplyFix — the ONE write-back path
|
|
57
|
+
# =======================================================================================
|
|
58
|
+
function Invoke-ApplyFix {
|
|
59
|
+
param([string]$Tmp, [string]$GenStatus, [string]$Target)
|
|
60
|
+
|
|
61
|
+
if ([string]::IsNullOrEmpty($Tmp)) {
|
|
62
|
+
[Console]::Error.WriteLine(' ✗ fix aborted: no result to write — apply_fix needs a source')
|
|
63
|
+
return 1
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if ([string]::IsNullOrEmpty($Target)) {
|
|
67
|
+
[Console]::Error.WriteLine(' ✗ fix aborted: no destination given — apply_fix does not guess')
|
|
68
|
+
Remove-Item -Force -ErrorAction SilentlyContinue $Tmp
|
|
69
|
+
return 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if ([int]$GenStatus -ne 0) {
|
|
73
|
+
[Console]::Error.WriteLine(" ✗ fix aborted: the transformer exited $GenStatus — $Target left untouched")
|
|
74
|
+
Remove-Item -Force -ErrorAction SilentlyContinue $Tmp
|
|
75
|
+
return 1
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# An empty result is a failure, not a fix: every caller's output is derived from an
|
|
79
|
+
# input it had to READ, so a zero-byte result means that read failed.
|
|
80
|
+
if ((-not (Test-Path -LiteralPath $Tmp)) -or ((Get-Item -LiteralPath $Tmp).Length -eq 0)) {
|
|
81
|
+
[Console]::Error.WriteLine(" ✗ fix aborted: the result is empty — $Target left untouched")
|
|
82
|
+
Remove-Item -Force -ErrorAction SilentlyContinue $Tmp
|
|
83
|
+
return 1
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
Move-Item -LiteralPath $Tmp -Destination $Target -Force
|
|
87
|
+
return 0
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# =======================================================================================
|
|
91
|
+
# dispatch
|
|
92
|
+
# =======================================================================================
|
|
93
|
+
# ⚠️ `@($Args)` is NOT empty when no arguments were passed — PowerShell binds one $null
|
|
94
|
+
# element, so `.Count` reads 1 and every arity guard below is skipped. Measured: with the
|
|
95
|
+
# null element left in, `apply-fix` with no arguments fell through to the write-back path
|
|
96
|
+
# and died on an empty path instead of printing the usage the Bash side prints.
|
|
97
|
+
$rest = @($Args | Where-Object { $null -ne $_ -and $_ -ne '' })
|
|
98
|
+
switch ($Command) {
|
|
99
|
+
'apply-fix' {
|
|
100
|
+
if ($rest.Count -lt 1) { Write-Usage; exit 1 }
|
|
101
|
+
$tmp = $rest[0]
|
|
102
|
+
$gen = if ($rest.Count -ge 2) { $rest[1] } else { '0' }
|
|
103
|
+
$dest = if ($rest.Count -ge 3) { $rest[2] } else { '' }
|
|
104
|
+
exit (Invoke-ApplyFix -Tmp $tmp -GenStatus $gen -Target $dest)
|
|
105
|
+
}
|
|
106
|
+
default {
|
|
107
|
+
Write-Usage
|
|
108
|
+
exit 1
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env pwsh
|
|
2
|
+
<#
|
|
3
|
+
.SYNOPSIS
|
|
4
|
+
Prepare the plan artifact for the current project.
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Contract-equivalent to `scripts/bash/setup-plan.sh`: ensure specs/ exists, seed
|
|
7
|
+
plan.md from the template when it is absent, and report the paths in either JSON or
|
|
8
|
+
text form.
|
|
9
|
+
|
|
10
|
+
As with the other scripts here, arguments are parsed manually so the `--json`
|
|
11
|
+
spelling used by the Bash side keeps working.
|
|
12
|
+
#>
|
|
13
|
+
Set-StrictMode -Version Latest
|
|
14
|
+
$ErrorActionPreference = 'Stop'
|
|
15
|
+
|
|
16
|
+
. (Join-Path $PSScriptRoot 'common.ps1')
|
|
17
|
+
|
|
18
|
+
$jsonMode = $false
|
|
19
|
+
foreach ($arg in $args) {
|
|
20
|
+
switch -Regex ($arg) {
|
|
21
|
+
'^--?json$' { $jsonMode = $true; continue }
|
|
22
|
+
'^--?help$|^-h$' {
|
|
23
|
+
Write-Output "Usage: setup-plan.ps1 [--json]"
|
|
24
|
+
Write-Output " --json Output results in JSON format"
|
|
25
|
+
Write-Output " --help Show this help message"
|
|
26
|
+
exit 0
|
|
27
|
+
}
|
|
28
|
+
default { continue } # The Bash side collects unknown args and ignores them.
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
$paths = Get-FeaturePaths
|
|
33
|
+
$featureDir = $paths.FEATURE_DIR
|
|
34
|
+
$implPlan = $paths.IMPL_PLAN
|
|
35
|
+
|
|
36
|
+
New-Item -ItemType Directory -Force -Path $featureDir | Out-Null
|
|
37
|
+
|
|
38
|
+
$template = Join-Path $paths.REPO_ROOT '.specpro' 'templates' 'plan-template.md'
|
|
39
|
+
|
|
40
|
+
if (Test-Path -LiteralPath $implPlan -PathType Leaf) {
|
|
41
|
+
Write-Output "Using existing plan at $implPlan"
|
|
42
|
+
} elseif (Test-Path -LiteralPath $template -PathType Leaf) {
|
|
43
|
+
Copy-Item -LiteralPath $template -Destination $implPlan
|
|
44
|
+
Write-Output "Created plan from template at $implPlan"
|
|
45
|
+
} else {
|
|
46
|
+
Write-Output "Warning: Plan template not found at $template"
|
|
47
|
+
# Same fallback as the Bash side: leave a file behind so later steps have something
|
|
48
|
+
# to write into rather than failing on a missing path.
|
|
49
|
+
New-Item -ItemType File -Force -Path $implPlan | Out-Null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if ($jsonMode) {
|
|
53
|
+
Write-Output ('{{"FEATURE_SPEC":"{0}","IMPL_PLAN":"{1}","SPECS_DIR":"{2}","BRANCH":"{3}","HAS_GIT":"{4}"}}' -f `
|
|
54
|
+
$paths.FEATURE_SPEC, $implPlan, $featureDir, $paths.CURRENT_BRANCH, $paths.HAS_GIT)
|
|
55
|
+
} else {
|
|
56
|
+
Write-Output "FEATURE_SPEC: $($paths.FEATURE_SPEC)"
|
|
57
|
+
Write-Output "IMPL_PLAN: $implPlan"
|
|
58
|
+
Write-Output "SPECS_DIR: $featureDir"
|
|
59
|
+
Write-Output "BRANCH: $($paths.CURRENT_BRANCH)"
|
|
60
|
+
Write-Output "HAS_GIT: $($paths.HAS_GIT)"
|
|
61
|
+
}
|