tmux-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex/skills/speckit/SKILL.md +173 -0
- package/.codex/skills/speckit/assets/templates/checklist-template.md +49 -0
- package/.codex/skills/speckit/assets/templates/notes-entrypoints-template.md +11 -0
- package/.codex/skills/speckit/assets/templates/notes-questions-template.md +7 -0
- package/.codex/skills/speckit/assets/templates/notes-readme-template.md +36 -0
- package/.codex/skills/speckit/assets/templates/notes-session-template.md +21 -0
- package/.codex/skills/speckit/assets/templates/plan-template.md +126 -0
- package/.codex/skills/speckit/assets/templates/spec-template.md +135 -0
- package/.codex/skills/speckit/assets/templates/tasks-template.md +269 -0
- package/.codex/skills/speckit/references/acceptance.md +183 -0
- package/.codex/skills/speckit/references/analyze.md +186 -0
- package/.codex/skills/speckit/references/checklist.md +302 -0
- package/.codex/skills/speckit/references/clarify-auto.md +69 -0
- package/.codex/skills/speckit/references/clarify-detailed.md +78 -0
- package/.codex/skills/speckit/references/clarify.md +189 -0
- package/.codex/skills/speckit/references/constitution.md +90 -0
- package/.codex/skills/speckit/references/group.md +89 -0
- package/.codex/skills/speckit/references/implement-task.md +115 -0
- package/.codex/skills/speckit/references/implement.md +129 -0
- package/.codex/skills/speckit/references/notes.md +82 -0
- package/.codex/skills/speckit/references/plan-deep.md +87 -0
- package/.codex/skills/speckit/references/plan-from-questions.md +115 -0
- package/.codex/skills/speckit/references/plan-from-review.md +89 -0
- package/.codex/skills/speckit/references/plan.md +97 -0
- package/.codex/skills/speckit/references/review-plan.md +156 -0
- package/.codex/skills/speckit/references/specify.md +246 -0
- package/.codex/skills/speckit/references/tasks.md +155 -0
- package/.codex/skills/speckit/references/taskstoissues.md +33 -0
- package/.codex/skills/speckit/scripts/bash/check-prerequisites.sh +206 -0
- package/.codex/skills/speckit/scripts/bash/common.sh +191 -0
- package/.codex/skills/speckit/scripts/bash/create-new-feature.sh +259 -0
- package/.codex/skills/speckit/scripts/bash/extract-coded-points.sh +322 -0
- package/.codex/skills/speckit/scripts/bash/extract-spec-ids.sh +238 -0
- package/.codex/skills/speckit/scripts/bash/extract-tasks.sh +295 -0
- package/.codex/skills/speckit/scripts/bash/extract-user-stories.sh +312 -0
- package/.codex/skills/speckit/scripts/bash/setup-notes.sh +182 -0
- package/.codex/skills/speckit/scripts/bash/setup-plan.sh +110 -0
- package/.codex/skills/speckit/scripts/bash/show-todo-tasks.sh +257 -0
- package/.codex/skills/speckit/scripts/bash/spec-group-checklist.sh +402 -0
- package/.codex/skills/speckit/scripts/bash/spec-group-members.sh +215 -0
- package/.codex/skills/speckit/scripts/bash/spec-registry-graph.sh +399 -0
- package/.specify/memory/constitution.md +67 -0
- package/.specify/templates/agent-file-template.md +28 -0
- package/.specify/templates/checklist-template.md +49 -0
- package/.specify/templates/plan-template.md +126 -0
- package/.specify/templates/spec-template.md +135 -0
- package/.specify/templates/tasks-template.md +269 -0
- package/README.md +128 -0
- package/README.zh-CN.md +127 -0
- package/bun.lock +269 -0
- package/dist/cli/commands/codex/forkHome.js +88 -0
- package/dist/cli/commands/codex/send.js +55 -0
- package/dist/cli/commands/codex/sessionInfo.js +42 -0
- package/dist/cli/commands/codex/spawn.js +68 -0
- package/dist/cli/commands/find.js +26 -0
- package/dist/cli/commands/paneKill.js +33 -0
- package/dist/cli/commands/paneSpawn.js +40 -0
- package/dist/cli/commands/paneTitle.js +33 -0
- package/dist/cli/commands/read.js +34 -0
- package/dist/cli/commands/send.js +51 -0
- package/dist/cli/commands/snapshot.js +19 -0
- package/dist/cli/commands/ui/select.js +41 -0
- package/dist/cli/commands/windowKill.js +25 -0
- package/dist/cli/commands/windowLs.js +15 -0
- package/dist/cli/commands/windowNew.js +28 -0
- package/dist/cli/commands/windowRename.js +25 -0
- package/dist/cli/index.js +365 -0
- package/dist/cli/parse.js +39 -0
- package/dist/lib/codex/forkHome.js +101 -0
- package/dist/lib/codex/isCodexPane.js +55 -0
- package/dist/lib/codex/send.js +58 -0
- package/dist/lib/codex/sessionInfo.js +449 -0
- package/dist/lib/codex/spawn.js +246 -0
- package/dist/lib/contracts/types.js +2 -0
- package/dist/lib/fs/safeRm.js +32 -0
- package/dist/lib/io/readStdin.js +14 -0
- package/dist/lib/os/process.js +55 -0
- package/dist/lib/output/format.js +95 -0
- package/dist/lib/proc/lsof.js +42 -0
- package/dist/lib/proc/ps.js +60 -0
- package/dist/lib/targeting/errors.js +13 -0
- package/dist/lib/targeting/resolvePaneTarget.js +91 -0
- package/dist/lib/targeting/resolveWindowTarget.js +40 -0
- package/dist/lib/targeting/scope.js +58 -0
- package/dist/lib/tmux/capturePane.js +20 -0
- package/dist/lib/tmux/exec.js +66 -0
- package/dist/lib/tmux/paneOps.js +29 -0
- package/dist/lib/tmux/paste.js +23 -0
- package/dist/lib/tmux/sendKeys.js +47 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/tmux/snapshotPanes.js +46 -0
- package/dist/lib/tmux/snapshotWindows.js +24 -0
- package/dist/lib/tmux/windowOps.js +32 -0
- package/dist/lib/ui/popupSelect.js +432 -0
- package/dist/lib/ui/popupSupport.js +76 -0
- package/package.json +23 -0
- package/src/cli/commands/codex/forkHome.ts +141 -0
- package/src/cli/commands/codex/send.ts +83 -0
- package/src/cli/commands/codex/sessionInfo.ts +59 -0
- package/src/cli/commands/codex/spawn.ts +90 -0
- package/src/cli/commands/find.ts +40 -0
- package/src/cli/commands/paneKill.ts +49 -0
- package/src/cli/commands/paneSpawn.ts +53 -0
- package/src/cli/commands/paneTitle.ts +50 -0
- package/src/cli/commands/read.ts +48 -0
- package/src/cli/commands/send.ts +71 -0
- package/src/cli/commands/snapshot.ts +28 -0
- package/src/cli/commands/ui/select.ts +49 -0
- package/src/cli/commands/windowKill.ts +35 -0
- package/src/cli/commands/windowLs.ts +20 -0
- package/src/cli/commands/windowNew.ts +40 -0
- package/src/cli/commands/windowRename.ts +36 -0
- package/src/cli/index.ts +430 -0
- package/src/lib/codex/forkHome.ts +148 -0
- package/src/lib/codex/isCodexPane.ts +56 -0
- package/src/lib/codex/send.ts +84 -0
- package/src/lib/codex/sessionInfo.ts +521 -0
- package/src/lib/codex/spawn.ts +305 -0
- package/src/lib/contracts/types.ts +30 -0
- package/src/lib/fs/safeRm.ts +32 -0
- package/src/lib/io/readStdin.ts +11 -0
- package/src/lib/output/format.ts +105 -0
- package/src/lib/proc/lsof.ts +44 -0
- package/src/lib/proc/ps.ts +70 -0
- package/src/lib/targeting/errors.ts +25 -0
- package/src/lib/targeting/resolvePaneTarget.ts +106 -0
- package/src/lib/targeting/resolveWindowTarget.ts +45 -0
- package/src/lib/targeting/scope.ts +76 -0
- package/src/lib/tmux/capturePane.ts +21 -0
- package/src/lib/tmux/exec.ts +90 -0
- package/src/lib/tmux/paneOps.ts +35 -0
- package/src/lib/tmux/paste.ts +20 -0
- package/src/lib/tmux/sendKeys.ts +72 -0
- package/src/lib/tmux/session.ts +27 -0
- package/src/lib/tmux/snapshotPanes.ts +52 -0
- package/src/lib/tmux/snapshotWindows.ts +23 -0
- package/src/lib/tmux/windowOps.ts +43 -0
- package/src/lib/ui/popupSelect.ts +561 -0
- package/src/lib/ui/popupSupport.ts +84 -0
- package/tests/e2e/codexForkHome.test.ts +146 -0
- package/tests/e2e/codexSessionInfo.test.ts +112 -0
- package/tests/e2e/codexTuiSend.test.ts +68 -0
- package/tests/integration/codexSpawn.test.ts +113 -0
- package/tests/integration/paneOps.test.ts +60 -0
- package/tests/integration/sendRead.test.ts +52 -0
- package/tests/integration/snapshot.test.ts +39 -0
- package/tests/integration/tmuxHarness.ts +39 -0
- package/tests/integration/windowOps.test.ts +60 -0
- package/tests/unit/codexSend.test.ts +105 -0
- package/tests/unit/codexSessionInfo.test.ts +88 -0
- package/tests/unit/codexSpawn.test.ts +34 -0
- package/tests/unit/keys.test.ts +30 -0
- package/tests/unit/outputFormat.test.ts +52 -0
- package/tests/unit/popupSelect.test.ts +77 -0
- package/tests/unit/popupSupport.test.ts +109 -0
- package/tests/unit/resolvePaneTarget.test.ts +43 -0
- package/tests/unit/resolveWindowTarget.test.ts +36 -0
- package/tests/unit/safeRm.test.ts +41 -0
- package/tests/unit/scope.test.ts +57 -0
- package/tsconfig.json +14 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Common functions and variables for all scripts
|
|
3
|
+
|
|
4
|
+
find_repo_root_by_marker() {
|
|
5
|
+
local dir="$1"
|
|
6
|
+
while [[ -n "$dir" && "$dir" != "/" ]]; do
|
|
7
|
+
if [[ -d "$dir/.specify" || -d "$dir/specs" ]]; then
|
|
8
|
+
echo "$dir"
|
|
9
|
+
return 0
|
|
10
|
+
fi
|
|
11
|
+
dir="$(dirname "$dir")"
|
|
12
|
+
done
|
|
13
|
+
return 1
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get_repo_root() {
|
|
17
|
+
local repo_root=""
|
|
18
|
+
|
|
19
|
+
if repo_root="$(find_repo_root_by_marker "${PWD:-}")"; then
|
|
20
|
+
echo "$repo_root"
|
|
21
|
+
return 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
local script_dir="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
25
|
+
if repo_root="$(find_repo_root_by_marker "$script_dir")"; then
|
|
26
|
+
echo "$repo_root"
|
|
27
|
+
return 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Fallback: if we're inside a Git repo, use the repo root.
|
|
31
|
+
if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
32
|
+
git rev-parse --show-toplevel
|
|
33
|
+
return 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Final fallback: use the current working directory so we never default to "/specs".
|
|
37
|
+
local pwd="${PWD:-}"
|
|
38
|
+
if [[ -n "$pwd" ]]; then
|
|
39
|
+
echo "$pwd"
|
|
40
|
+
return 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
pwd
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# Get current feature id (historical name: branch), without any VCS dependence
|
|
47
|
+
get_current_branch() {
|
|
48
|
+
# First check if SPECIFY_FEATURE environment variable is set
|
|
49
|
+
if [[ -n "${SPECIFY_FEATURE:-}" ]]; then
|
|
50
|
+
# Allow shorthand: if user sets SPECIFY_FEATURE to a bare numeric prefix (e.g., "026"),
|
|
51
|
+
# resolve it to the unique specs/<NNN-*> directory if possible.
|
|
52
|
+
if [[ "${SPECIFY_FEATURE}" =~ ^[0-9]{3}$ ]]; then
|
|
53
|
+
local repo_root=""
|
|
54
|
+
repo_root="$(get_repo_root 2>/dev/null || true)"
|
|
55
|
+
local specs_dir="$repo_root/specs"
|
|
56
|
+
if [[ -d "$specs_dir" ]]; then
|
|
57
|
+
local matches=()
|
|
58
|
+
for dir in "$specs_dir"/"${SPECIFY_FEATURE}"-*; do
|
|
59
|
+
if [[ -d "$dir" ]]; then
|
|
60
|
+
matches+=("$(basename "$dir")")
|
|
61
|
+
fi
|
|
62
|
+
done
|
|
63
|
+
if [[ ${#matches[@]} -eq 1 ]]; then
|
|
64
|
+
echo "${matches[0]}"
|
|
65
|
+
return
|
|
66
|
+
fi
|
|
67
|
+
fi
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "$SPECIFY_FEATURE"
|
|
71
|
+
return
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# Then infer from current working directory if we're inside specs/<NNN-*>
|
|
75
|
+
if [[ -n "${PWD:-}" ]] && [[ "${PWD:-}" =~ /specs/([0-9]{3}-[^/]+)(/|$) ]]; then
|
|
76
|
+
echo "${BASH_REMATCH[1]}"
|
|
77
|
+
return
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Finally, fall back to the latest feature directory by numeric prefix
|
|
81
|
+
local repo_root=""
|
|
82
|
+
repo_root="$(get_repo_root 2>/dev/null || true)"
|
|
83
|
+
local specs_dir="$repo_root/specs"
|
|
84
|
+
|
|
85
|
+
if [[ -d "$specs_dir" ]]; then
|
|
86
|
+
local latest_feature=""
|
|
87
|
+
local highest=0
|
|
88
|
+
|
|
89
|
+
for dir in "$specs_dir"/*; do
|
|
90
|
+
if [[ -d "$dir" ]]; then
|
|
91
|
+
local dirname=$(basename "$dir")
|
|
92
|
+
if [[ "$dirname" =~ ^([0-9]{3})- ]]; then
|
|
93
|
+
local number=${BASH_REMATCH[1]}
|
|
94
|
+
number=$((10#$number))
|
|
95
|
+
if [[ "$number" -gt "$highest" ]]; then
|
|
96
|
+
highest=$number
|
|
97
|
+
latest_feature=$dirname
|
|
98
|
+
fi
|
|
99
|
+
fi
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
if [[ -n "$latest_feature" ]]; then
|
|
104
|
+
echo "$latest_feature"
|
|
105
|
+
return
|
|
106
|
+
fi
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
echo ""
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
check_feature_branch() {
|
|
113
|
+
local feature_id="$1"
|
|
114
|
+
|
|
115
|
+
if [[ -z "$feature_id" || ! "$feature_id" =~ ^[0-9]{3}- ]]; then
|
|
116
|
+
echo "ERROR: Unable to determine current feature id (expected: 001-feature-name)" >&2
|
|
117
|
+
echo "Hint: set SPECIFY_FEATURE=\"001-feature-name\" (or \"001\" if unique), or pass --feature 001/001-feature-name." >&2
|
|
118
|
+
return 1
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
get_feature_dir() { echo "$1/specs/$2"; }
|
|
125
|
+
|
|
126
|
+
# Find feature directory by numeric prefix instead of exact branch match
|
|
127
|
+
# This allows multiple branches to work on the same spec (e.g., 004-fix-bug, 004-add-feature)
|
|
128
|
+
find_feature_dir_by_prefix() {
|
|
129
|
+
local repo_root="$1"
|
|
130
|
+
local branch_name="$2"
|
|
131
|
+
local specs_dir="$repo_root/specs"
|
|
132
|
+
|
|
133
|
+
# Extract numeric prefix from branch (e.g., "004" from "004-whatever")
|
|
134
|
+
if [[ ! "$branch_name" =~ ^([0-9]{3})- ]]; then
|
|
135
|
+
# If branch doesn't have numeric prefix, fall back to exact match
|
|
136
|
+
echo "$specs_dir/$branch_name"
|
|
137
|
+
return
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
local prefix="${BASH_REMATCH[1]}"
|
|
141
|
+
|
|
142
|
+
# Search for directories in specs/ that start with this prefix
|
|
143
|
+
local matches=()
|
|
144
|
+
if [[ -d "$specs_dir" ]]; then
|
|
145
|
+
for dir in "$specs_dir"/"$prefix"-*; do
|
|
146
|
+
if [[ -d "$dir" ]]; then
|
|
147
|
+
matches+=("$(basename "$dir")")
|
|
148
|
+
fi
|
|
149
|
+
done
|
|
150
|
+
fi
|
|
151
|
+
|
|
152
|
+
# Handle results
|
|
153
|
+
if [[ ${#matches[@]} -eq 0 ]]; then
|
|
154
|
+
# No match found - return the branch name path (will fail later with clear error)
|
|
155
|
+
echo "$specs_dir/$branch_name"
|
|
156
|
+
elif [[ ${#matches[@]} -eq 1 ]]; then
|
|
157
|
+
# Exactly one match - perfect!
|
|
158
|
+
echo "$specs_dir/${matches[0]}"
|
|
159
|
+
else
|
|
160
|
+
# Multiple matches - this shouldn't happen with proper naming convention
|
|
161
|
+
echo "ERROR: Multiple spec directories found with prefix '$prefix': ${matches[*]}" >&2
|
|
162
|
+
echo "Please ensure only one spec directory exists per numeric prefix." >&2
|
|
163
|
+
echo "$specs_dir/$branch_name" # Return something to avoid breaking the script
|
|
164
|
+
fi
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
get_feature_paths() {
|
|
168
|
+
local repo_root
|
|
169
|
+
repo_root="$(get_repo_root)"
|
|
170
|
+
local current_branch
|
|
171
|
+
current_branch="$(get_current_branch)"
|
|
172
|
+
|
|
173
|
+
# Use prefix-based lookup to support multiple branches per spec
|
|
174
|
+
local feature_dir=$(find_feature_dir_by_prefix "$repo_root" "$current_branch")
|
|
175
|
+
|
|
176
|
+
cat <<EOF
|
|
177
|
+
REPO_ROOT='$repo_root'
|
|
178
|
+
CURRENT_BRANCH='$current_branch'
|
|
179
|
+
FEATURE_DIR='$feature_dir'
|
|
180
|
+
FEATURE_SPEC='$feature_dir/spec.md'
|
|
181
|
+
IMPL_PLAN='$feature_dir/plan.md'
|
|
182
|
+
TASKS='$feature_dir/tasks.md'
|
|
183
|
+
RESEARCH='$feature_dir/research.md'
|
|
184
|
+
DATA_MODEL='$feature_dir/data-model.md'
|
|
185
|
+
QUICKSTART='$feature_dir/quickstart.md'
|
|
186
|
+
CONTRACTS_DIR='$feature_dir/contracts'
|
|
187
|
+
EOF
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
check_file() { [[ -f "$1" ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
|
191
|
+
check_dir() { [[ -d "$1" && -n $(ls -A "$1" 2>/dev/null) ]] && echo " ✓ $2" || echo " ✗ $2"; }
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -e
|
|
4
|
+
|
|
5
|
+
JSON_MODE=false
|
|
6
|
+
SHORT_NAME=""
|
|
7
|
+
BRANCH_NUMBER=""
|
|
8
|
+
ARGS=()
|
|
9
|
+
i=1
|
|
10
|
+
while [ $i -le $# ]; do
|
|
11
|
+
arg="${!i}"
|
|
12
|
+
case "$arg" in
|
|
13
|
+
--json)
|
|
14
|
+
JSON_MODE=true
|
|
15
|
+
;;
|
|
16
|
+
--short-name)
|
|
17
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
18
|
+
echo 'Error: --short-name requires a value' >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
i=$((i + 1))
|
|
22
|
+
next_arg="${!i}"
|
|
23
|
+
# Check if the next argument is another option (starts with --)
|
|
24
|
+
if [[ "$next_arg" == --* ]]; then
|
|
25
|
+
echo 'Error: --short-name requires a value' >&2
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
SHORT_NAME="$next_arg"
|
|
29
|
+
;;
|
|
30
|
+
--number)
|
|
31
|
+
if [ $((i + 1)) -gt $# ]; then
|
|
32
|
+
echo 'Error: --number requires a value' >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
i=$((i + 1))
|
|
36
|
+
next_arg="${!i}"
|
|
37
|
+
if [[ "$next_arg" == --* ]]; then
|
|
38
|
+
echo 'Error: --number requires a value' >&2
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
BRANCH_NUMBER="$next_arg"
|
|
42
|
+
;;
|
|
43
|
+
--help|-h)
|
|
44
|
+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>"
|
|
45
|
+
echo ""
|
|
46
|
+
echo "Options:"
|
|
47
|
+
echo " --json Output in JSON format"
|
|
48
|
+
echo " --short-name <name> Provide a custom short name (2-4 words) for the branch"
|
|
49
|
+
echo " --number N Specify branch number manually (overrides auto-detection)"
|
|
50
|
+
echo " --help, -h Show this help message"
|
|
51
|
+
echo ""
|
|
52
|
+
echo "Examples:"
|
|
53
|
+
echo " $0 'Add user authentication system' --short-name 'user-auth'"
|
|
54
|
+
echo " $0 'Implement OAuth2 integration for API' --number 5"
|
|
55
|
+
exit 0
|
|
56
|
+
;;
|
|
57
|
+
*)
|
|
58
|
+
ARGS+=("$arg")
|
|
59
|
+
;;
|
|
60
|
+
esac
|
|
61
|
+
i=$((i + 1))
|
|
62
|
+
done
|
|
63
|
+
|
|
64
|
+
FEATURE_DESCRIPTION="${ARGS[*]}"
|
|
65
|
+
if [ -z "$FEATURE_DESCRIPTION" ]; then
|
|
66
|
+
echo "Usage: $0 [--json] [--short-name <name>] [--number N] <feature_description>" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Function to find the repository root by searching for existing project markers
|
|
71
|
+
find_repo_root() {
|
|
72
|
+
local start_dir="$1"
|
|
73
|
+
local dir="$start_dir"
|
|
74
|
+
while [ "$dir" != "/" ]; do
|
|
75
|
+
if [ -d "$dir/.specify" ] || [ -d "$dir/specs" ]; then
|
|
76
|
+
echo "$dir"
|
|
77
|
+
return 0
|
|
78
|
+
fi
|
|
79
|
+
dir="$(dirname "$dir")"
|
|
80
|
+
done
|
|
81
|
+
|
|
82
|
+
# Fallback: if we're in a git repo, use the repo root.
|
|
83
|
+
if command -v git >/dev/null 2>&1 && git rev-parse --show-toplevel >/dev/null 2>&1; then
|
|
84
|
+
git rev-parse --show-toplevel
|
|
85
|
+
return 0
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Final fallback: use the provided directory so we never default to "/specs".
|
|
89
|
+
if [ -n "$start_dir" ] && [ "$start_dir" != "/" ]; then
|
|
90
|
+
echo "$start_dir"
|
|
91
|
+
return 0
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
return 1
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Function to get highest number from specs directory
|
|
98
|
+
get_highest_from_specs() {
|
|
99
|
+
local specs_dir="$1"
|
|
100
|
+
local highest=0
|
|
101
|
+
|
|
102
|
+
if [ -d "$specs_dir" ]; then
|
|
103
|
+
for dir in "$specs_dir"/*; do
|
|
104
|
+
[ -d "$dir" ] || continue
|
|
105
|
+
dirname=$(basename "$dir")
|
|
106
|
+
number=$(echo "$dirname" | grep -o '^[0-9]\+' || echo "0")
|
|
107
|
+
number=$((10#$number))
|
|
108
|
+
if [ "$number" -gt "$highest" ]; then
|
|
109
|
+
highest=$number
|
|
110
|
+
fi
|
|
111
|
+
done
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
echo "$highest"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Function to clean and format a branch name
|
|
118
|
+
clean_branch_name() {
|
|
119
|
+
local name="$1"
|
|
120
|
+
echo "$name" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/-\+/-/g' | sed 's/^-//' | sed 's/-$//'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# Resolve repository root by searching for the `.specify` marker.
|
|
124
|
+
SCRIPT_DIR="$(CDPATH="" cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
125
|
+
SKILL_DIR="$(CDPATH="" cd "$SCRIPT_DIR/../.." && pwd)"
|
|
126
|
+
|
|
127
|
+
REPO_ROOT="$(find_repo_root "${PWD:-$SCRIPT_DIR}")"
|
|
128
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
129
|
+
REPO_ROOT="$(find_repo_root "$SCRIPT_DIR")"
|
|
130
|
+
if [ -z "$REPO_ROOT" ]; then
|
|
131
|
+
echo "Error: Could not determine repository root. Please run this script from within the repository." >&2
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
fi
|
|
135
|
+
|
|
136
|
+
cd "$REPO_ROOT"
|
|
137
|
+
|
|
138
|
+
SPECS_DIR="$REPO_ROOT/specs"
|
|
139
|
+
mkdir -p "$SPECS_DIR"
|
|
140
|
+
|
|
141
|
+
# Function to generate branch name with stop word filtering and length filtering
|
|
142
|
+
generate_branch_name() {
|
|
143
|
+
local description="$1"
|
|
144
|
+
|
|
145
|
+
# Common stop words to filter out
|
|
146
|
+
local stop_words="^(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)$"
|
|
147
|
+
|
|
148
|
+
# Convert to lowercase and split into words
|
|
149
|
+
local clean_name=$(echo "$description" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/ /g')
|
|
150
|
+
|
|
151
|
+
# Filter words: remove stop words and words shorter than 3 chars (unless they're uppercase acronyms in original)
|
|
152
|
+
local meaningful_words=()
|
|
153
|
+
for word in $clean_name; do
|
|
154
|
+
# Skip empty words
|
|
155
|
+
[ -z "$word" ] && continue
|
|
156
|
+
|
|
157
|
+
# Keep words that are NOT stop words AND (length >= 3 OR are potential acronyms)
|
|
158
|
+
if ! echo "$word" | grep -qiE "$stop_words"; then
|
|
159
|
+
if [ ${#word} -ge 3 ]; then
|
|
160
|
+
meaningful_words+=("$word")
|
|
161
|
+
elif echo "$description" | grep -q "\b${word^^}\b"; then
|
|
162
|
+
# Keep short words if they appear as uppercase in original (likely acronyms)
|
|
163
|
+
meaningful_words+=("$word")
|
|
164
|
+
fi
|
|
165
|
+
fi
|
|
166
|
+
done
|
|
167
|
+
|
|
168
|
+
# If we have meaningful words, use first 3-4 of them
|
|
169
|
+
if [ ${#meaningful_words[@]} -gt 0 ]; then
|
|
170
|
+
local max_words=3
|
|
171
|
+
if [ ${#meaningful_words[@]} -eq 4 ]; then max_words=4; fi
|
|
172
|
+
|
|
173
|
+
local result=""
|
|
174
|
+
local count=0
|
|
175
|
+
for word in "${meaningful_words[@]}"; do
|
|
176
|
+
if [ $count -ge $max_words ]; then break; fi
|
|
177
|
+
if [ -n "$result" ]; then result="$result-"; fi
|
|
178
|
+
result="$result$word"
|
|
179
|
+
count=$((count + 1))
|
|
180
|
+
done
|
|
181
|
+
echo "$result"
|
|
182
|
+
else
|
|
183
|
+
# Fallback to original logic if no meaningful words found
|
|
184
|
+
local cleaned=$(clean_branch_name "$description")
|
|
185
|
+
echo "$cleaned" | tr '-' '\n' | grep -v '^$' | head -3 | tr '\n' '-' | sed 's/-$//'
|
|
186
|
+
fi
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Generate branch name
|
|
190
|
+
if [ -n "$SHORT_NAME" ]; then
|
|
191
|
+
# Use provided short name, just clean it up
|
|
192
|
+
BRANCH_SUFFIX=$(clean_branch_name "$SHORT_NAME")
|
|
193
|
+
else
|
|
194
|
+
# Generate from description with smart filtering
|
|
195
|
+
BRANCH_SUFFIX=$(generate_branch_name "$FEATURE_DESCRIPTION")
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
# Determine branch number
|
|
199
|
+
if [ -z "$BRANCH_NUMBER" ]; then
|
|
200
|
+
HIGHEST=$(get_highest_from_specs "$SPECS_DIR")
|
|
201
|
+
BRANCH_NUMBER=$((HIGHEST + 1))
|
|
202
|
+
fi
|
|
203
|
+
|
|
204
|
+
# Force base-10 interpretation to prevent octal conversion (e.g., 010 → 8 in octal, but should be 10 in decimal)
|
|
205
|
+
FEATURE_NUM=$(printf "%03d" "$((10#$BRANCH_NUMBER))")
|
|
206
|
+
|
|
207
|
+
# Guard: prevent duplicate numeric prefixes (especially when --number is used)
|
|
208
|
+
if compgen -G "$SPECS_DIR/${FEATURE_NUM}-*" >/dev/null; then
|
|
209
|
+
>&2 echo "[specify] Error: Spec id prefix '${FEATURE_NUM}' already exists under ${SPECS_DIR}"
|
|
210
|
+
>&2 echo "[specify] Existing:"
|
|
211
|
+
ls -1 "$SPECS_DIR/${FEATURE_NUM}-"* 2>/dev/null | sed 's|.*/|[specify] - |' >&2 || true
|
|
212
|
+
>&2 echo "[specify] Hint: omit --number to auto-allocate, or pick an unused number."
|
|
213
|
+
exit 1
|
|
214
|
+
fi
|
|
215
|
+
|
|
216
|
+
BRANCH_NAME="${FEATURE_NUM}-${BRANCH_SUFFIX}"
|
|
217
|
+
|
|
218
|
+
# Validate and truncate if necessary (keep feature ids at a reasonable length)
|
|
219
|
+
MAX_FEATURE_ID_LENGTH=244
|
|
220
|
+
if [ ${#BRANCH_NAME} -gt $MAX_FEATURE_ID_LENGTH ]; then
|
|
221
|
+
# Calculate how much we need to trim from suffix
|
|
222
|
+
# Account for: feature number (3) + hyphen (1) = 4 chars
|
|
223
|
+
MAX_SUFFIX_LENGTH=$((MAX_FEATURE_ID_LENGTH - 4))
|
|
224
|
+
|
|
225
|
+
# Truncate suffix at word boundary if possible
|
|
226
|
+
TRUNCATED_SUFFIX=$(echo "$BRANCH_SUFFIX" | cut -c1-$MAX_SUFFIX_LENGTH)
|
|
227
|
+
# Remove trailing hyphen if truncation created one
|
|
228
|
+
TRUNCATED_SUFFIX=$(echo "$TRUNCATED_SUFFIX" | sed 's/-$//')
|
|
229
|
+
|
|
230
|
+
ORIGINAL_BRANCH_NAME="$BRANCH_NAME"
|
|
231
|
+
BRANCH_NAME="${FEATURE_NUM}-${TRUNCATED_SUFFIX}"
|
|
232
|
+
|
|
233
|
+
>&2 echo "[specify] Warning: Feature id exceeded the configured length limit"
|
|
234
|
+
>&2 echo "[specify] Original: $ORIGINAL_BRANCH_NAME (${#ORIGINAL_BRANCH_NAME} bytes)"
|
|
235
|
+
>&2 echo "[specify] Truncated to: $BRANCH_NAME (${#BRANCH_NAME} bytes)"
|
|
236
|
+
fi
|
|
237
|
+
|
|
238
|
+
>&2 echo "[specify] Note: This workflow manages specs directories only (no VCS operations)."
|
|
239
|
+
>&2 echo "[specify] To let other scripts target this feature in your current shell, set:"
|
|
240
|
+
>&2 echo "[specify] export SPECIFY_FEATURE=\"$BRANCH_NAME\""
|
|
241
|
+
|
|
242
|
+
FEATURE_DIR="$SPECS_DIR/$BRANCH_NAME"
|
|
243
|
+
mkdir -p "$FEATURE_DIR"
|
|
244
|
+
|
|
245
|
+
TEMPLATE="$SKILL_DIR/assets/templates/spec-template.md"
|
|
246
|
+
SPEC_FILE="$FEATURE_DIR/spec.md"
|
|
247
|
+
if [ -f "$TEMPLATE" ]; then cp "$TEMPLATE" "$SPEC_FILE"; else touch "$SPEC_FILE"; fi
|
|
248
|
+
|
|
249
|
+
# Set the SPECIFY_FEATURE environment variable for the current session
|
|
250
|
+
export SPECIFY_FEATURE="$BRANCH_NAME"
|
|
251
|
+
|
|
252
|
+
if $JSON_MODE; then
|
|
253
|
+
printf '{"BRANCH_NAME":"%s","SPEC_FILE":"%s","FEATURE_NUM":"%s"}\n' "$BRANCH_NAME" "$SPEC_FILE" "$FEATURE_NUM"
|
|
254
|
+
else
|
|
255
|
+
echo "BRANCH_NAME: $BRANCH_NAME"
|
|
256
|
+
echo "SPEC_FILE: $SPEC_FILE"
|
|
257
|
+
echo "FEATURE_NUM: $FEATURE_NUM"
|
|
258
|
+
echo "SPECIFY_FEATURE environment variable set to: $BRANCH_NAME"
|
|
259
|
+
fi
|