After the templates are copied, create-project.sh adds /.claude, /.agents and /AGENTS.md to .git/info/exclude of the new project as its own step, "Git excludes". No .gitignore or tracked file changes and nothing is committed. The step is skipped when the framework steps are skipped, and a path git already tracks stays tracked and is named in the step. The .git/info/exclude code of exclude_env_file moves into the shared helper exclude_from_git (git.sh), which both now use; the .env behavior is unchanged. Refs #66 Task: MIL-009#2
204 lines
8.1 KiB
Bash
204 lines
8.1 KiB
Bash
# shellcheck shell=bash
|
|
# shellcheck disable=SC2004,SC2034,SC2154 # shared state and arrays are declared in constants.sh
|
|
# framework.sh - The SQA-QC-Framework in the new project: submodule, skills, hooks and templates.
|
|
#
|
|
# Part of create-project.sh: sourced by it, never run on its own.
|
|
#
|
|
# Provides: is_framework_skipped, init_framework_submodules, add_framework, run_framework_script, install_skills, install_hooks, install_framework, copy_template, copy_templates, exclude_framework_files
|
|
|
|
# is_framework_skipped: succeed when the framework steps are left out because
|
|
# SSH to Gitea is not available (the Maintainer agreed to that).
|
|
is_framework_skipped() {
|
|
[[ ${STATE[skip_framework]:-0} == 1 ]]
|
|
}
|
|
|
|
# init_framework_submodules DIR: fetch the submodules the framework holds
|
|
# itself (the qc checklists). Without a submodule of its own this changes
|
|
# nothing. A failure names the command to run by hand.
|
|
init_framework_submodules() {
|
|
local dir="$1" err
|
|
make_temp_file
|
|
err="$REPLY"
|
|
if ! git_project "$dir" submodule update -q --init --recursive 2>"$err"; then
|
|
die "the framework was added but git could not fetch its own submodules (the qc checklists). Run in $dir: git submodule update --init --recursive. Git said: $(head -n 2 "$err" | tr '\n' ' ')"
|
|
fi
|
|
}
|
|
|
|
# add_framework: git submodule add of the framework as "framework". SSH is
|
|
# needed for it; a failure says how to test the access.
|
|
add_framework() {
|
|
local label="Framework" dir="${PROJECT[directory]}" url err existing
|
|
url="$(framework_url)"
|
|
if is_framework_skipped; then
|
|
finish_step "$label" "skipped" "(no SSH access to Gitea)"
|
|
return 0
|
|
fi
|
|
begin_step "$label"
|
|
if [[ -e $dir/framework ]]; then
|
|
existing="$(git_project "$dir" config -f .gitmodules --get submodule.framework.url || true)"
|
|
if [[ $existing != "$url" ]]; then
|
|
die "'framework' already exists in $dir and is not the framework submodule ($url)"
|
|
fi
|
|
init_framework_submodules "$dir"
|
|
finish_step "$label" "reused" "$url (already a submodule)"
|
|
return 0
|
|
fi
|
|
make_temp_file
|
|
err="$REPLY"
|
|
if ! git_project "$dir" submodule add -q "$url" framework 2>"$err"; then
|
|
die "git could not add the framework from $url. Check the SSH access first: ssh -p ${CONFIG[GITEA_SSH_PORT]} -T git@$(gitea_host). Git said: $(head -n 2 "$err" | tr '\n' ' ')"
|
|
fi
|
|
init_framework_submodules "$dir"
|
|
finish_step "$label" "created" "$url"
|
|
}
|
|
|
|
# run_framework_script DIR SCRIPT [ARG...]: run one of the framework's own
|
|
# scripts inside the project. PROJECT_ROOT is set explicitly so that a
|
|
# PROJECT_ROOT in the caller's environment cannot point it elsewhere.
|
|
run_framework_script() {
|
|
local dir="$1" script="$2" out abs
|
|
shift 2
|
|
abs="$(cd "$dir" && pwd)"
|
|
make_temp_file
|
|
out="$REPLY"
|
|
if ! (cd "$abs" && env PROJECT_ROOT="$abs" bash "framework/scripts/$script" "$@" </dev/null) >"$out" 2>&1; then
|
|
die "the framework script $script failed: $(tail -n 3 "$out" | tr '\n' ' ')"
|
|
fi
|
|
}
|
|
|
|
# install_skills DIR: install the framework's skills once. The installer is
|
|
# safe to repeat but copies everything again, so a project that already has
|
|
# them is left alone.
|
|
install_skills() {
|
|
local dir="$1"
|
|
if [[ -f $dir/.agents/skills/.framework-skills && -f $dir/.claude/skills/.framework-skills ]]; then
|
|
STATE[skills_note]="skills already installed"
|
|
return 0
|
|
fi
|
|
run_framework_script "$dir" install-skills.sh
|
|
STATE[skills_note]="skills installed"
|
|
}
|
|
|
|
# install_hooks DIR: point core.hooksPath at the framework's hooks, and turn
|
|
# on the plan gate if chosen. A different hooks path that is already set is
|
|
# only replaced after a yes.
|
|
install_hooks() {
|
|
local dir="$1" current global gate=() gate_enabled
|
|
if ((PROJECT[is_plan_gate_enabled])); then
|
|
gate=(--enable-plan-gate)
|
|
fi
|
|
# Both settings are normally unset; git config exits 1 then.
|
|
current="$(git_project "$dir" config --local --get core.hooksPath || true)"
|
|
global="$(git_project "$dir" config --global --get core.hooksPath || true)"
|
|
gate_enabled="$(git_project "$dir" config --local --get planGate.enabled || true)"
|
|
if [[ -z $current && -n $global ]]; then
|
|
warn "your global core.hooksPath is '$global'; this project sets its own, which takes precedence here"
|
|
fi
|
|
if [[ -z $current ]]; then
|
|
run_framework_script "$dir" install-git-hooks.sh "${gate[@]}"
|
|
STATE[hooks_note]="hooks installed"
|
|
elif [[ $current == framework/githooks ]]; then
|
|
STATE[hooks_note]="hooks already installed"
|
|
if ((PROJECT[is_plan_gate_enabled])) && [[ $gate_enabled != true ]]; then
|
|
run_framework_script "$dir" install-git-hooks.sh "${gate[@]}"
|
|
fi
|
|
else
|
|
prompt_yes_no "core.hooksPath is already '$current'. Replace it with framework/githooks" n
|
|
if ((REPLY)); then
|
|
run_framework_script "$dir" install-git-hooks.sh "${gate[@]}"
|
|
STATE[hooks_note]="hooks installed (replaced '$current')"
|
|
else
|
|
STATE[hooks_note]="kept the existing hooks path '$current'"
|
|
if ((PROJECT[is_plan_gate_enabled])); then
|
|
warn "the plan gate is not enabled because the framework hooks were not installed"
|
|
fi
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# install_framework: skills, then hooks (and the plan gate). Each is done once.
|
|
install_framework() {
|
|
local label="Skills and hooks" dir="${PROJECT[directory]}" gate_state="plan gate off"
|
|
if is_framework_skipped; then
|
|
finish_step "$label" "skipped" "(no SSH access to Gitea)"
|
|
return 0
|
|
fi
|
|
begin_step "$label"
|
|
install_skills "$dir"
|
|
install_hooks "$dir"
|
|
if [[ $(git_project "$dir" config --local --get planGate.enabled || true) == true ]]; then
|
|
gate_state="plan gate on"
|
|
fi
|
|
finish_step "$label" "created" "(${STATE[skills_note]}; ${STATE[hooks_note]}; $gate_state)"
|
|
}
|
|
|
|
# copy_template DIR SOURCE TARGET: copy a framework template. An existing
|
|
# target is only replaced after a yes. The result goes to STATE[template_note].
|
|
copy_template() {
|
|
local dir="$1" source="$2" target="$3"
|
|
if [[ ! -f $dir/$source ]]; then
|
|
die "the framework has no $source; is the submodule complete?"
|
|
fi
|
|
if [[ -e $dir/$target ]]; then
|
|
prompt_yes_no "$target already exists. Replace it with the framework template" n
|
|
if ! ((REPLY)); then
|
|
STATE[template_note]="kept existing $target"
|
|
return 0
|
|
fi
|
|
fi
|
|
cp -- "$dir/$source" "$dir/$target"
|
|
STATE[template_note]="copied $target"
|
|
}
|
|
|
|
# copy_templates: AGENTS.md and docs/artifact-registry.md from the framework.
|
|
copy_templates() {
|
|
local label="Templates" dir="${PROJECT[directory]}" notes=""
|
|
if is_framework_skipped; then
|
|
finish_step "$label" "skipped" "(no SSH access to Gitea)"
|
|
return 0
|
|
fi
|
|
begin_step "$label"
|
|
mkdir -p -- "$dir/docs"
|
|
copy_template "$dir" framework/templates/AGENTS-template.md AGENTS.md
|
|
notes="${STATE[template_note]}"
|
|
copy_template "$dir" framework/templates/artifact-registry-template.md docs/artifact-registry.md
|
|
notes="$notes; ${STATE[template_note]}"
|
|
finish_step "$label" "created" "($notes)"
|
|
}
|
|
|
|
# exclude_framework_files: make git ignore .claude, .agents and AGENTS.md in
|
|
# the new project, through its .git/info/exclude (never .gitignore, so no
|
|
# tracked file changes and nothing is committed). Only when the framework
|
|
# steps ran. A path git already tracks stays tracked: an exclusion does not
|
|
# apply to it, so the step names it.
|
|
exclude_framework_files() {
|
|
local label="Git excludes" dir="${PROJECT[directory]}" entry path tracked=() note written
|
|
if is_framework_skipped; then
|
|
finish_step "$label" "skipped" "(no SSH access to Gitea)"
|
|
return 0
|
|
fi
|
|
begin_step "$label"
|
|
exclude_from_git "$dir" "RepoFoundry: the files installed from the framework" "${FRAMEWORK_EXCLUDES[@]}" ||
|
|
die "could not make git ignore the framework files in $dir; check $dir/.git/info/exclude"
|
|
written="$REPLY"
|
|
for entry in "${FRAMEWORK_EXCLUDES[@]}"; do
|
|
path="${entry#/}"
|
|
if [[ -n "$(git_project "$dir" ls-files -- "$path")" ]]; then
|
|
tracked+=("$path")
|
|
fi
|
|
done
|
|
if ((written == 0)); then
|
|
note="${FRAMEWORK_EXCLUDES[*]} already excluded"
|
|
else
|
|
note="${FRAMEWORK_EXCLUDES[*]} added to .git/info/exclude"
|
|
fi
|
|
if ((${#tracked[@]})); then
|
|
note="$note; git tracks ${tracked[*]}, so it is not ignored"
|
|
fi
|
|
if ((written == 0)); then
|
|
finish_step "$label" "reused" "($note)"
|
|
else
|
|
finish_step "$label" "created" "($note)"
|
|
fi
|
|
}
|