@infinitedusky/indusk-mcp 1.36.2 → 1.37.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/admin/.next/BUILD_ID +1 -1
- package/admin/.next/build-manifest.json +3 -3
- package/admin/.next/fallback-build-manifest.json +3 -3
- package/admin/.next/prerender-manifest.json +3 -3
- package/admin/.next/server/app/_global-error.html +1 -1
- package/admin/.next/server/app/_global-error.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/admin/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/admin/.next/server/chunks/ssr/[root-of-the-server]__0p.51_4._.js.map +1 -1
- package/admin/.next/server/middleware-build-manifest.js +3 -3
- package/admin/.next/server/pages/500.html +1 -1
- package/admin/.next/server/server-reference-manifest.js +1 -1
- package/admin/.next/server/server-reference-manifest.json +1 -1
- package/admin/.next/trace +1 -1
- package/admin/.next/trace-build +1 -1
- package/dist/bin/cli.js +42 -4
- package/dist/bin/commands/init.js +42 -10
- package/dist/bin/commands/telemetry.js +7 -1
- package/dist/bin/commands/update.js +104 -2
- package/dist/bin/commands/workbench.d.ts +76 -0
- package/dist/bin/commands/workbench.js +438 -0
- package/dist/bin/commands/worktree.d.ts +22 -6
- package/dist/bin/commands/worktree.js +154 -111
- package/dist/lib/config.d.ts +59 -0
- package/dist/lib/hook-migration.d.ts +42 -0
- package/dist/lib/hook-migration.js +94 -0
- package/dist/lib/path-segment.d.ts +23 -0
- package/dist/lib/path-segment.js +41 -0
- package/dist/lib/plan-parser.js +5 -13
- package/dist/lib/stray-state-audit.d.ts +3 -2
- package/dist/lib/stray-state-audit.js +14 -20
- package/dist/lib/telemetry/daemon.d.ts +18 -0
- package/dist/lib/telemetry/daemon.js +18 -2
- package/dist/lib/verify/git.d.ts +11 -4
- package/dist/lib/verify/git.js +11 -4
- package/dist/lib/verify/roots.d.ts +40 -0
- package/dist/lib/verify/roots.js +54 -0
- package/dist/lib/verify/verify.js +17 -2
- package/dist/lib/worktree/layout.d.ts +56 -0
- package/dist/lib/worktree/layout.js +136 -0
- package/dist/lib/worktree/repos.d.ts +46 -0
- package/dist/lib/worktree/repos.js +100 -0
- package/dist/lib/worktree/shareable.d.ts +47 -0
- package/dist/lib/worktree/shareable.js +301 -0
- package/dist/lib/worktree/sync.d.ts +62 -0
- package/dist/lib/worktree/sync.js +174 -0
- package/extensions/worktree/hooks/on_enable.sh +17 -10
- package/extensions/worktree/scripts/lib/workbench-helpers.sh +84 -0
- package/extensions/worktree/scripts/preflight.sh +4 -8
- package/extensions/worktree/scripts/refresh-worktree.sh +4 -8
- package/extensions/worktree/scripts/setup-worktree.sh +40 -16
- package/extensions/worktree/skill.md +8 -6
- package/hooks/_hook-paths.js +78 -15
- package/hooks/workbench-sync.js +119 -0
- package/package.json +109 -109
- package/skills/catchup.md +17 -0
- /package/admin/.next/static/{b30fOo0tUhHOxiaUT2sKQ → pg-wvx75mxUJvPhSjSoUe}/_buildManifest.js +0 -0
- /package/admin/.next/static/{b30fOo0tUhHOxiaUT2sKQ → pg-wvx75mxUJvPhSjSoUe}/_clientMiddlewareManifest.js +0 -0
- /package/admin/.next/static/{b30fOo0tUhHOxiaUT2sKQ → pg-wvx75mxUJvPhSjSoUe}/_ssgManifest.js +0 -0
|
@@ -15,6 +15,23 @@
|
|
|
15
15
|
# Caller is responsible for calling _resolve_workbench_root first
|
|
16
16
|
# and cd'ing or passing the path explicitly via WORKBENCH_ROOT env.
|
|
17
17
|
#
|
|
18
|
+
# _read_workbench_repos
|
|
19
|
+
# Echoes the declared repo names, one per line, in declared order.
|
|
20
|
+
# DELIBERATE PORT of readWorkbenchRepos() in
|
|
21
|
+
# src/lib/worktree/repos.ts — bash cannot import the TS module, so
|
|
22
|
+
# the reduction lives in two places and they change together.
|
|
23
|
+
# `worktree.repos[]` is canonical; the legacy `worktree.wrapped_repo`
|
|
24
|
+
# reduces to a one-element list. Names that are not clean path
|
|
25
|
+
# segments are dropped and duplicates collapse to first occurrence,
|
|
26
|
+
# because every caller joins these into a filesystem path.
|
|
27
|
+
#
|
|
28
|
+
# _resolve_workbench_repo [requested]
|
|
29
|
+
# Echoes the one repo the caller means. With `requested` it must
|
|
30
|
+
# match a declared repo. Without it, a single declared repo is
|
|
31
|
+
# implied; with several, this FAILS and names the candidates on
|
|
32
|
+
# stderr rather than picking one — putting a worktree in the wrong
|
|
33
|
+
# repo looks exactly like success until someone reads the branch.
|
|
34
|
+
#
|
|
18
35
|
# _read_worktree_config <repo> <jq-filter>
|
|
19
36
|
# Reads a value from .indusk/worktree-configs/<repo>.json using the
|
|
20
37
|
# provided jq filter. Echoes the value or empty string if config
|
|
@@ -78,6 +95,73 @@ _read_workbench_field() {
|
|
|
78
95
|
jq -r ".worktree.${field} // \"\"" "$config" 2>/dev/null || echo ""
|
|
79
96
|
}
|
|
80
97
|
|
|
98
|
+
_read_workbench_repos() {
|
|
99
|
+
local root="${WORKBENCH_ROOT:-}"
|
|
100
|
+
if [[ -z "$root" ]]; then
|
|
101
|
+
echo "Error: _read_workbench_repos: WORKBENCH_ROOT must be set" >&2
|
|
102
|
+
return 1
|
|
103
|
+
fi
|
|
104
|
+
local config="$root/.indusk/config.json"
|
|
105
|
+
if [[ ! -f "$config" ]]; then
|
|
106
|
+
echo "Error: _read_workbench_repos: $config not found" >&2
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
# The reduction, in jq. `unique` is deliberately NOT used: it sorts, and
|
|
110
|
+
# declared order is meaningful (the first repo is the implied one at N=1).
|
|
111
|
+
jq -r '
|
|
112
|
+
(.worktree // {}) as $w
|
|
113
|
+
| (
|
|
114
|
+
if ($w.repos | type) == "array" then $w.repos
|
|
115
|
+
elif ($w.wrapped_repo | type) == "string" then [{ name: $w.wrapped_repo }]
|
|
116
|
+
else []
|
|
117
|
+
end
|
|
118
|
+
)
|
|
119
|
+
| map(select(type == "object"))
|
|
120
|
+
| map(.name)
|
|
121
|
+
| map(select(type == "string"))
|
|
122
|
+
| map(select(. != "" and . != "." and . != ".." and (contains("/") | not) and (contains("\\") | not)))
|
|
123
|
+
| reduce .[] as $n ([]; if (index($n) == null) then . + [$n] else . end)
|
|
124
|
+
| .[]
|
|
125
|
+
' "$config" 2>/dev/null || echo ""
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
_resolve_workbench_repo() {
|
|
129
|
+
local requested="${1:-}"
|
|
130
|
+
local repos=()
|
|
131
|
+
while IFS= read -r line; do
|
|
132
|
+
[[ -n "$line" ]] && repos+=("$line")
|
|
133
|
+
done < <(_read_workbench_repos)
|
|
134
|
+
|
|
135
|
+
if [[ ${#repos[@]} -eq 0 ]]; then
|
|
136
|
+
echo "Error: this project is not a workbench (set worktree.shape=\"workbench\" and worktree.repos[] in .indusk/config.json, or run \`indusk init --workbench\`)." >&2
|
|
137
|
+
return 1
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
local joined
|
|
141
|
+
joined="$(printf '%s, ' "${repos[@]}")"
|
|
142
|
+
joined="${joined%, }"
|
|
143
|
+
|
|
144
|
+
if [[ -n "$requested" ]]; then
|
|
145
|
+
local r
|
|
146
|
+
for r in "${repos[@]}"; do
|
|
147
|
+
if [[ "$r" == "$requested" ]]; then
|
|
148
|
+
echo "$r"
|
|
149
|
+
return 0
|
|
150
|
+
fi
|
|
151
|
+
done
|
|
152
|
+
echo "Error: no declared repo named \"$requested\". This workbench declares: ${joined}." >&2
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
if [[ ${#repos[@]} -eq 1 ]]; then
|
|
157
|
+
echo "${repos[0]}"
|
|
158
|
+
return 0
|
|
159
|
+
fi
|
|
160
|
+
|
|
161
|
+
echo "Error: this workbench declares more than one repo, so the repo must be named: ${joined}." >&2
|
|
162
|
+
return 1
|
|
163
|
+
}
|
|
164
|
+
|
|
81
165
|
_read_worktree_config() {
|
|
82
166
|
local repo="$1"
|
|
83
167
|
local filter="$2"
|
|
@@ -53,20 +53,16 @@ fi
|
|
|
53
53
|
WORKBENCH_ROOT="$(_resolve_workbench_root)"
|
|
54
54
|
export WORKBENCH_ROOT
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
if [[ -z "$WRAPPED_REPO" ]]; then
|
|
58
|
-
echo "Error: .indusk/config.json missing worktree.wrapped_repo" >&2
|
|
59
|
-
exit 1
|
|
60
|
-
fi
|
|
56
|
+
REPO="$(_resolve_workbench_repo "${REPO_ARG:-}")"
|
|
61
57
|
|
|
62
|
-
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${
|
|
58
|
+
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${REPO}.json"
|
|
63
59
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
64
60
|
echo "Error: no worktree config at $CONFIG_FILE" >&2
|
|
65
61
|
exit 1
|
|
66
62
|
fi
|
|
67
63
|
|
|
68
64
|
# Reject preflighting the trunk — preflight is a feature-branch concept.
|
|
69
|
-
if [[ "$SLUG" == "$
|
|
65
|
+
if [[ "$SLUG" == "$REPO" ]]; then
|
|
70
66
|
echo "Error: preflight cannot target the trunk; pick a worktree slug" >&2
|
|
71
67
|
exit 1
|
|
72
68
|
fi
|
|
@@ -86,7 +82,7 @@ for entry in "$WORKBENCH_ROOT"/*; do
|
|
|
86
82
|
name="$(basename "$entry")"
|
|
87
83
|
_is_reserved_name "$name" && continue
|
|
88
84
|
# Skip the trunk symlink — preflight is for feature worktrees.
|
|
89
|
-
[[ "$name" == "$
|
|
85
|
+
[[ "$name" == "$REPO" ]] && continue
|
|
90
86
|
if [[ "$name" == "$SLUG" ]]; then
|
|
91
87
|
exact_paths+=("$entry")
|
|
92
88
|
elif [[ "$name" == *"-$SLUG" ]]; then
|
|
@@ -33,17 +33,13 @@ ARG="${1:?Usage: refresh-worktree.sh <slug|--all>}"
|
|
|
33
33
|
WORKBENCH_ROOT="$(_resolve_workbench_root)"
|
|
34
34
|
export WORKBENCH_ROOT
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
if [[ -z "$WRAPPED_REPO" ]]; then
|
|
38
|
-
echo "Error: .indusk/config.json missing worktree.wrapped_repo" >&2
|
|
39
|
-
exit 1
|
|
40
|
-
fi
|
|
36
|
+
REPO="$(_resolve_workbench_repo "${REPO_ARG:-}")"
|
|
41
37
|
|
|
42
38
|
SIBLING_PARENT_RAW="$(_read_workbench_field sibling_parent)"
|
|
43
39
|
SIBLING_PARENT="$(_expand_path "$SIBLING_PARENT_RAW")"
|
|
44
|
-
CLIENT_ROOT="$SIBLING_PARENT/$
|
|
40
|
+
CLIENT_ROOT="$SIBLING_PARENT/$REPO"
|
|
45
41
|
|
|
46
|
-
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${
|
|
42
|
+
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${REPO}.json"
|
|
47
43
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
48
44
|
echo "Error: no worktree config at $CONFIG_FILE" >&2
|
|
49
45
|
exit 1
|
|
@@ -59,7 +55,7 @@ refresh_one() {
|
|
|
59
55
|
echo " SKIP: $slug — directory not found"
|
|
60
56
|
return 0
|
|
61
57
|
fi
|
|
62
|
-
if [[ "$slug" == "$
|
|
58
|
+
if [[ "$slug" == "$REPO" ]]; then
|
|
63
59
|
return 0 # trunk symlink, not a worktree
|
|
64
60
|
fi
|
|
65
61
|
# Check this is actually a git worktree (has a .git file pointing into
|
|
@@ -3,16 +3,21 @@
|
|
|
3
3
|
# single-repo workbench.
|
|
4
4
|
#
|
|
5
5
|
# Usage:
|
|
6
|
-
# setup-worktree.sh <slug> [base-branch]
|
|
6
|
+
# setup-worktree.sh [--repo <name>] [--worktrees-dir <dir>] <slug> [base-branch]
|
|
7
7
|
#
|
|
8
8
|
# Examples:
|
|
9
9
|
# setup-worktree.sh feat-autoops-cancel-polish
|
|
10
|
-
# setup-worktree.sh feat-
|
|
10
|
+
# setup-worktree.sh --repo avoca-next feat-thing main
|
|
11
|
+
#
|
|
12
|
+
# `--repo` names which declared repo to branch from. It is optional when the
|
|
13
|
+
# workbench declares exactly one; with several it is REQUIRED, and omitting it
|
|
14
|
+
# fails naming the candidates rather than defaulting to the first.
|
|
11
15
|
#
|
|
12
16
|
# What it does:
|
|
13
17
|
# 1. Resolves the workbench root (walks up from cwd to find
|
|
14
18
|
# .indusk/config.json with worktree.shape == "workbench").
|
|
15
|
-
# 2.
|
|
19
|
+
# 2. Resolves which declared repo to use (worktree.repos[], legacy
|
|
20
|
+
# wrapped_repo reduced) + worktree.sibling_parent.
|
|
16
21
|
# 3. Reads .indusk/worktree-configs/<repo>.json for trunk_branch, base_branch,
|
|
17
22
|
# copy_files[], append_files[], apply_commits[].
|
|
18
23
|
# 4. Creates a worktree at <workbench>/<slug>, branched off <base-branch>
|
|
@@ -33,17 +38,27 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
33
38
|
# shellcheck source=lib/workbench-helpers.sh
|
|
34
39
|
source "$SCRIPT_DIR/lib/workbench-helpers.sh"
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
REPO_ARG=""
|
|
42
|
+
WORKTREES_DIR=""
|
|
43
|
+
# Flags in any order; both optional. `--worktrees-dir` is passed by the TS
|
|
44
|
+
# wrapper when the repo DECLARES a worktrees location. Absent, worktrees land
|
|
45
|
+
# at the workbench root — today's flat layout, unchanged.
|
|
46
|
+
while [[ "${1:-}" == --* ]]; do
|
|
47
|
+
case "$1" in
|
|
48
|
+
--repo) REPO_ARG="${2:?--repo requires a repo name}"; shift 2 ;;
|
|
49
|
+
--worktrees-dir) WORKTREES_DIR="${2:?--worktrees-dir requires a directory}"; shift 2 ;;
|
|
50
|
+
*) echo "Error: unknown flag $1" >&2; exit 1 ;;
|
|
51
|
+
esac
|
|
52
|
+
done
|
|
53
|
+
SLUG="${1:?Usage: setup-worktree.sh [--repo <name>] <slug> [base-branch]}"
|
|
37
54
|
BASE_BRANCH_ARG="${2:-}"
|
|
38
55
|
|
|
39
56
|
WORKBENCH_ROOT="$(_resolve_workbench_root)"
|
|
40
57
|
export WORKBENCH_ROOT
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
exit 1
|
|
46
|
-
fi
|
|
59
|
+
# One resolver for "which repo" — refuses rather than guessing when the
|
|
60
|
+
# workbench declares several and the caller named none.
|
|
61
|
+
REPO="$(_resolve_workbench_repo "$REPO_ARG")"
|
|
47
62
|
|
|
48
63
|
SIBLING_PARENT_RAW="$(_read_workbench_field sibling_parent)"
|
|
49
64
|
if [[ -z "$SIBLING_PARENT_RAW" ]]; then
|
|
@@ -52,31 +67,40 @@ if [[ -z "$SIBLING_PARENT_RAW" ]]; then
|
|
|
52
67
|
fi
|
|
53
68
|
SIBLING_PARENT="$(_expand_path "$SIBLING_PARENT_RAW")"
|
|
54
69
|
|
|
55
|
-
CLIENT_ROOT="$SIBLING_PARENT/$
|
|
70
|
+
CLIENT_ROOT="$SIBLING_PARENT/$REPO"
|
|
56
71
|
if [[ ! -d "$CLIENT_ROOT/.git" ]]; then
|
|
57
72
|
echo "Error: $CLIENT_ROOT is not a git repo" >&2
|
|
58
73
|
exit 1
|
|
59
74
|
fi
|
|
60
75
|
|
|
61
|
-
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${
|
|
76
|
+
CONFIG_FILE="$WORKBENCH_ROOT/.indusk/worktree-configs/${REPO}.json"
|
|
62
77
|
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
63
78
|
echo "Error: no worktree config at $CONFIG_FILE" >&2
|
|
64
79
|
echo "Run 'indusk extensions enable worktree' to scaffold the starter, or hand-create the file." >&2
|
|
65
80
|
exit 1
|
|
66
81
|
fi
|
|
67
82
|
|
|
68
|
-
TRUNK_BRANCH="$(_read_worktree_config "$
|
|
69
|
-
CONFIG_BASE_BRANCH="$(_read_worktree_config "$
|
|
83
|
+
TRUNK_BRANCH="$(_read_worktree_config "$REPO" '.trunk_branch // "main"')"
|
|
84
|
+
CONFIG_BASE_BRANCH="$(_read_worktree_config "$REPO" '.base_branch // .trunk_branch // "main"')"
|
|
70
85
|
BASE_BRANCH="${BASE_BRANCH_ARG:-$CONFIG_BASE_BRANCH}"
|
|
71
86
|
|
|
72
87
|
# Reject slug collisions with the wrapped repo's name (resolution would be
|
|
73
88
|
# ambiguous; the trunk lives at that path).
|
|
74
|
-
if [[ "$SLUG" == "$
|
|
89
|
+
if [[ -z "$WORKTREES_DIR" && "$SLUG" == "$REPO" ]]; then
|
|
90
|
+
# Only a flat layout can collide — a declared worktrees directory puts the
|
|
91
|
+
# worktree somewhere the trunk can never be.
|
|
75
92
|
echo "Error: slug '$SLUG' collides with the wrapped repo name; pick a different slug" >&2
|
|
76
93
|
exit 1
|
|
77
94
|
fi
|
|
78
95
|
|
|
79
|
-
|
|
96
|
+
# A declared location is a single clean segment (guarded on the TS side before
|
|
97
|
+
# it ever reaches here), so this join cannot escape the workbench.
|
|
98
|
+
if [[ -n "$WORKTREES_DIR" ]]; then
|
|
99
|
+
WORKTREE_PATH="$WORKBENCH_ROOT/$WORKTREES_DIR/$SLUG"
|
|
100
|
+
mkdir -p "$WORKBENCH_ROOT/$WORKTREES_DIR"
|
|
101
|
+
else
|
|
102
|
+
WORKTREE_PATH="$WORKBENCH_ROOT/$SLUG"
|
|
103
|
+
fi
|
|
80
104
|
if [[ -e "$WORKTREE_PATH" ]]; then
|
|
81
105
|
echo "Error: worktree already exists at $WORKTREE_PATH" >&2
|
|
82
106
|
echo "To remove: git -C $CLIENT_ROOT worktree remove --force $WORKTREE_PATH" >&2
|
|
@@ -91,7 +115,7 @@ BRANCH_NAME="$SLUG"
|
|
|
91
115
|
|
|
92
116
|
echo "Setting up worktree:"
|
|
93
117
|
echo " Workbench: $WORKBENCH_ROOT"
|
|
94
|
-
echo " Wrapped repo: $
|
|
118
|
+
echo " Wrapped repo: $REPO"
|
|
95
119
|
echo " Slug: $SLUG"
|
|
96
120
|
echo " Branch: $BRANCH_NAME (off $BASE_BRANCH)"
|
|
97
121
|
echo " Trunk: $TRUNK_BRANCH"
|
|
@@ -23,10 +23,10 @@ A workbench is an indusk project with this shape:
|
|
|
23
23
|
```
|
|
24
24
|
my-workbench/ # the indusk project
|
|
25
25
|
├── .indusk/ # single source of truth — plans, eval, highlights, worktree-configs
|
|
26
|
-
│ ├── config.json # `worktree.shape: "workbench"` + `worktree.wrapped_repo` + `worktree.sibling_parent`
|
|
26
|
+
│ ├── config.json # `worktree.shape: "workbench"` + `worktree.repos[]` (legacy `wrapped_repo` reduces) + `worktree.sibling_parent`
|
|
27
27
|
│ └── worktree-configs/
|
|
28
28
|
│ └── <repo>.json # per-wrapped-repo config (copy_files, apply_commits, preflight, etc.)
|
|
29
|
-
├── <repo> # symlink → canonical clone (
|
|
29
|
+
├── <repo> # symlink → canonical clone (one trunk per declared repo)
|
|
30
30
|
├── <slug-1>/ # active worktree (git worktree add'd from the trunk)
|
|
31
31
|
├── <slug-2>/ # another active worktree
|
|
32
32
|
├── ce.json # LEGACY composable.env config (deprecated — doppler is the default env layer); optional `composeProjectName: "<repo>"`
|
|
@@ -35,7 +35,9 @@ my-workbench/ # the indusk project
|
|
|
35
35
|
|
|
36
36
|
The wrapped repo's canonical clone lives elsewhere on disk (typically `~/code/<area>/<repo>`); the workbench's `<repo>` symlink points to it. The clone keeps its own `.git/`; the workbench keeps the `.indusk/`. Worktrees are created via `git worktree add` from inside the trunk symlink and land as siblings of the trunk.
|
|
37
37
|
|
|
38
|
-
**
|
|
38
|
+
**Declared layout**: a repo entry may carry `path` (where its checkout lives) and `worktrees` (where its worktrees go), both relative to the workbench and both single clean segments. Absent means flat — the workbench root — so an existing workbench is untouched and nesting is opt-in per repo. `worktree list` then groups structurally from the declared directory rather than asking git who owns what, and anything on disk it cannot place renders under **Unattributed**, naming the worktrees inside it: declarations add structure and can never subtract.
|
|
39
|
+
|
|
40
|
+
**Trunk vs worktrees**: a workbench declares its repos in `worktree.repos[]` (the legacy `worktree.wrapped_repo` reduces to a one-element list, so existing workbenches need no edit). Each declared repo gets its own trunk symlink at the workbench root; worktrees are real directories beside them. `indusk worktree list` renders one block per repo and attributes each worktree to its owning repo by asking git for `--git-common-dir` — not by guessing from the slug, because a wrong attribution reads exactly like a right one. Worktree slugs must not collide with any declared repo name. `docs/` is reserved at the workbench root (internal engagement docs), alongside `scripts/` and `env/`.
|
|
39
41
|
|
|
40
42
|
## When to enable
|
|
41
43
|
|
|
@@ -55,9 +57,9 @@ Four state-management commands, all run from the workbench root:
|
|
|
55
57
|
|
|
56
58
|
### `indusk worktree create <slug> [base-branch]`
|
|
57
59
|
|
|
58
|
-
Creates a new worktree at `<workbench-root>/<slug>/`, branched off `<base-branch>` (default per `.indusk/worktree-configs/<
|
|
60
|
+
Creates a new worktree at `<workbench-root>/<slug>/`, branched off `<base-branch>` (default per `.indusk/worktree-configs/<repo>.json`'s `base_branch`, keyed by the resolved repo). Applies the config's `copy_files[]` and `append_files[]` declarations. Applies any `apply_commits[]` entries as **upstream-file-overlay** (full-file replacement via `git show <sha>:<file>` followed by `git update-index --skip-worktree`), NOT cherry-pick — the overlay files are invisible to `git status` / `git diff` / `git commit -a`. After setup, env is **auto-provisioned** (doppler extension, if configured) and the config's **`post_create[]`** shell commands run in order inside the new worktree — `pnpm install`, build, anything — so `create` yields a *runnable* worktree in one shot, not a bare checkout. A `post_create` command that exits non-zero stops the rest and prints what to re-run. Idempotent: invoking twice with the same `<slug>` exits non-zero with "worktree already exists at <path>".
|
|
59
61
|
|
|
60
|
-
The `<repo>` argument
|
|
62
|
+
The `<repo>` argument is BACK (versioned-workbench): `indusk worktree create [repo] <slug> [base-branch]`. It is optional when the workbench declares exactly one repo; with several it is required, and omitting it fails naming the candidates rather than picking the first. A leading token counts as a repo only when the workbench actually declares it, so existing single-repo invocations keep their meaning.
|
|
61
63
|
|
|
62
64
|
### `indusk worktree refresh <slug>`
|
|
63
65
|
|
|
@@ -93,7 +95,7 @@ Examples (workbench is wrapping `numero`):
|
|
|
93
95
|
- `pnpm wt numero lint` — cd to `<workbench>/numero/` (the trunk symlink), run `pnpm lint`
|
|
94
96
|
- `pnpm wt cancel-polish ce dc:up local` — cd to `<workbench>/cancel-polish/`, run `pnpm ce dc:up local` (ce reads the worktree's env)
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
Each trunk is addressable by its repo name (`pnpm wt numero ...`). No `pnpm wt trunk` alias — keeps the surface minimal; the names are already in `worktree.repos[]` and stable.
|
|
97
99
|
|
|
98
100
|
### Composing with composable.env (legacy — deprecated in favor of doppler)
|
|
99
101
|
|
package/hooks/_hook-paths.js
CHANGED
|
@@ -107,18 +107,24 @@ function findGitPathFromCwd(cwd) {
|
|
|
107
107
|
* against it fails even though the commit DID happen in a real git repo
|
|
108
108
|
* inside the workbench.
|
|
109
109
|
*
|
|
110
|
-
* The fallback reads `${statePath}/.indusk/config.json` for the
|
|
111
|
-
* `worktree.
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
110
|
+
* The fallback reads `${statePath}/.indusk/config.json` for the declared repo
|
|
111
|
+
* set — `worktree.repos[]`, with the legacy `worktree.wrapped_repo` reducing
|
|
112
|
+
* to a one-element list. **Deliberate port of `lib/worktree/repos.ts`'s
|
|
113
|
+
* reduction**: a hook cannot import TypeScript, so the rule lives in two
|
|
114
|
+
* places and they must change together.
|
|
115
|
+
*
|
|
116
|
+
* With exactly ONE declared repo the behaviour is unchanged from 1.31.10.
|
|
117
|
+
*
|
|
118
|
+
* With MORE THAN ONE (versioned-workbench D6) the fallback is ambiguous, so it
|
|
119
|
+
* does not guess: it returns null, and the caller can name every declared repo
|
|
120
|
+
* via `declaredReposAt`. Picking the first would attribute a commit to the
|
|
121
|
+
* wrong repo, and a wrong attribution is indistinguishable from a right one in
|
|
122
|
+
* the eval record — a silent gap is recoverable, a confident wrong answer is
|
|
123
|
+
* not.
|
|
124
|
+
*
|
|
125
|
+
* Trade-off accepted (single-repo case, unchanged): if the user committed in a
|
|
126
|
+
* SIBLING WORKTREE rather than the wrapped repo, this resolves `gitPath` to the
|
|
127
|
+
* wrapped repo, whose HEAD may not be the commit just made.
|
|
122
128
|
*
|
|
123
129
|
* @param {string | null} statePath
|
|
124
130
|
* @returns {string | null}
|
|
@@ -133,9 +139,11 @@ function findGitPathFromWorkbenchConfig(statePath) {
|
|
|
133
139
|
} catch {
|
|
134
140
|
return null;
|
|
135
141
|
}
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
const declared = declaredRepoNames(config);
|
|
143
|
+
// Ambiguous: more than one repo could hold this commit. Refuse rather than
|
|
144
|
+
// attribute it to whichever happens to be declared first.
|
|
145
|
+
if (declared.length !== 1) return null;
|
|
146
|
+
const candidate = resolve(statePath, declared[0]);
|
|
139
147
|
if (!existsSync(candidate)) return null;
|
|
140
148
|
try {
|
|
141
149
|
return realpathSync(candidate);
|
|
@@ -144,6 +152,61 @@ function findGitPathFromWorkbenchConfig(statePath) {
|
|
|
144
152
|
}
|
|
145
153
|
}
|
|
146
154
|
|
|
155
|
+
/**
|
|
156
|
+
* The declared repo names, singular reduced into plural.
|
|
157
|
+
*
|
|
158
|
+
* Deliberate port of `readWorkbenchRepos` in `src/lib/worktree/repos.ts` —
|
|
159
|
+
* hooks are plain JS and cannot import the TS module. Change both together.
|
|
160
|
+
* Name-only: the hook never needs remotes.
|
|
161
|
+
*
|
|
162
|
+
* @param {unknown} config
|
|
163
|
+
* @returns {string[]}
|
|
164
|
+
*/
|
|
165
|
+
function declaredRepoNames(config) {
|
|
166
|
+
const worktree = config && typeof config === "object" ? config.worktree : null;
|
|
167
|
+
if (!worktree || typeof worktree !== "object") return [];
|
|
168
|
+
const raw = Array.isArray(worktree.repos)
|
|
169
|
+
? worktree.repos
|
|
170
|
+
: typeof worktree.wrapped_repo === "string"
|
|
171
|
+
? [{ name: worktree.wrapped_repo }]
|
|
172
|
+
: [];
|
|
173
|
+
const seen = new Set();
|
|
174
|
+
const names = [];
|
|
175
|
+
for (const entry of raw) {
|
|
176
|
+
if (!entry || typeof entry !== "object") continue;
|
|
177
|
+
const name = entry.name;
|
|
178
|
+
if (typeof name !== "string") continue;
|
|
179
|
+
const clean =
|
|
180
|
+
name.trim() !== "" &&
|
|
181
|
+
name !== "." &&
|
|
182
|
+
name !== ".." &&
|
|
183
|
+
!name.includes("/") &&
|
|
184
|
+
!name.includes("\\");
|
|
185
|
+
if (!clean || seen.has(name)) continue;
|
|
186
|
+
seen.add(name);
|
|
187
|
+
names.push(name);
|
|
188
|
+
}
|
|
189
|
+
return names;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Every declared repo name at a workbench root, so a caller that refuses can
|
|
194
|
+
* SAY what it could not choose between.
|
|
195
|
+
*
|
|
196
|
+
* @param {string | null} statePath
|
|
197
|
+
* @returns {string[]}
|
|
198
|
+
*/
|
|
199
|
+
export function declaredReposAt(statePath) {
|
|
200
|
+
if (!statePath) return [];
|
|
201
|
+
const configPath = resolve(statePath, ".indusk/config.json");
|
|
202
|
+
if (!existsSync(configPath)) return [];
|
|
203
|
+
try {
|
|
204
|
+
return declaredRepoNames(JSON.parse(readFileSync(configPath, "utf-8")));
|
|
205
|
+
} catch {
|
|
206
|
+
return [];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
147
210
|
/**
|
|
148
211
|
* Resolve both the InDusk state path and the git path for a given cwd. Either
|
|
149
212
|
* may be null:
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* workbench-sync.js — PostToolUse hook (Edit|Write).
|
|
4
|
+
*
|
|
5
|
+
* The trigger half of workbench sync. The mechanism lives in
|
|
6
|
+
* `lib/worktree/sync.ts` and is shared with `indusk workbench sync`; this only
|
|
7
|
+
* decides WHEN to call it. Keeping the two apart means the trigger can become
|
|
8
|
+
* a watcher daemon later without touching the mechanism.
|
|
9
|
+
*
|
|
10
|
+
* THREE GUARDS, and the first is the important one:
|
|
11
|
+
*
|
|
12
|
+
* 1. WORKBENCH ONLY. A normal-mode project keeps `.indusk/` inside its own
|
|
13
|
+
* product repo, so auto-committing there would commit a developer's
|
|
14
|
+
* half-finished source on every edit. dusk itself is such a project. This
|
|
15
|
+
* hook must be inert anywhere `worktree.shape` is not "workbench".
|
|
16
|
+
* 2. DEBOUNCED. Edit/Write fires constantly; a push per keystroke-burst is
|
|
17
|
+
* pointless network traffic. The stamp lives under the GITDIR, never the
|
|
18
|
+
* working tree — per-machine state that must not become a tracked file
|
|
19
|
+
* (the worktree extension keeps its overlay state the same way).
|
|
20
|
+
* 3. NEVER BLOCKS. Always exits 0. A sync failure must not fail a developer's
|
|
21
|
+
* edit, and an unreachable remote is a normal condition, not an error.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { spawnSync } from "node:child_process";
|
|
25
|
+
import { existsSync, readFileSync, statSync, utimesSync, writeFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { resolveStateAndGitPaths } from "./_hook-paths.js";
|
|
28
|
+
|
|
29
|
+
/** Seconds between syncs. Edits burst; the remote does not need every burst. */
|
|
30
|
+
const DEBOUNCE_SECONDS = 20;
|
|
31
|
+
|
|
32
|
+
function isWorkbench(root) {
|
|
33
|
+
try {
|
|
34
|
+
const cfg = JSON.parse(readFileSync(join(root, ".indusk", "config.json"), "utf-8"));
|
|
35
|
+
return cfg?.worktree?.shape === "workbench";
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Stamp path — under the gitdir when there is one, which keeps it untracked by
|
|
43
|
+
* construction rather than by a rule.
|
|
44
|
+
*
|
|
45
|
+
* CHICKEN-AND-EGG: a workbench that has never been git-initialized has no
|
|
46
|
+
* gitdir, and the only thing that git-inits a workbench is `sync` itself. A
|
|
47
|
+
* null here used to mean "not due", so exactly the workbenches that most need
|
|
48
|
+
* their first sync were the ones that never got one. The fallback is inside
|
|
49
|
+
* `.indusk/`, which every workbench has and which the managed ignore covers.
|
|
50
|
+
*/
|
|
51
|
+
function stampPath(root) {
|
|
52
|
+
const r = spawnSync("git", ["rev-parse", "--absolute-git-dir"], { cwd: root, encoding: "utf-8" });
|
|
53
|
+
if (r.status === 0) return join(r.stdout.trim(), "indusk-sync-stamp");
|
|
54
|
+
return join(root, ".indusk", "sync-stamp");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function dueForSync(stamp) {
|
|
58
|
+
if (!stamp) return true;
|
|
59
|
+
if (!existsSync(stamp)) return true;
|
|
60
|
+
try {
|
|
61
|
+
return (Date.now() - statSync(stamp).mtimeMs) / 1000 >= DEBOUNCE_SECONDS;
|
|
62
|
+
} catch {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function touch(stamp) {
|
|
68
|
+
try {
|
|
69
|
+
if (existsSync(stamp)) {
|
|
70
|
+
const now = new Date();
|
|
71
|
+
utimesSync(stamp, now, now);
|
|
72
|
+
} else {
|
|
73
|
+
writeFileSync(stamp, "workbench sync debounce stamp\n");
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// A stamp we cannot write means we sync more often than intended —
|
|
77
|
+
// noisy, never wrong. Silence is the correct response.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function main() {
|
|
82
|
+
let event = {};
|
|
83
|
+
try {
|
|
84
|
+
event = JSON.parse(readFileSync(0, "utf-8") || "{}");
|
|
85
|
+
} catch {
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const { statePath } = resolveStateAndGitPaths(event.cwd ?? process.cwd());
|
|
90
|
+
if (!statePath) process.exit(0);
|
|
91
|
+
|
|
92
|
+
// Guard 1 — the load-bearing one.
|
|
93
|
+
if (!isWorkbench(statePath)) process.exit(0);
|
|
94
|
+
|
|
95
|
+
// Guard 2.
|
|
96
|
+
const stamp = stampPath(statePath);
|
|
97
|
+
if (!dueForSync(stamp)) process.exit(0);
|
|
98
|
+
touch(stamp);
|
|
99
|
+
|
|
100
|
+
// Guard 3: spawn detached and never wait. A sync must not add latency to a
|
|
101
|
+
// developer's edit, and its outcome must not decide whether the edit stood.
|
|
102
|
+
const bin = process.env.INDUSK_BIN ?? "indusk";
|
|
103
|
+
try {
|
|
104
|
+
const child = spawnSync(bin, ["workbench", "sync"], {
|
|
105
|
+
cwd: statePath,
|
|
106
|
+
encoding: "utf-8",
|
|
107
|
+
timeout: 25_000,
|
|
108
|
+
});
|
|
109
|
+
if (child.status !== 0 && child.stderr) {
|
|
110
|
+
// Reported, not raised. The hook still exits 0.
|
|
111
|
+
process.stderr.write(`[workbench-sync] ${child.stderr.trim().split("\n")[0]}\n`);
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// ignore
|
|
115
|
+
}
|
|
116
|
+
process.exit(0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
main();
|