@reddb-io/red-skills 4.4.1 → 4.5.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/bin/build-info.mjs +87 -0
- package/bin/cli-frontdoor.mjs +14 -0
- package/bin/red-skills-brain.mjs +17 -22
- package/bin/red-skills-content.mjs +17 -0
- package/bin/red-skills-hook.mjs +45 -0
- package/bin/red-skills-memory.mjs +17 -22
- package/bin/red-skills-redskilled-mcp.mjs +2 -0
- package/bin/red-skills-resource.mjs +21 -0
- package/bin/runtime-compatibility.mjs +17 -0
- package/bin/runtime-paths.mjs +13 -0
- package/dist/code-nav.bundle.min.mjs +1 -1
- package/dist/opencode-host.bundle.min.mjs +1 -1
- package/dist/redskilled-mcp.bundle.min.mjs +36 -36
- package/dist/redskilled.bundle.min.mjs +1 -1
- package/dist/release.bundle.min.mjs +1 -1
- package/dist/rsp-core.bundle.min.mjs +1 -1
- package/dist/rsp.bundle.min.mjs +1 -1
- package/dist/statusline.bundle.min.mjs +1 -1
- package/package.json +9 -4
- package/runtime/hook-routes.json +125 -0
- package/runtime/plugins/brain/scripts/bootstrap.mjs +656 -0
- package/runtime/plugins/brain/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/hooks/branch-lock-codex.sh +105 -0
- package/runtime/plugins/dev/hooks/code-nav-mcp.sh +63 -0
- package/runtime/plugins/dev/hooks/command-guard.sh +893 -0
- package/runtime/plugins/dev/hooks/ensure-codex-statusline.mjs +144 -0
- package/runtime/plugins/dev/hooks/red-fetch.mjs +38 -0
- package/runtime/plugins/dev/hooks/redskilled-mcp.sh +64 -0
- package/runtime/plugins/dev/hooks/rsp-hook.sh +343 -0
- package/runtime/plugins/dev/hooks/tests/command-guard.test.sh +378 -0
- package/runtime/plugins/dev/hooks/tests/rsp-hook.test.sh +229 -0
- package/runtime/plugins/dev/scripts/memory-bridge.sh +230 -0
- package/runtime/plugins/dev/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/dev/scripts/tests/memory-bridge.test.sh +555 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/cargo-pre-worktree.sh +37 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/envelope-post-attempt.sh +47 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/gradle-pre-worktree.sh +45 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/heartbeat-post-attempt.sh +43 -0
- package/runtime/plugins/dev/skills/engineering/afk/defaults/validation-post-merge.sh +77 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/cargo.sh +27 -0
- package/runtime/plugins/dev/skills/engineering/afk/detectors/gradle.sh +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/only-mine.sh +32 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/red-afk-attempt-action.yml +53 -0
- package/runtime/plugins/dev/skills/engineering/afk/examples/rs-afk-attempt.yml +76 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-cargo +38 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-envelope +50 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-gradle +42 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-heartbeat +46 -0
- package/runtime/plugins/dev/skills/engineering/afk/hooks/red-validation +79 -0
- package/runtime/plugins/dev/skills/engineering/diagnose/scripts/hitl-loop.template.sh +41 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/scripts/install-runtime-shim.sh +181 -0
- package/runtime/plugins/dev/skills/engineering/red-setup/workflows/red-issues-needs-triage.yml +36 -0
- package/runtime/plugins/dev/skills/engineering/wizard/template.sh +211 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/graph-mode-plumbing.sh +36 -0
- package/runtime/plugins/dev/skills/engineering/zoom-out/scripts/tests/contract.test.sh +168 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-hook.sh +113 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock-session-start.sh +81 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/branch-lock.sh +68 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/dev-config.sh +126 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/git-command-classifier.sh +198 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/lock-store.sh +67 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/lib/scope-resolver.sh +43 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/branch-lock-cli.test.sh +114 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/claude-plugin-hook.test.sh +117 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/codex-hook.test.sh +230 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/dev-config.test.sh +99 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/git-command-classifier.test.sh +187 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/lock-store.test.sh +90 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/scope-resolver.test.sh +63 -0
- package/runtime/plugins/dev/skills/misc/branch-lock/scripts/tests/session-start.test.sh +123 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh +231 -0
- package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/tests/block-dangerous-git.test.sh +143 -0
- package/runtime/plugins/internal/scripts/structural-smoke.sh +7 -0
- package/runtime/plugins/internal/skills/maintainer/create-plugin/scripts/create-plugin.sh +308 -0
- package/runtime/plugins/memory/scripts/bootstrap.mjs +659 -0
- package/runtime/plugins/memory/scripts/structural-smoke.sh +7 -0
- package/scripts/check-skills-boundary.mjs +32 -0
package/runtime/plugins/dev/skills/misc/git-guardrails-claude-code/scripts/block-dangerous-git.sh
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# block-dangerous-git.sh — git-guardrails PreToolUse(Bash) hook.
|
|
3
|
+
#
|
|
4
|
+
# Two independent layers, both ending in exit 2 (Claude Code's "deny this tool
|
|
5
|
+
# call") with a message on stderr:
|
|
6
|
+
#
|
|
7
|
+
# 1. Always-on dangerous patterns — push, reset --hard, clean -f, branch -D,
|
|
8
|
+
# whole-tree checkout/restore. These fire regardless of any branch lock and
|
|
9
|
+
# are the skill's original purpose.
|
|
10
|
+
#
|
|
11
|
+
# 2. Branch-lock awareness — when a branch lock is active in the primary
|
|
12
|
+
# checkout (an opt-in `.red/tmp/branch-lock.yaml` whose content is the
|
|
13
|
+
# locked branch), the hook ALSO blocks the branch-leaving / work-loss family
|
|
14
|
+
# (switch/checkout to another branch, `switch -`, bare `git stash`). This is
|
|
15
|
+
# a self-contained re-implementation: git-guardrails reads the lock and
|
|
16
|
+
# classifies the command on its own and never sources or requires the
|
|
17
|
+
# branch-lock skill (issue #65 — independent, no dependency). If branch-lock
|
|
18
|
+
# is also installed the two hooks reach the same verdict and stack
|
|
19
|
+
# idempotently — both deny, neither conflicts.
|
|
20
|
+
#
|
|
21
|
+
# The lock layer stays silent (no block) when there is no lock file, and is
|
|
22
|
+
# scope-exempt inside registered worktree lanes under .red/tmp/worktrees/,
|
|
23
|
+
# mirroring the branch-lock hook so the autonomous loop is never strangled.
|
|
24
|
+
#
|
|
25
|
+
# 3. Primary branch guard — when `.red/config.yaml` sets
|
|
26
|
+
# `dev.lock.primary-branch: true`, the hook blocks branch-switching in the
|
|
27
|
+
# primary checkout even without a branch-lock file. This is also
|
|
28
|
+
# self-contained and keeps the same worktree exemption.
|
|
29
|
+
|
|
30
|
+
INPUT=$(cat)
|
|
31
|
+
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
|
|
32
|
+
|
|
33
|
+
DANGEROUS_PATTERNS=(
|
|
34
|
+
"git push"
|
|
35
|
+
"git reset --hard"
|
|
36
|
+
"git clean -fd"
|
|
37
|
+
"git clean -f"
|
|
38
|
+
"git branch -D"
|
|
39
|
+
"git checkout \."
|
|
40
|
+
"git restore \."
|
|
41
|
+
"push --force"
|
|
42
|
+
"reset --hard"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
for pattern in "${DANGEROUS_PATTERNS[@]}"; do
|
|
46
|
+
if echo "$COMMAND" | grep -qE "$pattern"; then
|
|
47
|
+
echo "BLOCKED: '$COMMAND' matches dangerous pattern '$pattern'. The user has prevented you from doing this." >&2
|
|
48
|
+
exit 2
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# --- Branch-lock awareness (self-contained; no branch-lock skill dependency) ---
|
|
53
|
+
|
|
54
|
+
[ -z "$COMMAND" ] && exit 0
|
|
55
|
+
|
|
56
|
+
# Project root: prefer the harness-provided dir, fall back to the git toplevel.
|
|
57
|
+
ROOT="${CLAUDE_PROJECT_DIR:-}"
|
|
58
|
+
if [ -z "$ROOT" ]; then
|
|
59
|
+
ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
|
|
60
|
+
fi
|
|
61
|
+
[ -z "$ROOT" ] && exit 0
|
|
62
|
+
|
|
63
|
+
# Scope: registered worktree lanes under .red/tmp/worktrees/ are exempt even when locked.
|
|
64
|
+
case "$ROOT" in
|
|
65
|
+
*/.red/tmp/worktrees/*/*) exit 0 ;;
|
|
66
|
+
*/.red/tmp/worktrees/*/*/*) exit 0 ;;
|
|
67
|
+
*/.red/tmp/work-*) exit 0 ;;
|
|
68
|
+
*/.red/tmp/work-*/*) exit 0 ;;
|
|
69
|
+
esac
|
|
70
|
+
|
|
71
|
+
# Runtime reader for the one dev flag ADR 0043 needs. Missing file/key or
|
|
72
|
+
# malformed input is disabled, so the plugin-level hook is dormant by default.
|
|
73
|
+
_dev_lock_primary_branch_enabled() {
|
|
74
|
+
local _file="$1"
|
|
75
|
+
[ -f "$_file" ] || return 1
|
|
76
|
+
|
|
77
|
+
local -a _stack=()
|
|
78
|
+
local -a _indents=()
|
|
79
|
+
local _raw _line _indent_str _indent _rest _key _value _full _i
|
|
80
|
+
|
|
81
|
+
while IFS= read -r _raw || [ -n "$_raw" ]; do
|
|
82
|
+
_raw="${_raw%$'\r'}"
|
|
83
|
+
_line="$_raw"
|
|
84
|
+
if [[ "$_line" != *\"* && "$_line" != *\'* ]]; then
|
|
85
|
+
_line="${_line%%#*}"
|
|
86
|
+
fi
|
|
87
|
+
_line="${_line%"${_line##*[![:space:]]}"}"
|
|
88
|
+
[ -z "${_line//[[:space:]]/}" ] && continue
|
|
89
|
+
|
|
90
|
+
_indent_str="${_line%%[![:space:]]*}"
|
|
91
|
+
_indent=${#_indent_str}
|
|
92
|
+
(( _indent % 2 == 0 )) || return 1
|
|
93
|
+
_rest="${_line:$_indent}"
|
|
94
|
+
|
|
95
|
+
[[ "$_rest" =~ ^[A-Za-z_][A-Za-z0-9_-]*: ]] || return 1
|
|
96
|
+
_key="${_rest%%:*}"
|
|
97
|
+
_value="${_rest#*:}"
|
|
98
|
+
_value="${_value#"${_value%%[![:space:]]*}"}"
|
|
99
|
+
|
|
100
|
+
while ((${#_indents[@]} > 0 && _indents[${#_indents[@]} - 1] >= _indent)); do
|
|
101
|
+
local _last_stack=$(( ${#_stack[@]} - 1 ))
|
|
102
|
+
local _last_indent=$(( ${#_indents[@]} - 1 ))
|
|
103
|
+
unset "_stack[$_last_stack]"
|
|
104
|
+
unset "_indents[$_last_indent]"
|
|
105
|
+
done
|
|
106
|
+
|
|
107
|
+
_full=""
|
|
108
|
+
for ((_i = 0; _i < ${#_stack[@]}; _i++)); do
|
|
109
|
+
[ -n "$_full" ] && _full+="."
|
|
110
|
+
_full+="${_stack[$_i]}"
|
|
111
|
+
done
|
|
112
|
+
[ -n "$_full" ] && _full+="."
|
|
113
|
+
_full+="$_key"
|
|
114
|
+
|
|
115
|
+
if [ -z "$_value" ]; then
|
|
116
|
+
_stack+=("$_key")
|
|
117
|
+
_indents+=("$_indent")
|
|
118
|
+
continue
|
|
119
|
+
fi
|
|
120
|
+
|
|
121
|
+
if [[ "$_value" == \"*\" && "$_value" == *\" ]]; then
|
|
122
|
+
_value="${_value:1:${#_value}-2}"
|
|
123
|
+
elif [[ "$_value" == \'*\' && "$_value" == *\' ]]; then
|
|
124
|
+
_value="${_value:1:${#_value}-2}"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
if [ "$_full" = "dev.lock.primary-branch" ] || [ "$_full" = "plugins.dev.lock.primary-branch" ]; then
|
|
128
|
+
[ "$_value" = "true" ] && return 0
|
|
129
|
+
fi
|
|
130
|
+
done < "$_file"
|
|
131
|
+
|
|
132
|
+
return 1
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_primary_branch_switch_verdict() {
|
|
136
|
+
local _cmd="$1"
|
|
137
|
+
read -ra _pb_toks <<<"$_cmd"
|
|
138
|
+
local _pb_n=${#_pb_toks[@]} _pb_i _pb_j
|
|
139
|
+
for ((_pb_i = 0; _pb_i < _pb_n; _pb_i++)); do
|
|
140
|
+
[ "${_pb_toks[_pb_i]}" = "git" ] || continue
|
|
141
|
+
local _pb_sub="${_pb_toks[_pb_i + 1]:-}"
|
|
142
|
+
case "$_pb_sub" in
|
|
143
|
+
worktree)
|
|
144
|
+
echo "allow"; return 0 ;;
|
|
145
|
+
checkout|switch)
|
|
146
|
+
local _pb_target="" _pb_sawdd=0
|
|
147
|
+
for ((_pb_j = _pb_i + 2; _pb_j < _pb_n; _pb_j++)); do
|
|
148
|
+
local _pb_t="${_pb_toks[_pb_j]}"
|
|
149
|
+
if [ "$_pb_t" = "--" ]; then _pb_sawdd=1; continue; fi
|
|
150
|
+
if [ "$_pb_t" = "-" ]; then _pb_target="-"; break; fi
|
|
151
|
+
case "$_pb_t" in -*) continue ;; esac
|
|
152
|
+
_pb_target="$_pb_t"; break
|
|
153
|
+
done
|
|
154
|
+
[ "$_pb_sawdd" -eq 1 ] && { echo "allow"; return 0; }
|
|
155
|
+
[ -z "$_pb_target" ] && { echo "allow"; return 0; }
|
|
156
|
+
[ "$_pb_target" = "." ] && { echo "allow"; return 0; }
|
|
157
|
+
echo "block"; return 0 ;;
|
|
158
|
+
esac
|
|
159
|
+
done
|
|
160
|
+
echo "allow"; return 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if _dev_lock_primary_branch_enabled "$ROOT/.red/config.yaml" &&
|
|
164
|
+
[ "$(_primary_branch_switch_verdict "$COMMAND")" = "block" ]; then
|
|
165
|
+
cat >&2 <<EOF
|
|
166
|
+
BLOCKED by primary branch guard: dev.lock.primary-branch is true.
|
|
167
|
+
The command '$COMMAND' would switch the agent's primary checkout branch.
|
|
168
|
+
|
|
169
|
+
Allowed in the primary checkout: git commit, git worktree add, read-only git,
|
|
170
|
+
and non-branch-changing commands. To work on another branch, create/use a
|
|
171
|
+
worktree under .red/tmp/worktrees/manual/<slug> or ask the user to change the primary branch.
|
|
172
|
+
EOF
|
|
173
|
+
exit 2
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Read the lock target (first line, trailing whitespace stripped). Absent or
|
|
177
|
+
# empty file means unlocked — nothing more to enforce.
|
|
178
|
+
LOCKFILE="$ROOT/.red/tmp/branch-lock.yaml"
|
|
179
|
+
[ -s "$LOCKFILE" ] || exit 0
|
|
180
|
+
IFS= read -r LOCK_BRANCH < "$LOCKFILE"
|
|
181
|
+
LOCK_BRANCH="${LOCK_BRANCH%"${LOCK_BRANCH##*[![:space:]]}"}"
|
|
182
|
+
[ -z "$LOCK_BRANCH" ] && exit 0
|
|
183
|
+
|
|
184
|
+
# Classify the command against the lock. Scans the token stream for a `git` token
|
|
185
|
+
# followed by a recognised subcommand, so compound commands (`cd x && git switch
|
|
186
|
+
# y`) are still caught. Only the branch-leaving / work-loss family blocks; the
|
|
187
|
+
# work-loss members already caught by the dangerous patterns above are harmless
|
|
188
|
+
# duplicates here.
|
|
189
|
+
read -ra _toks <<<"$COMMAND"
|
|
190
|
+
_n=${#_toks[@]}
|
|
191
|
+
_verdict="allow"
|
|
192
|
+
for ((_i = 0; _i < _n; _i++)); do
|
|
193
|
+
[ "${_toks[_i]}" = "git" ] || continue
|
|
194
|
+
_sub="${_toks[_i + 1]:-}"
|
|
195
|
+
case "$_sub" in
|
|
196
|
+
worktree)
|
|
197
|
+
_verdict="allow"; break ;;
|
|
198
|
+
stash)
|
|
199
|
+
_op="${_toks[_i + 2]:-}"
|
|
200
|
+
if [ -z "$_op" ] || [ "$_op" = "push" ] || [ "$_op" = "save" ]; then
|
|
201
|
+
_verdict="block"
|
|
202
|
+
fi
|
|
203
|
+
break ;;
|
|
204
|
+
checkout|switch)
|
|
205
|
+
_target=""; _sawdd=0
|
|
206
|
+
for ((_j = _i + 2; _j < _n; _j++)); do
|
|
207
|
+
_t="${_toks[_j]}"
|
|
208
|
+
if [ "$_t" = "--" ]; then _sawdd=1; continue; fi
|
|
209
|
+
if [ "$_t" = "-" ]; then _target="-"; break; fi # `switch -` = previous branch
|
|
210
|
+
case "$_t" in -*) continue ;; esac # skip flags (-b, -c, --track, …)
|
|
211
|
+
_target="$_t"; break
|
|
212
|
+
done
|
|
213
|
+
if [ "$_target" = "." ]; then _verdict="block"; break; fi # whole-tree restore (already a pattern)
|
|
214
|
+
[ "$_sawdd" -eq 1 ] && break # file restore: allow
|
|
215
|
+
[ -z "$_target" ] && break # bare checkout: allow
|
|
216
|
+
[ "$_target" = "$LOCK_BRANCH" ] && break # back to the lock target: allow
|
|
217
|
+
_verdict="block"; break ;;
|
|
218
|
+
esac
|
|
219
|
+
done
|
|
220
|
+
|
|
221
|
+
if [ "$_verdict" = "block" ]; then
|
|
222
|
+
cat >&2 <<EOF
|
|
223
|
+
BLOCKED by branch lock: this session is locked to '$LOCK_BRANCH'.
|
|
224
|
+
The command '$COMMAND' would switch the agent away from the locked branch or
|
|
225
|
+
shelve the working tree. To change or release the lock, ask the user — they
|
|
226
|
+
drive it with '/branch-lock <branch>' or '/branch-lock clear'.
|
|
227
|
+
EOF
|
|
228
|
+
exit 2
|
|
229
|
+
fi
|
|
230
|
+
|
|
231
|
+
exit 0
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Unit test for block-dangerous-git.sh — the git-guardrails PreToolUse(Bash) hook
|
|
3
|
+
# (issue #65: make git-guardrails lock-aware, independent, no dependency).
|
|
4
|
+
#
|
|
5
|
+
# Two responsibilities, exercised end-to-end against throwaway repos:
|
|
6
|
+
# 1. The always-on dangerous-pattern block (push, reset --hard, …) is
|
|
7
|
+
# unchanged and lock-independent.
|
|
8
|
+
# 2. When a branch lock is active in the primary checkout, the hook also blocks
|
|
9
|
+
# the branch-leaving / work-loss family — the same verdicts the branch-lock
|
|
10
|
+
# hook reaches — so git-guardrails alone enforces the lock (AC1). With both
|
|
11
|
+
# hooks installed the overlap is idempotent: both deny, never conflict (AC2).
|
|
12
|
+
# And the script depends on nothing from the branch-lock skill (AC3).
|
|
13
|
+
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
HERE="$(cd "$(dirname "$0")" && pwd)"
|
|
17
|
+
SCRIPTS="$(dirname "$HERE")"
|
|
18
|
+
HOOK="$SCRIPTS/block-dangerous-git.sh"
|
|
19
|
+
|
|
20
|
+
pass=0
|
|
21
|
+
fail=0
|
|
22
|
+
|
|
23
|
+
expect_eq() {
|
|
24
|
+
local label="$1" expected="$2" actual="$3"
|
|
25
|
+
if [[ "$expected" == "$actual" ]]; then
|
|
26
|
+
echo "PASS $label"; pass=$((pass + 1))
|
|
27
|
+
else
|
|
28
|
+
printf 'FAIL %s\n expected: %q\n actual: %q\n' "$label" "$expected" "$actual"
|
|
29
|
+
fail=$((fail + 1))
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
# A throwaway repo, optionally locked to <branch> via the gitignored lockfile.
|
|
34
|
+
mk_repo() {
|
|
35
|
+
local dir="$1" lock="${2:-}"
|
|
36
|
+
git init -q -b main "$dir"
|
|
37
|
+
git -C "$dir" config user.email t@t.t
|
|
38
|
+
git -C "$dir" config user.name t
|
|
39
|
+
git -C "$dir" commit -q --allow-empty -m init
|
|
40
|
+
git -C "$dir" branch feature
|
|
41
|
+
if [[ -n "$lock" ]]; then
|
|
42
|
+
mkdir -p "$dir/.red/tmp"
|
|
43
|
+
printf '%s\n' "$lock" > "$dir/.red/tmp/branch-lock.yaml"
|
|
44
|
+
fi
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# Run the hook against a Bash command with the project root pinned; echo exit code.
|
|
48
|
+
run_hook() {
|
|
49
|
+
local root="$1" cmd="$2"
|
|
50
|
+
( cd "$root" && CLAUDE_PROJECT_DIR="$root" bash "$HOOK" \
|
|
51
|
+
<<<"{\"tool_input\":{\"command\":\"$cmd\"}}" ) >/dev/null 2>&1
|
|
52
|
+
echo $?
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
# ===========================================================================
|
|
56
|
+
# Always-on dangerous patterns — unchanged, lock-independent.
|
|
57
|
+
# ===========================================================================
|
|
58
|
+
RU="$(mktemp -d)/red-skills"; mk_repo "$RU" # unlocked
|
|
59
|
+
expect_eq "guardrail/push blocked (unlocked)" "2" "$(run_hook "$RU" "git push origin main")"
|
|
60
|
+
expect_eq "guardrail/reset --hard blocked" "2" "$(run_hook "$RU" "git reset --hard")"
|
|
61
|
+
expect_eq "guardrail/clean -fd blocked" "2" "$(run_hook "$RU" "git clean -fd")"
|
|
62
|
+
expect_eq "guardrail/branch -D blocked" "2" "$(run_hook "$RU" "git branch -D feature")"
|
|
63
|
+
expect_eq "guardrail/checkout dot blocked" "2" "$(run_hook "$RU" "git checkout .")"
|
|
64
|
+
expect_eq "guardrail/status allowed" "0" "$(run_hook "$RU" "git status")"
|
|
65
|
+
# Branch switching is NOT a guardrail pattern: allowed when no lock is active.
|
|
66
|
+
expect_eq "no-lock/switch other allowed" "0" "$(run_hook "$RU" "git switch feature")"
|
|
67
|
+
expect_eq "no-lock/checkout -b allowed" "0" "$(run_hook "$RU" "git checkout -b shiny")"
|
|
68
|
+
expect_eq "no-lock/stash allowed" "0" "$(run_hook "$RU" "git stash")"
|
|
69
|
+
|
|
70
|
+
# ===========================================================================
|
|
71
|
+
# Primary branch guard — config flag on, no branch-lock file required.
|
|
72
|
+
# ===========================================================================
|
|
73
|
+
mkdir -p "$RU/.red"
|
|
74
|
+
cat > "$RU/.red/config.yaml" <<'EOF'
|
|
75
|
+
dev:
|
|
76
|
+
lock:
|
|
77
|
+
primary-branch: true
|
|
78
|
+
EOF
|
|
79
|
+
expect_eq "primary-guard/switch other blocked" "2" "$(run_hook "$RU" "git switch feature")"
|
|
80
|
+
expect_eq "primary-guard/checkout other blocked" "2" "$(run_hook "$RU" "git checkout feature")"
|
|
81
|
+
expect_eq "primary-guard/switch -b blocked" "2" "$(run_hook "$RU" "git switch -b shiny")"
|
|
82
|
+
expect_eq "primary-guard/commit allowed" "0" "$(run_hook "$RU" "git commit -m wip")"
|
|
83
|
+
expect_eq "primary-guard/worktree add allowed" "0" "$(run_hook "$RU" "git worktree add ../wt feature")"
|
|
84
|
+
expect_eq "primary-guard/status allowed" "0" "$(run_hook "$RU" "git status")"
|
|
85
|
+
|
|
86
|
+
cat > "$RU/.red/config.yaml" <<'EOF'
|
|
87
|
+
dev:
|
|
88
|
+
lock:
|
|
89
|
+
primary-branch: false
|
|
90
|
+
EOF
|
|
91
|
+
expect_eq "primary-guard/flag off allows switch" "0" "$(run_hook "$RU" "git switch feature")"
|
|
92
|
+
|
|
93
|
+
# ===========================================================================
|
|
94
|
+
# AC1 — lock active in the primary checkout: branch-leaving / work-loss blocked.
|
|
95
|
+
# ===========================================================================
|
|
96
|
+
RL="$(mktemp -d)/red-skills"; mk_repo "$RL" main # locked to main
|
|
97
|
+
expect_eq "AC1/switch other blocked" "2" "$(run_hook "$RL" "git switch feature")"
|
|
98
|
+
expect_eq "AC1/checkout other blocked" "2" "$(run_hook "$RL" "git checkout feature")"
|
|
99
|
+
expect_eq "AC1/checkout -b new blocked" "2" "$(run_hook "$RL" "git checkout -b shiny")"
|
|
100
|
+
expect_eq "AC1/switch - blocked" "2" "$(run_hook "$RL" "git switch -")"
|
|
101
|
+
expect_eq "AC1/stash blocked" "2" "$(run_hook "$RL" "git stash")"
|
|
102
|
+
# Same-branch / legitimate ops still allowed while locked.
|
|
103
|
+
expect_eq "AC1/switch back to lock allowed" "0" "$(run_hook "$RL" "git switch main")"
|
|
104
|
+
expect_eq "AC1/checkout back to lock allowed" "0" "$(run_hook "$RL" "git checkout main")"
|
|
105
|
+
expect_eq "AC1/file restore allowed" "0" "$(run_hook "$RL" "git checkout -- src/app.ts")"
|
|
106
|
+
expect_eq "AC1/status allowed while locked" "0" "$(run_hook "$RL" "git status")"
|
|
107
|
+
expect_eq "AC1/worktree add allowed" "0" "$(run_hook "$RL" "git worktree add ../wt feature")"
|
|
108
|
+
# The guardrail patterns still fire while locked (idempotent with the work-loss family).
|
|
109
|
+
expect_eq "AC1/push blocked while locked" "2" "$(run_hook "$RL" "git push")"
|
|
110
|
+
expect_eq "AC1/reset --hard blocked while locked" "2" "$(run_hook "$RL" "git reset --hard")"
|
|
111
|
+
|
|
112
|
+
# ===========================================================================
|
|
113
|
+
# Scope — registered worktree lanes under .red/tmp/worktrees/ are exempt even with a lock.
|
|
114
|
+
# ===========================================================================
|
|
115
|
+
WT="$(mktemp -d)/red-skills/.red/tmp/worktrees/manual/fix-docs"
|
|
116
|
+
mkdir -p "$(dirname "$WT")"
|
|
117
|
+
mk_repo "$WT" main # lockfile present, but it's a worktree path
|
|
118
|
+
expect_eq "scope/worktree switch other allowed" "0" "$(run_hook "$WT" "git switch feature")"
|
|
119
|
+
mkdir -p "$WT/.red"
|
|
120
|
+
cat > "$WT/.red/config.yaml" <<'EOF'
|
|
121
|
+
dev:
|
|
122
|
+
lock:
|
|
123
|
+
primary-branch: true
|
|
124
|
+
EOF
|
|
125
|
+
expect_eq "scope/worktree primary guard allowed" "0" "$(run_hook "$WT" "git switch feature")"
|
|
126
|
+
# …but a worktree's own dangerous patterns still fire (guardrail is scope-independent).
|
|
127
|
+
expect_eq "scope/worktree push still blocked" "2" "$(run_hook "$WT" "git push")"
|
|
128
|
+
|
|
129
|
+
# ===========================================================================
|
|
130
|
+
# AC3 — no dependency on the branch-lock skill: the script must not source or
|
|
131
|
+
# require any of its files. (Naming the skill in a comment is fine — what would
|
|
132
|
+
# create a dependency is sourcing/dot-including its modules or reaching into its
|
|
133
|
+
# directory.)
|
|
134
|
+
# ===========================================================================
|
|
135
|
+
if grep -qE '(source|^\s*\.)[[:space:]].*(lock-store|scope-resolver|git-command-classifier|branch-lock/)' "$HOOK"; then
|
|
136
|
+
echo "FAIL AC3/no branch-lock dependency (sources a branch-lock module)"; fail=$((fail + 1))
|
|
137
|
+
else
|
|
138
|
+
echo "PASS AC3/no branch-lock dependency"; pass=$((pass + 1))
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
echo
|
|
142
|
+
echo "summary: $pass passed, $fail failed"
|
|
143
|
+
[ "$fail" -eq 0 ]
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
usage() {
|
|
5
|
+
cat <<'EOF'
|
|
6
|
+
usage: create-plugin.sh [--root REPO_ROOT] <plugin-name>
|
|
7
|
+
|
|
8
|
+
Scaffolds a RedSkills plugin and appends it to both marketplace manifests.
|
|
9
|
+
EOF
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
fail() {
|
|
13
|
+
printf 'error: %s\n' "$*" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
|
|
18
|
+
|
|
19
|
+
while [ "$#" -gt 0 ]; do
|
|
20
|
+
case "$1" in
|
|
21
|
+
--root)
|
|
22
|
+
[ "$#" -ge 2 ] || fail "--root requires a path"
|
|
23
|
+
ROOT="$2"
|
|
24
|
+
shift 2
|
|
25
|
+
;;
|
|
26
|
+
-h|--help)
|
|
27
|
+
usage
|
|
28
|
+
exit 0
|
|
29
|
+
;;
|
|
30
|
+
--*)
|
|
31
|
+
fail "unknown option: $1"
|
|
32
|
+
;;
|
|
33
|
+
*)
|
|
34
|
+
break
|
|
35
|
+
;;
|
|
36
|
+
esac
|
|
37
|
+
done
|
|
38
|
+
|
|
39
|
+
[ "$#" -eq 1 ] || {
|
|
40
|
+
usage >&2
|
|
41
|
+
exit 2
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
command -v jq >/dev/null || fail "jq is required"
|
|
45
|
+
|
|
46
|
+
PLUGIN="$1"
|
|
47
|
+
PLUGIN="$(printf '%s' "$PLUGIN" \
|
|
48
|
+
| tr '[:upper:]' '[:lower:]' \
|
|
49
|
+
| sed -E 's/[^a-z0-9]+/-/g; s/^-+//; s/-+$//; s/-+/-/g')"
|
|
50
|
+
|
|
51
|
+
[ -n "$PLUGIN" ] || fail "plugin name normalizes to empty"
|
|
52
|
+
[ "${#PLUGIN}" -le 64 ] || fail "plugin name must be 64 characters or fewer after normalization"
|
|
53
|
+
|
|
54
|
+
ROOT="$(cd "$ROOT" && pwd)"
|
|
55
|
+
PLUGIN_DIR="$ROOT/plugins/$PLUGIN"
|
|
56
|
+
SKILL_NAME="${PLUGIN}-demo"
|
|
57
|
+
SKILL_DIR="$PLUGIN_DIR/skills/core/$SKILL_NAME"
|
|
58
|
+
|
|
59
|
+
[ ! -e "$PLUGIN_DIR" ] || fail "plugin already exists: plugins/$PLUGIN"
|
|
60
|
+
[ -f "$ROOT/.claude-plugin/marketplace.json" ] || fail "missing .claude-plugin/marketplace.json"
|
|
61
|
+
[ -f "$ROOT/.agents/plugins/marketplace.json" ] || fail "missing .agents/plugins/marketplace.json"
|
|
62
|
+
[ -f "$ROOT/.gemini-plugin/marketplace.json" ] || fail "missing .gemini-plugin/marketplace.json"
|
|
63
|
+
|
|
64
|
+
mkdir -p \
|
|
65
|
+
"$PLUGIN_DIR/.claude-plugin" \
|
|
66
|
+
"$PLUGIN_DIR/.codex-plugin" \
|
|
67
|
+
"$PLUGIN_DIR/.gemini-plugin" \
|
|
68
|
+
"$SKILL_DIR"
|
|
69
|
+
|
|
70
|
+
cat > "$PLUGIN_DIR/.claude-plugin/plugin.json" <<EOF
|
|
71
|
+
{
|
|
72
|
+
"name": "$PLUGIN",
|
|
73
|
+
"version": "0.1.0",
|
|
74
|
+
"description": "reddb.io $PLUGIN plugin - repository-local RedSkills extension.",
|
|
75
|
+
"skills": [
|
|
76
|
+
"./skills/core/$SKILL_NAME"
|
|
77
|
+
]
|
|
78
|
+
}
|
|
79
|
+
EOF
|
|
80
|
+
|
|
81
|
+
cat > "$PLUGIN_DIR/.codex-plugin/plugin.json" <<EOF
|
|
82
|
+
{
|
|
83
|
+
"name": "$PLUGIN",
|
|
84
|
+
"version": "0.1.0",
|
|
85
|
+
"description": "reddb.io $PLUGIN plugin - repository-local RedSkills extension.",
|
|
86
|
+
"author": {
|
|
87
|
+
"name": "reddb.io",
|
|
88
|
+
"url": "https://github.com/reddb-io"
|
|
89
|
+
},
|
|
90
|
+
"homepage": "https://github.com/reddb-io/red-skills",
|
|
91
|
+
"repository": "https://github.com/reddb-io/red-skills",
|
|
92
|
+
"license": "Apache-2.0",
|
|
93
|
+
"keywords": [
|
|
94
|
+
"codex",
|
|
95
|
+
"claude-code",
|
|
96
|
+
"skills",
|
|
97
|
+
"agents"
|
|
98
|
+
],
|
|
99
|
+
"skills": "./skills/",
|
|
100
|
+
"interface": {
|
|
101
|
+
"displayName": "$PLUGIN",
|
|
102
|
+
"shortDescription": "Repository-local RedSkills extension.",
|
|
103
|
+
"longDescription": "The $PLUGIN plugin is a repository-local RedSkills extension scaffolded with born-compliant marketplace metadata, skill structure, documentation, changelog, and structural smoke checks.",
|
|
104
|
+
"developerName": "reddb.io",
|
|
105
|
+
"category": "Developer Tools",
|
|
106
|
+
"capabilities": [
|
|
107
|
+
"Interactive",
|
|
108
|
+
"Read",
|
|
109
|
+
"Shell"
|
|
110
|
+
],
|
|
111
|
+
"websiteURL": "https://github.com/reddb-io/red-skills",
|
|
112
|
+
"defaultPrompt": [
|
|
113
|
+
"Run \$$SKILL_NAME to verify the $PLUGIN plugin is available."
|
|
114
|
+
],
|
|
115
|
+
"brandColor": "#2563EB"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
EOF
|
|
119
|
+
|
|
120
|
+
cat > "$PLUGIN_DIR/.gemini-plugin/plugin.json" <<EOF
|
|
121
|
+
{
|
|
122
|
+
"name": "$PLUGIN",
|
|
123
|
+
"version": "0.1.0",
|
|
124
|
+
"description": "reddb.io $PLUGIN plugin - repository-local RedSkills extension.",
|
|
125
|
+
"author": {
|
|
126
|
+
"name": "reddb.io",
|
|
127
|
+
"url": "https://github.com/reddb-io"
|
|
128
|
+
},
|
|
129
|
+
"homepage": "https://github.com/reddb-io/red-skills",
|
|
130
|
+
"repository": "https://github.com/reddb-io/red-skills",
|
|
131
|
+
"license": "Apache-2.0",
|
|
132
|
+
"keywords": [
|
|
133
|
+
"gemini-cli",
|
|
134
|
+
"skills",
|
|
135
|
+
"agents"
|
|
136
|
+
],
|
|
137
|
+
"skills": [
|
|
138
|
+
"./skills/core/$SKILL_NAME/"
|
|
139
|
+
],
|
|
140
|
+
"interface": {
|
|
141
|
+
"displayName": "$PLUGIN",
|
|
142
|
+
"shortDescription": "Repository-local RedSkills extension.",
|
|
143
|
+
"longDescription": "The $PLUGIN plugin is a repository-local RedSkills extension scaffolded with born-compliant marketplace metadata, skill structure, documentation, changelog, and structural smoke checks.",
|
|
144
|
+
"developerName": "reddb.io",
|
|
145
|
+
"category": "Developer Tools",
|
|
146
|
+
"capabilities": [
|
|
147
|
+
"Interactive",
|
|
148
|
+
"Read",
|
|
149
|
+
"Shell"
|
|
150
|
+
],
|
|
151
|
+
"websiteURL": "https://github.com/reddb-io/red-skills",
|
|
152
|
+
"defaultPrompt": [
|
|
153
|
+
"Run \$$SKILL_NAME to verify the $PLUGIN plugin is available."
|
|
154
|
+
],
|
|
155
|
+
"brandColor": "#2563EB"
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
EOF
|
|
159
|
+
|
|
160
|
+
cat > "$SKILL_DIR/SKILL.md" <<EOF
|
|
161
|
+
---
|
|
162
|
+
name: $SKILL_NAME
|
|
163
|
+
working-mode: interactive
|
|
164
|
+
description: Use when verifying that the scaffolded $PLUGIN plugin is installed, discoverable, and ready for repository-specific implementation.
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
# $SKILL_NAME
|
|
168
|
+
|
|
169
|
+
**Verify the scaffold before adding domain behavior.** Keep this seed skill
|
|
170
|
+
small until the plugin has a real maintainer workflow.
|
|
171
|
+
|
|
172
|
+
<what-to-do>
|
|
173
|
+
|
|
174
|
+
Run the plugin smoke script from the RedSkills repository root:
|
|
175
|
+
|
|
176
|
+
\`\`\`bash
|
|
177
|
+
red-skills-content --check "$ROOT"
|
|
178
|
+
\`\`\`
|
|
179
|
+
|
|
180
|
+
Report the printed result.
|
|
181
|
+
|
|
182
|
+
</what-to-do>
|
|
183
|
+
|
|
184
|
+
<supporting-info>
|
|
185
|
+
|
|
186
|
+
This seed exists so new plugins ship with a valid skill tree on day one. Replace
|
|
187
|
+
it only after the replacement skill keeps the same frontmatter discipline,
|
|
188
|
+
two-section body structure, and marketplace-safe tool grants.
|
|
189
|
+
|
|
190
|
+
</supporting-info>
|
|
191
|
+
EOF
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
cat > "$PLUGIN_DIR/README.md" <<EOF
|
|
195
|
+
# $PLUGIN
|
|
196
|
+
|
|
197
|
+
Repository-local RedSkills plugin scaffolded by the internal create-plugin
|
|
198
|
+
maintainer skill.
|
|
199
|
+
|
|
200
|
+
## Install
|
|
201
|
+
|
|
202
|
+
Install through the RedSkills marketplace after this plugin has been committed
|
|
203
|
+
to the repository and the marketplace manifests have been published.
|
|
204
|
+
|
|
205
|
+
## Skills
|
|
206
|
+
|
|
207
|
+
- \`$SKILL_NAME\` - verifies the scaffolded plugin structure before real
|
|
208
|
+
maintainer workflows are added.
|
|
209
|
+
|
|
210
|
+
## Validation
|
|
211
|
+
|
|
212
|
+
\`\`\`bash
|
|
213
|
+
bash scripts/validate-marketplace-manifests.sh
|
|
214
|
+
bash scripts/lint-skill-frontmatter.sh
|
|
215
|
+
red-skills-content --check "$ROOT"
|
|
216
|
+
\`\`\`
|
|
217
|
+
|
|
218
|
+
## Maintenance
|
|
219
|
+
|
|
220
|
+
Change the internal create-plugin scaffolder before changing this plugin
|
|
221
|
+
contract. Generated plugins are the contract fixture for future plugin work.
|
|
222
|
+
EOF
|
|
223
|
+
|
|
224
|
+
cat > "$PLUGIN_DIR/CHANGES.md" <<EOF
|
|
225
|
+
# CHANGES - $PLUGIN
|
|
226
|
+
|
|
227
|
+
## $PLUGIN
|
|
228
|
+
|
|
229
|
+
- **status**: added
|
|
230
|
+
- **upstream**: none
|
|
231
|
+
- **why**: new repository-local plugin scaffold.
|
|
232
|
+
- **what changed**: created born-compliant plugin manifests, seed skill,
|
|
233
|
+
structural smoke script, README, and marketplace entries.
|
|
234
|
+
EOF
|
|
235
|
+
|
|
236
|
+
tmp="$(mktemp)"
|
|
237
|
+
trap 'rm -f "$tmp"' EXIT
|
|
238
|
+
|
|
239
|
+
jq --arg name "$PLUGIN" --arg source "./plugins/$PLUGIN" --arg description "reddb.io $PLUGIN plugin - repository-local RedSkills extension." '
|
|
240
|
+
if any(.plugins[]?; .name == $name) then
|
|
241
|
+
error("Claude marketplace already contains plugin " + $name)
|
|
242
|
+
else
|
|
243
|
+
.plugins += [{
|
|
244
|
+
"name": $name,
|
|
245
|
+
"source": $source,
|
|
246
|
+
"description": $description
|
|
247
|
+
}]
|
|
248
|
+
end
|
|
249
|
+
' "$ROOT/.claude-plugin/marketplace.json" > "$tmp"
|
|
250
|
+
mv "$tmp" "$ROOT/.claude-plugin/marketplace.json"
|
|
251
|
+
|
|
252
|
+
jq --arg name "$PLUGIN" --arg path "./plugins/$PLUGIN" --arg description "Repository-local RedSkills extension." '
|
|
253
|
+
if any(.plugins[]?; .name == $name) then
|
|
254
|
+
error("Codex marketplace already contains plugin " + $name)
|
|
255
|
+
else
|
|
256
|
+
.plugins += [{
|
|
257
|
+
"name": $name,
|
|
258
|
+
"description": $description,
|
|
259
|
+
"source": {
|
|
260
|
+
"source": "local",
|
|
261
|
+
"path": $path
|
|
262
|
+
},
|
|
263
|
+
"policy": {
|
|
264
|
+
"installation": "AVAILABLE",
|
|
265
|
+
"authentication": "ON_USE"
|
|
266
|
+
},
|
|
267
|
+
"category": "Developer Tools"
|
|
268
|
+
}]
|
|
269
|
+
end
|
|
270
|
+
' "$ROOT/.agents/plugins/marketplace.json" > "$tmp"
|
|
271
|
+
mv "$tmp" "$ROOT/.agents/plugins/marketplace.json"
|
|
272
|
+
|
|
273
|
+
jq --arg name "$PLUGIN" --arg path "./plugins/$PLUGIN" --arg description "Repository-local RedSkills extension." '
|
|
274
|
+
if any(.plugins[]?; .name == $name) then
|
|
275
|
+
error("Gemini marketplace already contains plugin " + $name)
|
|
276
|
+
else
|
|
277
|
+
.plugins += [{
|
|
278
|
+
"name": $name,
|
|
279
|
+
"description": $description,
|
|
280
|
+
"source": {
|
|
281
|
+
"source": "local",
|
|
282
|
+
"path": $path
|
|
283
|
+
},
|
|
284
|
+
"policy": {
|
|
285
|
+
"installation": "AVAILABLE",
|
|
286
|
+
"authentication": "ON_USE"
|
|
287
|
+
},
|
|
288
|
+
"category": "Developer Tools"
|
|
289
|
+
}]
|
|
290
|
+
end
|
|
291
|
+
' "$ROOT/.gemini-plugin/marketplace.json" > "$tmp"
|
|
292
|
+
mv "$tmp" "$ROOT/.gemini-plugin/marketplace.json"
|
|
293
|
+
|
|
294
|
+
if ! grep -Fq "./plugins/$PLUGIN/README.md" "$ROOT/README.md"; then
|
|
295
|
+
cat >> "$ROOT/README.md" <<EOF
|
|
296
|
+
|
|
297
|
+
- [\`$PLUGIN\`](./plugins/$PLUGIN/README.md) - Repository-local RedSkills extension.
|
|
298
|
+
EOF
|
|
299
|
+
fi
|
|
300
|
+
|
|
301
|
+
cat <<EOF
|
|
302
|
+
created plugins/$PLUGIN
|
|
303
|
+
|
|
304
|
+
validate with:
|
|
305
|
+
red-skills-content --check "$ROOT"
|
|
306
|
+
|
|
307
|
+
red-skills-content --check "$ROOT"
|
|
308
|
+
EOF
|