@kafka0102/onespec 0.1.2
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/README.md +84 -0
- package/assets/skills/onespec/SKILL.md +58 -0
- package/assets/skills/onespec/scripts/onespec-closeout.sh +404 -0
- package/assets/skills/onespec/scripts/onespec-commit.sh +444 -0
- package/assets/skills/onespec/scripts/onespec-env.sh +15 -0
- package/assets/skills/onespec/scripts/onespec-handoff.sh +115 -0
- package/assets/skills/onespec/scripts/onespec-state.sh +341 -0
- package/assets/skills/onespec-archive/SKILL.md +202 -0
- package/assets/skills/onespec-design/SKILL.md +226 -0
- package/assets/skills/onespec-execute/SKILL.md +219 -0
- package/assets/skills-en/onespec/SKILL.md +57 -0
- package/assets/skills-en/onespec-archive/SKILL.md +199 -0
- package/assets/skills-en/onespec-design/SKILL.md +226 -0
- package/assets/skills-en/onespec-execute/SKILL.md +219 -0
- package/bin/onespec.js +7 -0
- package/package.json +38 -0
- package/scripts/postinstall.js +28 -0
- package/src/cli.js +244 -0
- package/src/doctor.js +172 -0
- package/src/init.js +136 -0
- package/src/platforms.js +23 -0
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# OneSpec
|
|
2
|
+
|
|
3
|
+
> 中文说明见 [README-zh.md](README-zh.md)
|
|
4
|
+
|
|
5
|
+
OneSpec is a Codex skill package for running an OpenSpec + Superpowers workflow.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Use one of these installation methods.
|
|
10
|
+
|
|
11
|
+
### Via `npm install -g`
|
|
12
|
+
|
|
13
|
+
Install the CLI globally:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install -g @kafka0102/onespec
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Then install the bundled skills into Codex:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
onespec init --scope global --yes
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or install into the current project only:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
onespec init . --scope project --yes
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Via `npx`
|
|
32
|
+
|
|
33
|
+
If you publish the package to npm:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx @kafka0102/onespec init
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Via Repository Source
|
|
40
|
+
|
|
41
|
+
Install the skill bundle directly from this repository:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npx skills add https://github.com/kafka0102/onespec/tree/main/assets/skills-en -a codex -y
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For the Chinese bundle:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx skills add https://github.com/kafka0102/onespec/tree/main/assets/skills -a codex -y
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `skills` CLI does not parse `owner/repo/tree/<ref>/...` shorthand correctly. Use the full GitHub URL above, or the shorthand form with an explicit ref such as `kafka0102/onespec/assets/skills#main`.
|
|
54
|
+
|
|
55
|
+
## Use
|
|
56
|
+
|
|
57
|
+
`npm install -g @kafka0102/onespec` only installs the `onespec` CLI.
|
|
58
|
+
`onespec init` is the step that copies the bundled skills into Codex.
|
|
59
|
+
|
|
60
|
+
After `onespec init`, restart Codex and invoke the OneSpec skills in your task.
|
|
61
|
+
During active work, OneSpec keeps runtime state in `openspec/changes/<change-id>/.onespec.yaml`; archive is the point where that file is cleaned up.
|
|
62
|
+
|
|
63
|
+
## Release
|
|
64
|
+
|
|
65
|
+
To publish from GitHub Actions:
|
|
66
|
+
|
|
67
|
+
1. Recommended: configure npm Trusted Publishing for `@kafka0102/onespec` and this GitHub repository.
|
|
68
|
+
2. Fallback: if you do not use Trusted Publishing, add repository secret `NPM_TOKEN` with an npm granular access token that has publish permission for `@kafka0102/onespec` and `bypass 2FA` enabled.
|
|
69
|
+
3. Update `package.json` version.
|
|
70
|
+
4. Create and push a matching tag such as `v0.1.2`.
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
git tag v0.1.2
|
|
74
|
+
git push origin main --tags
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
The workflow at `.github/workflows/publish.yml` runs on `v*` tags, checks that the tag matches `package.json`, runs `npm test`, previews the published package, and then publishes to npm.
|
|
78
|
+
|
|
79
|
+
It now supports both modes:
|
|
80
|
+
|
|
81
|
+
1. Trusted Publishing if no `NPM_TOKEN` secret is configured.
|
|
82
|
+
2. Token-based publishing if `NPM_TOKEN` is present.
|
|
83
|
+
|
|
84
|
+
If npm returns `E403` with a message about 2FA or granular access tokens, the token is not valid for publishing under npm's current policy. Replace it with a granular token that can bypass 2FA, or switch the package to Trusted Publishing.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: onespec
|
|
3
|
+
description: 当用户需要用 OpenSpec 与 Superpowers 管理完整 AI Coding 变更生命周期,或不确定应进入设计、执行还是归档阶段时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# OneSpec 工作流
|
|
7
|
+
|
|
8
|
+
OneSpec 是组合路由 Skill。它只负责恢复状态、判断阶段并切换到 `onespec-design`、`onespec-execute` 或 `onespec-archive`;具体阶段规则以对应子 Skill 为准。
|
|
9
|
+
|
|
10
|
+
开始时说明:
|
|
11
|
+
|
|
12
|
+
> 我正在使用 `onespec` 工作流。
|
|
13
|
+
|
|
14
|
+
## 恢复优先
|
|
15
|
+
|
|
16
|
+
每次进入先检查状态,不依赖聊天历史:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
ONESPEC_ENV="${ONESPEC_ENV:-$(find . "$HOME"/.codex "$HOME"/.agents "$HOME"/.config -path '*/onespec/scripts/onespec-env.sh' -type f -print -quit 2>/dev/null)}"
|
|
20
|
+
. "$ONESPEC_ENV"
|
|
21
|
+
"$ONESPEC_BASH" "$ONESPEC_STATE" list
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
如果发现相关 change,运行:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
"$ONESPEC_BASH" "$ONESPEC_STATE" recover <change-id>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
运行时状态文件是 `openspec/changes/<change-id>/.onespec.yaml`。handoff 摘要、哈希和 touched files 都写回这里;archive 前保留,archive 后删除。
|
|
31
|
+
`recover` 的输出是执行合同,不是参考信息。至少先读取 `phase`、`next_skill`、`next_gate` 与 `allowed_actions`,再决定是否继续。
|
|
32
|
+
|
|
33
|
+
## 阶段路由
|
|
34
|
+
|
|
35
|
+
先判断当前请求属于哪一类:
|
|
36
|
+
|
|
37
|
+
- `propose`:定义新 change、梳理范围、生成 `proposal.md`、`design.md`、`tasks.md` 与 spec delta。使用 `onespec-design`。
|
|
38
|
+
- `apply`:实现已批准 change、继续已有 change、生成或恢复 Superpowers plan、回填 OpenSpec 状态。使用 `onespec-execute`。
|
|
39
|
+
- `review-closeout`:用户评审、处理反馈、删除 worktree 或执行归档。使用 `onespec-archive`。
|
|
40
|
+
|
|
41
|
+
如果用户意图不清,只问一个简短问题,不要同时问多个。
|
|
42
|
+
|
|
43
|
+
默认意图映射:
|
|
44
|
+
|
|
45
|
+
- 用户说“新需求”、“设计一下”、“写 proposal / spec”、“定义 change”时,进入 `onespec-design`。
|
|
46
|
+
- 用户说“开始实现”、“执行这个 change”、“apply 这个 proposal / change”、“继续做这个 change”、“开始 coding / 开发”、“make plan”时,进入 `onespec-execute`。如果 proposal 尚未批准,`onespec-execute` 必须停止并转回 `onespec-design` 的批准 gate。
|
|
47
|
+
- 用户说“review”、“收尾”、“归档”、“archive”、“删除 worktree”时,进入 `onespec-archive`。
|
|
48
|
+
|
|
49
|
+
## 共同约束
|
|
50
|
+
|
|
51
|
+
- OpenSpec 负责“要做什么”、formal artifacts、approval gate、spec delta 与归档语义。
|
|
52
|
+
- Superpowers 负责高歧义需求澄清、实现计划、TDD、分任务 review 与工程执行质量。
|
|
53
|
+
- 不要询问变更名称。根据任务自动生成简短短横线命名的 `change-id`,如冲突则追加数字。
|
|
54
|
+
- 读取最少必要上下文:`openspec/config.yaml`、`openspec/project.md`、相关 `openspec/specs/**`、项目入口文档、当前分支和工作区状态。
|
|
55
|
+
- 只问会改变 proposal、执行路径、分支处理或归档结果的问题。
|
|
56
|
+
- 当阶段规则冲突时,以当前阶段子 Skill 的停止条件为准。
|
|
57
|
+
- 每个阶段子 Skill 定义了强制暂停 gate(如 `onespec-execute` 的"实现完成 Gate"、`onespec-design` 的"批准 Gate")。路由进入下一阶段前,必须确认上一阶段的 gate 已完成。如果 gate 未完成就试图进入下一阶段,必须拒绝并指出缺失步骤。
|
|
58
|
+
- 如果 `recover` 已经给出 `next_skill`,默认先按它恢复;只有在用户当前请求明确改变阶段,且上一阶段的 gate 已完成时,才允许覆盖该恢复结果。
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
die() {
|
|
5
|
+
echo "ERROR: $*" >&2
|
|
6
|
+
exit 1
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
valid_change() {
|
|
10
|
+
local change="$1"
|
|
11
|
+
[[ -n "$change" ]] || die "change name is required"
|
|
12
|
+
[[ "$change" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || die "invalid change name: $change"
|
|
13
|
+
[[ "$change" != *".."* ]] || die "change name must not contain '..'"
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
change_dir() {
|
|
17
|
+
local change="$1"
|
|
18
|
+
if [ -d "openspec/changes/$change" ]; then
|
|
19
|
+
printf 'openspec/changes/%s\n' "$change"
|
|
20
|
+
elif [ -d "openspec/changes/archive/$change" ]; then
|
|
21
|
+
printf 'openspec/changes/archive/%s\n' "$change"
|
|
22
|
+
else
|
|
23
|
+
printf 'openspec/changes/%s\n' "$change"
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
state_file() {
|
|
28
|
+
local change="$1"
|
|
29
|
+
printf '%s/.onespec.yaml\n' "$(change_dir "$change")"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
field_value() {
|
|
33
|
+
local file="$1"
|
|
34
|
+
local key="$2"
|
|
35
|
+
awk -F ': *' -v key="$key" '$1 == key { sub(/^[^:]+: */, ""); print; found=1; exit } END { if (!found) exit 0 }' "$file" 2>/dev/null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get_state_value() {
|
|
39
|
+
local change="$1"
|
|
40
|
+
local key="$2"
|
|
41
|
+
local file
|
|
42
|
+
file="$(state_file "$change")"
|
|
43
|
+
[ -f "$file" ] || die "state not found: $file"
|
|
44
|
+
field_value "$file" "$key"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ensure_git_repo() {
|
|
48
|
+
git rev-parse --show-toplevel >/dev/null 2>&1 || die "current directory is not inside a git repository"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
git_common_dir() {
|
|
52
|
+
git rev-parse --git-common-dir
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
current_branch() {
|
|
56
|
+
local branch
|
|
57
|
+
branch="$(git branch --show-current 2>/dev/null || true)"
|
|
58
|
+
printf '%s\n' "${branch:-detached}"
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
current_workspace_path() {
|
|
62
|
+
pwd -P
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
canonicalize_path() {
|
|
66
|
+
local input="$1"
|
|
67
|
+
if [ -z "$input" ] || [ "$input" = "unknown" ] || [ ! -d "$input" ]; then
|
|
68
|
+
printf '%s\n' "$input"
|
|
69
|
+
return 0
|
|
70
|
+
fi
|
|
71
|
+
(
|
|
72
|
+
cd "$input"
|
|
73
|
+
pwd -P
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
selected_actions_csv() {
|
|
78
|
+
local joined=""
|
|
79
|
+
local item
|
|
80
|
+
for item in "$@"; do
|
|
81
|
+
if [ -n "$joined" ]; then
|
|
82
|
+
joined="${joined},${item}"
|
|
83
|
+
else
|
|
84
|
+
joined="$item"
|
|
85
|
+
fi
|
|
86
|
+
done
|
|
87
|
+
printf '%s\n' "$joined"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
state_destination_in_origin() {
|
|
91
|
+
local change="$1"
|
|
92
|
+
local origin_path
|
|
93
|
+
origin_path="$(canonicalize_path "$(get_state_value "$change" origin_workspace_path)")"
|
|
94
|
+
[ -n "$origin_path" ] || die "origin workspace path is empty"
|
|
95
|
+
[ "$origin_path" != "unknown" ] || die "origin workspace path is unknown"
|
|
96
|
+
printf '%s/openspec/changes/%s/.onespec.yaml\n' "$origin_path" "$change"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
normalize_action() {
|
|
100
|
+
case "$1" in
|
|
101
|
+
delete-worktree|drop-worktree|cleanup-worktree)
|
|
102
|
+
echo "delete-worktree"
|
|
103
|
+
;;
|
|
104
|
+
archive|run-archive)
|
|
105
|
+
echo "archive"
|
|
106
|
+
;;
|
|
107
|
+
"")
|
|
108
|
+
echo ""
|
|
109
|
+
;;
|
|
110
|
+
*)
|
|
111
|
+
die "unsupported closeout action: $1"
|
|
112
|
+
;;
|
|
113
|
+
esac
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
temporary_worktree_status() {
|
|
117
|
+
local change="$1"
|
|
118
|
+
local origin_branch origin_path origin_mode current_path current_head temporary reason
|
|
119
|
+
|
|
120
|
+
origin_branch="$(get_state_value "$change" origin_branch)"
|
|
121
|
+
origin_path="$(get_state_value "$change" origin_workspace_path)"
|
|
122
|
+
origin_mode="$(get_state_value "$change" origin_workspace_mode)"
|
|
123
|
+
origin_path="$(canonicalize_path "$origin_path")"
|
|
124
|
+
current_path="$(current_workspace_path)"
|
|
125
|
+
current_head="$(current_branch)"
|
|
126
|
+
temporary="false"
|
|
127
|
+
reason="none"
|
|
128
|
+
|
|
129
|
+
if [ "$origin_mode" = "worktree" ]; then
|
|
130
|
+
temporary="true"
|
|
131
|
+
reason="origin-workspace-mode"
|
|
132
|
+
fi
|
|
133
|
+
if [ -n "$origin_path" ] && [ "$origin_path" != "unknown" ] && [ "$origin_path" != "$current_path" ]; then
|
|
134
|
+
temporary="true"
|
|
135
|
+
reason="workspace-path-differs"
|
|
136
|
+
fi
|
|
137
|
+
if [ -n "$origin_branch" ] && [ "$origin_branch" != "unknown" ] && [ "$origin_branch" != "$current_head" ]; then
|
|
138
|
+
temporary="true"
|
|
139
|
+
if [ "$reason" = "none" ]; then
|
|
140
|
+
reason="branch-differs"
|
|
141
|
+
fi
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
cat <<EOF
|
|
145
|
+
current_branch: $current_head
|
|
146
|
+
current_workspace_path: $current_path
|
|
147
|
+
origin_branch: $origin_branch
|
|
148
|
+
origin_workspace_path: $origin_path
|
|
149
|
+
origin_workspace_mode: $origin_mode
|
|
150
|
+
temporary_worktree: $temporary
|
|
151
|
+
temporary_worktree_reason: $reason
|
|
152
|
+
EOF
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
recommended_combination() {
|
|
156
|
+
local change="$1"
|
|
157
|
+
local worktree_probe temporary recommendation reason
|
|
158
|
+
|
|
159
|
+
worktree_probe="$(temporary_worktree_status "$change")"
|
|
160
|
+
temporary="$(printf '%s\n' "$worktree_probe" | awk -F ': ' '$1 == "temporary_worktree" { print $2 }')"
|
|
161
|
+
|
|
162
|
+
recommendation="none"
|
|
163
|
+
reason="review-only"
|
|
164
|
+
|
|
165
|
+
if [ "$temporary" = "true" ]; then
|
|
166
|
+
recommendation="delete-worktree,archive"
|
|
167
|
+
reason="temporary-worktree-can-be-cleaned-before-or-with-archive"
|
|
168
|
+
else
|
|
169
|
+
recommendation="archive"
|
|
170
|
+
reason="already-on-target-path"
|
|
171
|
+
fi
|
|
172
|
+
|
|
173
|
+
cat <<EOF
|
|
174
|
+
recommended_actions: $recommendation
|
|
175
|
+
recommended_reason: $reason
|
|
176
|
+
EOF
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
cmd_inspect() {
|
|
180
|
+
local change="$1"
|
|
181
|
+
valid_change "$change"
|
|
182
|
+
ensure_git_repo
|
|
183
|
+
|
|
184
|
+
temporary_worktree_status "$change"
|
|
185
|
+
cat <<'EOF'
|
|
186
|
+
cleanup_local_branch_after_merge: true
|
|
187
|
+
cleanup_local_worktree_after_merge: true
|
|
188
|
+
cleanup_remote_branch_after_merge: false
|
|
189
|
+
cleanup_local_branch_after_preserve: false
|
|
190
|
+
cleanup_local_worktree_after_preserve: false
|
|
191
|
+
EOF
|
|
192
|
+
recommended_combination "$change"
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
cmd_recommend_actions() {
|
|
196
|
+
local change="$1"
|
|
197
|
+
valid_change "$change"
|
|
198
|
+
ensure_git_repo
|
|
199
|
+
cmd_inspect "$change"
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
cmd_validate_actions() {
|
|
203
|
+
local change="$1"
|
|
204
|
+
shift
|
|
205
|
+
valid_change "$change"
|
|
206
|
+
ensure_git_repo
|
|
207
|
+
|
|
208
|
+
local -a selected=()
|
|
209
|
+
local action normalized
|
|
210
|
+
local already_selected
|
|
211
|
+
local has_delete_worktree="false"
|
|
212
|
+
local has_archive="false"
|
|
213
|
+
local current_head origin_branch temporary valid message
|
|
214
|
+
|
|
215
|
+
for action in "$@"; do
|
|
216
|
+
normalized="$(normalize_action "$action")"
|
|
217
|
+
[ -n "$normalized" ] || continue
|
|
218
|
+
already_selected="false"
|
|
219
|
+
for action in "${selected[@]:-}"; do
|
|
220
|
+
if [ "$action" = "$normalized" ]; then
|
|
221
|
+
already_selected="true"
|
|
222
|
+
break
|
|
223
|
+
fi
|
|
224
|
+
done
|
|
225
|
+
if [ "$already_selected" != "true" ]; then
|
|
226
|
+
selected+=("$normalized")
|
|
227
|
+
fi
|
|
228
|
+
done
|
|
229
|
+
|
|
230
|
+
for action in "${selected[@]}"; do
|
|
231
|
+
case "$action" in
|
|
232
|
+
delete-worktree) has_delete_worktree="true" ;;
|
|
233
|
+
archive) has_archive="true" ;;
|
|
234
|
+
esac
|
|
235
|
+
done
|
|
236
|
+
|
|
237
|
+
valid="true"
|
|
238
|
+
message="可以执行所选收尾动作。"
|
|
239
|
+
current_head="$(current_branch)"
|
|
240
|
+
origin_branch="$(get_state_value "$change" origin_branch)"
|
|
241
|
+
temporary="$(temporary_worktree_status "$change" | awk -F ': ' '$1 == "temporary_worktree" { print $2 }')"
|
|
242
|
+
|
|
243
|
+
if [ "$has_delete_worktree" = "true" ] && [ "$temporary" != "true" ]; then
|
|
244
|
+
valid="false"
|
|
245
|
+
message="不能删除 worktree:当前不在临时 worktree。"
|
|
246
|
+
elif [ "$has_archive" = "true" ] && [ "$has_delete_worktree" = "true" ]; then
|
|
247
|
+
message="允许先删除临时 worktree,再继续归档。"
|
|
248
|
+
elif [ "$has_delete_worktree" = "true" ] && [ "$has_archive" != "true" ]; then
|
|
249
|
+
message="允许仅删除临时 worktree;之后仍可单独执行归档。"
|
|
250
|
+
elif [ "$has_archive" = "true" ] && [ "$has_delete_worktree" != "true" ]; then
|
|
251
|
+
if [ "$temporary" = "true" ] || { [ "$origin_branch" != "unknown" ] && [ "$current_head" != "$origin_branch" ]; }; then
|
|
252
|
+
valid="false"
|
|
253
|
+
message="不能单独执行归档:当前代码尚未确认位于目标分支。"
|
|
254
|
+
else
|
|
255
|
+
message="允许单独执行归档:当前已在目标分支路径上。"
|
|
256
|
+
fi
|
|
257
|
+
elif [ "${#selected[@]}" -eq 0 ]; then
|
|
258
|
+
message="本次不删除 worktree 或归档;之后仍可再次进入收尾。"
|
|
259
|
+
fi
|
|
260
|
+
|
|
261
|
+
cat <<EOF
|
|
262
|
+
selected_actions: $(selected_actions_csv "${selected[@]}")
|
|
263
|
+
valid: $valid
|
|
264
|
+
message: $message
|
|
265
|
+
temporary_worktree: $temporary
|
|
266
|
+
current_branch: $current_head
|
|
267
|
+
origin_branch: $origin_branch
|
|
268
|
+
EOF
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
run_archive_command() {
|
|
272
|
+
local change="$1"
|
|
273
|
+
local archive_bin="${ONESPEC_ARCHIVE_BIN:-openspec}"
|
|
274
|
+
"$archive_bin" archive "$change" --yes
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
preserve_runtime_state_in_origin() {
|
|
278
|
+
local change="$1"
|
|
279
|
+
local source_file destination_file destination_dir
|
|
280
|
+
source_file="$(state_file "$change")"
|
|
281
|
+
destination_file="$(state_destination_in_origin "$change")"
|
|
282
|
+
destination_dir="$(dirname "$destination_file")"
|
|
283
|
+
|
|
284
|
+
mkdir -p "$destination_dir"
|
|
285
|
+
cp "$source_file" "$destination_file"
|
|
286
|
+
printf '%s\n' "$destination_file"
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
delete_current_worktree() {
|
|
290
|
+
local current_path common_dir
|
|
291
|
+
current_path="$(current_workspace_path)"
|
|
292
|
+
common_dir="$(git_common_dir)"
|
|
293
|
+
git --git-dir="$common_dir" worktree remove --force "$current_path"
|
|
294
|
+
printf '%s\n' "$current_path"
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
cmd_run_actions() {
|
|
298
|
+
local change="$1"
|
|
299
|
+
shift
|
|
300
|
+
valid_change "$change"
|
|
301
|
+
ensure_git_repo
|
|
302
|
+
|
|
303
|
+
local validation selected valid archive_selected delete_selected preserved_state removed_worktree
|
|
304
|
+
validation="$(cmd_validate_actions "$change" "$@")"
|
|
305
|
+
selected="$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "selected_actions" { print $2 }')"
|
|
306
|
+
valid="$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "valid" { print $2 }')"
|
|
307
|
+
|
|
308
|
+
[ "$valid" = "true" ] || die "$(printf '%s\n' "$validation" | awk -F ': ' '$1 == "message" { print $2 }')"
|
|
309
|
+
[ -n "$selected" ] || die "run-actions requires at least one closeout action"
|
|
310
|
+
|
|
311
|
+
archive_selected="false"
|
|
312
|
+
delete_selected="false"
|
|
313
|
+
if printf '%s\n' "$selected" | grep -Eq '(^|,)archive($|,)'; then
|
|
314
|
+
archive_selected="true"
|
|
315
|
+
fi
|
|
316
|
+
if printf '%s\n' "$selected" | grep -Eq '(^|,)delete-worktree($|,)'; then
|
|
317
|
+
delete_selected="true"
|
|
318
|
+
fi
|
|
319
|
+
|
|
320
|
+
preserved_state=""
|
|
321
|
+
removed_worktree=""
|
|
322
|
+
|
|
323
|
+
# Safe order: archive first, then delete the temporary worktree.
|
|
324
|
+
if [ "$archive_selected" = "true" ]; then
|
|
325
|
+
run_archive_command "$change"
|
|
326
|
+
local script_dir
|
|
327
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
|
|
328
|
+
"${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" phase archived
|
|
329
|
+
"${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" archive archived
|
|
330
|
+
"${BASH:-bash}" "$script_dir/onespec-closeout.sh" cleanup-runtime "$change" >/dev/null
|
|
331
|
+
fi
|
|
332
|
+
|
|
333
|
+
if [ "$delete_selected" = "true" ]; then
|
|
334
|
+
if [ "$archive_selected" != "true" ]; then
|
|
335
|
+
local script_dir
|
|
336
|
+
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd -P)"
|
|
337
|
+
"${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" phase done
|
|
338
|
+
"${BASH:-bash}" "$script_dir/onespec-state.sh" set "$change" archive skipped
|
|
339
|
+
preserved_state="$(preserve_runtime_state_in_origin "$change")"
|
|
340
|
+
fi
|
|
341
|
+
removed_worktree="$(delete_current_worktree)"
|
|
342
|
+
fi
|
|
343
|
+
|
|
344
|
+
cat <<EOF
|
|
345
|
+
selected_actions: $selected
|
|
346
|
+
archive_executed: $archive_selected
|
|
347
|
+
worktree_deleted: $delete_selected
|
|
348
|
+
preserved_state_file: ${preserved_state:-none}
|
|
349
|
+
deleted_worktree_path: ${removed_worktree:-none}
|
|
350
|
+
EOF
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
cmd_cleanup_runtime() {
|
|
354
|
+
local change="$1"
|
|
355
|
+
valid_change "$change"
|
|
356
|
+
|
|
357
|
+
local file
|
|
358
|
+
file="$(state_file "$change")"
|
|
359
|
+
if [ -f "$file" ]; then
|
|
360
|
+
rm -f "$file"
|
|
361
|
+
echo "$file"
|
|
362
|
+
fi
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
usage() {
|
|
366
|
+
cat <<'EOF'
|
|
367
|
+
用法:
|
|
368
|
+
onespec-closeout.sh inspect <change>
|
|
369
|
+
onespec-closeout.sh recommend-actions <change>
|
|
370
|
+
onespec-closeout.sh validate-actions <change> [delete-worktree] [archive]
|
|
371
|
+
onespec-closeout.sh run-actions <change> [delete-worktree] [archive]
|
|
372
|
+
onespec-closeout.sh cleanup-runtime <change>
|
|
373
|
+
EOF
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
cmd="${1:-}"
|
|
377
|
+
case "$cmd" in
|
|
378
|
+
inspect)
|
|
379
|
+
[ "$#" -eq 2 ] || { usage; exit 2; }
|
|
380
|
+
cmd_inspect "$2"
|
|
381
|
+
;;
|
|
382
|
+
recommend-actions)
|
|
383
|
+
[ "$#" -eq 2 ] || { usage; exit 2; }
|
|
384
|
+
cmd_recommend_actions "$2"
|
|
385
|
+
;;
|
|
386
|
+
validate-actions)
|
|
387
|
+
[ "$#" -ge 2 ] || { usage; exit 2; }
|
|
388
|
+
shift
|
|
389
|
+
cmd_validate_actions "$@"
|
|
390
|
+
;;
|
|
391
|
+
run-actions)
|
|
392
|
+
[ "$#" -ge 3 ] || { usage; exit 2; }
|
|
393
|
+
shift
|
|
394
|
+
cmd_run_actions "$@"
|
|
395
|
+
;;
|
|
396
|
+
cleanup-runtime)
|
|
397
|
+
[ "$#" -eq 2 ] || { usage; exit 2; }
|
|
398
|
+
cmd_cleanup_runtime "$2"
|
|
399
|
+
;;
|
|
400
|
+
*)
|
|
401
|
+
usage
|
|
402
|
+
exit 2
|
|
403
|
+
;;
|
|
404
|
+
esac
|