flower-trellis 0.6.0-beta.3 → 0.6.0-beta.5
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/enhancements/0.6/.agents/skills/trellis-flower-update/SKILL.md +38 -0
- package/enhancements/0.6/.agents/skills/trellis-worktree/SKILL.md +56 -0
- package/enhancements/0.6/.claude/skills/trellis-flower-update/SKILL.md +38 -0
- package/enhancements/0.6/.claude/skills/trellis-worktree/SKILL.md +56 -0
- package/enhancements/0.6/overrides/conflicts.json +3 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/patch.json +19 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/worktree-root-content.py +70 -0
- package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/worktree-root-selector.py +12 -0
- package/enhancements/0.6/scripts/untracked_flow.py +78 -3
- package/enhancements/0.6/scripts/worktree_setup.py +422 -0
- package/enhancements/MANIFEST.json +12 -5
- package/enhancements/common/.common/.claude/skills/aliyun-sls-query/SKILL.md +2 -0
- package/enhancements/common/.common/.claude/skills/humanize-writing/SKILL.md +140 -16
- package/enhancements/common/.common/.codex/skills/aliyun-sls-query/SKILL.md +2 -0
- package/enhancements/common/.common/.codex/skills/humanize-writing/SKILL.md +140 -16
- package/package.json +3 -3
- package/src/cli.js +3 -2
- package/src/commands/self-check.js +4 -1
- package/src/commands/self-update.js +1 -0
- package/src/lib/skill-catalog.js +2 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-flower-update
|
|
3
|
+
description: "手动检查和执行已安装 Flower/Trellis 强化包升级。用于用户明确要求更新或升级 flower-trellis、Flower、Trellis 强化包、skill-garden 快照、项目 flower 版本追平,或自动更新提示被稍后/跳过后仍想在对话里升级时。不要用于用户说想发版、发布版本、release、打 tag、npm publish 或修改 package 版本号;这些属于项目发版流程。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Trellis Flower Update
|
|
7
|
+
|
|
8
|
+
用于用户主动要求升级已安装的 Flower/Trellis 强化层时。自动 SessionStart 提示的 snooze、skip 和 cooldown 只是不主动打扰,不能阻止用户显式要求升级。
|
|
9
|
+
|
|
10
|
+
本 skill 不是发版入口。用户说“我想发版了”、release、打 tag、npm publish、更新 package 版本号或准备发布包时,不使用本 skill;按当前项目的 release SOP、`trellis-release` 或发布规范处理。
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. 确认目标项目。用户给出路径时使用该路径,否则使用当前工作目录。
|
|
15
|
+
2. 运行人工检查,默认强制刷新远端版本证据:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
flower-trellis self-check --json --manual --force-remote --target <target>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
3. 解析 JSON:
|
|
22
|
+
- `update_available`:展示当前版本、推荐版本、release notes 摘要和 `commands.recommended`。
|
|
23
|
+
- `project_out_of_sync`:展示当前 Flower/Trellis 与项目记录的差异,并展示 `commands.recommended`。
|
|
24
|
+
- `up_to_date`:说明当前安装和项目记录已一致。
|
|
25
|
+
- `disabled` / `offline` / `skipped`:说明原因;不要靠重置缓存伪造可执行状态。
|
|
26
|
+
4. 写入前遵守确认和安全门槛:
|
|
27
|
+
- 用户当前消息已经明确要求执行升级时,可以执行 `commands.recommended`。
|
|
28
|
+
- 用户只是询问、查看或比较版本时,只展示结果并等待确认。
|
|
29
|
+
- `safety.reasons` 非空时,先说明风险,再等待用户明确确认。
|
|
30
|
+
5. 执行推荐命令后读取 `<flower-update-result>`。如果结果要求 `run_trellis_push_confirmation`,进入 `trellis-push`,展示文件和 commit message 后等待确认。
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- 不直接读写 `.flower/update-check.tmp`。
|
|
35
|
+
- 不使用 `update-check reset`、`snooze` 或 `skip` 作为升级绕过手段。
|
|
36
|
+
- 不运行 `npm run release`、不打 tag、不 publish,也不修改 `package.json` 版本号。
|
|
37
|
+
- 不把 `self-check --manual` 用在 SessionStart 自动 hook;自动路径必须继续尊重提示节流。
|
|
38
|
+
- 目标项目有脏文件、活动任务或命令缺失时,按 `safety.reasons` 报告并等待用户确认。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-worktree
|
|
3
|
+
description: "Prepare and diagnose Trellis usage inside linked Git worktrees. Use when the user mentions worktree, linked worktree, worktree development, missing .trellis in a worktree, or asks to make .claude/.codex/.agents/Trellis files work from a worktree."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Trellis Worktree
|
|
7
|
+
|
|
8
|
+
Use this skill before normal Trellis routing when the user's current request is about using Trellis from a linked Git worktree.
|
|
9
|
+
|
|
10
|
+
The problem is usually not task or untracked routing itself: linked worktrees often do not contain the untracked `.trellis`, `.agents`, `.codex`, or `.claude` directories that AI tools need before any Trellis hook or skill can run.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Identify the target worktree. Use the user's explicit path if present; otherwise use the current working directory.
|
|
15
|
+
2. Locate a source worktree in the same Git worktree set that contains `.trellis/scripts/worktree_setup.py`.
|
|
16
|
+
3. Run read-only diagnosis first:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py status --target <target-worktree> --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
4. If status is `needs-prepare`, run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py prepare --target <target-worktree> --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
5. If status is `blocked` or `error`, stop and report the reason and conflict paths. Do not overwrite or delete user files.
|
|
29
|
+
6. After `ready` or `prepared`, return to the user's original Trellis intent and follow the normal workflow state.
|
|
30
|
+
|
|
31
|
+
## Source Worktree Discovery
|
|
32
|
+
|
|
33
|
+
If `<source-worktree>` is not obvious, inspect Git worktrees:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git -C <target-worktree> worktree list --porcelain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Choose a listed worktree that contains `.trellis/scripts/worktree_setup.py`. If none exists, report that this project has not been updated with `trellis-worktree` support yet.
|
|
40
|
+
|
|
41
|
+
## Safety Rules
|
|
42
|
+
|
|
43
|
+
- The helper only projects source paths that already exist.
|
|
44
|
+
- MVP projection paths are `.trellis`, `.agents`, `.codex`, and `.claude`.
|
|
45
|
+
- Projection uses symlinks and records `<target-worktree>/.trellis-worktree.json`.
|
|
46
|
+
- Existing non-managed `.codex`, `.claude`, `.agents`, or `.trellis` paths are conflicts.
|
|
47
|
+
- Do not hand-copy platform directories. Copying creates drift between worktrees.
|
|
48
|
+
- Do not treat successful setup as task approval, check approval, or push approval.
|
|
49
|
+
|
|
50
|
+
## Expected Results
|
|
51
|
+
|
|
52
|
+
- `ready`: target worktree already has valid Trellis entry points.
|
|
53
|
+
- `needs-prepare`: target worktree is safe to prepare; run `prepare`.
|
|
54
|
+
- `prepared`: symlinks and manifest were created or repaired.
|
|
55
|
+
- `blocked`: user-owned paths would be overwritten; stop and report `conflicts`.
|
|
56
|
+
- `error`: Git or source-root resolution failed; stop and report `reason`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-flower-update
|
|
3
|
+
description: "手动检查和执行已安装 Flower/Trellis 强化包升级。用于用户明确要求更新或升级 flower-trellis、Flower、Trellis 强化包、skill-garden 快照、项目 flower 版本追平,或自动更新提示被稍后/跳过后仍想在对话里升级时。不要用于用户说想发版、发布版本、release、打 tag、npm publish 或修改 package 版本号;这些属于项目发版流程。"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Trellis Flower Update
|
|
7
|
+
|
|
8
|
+
用于用户主动要求升级已安装的 Flower/Trellis 强化层时。自动 SessionStart 提示的 snooze、skip 和 cooldown 只是不主动打扰,不能阻止用户显式要求升级。
|
|
9
|
+
|
|
10
|
+
本 skill 不是发版入口。用户说“我想发版了”、release、打 tag、npm publish、更新 package 版本号或准备发布包时,不使用本 skill;按当前项目的 release SOP、`trellis-release` 或发布规范处理。
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. 确认目标项目。用户给出路径时使用该路径,否则使用当前工作目录。
|
|
15
|
+
2. 运行人工检查,默认强制刷新远端版本证据:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
flower-trellis self-check --json --manual --force-remote --target <target>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
3. 解析 JSON:
|
|
22
|
+
- `update_available`:展示当前版本、推荐版本、release notes 摘要和 `commands.recommended`。
|
|
23
|
+
- `project_out_of_sync`:展示当前 Flower/Trellis 与项目记录的差异,并展示 `commands.recommended`。
|
|
24
|
+
- `up_to_date`:说明当前安装和项目记录已一致。
|
|
25
|
+
- `disabled` / `offline` / `skipped`:说明原因;不要靠重置缓存伪造可执行状态。
|
|
26
|
+
4. 写入前遵守确认和安全门槛:
|
|
27
|
+
- 用户当前消息已经明确要求执行升级时,可以执行 `commands.recommended`。
|
|
28
|
+
- 用户只是询问、查看或比较版本时,只展示结果并等待确认。
|
|
29
|
+
- `safety.reasons` 非空时,先说明风险,再等待用户明确确认。
|
|
30
|
+
5. 执行推荐命令后读取 `<flower-update-result>`。如果结果要求 `run_trellis_push_confirmation`,进入 `trellis-push`,展示文件和 commit message 后等待确认。
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- 不直接读写 `.flower/update-check.tmp`。
|
|
35
|
+
- 不使用 `update-check reset`、`snooze` 或 `skip` 作为升级绕过手段。
|
|
36
|
+
- 不运行 `npm run release`、不打 tag、不 publish,也不修改 `package.json` 版本号。
|
|
37
|
+
- 不把 `self-check --manual` 用在 SessionStart 自动 hook;自动路径必须继续尊重提示节流。
|
|
38
|
+
- 目标项目有脏文件、活动任务或命令缺失时,按 `safety.reasons` 报告并等待用户确认。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: trellis-worktree
|
|
3
|
+
description: "Prepare and diagnose Trellis usage inside linked Git worktrees. Use when the user mentions worktree, linked worktree, worktree development, missing .trellis in a worktree, or asks to make .claude/.codex/.agents/Trellis files work from a worktree."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Trellis Worktree
|
|
7
|
+
|
|
8
|
+
Use this skill before normal Trellis routing when the user's current request is about using Trellis from a linked Git worktree.
|
|
9
|
+
|
|
10
|
+
The problem is usually not task or untracked routing itself: linked worktrees often do not contain the untracked `.trellis`, `.agents`, `.codex`, or `.claude` directories that AI tools need before any Trellis hook or skill can run.
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
1. Identify the target worktree. Use the user's explicit path if present; otherwise use the current working directory.
|
|
15
|
+
2. Locate a source worktree in the same Git worktree set that contains `.trellis/scripts/worktree_setup.py`.
|
|
16
|
+
3. Run read-only diagnosis first:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py status --target <target-worktree> --json
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
4. If status is `needs-prepare`, run:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
python3 <source-worktree>/.trellis/scripts/worktree_setup.py prepare --target <target-worktree> --json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
5. If status is `blocked` or `error`, stop and report the reason and conflict paths. Do not overwrite or delete user files.
|
|
29
|
+
6. After `ready` or `prepared`, return to the user's original Trellis intent and follow the normal workflow state.
|
|
30
|
+
|
|
31
|
+
## Source Worktree Discovery
|
|
32
|
+
|
|
33
|
+
If `<source-worktree>` is not obvious, inspect Git worktrees:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
git -C <target-worktree> worktree list --porcelain
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Choose a listed worktree that contains `.trellis/scripts/worktree_setup.py`. If none exists, report that this project has not been updated with `trellis-worktree` support yet.
|
|
40
|
+
|
|
41
|
+
## Safety Rules
|
|
42
|
+
|
|
43
|
+
- The helper only projects source paths that already exist.
|
|
44
|
+
- MVP projection paths are `.trellis`, `.agents`, `.codex`, and `.claude`.
|
|
45
|
+
- Projection uses symlinks and records `<target-worktree>/.trellis-worktree.json`.
|
|
46
|
+
- Existing non-managed `.codex`, `.claude`, `.agents`, or `.trellis` paths are conflicts.
|
|
47
|
+
- Do not hand-copy platform directories. Copying creates drift between worktrees.
|
|
48
|
+
- Do not treat successful setup as task approval, check approval, or push approval.
|
|
49
|
+
|
|
50
|
+
## Expected Results
|
|
51
|
+
|
|
52
|
+
- `ready`: target worktree already has valid Trellis entry points.
|
|
53
|
+
- `needs-prepare`: target worktree is safe to prepare; run `prepare`.
|
|
54
|
+
- `prepared`: symlinks and manifest were created or repaired.
|
|
55
|
+
- `blocked`: user-owned paths would be overwritten; stop and report `conflicts`.
|
|
56
|
+
- `error`: Git or source-root resolution failed; stop and report `reason`.
|
|
@@ -303,6 +303,7 @@
|
|
|
303
303
|
"severity": "error",
|
|
304
304
|
"target": ".codex/hooks/inject-workflow-state.py",
|
|
305
305
|
"whenOperations": [
|
|
306
|
+
"workflow-state-worktree-root-fallback",
|
|
306
307
|
"workflow-state-codex-session-start-guard",
|
|
307
308
|
"workflow-state-stale-task-status",
|
|
308
309
|
"workflow-state-untracked-helper",
|
|
@@ -312,11 +313,13 @@
|
|
|
312
313
|
"assertion": {
|
|
313
314
|
"type": "required-literal",
|
|
314
315
|
"values": [
|
|
316
|
+
"# BEGIN skill-garden patch workflow-state-worktree-root-fallback v0.6",
|
|
315
317
|
"# BEGIN skill-garden patch workflow-state-codex-session-start-guard v0.6",
|
|
316
318
|
"# BEGIN skill-garden patch workflow-state-stale-task-status v0.6",
|
|
317
319
|
"# BEGIN skill-garden patch workflow-state-untracked-helper v0.6",
|
|
318
320
|
"# BEGIN skill-garden patch workflow-state-breadcrumb-subject v0.6",
|
|
319
321
|
"# BEGIN skill-garden patch workflow-state-main-subject-routing v0.6",
|
|
322
|
+
"_find_trellis_root_from_git",
|
|
320
323
|
"DEFAULT_PROMPT_INJECTION_SKIP_KEYWORD = \"no-trellis\"",
|
|
321
324
|
"\"ZCODE_PROJECT_DIR\": \"zcode\"",
|
|
322
325
|
"Normalize `codex.dispatch_mode` from .trellis/config.yaml to \"auto\" or \"inline\"."
|
package/enhancements/0.6/overrides/patches/hooks/inject-workflow-state/shared-runtime/patch.json
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
"id": "inject-workflow-state-shared-runtime",
|
|
4
4
|
"purpose": "hook_override",
|
|
5
5
|
"operations": [
|
|
6
|
+
{
|
|
7
|
+
"id": "workflow-state-worktree-root-fallback",
|
|
8
|
+
"operation": "replace",
|
|
9
|
+
"targetPolicy": "each-existing",
|
|
10
|
+
"targets": [
|
|
11
|
+
{ "kind": "file", "path": ".codex/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
12
|
+
{ "kind": "file", "path": ".claude/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
13
|
+
{ "kind": "file", "path": ".gemini/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
14
|
+
{ "kind": "file", "path": ".qoder/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
15
|
+
{ "kind": "file", "path": ".github/copilot/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
16
|
+
{ "kind": "file", "path": ".codebuddy/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
17
|
+
{ "kind": "file", "path": ".factory/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
18
|
+
{ "kind": "file", "path": ".kiro/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
19
|
+
{ "kind": "file", "path": ".trae/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" },
|
|
20
|
+
{ "kind": "file", "path": ".zcode/hooks/inject-workflow-state.py", "missing": "skip", "markerStyle": "hash" }
|
|
21
|
+
],
|
|
22
|
+
"selector": { "type": "literal", "source": "worktree-root-selector.py" },
|
|
23
|
+
"content": { "source": "worktree-root-content.py" }
|
|
24
|
+
},
|
|
6
25
|
{
|
|
7
26
|
"id": "workflow-state-codex-session-start-guard",
|
|
8
27
|
"operation": "insert",
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
def _git_output(start: Path, *args: str) -> Optional[str]:
|
|
2
|
+
"""Run a read-only git command for best-effort Trellis root fallback."""
|
|
3
|
+
import subprocess
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
result = subprocess.run(
|
|
7
|
+
["git", "-C", str(start), *args],
|
|
8
|
+
check=False,
|
|
9
|
+
stdout=subprocess.PIPE,
|
|
10
|
+
stderr=subprocess.DEVNULL,
|
|
11
|
+
text=True,
|
|
12
|
+
timeout=2,
|
|
13
|
+
)
|
|
14
|
+
except (OSError, subprocess.SubprocessError):
|
|
15
|
+
return None
|
|
16
|
+
if result.returncode != 0:
|
|
17
|
+
return None
|
|
18
|
+
output = result.stdout.strip()
|
|
19
|
+
return output or None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _resolve_git_common_dir(start: Path) -> Optional[Path]:
|
|
23
|
+
"""Return the current repository's common git dir."""
|
|
24
|
+
output = _git_output(start, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
25
|
+
if output is None:
|
|
26
|
+
output = _git_output(start, "rev-parse", "--git-common-dir")
|
|
27
|
+
if output is None:
|
|
28
|
+
return None
|
|
29
|
+
common_dir = Path(output)
|
|
30
|
+
if common_dir.is_absolute():
|
|
31
|
+
return common_dir.resolve()
|
|
32
|
+
top_level = _git_output(start, "rev-parse", "--show-toplevel")
|
|
33
|
+
if top_level is None:
|
|
34
|
+
return None
|
|
35
|
+
return (Path(top_level) / common_dir).resolve()
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _find_trellis_root_from_git(start: Path) -> Optional[Path]:
|
|
39
|
+
"""Find a sibling worktree that carries .trellis/ for this Git repository."""
|
|
40
|
+
common_dir = _resolve_git_common_dir(start)
|
|
41
|
+
if common_dir is not None:
|
|
42
|
+
candidate = common_dir.parent
|
|
43
|
+
if (candidate / ".trellis").is_dir():
|
|
44
|
+
return candidate
|
|
45
|
+
|
|
46
|
+
output = _git_output(start, "worktree", "list", "--porcelain")
|
|
47
|
+
if output is None:
|
|
48
|
+
return None
|
|
49
|
+
for line in output.splitlines():
|
|
50
|
+
if not line.startswith("worktree "):
|
|
51
|
+
continue
|
|
52
|
+
candidate = Path(line.split(" ", 1)[1]).expanduser().resolve()
|
|
53
|
+
if (candidate / ".trellis").is_dir():
|
|
54
|
+
return candidate
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def find_trellis_root(start: Path) -> Optional[Path]:
|
|
59
|
+
"""Walk up from start, then fall back to the Git worktree carrying .trellis/.
|
|
60
|
+
|
|
61
|
+
Linked worktrees often do not contain the untracked .trellis/ runtime. The
|
|
62
|
+
hook still needs the main worktree's runtime state so direct-edit cursors can
|
|
63
|
+
be resumed from the linked worktree without copying .trellis/.
|
|
64
|
+
"""
|
|
65
|
+
cur = start.resolve()
|
|
66
|
+
while cur != cur.parent:
|
|
67
|
+
if (cur / ".trellis").is_dir():
|
|
68
|
+
return cur
|
|
69
|
+
cur = cur.parent
|
|
70
|
+
return _find_trellis_root_from_git(start.resolve())
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
def find_trellis_root(start: Path) -> Optional[Path]:
|
|
2
|
+
"""Walk up from start to find directory containing .trellis/.
|
|
3
|
+
|
|
4
|
+
Handles CWD drift: subdirectory launches, monorepo packages, etc.
|
|
5
|
+
Returns None if no .trellis/ found (silent no-op).
|
|
6
|
+
"""
|
|
7
|
+
cur = start.resolve()
|
|
8
|
+
while cur != cur.parent:
|
|
9
|
+
if (cur / ".trellis").is_dir():
|
|
10
|
+
return cur
|
|
11
|
+
cur = cur.parent
|
|
12
|
+
return None
|
|
@@ -6,6 +6,7 @@ from __future__ import annotations
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
8
|
import os
|
|
9
|
+
import subprocess
|
|
9
10
|
import sys
|
|
10
11
|
import tempfile
|
|
11
12
|
import uuid
|
|
@@ -82,9 +83,9 @@ def _utc_now() -> str:
|
|
|
82
83
|
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
83
84
|
|
|
84
85
|
|
|
85
|
-
def
|
|
86
|
-
"""
|
|
87
|
-
current =
|
|
86
|
+
def _find_root_upwards(start: Path) -> Path | None:
|
|
87
|
+
"""从指定路径向上查找带 .trellis 的目录。"""
|
|
88
|
+
current = start.resolve()
|
|
88
89
|
if current.is_file():
|
|
89
90
|
current = current.parent
|
|
90
91
|
while True:
|
|
@@ -95,6 +96,80 @@ def _find_repo_root(start: Path | None = None) -> Path | None:
|
|
|
95
96
|
current = current.parent
|
|
96
97
|
|
|
97
98
|
|
|
99
|
+
def _git_output(start: Path, *args: str) -> str | None:
|
|
100
|
+
"""执行只读 git 命令并返回 stdout,失败时返回 None。"""
|
|
101
|
+
try:
|
|
102
|
+
result = subprocess.run(
|
|
103
|
+
["git", "-C", str(start), *args],
|
|
104
|
+
check=False,
|
|
105
|
+
stdout=subprocess.PIPE,
|
|
106
|
+
stderr=subprocess.DEVNULL,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=2,
|
|
109
|
+
)
|
|
110
|
+
except (OSError, subprocess.SubprocessError):
|
|
111
|
+
return None
|
|
112
|
+
if result.returncode != 0:
|
|
113
|
+
return None
|
|
114
|
+
output = result.stdout.strip()
|
|
115
|
+
return output or None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _resolve_git_common_dir(start: Path) -> Path | None:
|
|
119
|
+
"""返回当前 Git 仓库的 common dir,兼容旧 git 的相对路径输出。"""
|
|
120
|
+
output = _git_output(start, "rev-parse", "--path-format=absolute", "--git-common-dir")
|
|
121
|
+
if output is None:
|
|
122
|
+
output = _git_output(start, "rev-parse", "--git-common-dir")
|
|
123
|
+
if output is None:
|
|
124
|
+
return None
|
|
125
|
+
common_dir = Path(output)
|
|
126
|
+
if common_dir.is_absolute():
|
|
127
|
+
return common_dir.resolve()
|
|
128
|
+
top_level = _git_output(start, "rev-parse", "--show-toplevel")
|
|
129
|
+
if top_level is None:
|
|
130
|
+
return None
|
|
131
|
+
return (Path(top_level) / common_dir).resolve()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _find_root_from_git_worktrees(start: Path) -> Path | None:
|
|
135
|
+
"""从同一个 Git worktree 集合里寻找承载 .trellis 的主项目根。"""
|
|
136
|
+
current = start.resolve()
|
|
137
|
+
if current.is_file():
|
|
138
|
+
current = current.parent
|
|
139
|
+
|
|
140
|
+
common_dir = _resolve_git_common_dir(current)
|
|
141
|
+
if common_dir is not None:
|
|
142
|
+
candidate = common_dir.parent
|
|
143
|
+
if (candidate / ".trellis").is_dir():
|
|
144
|
+
return candidate
|
|
145
|
+
|
|
146
|
+
output = _git_output(current, "worktree", "list", "--porcelain")
|
|
147
|
+
if output is None:
|
|
148
|
+
return None
|
|
149
|
+
for line in output.splitlines():
|
|
150
|
+
if not line.startswith("worktree "):
|
|
151
|
+
continue
|
|
152
|
+
candidate = Path(line.split(" ", 1)[1]).expanduser().resolve()
|
|
153
|
+
if (candidate / ".trellis").is_dir():
|
|
154
|
+
return candidate
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _find_repo_root(start: Path | None = None) -> Path | None:
|
|
159
|
+
"""查找 Trellis 项目根目录,兼容没有 .trellis 的 linked worktree。"""
|
|
160
|
+
current = (start or Path.cwd()).resolve()
|
|
161
|
+
root = _find_root_upwards(current)
|
|
162
|
+
if root is not None:
|
|
163
|
+
return root
|
|
164
|
+
root = _find_root_from_git_worktrees(current)
|
|
165
|
+
if root is not None:
|
|
166
|
+
return root
|
|
167
|
+
for parent in Path(__file__).resolve().parents:
|
|
168
|
+
if (parent / ".trellis").is_dir():
|
|
169
|
+
return parent
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
|
|
98
173
|
def _session_path(repo_root: Path, context_key: str) -> Path:
|
|
99
174
|
"""返回指定 session runtime 文件路径。"""
|
|
100
175
|
return repo_root / ".trellis/.runtime/sessions" / f"{context_key}.json"
|