code-foundry 0.1.4

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.
Files changed (50) hide show
  1. package/.editorconfig +18 -0
  2. package/.env.example +3 -0
  3. package/.gitattributes +9 -0
  4. package/.githooks/pre-commit +67 -0
  5. package/.github/CODEOWNERS +4 -0
  6. package/.github/CODE_OF_CONDUCT.md +41 -0
  7. package/.github/CONTRIBUTING.md +198 -0
  8. package/.github/ISSUE_TEMPLATE/bug_report.yml +56 -0
  9. package/.github/ISSUE_TEMPLATE/config.yml +5 -0
  10. package/.github/ISSUE_TEMPLATE/feature_request.yml +38 -0
  11. package/.github/PULL_REQUEST_TEMPLATE.md +29 -0
  12. package/.github/SECURITY.md +23 -0
  13. package/.github/actions/cache/action.yml +43 -0
  14. package/.github/actions/codeql/action.yml +29 -0
  15. package/.github/actions/setup/action.yml +776 -0
  16. package/.github/dependabot.yml +42 -0
  17. package/.github/scripts/bootstrap.sh +20 -0
  18. package/.github/scripts/changed-files.sh +96 -0
  19. package/.github/scripts/ci.sh +636 -0
  20. package/.github/scripts/codeql-languages.sh +86 -0
  21. package/.github/scripts/doctor.sh +103 -0
  22. package/.github/scripts/format-fast-path.sh +80 -0
  23. package/.github/scripts/init-repo.sh +134 -0
  24. package/.github/scripts/security.sh +209 -0
  25. package/.github/scripts/sitecustomize.py +17 -0
  26. package/.github/scripts/sync-codeowners.sh +76 -0
  27. package/.github/scripts/sync-protection.sh +137 -0
  28. package/.github/scripts/sync-template.sh +432 -0
  29. package/.github/scripts/turbo-cache-probe.sh +102 -0
  30. package/.github/template.yml +7 -0
  31. package/.github/template.yml.example +8 -0
  32. package/.github/workflows/ci.yml +166 -0
  33. package/.github/workflows/codeql.yml +160 -0
  34. package/.github/workflows/draft-pr.yml +59 -0
  35. package/.github/workflows/release-pr.yml +66 -0
  36. package/.github/workflows/release.yml +103 -0
  37. package/.github/workflows/security.yml +228 -0
  38. package/.github/workflows/test.yml +175 -0
  39. package/.gitignore +77 -0
  40. package/.mise.toml +6 -0
  41. package/.prettierrc +6 -0
  42. package/AGENTS.md +155 -0
  43. package/CHANGELOG.md +3 -0
  44. package/LICENSE +616 -0
  45. package/NOTICE +7 -0
  46. package/README.md +172 -0
  47. package/package.json +44 -0
  48. package/release-please-config.json +16 -0
  49. package/ruff.toml +6 -0
  50. package/src/cli.mjs +137 -0
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ mode=check
5
+ while [ "$#" -gt 0 ]; do
6
+ case "$1" in
7
+ --check) mode=check; shift ;;
8
+ --apply) mode=apply; shift ;;
9
+ -h|--help)
10
+ printf '%s\n' 'Usage: sync-codeowners.sh [--check|--apply]'
11
+ exit 0
12
+ ;;
13
+ *) printf 'Unknown option: %s\n' "$1" >&2; exit 2 ;;
14
+ esac
15
+ done
16
+
17
+ codeowners_file=.github/CODEOWNERS
18
+ mkdir -p .github
19
+ remote="$(git remote get-url origin 2>/dev/null || true)"
20
+ repo="$(printf '%s\n' "$remote" | sed -E 's#.*github.com[:/]([^/]+/[^/.]+)(\.git)?$#\1#')"
21
+ owners=()
22
+
23
+ if [ -n "$repo" ] && command -v gh >/dev/null 2>&1; then
24
+ owner_json="$(gh repo view "$repo" --json owner 2>/dev/null || true)"
25
+ owner_login="$(jq -r '.owner.login // empty' <<< "$owner_json" 2>/dev/null || true)"
26
+ owner_type="$(jq -r '.owner.type // empty' <<< "$owner_json" 2>/dev/null || true)"
27
+ if [ "$owner_type" = User ] && [ -n "$owner_login" ]; then
28
+ owners+=("@$owner_login")
29
+ fi
30
+ admins="$(gh api --paginate "repos/$repo/collaborators?per_page=100" \
31
+ --jq '.[] | select(.permissions.admin == true) | .login' 2>/dev/null || true)"
32
+ while IFS= read -r admin; do
33
+ [ -n "$admin" ] && owners+=("@$admin")
34
+ done <<< "$admins"
35
+ fi
36
+
37
+ if [ "${#owners[@]}" -gt 0 ]; then
38
+ owner_line="$(printf '%s\n' "${owners[@]}" | awk '!seen[$0]++' | paste -sd ' ' -)"
39
+ else
40
+ owner_line='@OWNER'
41
+ printf '%s\n' 'GitHub owner/admin discovery unavailable; retaining @OWNER placeholder.' >&2
42
+ fi
43
+
44
+ existing_file=""
45
+ if [ -f "$codeowners_file" ]; then
46
+ existing_file="$(mktemp)"
47
+ cp "$codeowners_file" "$existing_file"
48
+ fi
49
+
50
+ if [ "$mode" = check ]; then
51
+ if [ -f "$codeowners_file" ] && grep -q "^\* $owner_line$" "$codeowners_file"; then
52
+ printf '%s\n' 'CODEOWNERS is current.'
53
+ else
54
+ printf 'Would update %s with %s\n' "$codeowners_file" "$owner_line"
55
+ fi
56
+ [ -n "$existing_file" ] && unlink "$existing_file"
57
+ exit 0
58
+ fi
59
+
60
+ tmp_file="$(mktemp)"
61
+ {
62
+ printf '%s\n' '# Managed by code-foundry. Repository owners/admins are discovered during initialization.'
63
+ printf '* %s\n' "$owner_line"
64
+ if [ -n "$existing_file" ]; then
65
+ awk '
66
+ /^# Managed by code-foundry\./ { managed=1; next }
67
+ managed && /^\* / { managed=0; next }
68
+ managed { next }
69
+ /^\* @(OWNER|[^[:space:]]+)/ { next }
70
+ { print }
71
+ ' "$existing_file"
72
+ fi
73
+ } > "$tmp_file"
74
+ mv "$tmp_file" "$codeowners_file"
75
+ [ -n "$existing_file" ] && unlink "$existing_file"
76
+ printf 'Updated %s with %s\n' "$codeowners_file" "$owner_line"
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ branch="main"
5
+ repo=""
6
+ mode="check"
7
+ configured_features="all"
8
+ configured_languages="auto"
9
+
10
+ if [ -f .github/template.yml ]; then
11
+ configured_features="$(awk -F': ' '/^features:/ {print $2; exit}' .github/template.yml)"
12
+ configured_languages="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml)"
13
+ [ -n "$configured_features" ] || configured_features="all"
14
+ [ -n "$configured_languages" ] || configured_languages="auto"
15
+ fi
16
+
17
+ feature_enabled() {
18
+ [ "$configured_features" = all ] && return 0
19
+ case " $(printf '%s' "$configured_features" | tr ',' ' ') " in
20
+ *" $1 "*) return 0 ;;
21
+ *) return 1 ;;
22
+ esac
23
+ }
24
+
25
+ language_enabled() {
26
+ [ "$configured_languages" = auto ] || [ "$configured_languages" = all ] && return 0
27
+ case " $(printf '%s' "$configured_languages" | tr ',' ' ') " in
28
+ *" $1 "*) return 0 ;;
29
+ *) return 1 ;;
30
+ esac
31
+ }
32
+
33
+ usage() {
34
+ printf '%s\n' 'Usage: sync-protection.sh --repo OWNER/REPO [--branch main] [--check] [--apply]'
35
+ }
36
+
37
+ while [ "$#" -gt 0 ]; do
38
+ case "$1" in
39
+ --repo) repo="${2:?missing OWNER/REPO}"; shift 2 ;;
40
+ --branch) branch="${2:?missing branch}"; shift 2 ;;
41
+ --check) mode="check"; shift ;;
42
+ --apply) mode="apply"; shift ;;
43
+ -h|--help) usage; exit 0 ;;
44
+ *) usage >&2; exit 2 ;;
45
+ esac
46
+ done
47
+
48
+ [ -n "$repo" ] || { usage >&2; exit 2; }
49
+ command -v gh >/dev/null 2>&1 || { echo "gh is required" >&2; exit 1; }
50
+
51
+ is_private="$(gh repo view "$repo" --json isPrivate --jq '.isPrivate')"
52
+ code_security_status="disabled"
53
+ if [ "$is_private" = true ]; then
54
+ code_security_status="$(gh api "repos/$repo" --jq '.security_and_analysis.code_security.status // "disabled"' 2>/dev/null || printf 'disabled')"
55
+ fi
56
+ contexts=()
57
+ if feature_enabled ci; then contexts+=(Format Lint Type-Check Build); fi
58
+ if feature_enabled test; then contexts+=(Unit Integration E2E Smoke); fi
59
+ if feature_enabled security; then
60
+ contexts+=(Profile 'Dependency Audit (JavaScript)' 'Dependency Audit (Rust)' 'Dependency Audit (Python)')
61
+ fi
62
+ if feature_enabled codeql; then contexts+=(Detect); fi
63
+
64
+ if { [ "$is_private" != true ] || [ "$code_security_status" = enabled ]; } && feature_enabled codeql; then
65
+ contexts+=("Analyze (Actions)")
66
+ if language_enabled typescript && git ls-files -- '*.ts' '*.tsx' '*.js' '*.jsx' package.json tsconfig\*.json | grep -q .; then contexts+=("Analyze (TypeScript)"); fi
67
+ if language_enabled python && git ls-files -- '*.py' pyproject.toml requirements\*.txt setup.py ':!.github/**' | grep -q .; then contexts+=("Analyze (Python)"); fi
68
+ if language_enabled rust && git ls-files -- '*.rs' Cargo.toml Cargo.lock | grep -q .; then contexts+=("Analyze (Rust)"); fi
69
+ fi
70
+
71
+ protection="$(gh api "repos/$repo/branches/$branch/protection" 2>/dev/null || true)"
72
+ if ! jq -e 'type == "object" and (.required_status_checks | type == "object")' >/dev/null 2>&1 <<< "$protection"; then
73
+ if [ "$mode" = apply ]; then
74
+ echo "Cannot read branch protection for $repo:$branch (repository plan or permissions may not allow it)." >&2
75
+ exit 1
76
+ fi
77
+ existing=""
78
+ else
79
+ existing="$(jq -r '.required_status_checks.contexts[]?' <<< "$protection")"
80
+ fi
81
+ preserved=()
82
+ while IFS= read -r context; do
83
+ [ -n "$context" ] || continue
84
+ case "$context" in
85
+ 'Slither / Analyze') continue ;; # removed from the standard workflow set
86
+ # Remove both the current concise job names and older workflow-prefixed
87
+ # names before rebuilding the standard set. This prevents stale required
88
+ # checks from remaining required after a repository changes visibility or
89
+ # template version.
90
+ 'Dependency Audit'|'Dependency Audit ('*|'Profile'|'Test Profile'|\
91
+ 'Format'|'Lint'|'Type-Check'|'Build'|'Unit'|'Integration'|'E2E'|'Smoke'|\
92
+ 'Detect'|'Analyze'|'Analyze ('*|\
93
+ 'CI / '*|'Test / '*|'Security / '*|'CodeQL / '*) ;;
94
+ *) preserved+=("$context") ;;
95
+ esac
96
+ done <<< "$existing"
97
+
98
+ payload="$(printf '%s\n' "${preserved[@]-}" "${contexts[@]}" | jq -Rsc 'split("\n") | map(select(length > 0)) | unique | {strict: true, contexts: .}')"
99
+
100
+ if [ "$mode" = check ]; then
101
+ printf '%s\n' "$payload" | jq .
102
+ exit 0
103
+ fi
104
+
105
+ current="$protection"
106
+ full_payload="$(jq --argjson checks "$payload" '
107
+ {
108
+ required_status_checks: $checks,
109
+ enforce_admins: .enforce_admins.enabled,
110
+ required_pull_request_reviews: (
111
+ if .required_pull_request_reviews == null then null else {
112
+ dismiss_stale_reviews: .required_pull_request_reviews.dismiss_stale_reviews,
113
+ require_code_owner_reviews: .required_pull_request_reviews.require_code_owner_reviews,
114
+ required_approving_review_count: .required_pull_request_reviews.required_approving_review_count,
115
+ require_last_push_approval: .required_pull_request_reviews.require_last_push_approval
116
+ } end
117
+ ),
118
+ restrictions: (
119
+ if .restrictions == null then null else {
120
+ users: [.restrictions.users[].login],
121
+ teams: [.restrictions.teams[].slug],
122
+ apps: [.restrictions.apps[].slug]
123
+ } end
124
+ ),
125
+ required_linear_history: .required_linear_history.enabled,
126
+ allow_force_pushes: .allow_force_pushes.enabled,
127
+ allow_deletions: .allow_deletions.enabled,
128
+ block_creations: .block_creations.enabled,
129
+ required_conversation_resolution: .required_conversation_resolution.enabled,
130
+ lock_branch: .lock_branch.enabled,
131
+ allow_fork_syncing: .allow_fork_syncing.enabled
132
+ }
133
+ ' <<< "$current")"
134
+
135
+ gh api --method PUT "repos/$repo/branches/$branch/protection" \
136
+ --input - <<< "$full_payload" >/dev/null
137
+ printf 'Updated required checks for %s:%s\n' "$repo" "$branch"
@@ -0,0 +1,432 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ usage() {
5
+ cat <<'EOF'
6
+ Usage: sync-template.sh --source PATH_OR_URL [options]
7
+
8
+ Synchronize the repository-owned baseline without replacing project-specific
9
+ README files, mise tool selections, or additional workflows.
10
+
11
+ Options:
12
+ --languages LIST auto or comma-separated: typescript,rust,python,solidity
13
+ --features LIST all or comma-separated standard features
14
+ --package-manager NAME auto, bun, pnpm, yarn, or npm
15
+ --check Preview changes (default)
16
+ --apply Apply changes
17
+ --prune Remove disabled standard workflows
18
+ EOF
19
+ }
20
+
21
+ source_ref="main"
22
+ mode="check"
23
+ source=""
24
+ temp_dir=""
25
+ languages="auto"
26
+ features="all"
27
+ prune=false
28
+ languages_set=false
29
+ features_set=false
30
+ package_manager=""
31
+ package_manager_set=false
32
+ template_ref=""
33
+ release_type="auto"
34
+ npm_publish="false"
35
+ node_version="24.18.0"
36
+ bun_version="1.3.14"
37
+ python_version="3.13"
38
+ rust_version="1.97.1"
39
+ uv_version="0.11.32"
40
+ ruff_version="0.16.0"
41
+ gitignore_backup=""
42
+ custom_ignores=""
43
+
44
+ valid_languages="typescript rust python solidity"
45
+ valid_features="ci codeql security test draft-pr release-pr release dependabot"
46
+ valid_release_types="auto node python rust simple none"
47
+
48
+ contains_word() {
49
+ case " $1 " in *" $2 "*) return 0 ;; *) return 1 ;; esac
50
+ }
51
+
52
+ normalize_csv() {
53
+ printf '%s' "$1" | tr ',' ' ' | awk '{$1=$1; print}'
54
+ }
55
+
56
+ validate_list() {
57
+ local kind="$1" value="$2" valid="$3" item
58
+ [ "$value" = auto ] || [ "$value" = all ] || {
59
+ for item in $(normalize_csv "$value"); do
60
+ contains_word "$valid" "$item" || { echo "Unsupported $kind: $item" >&2; exit 2; }
61
+ done
62
+ }
63
+ }
64
+
65
+ workflow_enabled() {
66
+ local workflow="$1" list
67
+ [ "$features" = all ] && return 0
68
+ list="$(normalize_csv "$features")"
69
+ contains_word "$list" "$workflow"
70
+ }
71
+
72
+ cleanup() {
73
+ if [ -n "$temp_dir" ]; then rm -rf "$temp_dir"; fi
74
+ if [ -n "$gitignore_backup" ]; then rm -f "$gitignore_backup"; fi
75
+ if [ -n "$custom_ignores" ]; then rm -f "$custom_ignores"; fi
76
+ }
77
+ trap cleanup EXIT
78
+
79
+ while [ "$#" -gt 0 ]; do
80
+ case "$1" in
81
+ --source) source="${2:?missing source path or URL}"; shift 2 ;;
82
+ --ref) source_ref="${2:?missing ref}"; shift 2 ;;
83
+ --languages) languages="${2:?missing language list}"; languages_set=true; shift 2 ;;
84
+ --features) features="${2:?missing feature list}"; features_set=true; shift 2 ;;
85
+ --package-manager) package_manager="${2:?missing package manager}"; package_manager_set=true; shift 2 ;;
86
+ --check) mode="check"; shift ;;
87
+ --apply) mode="apply"; shift ;;
88
+ --prune) prune=true; shift ;;
89
+ -h|--help) usage; exit 0 ;;
90
+ *) usage >&2; exit 2 ;;
91
+ esac
92
+ done
93
+
94
+ [ -n "$source" ] || { usage >&2; exit 2; }
95
+ if [ -f .github/template.yml ]; then
96
+ if [ "$languages_set" = false ]; then
97
+ configured_languages="$(awk -F': ' '/^languages:/ {print $2; exit}' .github/template.yml)"
98
+ [ -n "$configured_languages" ] && languages="$configured_languages"
99
+ fi
100
+ if [ "$features_set" = false ]; then
101
+ configured_features="$(awk -F': ' '/^features:/ {print $2; exit}' .github/template.yml)"
102
+ [ -n "$configured_features" ] && features="$configured_features"
103
+ fi
104
+ if [ "$package_manager_set" = false ]; then
105
+ package_manager="$(awk -F': ' '/^package_manager:/ {print $2; exit}' .github/template.yml)"
106
+ fi
107
+ template_ref="$(awk -F': ' '/^template:/ {print $2; exit}' .github/template.yml)"
108
+ configured_release_type="$(awk -F': ' '/^release_type:/ {print $2; exit}' .github/template.yml)"
109
+ configured_npm_publish="$(awk -F': ' '/^npm_publish:/ {print $2; exit}' .github/template.yml)"
110
+ [ -n "$configured_release_type" ] && release_type="$configured_release_type"
111
+ [ -n "$configured_npm_publish" ] && npm_publish="$configured_npm_publish"
112
+ fi
113
+ [ -n "$package_manager" ] || package_manager=auto
114
+ case "$package_manager" in
115
+ auto|bun|pnpm|yarn|npm) ;;
116
+ *) printf 'Unsupported package manager: %s\n' "$package_manager" >&2; exit 2 ;;
117
+ esac
118
+ validate_list language "$languages" "$valid_languages"
119
+ validate_list feature "$features" "$valid_features"
120
+ contains_word "$valid_release_types" "$release_type" || {
121
+ printf 'Unsupported release type: %s\n' "$release_type" >&2
122
+ exit 2
123
+ }
124
+
125
+ if [ -d "$source" ] && [ -f "$source/.github/scripts/sync-template.sh" ]; then
126
+ template_root="$source"
127
+ else
128
+ command -v git >/dev/null 2>&1 || { echo "git is required" >&2; exit 1; }
129
+ temp_dir="$(mktemp -d)"
130
+ git clone --quiet --depth 1 --branch "$source_ref" "$source" "$temp_dir/template-repo"
131
+ template_root="$temp_dir/template-repo"
132
+ fi
133
+
134
+ if [ -f "$template_root/package.json" ]; then
135
+ template_version="$(awk -F'"' '/"version"[[:space:]]*:/ {print $4; exit}' "$template_root/package.json")"
136
+ [ -n "$template_version" ] && template_ref="code-foundry@$template_version"
137
+ fi
138
+
139
+ files=(
140
+ .editorconfig
141
+ .gitattributes
142
+ .gitignore
143
+ release-please-config.json
144
+ .githooks/pre-commit
145
+ AGENTS.md
146
+ LICENSE
147
+ NOTICE
148
+ ruff.toml
149
+ .prettierrc
150
+ .github/CODEOWNERS
151
+ .github/template.yml.example
152
+ .github/CODE_OF_CONDUCT.md
153
+ .github/CONTRIBUTING.md
154
+ .github/PULL_REQUEST_TEMPLATE.md
155
+ .github/SECURITY.md
156
+ .github/dependabot.yml
157
+ .github/ISSUE_TEMPLATE/bug_report.yml
158
+ .github/ISSUE_TEMPLATE/config.yml
159
+ .github/ISSUE_TEMPLATE/feature_request.yml
160
+ .github/actions/setup/action.yml
161
+ .github/actions/cache/action.yml
162
+ .github/actions/codeql/action.yml
163
+ .github/scripts/bootstrap.sh
164
+ .github/scripts/changed-files.sh
165
+ .github/scripts/ci.sh
166
+ .github/scripts/codeql-languages.sh
167
+ .github/scripts/doctor.sh
168
+ .github/scripts/security.sh
169
+ .github/scripts/sitecustomize.py
170
+ .github/scripts/sync-template.sh
171
+ .github/scripts/init-repo.sh
172
+ .github/scripts/sync-codeowners.sh
173
+ .github/scripts/sync-protection.sh
174
+ .github/workflows/ci.yml
175
+ .github/workflows/codeql.yml
176
+ .github/workflows/draft-pr.yml
177
+ .github/workflows/release-pr.yml
178
+ .github/workflows/release.yml
179
+ .github/workflows/security.yml
180
+ .github/workflows/test.yml
181
+ )
182
+
183
+ filtered_files=()
184
+ for file in "${files[@]}"; do
185
+ case "$file" in
186
+ .github/dependabot.yml) workflow_enabled dependabot && filtered_files+=("$file") ;;
187
+ .github/workflows/ci.yml) workflow_enabled ci && filtered_files+=("$file") ;;
188
+ .github/workflows/codeql.yml) workflow_enabled codeql && filtered_files+=("$file") ;;
189
+ .github/workflows/security.yml) workflow_enabled security && filtered_files+=("$file") ;;
190
+ .github/workflows/test.yml) workflow_enabled test && filtered_files+=("$file") ;;
191
+ .github/workflows/draft-pr.yml) workflow_enabled draft-pr && filtered_files+=("$file") ;;
192
+ .github/workflows/release-pr.yml) workflow_enabled release-pr && filtered_files+=("$file") ;;
193
+ .github/workflows/release.yml) workflow_enabled release && filtered_files+=("$file") ;;
194
+ *) filtered_files+=("$file") ;;
195
+ esac
196
+ done
197
+ files=("${filtered_files[@]}")
198
+
199
+ # Workflows outside the standard baseline are repository-owned extensions.
200
+ # The sync operation never deletes or replaces them; surface them explicitly
201
+ # so maintainers can verify custom deployment, indexing, or security flows.
202
+ standard_workflow() {
203
+ case "$1" in
204
+ ci.yml|codeql.yml|draft-pr.yml|release-pr.yml|release.yml|security.yml|test.yml) return 0 ;;
205
+ *) return 1 ;;
206
+ esac
207
+ }
208
+
209
+ custom_workflows=()
210
+ if [ -d .github/workflows ]; then
211
+ while IFS= read -r workflow; do
212
+ workflow="${workflow#./}"
213
+ workflow="${workflow#.github/workflows/}"
214
+ standard_workflow "$workflow" || custom_workflows+=("$workflow")
215
+ done < <(find .github/workflows -maxdepth 1 -type f -print | sort)
216
+ fi
217
+ if [ "${#custom_workflows[@]}" -gt 0 ]; then
218
+ printf 'Preserving custom workflows: %s\n' "${custom_workflows[*]}"
219
+ fi
220
+
221
+ changed=0
222
+ for file in "${files[@]}"; do
223
+ template_file="$template_root/$file"
224
+ # npm renames .gitignore to .npmignore when installing a package. Treat the
225
+ # renamed file as the same template asset so packaged initialization works.
226
+ if [ "$file" = .gitignore ] && [ ! -f "$template_file" ] && [ -f "$template_root/.npmignore" ]; then
227
+ template_file="$template_root/.npmignore"
228
+ fi
229
+ if [ ! -f "$template_file" ]; then
230
+ echo "Template file missing: $file" >&2
231
+ exit 1
232
+ fi
233
+ if [ "$file" = .github/CODEOWNERS ] && [ -f "$file" ]; then
234
+ continue
235
+ fi
236
+ if [ ! -f "$file" ] || ! cmp -s "$template_file" "$file"; then
237
+ changed=$((changed + 1))
238
+ if [ "$mode" = "check" ]; then
239
+ printf 'Would sync %s\n' "$file"
240
+ else
241
+ mkdir -p "$(dirname "$file")"
242
+ if [ "$file" = .gitignore ] && [ -f "$file" ]; then
243
+ # Keep repository-specific ignore rules while refreshing the shared
244
+ # baseline. This prevents a template sync from hiding generated files
245
+ # or local tooling that only one project uses.
246
+ gitignore_backup="$(mktemp)"
247
+ custom_ignores="$(mktemp)"
248
+ cp "$file" "$gitignore_backup"
249
+ cp "$template_file" "$file"
250
+ awk 'NR == FNR { seen[$0] = 1; next } /^# Repository-specific rules$/ { next } NF && !seen[$0] { print }' \
251
+ "$template_file" "$gitignore_backup" > "$custom_ignores"
252
+ if [ -s "$custom_ignores" ]; then
253
+ {
254
+ printf '\n# Repository-specific rules\n'
255
+ cat "$custom_ignores"
256
+ } >> "$file"
257
+ fi
258
+ rm -f "$gitignore_backup" "$custom_ignores"
259
+ gitignore_backup=""
260
+ custom_ignores=""
261
+ else
262
+ cp "$template_file" "$file"
263
+ fi
264
+ case "$file" in
265
+ .githooks/*|.github/scripts/*) chmod +x "$file" ;;
266
+ esac
267
+ printf 'Synced %s\n' "$file"
268
+ fi
269
+ fi
270
+ done
271
+
272
+ # Release Please owns changelog updates after initialization. Preserve an
273
+ # existing project history, but give new repositories the expected file.
274
+ if [ ! -f CHANGELOG.md ]; then
275
+ changed=$((changed + 1))
276
+ if [ "$mode" = "check" ]; then
277
+ printf '%s\n' 'Would initialize CHANGELOG.md'
278
+ else
279
+ cat > CHANGELOG.md <<'EOF'
280
+ # Changelog
281
+
282
+ All notable changes to this project are documented here.
283
+ EOF
284
+ printf '%s\n' 'Initialized CHANGELOG.md'
285
+ fi
286
+ fi
287
+
288
+ detect_languages() {
289
+ local detected=()
290
+ if [ -f package.json ] || git ls-files -- '*.js' '*.jsx' '*.ts' '*.tsx' | grep -q .; then
291
+ detected+=(typescript)
292
+ fi
293
+ if [ -f Cargo.toml ] || git ls-files -- '*.rs' | grep -q .; then
294
+ detected+=(rust)
295
+ fi
296
+ if [ -f pyproject.toml ] || [ -f requirements.txt ] || [ -f requirements-dev.txt ] ||
297
+ git ls-files -- '*.py' ':!.github/**' | grep -q .; then
298
+ detected+=(python)
299
+ fi
300
+ if find . -path './.git' -prune -o -type f -name '*.sol' -print | grep -q .; then
301
+ detected+=(solidity)
302
+ fi
303
+ printf '%s\n' "${detected[*]}"
304
+ }
305
+
306
+ initialize_mise() {
307
+ local selected_languages="$1"
308
+ local language selected_package_manager
309
+ if [ -f .mise.toml ]; then
310
+ return
311
+ fi
312
+ if [ "$selected_languages" = auto ]; then
313
+ selected_languages="$(detect_languages)"
314
+ fi
315
+ selected_package_manager="$package_manager"
316
+ if [ "$selected_package_manager" = auto ]; then
317
+ if [ -f bun.lock ] || [ -f bun.lockb ]; then selected_package_manager=bun
318
+ elif [ -f pnpm-lock.yaml ]; then selected_package_manager=pnpm
319
+ elif [ -f yarn.lock ]; then selected_package_manager=yarn
320
+ elif [ -f package-lock.json ]; then selected_package_manager=npm
321
+ else selected_package_manager=bun
322
+ fi
323
+ fi
324
+ changed=$((changed + 1))
325
+ if [ "$mode" = check ]; then
326
+ printf '%s\n' 'Would initialize .mise.toml'
327
+ return
328
+ fi
329
+ {
330
+ printf '%s\n' '[tools]'
331
+ for language in $(normalize_csv "$selected_languages"); do
332
+ case "$language" in
333
+ typescript|solidity)
334
+ printf 'node = "%s"\n' "$node_version"
335
+ if [ "$selected_package_manager" = bun ]; then
336
+ printf 'bun = "%s"\n' "$bun_version"
337
+ fi
338
+ ;;
339
+ python)
340
+ printf 'python = "%s"\n' "$python_version"
341
+ printf 'uv = "%s"\n' "$uv_version"
342
+ printf 'ruff = "%s"\n' "$ruff_version"
343
+ ;;
344
+ rust)
345
+ printf 'rust = { version = "%s", components = ["rustfmt", "clippy"] }\n' "$rust_version"
346
+ ;;
347
+ esac
348
+ done | awk '!seen[$0]++'
349
+ printf '\n%s\n' '[settings]'
350
+ printf '%s\n' 'experimental = true'
351
+ } > .mise.toml
352
+ printf '%s\n' 'Initialized .mise.toml'
353
+ }
354
+
355
+ initialize_mise "$languages"
356
+
357
+ initialize_mise_lock() {
358
+ if [ ! -f .mise.toml ] || [ -f mise.lock ]; then
359
+ return
360
+ fi
361
+ if ! command -v mise >/dev/null 2>&1; then
362
+ return
363
+ fi
364
+ changed=$((changed + 1))
365
+ if [ "$mode" = check ]; then
366
+ printf '%s\n' 'Would initialize mise.lock'
367
+ return
368
+ fi
369
+ if MISE_TRUSTED_CONFIG_PATHS="$PWD" mise lock >/dev/null 2>&1; then
370
+ printf '%s\n' 'Initialized mise.lock'
371
+ else
372
+ printf '%s\n' 'Warning: mise.lock could not be generated; CI will resolve pinned tools normally.' >&2
373
+ fi
374
+ }
375
+
376
+ initialize_mise_lock
377
+
378
+ if [ -x .github/scripts/sync-codeowners.sh ]; then
379
+ if [ "$mode" = "check" ]; then
380
+ bash .github/scripts/sync-codeowners.sh --check
381
+ else
382
+ bash .github/scripts/sync-codeowners.sh --apply
383
+ fi
384
+ fi
385
+
386
+ # Release Please's simple strategy needs a version file. Initialize one only
387
+ # for Solidity-only repositories, where no package manifest owns the version.
388
+ if [ ! -f version.txt ] && find . -path './.git' -prune -o -type f -name '*.sol' -print | grep -q . &&
389
+ [ ! -f package.json ] && [ ! -f Cargo.toml ] && [ ! -f pyproject.toml ]; then
390
+ changed=$((changed + 1))
391
+ if [ "$mode" = "check" ]; then
392
+ printf '%s\n' 'Would initialize version.txt'
393
+ else
394
+ printf '%s\n' '0.1.0' > version.txt
395
+ printf '%s\n' 'Initialized version.txt'
396
+ fi
397
+ fi
398
+
399
+ if [ "$mode" = "apply" ]; then
400
+ git config core.hooksPath .githooks
401
+ mkdir -p .github
402
+ {
403
+ printf 'version: 1\n'
404
+ if [ -n "$template_ref" ]; then
405
+ printf 'template: %s\n' "$template_ref"
406
+ fi
407
+ printf 'languages: %s\n' "$languages"
408
+ printf 'features: %s\n' "$features"
409
+ if [ -n "$package_manager" ]; then
410
+ printf 'package_manager: %s\n' "$package_manager"
411
+ fi
412
+ printf 'release_type: %s\n' "$release_type"
413
+ printf 'npm_publish: %s\n' "$npm_publish"
414
+ } > .github/template.yml
415
+ fi
416
+
417
+ if [ "$prune" = true ]; then
418
+ for workflow in ci codeql security test draft-pr release-pr release; do
419
+ file=".github/workflows/$workflow.yml"
420
+ if [ -f "$file" ] && ! workflow_enabled "$workflow"; then
421
+ changed=$((changed + 1))
422
+ if [ "$mode" = "check" ]; then
423
+ printf 'Would remove disabled standard workflow %s\n' "$file"
424
+ else
425
+ rm "$file"
426
+ printf 'Removed disabled standard workflow %s\n' "$file"
427
+ fi
428
+ fi
429
+ done
430
+ fi
431
+
432
+ printf '%s\n' "$changed baseline file(s) differ."