@sassywares/skills 0.5.13 → 0.5.15
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/package.json +1 -1
- package/skills/sassy-setup/SKILL.md +5 -5
- package/skills/sassy-setup/assets/sassywares-readme.md +22 -0
- package/skills/sassy-setup/assets/workspace/_lib.sh +93 -0
- package/skills/sassy-setup/assets/workspace/branches +57 -0
- package/skills/sassy-setup/assets/workspace/checkout-bases +91 -0
- package/skills/sassy-setup/assets/workspace/pull +92 -0
- package/skills/sassy-setup/references/workspace.md +100 -14
- package/skills/sassy-setup/scripts/detect.sh +46 -0
package/package.json
CHANGED
|
@@ -52,7 +52,7 @@ A check run never edits the config and never rewrites the tree: it prints what `
|
|
|
52
52
|
- **Write `.sassywares/README.md`** from `assets/sassywares-readme.md` when absent. Never overwrite it — it may carry project notes.
|
|
53
53
|
- **Tell the user to `/reload`.** The skills are invisible to the harness until it rescans.
|
|
54
54
|
|
|
55
|
-
6. **Install the
|
|
55
|
+
6. **Install the workspace tooling, `workspace` only.** The snapshot plus the `branches` / `pull` / `checkout-bases` git helpers, and one `workspace:<script>` alias per script in the project's own manifest. Read `references/workspace.md` and follow it.
|
|
56
56
|
7. **Verify.** Run `scripts/detect.sh --root <path> --check`. It validates the config against the tree — topology and remotes, `base_map` shape and cycles, `repos` against each nested repo, hooks, artifacts paths, `knowledge`, `pr.*` booleans, `commit.branch`, any unknown key, and in `workspace` mode the installed snapshot's version — plus the vendored pin: every `.agents/skills/sassy-*` folder hashed against `skills-lock.json`, the lock's coverage both ways, and whether git carries the lock. Report every `drift:` line verbatim, or that the config matches the tree. Drift blocks step 8.
|
|
57
57
|
8. **Commit what setup owns.** `repo` topology only, and only when step 7 is clean. The file list is exactly what setup wrote or found unwritten:
|
|
58
58
|
|
|
@@ -65,7 +65,7 @@ A check run never edits the config and never rewrites the tree: it prints what `
|
|
|
65
65
|
- **`workspace`** — nothing to commit: the root is not a repo, and `references/workspace.md` forbids running the snapshot here. Say the root config and the pin land with the project's next snapshot.
|
|
66
66
|
- **`none`** — nothing to commit, deliberately.
|
|
67
67
|
|
|
68
|
-
9. **Report.** The config path, the topology, the resolved artifact and knowledge paths, the commit (hash and branch) or why there is none, and in `workspace` mode what was installed. Remind: `/reload` so the harness rescans. Close with what is left over — at most dirt setup deliberately did not commit — and nothing else: a clean setup leaves the user good to go.
|
|
68
|
+
9. **Report.** The config path, the topology, the resolved artifact and knowledge paths, the commit (hash and branch) or why there is none, and in `workspace` mode what was installed, with versions (`snapshot vN`, `workspace-git vN`), and the manifest the `workspace:*` aliases went into — naming it as created when there was no script surface to begin with. Remind: `/reload` so the harness rescans. Close with what is left over — at most dirt setup deliberately did not commit — and nothing else: a clean setup leaves the user good to go.
|
|
69
69
|
|
|
70
70
|
## Hard rules
|
|
71
71
|
|
|
@@ -76,10 +76,10 @@ A check run never edits the config and never rewrites the tree: it prints what `
|
|
|
76
76
|
| Verify before commit | A config that fails `--check` is reported, never committed. |
|
|
77
77
|
| Approval to create a remote | Creating the mirror repo needs an explicit yes and a named slug first. |
|
|
78
78
|
| No silent overwrite | An existing config or an existing `.sassywares/workspace/snapshot` is shown and asked about, never replaced blind. |
|
|
79
|
-
| Version is reported | Setup names the snapshot
|
|
79
|
+
| Version is reported | Setup names the snapshot and `workspace-git` versions it installed; `--check` flags an installed copy older than the skill's. |
|
|
80
80
|
| Skills come from upstream | Install with `npx skills add`; never hand-copy skill files or maintain the links yourself. |
|
|
81
|
-
| Never edit the manifest | No dependencies, no
|
|
82
|
-
| Write boundary | Only `.sassywares/`, the vendored `.agents/skills/sassy-*`, `skills-lock.json`, and one `.gitignore` line — and the same list is step 8's commit. Never `package.json
|
|
81
|
+
| Never edit the manifest | No dependencies, no `postinstall`, no `skills:*` scripts. The one exception is step 6's `workspace:*` aliases in `workspace` topology: appended or corrected, never removed, and nothing else in the file touched. Setup installs the suite; the project decides how it updates. |
|
|
82
|
+
| Write boundary | Only `.sassywares/`, the vendored `.agents/skills/sassy-*`, `skills-lock.json`, and one `.gitignore` line — and the same list is step 8's commit. Never `package.json` outside step 6's `workspace:*` aliases, never a root `.gitignore` in a workspace. Workspace git helpers land in `.sassywares/workspace/`, never in a project's `bin/`. |
|
|
83
83
|
| Detect, don't guess | An unknown base branch stays unknown and gets asked about. A probed value is reported as probed. |
|
|
84
84
|
| Comments are part of the file | Every key keeps its description and allowed values. A stripped config is an incomplete config. |
|
|
85
85
|
| Hooks are named, not written | Setup records hook skill names. Creating those skills is the project's work. |
|
|
@@ -31,6 +31,28 @@ If a skill tells you another skill is missing, run `npx skills add sassywares/sa
|
|
|
31
31
|
|
|
32
32
|
Or ask for `mode:check` on the setup skill, which runs the same check and reports it. Neither moves the pin: `npx skills check` is an alias of `update`.
|
|
33
33
|
|
|
34
|
+
## Workspace scripts
|
|
35
|
+
|
|
36
|
+
In a `workspace` project, `.sassywares/workspace/` holds the scripts that work
|
|
37
|
+
across the sibling repos, next to the `snapshot` that versions the glue:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
.sassywares/workspace/branches # branch + dirty marker per repo
|
|
41
|
+
.sassywares/workspace/pull # fetch, then ff-only per repo
|
|
42
|
+
.sassywares/workspace/checkout-bases # put each repo on its base
|
|
43
|
+
.sassywares/workspace/snapshot # commit + push the glue
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Each one also has an alias in the project manifest (`workspace:branches`,
|
|
47
|
+
`workspace:pull`, `workspace:checkout-bases`, `workspace:snapshot`), so
|
|
48
|
+
`npm run workspace:pull` — or the equivalent for the runner this project uses —
|
|
49
|
+
works from the root.
|
|
50
|
+
|
|
51
|
+
They read the repo inventory and the PR bases from `repos:` in `config.yaml`, so
|
|
52
|
+
there is no second base map to keep in step. They are this project's files:
|
|
53
|
+
extend them freely — just run the snapshot afterwards, since they live in the
|
|
54
|
+
mirrored tree. The `sassy-*` skills do their own git and never call them.
|
|
55
|
+
|
|
34
56
|
## Hooks
|
|
35
57
|
|
|
36
58
|
`hooks.pr` names project skills that run inside the PR pipeline, keyed by the PR's **head** branch: `"*"` for every PR, an exact branch, or a family such as `release/*`, overriding per point (`before_create`, `after_create`, `before_merge`, `after_merge`). An entry is `<skill> <args…>`; the args mean whatever that skill says they mean — `mode:auto` pre-authorizes the step. Hooks are ordinary skills in `.agents/skills/`, which makes them **code**: review them like code, and never point a hook at a copy of a suite skill.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Shared helpers for the workspace git scripts: branches, pull, checkout-bases.
|
|
3
|
+
#
|
|
4
|
+
# Installed next to those scripts by sassy-setup, and owned by this project from
|
|
5
|
+
# then on — extend it here rather than forking a copy. The one rule worth keeping
|
|
6
|
+
# is that the repo inventory and the PR bases come from `repos:` in
|
|
7
|
+
# .sassywares/config.yaml; never grow a second base map in this file.
|
|
8
|
+
#
|
|
9
|
+
# workspace-git-version: 1
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
WORKSPACE_GIT_VERSION="1"
|
|
14
|
+
|
|
15
|
+
WS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
ROOT="$(cd "$WS_DIR/../.." && pwd)"
|
|
17
|
+
CONFIG="$ROOT/.sassywares/config.yaml"
|
|
18
|
+
# Nested repos conventionally push to origin; override for a workspace that does not.
|
|
19
|
+
REMOTE="${WORKSPACE_GIT_REMOTE:-origin}"
|
|
20
|
+
|
|
21
|
+
log() { printf '%s\n' "$*"; }
|
|
22
|
+
ok() { printf ' ✓ %s\n' "$*"; }
|
|
23
|
+
warn() { printf ' ! %s\n' "$*" >&2; }
|
|
24
|
+
die() { printf ' ✗ %s\n' "$*" >&2; exit 1; }
|
|
25
|
+
|
|
26
|
+
need_git() {
|
|
27
|
+
command -v git >/dev/null 2>&1 || die "git not found"
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
[[ -f "$CONFIG" ]] || die "no config at $CONFIG — run sassy-setup"
|
|
31
|
+
|
|
32
|
+
# Print `<dir><TAB><base>` for every entry under `repos:`, in file order.
|
|
33
|
+
# Deliberately shallow: that one map, not a YAML parser.
|
|
34
|
+
workspace_repos() {
|
|
35
|
+
awk '
|
|
36
|
+
/^[[:space:]]*#/ { next }
|
|
37
|
+
/^repos:[[:space:]]*$/ { in_repos = 1; next }
|
|
38
|
+
!in_repos { next }
|
|
39
|
+
/^[^[:space:]]/ { exit }
|
|
40
|
+
/^[[:space:]]*$/ { next }
|
|
41
|
+
{
|
|
42
|
+
line = $0
|
|
43
|
+
sub(/^[[:space:]]+/, "", line)
|
|
44
|
+
dir = line; sub(/:.*/, "", dir)
|
|
45
|
+
base = line; sub(/^[^:]*:[[:space:]]*/, "", base); sub(/[[:space:]]+#.*$/, "", base)
|
|
46
|
+
if (dir != "" && base != "") printf "%s\t%s\n", dir, base
|
|
47
|
+
}
|
|
48
|
+
' "$CONFIG"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Immediate children that are their own git repo but carry no `repos:` entry.
|
|
52
|
+
# A drift signal for `branches`: never a base inferred on the project's behalf.
|
|
53
|
+
unmapped_repos() {
|
|
54
|
+
local path name mapped
|
|
55
|
+
mapped="$(workspace_repos | cut -f1)"
|
|
56
|
+
for path in "$ROOT"/*/; do
|
|
57
|
+
name="$(basename "$path")"
|
|
58
|
+
[[ "$name" == node_modules || "$name" == .* ]] && continue
|
|
59
|
+
[[ -d "$path/.git" ]] || continue
|
|
60
|
+
if ! printf '%s\n' "$mapped" | grep -Fxq -- "$name"; then
|
|
61
|
+
printf '%s\n' "$name"
|
|
62
|
+
fi
|
|
63
|
+
done
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
repo_has_git() {
|
|
67
|
+
[[ -d "$1/.git" ]]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
current_branch() {
|
|
71
|
+
git -C "$1" rev-parse --abbrev-ref HEAD 2>/dev/null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
is_dirty() {
|
|
75
|
+
[[ -n "$(git -C "$1" status --porcelain 2>/dev/null)" ]]
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
short_head() {
|
|
79
|
+
git -C "$1" rev-parse --short HEAD 2>/dev/null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# The most useful line of captured git output: its first `error:`/`fatal:` line
|
|
83
|
+
# when there is one, else the last non-blank line. Git's advice blocks are noise
|
|
84
|
+
# in a one-line warn.
|
|
85
|
+
git_err() {
|
|
86
|
+
local line
|
|
87
|
+
line="$(printf '%s\n' "$1" | grep -m1 -E '^(error|fatal):' || true)"
|
|
88
|
+
if [[ -n "$line" ]]; then
|
|
89
|
+
printf '%s\n' "$line"
|
|
90
|
+
return 0
|
|
91
|
+
fi
|
|
92
|
+
printf '%s\n' "$1" | grep -v '^[[:space:]]*$' | tail -n 1
|
|
93
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Show the current branch (and a dirty marker) for every repo in `repos:`.
|
|
3
|
+
#
|
|
4
|
+
# Repos that exist on disk but have no `repos:` entry are listed as unmapped —
|
|
5
|
+
# add them to .sassywares/config.yaml rather than letting a base be guessed.
|
|
6
|
+
#
|
|
7
|
+
# Usage: .sassywares/workspace/branches [--version]
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
# shellcheck source=./_lib.sh
|
|
12
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
13
|
+
|
|
14
|
+
usage() {
|
|
15
|
+
cat <<EOF
|
|
16
|
+
Usage: .sassywares/workspace/branches [--version]
|
|
17
|
+
|
|
18
|
+
Print the current branch for each repo in the config's \`repos:\` map, with a
|
|
19
|
+
trailing * when the tree is dirty and \`(base: …)\` when it is not on its base.
|
|
20
|
+
|
|
21
|
+
Repos without a \`repos:\` entry are listed as unmapped; setup fills that map in.
|
|
22
|
+
EOF
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case "${1:-}" in
|
|
26
|
+
-h|--help) usage; exit 0 ;;
|
|
27
|
+
--version) printf '%s\n' "$WORKSPACE_GIT_VERSION"; exit 0 ;;
|
|
28
|
+
"") ;;
|
|
29
|
+
*) die "unknown argument: $1 (try --help)" ;;
|
|
30
|
+
esac
|
|
31
|
+
|
|
32
|
+
need_git
|
|
33
|
+
|
|
34
|
+
found=0
|
|
35
|
+
while IFS=$'\t' read -r name base; do
|
|
36
|
+
[[ -n "$name" ]] || continue
|
|
37
|
+
path="$ROOT/$name"
|
|
38
|
+
if ! repo_has_git "$path"; then
|
|
39
|
+
printf '%-22s (missing)\n' "$name"
|
|
40
|
+
continue
|
|
41
|
+
fi
|
|
42
|
+
found=1
|
|
43
|
+
branch="$(current_branch "$path")"
|
|
44
|
+
dirty=""
|
|
45
|
+
if is_dirty "$path"; then dirty=" *"; fi
|
|
46
|
+
if [[ "$branch" == "$base" ]]; then
|
|
47
|
+
printf '%-22s %s%s\n' "$name" "$branch" "$dirty"
|
|
48
|
+
else
|
|
49
|
+
printf '%-22s %s%s (base: %s)\n' "$name" "$branch" "$dirty" "$base"
|
|
50
|
+
fi
|
|
51
|
+
done < <(workspace_repos)
|
|
52
|
+
|
|
53
|
+
while IFS= read -r name; do
|
|
54
|
+
printf '%-22s %s\n' "$name" "(unmapped — add it to repos: in .sassywares/config.yaml)"
|
|
55
|
+
done < <(unmapped_repos)
|
|
56
|
+
|
|
57
|
+
(( found > 0 )) || die "no git repos found under repos: — is the config complete?"
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Check out each repo's base branch from `repos:`.
|
|
3
|
+
#
|
|
4
|
+
# Only switches a repo that is on some other branch. A dirty tree that git would
|
|
5
|
+
# clobber stops that repo with an error; nothing is stashed, reset, or forced.
|
|
6
|
+
#
|
|
7
|
+
# Usage: .sassywares/workspace/checkout-bases [--version]
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
11
|
+
# shellcheck source=./_lib.sh
|
|
12
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
13
|
+
|
|
14
|
+
usage() {
|
|
15
|
+
cat <<EOF
|
|
16
|
+
Usage: .sassywares/workspace/checkout-bases [--version]
|
|
17
|
+
|
|
18
|
+
For each repo in the config's \`repos:\` map, check out its base branch:
|
|
19
|
+
|
|
20
|
+
- base exists locally → git checkout <base>
|
|
21
|
+
- base missing locally → fetch, then track $REMOTE/<base>
|
|
22
|
+
- already on the base → reported, untouched
|
|
23
|
+
|
|
24
|
+
Uncommitted changes are never discarded. Does not fetch or pull a repo it
|
|
25
|
+
already has on the base — run .sassywares/workspace/pull for that.
|
|
26
|
+
EOF
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
case "${1:-}" in
|
|
30
|
+
-h|--help) usage; exit 0 ;;
|
|
31
|
+
--version) printf '%s\n' "$WORKSPACE_GIT_VERSION"; exit 0 ;;
|
|
32
|
+
"") ;;
|
|
33
|
+
*) die "unknown argument: $1 (try --help)" ;;
|
|
34
|
+
esac
|
|
35
|
+
|
|
36
|
+
need_git
|
|
37
|
+
|
|
38
|
+
checkout_one() {
|
|
39
|
+
local name="$1" base="$2" path="$3"
|
|
40
|
+
log "$name → $base"
|
|
41
|
+
|
|
42
|
+
if [[ "$(current_branch "$path")" == "$base" ]]; then
|
|
43
|
+
ok "already on $base ($(short_head "$path"))"
|
|
44
|
+
return 0
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
if git -C "$path" show-ref --verify --quiet "refs/heads/$base"; then
|
|
48
|
+
if ! out="$(git -C "$path" checkout "$base" 2>&1)"; then
|
|
49
|
+
warn "$name: could not check out $base — $(git_err "$out")"
|
|
50
|
+
return 1
|
|
51
|
+
fi
|
|
52
|
+
else
|
|
53
|
+
if ! out="$(git -C "$path" fetch --prune "$REMOTE" 2>&1)"; then
|
|
54
|
+
warn "$name: fetch failed — $(git_err "$out")"
|
|
55
|
+
return 1
|
|
56
|
+
fi
|
|
57
|
+
if ! git -C "$path" show-ref --verify --quiet "refs/remotes/$REMOTE/$base"; then
|
|
58
|
+
warn "$name: $REMOTE/$base does not exist"
|
|
59
|
+
return 1
|
|
60
|
+
fi
|
|
61
|
+
if ! out="$(git -C "$path" checkout -B "$base" "$REMOTE/$base" 2>&1)"; then
|
|
62
|
+
warn "$name: could not create $base — $(git_err "$out")"
|
|
63
|
+
return 1
|
|
64
|
+
fi
|
|
65
|
+
git -C "$path" branch --set-upstream-to="$REMOTE/$base" "$base" >/dev/null 2>&1 || true
|
|
66
|
+
fi
|
|
67
|
+
|
|
68
|
+
ok "on $base ($(short_head "$path"))"
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
found=0
|
|
72
|
+
failed=0
|
|
73
|
+
|
|
74
|
+
while IFS=$'\t' read -r name base; do
|
|
75
|
+
[[ -n "$name" ]] || continue
|
|
76
|
+
path="$ROOT/$name"
|
|
77
|
+
if ! repo_has_git "$path"; then
|
|
78
|
+
warn "$name: missing .git — skip"
|
|
79
|
+
continue
|
|
80
|
+
fi
|
|
81
|
+
found=1
|
|
82
|
+
checkout_one "$name" "$base" "$path" || failed=1
|
|
83
|
+
done < <(workspace_repos)
|
|
84
|
+
|
|
85
|
+
(( found > 0 )) || die "no git repos found under repos: — is the config complete?"
|
|
86
|
+
|
|
87
|
+
if (( failed != 0 )); then
|
|
88
|
+
die "one or more repos could not be put on their base"
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
ok "checkout-bases done"
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Fetch every repo in `repos:`, then fast-forward the checked-out branch.
|
|
3
|
+
#
|
|
4
|
+
# Nothing is discarded: dirty trees are kept, a branch without an upstream is
|
|
5
|
+
# fetched only, and a divergent branch is reported instead of merged. Switching
|
|
6
|
+
# branches is checkout-bases' job, not this one.
|
|
7
|
+
#
|
|
8
|
+
# Usage: .sassywares/workspace/pull [--version]
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
12
|
+
# shellcheck source=./_lib.sh
|
|
13
|
+
source "$SCRIPT_DIR/_lib.sh"
|
|
14
|
+
|
|
15
|
+
usage() {
|
|
16
|
+
cat <<EOF
|
|
17
|
+
Usage: .sassywares/workspace/pull [--version]
|
|
18
|
+
|
|
19
|
+
For each repo in the config's \`repos:\` map:
|
|
20
|
+
1. git fetch --prune $REMOTE
|
|
21
|
+
2. git pull --ff-only when the checked-out branch has an upstream
|
|
22
|
+
|
|
23
|
+
Does not switch branches. Uncommitted changes are left as-is. A repo whose base
|
|
24
|
+
is missing from the remote is reported, never fixed here.
|
|
25
|
+
EOF
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
case "${1:-}" in
|
|
29
|
+
-h|--help) usage; exit 0 ;;
|
|
30
|
+
--version) printf '%s\n' "$WORKSPACE_GIT_VERSION"; exit 0 ;;
|
|
31
|
+
"") ;;
|
|
32
|
+
*) die "unknown argument: $1 (try --help)" ;;
|
|
33
|
+
esac
|
|
34
|
+
|
|
35
|
+
need_git
|
|
36
|
+
|
|
37
|
+
found=0
|
|
38
|
+
failed=0
|
|
39
|
+
|
|
40
|
+
while IFS=$'\t' read -r name base; do
|
|
41
|
+
[[ -n "$name" ]] || continue
|
|
42
|
+
path="$ROOT/$name"
|
|
43
|
+
if ! repo_has_git "$path"; then
|
|
44
|
+
warn "$name: missing .git — skip"
|
|
45
|
+
continue
|
|
46
|
+
fi
|
|
47
|
+
found=1
|
|
48
|
+
branch="$(current_branch "$path")"
|
|
49
|
+
log "$name ($branch)"
|
|
50
|
+
if is_dirty "$path"; then
|
|
51
|
+
warn "dirty working tree on $branch — pulling anyway (uncommitted changes kept)"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
if ! out="$(git -C "$path" fetch --prune "$REMOTE" 2>&1)"; then
|
|
55
|
+
warn "$name: fetch failed — $(git_err "$out")"
|
|
56
|
+
failed=1
|
|
57
|
+
continue
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
if git -C "$path" show-ref --verify --quiet "refs/remotes/$REMOTE/$base"; then
|
|
61
|
+
ok "fetched $REMOTE/$base"
|
|
62
|
+
else
|
|
63
|
+
warn "$REMOTE/$base missing"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
upstream="$(git -C "$path" rev-parse --abbrev-ref '@{upstream}' 2>/dev/null || true)"
|
|
67
|
+
if [[ -z "$upstream" ]]; then
|
|
68
|
+
warn "no upstream for $branch — fetched only"
|
|
69
|
+
if [[ "$branch" == "$base" ]]; then
|
|
70
|
+
git -C "$path" branch --set-upstream-to="$REMOTE/$base" "$base" >/dev/null 2>&1 || true
|
|
71
|
+
upstream="$REMOTE/$base"
|
|
72
|
+
else
|
|
73
|
+
continue
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
if out="$(git -C "$path" pull --ff-only 2>&1)"; then
|
|
78
|
+
ok "pulled $branch ← $upstream"
|
|
79
|
+
else
|
|
80
|
+
warn "ff-only pull failed on $branch — $(git_err "$out")"
|
|
81
|
+
warn " .sassywares/workspace/checkout-bases puts it back on $base"
|
|
82
|
+
failed=1
|
|
83
|
+
fi
|
|
84
|
+
done < <(workspace_repos)
|
|
85
|
+
|
|
86
|
+
(( found > 0 )) || die "no git repos found under repos: — is the config complete?"
|
|
87
|
+
|
|
88
|
+
if (( failed != 0 )); then
|
|
89
|
+
die "one or more repos could not be fast-forwarded"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
ok "pull done"
|
|
@@ -1,30 +1,116 @@
|
|
|
1
1
|
# Workspace install
|
|
2
2
|
|
|
3
|
-
Read this only when the topology is `workspace`. Everything
|
|
3
|
+
Read this only when the topology is `workspace`. Everything setup installs
|
|
4
4
|
lives under one directory:
|
|
5
5
|
|
|
6
6
|
```text
|
|
7
7
|
.sassywares/
|
|
8
8
|
├── config.yaml
|
|
9
9
|
└── workspace/
|
|
10
|
-
├── snapshot
|
|
11
|
-
├──
|
|
12
|
-
|
|
10
|
+
├── snapshot sync + commit + push the glue (tracked)
|
|
11
|
+
├── branches current branch per repo (tracked)
|
|
12
|
+
├── pull fetch + fast-forward per repo (tracked)
|
|
13
|
+
├── checkout-bases put each repo on its base (tracked)
|
|
14
|
+
├── _lib.sh shared helpers; reads `repos:` (tracked)
|
|
15
|
+
├── .gitignore exclude patterns for the sync (tracked)
|
|
16
|
+
└── mirror/ the side clone (untracked, self-excluded)
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
- **Snapshot.** Copy `assets/snapshot` to `.sassywares/workspace/snapshot` and
|
|
22
|
+
make it executable. **If a `snapshot` file is already there, say so and ask** —
|
|
23
|
+
it may carry hand edits. Report the version you installed
|
|
24
|
+
(`snapshot --version`) so a later `--check` can catch a stale copy.
|
|
25
|
+
- **Git helpers.** Copy every file in `assets/workspace/` flat into
|
|
26
|
+
`.sassywares/workspace/` and make the three scripts executable. Report
|
|
27
|
+
`_lib.sh`'s `workspace-git-version` (or `branches --version`) the same way.
|
|
28
|
+
**Ask before overwriting any of them when they already exist** — from the first
|
|
29
|
+
install on they are the project's files, and extending them is expected.
|
|
30
|
+
- **Aliases.** One `workspace:<script>` entry per installed script, in the
|
|
31
|
+
project's own manifest. See below.
|
|
32
|
+
- **Excludes.** Write `.sassywares/workspace/.gitignore` for excluded paths
|
|
33
|
+
(vendored trees, generated dirs, `.run/`). One pattern per line, `.gitignore`
|
|
34
|
+
style. Skip it when there is nothing to exclude; the sync works without it. The
|
|
35
|
+
mirror needs no line — the script excludes it itself.
|
|
23
36
|
- Do **not** add a `.gitignore` at the project root, and do not modify one that
|
|
24
|
-
exists.
|
|
37
|
+
exists. A workspace's only ignore file is the one in `workspace/`.
|
|
25
38
|
- Do **not** run the snapshot during setup. It clones and pushes; that is the
|
|
26
39
|
user's call. Offer `--dry-run` and run it only if they ask.
|
|
27
40
|
|
|
28
|
-
|
|
41
|
+
## Script aliases
|
|
42
|
+
|
|
43
|
+
The scripts should be reachable from the project's own runner, so setup wires one
|
|
44
|
+
alias per installed script:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
"scripts": {
|
|
48
|
+
"workspace:branches": ".sassywares/workspace/branches",
|
|
49
|
+
"workspace:pull": ".sassywares/workspace/pull",
|
|
50
|
+
"workspace:checkout-bases": ".sassywares/workspace/checkout-bases",
|
|
51
|
+
"workspace:snapshot": ".sassywares/workspace/snapshot"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Find the file that carries named commands — at the **workspace root only**, never
|
|
56
|
+
inside a repo:
|
|
57
|
+
|
|
58
|
+
| File at the root | Write the aliases to |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `package.json` | its `scripts` |
|
|
61
|
+
| `deno.json` / `deno.jsonc` | its `tasks` |
|
|
62
|
+
| `composer.json` | its `scripts` |
|
|
63
|
+
| `go.mod`, `pyproject.toml`, `Cargo.toml`, `Gemfile`, `Makefile`, `justfile`, … | nothing: create `package.json` as below |
|
|
64
|
+
|
|
65
|
+
`go.mod` and friends name dependencies, not commands, and a make/just target
|
|
66
|
+
cannot carry the `workspace:` form. Never invent a `Makefile` or `justfile` to
|
|
67
|
+
avoid `package.json` — one naming convention across projects is worth more than
|
|
68
|
+
one file fewer.
|
|
69
|
+
|
|
70
|
+
- **Name and value are fixed**: `workspace:<script>` → `.sassywares/workspace/<script>`.
|
|
71
|
+
No `./`, no absolute paths, no `bin/`.
|
|
72
|
+
- **Merge, never rewrite**: existing scripts and every other field stay as they
|
|
73
|
+
are; new keys go at the end of the scripts block; keep the file's indentation.
|
|
74
|
+
Never touch dependencies, `postinstall`, or `skills:*`.
|
|
75
|
+
- **Idempotent**: a matching alias is left alone, a stale value is corrected, and
|
|
76
|
+
an alias the project added itself is never removed.
|
|
77
|
+
- **No manifest at all?** Look at what the repos are — Node, Go, Python, a mix —
|
|
78
|
+
and say so. Then create a minimal `package.json`:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"private": true,
|
|
83
|
+
"scripts": { … }
|
|
84
|
+
}
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`private: true` keeps it unpublishable; no `name`, no `version`, no
|
|
88
|
+
dependencies. It exists so the aliases have a home. **Say that in the report** —
|
|
89
|
+
created versus already there — and skip the aliases entirely when the project
|
|
90
|
+
has removed the scripts they point at.
|
|
91
|
+
|
|
92
|
+
## What the git helpers are for
|
|
93
|
+
|
|
94
|
+
`branches`, `pull`, and `checkout-bases` do across the workspace's repos what the
|
|
95
|
+
same commands do in one repo, and they are a **human/CI convenience**. They read
|
|
96
|
+
the repo inventory and the PR bases from `repos:` in `config.yaml` — no second
|
|
97
|
+
base map, no probing, no inference: a repo missing from the map is reported as
|
|
98
|
+
unmapped, never guessed at.
|
|
99
|
+
|
|
100
|
+
- `branches` — current branch and dirty marker per repo, `(base: …)` when off base.
|
|
101
|
+
- `pull` — `fetch --prune`, then `pull --ff-only` when there is an upstream.
|
|
102
|
+
Dirty trees are kept, divergent branches are reported, nothing is reset.
|
|
103
|
+
- `checkout-bases` — switch to each repo's base, creating it from the remote when
|
|
104
|
+
it is missing locally. Refuses to clobber uncommitted work.
|
|
105
|
+
|
|
106
|
+
They are **not** part of the skill pipeline: skills resolve bases from the config
|
|
107
|
+
themselves and run their own git (`sassy-commit` cuts the branch, `sassy-babysit`
|
|
108
|
+
checks out the PR head). Never point a hook at these scripts. If a project wants
|
|
109
|
+
more — a status summary, a rebase flow — it extends its own copy; setup never
|
|
110
|
+
rewrites them without asking.
|
|
111
|
+
|
|
112
|
+
## The mirror
|
|
113
|
+
|
|
114
|
+
The mirror remote comes from `git.remote`. The script falls back to the mirror's
|
|
29
115
|
own `origin` once it exists. Never create the GitHub repository without the
|
|
30
116
|
explicit approval described in the skill.
|
|
@@ -53,6 +53,7 @@ START="$(cd "$START" && pwd -P)"
|
|
|
53
53
|
|
|
54
54
|
SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
55
55
|
ASSET_SNAPSHOT="$SKILL_DIR/assets/snapshot"
|
|
56
|
+
ASSET_WORKSPACE="$SKILL_DIR/assets/workspace"
|
|
56
57
|
|
|
57
58
|
# The nearest config wins, walking up. Report where it is if one exists.
|
|
58
59
|
CONFIG=""
|
|
@@ -406,6 +407,51 @@ if (( CHECK )); then
|
|
|
406
407
|
else
|
|
407
408
|
bad "missing .sassywares/workspace/snapshot"
|
|
408
409
|
fi
|
|
410
|
+
|
|
411
|
+
# The git helpers are optional as a set, but a half-installed or stale set is
|
|
412
|
+
# drift: they read `repos:` from this config, so a version skew means the
|
|
413
|
+
# project is running a reader written for a different map.
|
|
414
|
+
installed_lib="$ROOT/.sassywares/workspace/_lib.sh"
|
|
415
|
+
if [[ -f "$installed_lib" ]]; then
|
|
416
|
+
lib_v="$(sed -n 's/^# workspace-git-version: //p' "$installed_lib" | head -n 1)"
|
|
417
|
+
asset_lib_v="$(sed -n 's/^# workspace-git-version: //p' "$ASSET_WORKSPACE/_lib.sh" | head -n 1)"
|
|
418
|
+
if [[ -z "$lib_v" ]]; then
|
|
419
|
+
bad "installed .sassywares/workspace/_lib.sh has no workspace-git-version line"
|
|
420
|
+
elif [[ -n "$asset_lib_v" && "$lib_v" != "$asset_lib_v" ]]; then
|
|
421
|
+
bad "installed workspace git helpers are v${lib_v}, this skill ships v${asset_lib_v} — re-run setup"
|
|
422
|
+
else
|
|
423
|
+
note " workspace-git v${lib_v}"
|
|
424
|
+
fi
|
|
425
|
+
for helper in branches pull checkout-bases; do
|
|
426
|
+
if [[ ! -x "$ROOT/.sassywares/workspace/$helper" ]]; then
|
|
427
|
+
bad "missing or not executable: .sassywares/workspace/$helper"
|
|
428
|
+
fi
|
|
429
|
+
done
|
|
430
|
+
else
|
|
431
|
+
note " no workspace git helpers installed (optional)"
|
|
432
|
+
fi
|
|
433
|
+
|
|
434
|
+
# Aliases live in the project's own manifest; setup only ever adds them.
|
|
435
|
+
manifest=""
|
|
436
|
+
for candidate in package.json deno.json deno.jsonc composer.json; do
|
|
437
|
+
if [[ -f "$ROOT/$candidate" ]]; then
|
|
438
|
+
manifest="$candidate"
|
|
439
|
+
break
|
|
440
|
+
fi
|
|
441
|
+
done
|
|
442
|
+
if [[ -n "$manifest" ]]; then
|
|
443
|
+
missing_aliases=()
|
|
444
|
+
for script in branches pull checkout-bases snapshot; do
|
|
445
|
+
grep -q "\"workspace:${script}\"" "$ROOT/$manifest" || missing_aliases+=("$script")
|
|
446
|
+
done
|
|
447
|
+
if [[ ${#missing_aliases[@]} -eq 0 ]]; then
|
|
448
|
+
note " workspace:* aliases in ${manifest}"
|
|
449
|
+
elif [[ ${#missing_aliases[@]} -lt 4 ]]; then
|
|
450
|
+
bad "${manifest} is missing $(printf '`workspace:%s` ' "${missing_aliases[@]}")— half-wired aliases; re-run setup"
|
|
451
|
+
else
|
|
452
|
+
note " no workspace:* aliases in ${manifest} (optional)"
|
|
453
|
+
fi
|
|
454
|
+
fi
|
|
409
455
|
while IFS= read -r name; do
|
|
410
456
|
[[ -n "$name" ]] || continue
|
|
411
457
|
base="$(detect_base "$ROOT/$name")"
|