diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index c9d7b96..9e7170f 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -43,6 +43,7 @@ The following files in `.machine_readable/` contain structured project metadata: | Bun | Deno | | pnpm/yarn | Deno | | Go | Rust | +| V (vlang) | Zig/Rust | | Python | Julia/Rust/ReScript | | Java/Kotlin | Rust/Tauri/Dioxus | | Swift | Tauri/Dioxus | diff --git a/.github/workflows/dogfood-gate.yml b/.github/workflows/dogfood-gate.yml index 4cdcaa0..e44f42c 100644 --- a/.github/workflows/dogfood-gate.yml +++ b/.github/workflows/dogfood-gate.yml @@ -136,7 +136,7 @@ jobs: -type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \ -o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \ -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \ - -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \ + -o -name '*.idr' -o -name '*.zig' -o -name '*.jl' \ -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \ -exec grep -Prl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null EL_EXIT=$? @@ -203,13 +203,13 @@ jobs: fi # Check for Groove endpoint code (Rust, Elixir, Zig, V) - if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then + if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then HAS_GROOVE_CODE="true" fi # Check if this repo likely serves HTTP (has server/listener code) HAS_SERVER="false" - if grep -rl 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' . 2>/dev/null | head -1 | grep -q .; then + if grep -rl 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --include='*.rs' --include='*.ex' --include='*.zig' . 2>/dev/null | head -1 | grep -q .; then HAS_SERVER="true" fi diff --git a/api/v/reposystem.v b/api/v/reposystem.v deleted file mode 100644 index 36f6875..0000000 --- a/api/v/reposystem.v +++ /dev/null @@ -1,39 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -// -// Reposystem V-lang API — Repository management client. -module reposystem - -pub enum Forge { - github - gitlab - bitbucket - codeberg -} - -pub enum ComplianceLevel { - non_compliant - partial - full -} - -pub struct RepoHealth { -pub: - name string - forge Forge - score int // 0-100, clamped by ABI - compliance ComplianceLevel -} - -fn C.reposystem_clamp_health(score int) int -fn C.reposystem_valid_forge(forge int) int - -// clamp_health ensures a health score is within bounds [0, 100]. -pub fn clamp_health(score int) int { - return C.reposystem_clamp_health(score) -} - -// is_valid_forge checks if a forge identifier is known. -pub fn is_valid_forge(forge Forge) bool { - return C.reposystem_valid_forge(int(forge)) == 1 -} diff --git a/rpa-elysium/.github/workflows/dogfood-gate.yml b/rpa-elysium/.github/workflows/dogfood-gate.yml index 700b9ba..8430cdb 100644 --- a/rpa-elysium/.github/workflows/dogfood-gate.yml +++ b/rpa-elysium/.github/workflows/dogfood-gate.yml @@ -136,7 +136,7 @@ jobs: -type f \( -name '*.rs' -o -name '*.ex' -o -name '*.exs' -o -name '*.res' \ -o -name '*.js' -o -name '*.ts' -o -name '*.json' -o -name '*.toml' \ -o -name '*.yml' -o -name '*.yaml' -o -name '*.md' -o -name '*.adoc' \ - -o -name '*.idr' -o -name '*.zig' -o -name '*.v' -o -name '*.jl' \ + -o -name '*.idr' -o -name '*.zig' -o -name '*.jl' \ -o -name '*.gleam' -o -name '*.hs' -o -name '*.ml' -o -name '*.sh' \) \ -exec grep -Prl "$PATTERNS" {} \; > /tmp/empty-lint-results.txt 2>/dev/null EL_EXIT=$? @@ -203,13 +203,13 @@ jobs: fi # Check for Groove endpoint code (Rust, Elixir, Zig, V) - if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then + if grep -rl 'well-known/groove' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.res' . 2>/dev/null | head -1 | grep -q .; then HAS_GROOVE_CODE="true" fi # Check if this repo likely serves HTTP (has server/listener code) HAS_SERVER="false" - if grep -rl 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --include='*.rs' --include='*.ex' --include='*.zig' --include='*.v' . 2>/dev/null | head -1 | grep -q .; then + if grep -rl 'TcpListener\|Bandit\|Plug.Cowboy\|httpz\|vweb\|axum::serve\|actix_web' --include='*.rs' --include='*.ex' --include='*.zig' . 2>/dev/null | head -1 | grep -q .; then HAS_SERVER="true" fi diff --git a/scaffoldia/.claude/CLAUDE.md b/scaffoldia/.claude/CLAUDE.md index 1f18a05..35ba167 100644 --- a/scaffoldia/.claude/CLAUDE.md +++ b/scaffoldia/.claude/CLAUDE.md @@ -43,6 +43,7 @@ The following files in `.machine_readable/` contain structured project metadata: | Bun | Deno | | pnpm/yarn | Deno | | Go | Rust | +| V (vlang) | Zig/Rust | | Python | Julia/Rust/ReScript | | Java/Kotlin | Rust/Tauri/Dioxus | | Swift | Tauri/Dioxus | diff --git a/scaffoldia/repo-batcher/GETTING-STARTED.adoc b/scaffoldia/repo-batcher/GETTING-STARTED.adoc index 78140df..baa6dfc 100644 --- a/scaffoldia/repo-batcher/GETTING-STARTED.adoc +++ b/scaffoldia/repo-batcher/GETTING-STARTED.adoc @@ -15,7 +15,7 @@ This project transforms the bash scripts (`sync_repos.sh`, `robust_sync.sh`) int * [x] Project structure from RSR template * [x] Architecture documentation (link:docs/ARCHITECTURE.adoc[ARCHITECTURE.adoc]) * [x] ATS2 operation type definitions with dependent types (link:src/ats2/operations/types.dats[types.dats]) -* [x] V CLI skeleton with all commands (link:src/v/main.v[main.v]) +* [x] Zig FFI layer (link:ffi/zig/src/main.zig[main.zig]) * [x] Checkpoint files (STATE.scm, ECOSYSTEM.scm, META.scm) * [x] Build system (Justfile) * [x] Configuration templates @@ -30,7 +30,7 @@ This project transforms the bash scripts (`sync_repos.sh`, `robust_sync.sh`) int === 📋 To Do -* [ ] ATS2-V FFI bridge +* [ ] ATS2-Zig FFI bridge * [ ] Watch folder monitoring * [ ] Parallel execution engine * [ ] Rollback system @@ -45,10 +45,8 @@ This project transforms the bash scripts (`sync_repos.sh`, `robust_sync.sh`) int # ATS2 (formal verification) # Download from: http://www.ats-lang.org/Downloads.html -# V (CLI and execution) -git clone https://github.com/vlang/v -cd v && make -sudo ./v symlink +# Zig (FFI and execution) +# Download from: https://ziglang.org/download/ ---- === 2. Build repo-batcher @@ -94,12 +92,8 @@ repo-batcher/ │ │ ├── operations/ # Operation types with proofs │ │ ├── validation/ # Validation logic │ │ └── batch/ # Batch execution engine -│ ├── v/ # V CLI layer -│ │ ├── cli/ # Command-line interface -│ │ ├── watcher/ # Watch folder monitoring -│ │ └── executor/ # Parallel execution -│ ├── abi/ # Idris2 ABI (if needed for FFI) -│ └── ffi/zig/ # Zig FFI bridge (if needed) +│ └── abi/ # Idris2 ABI (if needed for FFI) +├── ffi/zig/ # Zig FFI layer (execution + bridge) ├── templates/ # Operation and config templates ├── watch/ # Drop folder for batch operations └── docs/ # Architecture and operations guide @@ -120,16 +114,15 @@ Key types defined in `src/ats2/operations/types.dats`: * `git_sync_op` - Proves repos are valid git repositories * `batch_result` - Tracks success/failure with dependent types -=== V CLI Layer (src/v/) +=== Zig FFI Layer (ffi/zig/) -Provides **fast parallel execution** with simple interface: +Provides **fast parallel execution** with a simple interface: -* **CLI commands**: Intuitive command structure -* **Parallel execution**: V coroutines for multi-repo operations +* **Parallel execution**: Zig for multi-repo operations * **Watch daemon**: Automatic operation processing * **FFI to ATS2**: Calls formally verified core -Available commands in `src/v/main.v`: +Available commands: * `list-ops` - Show available operations * `license-update` - Update licenses across repos diff --git a/scaffoldia/repo-batcher/Justfile b/scaffoldia/repo-batcher/Justfile index 3ad34be..6c76bcd 100644 --- a/scaffoldia/repo-batcher/Justfile +++ b/scaffoldia/repo-batcher/Justfile @@ -10,15 +10,15 @@ build: @echo "Building repo-batcher..." # Compile ATS2 core cd src/ats2 && patscc -o ../../build/librepobatcher.a operations/*.dats - # Compile V CLI - v -prod -o build/repo-batcher src/v/main.v - @echo "Build complete: build/repo-batcher" + # Build Zig FFI layer + cd ffi/zig && zig build -Doptimize=ReleaseSafe + @echo "Build complete" # Build in development mode (faster, less optimized) build-dev: @echo "Building repo-batcher (dev mode)..." - v -o build/repo-batcher src/v/main.v - @echo "Build complete: build/repo-batcher" + cd ffi/zig && zig build + @echo "Build complete" # Run smoke test (structure validation, no build required) test-smoke: @@ -28,8 +28,7 @@ test-smoke: # Run tests test: @echo "Running tests..." - # V integration tests - v run tests/integration_test.v + cd ffi/zig && zig build test && zig build test-integration @echo "" @echo "All tests passed!" @@ -67,15 +66,12 @@ uninstall: # Format code fmt: - # V has built-in formatter - v fmt -w src/v/ + zig fmt ffi/zig/src/ ffi/zig/test/ # Check code quality check: @echo "Checking ATS2 code..." patsopt -tc -d src/ats2/operations/*.dats - @echo "Checking V code..." - v vet src/v/ # Create example config setup-config: diff --git a/scaffoldia/repo-batcher/docs/ARCHITECTURE.adoc b/scaffoldia/repo-batcher/docs/ARCHITECTURE.adoc index f9d1c6b..0039ded 100644 --- a/scaffoldia/repo-batcher/docs/ARCHITECTURE.adoc +++ b/scaffoldia/repo-batcher/docs/ARCHITECTURE.adoc @@ -415,7 +415,7 @@ See `STATE.scm` for detailed milestones. ---- # Install dependencies # ATS2: http://www.ats-lang.org/Downloads.html -# V: https://github.com/vlang/v +# Zig: https://ziglang.org/download/ # Build cd ~/Documents/hyperpolymath-repos/repo-batcher diff --git a/scaffoldia/repo-batcher/src/v/executor/parallel.v b/scaffoldia/repo-batcher/src/v/executor/parallel.v deleted file mode 100644 index 5528861..0000000 --- a/scaffoldia/repo-batcher/src/v/executor/parallel.v +++ /dev/null @@ -1,292 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Parallel Executor -// V coroutines for parallel repository processing - -module executor - -import time -import sync -import ffi - -// WorkerPool manages parallel execution of operations across repositories -pub struct WorkerPool { -pub mut: - workers int - repos []string - progress int - total int - results []ffi.BatchResult - errors []string - mtx &sync.Mutex - completed_ch chan int -} - -// TaskResult represents result of a single repository operation -struct TaskResult { - repo_path string - result ffi.BatchResult - err string -} - -// Creates new worker pool for parallel execution -pub fn new_worker_pool(repos []string, workers int) &WorkerPool { - return &WorkerPool{ - workers: workers - repos: repos - progress: 0 - total: repos.len - results: []ffi.BatchResult{} - errors: []string{} - mtx: sync.new_mutex() - completed_ch: chan int{cap: repos.len} - } -} - -// Executes git-sync operation across all repositories in parallel -pub fn (mut pool WorkerPool) execute_git_sync(commit_msg string, dry_run bool) ffi.BatchResult { - println('Executing git-sync with ${pool.workers} workers on ${pool.total} repositories...') - println('') - - // Spawn worker coroutines - for i in 0 .. pool.workers { - spawn pool.git_sync_worker(i, commit_msg, dry_run) - } - - // Wait for all workers to complete - pool.wait_for_completion() - - // Aggregate results - return pool.aggregate_results() -} - -// Worker coroutine for git-sync operations -fn (mut pool WorkerPool) git_sync_worker(worker_id int, commit_msg string, dry_run bool) { - for { - // Get next repository to process - repo := pool.get_next_repo() or { break } - - // Call ATS2 formally verified git-sync for single repo - params := ffi.GitSyncParams{ - base_dir: repo - max_depth: 0 // Already at repo level - commit_msg: commit_msg - parallel_jobs: 1 // Single repo at a time - dry_run: dry_run - } - - result := ffi.git_sync(params) - - // Record result - pool.record_result(repo, result) - pool.update_progress() - - // Show progress - if !dry_run || result.has_failures() { - pool.mtx.@lock() - status := if result.is_success() { '✓' } else { '✗' } - println('[${worker_id}] ${status} ${repo} (${pool.progress}/${pool.total})') - pool.mtx.unlock() - } - } - - // Signal completion - pool.completed_ch <- worker_id -} - -// Gets next repository to process (thread-safe) -fn (mut pool WorkerPool) get_next_repo() ?string { - pool.mtx.@lock() - defer { - pool.mtx.unlock() - } - - if pool.repos.len == 0 { - return none - } - - repo := pool.repos[0] - pool.repos = pool.repos[1..] - return repo -} - -// Records operation result (thread-safe) -fn (mut pool WorkerPool) record_result(repo string, result ffi.BatchResult) { - pool.mtx.@lock() - defer { - pool.mtx.unlock() - } - - pool.results << result - if result.has_failures() { - pool.errors << '${repo}: ${result.message}' - } -} - -// Updates progress counter (thread-safe) -fn (mut pool WorkerPool) update_progress() { - pool.mtx.@lock() - defer { - pool.mtx.unlock() - } - - pool.progress++ -} - -// Waits for all workers to complete -fn (mut pool WorkerPool) wait_for_completion() { - completed := 0 - for completed < pool.workers { - <-pool.completed_ch - completed++ - } -} - -// Aggregates all results into single BatchResult -fn (pool WorkerPool) aggregate_results() ffi.BatchResult { - mut total_success := 0 - mut total_failure := 0 - mut total_skipped := 0 - - for result in pool.results { - total_success += result.success_count - total_failure += result.failure_count - total_skipped += result.skipped_count - } - - msg := if pool.errors.len > 0 { - 'Completed with ${pool.errors.len} repository failures' - } else { - 'All repositories processed successfully' - } - - return ffi.BatchResult{ - success_count: total_success - failure_count: total_failure - skipped_count: total_skipped - message: msg - } -} - -// Executes license-update operation across all repositories in parallel -pub fn (mut pool WorkerPool) execute_license_update(old_license string, new_license string, backup bool, dry_run bool) ffi.BatchResult { - println('Executing license-update with ${pool.workers} workers on ${pool.total} repositories...') - println('') - - // Spawn worker coroutines - for i in 0 .. pool.workers { - spawn pool.license_update_worker(i, old_license, new_license, backup, dry_run) - } - - // Wait for all workers to complete - pool.wait_for_completion() - - // Aggregate results - return pool.aggregate_results() -} - -// Worker coroutine for license-update operations -fn (mut pool WorkerPool) license_update_worker(worker_id int, old_license string, new_license string, backup bool, dry_run bool) { - for { - // Get next repository to process - repo := pool.get_next_repo() or { break } - - // Call ATS2 formally verified license-update for single repo - params := ffi.LicenseUpdateParams{ - old_license: old_license - new_license: new_license - base_dir: repo - max_depth: 5 // Deep scan within repo - dry_run: dry_run - backup: backup - } - - result := ffi.license_update(params) - - // Record result - pool.record_result(repo, result) - pool.update_progress() - - // Show progress - pool.mtx.@lock() - status := if result.is_success() { '✓' } else { '✗' } - println('[${worker_id}] ${status} ${repo} (${pool.progress}/${pool.total})') - pool.mtx.unlock() - } - - // Signal completion - pool.completed_ch <- worker_id -} - -// Executes file-replace operation across all repositories in parallel -pub fn (mut pool WorkerPool) execute_file_replace(pattern string, replacement string, backup bool, dry_run bool) ffi.BatchResult { - println('Executing file-replace with ${pool.workers} workers on ${pool.total} repositories...') - println('') - - // Spawn worker coroutines - for i in 0 .. pool.workers { - spawn pool.file_replace_worker(i, pattern, replacement, backup, dry_run) - } - - // Wait for all workers to complete - pool.wait_for_completion() - - // Aggregate results - return pool.aggregate_results() -} - -// Worker coroutine for file-replace operations -fn (mut pool WorkerPool) file_replace_worker(worker_id int, pattern string, replacement string, backup bool, dry_run bool) { - for { - // Get next repository to process - repo := pool.get_next_repo() or { break } - - // Call ATS2 formally verified file-replace for single repo - params := ffi.FileReplaceParams{ - pattern: pattern - replacement: replacement - base_dir: repo - max_depth: 5 - dry_run: dry_run - backup: backup - } - - result := ffi.file_replace(params) - - // Record result - pool.record_result(repo, result) - pool.update_progress() - - // Show progress - pool.mtx.@lock() - status := if result.is_success() { '✓' } else { '✗' } - println('[${worker_id}] ${status} ${repo} (${pool.progress}/${pool.total})') - pool.mtx.unlock() - } - - // Signal completion - pool.completed_ch <- worker_id -} - -// Displays progress bar during execution -pub fn show_progress(current int, total int, width int) { - percent := f32(current) / f32(total) * 100.0 - filled := int(f32(current) / f32(total) * f32(width)) - - mut bar := '[' - for i in 0 .. width { - if i < filled { - bar += '=' - } else if i == filled { - bar += '>' - } else { - bar += ' ' - } - } - bar += '] ${percent:5.1f}% (${current}/${total})' - - print('\r${bar}') - if current == total { - println('') - } -} diff --git a/scaffoldia/repo-batcher/src/v/ffi/ats2_bridge.v b/scaffoldia/repo-batcher/src/v/ffi/ats2_bridge.v deleted file mode 100644 index 17ebc60..0000000 --- a/scaffoldia/repo-batcher/src/v/ffi/ats2_bridge.v +++ /dev/null @@ -1,395 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// ATS2 FFI Bridge -// V bindings for ATS2 formally verified operations - -module ffi - -// C-compatible structures matching ATS2 exports -pub struct CBatchResult { -pub mut: - success_count int - failure_count int - skipped_count int - message &char -} - -pub struct CAuditStats { -pub mut: - total_files int - with_spdx int - without_spdx int - invalid_spdx int - pmpl_license int - other_licenses int -} - -pub struct CAuditResult { -pub mut: - repo_path &char - stats CAuditStats - compliance_percent int -} - -pub struct CAuditResults { -pub mut: - total_repos int - results voidptr -} - -pub struct CGitHubSettings { -pub mut: - // Repository features (-1=none, 0=false, 1=true) - has_issues int - has_wiki int - has_projects int - has_downloads int - // Merge settings - allow_squash_merge int - allow_merge_commit int - allow_rebase_merge int - delete_branch_on_merge int - allow_auto_merge int -} - -// Convert C string to V string -fn c_string_to_v(s &char) string { - if s == 0 { - return '' - } - return unsafe { cstring_to_vstring(s) } -} - -// ========== ATS2 C Function Declarations ========== - -fn C.c_validate_spdx(&char) int - -fn C.c_license_update(&char, &char, &char, int, int, int) CBatchResult - -fn C.c_git_sync(&char, int, &char, int, int) CBatchResult - -fn C.c_file_replace(&char, &char, &char, int, int, int) CBatchResult - -fn C.c_workflow_update(&char, int, int, int) CBatchResult - -fn C.c_spdx_audit(&char, int) CAuditResults - -fn C.c_github_settings(&char, int, CGitHubSettings, int) int - -fn C.c_get_version() &char - -// ========== V-friendly wrappers ========== - -// BatchResult is the V-friendly version of batch results -pub struct BatchResult { -pub mut: - success_count int - failure_count int - skipped_count int - message string -} - -// Validates SPDX identifier -// Returns true if valid, false otherwise -pub fn validate_spdx(license string) bool { - result := C.c_validate_spdx(license.str) - return result == 1 -} - -// LicenseUpdateParams contains parameters for license update -pub struct LicenseUpdateParams { -pub: - old_license string - new_license string - base_dir string - max_depth int - dry_run bool - backup bool -} - -// Performs license update operation -// Calls formally verified ATS2 implementation -pub fn license_update(params LicenseUpdateParams) BatchResult { - dry_run_flag := if params.dry_run { 1 } else { 0 } - backup_flag := if params.backup { 1 } else { 0 } - - c_result := C.c_license_update( - params.old_license.str, - params.new_license.str, - params.base_dir.str, - params.max_depth, - dry_run_flag, - backup_flag - ) - - return BatchResult{ - success_count: c_result.success_count - failure_count: c_result.failure_count - skipped_count: c_result.skipped_count - message: c_string_to_v(c_result.message) - } -} - -// GitSyncParams contains parameters for git sync -pub struct GitSyncParams { -pub: - base_dir string - max_depth int - commit_msg string - parallel_jobs int - dry_run bool -} - -// Performs git batch sync operation -// Calls formally verified ATS2 implementation -// This replaces sync_repos.sh with type-safe version -pub fn git_sync(params GitSyncParams) BatchResult { - dry_run_flag := if params.dry_run { 1 } else { 0 } - - c_result := C.c_git_sync( - params.base_dir.str, - params.max_depth, - params.commit_msg.str, - params.parallel_jobs, - dry_run_flag - ) - - return BatchResult{ - success_count: c_result.success_count - failure_count: c_result.failure_count - skipped_count: c_result.skipped_count - message: c_string_to_v(c_result.message) - } -} - -// FileReplaceParams contains parameters for file replace -pub struct FileReplaceParams { -pub: - pattern string - replacement string - base_dir string - max_depth int - dry_run bool - backup bool -} - -// Performs file replace operation -// Calls formally verified ATS2 implementation -pub fn file_replace(params FileReplaceParams) BatchResult { - dry_run_flag := if params.dry_run { 1 } else { 0 } - backup_flag := if params.backup { 1 } else { 0 } - - c_result := C.c_file_replace( - params.pattern.str, - params.replacement.str, - params.base_dir.str, - params.max_depth, - dry_run_flag, - backup_flag - ) - - return BatchResult{ - success_count: c_result.success_count - failure_count: c_result.failure_count - skipped_count: c_result.skipped_count - message: c_string_to_v(c_result.message) - } -} - -// Gets version from ATS2 core -pub fn get_version() string { - c_version := C.c_get_version() - return c_string_to_v(c_version) -} - -// Prints batch result in human-readable format -pub fn (result BatchResult) print() { - println('\n=== Batch Operation Results ===') - println('Success: ${result.success_count}') - println('Failure: ${result.failure_count}') - println('Skipped: ${result.skipped_count}') - println('Total: ${result.success_count + result.failure_count + result.skipped_count}') - if result.message != '' { - println('\nMessage: ${result.message}') - } - println('') -} - -// Checks if operation was successful -pub fn (result BatchResult) is_success() bool { - return result.failure_count == 0 -} - -// Checks if any operations failed -pub fn (result BatchResult) has_failures() bool { - return result.failure_count > 0 -} - -// WorkflowUpdateParams contains parameters for workflow update -pub struct WorkflowUpdateParams { -pub: - base_dir string - max_depth int - backup bool - dry_run bool -} - -// Performs workflow update with SHA pinning -// Updates GitHub Actions workflows across repositories -pub fn workflow_update(params WorkflowUpdateParams) BatchResult { - dry_run_flag := if params.dry_run { 1 } else { 0 } - backup_flag := if params.backup { 1 } else { 0 } - - c_result := C.c_workflow_update( - params.base_dir.str, - params.max_depth, - backup_flag, - dry_run_flag - ) - - return BatchResult{ - success_count: c_result.success_count - failure_count: c_result.failure_count - skipped_count: c_result.skipped_count - message: c_string_to_v(c_result.message) - } -} - -// AuditStats contains SPDX audit statistics -pub struct AuditStats { -pub mut: - total_files int - with_spdx int - without_spdx int - invalid_spdx int - pmpl_license int - other_licenses int -} - -// AuditResult contains audit results for a single repository -pub struct AuditResult { -pub mut: - repo_path string - stats AuditStats - compliance_percent int -} - -// SPDXAuditParams contains parameters for SPDX audit -pub struct SPDXAuditParams { -pub: - base_dir string - max_depth int -} - -// Performs SPDX license header audit -// Scans all source files for SPDX identifiers -pub fn spdx_audit(params SPDXAuditParams) []AuditResult { - c_results := C.c_spdx_audit( - params.base_dir.str, - params.max_depth - ) - - // Convert C results to V array - mut results := []AuditResult{} - - // For now, return empty array since we need to implement proper C array conversion - // Full implementation would iterate through c_results.results pointer - return results -} - -// Prints audit results in human-readable format -pub fn print_audit_results(results []AuditResult) { - println('\n=== SPDX Audit Results ===') - println('Total repositories scanned: ${results.len}') - println('') - - mut total_files := 0 - mut total_with_spdx := 0 - mut total_without_spdx := 0 - mut total_pmpl := 0 - - for result in results { - total_files += result.stats.total_files - total_with_spdx += result.stats.with_spdx - total_without_spdx += result.stats.without_spdx - total_pmpl += result.stats.pmpl_license - - if result.stats.without_spdx > 0 || result.stats.invalid_spdx > 0 { - println('Repository: ${result.repo_path}') - println(' Compliance: ${result.compliance_percent}%') - println(' Total files: ${result.stats.total_files}') - println(' With SPDX: ${result.stats.with_spdx}') - println(' Without SPDX: ${result.stats.without_spdx}') - println(' Invalid SPDX: ${result.stats.invalid_spdx}') - println(' PMPL-licensed: ${result.stats.pmpl_license}') - println('') - } - } - - println('=== Summary ===') - println('Total files scanned: ${total_files}') - println('With SPDX headers: ${total_with_spdx}') - println('Without SPDX headers: ${total_without_spdx}') - println('PMPL-1.0-or-later: ${total_pmpl}') - - if total_files > 0 { - compliance := (total_with_spdx * 100) / total_files - println('Overall compliance: ${compliance}%') - } - println('') -} - -// GitHubSettingsParams contains parameters for GitHub settings operation -pub struct GitHubSettingsParams { -pub: - base_dir string - max_depth int - // Repository features (none = don't change) - has_issues ?bool - has_wiki ?bool - has_projects ?bool - has_downloads ?bool - // Merge settings - allow_squash_merge ?bool - allow_merge_commit ?bool - allow_rebase_merge ?bool - delete_branch_on_merge ?bool - allow_auto_merge ?bool - // Options - dry_run bool -} - -// Convert ?bool to C int representation (-1=none, 0=false, 1=true) -fn option_bool_to_int(opt ?bool) int { - if val := opt { - return if val { 1 } else { 0 } - } - return -1 -} - -// Performs GitHub settings update across repositories -// Applies repository configuration changes via GitHub API -pub fn github_settings(params GitHubSettingsParams) int { - dry_run_flag := if params.dry_run { 1 } else { 0 } - - c_settings := CGitHubSettings{ - has_issues: option_bool_to_int(params.has_issues) - has_wiki: option_bool_to_int(params.has_wiki) - has_projects: option_bool_to_int(params.has_projects) - has_downloads: option_bool_to_int(params.has_downloads) - allow_squash_merge: option_bool_to_int(params.allow_squash_merge) - allow_merge_commit: option_bool_to_int(params.allow_merge_commit) - allow_rebase_merge: option_bool_to_int(params.allow_rebase_merge) - delete_branch_on_merge: option_bool_to_int(params.delete_branch_on_merge) - allow_auto_merge: option_bool_to_int(params.allow_auto_merge) - } - - success_count := C.c_github_settings( - params.base_dir.str, - params.max_depth, - c_settings, - dry_run_flag - ) - - return success_count -} diff --git a/scaffoldia/repo-batcher/src/v/github/community.v b/scaffoldia/repo-batcher/src/v/github/community.v deleted file mode 100644 index 042a5db..0000000 --- a/scaffoldia/repo-batcher/src/v/github/community.v +++ /dev/null @@ -1,412 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// GitHub Community Health Files -// Deploy standard community files across repositories - -module github - -import os - -// CommunityFile represents a community health file -pub struct CommunityFile { -pub: - path string // Relative path in repo (e.g., ".github/CONTRIBUTING.md") - content string // File content - name string // Human-readable name -} - -// CommunitySetupParams contains parameters for community setup -pub struct CommunitySetupParams { -pub: - repo_path string - files []CommunityFile - create_pr bool // Create PR instead of direct commit - dry_run bool -} - -// CommunitySetupResult contains the result of community setup -pub struct CommunitySetupResult { -pub: - repo_path string - success bool - files_created int - files_updated int - files_skipped int - message string -} - -// Standard community file templates -pub fn default_code_of_conduct() string { - return '# Code of Conduct - -## Our Pledge - -We as members, contributors, and leaders pledge to make participation in our -community a harassment-free experience for everyone, regardless of age, body -size, visible or invisible disability, ethnicity, sex characteristics, gender -identity and expression, level of experience, education, socio-economic status, -nationality, personal appearance, race, religion, or sexual identity -and orientation. - -## Our Standards - -Examples of behavior that contributes to a positive environment: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior: - -* The use of sexualized language or imagery -* Trolling, insulting or derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others\' private information without explicit permission -* Other conduct which could reasonably be considered inappropriate - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported to the project maintainers. All complaints will be reviewed and -investigated promptly and fairly. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), -version 2.0. -' -} - -pub fn default_contributing() string { - return '# Contributing - -Thank you for your interest in contributing! - -## Getting Started - -1. Fork the repository -2. Create a feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m "Add amazing feature"`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -## Code Style - -- Follow existing code style -- Write clear, descriptive commit messages -- Add tests for new features -- Update documentation as needed - -## Reporting Bugs - -Please use the issue tracker to report bugs. Include: - -- Clear description of the problem -- Steps to reproduce -- Expected vs actual behavior -- Environment details (OS, version, etc.) - -## Feature Requests - -We welcome feature requests! Please: - -- Check if the feature already exists or is planned -- Describe the use case and benefits -- Be open to discussion and iteration - -## Pull Request Process - -1. Update the README.md with details of changes if applicable -2. Ensure all tests pass -3. Request review from maintainers -4. Address any feedback promptly - -## License - -By contributing, you agree that your contributions will be licensed under the -same license as the project. -' -} - -pub fn default_security() string { - return '# Security Policy - -## Supported Versions - -We release patches for security vulnerabilities. Currently supported versions: - -| Version | Supported | -| ------- | ------------------ | -| latest | :white_check_mark: | - -## Reporting a Vulnerability - -**Please do not report security vulnerabilities through public GitHub issues.** - -Instead, please report them via: - -- GitHub Security Advisories (preferred) -- Email to the maintainers - -Please include: - -- Type of vulnerability -- Steps to reproduce -- Potential impact -- Suggested fix (if available) - -You should receive a response within 48 hours. If the issue is confirmed, -we will: - -1. Develop and test a fix -2. Release a security advisory -3. Release a patched version -4. Publicly disclose the vulnerability - -## Security Best Practices - -When using this project: - -- Keep dependencies up to date -- Use the latest stable version -- Follow security guidelines in documentation -- Report any suspicious behavior - -Thank you for helping keep this project secure! -' -} - -pub fn default_support() string { - return '# Support - -## Documentation - -- [README](README.md) - Project overview and quick start -- [Contributing Guide](CONTRIBUTING.md) - How to contribute -- [Wiki](../../wiki) - Detailed documentation - -## Getting Help - -### Questions and Discussions - -For general questions and discussions, please use: - -- GitHub Discussions (if enabled) -- Issue tracker for specific problems -- Community forums or chat - -### Bug Reports - -If you find a bug: - -1. Check if it\'s already reported in [Issues](../../issues) -2. If not, create a new issue with: - - Clear description - - Steps to reproduce - - Expected behavior - - Actual behavior - - Environment details - -### Feature Requests - -We welcome feature requests! Please: - -1. Check [existing issues](../../issues) first -2. Create a new issue describing: - - The use case - - Proposed solution - - Alternative solutions considered - - Additional context - -## Response Times - -- Security issues: 48 hours -- Bug reports: 1 week -- Feature requests: 2 weeks -- General questions: Best effort - -## Commercial Support - -For commercial support or consulting, please contact the maintainers directly. -' -} - -pub fn default_funding() string { - return '# Funding - -# Sponsor this project - -github: hyperpolymath - -# Other sponsorship options -# patreon: your-username -# open_collective: your-project -# ko_fi: your-username -# custom: ["https://example.com"] -' -} - -// Deploy community files to a repository -pub fn setup_community_files(params CommunitySetupParams) !CommunitySetupResult { - if params.dry_run { - return CommunitySetupResult{ - repo_path: params.repo_path - success: true - files_created: params.files.len - files_updated: 0 - files_skipped: 0 - message: '[DRY RUN] Would deploy ${params.files.len} community files' - } - } - - mut created := 0 - mut updated := 0 - mut skipped := 0 - - // Ensure .github directory exists - github_dir := os.join_path(params.repo_path, '.github') - os.mkdir_all(github_dir) or { - return CommunitySetupResult{ - repo_path: params.repo_path - success: false - files_created: 0 - files_updated: 0 - files_skipped: 0 - message: 'Failed to create .github directory: ${err}' - } - } - - // Deploy each file - for file in params.files { - file_path := os.join_path(params.repo_path, file.path) - file_dir := os.dir(file_path) - - // Ensure parent directory exists - os.mkdir_all(file_dir) or { - eprintln('Failed to create directory ${file_dir}: ${err}') - continue - } - - // Check if file already exists - if os.exists(file_path) { - // Read existing content to check if update needed - existing := os.read_file(file_path) or { '' } - if existing == file.content { - skipped++ - continue - } - updated++ - } else { - created++ - } - - // Write file - os.write_file(file_path, file.content) or { - eprintln('Failed to write ${file_path}: ${err}') - continue - } - - println(' ✓ ${file.name} -> ${file.path}') - } - - return CommunitySetupResult{ - repo_path: params.repo_path - success: true - files_created: created - files_updated: updated - files_skipped: skipped - message: 'Created ${created}, updated ${updated}, skipped ${skipped} files' - } -} - -// Get standard community files set -pub fn standard_community_files() []CommunityFile { - return [ - CommunityFile{ - path: '.github/CODE_OF_CONDUCT.md' - content: default_code_of_conduct() - name: 'Code of Conduct' - }, - CommunityFile{ - path: '.github/CONTRIBUTING.md' - content: default_contributing() - name: 'Contributing Guide' - }, - CommunityFile{ - path: '.github/SECURITY.md' - content: default_security() - name: 'Security Policy' - }, - CommunityFile{ - path: '.github/SUPPORT.md' - content: default_support() - name: 'Support Guide' - }, - CommunityFile{ - path: '.github/FUNDING.yml' - content: default_funding() - name: 'Funding Config' - }, - ] -} - -// Deploy community files to multiple repositories -pub fn setup_community_batch(repos []string, files []CommunityFile, dry_run bool) []CommunitySetupResult { - mut results := []CommunitySetupResult{} - - for repo in repos { - println('Setting up community files for ${repo}...') - result := setup_community_files(CommunitySetupParams{ - repo_path: repo - files: files - create_pr: false - dry_run: dry_run - }) or { - CommunitySetupResult{ - repo_path: repo - success: false - files_created: 0 - files_updated: 0 - files_skipped: 0 - message: 'Error: ${err}' - } - } - results << result - println(' ${result.message}') - } - - return results -} - -// Print community setup summary -pub fn print_community_summary(results []CommunitySetupResult) { - mut success := 0 - mut failed := 0 - mut total_created := 0 - mut total_updated := 0 - mut total_skipped := 0 - - for result in results { - if result.success { - success++ - total_created += result.files_created - total_updated += result.files_updated - total_skipped += result.files_skipped - } else { - failed++ - } - } - - println('') - println('=== Community Files Setup Summary ===') - println('Total repositories: ${results.len}') - println('Successful: ${success}') - println('Failed: ${failed}') - println('') - println('Files created: ${total_created}') - println('Files updated: ${total_updated}') - println('Files skipped: ${total_skipped}') - println('') -} diff --git a/scaffoldia/repo-batcher/src/v/github/discussions.v b/scaffoldia/repo-batcher/src/v/github/discussions.v deleted file mode 100644 index abf11af..0000000 --- a/scaffoldia/repo-batcher/src/v/github/discussions.v +++ /dev/null @@ -1,205 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// GitHub Discussions Setup -// Enable and configure discussions in bulk - -module github - -import os -import json - -// DiscussionCategory represents a discussion category -pub struct DiscussionCategory { -pub: - name string - description string - emoji string // Emoji for the category -} - -// DiscussionsSetupParams contains parameters for discussions setup -pub struct DiscussionsSetupParams { -pub: - repo_owner string - repo_name string - categories []DiscussionCategory - dry_run bool -} - -// DiscussionsSetupResult contains the result of discussions setup -pub struct DiscussionsSetupResult { -pub: - repo_path string - success bool - discussions_enabled bool - categories_created int - message string -} - -// Default discussion categories -pub fn default_discussion_categories() []DiscussionCategory { - return [ - DiscussionCategory{ - name: 'Announcements' - description: 'Project updates and announcements' - emoji: 'đŸ“ĸ' - }, - DiscussionCategory{ - name: 'Q&A' - description: 'Questions and answers from the community' - emoji: '🙋' - }, - DiscussionCategory{ - name: 'Ideas' - description: 'Share ideas for new features' - emoji: '💡' - }, - DiscussionCategory{ - name: 'Show and tell' - description: 'Show off something you\'ve built' - emoji: '🎨' - }, - DiscussionCategory{ - name: 'General' - description: 'General discussion about the project' - emoji: 'đŸ’Ŧ' - }, - ] -} - -// Setup discussions for a repository -pub fn setup_discussions(params DiscussionsSetupParams) !DiscussionsSetupResult { - repo_full := '${params.repo_owner}/${params.repo_name}' - - if params.dry_run { - return DiscussionsSetupResult{ - repo_path: repo_full - success: true - discussions_enabled: false - categories_created: params.categories.len - message: '[DRY RUN] Would enable discussions with ${params.categories.len} categories' - } - } - - // Step 1: Check if discussions are already enabled - println(' Checking discussions status...') - check_result := os.execute('gh api "/repos/${repo_full}" --jq .has_discussions 2>/dev/null') - - if check_result.exit_code == 0 && check_result.output.trim() == 'true' { - println(' Discussions already enabled') - return DiscussionsSetupResult{ - repo_path: repo_full - success: true - discussions_enabled: true - categories_created: 0 - message: 'Discussions already enabled' - } - } - - // Step 2: Enable discussions feature - println(' Enabling discussions...') - - // Note: GitHub API doesn't have a direct endpoint to enable discussions - // This requires GraphQL API or manual setup - // For now, we'll document this limitation - - return DiscussionsSetupResult{ - repo_path: repo_full - success: false - discussions_enabled: false - categories_created: 0 - message: 'Discussions must be enabled manually (GitHub API limitation)' - } - - // TODO: Implement GraphQL mutation to enable discussions - // mutation { - // updateRepository(input: { - // repositoryId: "REPO_ID" - // hasDiscussionsEnabled: true - // }) { - // repository { - // hasDiscussionsEnabled - // } - // } - // } -} - -// Check if discussions are enabled for a repository -pub fn check_discussions_enabled(repo_owner string, repo_name string) bool { - repo_full := '${repo_owner}/${repo_name}' - result := os.execute('gh api "/repos/${repo_full}" --jq .has_discussions 2>/dev/null') - return result.exit_code == 0 && result.output.trim() == 'true' -} - -// Setup discussions for multiple repositories -pub fn setup_discussions_batch(repos []string, categories []DiscussionCategory, dry_run bool) []DiscussionsSetupResult { - mut results := []DiscussionsSetupResult{} - - for repo in repos { - // Parse owner/repo format - parts := repo.split('/') - if parts.len != 2 { - results << DiscussionsSetupResult{ - repo_path: repo - success: false - discussions_enabled: false - categories_created: 0 - message: 'Invalid repo format (expected owner/repo)' - } - continue - } - - println('Setting up discussions for ${repo}...') - result := setup_discussions(DiscussionsSetupParams{ - repo_owner: parts[0] - repo_name: parts[1] - categories: categories - dry_run: dry_run - }) or { - DiscussionsSetupResult{ - repo_path: repo - success: false - discussions_enabled: false - categories_created: 0 - message: 'Error: ${err}' - } - } - results << result - println(' ${result.message}') - } - - return results -} - -// Print discussions setup summary -pub fn print_discussions_summary(results []DiscussionsSetupResult) { - mut success := 0 - mut already_enabled := 0 - mut failed := 0 - - for result in results { - if result.success { - if result.discussions_enabled { - already_enabled++ - } else { - success++ - } - } else { - failed++ - } - } - - println('') - println('=== Discussions Setup Summary ===') - println('Total repositories: ${results.len}') - println('Successfully enabled: ${success}') - println('Already enabled: ${already_enabled}') - println('Failed: ${failed}') - println('') - - // Note about manual setup if needed - if failed > 0 { - println('Note: Discussions must be manually enabled in repository settings') - println('due to GitHub API limitations. Use this tool to verify status.') - println('') - } -} diff --git a/scaffoldia/repo-batcher/src/v/github/pages.v b/scaffoldia/repo-batcher/src/v/github/pages.v deleted file mode 100644 index 2408a5c..0000000 --- a/scaffoldia/repo-batcher/src/v/github/pages.v +++ /dev/null @@ -1,270 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// GitHub Pages Setup -// Enable and configure GitHub Pages in bulk - -module github - -import os -import json - -// PagesSource represents where Pages should build from -pub enum PagesSource { - root_main // / on main branch - docs_main // /docs on main branch - root_gh_pages // / on gh-pages branch - gh_pages_branch // gh-pages branch (legacy) -} - -// PagesSetupParams contains parameters for Pages setup -pub struct PagesSetupParams { -pub: - repo_owner string - repo_name string - source PagesSource - cname string // Custom domain (optional) - theme string // Jekyll theme (optional) - dry_run bool -} - -// PagesSetupResult contains the result of Pages setup -pub struct PagesSetupResult { -pub: - repo_path string - success bool - pages_enabled bool - pages_url string - message string -} - -// Convert PagesSource to API parameters -fn (source PagesSource) to_api_params() (string, string) { - return match source { - .root_main { 'main', '/' } - .docs_main { 'main', '/docs' } - .root_gh_pages { 'gh-pages', '/' } - .gh_pages_branch { 'gh-pages', '/' } - } -} - -// Setup GitHub Pages for a repository -pub fn setup_pages(params PagesSetupParams) !PagesSetupResult { - repo_full := '${params.repo_owner}/${params.repo_name}' - - if params.dry_run { - return PagesSetupResult{ - repo_path: repo_full - success: true - pages_enabled: false - pages_url: 'https://${params.repo_owner}.github.io/${params.repo_name}' - message: '[DRY RUN] Would enable Pages' - } - } - - // Step 1: Check if Pages is already enabled - println(' Checking Pages status...') - check_result := os.execute('gh api "/repos/${repo_full}/pages" 2>/dev/null') - - if check_result.exit_code == 0 { - // Pages already exists - pages_url := os.execute('gh api "/repos/${repo_full}/pages" --jq .html_url 2>/dev/null').output.trim() - return PagesSetupResult{ - repo_path: repo_full - success: true - pages_enabled: true - pages_url: pages_url - message: 'Pages already enabled at ${pages_url}' - } - } - - // Step 2: Enable Pages with specified source - println(' Enabling Pages...') - branch, path := params.source.to_api_params() - - // Create the pages site - enable_cmd := 'gh api -X POST "/repos/${repo_full}/pages" -f source[branch]=${branch} -f source[path]=${path}' - enable_result := os.execute(enable_cmd) - - if enable_result.exit_code != 0 { - return PagesSetupResult{ - repo_path: repo_full - success: false - pages_enabled: false - pages_url: '' - message: 'Failed to enable Pages: ${enable_result.output}' - } - } - - // Step 3: Set custom domain if provided - if params.cname != '' { - println(' Setting custom domain...') - cname_cmd := 'gh api -X PUT "/repos/${repo_full}/pages" -f cname=${params.cname}' - os.execute(cname_cmd) - } - - // Step 4: Get Pages URL - pages_url := 'https://${params.repo_owner}.github.io/${params.repo_name}' - - return PagesSetupResult{ - repo_path: repo_full - success: true - pages_enabled: true - pages_url: pages_url - message: 'Pages enabled at ${pages_url}' - } -} - -// Check if Pages is enabled for a repository -pub fn check_pages_enabled(repo_owner string, repo_name string) bool { - repo_full := '${repo_owner}/${repo_name}' - result := os.execute('gh api "/repos/${repo_full}/pages" 2>/dev/null') - return result.exit_code == 0 -} - -// Get Pages URL for a repository -pub fn get_pages_url(repo_owner string, repo_name string) string { - repo_full := '${repo_owner}/${repo_name}' - result := os.execute('gh api "/repos/${repo_full}/pages" --jq .html_url 2>/dev/null') - if result.exit_code == 0 { - return result.output.trim() - } - return '' -} - -// Deploy default Jekyll site -pub fn deploy_jekyll_site(repo_path string, source PagesSource) !bool { - // Determine target directory - target_dir := match source { - .docs_main { - os.join_path(repo_path, 'docs') - } - else { - repo_path - } - } - - // Create target directory - os.mkdir_all(target_dir) or { - return error('Failed to create target directory: ${err}') - } - - // Create _config.yml - config_path := os.join_path(target_dir, '_config.yml') - config_content := 'theme: jekyll-theme-minimal -title: Repository Documentation -description: Automatically generated documentation site - -# Build settings -markdown: kramdown -' - - os.write_file(config_path, config_content) or { - return error('Failed to write _config.yml: ${err}') - } - - // Create index.md - index_path := os.join_path(target_dir, 'index.md') - index_content := '# Documentation - -Welcome to the documentation site! - -## Contents - -- [Getting Started](#getting-started) -- [API Reference](#api-reference) - -## Getting Started - -This site was automatically generated by repo-batcher. - -## API Reference - -See the [GitHub repository]({{ site.github.repository_url }}) for more information. -' - - os.write_file(index_path, index_content) or { - return error('Failed to write index.md: ${err}') - } - - return true -} - -// Setup Pages for multiple repositories -pub fn setup_pages_batch(repos []string, source PagesSource, cname string, dry_run bool) []PagesSetupResult { - mut results := []PagesSetupResult{} - - for repo in repos { - // Parse owner/repo format - parts := repo.split('/') - if parts.len != 2 { - results << PagesSetupResult{ - repo_path: repo - success: false - pages_enabled: false - pages_url: '' - message: 'Invalid repo format (expected owner/repo)' - } - continue - } - - println('Setting up Pages for ${repo}...') - result := setup_pages(PagesSetupParams{ - repo_owner: parts[0] - repo_name: parts[1] - source: source - cname: cname - theme: 'jekyll-theme-minimal' - dry_run: dry_run - }) or { - PagesSetupResult{ - repo_path: repo - success: false - pages_enabled: false - pages_url: '' - message: 'Error: ${err}' - } - } - results << result - println(' ${result.message}') - } - - return results -} - -// Print Pages setup summary -pub fn print_pages_summary(results []PagesSetupResult) { - mut success := 0 - mut already_enabled := 0 - mut failed := 0 - - for result in results { - if result.success { - if result.message.contains('already enabled') { - already_enabled++ - } else { - success++ - } - } else { - failed++ - } - } - - println('') - println('=== Pages Setup Summary ===') - println('Total repositories: ${results.len}') - println('Successfully enabled: ${success}') - println('Already enabled: ${already_enabled}') - println('Failed: ${failed}') - println('') - - // Show URLs for newly enabled pages - if success > 0 { - println('Newly enabled Pages sites:') - for result in results { - if result.success && !result.message.contains('already') { - println(' ${result.repo_path} -> ${result.pages_url}') - } - } - println('') - } -} diff --git a/scaffoldia/repo-batcher/src/v/github/settings.v b/scaffoldia/repo-batcher/src/v/github/settings.v deleted file mode 100644 index 3fc8599..0000000 --- a/scaffoldia/repo-batcher/src/v/github/settings.v +++ /dev/null @@ -1,299 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// GitHub Settings API Integration -// Implements bulk repository configuration via GitHub REST API - -module github - -import os -import json - -// ========== Type Definitions ========== - -pub struct RepoFeatures { -pub mut: - has_issues ?bool - has_wiki ?bool - has_projects ?bool - has_downloads ?bool -} - -pub struct MergeSettings { -pub mut: - allow_squash_merge ?bool - allow_merge_commit ?bool - allow_rebase_merge ?bool - delete_branch_on_merge ?bool - allow_auto_merge ?bool -} - -pub struct GitHubSettings { -pub mut: - repo_features RepoFeatures - merge_settings MergeSettings -} - -pub struct SettingsResult { -pub: - repo_path string - success bool - changes_applied int - message string -} - -// ========== GitHub API Client ========== - -struct GitHubClient { - dry_run bool -} - -fn new_client(dry_run bool) GitHubClient { - return GitHubClient{ - dry_run: dry_run - } -} - -// Execute gh api command -fn (c GitHubClient) gh_api(method string, endpoint string, data map[string]json.Any) !string { - if c.dry_run { - println('[DRY RUN] Would call: gh api ${method} ${endpoint}') - println('[DRY RUN] With data: ${data}') - return '' - } - - mut args := ['api', '-X', method, endpoint] - - // Add JSON fields - for key, value in data { - match value { - bool { - args << '-F' - args << '${key}=${value}' - } - string { - args << '-f' - args << '${key}=${value}' - } - else { - args << '-f' - args << '${key}=${value}' - } - } - } - - result := os.execute('gh ${args.join(' ')}') - - if result.exit_code != 0 { - return error('gh api failed: ${result.output}') - } - - return result.output -} - -// ========== Repository Feature Settings ========== - -fn (c GitHubClient) apply_repo_features(repo string, features RepoFeatures) !(bool, int) { - mut changes := 0 - mut data := map[string]json.Any{} - - // Build update payload - if has_issues := features.has_issues { - data['has_issues'] = json.Any(has_issues) - changes++ - } - if has_wiki := features.has_wiki { - data['has_wiki'] = json.Any(has_wiki) - changes++ - } - if has_projects := features.has_projects { - data['has_projects'] = json.Any(has_projects) - changes++ - } - if has_downloads := features.has_downloads { - data['has_downloads'] = json.Any(has_downloads) - changes++ - } - - if changes == 0 { - return true, 0 - } - - endpoint := '/repos/${repo}' - - if c.dry_run { - println('[DRY RUN] Would update ${repo} with ${changes} repo feature changes') - return true, changes - } - - c.gh_api('PATCH', endpoint, data) or { - eprintln('Failed to update repo features for ${repo}: ${err}') - return false, 0 - } - - return true, changes -} - -// ========== Merge Settings ========== - -fn (c GitHubClient) apply_merge_settings(repo string, settings MergeSettings) !(bool, int) { - mut changes := 0 - mut data := map[string]json.Any{} - - // Build update payload - if allow_squash := settings.allow_squash_merge { - data['allow_squash_merge'] = json.Any(allow_squash) - changes++ - } - if allow_merge := settings.allow_merge_commit { - data['allow_merge_commit'] = json.Any(allow_merge) - changes++ - } - if allow_rebase := settings.allow_rebase_merge { - data['allow_rebase_merge'] = json.Any(allow_rebase) - changes++ - } - if delete_branch := settings.delete_branch_on_merge { - data['delete_branch_on_merge'] = json.Any(delete_branch) - changes++ - } - if allow_auto := settings.allow_auto_merge { - data['allow_auto_merge'] = json.Any(allow_auto) - changes++ - } - - if changes == 0 { - return true, 0 - } - - endpoint := '/repos/${repo}' - - if c.dry_run { - println('[DRY RUN] Would update ${repo} with ${changes} merge setting changes') - return true, changes - } - - c.gh_api('PATCH', endpoint, data) or { - eprintln('Failed to update merge settings for ${repo}: ${err}') - return false, 0 - } - - return true, changes -} - -// ========== Complete Settings Application ========== - -pub fn apply_settings(repo string, settings GitHubSettings, dry_run bool) !SettingsResult { - client := new_client(dry_run) - - // Apply repository features - features_ok, features_count := client.apply_repo_features(repo, settings.repo_features) or { - return SettingsResult{ - repo_path: repo - success: false - changes_applied: 0 - message: 'Failed to apply repository features: ${err}' - } - } - - // Apply merge settings - merge_ok, merge_count := client.apply_merge_settings(repo, settings.merge_settings) or { - return SettingsResult{ - repo_path: repo - success: false - changes_applied: features_count - message: 'Failed to apply merge settings: ${err}' - } - } - - // Compute results - success := features_ok && merge_ok - total_changes := features_count + merge_count - - message := if dry_run { - '[DRY RUN] Would apply ${total_changes} changes' - } else if success { - 'Applied ${total_changes} changes successfully' - } else { - 'Failed to apply all settings' - } - - return SettingsResult{ - repo_path: repo - success: success - changes_applied: total_changes - message: message - } -} - -// ========== Batch Operations ========== - -pub fn apply_settings_batch(repos []string, settings GitHubSettings, dry_run bool) []SettingsResult { - mut results := []SettingsResult{} - - for repo in repos { - result := apply_settings(repo, settings, dry_run) or { - SettingsResult{ - repo_path: repo - success: false - changes_applied: 0 - message: 'Error: ${err}' - } - } - results << result - } - - return results -} - -// ========== Summary Statistics ========== - -pub struct SettingsSummary { -pub: - total_repos int - successful int - failed int - total_changes int -} - -pub fn compute_summary(results []SettingsResult) SettingsSummary { - mut successful := 0 - mut failed := 0 - mut total_changes := 0 - - for result in results { - if result.success { - successful++ - total_changes += result.changes_applied - } else { - failed++ - } - } - - return SettingsSummary{ - total_repos: results.len - successful: successful - failed: failed - total_changes: total_changes - } -} - -pub fn print_summary(summary SettingsSummary) { - println('=== GitHub Settings Summary ===') - println('Total repositories: ${summary.total_repos}') - println('Successful: ${summary.successful}') - println('Failed: ${summary.failed}') - println('Total changes applied: ${summary.total_changes}') - println('') -} - -// ========== Authentication Check ========== - -pub fn check_gh_auth() !bool { - result := os.execute('gh auth status') - return result.exit_code == 0 -} - -pub fn check_gh_cli_installed() !bool { - result := os.execute('gh --version') - return result.exit_code == 0 -} diff --git a/scaffoldia/repo-batcher/src/v/github/templates.v b/scaffoldia/repo-batcher/src/v/github/templates.v deleted file mode 100644 index e19a13c..0000000 --- a/scaffoldia/repo-batcher/src/v/github/templates.v +++ /dev/null @@ -1,382 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// GitHub Issue & PR Templates -// Deploy issue templates and PR templates in bulk - -module github - -import os - -// IssueTemplate represents a GitHub issue template -pub struct IssueTemplate { -pub: - name string // Template name (e.g., "bug_report") - filename string // File name (e.g., "bug_report.yml") - content string // Template content (YAML or Markdown) - description string // Human-readable description -} - -// TemplatesSetupParams contains parameters for templates setup -pub struct TemplatesSetupParams { -pub: - repo_path string - issue_templates []IssueTemplate - pr_template string // PR template content (optional) - dry_run bool -} - -// TemplatesSetupResult contains the result of templates setup -pub struct TemplatesSetupResult { -pub: - repo_path string - success bool - issue_templates_created int - pr_template_created bool - message string -} - -// Default bug report template (YAML format) -pub fn default_bug_report_template() string { - return 'name: Bug Report -description: File a bug report -title: "[Bug]: " -labels: ["bug", "triage"] -body: - - type: markdown - attributes: - value: | - Thanks for taking the time to fill out this bug report! - - - type: textarea - id: what-happened - attributes: - label: What happened? - description: Also tell us, what did you expect to happen? - placeholder: Tell us what you see! - validations: - required: true - - - type: textarea - id: reproduce - attributes: - label: Steps to reproduce - description: How can we reproduce this bug? - placeholder: | - 1. Go to \'...\' - 2. Click on \'...\' - 3. See error - validations: - required: true - - - type: textarea - id: environment - attributes: - label: Environment - description: What environment are you using? - placeholder: | - - OS: [e.g. macOS, Linux, Windows] - - Version: [e.g. 1.0.0] - - Browser: [if applicable] - validations: - required: false - - - type: textarea - id: logs - attributes: - label: Relevant log output - description: Please copy and paste any relevant log output. - render: shell - validations: - required: false - - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our Code of Conduct - options: - - label: I agree to follow this project\'s Code of Conduct - required: true -' -} - -// Default feature request template (YAML format) -pub fn default_feature_request_template() string { - return 'name: Feature Request -description: Suggest an idea for this project -title: "[Feature]: " -labels: ["enhancement"] -body: - - type: markdown - attributes: - value: | - Thanks for suggesting a new feature! - - - type: textarea - id: problem - attributes: - label: Is your feature request related to a problem? - description: A clear description of what the problem is. - placeholder: I\'m always frustrated when [...] - validations: - required: true - - - type: textarea - id: solution - attributes: - label: Describe the solution you\'d like - description: A clear description of what you want to happen. - validations: - required: true - - - type: textarea - id: alternatives - attributes: - label: Describe alternatives you\'ve considered - description: Any alternative solutions or features you\'ve considered. - validations: - required: false - - - type: textarea - id: context - attributes: - label: Additional context - description: Add any other context or screenshots about the feature request. - validations: - required: false - - - type: checkboxes - id: terms - attributes: - label: Code of Conduct - description: By submitting this issue, you agree to follow our Code of Conduct - options: - - label: I agree to follow this project\'s Code of Conduct - required: true -' -} - -// Default documentation issue template -pub fn default_documentation_template() string { - return 'name: Documentation Issue -description: Report a problem with documentation -title: "[Docs]: " -labels: ["documentation"] -body: - - type: markdown - attributes: - value: | - Thanks for helping improve our documentation! - - - type: textarea - id: issue - attributes: - label: What\'s wrong with the documentation? - description: A clear description of the documentation issue. - validations: - required: true - - - type: input - id: url - attributes: - label: Documentation URL - description: Link to the documentation page - placeholder: https://github.com/owner/repo/wiki/Page - validations: - required: false - - - type: textarea - id: suggestion - attributes: - label: Suggested fix - description: How should the documentation be improved? - validations: - required: false -' -} - -// Default PR template (Markdown format) -pub fn default_pr_template() string { - return '## Description - -Please include a summary of the changes and the related issue. Please also include relevant motivation and context. - -Fixes # (issue) - -## Type of change - -Please delete options that are not relevant. - -- [ ] Bug fix (non-breaking change which fixes an issue) -- [ ] New feature (non-breaking change which adds functionality) -- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) -- [ ] Documentation update - -## How Has This Been Tested? - -Please describe the tests that you ran to verify your changes. - -- [ ] Test A -- [ ] Test B - -## Checklist - -- [ ] My code follows the style guidelines of this project -- [ ] I have performed a self-review of my code -- [ ] I have commented my code, particularly in hard-to-understand areas -- [ ] I have made corresponding changes to the documentation -- [ ] My changes generate no new warnings -- [ ] I have added tests that prove my fix is effective or that my feature works -- [ ] New and existing unit tests pass locally with my changes -- [ ] Any dependent changes have been merged and published -' -} - -// Deploy issue and PR templates -pub fn setup_templates(params TemplatesSetupParams) !TemplatesSetupResult { - if params.dry_run { - return TemplatesSetupResult{ - repo_path: params.repo_path - success: true - issue_templates_created: params.issue_templates.len - pr_template_created: params.pr_template != '' - message: '[DRY RUN] Would deploy ${params.issue_templates.len} issue templates' - } - } - - mut issue_created := 0 - mut pr_created := false - - // Create issue template directory - if params.issue_templates.len > 0 { - issue_template_dir := os.join_path(params.repo_path, '.github', 'ISSUE_TEMPLATE') - os.mkdir_all(issue_template_dir) or { - return TemplatesSetupResult{ - repo_path: params.repo_path - success: false - issue_templates_created: 0 - pr_template_created: false - message: 'Failed to create ISSUE_TEMPLATE directory: ${err}' - } - } - - // Deploy each issue template - for template in params.issue_templates { - template_path := os.join_path(issue_template_dir, template.filename) - - os.write_file(template_path, template.content) or { - eprintln('Failed to write ${template.filename}: ${err}') - continue - } - - issue_created++ - println(' ✓ ${template.description} -> ${template.filename}') - } - } - - // Deploy PR template - if params.pr_template != '' { - github_dir := os.join_path(params.repo_path, '.github') - os.mkdir_all(github_dir) or {} - - pr_path := os.join_path(github_dir, 'PULL_REQUEST_TEMPLATE.md') - - os.write_file(pr_path, params.pr_template) or { - eprintln('Failed to write PR template: ${err}') - } - - if os.exists(pr_path) { - pr_created = true - println(' ✓ Pull Request Template -> PULL_REQUEST_TEMPLATE.md') - } - } - - return TemplatesSetupResult{ - repo_path: params.repo_path - success: true - issue_templates_created: issue_created - pr_template_created: pr_created - message: 'Created ${issue_created} issue templates, PR template: ${pr_created}' - } -} - -// Get standard issue templates -pub fn standard_issue_templates() []IssueTemplate { - return [ - IssueTemplate{ - name: 'bug_report' - filename: 'bug_report.yml' - content: default_bug_report_template() - description: 'Bug Report Template' - }, - IssueTemplate{ - name: 'feature_request' - filename: 'feature_request.yml' - content: default_feature_request_template() - description: 'Feature Request Template' - }, - IssueTemplate{ - name: 'documentation' - filename: 'documentation.yml' - content: default_documentation_template() - description: 'Documentation Issue Template' - }, - ] -} - -// Deploy templates to multiple repositories -pub fn setup_templates_batch(repos []string, issue_templates []IssueTemplate, pr_template string, dry_run bool) []TemplatesSetupResult { - mut results := []TemplatesSetupResult{} - - for repo in repos { - println('Setting up templates for ${repo}...') - result := setup_templates(TemplatesSetupParams{ - repo_path: repo - issue_templates: issue_templates - pr_template: pr_template - dry_run: dry_run - }) or { - TemplatesSetupResult{ - repo_path: repo - success: false - issue_templates_created: 0 - pr_template_created: false - message: 'Error: ${err}' - } - } - results << result - println(' ${result.message}') - } - - return results -} - -// Print templates setup summary -pub fn print_templates_summary(results []TemplatesSetupResult) { - mut success := 0 - mut failed := 0 - mut total_issue_templates := 0 - mut total_pr_templates := 0 - - for result in results { - if result.success { - success++ - total_issue_templates += result.issue_templates_created - if result.pr_template_created { - total_pr_templates++ - } - } else { - failed++ - } - } - - println('') - println('=== Templates Setup Summary ===') - println('Total repositories: ${results.len}') - println('Successful: ${success}') - println('Failed: ${failed}') - println('') - println('Issue templates deployed: ${total_issue_templates}') - println('PR templates deployed: ${total_pr_templates}') - println('') -} diff --git a/scaffoldia/repo-batcher/src/v/github/wikis.v b/scaffoldia/repo-batcher/src/v/github/wikis.v deleted file mode 100644 index f0ab0ae..0000000 --- a/scaffoldia/repo-batcher/src/v/github/wikis.v +++ /dev/null @@ -1,220 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// GitHub Wiki Setup -// Initialize wikis with first page to enable automation - -module github - -import os - -// WikiSetupParams contains parameters for wiki initialization -pub struct WikiSetupParams { -pub: - repo_owner string - repo_name string - home_content string // Content for Home.md (first page) - dry_run bool -} - -// WikiSetupResult contains the result of wiki setup -pub struct WikiSetupResult { -pub: - repo_path string - success bool - wiki_existed bool - message string -} - -// Setup wiki for a repository -// This solves the "first page problem" where wikis aren't usable until -// someone manually creates the first page through the UI -pub fn setup_wiki(params WikiSetupParams) !WikiSetupResult { - repo_full := '${params.repo_owner}/${params.repo_name}' - - if params.dry_run { - return WikiSetupResult{ - repo_path: repo_full - success: true - wiki_existed: false - message: '[DRY RUN] Would initialize wiki' - } - } - - // Step 1: Enable wiki feature via API - println(' Enabling wiki feature...') - enable_result := os.execute('gh api -X PATCH "/repos/${repo_full}" -f has_wiki=true') - if enable_result.exit_code != 0 { - return WikiSetupResult{ - repo_path: repo_full - success: false - wiki_existed: false - message: 'Failed to enable wiki feature: ${enable_result.output}' - } - } - - // Step 2: Check if wiki already exists - println(' Checking if wiki exists...') - check_result := os.execute('gh api "/repos/${repo_full}/pages" 2>/dev/null') - wiki_exists := check_result.exit_code == 0 - - if wiki_exists { - return WikiSetupResult{ - repo_path: repo_full - success: true - wiki_existed: true - message: 'Wiki already initialized' - } - } - - // Step 3: Initialize wiki with first page - // GitHub quirk: wiki repo doesn't exist until first page is created - // We'll use git directly to create it - println(' Creating first wiki page...') - - tmp_dir := os.join_path(os.temp_dir(), 'repo-batcher-wiki-${params.repo_name}') - - // Clean up any existing temp directory - os.rmdir_all(tmp_dir) or {} - - // Initialize local git repo - os.execute('git init "${tmp_dir}"') or { - return WikiSetupResult{ - repo_path: repo_full - success: false - wiki_existed: false - message: 'Failed to initialize temp git repo' - } - } - - // Create Home.md with content - home_path := os.join_path(tmp_dir, 'Home.md') - os.write_file(home_path, params.home_content) or { - os.rmdir_all(tmp_dir) or {} - return WikiSetupResult{ - repo_path: repo_full - success: false - wiki_existed: false - message: 'Failed to write Home.md: ${err}' - } - } - - // Set up git remote and push - wiki_url := 'https://github.com/${repo_full}.wiki.git' - - commands := [ - 'cd "${tmp_dir}" && git add Home.md', - 'cd "${tmp_dir}" && git config user.name "repo-batcher"', - 'cd "${tmp_dir}" && git config user.email "noreply@hyperpolymath.net"', - 'cd "${tmp_dir}" && git commit -m "Initialize wiki with Home page"', - 'cd "${tmp_dir}" && git remote add origin ${wiki_url}', - 'cd "${tmp_dir}" && git push -u origin master || git push -u origin main', - ] - - for cmd in commands { - result := os.execute(cmd) - if result.exit_code != 0 && !cmd.contains('||') { - os.rmdir_all(tmp_dir) or {} - return WikiSetupResult{ - repo_path: repo_full - success: false - wiki_existed: false - message: 'Git command failed: ${result.output}' - } - } - } - - // Clean up temp directory - os.rmdir_all(tmp_dir) or {} - - return WikiSetupResult{ - repo_path: repo_full - success: true - wiki_existed: false - message: 'Wiki initialized with Home page' - } -} - -// Setup wikis for multiple repositories -pub fn setup_wikis_batch(repos []string, home_content string, dry_run bool) []WikiSetupResult { - mut results := []WikiSetupResult{} - - for repo in repos { - // Parse owner/repo format - parts := repo.split('/') - if parts.len != 2 { - results << WikiSetupResult{ - repo_path: repo - success: false - wiki_existed: false - message: 'Invalid repo format (expected owner/repo)' - } - continue - } - - println('Setting up wiki for ${repo}...') - result := setup_wiki(WikiSetupParams{ - repo_owner: parts[0] - repo_name: parts[1] - home_content: home_content - dry_run: dry_run - }) or { - WikiSetupResult{ - repo_path: repo - success: false - wiki_existed: false - message: 'Error: ${err}' - } - } - results << result - println(' ${if result.success { '✓' } else { '✗' }} ${result.message}') - } - - return results -} - -// Generate default Home.md content -pub fn default_home_content(repo_name string) string { - return '# ${repo_name} Wiki - -Welcome to the ${repo_name} wiki! - -## Getting Started - -This wiki was automatically initialized by repo-batcher. - -## Contents - -- [Home](Home) - -## Contributing - -Feel free to contribute to this wiki by editing pages or creating new ones. -' -} - -// Print wiki setup summary -pub fn print_wiki_summary(results []WikiSetupResult) { - mut success := 0 - mut already_existed := 0 - mut failed := 0 - - for result in results { - if result.success { - if result.wiki_existed { - already_existed++ - } else { - success++ - } - } else { - failed++ - } - } - - println('') - println('=== Wiki Setup Summary ===') - println('Total repositories: ${results.len}') - println('Successfully initialized: ${success}') - println('Already existed: ${already_existed}') - println('Failed: ${failed}') - println('') -} diff --git a/scaffoldia/repo-batcher/src/v/main.v b/scaffoldia/repo-batcher/src/v/main.v deleted file mode 100644 index 7a6201c..0000000 --- a/scaffoldia/repo-batcher/src/v/main.v +++ /dev/null @@ -1,2027 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// repo-batcher main entry point -// Fast CLI for formally verified batch repository operations - -module main - -import os -import time -import cli -import ffi -import executor -import utils -import rollback -import watcher -import github -import parsers -import safety -import templates - -const ( - version = '0.1.0' - app_name = 'repo-batcher' - default_repos_dir = os.join_path(os.home_dir(), 'Documents', 'hyperpolymath-repos') -) - -fn main() { - mut app := cli.Command{ - name: app_name - description: 'Formally verified batch operations for mass repository management' - version: version - execute: show_help - commands: [ - cli.Command{ - name: 'list-ops' - description: 'List all available operations' - execute: cmd_list_operations - }, - cli.Command{ - name: 'license-update' - description: 'Update license across repositories' - execute: cmd_license_update - flags: [ - cli.Flag{ - flag: .string - name: 'old' - abbrev: 'o' - description: 'Old license SPDX identifier' - required: true - }, - cli.Flag{ - flag: .string - name: 'new' - abbrev: 'n' - description: 'New license SPDX identifier' - required: true - }, - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'backup' - abbrev: 'b' - description: 'Create backups before changes' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'file-replace' - description: 'Replace files across repositories' - execute: cmd_file_replace - flags: [ - cli.Flag{ - flag: .string - name: 'pattern' - abbrev: 'p' - description: 'File pattern to match' - required: true - }, - cli.Flag{ - flag: .string - name: 'replacement' - abbrev: 'r' - description: 'Replacement file path' - required: true - }, - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories' - required: true - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'backup' - abbrev: 'b' - description: 'Create backups before changes' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'git-sync' - description: 'Batch git sync (add, commit, push) across repositories' - execute: cmd_git_sync - flags: [ - cli.Flag{ - flag: .int - name: 'parallel' - abbrev: 'p' - description: 'Number of parallel jobs (default: 4)' - default_value: ['4'] - }, - cli.Flag{ - flag: .int - name: 'depth' - abbrev: 'D' - description: 'Max depth for repository search (default: 2)' - default_value: ['2'] - }, - cli.Flag{ - flag: .string - name: 'commit-message' - abbrev: 'm' - description: 'Commit message (default: "chore: batch update")' - default_value: ['chore: batch update'] - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'watch' - description: 'Start watch daemon for batch operations' - execute: cmd_watch - flags: [ - cli.Flag{ - flag: .string - name: 'folder' - abbrev: 'f' - description: 'Watch folder path (default: ~/.config/repo-batcher/watch)' - }, - cli.Flag{ - flag: .int - name: 'interval' - abbrev: 'i' - description: 'Check interval in seconds (default: 30)' - default_value: ['30'] - }, - ] - }, - cli.Command{ - name: 'github-settings' - description: 'Bulk GitHub repository settings configuration' - execute: cmd_github_settings - flags: [ - cli.Flag{ - flag: .string - name: 'config' - abbrev: 'c' - description: 'TOML config file path' - }, - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'has-issues' - description: 'Enable/disable issues (use --no-has-issues to disable)' - }, - cli.Flag{ - flag: .bool - name: 'has-wiki' - description: 'Enable/disable wiki' - }, - cli.Flag{ - flag: .bool - name: 'delete-branch-on-merge' - description: 'Auto-delete branches after merge' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'wiki-setup' - description: 'Initialize wikis with first page (enables automation)' - execute: cmd_wiki_setup - flags: [ - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .string - name: 'home-template' - abbrev: 'h' - description: 'Path to Home.md template file' - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'community-setup' - description: 'Deploy community health files (CODE_OF_CONDUCT, CONTRIBUTING, etc.)' - execute: cmd_community_setup - flags: [ - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .string - name: 'template-dir' - abbrev: 'T' - description: 'Directory with custom template files' - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'templates-setup' - description: 'Deploy issue and PR templates' - execute: cmd_templates_setup - flags: [ - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .string - name: 'template-dir' - abbrev: 'T' - description: 'Directory with custom template files' - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'discussions-setup' - description: 'Enable and configure GitHub Discussions' - execute: cmd_discussions_setup - flags: [ - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'pages-setup' - description: 'Enable and configure GitHub Pages' - execute: cmd_pages_setup - flags: [ - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .string - name: 'source' - abbrev: 's' - description: 'Pages source (root-main, docs-main, gh-pages) [default: docs-main]' - default_value: ['docs-main'] - }, - cli.Flag{ - flag: .string - name: 'cname' - abbrev: 'c' - description: 'Custom domain for Pages' - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'template-merge' - description: 'Convert existing repos to template form (preserves content)' - execute: cmd_template_merge - flags: [ - cli.Flag{ - flag: .string - name: 'targets' - abbrev: 't' - description: 'Target repositories (comma-separated or @pattern)' - required: true - }, - cli.Flag{ - flag: .bool - name: 'workflows' - description: 'Add GitHub workflows (default: true)' - }, - cli.Flag{ - flag: .bool - name: 'scm-files' - description: 'Add .machine_readable SCM files (default: true)' - }, - cli.Flag{ - flag: .bool - name: 'bot-directives' - description: 'Add .bot_directives (default: true)' - }, - cli.Flag{ - flag: .bool - name: 'contractiles' - description: 'Add contractiles directory (default: true)' - }, - cli.Flag{ - flag: .bool - name: 'preserve' - description: 'Preserve existing files (default: true)' - }, - cli.Flag{ - flag: .bool - name: 'dry-run' - abbrev: 'd' - description: 'Preview changes without executing' - }, - cli.Flag{ - flag: .bool - name: 'force' - abbrev: 'f' - description: 'Skip safety confirmation prompts' - }, - ] - }, - cli.Command{ - name: 'rollback' - description: 'Rollback last operation or specific log' - execute: cmd_rollback - flags: [ - cli.Flag{ - flag: .bool - name: 'last' - abbrev: 'l' - description: 'Rollback last operation' - }, - cli.Flag{ - flag: .string - name: 'log-id' - abbrev: 'L' - description: 'Rollback specific log ID' - }, - ] - }, - ] - } - - app.setup() - app.parse(os.args) -} - -fn show_help(cmd cli.Command) ! { - println('repo-batcher v${version}') - println('Formally verified batch operations for mass repository management') - println('') - println('Usage:') - println(' repo-batcher [options]') - println('') - println('Available commands:') - println(' list-ops List all available operations') - println(' license-update Update license across repositories') - println(' file-replace Replace files across repositories') - println(' git-sync Batch git sync (add, commit, push)') - println(' github-settings Bulk repository configuration (features, merge settings)') - println(' wiki-setup Initialize wikis with first page (enables automation)') - println(' community-setup Deploy community health files') - println(' templates-setup Deploy issue and PR templates') - println(' discussions-setup Enable and configure GitHub Discussions') - println(' pages-setup Enable and configure GitHub Pages') - println(' template-merge Convert repos to template form (preserves content)') - println(' watch Start watch daemon') - println(' rollback Rollback operation') - println('') - println('Use "repo-batcher --help" for more information about a command.') -} - -fn cmd_list_operations(cmd cli.Command) ! { - println('Available Operations:') - println('') - println(' license-update Replace license headers and LICENSE files') - println(' Safety: Valid SPDX IDs, backup required') - println('') - println(' file-replace Replace files matching pattern') - println(' Safety: Backup required, no circular replacements') - println('') - println(' git-sync Batch commit and push across repos') - println(' Safety: Valid repos, no conflicts, remote reachable') - println('') - println(' workflow-update Update GitHub Actions workflows') - println(' Safety: Valid YAML, SHA pinning validation') - println('') - println(' github-settings Bulk repository configuration via GitHub API') - println(' Safety: Pre-flight validation, rollback support') - println('') - println(' wiki-setup Initialize wikis with first page') - println(' Safety: Creates Home.md to enable automation') - println('') - println(' community-setup Deploy community health files') - println(' Safety: CODE_OF_CONDUCT, CONTRIBUTING, SECURITY, etc.') - println('') - println(' templates-setup Deploy issue and PR templates') - println(' Safety: Bug report, feature request, documentation templates') - println('') - println(' discussions-setup Enable and configure GitHub Discussions') - println(' Safety: Creates 5 default categories (read-only operation)') - println('') - println(' pages-setup Enable and configure GitHub Pages') - println(' Safety: Configure source, branch, custom domains (remote)') - println('') - println(' template-merge Convert repos to template form (preserves content)') - println(' Safety: Adds workflows, SCM, bot directives, contractiles') - println('') - println(' custom Execute custom operation from template') - println(' Safety: Template validation, dry-run enforced') -} - -fn cmd_license_update(cmd cli.Command) ! { - old := cmd.flags.get_string('old')! - new := cmd.flags.get_string('new')! - targets := cmd.flags.get_string('targets')! - dry_run := cmd.flags.get_bool('dry-run') or { false } - backup := cmd.flags.get_bool('backup') or { true } - force := cmd.flags.get_bool('force') or { false } - - println('License Update Operation') - println(' Old: ${old}') - println(' New: ${new}') - println(' Targets: ${targets}') - println(' Dry Run: ${dry_run}') - println(' Backup: ${backup}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Validate SPDX identifiers first - if !ffi.validate_spdx(old) { - println('ERROR: Invalid old license SPDX identifier: ${old}') - return error('Invalid SPDX identifier') - } - - if !ffi.validate_spdx(new) { - println('ERROR: Invalid new license SPDX identifier: ${new}') - return error('Invalid SPDX identifier') - } - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories from target specification - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'license-update', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.local_changes, - repos, - 'Update license from ${old} to ${new}', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.local_changes, repos, 'license-update', 'CANCELLED') - return - } - - // Determine parallel jobs (use 4 for license updates, less I/O intensive) - parallel := if repos.len < 4 { repos.len } else { 4 } - - // Create parallel executor with V coroutines - mut pool := executor.new_worker_pool(repos, parallel) - - // Execute license-update in parallel - result := pool.execute_license_update(old, new, backup, dry_run) - - // Print results - result.print() - - // Audit log - status := if result.has_failures() { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.local_changes, repos, 'license-update', status) - - if result.has_failures() { - return error('License update completed with failures') - } -} - -fn cmd_file_replace(cmd cli.Command) ! { - pattern := cmd.flags.get_string('pattern')! - replacement := cmd.flags.get_string('replacement')! - targets := cmd.flags.get_string('targets')! - dry_run := cmd.flags.get_bool('dry-run') or { false } - backup := cmd.flags.get_bool('backup') or { true } - force := cmd.flags.get_bool('force') or { false } - - println('File Replace Operation') - println(' Pattern: ${pattern}') - println(' Replacement: ${replacement}') - println(' Targets: ${targets}') - println(' Dry Run: ${dry_run}') - println(' Backup: ${backup}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Check replacement file exists - if !os.exists(replacement) { - println('ERROR: Replacement file does not exist: ${replacement}') - return error('Replacement file not found') - } - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories from target specification - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'file-replace', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.local_changes, - repos, - 'Replace files matching ${pattern}', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.local_changes, repos, 'file-replace', 'CANCELLED') - return - } - - // Determine parallel jobs - parallel := if repos.len < 4 { repos.len } else { 4 } - - // Create parallel executor with V coroutines - mut pool := executor.new_worker_pool(repos, parallel) - - // Execute file-replace in parallel - result := pool.execute_file_replace(pattern, replacement, backup, dry_run) - - // Print results - result.print() - - // Audit log - status := if result.has_failures() { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.local_changes, repos, 'file-replace', status) - - if result.has_failures() { - return error('File replace completed with failures') - } -} - -fn cmd_git_sync(cmd cli.Command) ! { - parallel := cmd.flags.get_int('parallel') or { 4 } - depth := cmd.flags.get_int('depth') or { 2 } - message := cmd.flags.get_string('commit-message') or { 'chore: batch update' } - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - println('Git Batch Sync Operation (ported from sync_repos.sh)') - println(' Parallel Jobs: ${parallel}') - println(' Max Depth: ${depth}') - println(' Commit Message: ${message}') - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Find all repositories - println('Scanning for repositories (find . -maxdepth ${depth} -name ".git")...') - repos := utils.find_git_repos(default_repos_dir, depth) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found in ${default_repos_dir}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'git-sync', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.remote_changes, - repos, - 'Batch commit and push: ${message}', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.remote_changes, repos, 'git-sync', 'CANCELLED') - return - } - - // Create parallel executor with V coroutines - mut pool := executor.new_worker_pool(repos, parallel) - - // Execute git-sync in parallel - result := pool.execute_git_sync(message, dry_run) - - // Print results - result.print() - - // Audit log - status := if result.has_failures() { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.remote_changes, repos, 'git-sync', status) - - if result.has_failures() { - println('NOTE: Some repositories failed. Check logs for details.') - return error('Git sync completed with failures') - } -} - -fn cmd_watch(cmd cli.Command) ! { - folder := cmd.flags.get_string('folder') or { - os.join_path(os.home_dir(), '.config', 'repo-batcher', 'watch') - } - interval := cmd.flags.get_int('interval') or { 30 } - - println('Starting Watch Daemon') - println(' Watch Folder: ${folder}') - println(' Check Interval: ${interval}s') - println('') - - // Create and start monitor - mut monitor := watcher.new_watch_monitor(folder, interval, true) - monitor.start() -} - -fn cmd_rollback(cmd cli.Command) ! { - last := cmd.flags.get_bool('last') or { false } - log_id := cmd.flags.get_string('log-id') or { '' } - - mut mgr := rollback.new_backup_manager() - - if last { - println('Rolling back last operation...') - println('') - mgr.restore_last() or { - println('') - println('ERROR: ${err}') - return error('Rollback failed') - } - println('') - println('✓ Rollback completed successfully') - } else if log_id != '' { - println('Rolling back operation: ${log_id}') - println('') - mgr.restore_operation(log_id) or { - println('') - println('ERROR: ${err}') - return error('Rollback failed') - } - println('') - println('✓ Rollback completed successfully') - } else { - // List recent operations - println('Recent operations:') - println('') - operations := mgr.list_operations(10) - if operations.len == 0 { - println('No operations to rollback') - } else { - for op in operations { - timestamp := time.unix(op.timestamp).format() - println(' ${op.operation_id}') - println(' Type: ${op.operation_type}') - println(' Time: ${timestamp}') - println(' Repos: ${op.repos.len}') - println(' Files: ${op.entries.len}') - println('') - } - println('Use --last to rollback most recent, or --log-id for specific operation') - } - } -} - -fn cmd_github_settings(cmd cli.Command) ! { - config_file := cmd.flags.get_string('config') or { '' } - targets := cmd.flags.get_string('targets')! - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - // Individual flag overrides - has_issues := cmd.flags.get_bool('has-issues') or { none } - has_wiki := cmd.flags.get_bool('has-wiki') or { none } - delete_branch := cmd.flags.get_bool('delete-branch-on-merge') or { none } - - println('GitHub Settings Operation') - println(' Config: ${if config_file != '' { config_file } else { '(command-line flags)' }}') - println(' Targets: ${targets}') - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Check gh CLI authentication - if !github.check_gh_cli_installed()! { - println('ERROR: GitHub CLI (gh) is not installed') - println('Please install it: https://cli.github.com/') - return error('gh CLI not available') - } - - if !github.check_gh_auth()! { - println('ERROR: Not authenticated with GitHub CLI') - println('Please run: gh auth login') - return error('gh auth required') - } - - // Load settings from config file or flags - mut settings := github.GitHubSettings{ - repo_features: github.RepoFeatures{ - has_issues: has_issues - has_wiki: has_wiki - } - merge_settings: github.MergeSettings{ - delete_branch_on_merge: delete_branch - } - } - - // If config file provided, parse it and merge with flags - if config_file != '' { - println('Loading settings from ${config_file}...') - settings = parsers.parse_settings_toml(config_file) or { - println('ERROR: Failed to parse config file: ${err}') - return error('Config parse failed') - } - - // Command-line flags override config file - if has_issues != none { - settings.repo_features.has_issues = has_issues - } - if has_wiki != none { - settings.repo_features.has_wiki = has_wiki - } - if delete_branch != none { - settings.merge_settings.delete_branch_on_merge = delete_branch - } - } - - // Validate settings - parsers.validate_settings(settings) or { - println('ERROR: Invalid settings: ${err}') - return error('Validation failed') - } - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories from target specification - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'github-settings', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Convert repo paths to owner/repo format (assumes repos in ~/Documents/hyperpolymath-repos/) - mut repo_names := []string{} - for repo_path in repos { - // Extract repo name from path - parts := repo_path.split(os.path_separator) - if parts.len > 0 { - repo_name := parts[parts.len - 1] - // Assume hyperpolymath organization - repo_names << 'hyperpolymath/${repo_name}' - } - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.remote_changes, - repo_names, - 'Apply GitHub repository settings', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.remote_changes, repo_names, 'github-settings', 'CANCELLED') - return - } - - println('Applying settings to ${repo_names.len} repositories...') - println('') - - // Execute settings update with rate limiting - start_time := time.now() - mut results := []github.SettingsResult{} - - for i, repo_name in repo_names { - if i > 0 { - safety_ctx.rate_limit() - } - - result := github.apply_settings(repo_name, settings, dry_run) or { - results << github.SettingsResult{ - repo_path: repo_name - success: false - message: 'Error: ${err}' - } - continue - } - - results << result - - if i % 10 == 0 && i > 0 { - println(' Progress: ${i}/${repo_names.len}') - } - } - - duration := time.since(start_time) - - // Print results - println('') - mut success := 0 - mut failed := 0 - for result in results { - if result.success { - success++ - println('✓ ${result.repo_path}: ${result.message}') - } else { - failed++ - println('✗ ${result.repo_path}: ${result.message}') - } - } - - println('') - summary := github.compute_summary(results) - github.print_summary(summary) - - println('Completed in ${duration.seconds():.2f}s') - - // Audit log - status := if failed > 0 { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.remote_changes, repo_names, 'github-settings', status) - - if failed > 0 { - return error('GitHub settings update completed with failures') - } -} - -fn cmd_wiki_setup(cmd cli.Command) ! { - targets := cmd.flags.get_string('targets')! - home_template := cmd.flags.get_string('home-template') or { '' } - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - println('Wiki Setup Operation') - println(' Targets: ${targets}') - println(' Home template: ${if home_template != '' { home_template } else { '(default)' }}') - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Check gh CLI authentication - if !github.check_gh_cli_installed()! { - println('ERROR: GitHub CLI (gh) is not installed') - println('Please install it: https://cli.github.com/') - return error('gh CLI not available') - } - - if !github.check_gh_auth()! { - println('ERROR: Not authenticated with GitHub CLI') - println('Please run: gh auth login') - return error('gh auth required') - } - - // Load home content from template or use default - mut home_content := '' - if home_template != '' { - home_content = os.read_file(home_template) or { - println('ERROR: Failed to read home template: ${err}') - return error('Template read failed') - } - } - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'wiki-setup', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Convert to owner/repo format - mut repo_names := []string{} - for repo_path in repos { - parts := repo_path.split(os.path_separator) - if parts.len > 0 { - repo_name := parts[parts.len - 1] - repo_names << 'hyperpolymath/${repo_name}' - - // Use default content if no template provided - if home_content == '' { - home_content = github.default_home_content(repo_name) - } - } - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.remote_changes, - repo_names, - 'Initialize wikis with first page', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.remote_changes, repo_names, 'wiki-setup', 'CANCELLED') - return - } - - println('Initializing wikis for ${repo_names.len} repositories...') - println('') - - // Execute wiki setup with rate limiting - start_time := time.now() - mut results := []github.WikiSetupResult{} - - for i, repo_name in repo_names { - if i > 0 { - safety_ctx.rate_limit() - } - - result := github.setup_wiki(github.WikiSetupParams{ - repo: repo_name - home_content: home_content - dry_run: dry_run - }) or { - results << github.WikiSetupResult{ - repo: repo_name - success: false - message: 'Error: ${err}' - } - continue - } - - results << result - - if i % 10 == 0 && i > 0 { - println(' Progress: ${i}/${repo_names.len}') - } - } - - duration := time.since(start_time) - - // Print summary - github.print_wiki_summary(results) - println('Completed in ${duration.seconds():.2f}s') - - // Check for failures - mut failed := 0 - for result in results { - if !result.success { - failed++ - } - } - - // Audit log - status := if failed > 0 { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.remote_changes, repo_names, 'wiki-setup', status) - - if failed > 0 { - return error('Wiki setup completed with failures') - } -} - -fn cmd_community_setup(cmd cli.Command) ! { - targets := cmd.flags.get_string('targets')! - template_dir := cmd.flags.get_string('template-dir') or { '' } - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - println('Community Health Files Setup') - println(' Targets: ${targets}') - println(' Templates: ${if template_dir != '' { template_dir } else { '(default)' }}') - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Get community files (default or from template directory) - mut files := []github.CommunityFile{} - if template_dir != '' { - // Load custom templates - println('Loading custom templates from ${template_dir}...') - - // Define expected files - template_files := [ - 'CODE_OF_CONDUCT.md', - 'CONTRIBUTING.md', - 'SECURITY.md', - 'SUPPORT.md', - 'FUNDING.yml', - ] - - for template_file in template_files { - file_path := os.join_path(template_dir, template_file) - if os.exists(file_path) { - content := os.read_file(file_path) or { - println('WARNING: Failed to read ${template_file}: ${err}') - continue - } - - target_path := if template_file.ends_with('.yml') { - '.github/${template_file}' - } else { - '.github/${template_file}' - } - - files << github.CommunityFile{ - path: target_path - content: content - name: template_file - } - } - } - - if files.len == 0 { - println('ERROR: No template files found in ${template_dir}') - return error('No templates found') - } - println('Loaded ${files.len} custom templates') - } else { - // Use default templates - files = github.standard_community_files() - println('Using default community file templates (${files.len} files)') - } - println('') - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'community-setup', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.local_changes, - repos, - 'Deploy community health files', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.local_changes, repos, 'community-setup', 'CANCELLED') - return - } - - println('Deploying community files to ${repos.len} repositories...') - println('') - - // Execute community setup with rate limiting - start_time := time.now() - mut results := []github.CommunitySetupResult{} - - for i, repo_path in repos { - if i > 0 { - safety_ctx.rate_limit() - } - - result := github.setup_community_files(github.CommunitySetupParams{ - repo_path: repo_path - files: files - dry_run: dry_run - }) or { - results << github.CommunitySetupResult{ - repo_path: repo_path - success: false - files_created: 0 - message: 'Error: ${err}' - } - continue - } - - results << result - - if i % 10 == 0 && i > 0 { - println(' Progress: ${i}/${repos.len}') - } - } - - duration := time.since(start_time) - - // Print summary - github.print_community_summary(results) - println('Completed in ${duration.seconds():.2f}s') - - // Check for failures - mut failed := 0 - for result in results { - if !result.success { - failed++ - } - } - - // Audit log - status := if failed > 0 { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.local_changes, repos, 'community-setup', status) - - if failed > 0 { - return error('Community setup completed with failures') - } -} - -fn cmd_templates_setup(cmd cli.Command) ! { - targets := cmd.flags.get_string('targets')! - template_dir := cmd.flags.get_string('template-dir') or { '' } - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - println('Issue & PR Templates Setup') - println(' Targets: ${targets}') - println(' Templates: ${if template_dir != '' { template_dir } else { '(default)' }}') - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Get templates (default or custom) - mut issue_templates := []github.IssueTemplate{} - mut pr_template := '' - - if template_dir != '' { - // Load custom templates - println('Loading custom templates from ${template_dir}...') - - // Try to load issue templates - issue_template_dir := os.join_path(template_dir, 'ISSUE_TEMPLATE') - if os.exists(issue_template_dir) { - files := os.ls(issue_template_dir) or { []string{} } - for file in files { - if file.ends_with('.yml') || file.ends_with('.md') { - file_path := os.join_path(issue_template_dir, file) - content := os.read_file(file_path) or { - println('WARNING: Failed to read ${file}') - continue - } - - issue_templates << github.IssueTemplate{ - name: file.replace('.yml', '').replace('.md', '') - filename: file - content: content - description: file - } - } - } - } - - // Try to load PR template - pr_template_path := os.join_path(template_dir, 'PULL_REQUEST_TEMPLATE.md') - if os.exists(pr_template_path) { - pr_template = os.read_file(pr_template_path) or { '' } - } - - println('Loaded ${issue_templates.len} issue templates') - } else { - // Use default templates - issue_templates = github.standard_issue_templates() - pr_template = github.default_pr_template() - println('Using default templates (${issue_templates.len} issue + 1 PR)') - } - println('') - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'templates-setup', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.local_changes, - repos, - 'Deploy issue and PR templates', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.local_changes, repos, 'templates-setup', 'CANCELLED') - return - } - - println('Deploying templates to ${repos.len} repositories...') - println('') - - // Execute templates setup with rate limiting - start_time := time.now() - mut results := []github.TemplatesSetupResult{} - - for i, repo_path in repos { - if i > 0 { - safety_ctx.rate_limit() - } - - result := github.setup_templates(github.TemplatesSetupParams{ - repo_path: repo_path - issue_templates: issue_templates - pr_template: pr_template - dry_run: dry_run - }) or { - results << github.TemplatesSetupResult{ - repo_path: repo_path - success: false - templates_created: 0 - message: 'Error: ${err}' - } - continue - } - - results << result - - if i % 10 == 0 && i > 0 { - println(' Progress: ${i}/${repos.len}') - } - } - - duration := time.since(start_time) - - // Print summary - github.print_templates_summary(results) - println('Completed in ${duration.seconds():.2f}s') - - // Check for failures - mut failed := 0 - for result in results { - if !result.success { - failed++ - } - } - - // Audit log - status := if failed > 0 { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.local_changes, repos, 'templates-setup', status) - - if failed > 0 { - return error('Templates setup completed with failures') - } -} - -fn cmd_discussions_setup(cmd cli.Command) ! { - targets := cmd.flags.get_string('targets')! - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - println('GitHub Discussions Setup') - println(' Targets: ${targets}') - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Check gh CLI authentication - if !github.check_gh_cli_installed()! { - println('ERROR: GitHub CLI (gh) is not installed') - println('Please install it: https://cli.github.com/') - return error('gh CLI not available') - } - - if !github.check_gh_auth()! { - println('ERROR: Not authenticated with GitHub CLI') - println('Please run: gh auth login') - return error('gh auth required') - } - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'discussions-setup', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Convert to owner/repo format - mut repo_names := []string{} - for repo_path in repos { - parts := repo_path.split(os.path_separator) - if parts.len > 0 { - repo_name := parts[parts.len - 1] - repo_names << 'hyperpolymath/${repo_name}' - } - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.read_only, - repo_names, - 'Check GitHub Discussions status', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.read_only, repo_names, 'discussions-setup', 'CANCELLED') - return - } - - println('Setting up discussions for ${repo_names.len} repositories...') - println('') - - // Execute discussions setup with rate limiting - categories := github.default_discussion_categories() - start_time := time.now() - mut results := []github.DiscussionsSetupResult{} - - for i, repo_name in repo_names { - if i > 0 { - safety_ctx.rate_limit() - } - - result := github.check_discussions_status(repo_name, categories, dry_run) or { - results << github.DiscussionsSetupResult{ - repo: repo_name - success: false - enabled: false - message: 'Error: ${err}' - } - continue - } - - results << result - - if i % 10 == 0 && i > 0 { - println(' Progress: ${i}/${repo_names.len}') - } - } - - duration := time.since(start_time) - - // Print summary - github.print_discussions_summary(results) - println('Completed in ${duration.seconds():.2f}s') - - println('') - println('Note: Discussions must be manually enabled in GitHub repository settings') - println('due to API limitations. This command verifies their status.') - - // Audit log - safety_ctx.audit(safety.OperationType.read_only, repo_names, 'discussions-setup', 'SUCCESS') -} - -fn cmd_pages_setup(cmd cli.Command) ! { - targets := cmd.flags.get_string('targets')! - source_str := cmd.flags.get_string('source') or { 'docs-main' } - cname := cmd.flags.get_string('cname') or { '' } - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - // Parse source parameter - source := match source_str { - 'root-main' { github.PagesSource.root_main } - 'docs-main' { github.PagesSource.docs_main } - 'gh-pages' { github.PagesSource.root_gh_pages } - else { github.PagesSource.docs_main } - } - - println('GitHub Pages Setup') - println(' Targets: ${targets}') - println(' Source: ${source_str}') - if cname != '' { - println(' Custom domain: ${cname}') - } - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - // Check gh CLI authentication - if !github.check_gh_cli_installed()! { - println('ERROR: GitHub CLI (gh) is not installed') - println('Please install it: https://cli.github.com/') - return error('gh CLI not available') - } - - if !github.check_gh_auth()! { - println('ERROR: Not authenticated with GitHub CLI') - println('Please run: gh auth login') - return error('gh auth required') - } - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'pages-setup', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Convert to owner/repo format - mut repo_names := []string{} - for repo_path in repos { - parts := repo_path.split(os.path_separator) - if parts.len > 0 { - repo_name := parts[parts.len - 1] - repo_names << 'hyperpolymath/${repo_name}' - } - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.remote_changes, - repo_names, - 'Enable GitHub Pages', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.remote_changes, repo_names, 'pages-setup', 'CANCELLED') - return - } - - println('Setting up Pages for ${repo_names.len} repositories...') - println('') - - // Execute Pages setup with rate limiting - start_time := time.now() - mut results := []github.PagesSetupResult{} - - for i, repo_name in repo_names { - if i > 0 { - safety_ctx.rate_limit() - } - - result := github.setup_pages(github.PagesSetupParams{ - repo: repo_name - source: source - cname: cname - dry_run: dry_run - }) or { - results << github.PagesSetupResult{ - repo: repo_name - success: false - url: '' - message: 'Error: ${err}' - } - continue - } - - results << result - - if i % 10 == 0 && i > 0 { - println(' Progress: ${i}/${repo_names.len}') - } - } - - duration := time.since(start_time) - - // Print summary - github.print_pages_summary(results) - println('Completed in ${duration.seconds():.2f}s') - - // Check for failures - mut failed := 0 - for result in results { - if !result.success { - failed++ - } - } - - // Audit log - status := if failed > 0 { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.remote_changes, repo_names, 'pages-setup', status) - - if failed > 0 { - return error('Pages setup completed with failures') - } -} - -fn cmd_template_merge(cmd cli.Command) ! { - targets := cmd.flags.get_string('targets')! - workflows := cmd.flags.get_bool('workflows') or { true } - scm_files := cmd.flags.get_bool('scm-files') or { true } - bot_directives := cmd.flags.get_bool('bot-directives') or { true } - contractiles := cmd.flags.get_bool('contractiles') or { true } - preserve := cmd.flags.get_bool('preserve') or { true } - dry_run := cmd.flags.get_bool('dry-run') or { false } - force := cmd.flags.get_bool('force') or { false } - - println('Template Merge Operation') - println(' Targets: ${targets}') - println(' Add workflows: ${workflows}') - println(' Add SCM files: ${scm_files}') - println(' Add bot directives: ${bot_directives}') - println(' Add contractiles: ${contractiles}') - println(' Preserve existing: ${preserve}') - println(' Dry Run: ${dry_run}') - println('') - - // Initialize safety system - mut safety_ctx := safety.new_safety_context() or { - println('âš ī¸ Failed to initialize safety system: ${err}') - println('âš ī¸ Proceeding with default strict safety') - safety.SafetyContext{ - config: safety.default_safety_config() - rate_limiter: safety.new_rate_limiter(100) - audit_log: safety.AuditLog{ log_file: '' } - enabled: true - } - } - safety_ctx.print_banner() - - if dry_run { - println('[DRY RUN] No changes will be made') - println('') - } - - // Resolve repositories - println('Resolving target repositories...') - repos := utils.resolve_targets(targets, default_repos_dir, 2) - println('Found ${repos.len} repositories') - println('') - - if repos.len == 0 { - println('No repositories found matching: ${targets}') - return - } - - // Pre-flight validation - validation_result := safety_ctx.validate(repos, 'template-merge', dry_run) or { - println('âš ī¸ Validation failed: ${err}') - return error('Validation error') - } - - safety.print_validation_result(validation_result) - - if !validation_result.passed { - return error('Pre-flight validation failed') - } - - // Safety check - should we proceed? - if !safety_ctx.should_proceed( - safety.OperationType.local_changes, - repos, - 'Convert to template form (preserve existing content)', - dry_run, - force - )! { - println('Operation cancelled by user or safety system') - safety_ctx.audit(safety.OperationType.local_changes, repos, 'template-merge', 'CANCELLED') - return - } - - // Build template merge config - config := templates.TemplateMergeConfig{ - add_github_workflows: workflows - add_scm_files: scm_files - add_bot_directives: bot_directives - add_contractiles: contractiles - add_justfile: true - add_editorconfig: true - preserve_existing: preserve - template_source: 'rsr-template-repo' - } - - println('Applying template merge to ${repos.len} repositories...') - println('') - - // Execute template merge with rate limiting - start_time := time.now() - mut results := []templates.TemplateMergeResult{} - - for i, repo_path in repos { - if i > 0 { - safety_ctx.rate_limit() - } - - result := templates.apply_template_merge(repo_path, config, dry_run) or { - results << templates.TemplateMergeResult{ - repo_path: repo_path - success: false - files_added: 0 - files_preserved: 0 - files_updated: 0 - message: 'Error: ${err}' - } - continue - } - - results << result - - // Progress indicator - if i % 10 == 0 && i > 0 { - println(' Progress: ${i}/${repos.len}') - } - } - - duration := time.since(start_time) - - // Print summary - println('') - println('Template Merge Summary') - println('=====================') - mut successful := 0 - mut failed := 0 - mut total_added := 0 - mut total_preserved := 0 - mut total_updated := 0 - - for result in results { - if result.success { - successful++ - total_added += result.files_added - total_preserved += result.files_preserved - total_updated += result.files_updated - } else { - failed++ - println(' ✗ ${result.repo_path}: ${result.message}') - } - } - - println('') - println('Results:') - println(' Successful: ${successful}') - println(' Failed: ${failed}') - println(' Files added: ${total_added}') - println(' Files preserved: ${total_preserved}') - println(' Files updated: ${total_updated}') - println('Completed in ${duration.seconds():.2f}s') - - // Audit log - status := if failed > 0 { 'PARTIAL_FAILURE' } else { 'SUCCESS' } - safety_ctx.audit(safety.OperationType.local_changes, repos, 'template-merge', status) - - if failed > 0 { - return error('Template merge completed with failures') - } -} diff --git a/scaffoldia/repo-batcher/src/v/main_simple.v b/scaffoldia/repo-batcher/src/v/main_simple.v deleted file mode 100644 index 88df907..0000000 --- a/scaffoldia/repo-batcher/src/v/main_simple.v +++ /dev/null @@ -1,315 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// repo-batcher - Simplified demo version -// Demonstrates core functionality without external dependencies - -module main - -import os - -fn main() { - args := os.args[1..] - - if args.len == 0 { - show_help() - return - } - - command := args[0] - - match command { - 'list-ops' { - cmd_list_operations() - } - 'git-sync' { - cmd_git_sync_demo(args[1..]) - } - 'license-update' { - cmd_license_update_demo(args[1..]) - } - 'workflow-update' { - cmd_workflow_update_demo(args[1..]) - } - 'file-replace' { - cmd_file_replace_demo(args[1..]) - } - 'spdx-audit' { - cmd_spdx_audit_demo(args[1..]) - } - 'scan' { - cmd_scan_repos() - } - 'version' { - println('repo-batcher v0.9.0') - println('Formally verified batch operations (Demo Mode)') - } - else { - println('Unknown command: ${command}') - println('') - show_help() - } - } -} - -fn show_help() { - println('repo-batcher v0.9.0 - Formally Verified Batch Operations') - println('') - println('Usage:') - println(' repo-batcher [options]') - println('') - println('Commands:') - println(' list-ops List available operations') - println(' scan Scan for repositories') - println(' git-sync Demo git batch sync') - println(' license-update Demo license update') - println(' workflow-update Demo workflow SHA pinning') - println(' file-replace Demo file replacement') - println(' spdx-audit Demo SPDX compliance audit') - println(' version Show version') - println('') - println('Full version requires:') - println(' - ATS2 compiler (for formal verification)') - println(' - V compiler properly configured') -} - -fn cmd_list_operations() { - println('Available Operations:') - println('') - println('✅ license-update Replace license headers and LICENSE files') - println(' Safety: Valid SPDX IDs, backup required') - println('') - println('✅ git-sync Batch commit and push across repos') - println(' Safety: Valid repos, no conflicts, remote reachable') - println('') - println('✅ file-replace Replace files matching pattern') - println(' Safety: Backup required, no circular replacements') - println('') - println('✅ workflow-update Update GitHub Actions with SHA pinning') - println(' Safety: Known SHA pins, backup required') - println('') - println('✅ spdx-audit Audit SPDX license headers') - println(' Safety: Read-only operation, comprehensive reporting') - println('') - println('✅ custom Execute custom operation from template') - println(' Safety: Template validation, dry-run enforced') - println('') - println('All operations formally verified with ATS2 dependent types!') -} - -fn cmd_scan_repos() { - println('Scanning for repositories...') - println('') - - repos_dir := os.join_path(os.home_dir(), 'Documents', 'hyperpolymath-repos') - - if !os.exists(repos_dir) { - println('Repos directory not found: ${repos_dir}') - return - } - - repos := find_git_repos(repos_dir, 2) - - println('Found ${repos.len} repositories:') - println('') - - for i, repo in repos { - if i < 10 { // Show first 10 - repo_name := os.file_name(repo) - println(' ${i + 1}. ${repo_name}') - } - } - - if repos.len > 10 { - println(' ... and ${repos.len - 10} more') - } - - println('') - println('Repository scanner working! ✓') -} - -fn find_git_repos(base_dir string, max_depth int) []string { - mut repos := []string{} - scan_for_repos(base_dir, 0, max_depth, mut repos) - return repos -} - -fn scan_for_repos(dir string, depth int, max_depth int, mut repos []string) { - if depth > max_depth { - return - } - - entries := os.ls(dir) or { return } - - for entry in entries { - full_path := os.join_path(dir, entry) - - if !os.is_dir(full_path) { - continue - } - - if entry == '.git' { - repos << dir - return - } - - scan_for_repos(full_path, depth + 1, max_depth, mut repos) - } -} - -fn cmd_git_sync_demo(args []string) { - println('Git Batch Sync Demo') - println('===================') - println('') - println('This would execute:') - println(' 1. Find all git repositories') - println(' 2. For each repo:') - println(' - git add .') - println(' - git commit -m "message"') - println(' - git push') - println(' 3. Run in parallel with 4-8 workers') - println(' 4. Report success/failure per repo') - println('') - println('Example:') - println(' Found 502 repositories') - println(' [0] ✓ repo-batcher (1/502)') - println(' [1] ✓ lithoglyph (2/502)') - println(' [2] ✓ gitvisor (3/502)') - println(' ...') - println('') - println('Performance: 8x faster than bash!') - println('Safety: Formally verified with ATS2 ✓') -} - -fn cmd_license_update_demo(args []string) { - println('License Update Demo') - println('===================') - println('') - println('This would execute:') - println(' 1. Validate SPDX identifiers (ATS2 proofs)') - println(' 2. For each repository:') - println(' - Backup existing LICENSE file') - println(' - Replace LICENSE file') - println(' - Update SPDX headers in source files') - println(' 3. Run with 4 parallel workers') - println(' 4. Report results') - println('') - println('Example:') - println(' Old: MIT') - println(' New: PMPL-1.0-or-later') - println(' Targets: @all-repos') - println(' ') - println(' ✓ Updated: repo-batcher') - println(' ✓ Updated: lithoglyph') - println(' ✓ Updated: gitvisor') - println(' ...') - println('') - println('Safety: Type-safe operations with automatic backups ✓') -} - -fn cmd_workflow_update_demo(args []string) { - println('Workflow Update Demo (SHA Pinning)') - println('==================================') - println('') - println('This would execute:') - println(' 1. Find all .github/workflows/*.yml files') - println(' 2. For each workflow file:') - println(' - Identify GitHub Actions references') - println(' - Replace version tags with commit SHAs') - println(' - Preserve original version in comments') - println(' 3. Use pinned SHAs from hyperpolymath standards') - println(' 4. Create backups before changes') - println('') - println('Example:') - println(' Before: uses: actions/checkout@v4') - println(' After: uses: actions/checkout@34e114876b0b... # v4') - println('') - println(' Pinned actions:') - println(' actions/checkout@v4 → 34e114876b0b...') - println(' github/codeql-action@v3 → 6624720a57d4...') - println(' ossf/scorecard-action@v2.4.0 → 62b2cac7ed81...') - println(' + 15 more standard actions') - println('') - println(' ✓ Updated: .github/workflows/codeql.yml (3 actions)') - println(' ✓ Updated: .github/workflows/scorecard.yml (2 actions)') - println(' ✓ Updated: .github/workflows/quality.yml (4 actions)') - println(' ...') - println('') - println('Safety: Prevents supply chain attacks with commit pinning ✓') - println('Standard: Hyperpolymath GitHub Actions SHA database (2026-02-04)') -} - -fn cmd_file_replace_demo(args []string) { - println('File Replace Demo') - println('=================') - println('') - println('This would execute:') - println(' 1. Find files matching pattern across repositories') - println(' 2. For each matching file:') - println(' - Create backup if requested') - println(' - Replace with template file') - println(' - Validate no circular replacements') - println(' 3. Run with 4 parallel workers') - println(' 4. Report results') - println('') - println('Example:') - println(' Pattern: .github/workflows/ci.yml') - println(' Replacement: ~/templates/new-ci.yml') - println(' Targets: @all-repos') - println(' ') - println(' ✓ Replaced: repo-batcher/.github/workflows/ci.yml') - println(' ✓ Replaced: lithoglyph/.github/workflows/ci.yml') - println(' ✓ Replaced: gitvisor/.github/workflows/ci.yml') - println(' ⚠ Skipped: formdb (circular replacement detected)') - println(' ...') - println('') - println('Use cases:') - println(' - Standardize CI/CD workflows') - println(' - Update configuration files') - println(' - Replace deprecated templates') - println(' - Sync common files (.editorconfig, .gitignore)') - println('') - println('Safety: Circular replacement detection with FNV-1a hash ✓') -} - -fn cmd_spdx_audit_demo(args []string) { - println('SPDX Audit Demo') - println('===============') - println('') - println('This would execute:') - println(' 1. Scan all source files (30+ extensions)') - println(' 2. Check for SPDX-License-Identifier headers') - println(' 3. Validate SPDX identifiers') - println(' 4. Track PMPL-1.0-or-later compliance') - println(' 5. Generate compliance report') - println('') - println('Example output:') - println(' ') - println(' === SPDX Audit Results ===') - println(' Total repositories: 574') - println(' ') - println(' Repository: repo-batcher') - println(' Compliance: 100%') - println(' Total files: 42') - println(' With SPDX: 42') - println(' PMPL-1.0-or-later: 42') - println(' ') - println(' Repository: legacy-project') - println(' Compliance: 45%') - println(' Total files: 120') - println(' With SPDX: 54') - println(' Without SPDX: 66') - println(' PMPL-1.0-or-later: 54') - println(' ') - println(' === Summary ===') - println(' Total files scanned: 12,847') - println(' With SPDX headers: 11,203 (87%)') - println(' Without SPDX headers: 1,644 (13%)') - println(' PMPL-1.0-or-later: 10,891 (85%)') - println(' Overall compliance: 87%') - println('') - println('Supported extensions: .rs .v .c .h .cpp .js .ts .py .go .java') - println(' .kt .ml .ex .gleam .dats .idr .zig .sh') - println(' .yml .toml .scm .jl .ad + more') - println('') - println('Safety: Read-only operation, no modifications ✓') -} diff --git a/scaffoldia/repo-batcher/src/v/parsers/settings_toml.v b/scaffoldia/repo-batcher/src/v/parsers/settings_toml.v deleted file mode 100644 index 28c10dd..0000000 --- a/scaffoldia/repo-batcher/src/v/parsers/settings_toml.v +++ /dev/null @@ -1,106 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Settings TOML Parser -// Parses GitHub repository settings from TOML configuration files - -module parsers - -import toml -import github - -// Parse GitHub settings from TOML file -pub fn parse_settings_toml(file_path string) !github.GitHubSettings { - content := toml.parse_file(file_path) or { - return error('Failed to parse TOML file: ${err}') - } - - mut settings := github.GitHubSettings{ - repo_features: github.RepoFeatures{} - merge_settings: github.MergeSettings{} - } - - // Parse repository features - if repo_table := content.value('repository') { - if repo_map := repo_table.as_map() { - settings.repo_features.has_issues = parse_bool_option(repo_map, 'has_issues') - settings.repo_features.has_wiki = parse_bool_option(repo_map, 'has_wiki') - settings.repo_features.has_projects = parse_bool_option(repo_map, 'has_projects') - settings.repo_features.has_downloads = parse_bool_option(repo_map, 'has_downloads') - } - } - - // Parse merge settings - if merge_table := content.value('merge') { - if merge_map := merge_table.as_map() { - settings.merge_settings.allow_squash_merge = parse_bool_option(merge_map, 'allow_squash_merge') - settings.merge_settings.allow_merge_commit = parse_bool_option(merge_map, 'allow_merge_commit') - settings.merge_settings.allow_rebase_merge = parse_bool_option(merge_map, 'allow_rebase_merge') - settings.merge_settings.delete_branch_on_merge = parse_bool_option(merge_map, 'delete_branch_on_merge') - settings.merge_settings.allow_auto_merge = parse_bool_option(merge_map, 'allow_auto_merge') - } - } - - return settings -} - -// Parse boolean option from TOML map -fn parse_bool_option(map map[string]toml.Any, key string) ?bool { - if val := map[key] { - if b := val.bool() { - return b - } - } - return none -} - -// Validate settings configuration -pub fn validate_settings(settings github.GitHubSettings) !bool { - mut changes := 0 - - // Count repository feature changes - if _ := settings.repo_features.has_issues { changes++ } - if _ := settings.repo_features.has_wiki { changes++ } - if _ := settings.repo_features.has_projects { changes++ } - if _ := settings.repo_features.has_downloads { changes++ } - - // Count merge setting changes - if _ := settings.merge_settings.allow_squash_merge { changes++ } - if _ := settings.merge_settings.allow_merge_commit { changes++ } - if _ := settings.merge_settings.allow_rebase_merge { changes++ } - if _ := settings.merge_settings.delete_branch_on_merge { changes++ } - if _ := settings.merge_settings.allow_auto_merge { changes++ } - - // Must have at least one change - if changes == 0 { - return error('No settings changes specified') - } - - // Too many changes might be dangerous - if changes > 20 { - return error('Too many simultaneous changes (${changes} > 20)') - } - - return true -} - -// Generate example settings TOML -pub fn generate_example_toml() string { - return '# GitHub Repository Settings Configuration -# -# All settings are optional. Omit a setting to leave it unchanged. -# Use true/false to enable/disable features. - -[repository] -has_issues = true -has_wiki = false -has_projects = true -has_downloads = false - -[merge] -allow_squash_merge = true -allow_merge_commit = false -allow_rebase_merge = false -delete_branch_on_merge = true -allow_auto_merge = false -' -} diff --git a/scaffoldia/repo-batcher/src/v/rollback/backup_manager.v b/scaffoldia/repo-batcher/src/v/rollback/backup_manager.v deleted file mode 100644 index 6742470..0000000 --- a/scaffoldia/repo-batcher/src/v/rollback/backup_manager.v +++ /dev/null @@ -1,265 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Backup Manager -// Tracks and manages backups for rollback support - -module rollback - -import os -import time -import json - -// BackupEntry represents a single backed up file -struct BackupEntry { -pub mut: - original_path string - backup_path string - timestamp i64 - checksum string -} - -// OperationBackup tracks all backups for an operation -pub struct OperationBackup { -pub mut: - operation_id string - operation_type string - timestamp i64 - repos []string - entries []BackupEntry - metadata map[string]string -} - -// BackupManager handles backup creation and restoration -pub struct BackupManager { -pub mut: - backup_dir string - log_file string -} - -// Creates new backup manager -pub fn new_backup_manager() BackupManager { - home := os.home_dir() - backup_dir := os.join_path(home, '.local', 'share', 'repo-batcher', 'backups') - log_file := os.join_path(home, '.local', 'share', 'repo-batcher', 'backup-log.json') - - // Create backup directory if needed - os.mkdir_all(backup_dir) or {} - - return BackupManager{ - backup_dir: backup_dir - log_file: log_file - } -} - -// Starts new operation backup -pub fn (mut mgr BackupManager) start_operation(op_type string, repos []string) OperationBackup { - timestamp := time.now().unix - op_id := '${op_type}-${timestamp}' - - return OperationBackup{ - operation_id: op_id - operation_type: op_type - timestamp: timestamp - repos: repos - entries: []BackupEntry{} - metadata: map[string]string{} - } -} - -// Backs up a file before modification -pub fn (mut mgr BackupManager) backup_file(mut op_backup OperationBackup, file_path string) !string { - // Check file exists - if !os.exists(file_path) { - return error('File does not exist: ${file_path}') - } - - // Create backup path - timestamp := time.now().unix - file_name := os.file_name(file_path) - backup_name := '${file_name}.${timestamp}.backup' - backup_path := os.join_path(mgr.backup_dir, op_backup.operation_id, backup_name) - - // Ensure backup directory exists - backup_subdir := os.join_path(mgr.backup_dir, op_backup.operation_id) - os.mkdir_all(backup_subdir) or { - return error('Failed to create backup directory: ${err}') - } - - // Copy file to backup location - os.cp(file_path, backup_path) or { - return error('Failed to backup file: ${err}') - } - - // Calculate checksum - content := os.read_file(backup_path) or { '' } - checksum := calculate_checksum(content) - - // Record backup entry - entry := BackupEntry{ - original_path: file_path - backup_path: backup_path - timestamp: timestamp - checksum: checksum - } - - op_backup.entries << entry - - return backup_path -} - -// Completes operation backup and saves log -pub fn (mut mgr BackupManager) complete_operation(op_backup OperationBackup) ! { - // Save operation backup to log - mgr.append_to_log(op_backup) or { - return error('Failed to save backup log: ${err}') - } -} - -// Appends operation to backup log -fn (mut mgr BackupManager) append_to_log(op_backup OperationBackup) ! { - // Read existing log - mut operations := []OperationBackup{} - if os.exists(mgr.log_file) { - content := os.read_file(mgr.log_file) or { '[]' } - operations = json.decode([]OperationBackup, content) or { []OperationBackup{} } - } - - // Append new operation - operations << op_backup - - // Write back - json_data := json.encode(operations) - os.write_file(mgr.log_file, json_data) or { - return error('Failed to write log: ${err}') - } -} - -// Lists recent operations -pub fn (mgr BackupManager) list_operations(limit int) []OperationBackup { - if !os.exists(mgr.log_file) { - return []OperationBackup{} - } - - content := os.read_file(mgr.log_file) or { return []OperationBackup{} } - operations := json.decode([]OperationBackup, content) or { return []OperationBackup{} } - - // Return last N operations - start := if operations.len > limit { operations.len - limit } else { 0 } - return operations[start..] -} - -// Gets operation by ID -pub fn (mgr BackupManager) get_operation(op_id string) ?OperationBackup { - if !os.exists(mgr.log_file) { - return none - } - - content := os.read_file(mgr.log_file) or { return none } - operations := json.decode([]OperationBackup, content) or { return none } - - for op in operations { - if op.operation_id == op_id { - return op - } - } - - return none -} - -// Restores files from operation backup -pub fn (mut mgr BackupManager) restore_operation(op_id string) ! { - op_backup := mgr.get_operation(op_id) or { - return error('Operation not found: ${op_id}') - } - - println('Restoring operation: ${op_backup.operation_type} (${op_backup.operation_id})') - println('Backed up files: ${op_backup.entries.len}') - println('') - - mut restored := 0 - mut failed := 0 - - for entry in op_backup.entries { - // Verify backup exists - if !os.exists(entry.backup_path) { - println('✗ Backup missing: ${entry.original_path}') - failed++ - continue - } - - // Verify checksum - content := os.read_file(entry.backup_path) or { - println('✗ Failed to read backup: ${entry.original_path}') - failed++ - continue - } - - checksum := calculate_checksum(content) - if checksum != entry.checksum { - println('✗ Checksum mismatch: ${entry.original_path}') - failed++ - continue - } - - // Restore file - os.cp(entry.backup_path, entry.original_path) or { - println('✗ Failed to restore: ${entry.original_path}') - failed++ - continue - } - - println('✓ Restored: ${entry.original_path}') - restored++ - } - - println('') - println('Restored: ${restored}') - println('Failed: ${failed}') - println('Total: ${op_backup.entries.len}') - - if failed > 0 { - return error('Rollback completed with ${failed} failures') - } -} - -// Restores last operation -pub fn (mut mgr BackupManager) restore_last() ! { - operations := mgr.list_operations(1) - if operations.len == 0 { - return error('No operations to restore') - } - - last_op := operations[0] - mgr.restore_operation(last_op.operation_id)! -} - -// Simple checksum calculation (FNV-1a hash) -fn calculate_checksum(data string) string { - mut hash := u32(2166136261) - for c in data { - hash ^= u32(c) - hash *= 16777619 - } - return hash.hex() -} - -// Cleans up old backups (older than days) -pub fn (mut mgr BackupManager) cleanup_old_backups(days int) ! { - cutoff := time.now().unix - i64(days * 86400) - - operations := mgr.list_operations(1000) - mut cleaned := 0 - - for op in operations { - if op.timestamp < cutoff { - // Remove backup directory - backup_dir := os.join_path(mgr.backup_dir, op.operation_id) - if os.exists(backup_dir) { - os.rmdir_all(backup_dir) or {} - cleaned++ - } - } - } - - println('Cleaned up ${cleaned} old backup(s)') -} diff --git a/scaffoldia/repo-batcher/src/v/safety/guards.v b/scaffoldia/repo-batcher/src/v/safety/guards.v deleted file mode 100644 index 4870ad5..0000000 --- a/scaffoldia/repo-batcher/src/v/safety/guards.v +++ /dev/null @@ -1,337 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Safety Guards -// Comprehensive safety features for bulk repository operations - -module safety - -import os -import time - -// SafetyLevel defines how strict safety checks are -pub enum SafetyLevel { - paranoid // Maximum safety, confirm everything - strict // Standard safety checks - relaxed // Minimal safety checks - disabled // No safety checks (dangerous!) -} - -// OperationType categorizes risk level -pub enum OperationType { - read_only // No changes (audit, list) - local_changes // Local file changes only - remote_changes // Pushes to remote - destructive // Delete, force-push, etc. -} - -// SafetyConfig holds safety configuration -pub struct SafetyConfig { -pub mut: - level SafetyLevel - require_dry_run bool // Force dry-run first - max_repos int // Maximum repos per operation - confirm_threshold int // Require confirmation if > N repos - rate_limit_ms int // Milliseconds between operations - backup_before bool // Create backup before changes - audit_log bool // Log all operations - exclusion_list []string // Never touch these repos -} - -// OperationRequest represents a pending operation -pub struct OperationRequest { -pub: - operation_type OperationType - target_repos []string - description string - dry_run bool - force bool -} - -// SafetyCheck result -pub struct SafetyCheckResult { -pub: - approved bool - warnings []string - blockers []string - requires_confirm bool - message string -} - -// Default safety configuration -pub fn default_safety_config() SafetyConfig { - return SafetyConfig{ - level: .strict - require_dry_run: true - max_repos: 100 - confirm_threshold: 10 - rate_limit_ms: 100 - backup_before: true - audit_log: true - exclusion_list: [ - '.git', - 'node_modules', - 'vendor', - '.env', - '.secrets', - ] - } -} - -// Paranoid safety configuration -pub fn paranoid_safety_config() SafetyConfig { - return SafetyConfig{ - level: .paranoid - require_dry_run: true - max_repos: 50 - confirm_threshold: 5 - rate_limit_ms: 500 - backup_before: true - audit_log: true - exclusion_list: [ - '.git', - 'node_modules', - 'vendor', - '.env', - '.secrets', - '*.pem', - '*.key', - ] - } -} - -// Check if operation is safe to proceed -pub fn check_operation_safety(request OperationRequest, config SafetyConfig) !SafetyCheckResult { - mut warnings := []string{} - mut blockers := []string{} - mut requires_confirm := false - - // Check 1: Dry-run requirement - if config.require_dry_run && !request.dry_run { - if request.operation_type == .destructive { - blockers << 'Destructive operations require dry-run first' - } else if request.operation_type == .remote_changes { - warnings << 'Remote changes without dry-run - proceed with caution' - requires_confirm = true - } - } - - // Check 2: Repository count limits - repo_count := request.target_repos.len - if repo_count > config.max_repos { - blockers << 'Operation targets ${repo_count} repos (max: ${config.max_repos})' - } - - if repo_count > config.confirm_threshold { - warnings << 'Operation targets ${repo_count} repositories' - requires_confirm = true - } - - // Check 3: Destructive operation check - if request.operation_type == .destructive && !request.force { - blockers << 'Destructive operations require --force flag' - } - - // Check 4: Exclusion list - mut excluded_repos := []string{} - for repo in request.target_repos { - for exclusion in config.exclusion_list { - if repo.contains(exclusion) { - excluded_repos << repo - } - } - } - - if excluded_repos.len > 0 { - blockers << 'Some repos match exclusion list: ${excluded_repos}' - } - - // Check 5: Safety level specific checks - match config.level { - .paranoid { - if repo_count > 5 { - requires_confirm = true - warnings << 'Paranoid mode: Confirm batch operation' - } - } - .strict { - if request.operation_type != .read_only && !request.dry_run { - requires_confirm = true - } - } - .relaxed { - // Minimal checks - } - .disabled { - // No checks - } - } - - // Determine approval - approved := blockers.len == 0 && (config.level == .disabled || !requires_confirm || request.force) - - // Generate message - mut message := '' - if blockers.len > 0 { - message = 'Operation BLOCKED: ${blockers.join(', ')}' - } else if warnings.len > 0 { - message = 'Warnings: ${warnings.join(', ')}' - } else { - message = 'Operation safety checks passed' - } - - return SafetyCheckResult{ - approved: approved - warnings: warnings - blockers: blockers - requires_confirm: requires_confirm - message: message - } -} - -// Prompt user for confirmation -pub fn prompt_confirmation(request OperationRequest, warnings []string) !bool { - println('') - println('=== OPERATION CONFIRMATION REQUIRED ===') - println('Operation: ${request.description}') - println('Type: ${request.operation_type}') - println('Target repos: ${request.target_repos.len}') - println('Dry run: ${request.dry_run}') - println('') - - if warnings.len > 0 { - println('âš ī¸ WARNINGS:') - for warning in warnings { - println(' - ${warning}') - } - println('') - } - - if request.target_repos.len <= 10 { - println('Repositories:') - for repo in request.target_repos { - println(' - ${repo}') - } - } else { - println('First 5 repositories:') - for i := 0; i < 5 && i < request.target_repos.len; i++ { - println(' - ${request.target_repos[i]}') - } - println(' ... and ${request.target_repos.len - 5} more') - } - - println('') - print('Proceed with this operation? [y/N]: ') - - // Read user input - input := os.input('').trim().to_lower() - - return input == 'y' || input == 'yes' -} - -// Rate limiter to avoid hammering APIs -pub struct RateLimiter { -mut: - last_operation_time i64 - delay_ms int -} - -pub fn new_rate_limiter(delay_ms int) RateLimiter { - return RateLimiter{ - last_operation_time: 0 - delay_ms: delay_ms - } -} - -pub fn (mut limiter RateLimiter) wait() { - if limiter.last_operation_time > 0 { - now := time.now().unix_milli() - elapsed := now - limiter.last_operation_time - - if elapsed < limiter.delay_ms { - sleep_time := limiter.delay_ms - int(elapsed) - time.sleep(sleep_time * time.millisecond) - } - } - - limiter.last_operation_time = time.now().unix_milli() -} - -// Audit logger -pub struct AuditLog { -mut: - log_file string -} - -pub fn new_audit_log(log_file string) !AuditLog { - // Ensure parent directory exists - log_dir := os.dir(log_file) - os.mkdir_all(log_dir) or { - return error('Failed to create audit log directory: ${err}') - } - - return AuditLog{ - log_file: log_file - } -} - -pub fn (mut log AuditLog) record(request OperationRequest, result string) ! { - timestamp := time.now().format_ss() - - entry := '${timestamp} | ${request.operation_type} | ${request.target_repos.len} repos | ${request.description} | ${result}\n' - - mut f := os.open_append(log.log_file) or { - return error('Failed to open audit log: ${err}') - } - defer { f.close() } - - f.write_string(entry) or { - return error('Failed to write to audit log: ${err}') - } -} - -// Check if repo should be excluded -pub fn is_repo_excluded(repo_path string, exclusions []string) bool { - for exclusion in exclusions { - if repo_path.contains(exclusion) { - return true - } - } - return false -} - -// Validate repository count -pub fn validate_repo_count(count int, max int) !bool { - if count == 0 { - return error('No repositories specified') - } - - if count > max { - return error('Repository count (${count}) exceeds maximum (${max})') - } - - return true -} - -// Generate operation summary for review -pub fn generate_operation_summary(request OperationRequest) string { - mut summary := '=== Operation Summary ===\n' - summary += 'Description: ${request.description}\n' - summary += 'Type: ${request.operation_type}\n' - summary += 'Repositories: ${request.target_repos.len}\n' - summary += 'Dry run: ${request.dry_run}\n' - - if request.target_repos.len <= 20 { - summary += '\nRepositories:\n' - for repo in request.target_repos { - summary += ' - ${repo}\n' - } - } else { - summary += '\nFirst 10 repositories:\n' - for i := 0; i < 10; i++ { - summary += ' - ${request.target_repos[i]}\n' - } - summary += ' ... and ${request.target_repos.len - 10} more\n' - } - - return summary -} diff --git a/scaffoldia/repo-batcher/src/v/safety/integration.v b/scaffoldia/repo-batcher/src/v/safety/integration.v deleted file mode 100644 index c847ad1..0000000 --- a/scaffoldia/repo-batcher/src/v/safety/integration.v +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Safety Integration -// Bridge between CLI operations and safety system - -module safety - -import os -import toml - -// SafetyContext holds safety state for an operation -pub struct SafetyContext { -pub mut: - config SafetyConfig - rate_limiter RateLimiter - audit_log AuditLog - enabled bool -} - -// Load safety config from .safety.toml or use defaults -pub fn load_safety_config() !SafetyConfig { - config_path := '.safety.toml' - - if !os.exists(config_path) { - println('âš ī¸ No .safety.toml found, using default strict safety') - return default_safety_config() - } - - doc := toml.parse_file(config_path) or { - println('âš ī¸ Failed to parse .safety.toml: ${err}') - println('âš ī¸ Using default strict safety') - return default_safety_config() - } - - // Parse [safety] section - safety_table := doc.value('safety') - - mut config := default_safety_config() - - // Parse safety level - if level_str := safety_table.value('level').string() { - config.level = match level_str { - 'paranoid' { SafetyLevel.paranoid } - 'strict' { SafetyLevel.strict } - 'relaxed' { SafetyLevel.relaxed } - 'disabled' { SafetyLevel.disabled } - else { SafetyLevel.strict } - } - } - - // Parse other settings - if require_dry_run := safety_table.value('require_dry_run').bool() { - config.require_dry_run = require_dry_run - } - - if max_repos := safety_table.value('max_repos').i64() { - config.max_repos = int(max_repos) - } - - if confirm_threshold := safety_table.value('confirm_threshold').i64() { - config.confirm_threshold = int(confirm_threshold) - } - - if rate_limit_ms := safety_table.value('rate_limit_ms').i64() { - config.rate_limit_ms = int(rate_limit_ms) - } - - if backup_before := safety_table.value('backup_before').bool() { - config.backup_before = backup_before - } - - if audit_log := safety_table.value('audit_log').bool() { - config.audit_log = audit_log - } - - // Parse exclusions - if exclusions_table := doc.value('exclusions') { - if patterns := exclusions_table.value('patterns').array() { - config.exclusion_list = [] - for pattern in patterns { - if pattern_str := pattern.string() { - config.exclusion_list << pattern_str - } - } - } - - if repositories := exclusions_table.value('repositories').array() { - for repo in repositories { - if repo_str := repo.string() { - config.exclusion_list << repo_str - } - } - } - } - - return config -} - -// Create safety context for an operation -pub fn new_safety_context() !SafetyContext { - config := load_safety_config()! - - // Expand ~ in audit log path - mut audit_log_file := config.audit_log ? os.expand_tilde_to_home('~/.local/share/repo-batcher/audit.log') : '' - - mut audit_log := if config.audit_log { - new_audit_log(audit_log_file) or { - println('âš ī¸ Failed to create audit log: ${err}') - AuditLog{ log_file: '' } - } - } else { - AuditLog{ log_file: '' } - } - - rate_limiter := new_rate_limiter(config.rate_limit_ms) - - return SafetyContext{ - config: config - rate_limiter: rate_limiter - audit_log: audit_log - enabled: config.level != .disabled - } -} - -// Check if operation is safe to proceed -pub fn (ctx &SafetyContext) check_operation( - operation_type OperationType, - target_repos []string, - description string, - dry_run bool, - force bool -) !SafetyCheckResult { - if !ctx.enabled { - return SafetyCheckResult{ - approved: true - warnings: [] - blockers: [] - requires_confirm: false - message: 'Safety disabled' - } - } - - request := OperationRequest{ - operation_type: operation_type - target_repos: target_repos - description: description - dry_run: dry_run - force: force - } - - return check_operation_safety(request, ctx.config) -} - -// Run validation rules -pub fn (ctx &SafetyContext) validate( - repo_paths []string, - operation_type string, - dry_run bool -) !ValidationResult { - if !ctx.enabled { - return ValidationResult{ - passed: true - errors: [] - warnings: [] - info: [] - } - } - - validation_ctx := ValidationContext{ - repo_paths: repo_paths - operation_type: operation_type - dry_run: dry_run - } - - rules := get_common_validation_rules() - return run_validation(validation_ctx, rules) -} - -// Prompt user for confirmation -pub fn (ctx &SafetyContext) confirm_operation( - operation_type OperationType, - target_repos []string, - description string, - dry_run bool, - warnings []string -) !bool { - if !ctx.enabled { - return true - } - - request := OperationRequest{ - operation_type: operation_type - target_repos: target_repos - description: description - dry_run: dry_run - force: false - } - - return prompt_confirmation(request, warnings) -} - -// Wait with rate limiting -pub fn (mut ctx SafetyContext) rate_limit() { - if ctx.enabled && ctx.config.rate_limit_ms > 0 { - ctx.rate_limiter.wait() - } -} - -// Record operation in audit log -pub fn (mut ctx SafetyContext) audit( - operation_type OperationType, - target_repos []string, - description string, - result string -) { - if ctx.enabled && ctx.config.audit_log { - request := OperationRequest{ - operation_type: operation_type - target_repos: target_repos - description: description - dry_run: false - force: false - } - - ctx.audit_log.record(request, result) or { - println('âš ī¸ Failed to write audit log: ${err}') - } - } -} - -// Print safety banner -pub fn (ctx &SafetyContext) print_banner() { - if !ctx.enabled { - println('âš ī¸ SAFETY DISABLED - NO PROTECTION!') - println('') - return - } - - level_str := match ctx.config.level { - .paranoid { 'đŸ›Ąī¸ PARANOID (maximum safety)' } - .strict { '🔒 STRICT (recommended)' } - .relaxed { '⚡ RELAXED (minimal checks)' } - .disabled { 'âš ī¸ DISABLED (no protection)' } - } - - println('Safety Level: ${level_str}') - println(' Max repos: ${ctx.config.max_repos}') - println(' Confirm threshold: ${ctx.config.confirm_threshold}') - println(' Rate limit: ${ctx.config.rate_limit_ms}ms') - println('') -} - -// Check if operation should be allowed -pub fn (ctx &SafetyContext) should_proceed( - operation_type OperationType, - target_repos []string, - description string, - dry_run bool, - force bool -) !bool { - if !ctx.enabled { - return true - } - - // Check safety - check_result := ctx.check_operation(operation_type, target_repos, description, dry_run, force)! - - // Print blockers if any - if check_result.blockers.len > 0 { - println('') - println('❌ OPERATION BLOCKED:') - for blocker in check_result.blockers { - println(' - ${blocker}') - } - println('') - return false - } - - // If confirmation required and not forced - if check_result.requires_confirm && !force { - return ctx.confirm_operation(operation_type, target_repos, description, dry_run, check_result.warnings)! - } - - return check_result.approved -} diff --git a/scaffoldia/repo-batcher/src/v/safety/validation.v b/scaffoldia/repo-batcher/src/v/safety/validation.v deleted file mode 100644 index a12d001..0000000 --- a/scaffoldia/repo-batcher/src/v/safety/validation.v +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Safety Validation -// Pre-flight validation for operations - -module safety - -import os - -// ValidationRule represents a safety validation rule -pub struct ValidationRule { -pub: - name string - description string - check_fn fn (ValidationContext) !bool - severity RuleSeverity -} - -pub enum RuleSeverity { - info // Informational only - warning // Warning but can proceed - error // Blocks operation - critical // Critical blocker -} - -// ValidationContext provides context for validation -pub struct ValidationContext { -pub: - repo_paths []string - operation_type string - dry_run bool -} - -// ValidationResult contains validation outcome -pub struct ValidationResult { -pub: - passed bool - errors []string - warnings []string - info []string -} - -// Common validation rules -pub fn get_common_validation_rules() []ValidationRule { - return [ - ValidationRule{ - name: 'git_repository_check' - description: 'Verify all paths are git repositories' - check_fn: validate_git_repositories - severity: .error - }, - ValidationRule{ - name: 'write_permission_check' - description: 'Verify write permissions' - check_fn: validate_write_permissions - severity: .error - }, - ValidationRule{ - name: 'uncommitted_changes_check' - description: 'Check for uncommitted changes' - check_fn: validate_no_uncommitted_changes - severity: .warning - }, - ValidationRule{ - name: 'remote_exists_check' - description: 'Verify remote exists for push operations' - check_fn: validate_remote_exists - severity: .warning - }, - ValidationRule{ - name: 'disk_space_check' - description: 'Verify sufficient disk space' - check_fn: validate_disk_space - severity: .warning - }, - ] -} - -// Validate that all paths are git repositories -fn validate_git_repositories(ctx ValidationContext) !bool { - for repo_path in ctx.repo_paths { - git_dir := os.join_path(repo_path, '.git') - if !os.exists(git_dir) { - return error('Not a git repository: ${repo_path}') - } - } - return true -} - -// Validate write permissions -fn validate_write_permissions(ctx ValidationContext) !bool { - if ctx.dry_run { - return true // Skip for dry-run - } - - for repo_path in ctx.repo_paths { - // Try to create a temp file to test write permission - test_file := os.join_path(repo_path, '.repo-batcher-test') - - os.write_file(test_file, 'test') or { - return error('No write permission: ${repo_path}') - } - - os.rm(test_file) or {} - } - - return true -} - -// Check for uncommitted changes -fn validate_no_uncommitted_changes(ctx ValidationContext) !bool { - if ctx.dry_run { - return true - } - - mut repos_with_changes := []string{} - - for repo_path in ctx.repo_paths { - result := os.execute('cd "${repo_path}" && git status --porcelain') - if result.exit_code == 0 && result.output.trim() != '' { - repos_with_changes << repo_path - } - } - - if repos_with_changes.len > 0 { - return error('Uncommitted changes in ${repos_with_changes.len} repositories') - } - - return true -} - -// Validate remote exists for push operations -fn validate_remote_exists(ctx ValidationContext) !bool { - if !ctx.operation_type.contains('sync') && !ctx.operation_type.contains('push') { - return true // Only relevant for push operations - } - - mut repos_without_remote := []string{} - - for repo_path in ctx.repo_paths { - result := os.execute('cd "${repo_path}" && git remote -v') - if result.exit_code != 0 || result.output.trim() == '' { - repos_without_remote << repo_path - } - } - - if repos_without_remote.len > 0 { - return error('No remote configured in ${repos_without_remote.len} repositories') - } - - return true -} - -// Validate sufficient disk space -fn validate_disk_space(ctx ValidationContext) !bool { - if ctx.dry_run { - return true - } - - // Get disk usage for home directory - result := os.execute('df -h ~ | tail -1 | awk \'{print $5}\' | sed \'s/%//\'') - - if result.exit_code == 0 { - usage := result.output.trim().int() - if usage > 90 { - return error('Disk usage at ${usage}% (>90%)') - } - } - - return true -} - -// Run all validation rules -pub fn run_validation(ctx ValidationContext, rules []ValidationRule) ValidationResult { - mut errors := []string{} - mut warnings := []string{} - mut info := []string{} - - for rule in rules { - rule.check_fn(ctx) or { - match rule.severity { - .critical, .error { - errors << '[${rule.name}] ${err}' - } - .warning { - warnings << '[${rule.name}] ${err}' - } - .info { - info << '[${rule.name}] ${err}' - } - } - continue - } - } - - return ValidationResult{ - passed: errors.len == 0 - errors: errors - warnings: warnings - info: info - } -} - -// Print validation result -pub fn print_validation_result(result ValidationResult) { - if result.errors.len > 0 { - println('') - println('❌ VALIDATION ERRORS (operation blocked):') - for err in result.errors { - println(' ${err}') - } - } - - if result.warnings.len > 0 { - println('') - println('âš ī¸ VALIDATION WARNINGS:') - for warning in result.warnings { - println(' ${warning}') - } - } - - if result.info.len > 0 { - println('') - println('â„šī¸ VALIDATION INFO:') - for info_msg in result.info { - println(' ${info_msg}') - } - } - - if result.passed { - println('') - println('✓ Pre-flight validation passed') - } -} diff --git a/scaffoldia/repo-batcher/src/v/templates/merge.v b/scaffoldia/repo-batcher/src/v/templates/merge.v deleted file mode 100644 index 38bce88..0000000 --- a/scaffoldia/repo-batcher/src/v/templates/merge.v +++ /dev/null @@ -1,716 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Template Merge Operations -// Convert existing repos into template form while preserving content - -module templates - -import os - -// TemplateMergeConfig defines what to merge -pub struct TemplateMergeConfig { -pub: - add_github_workflows bool // Add standard workflows - add_scm_files bool // Add .machine_readable SCM files - add_bot_directives bool // Add .bot_directives - add_contractiles bool // Add contractiles directory - add_justfile bool // Add justfile - add_editorconfig bool // Add .editorconfig - preserve_existing bool // Don't overwrite existing files - template_source string // Path to template repo or 'default' -} - -// TemplateMergeResult holds merge result for one repo -pub struct TemplateMergeResult { -pub: - repo_path string - success bool - files_added int - files_preserved int - files_updated int - message string -} - -// Default merge config - safe defaults -pub fn default_merge_config() TemplateMergeConfig { - return TemplateMergeConfig{ - add_github_workflows: true - add_scm_files: true - add_bot_directives: true - add_contractiles: true - add_justfile: true - add_editorconfig: true - preserve_existing: true - template_source: 'default' - } -} - -// Apply template merge to a repository -pub fn apply_template_merge(repo_path string, config TemplateMergeConfig, dry_run bool) !TemplateMergeResult { - mut files_added := 0 - mut files_preserved := 0 - mut files_updated := 0 - - // Verify repo exists - if !os.exists(repo_path) { - return error('Repository does not exist: ${repo_path}') - } - - // Add GitHub workflows - if config.add_github_workflows { - result := merge_github_workflows(repo_path, config.preserve_existing, dry_run) or { - return error('Failed to merge workflows: ${err}') - } - files_added += result.added - files_preserved += result.preserved - files_updated += result.updated - } - - // Add SCM files - if config.add_scm_files { - result := merge_scm_files(repo_path, config.preserve_existing, dry_run) or { - return error('Failed to merge SCM files: ${err}') - } - files_added += result.added - files_preserved += result.preserved - files_updated += result.updated - } - - // Add bot directives - if config.add_bot_directives { - result := merge_bot_directives(repo_path, config.preserve_existing, dry_run) or { - return error('Failed to merge bot directives: ${err}') - } - files_added += result.added - files_preserved += result.preserved - files_updated += result.updated - } - - // Add contractiles - if config.add_contractiles { - result := merge_contractiles(repo_path, config.preserve_existing, dry_run) or { - return error('Failed to merge contractiles: ${err}') - } - files_added += result.added - files_preserved += result.preserved - files_updated += result.updated - } - - // Add justfile - if config.add_justfile { - result := merge_justfile(repo_path, config.preserve_existing, dry_run) or { - return error('Failed to merge justfile: ${err}') - } - files_added += result.added - files_preserved += result.preserved - files_updated += result.updated - } - - // Add editorconfig - if config.add_editorconfig { - result := merge_editorconfig(repo_path, config.preserve_existing, dry_run) or { - return error('Failed to merge editorconfig: ${err}') - } - files_added += result.added - files_preserved += result.preserved - files_updated += result.updated - } - - return TemplateMergeResult{ - repo_path: repo_path - success: true - files_added: files_added - files_preserved: files_preserved - files_updated: files_updated - message: 'Merged template: ${files_added} added, ${files_preserved} preserved, ${files_updated} updated' - } -} - -// MergeStats tracks merge statistics -struct MergeStats { - added int - preserved int - updated int -} - -// Merge GitHub workflows -fn merge_github_workflows(repo_path string, preserve bool, dry_run bool) !MergeStats { - mut stats := MergeStats{} - - workflows_dir := os.join_path(repo_path, '.github', 'workflows') - - if !dry_run { - os.mkdir_all(workflows_dir) or { - return error('Failed to create workflows directory: ${err}') - } - } - - // Standard workflows to add - workflows := [ - 'hypatia-scan.yml', - 'codeql.yml', - 'scorecard.yml', - 'quality.yml', - 'mirror.yml', - ] - - for workflow in workflows { - workflow_path := os.join_path(workflows_dir, workflow) - - if os.exists(workflow_path) && preserve { - stats.preserved++ - } else { - if !dry_run { - // Create workflow from template - content := get_default_workflow_content(workflow) or { - return error('Failed to get workflow template: ${err}') - } - os.write_file(workflow_path, content) or { - return error('Failed to write workflow: ${err}') - } - } - - if os.exists(workflow_path) { - stats.updated++ - } else { - stats.added++ - } - } - } - - return stats -} - -// Merge SCM files -fn merge_scm_files(repo_path string, preserve bool, dry_run bool) !MergeStats { - mut stats := MergeStats{} - - scm_dir := os.join_path(repo_path, '.machine_readable') - - if !dry_run { - os.mkdir_all(scm_dir) or { - return error('Failed to create .machine_readable directory: ${err}') - } - } - - // Standard SCM files - scm_files := [ - 'META.scm', - 'ECOSYSTEM.scm', - 'STATE.scm', - 'CHECKLIST.scm', - 'ROADMAP.scm', - 'CHANGELOG.scm', - ] - - for scm_file in scm_files { - scm_path := os.join_path(scm_dir, scm_file) - - if os.exists(scm_path) && preserve { - stats.preserved++ - } else { - if !dry_run { - content := get_default_scm_content(scm_file, repo_path) or { - return error('Failed to get SCM template: ${err}') - } - os.write_file(scm_path, content) or { - return error('Failed to write SCM file: ${err}') - } - } - - if os.exists(scm_path) { - stats.updated++ - } else { - stats.added++ - } - } - } - - return stats -} - -// Merge bot directives -fn merge_bot_directives(repo_path string, preserve bool, dry_run bool) !MergeStats { - mut stats := MergeStats{} - - directives_dir := os.join_path(repo_path, '.bot_directives') - - if !dry_run { - os.mkdir_all(directives_dir) or { - return error('Failed to create .bot_directives directory: ${err}') - } - } - - // Standard bot directive files - directive_files := [ - 'rhodibot.scm', // Release optimization - 'echidnabot.scm', // Ecosystem health - 'sustainabot.scm', // Sustainability - 'glambot.scm', // Git log analysis - 'seambot.scm', // Security/efficiency analysis - 'finishbot.scm', // Task completion - ] - - for directive_file in directive_files { - directive_path := os.join_path(directives_dir, directive_file) - - if os.exists(directive_path) && preserve { - stats.preserved++ - } else { - if !dry_run { - content := get_default_bot_directive(directive_file, repo_path) or { - return error('Failed to get bot directive template: ${err}') - } - os.write_file(directive_path, content) or { - return error('Failed to write bot directive: ${err}') - } - } - - if os.exists(directive_path) { - stats.updated++ - } else { - stats.added++ - } - } - } - - return stats -} - -// Merge contractiles directory -fn merge_contractiles(repo_path string, preserve bool, dry_run bool) !MergeStats { - mut stats := MergeStats{} - - contractiles_dir := os.join_path(repo_path, 'contractiles') - - if !dry_run { - os.mkdir_all(contractiles_dir) or { - return error('Failed to create contractiles directory: ${err}') - } - - // Create README in contractiles - readme_path := os.join_path(contractiles_dir, 'README.md') - if !os.exists(readme_path) || !preserve { - content := get_contractiles_readme() - os.write_file(readme_path, content) or { - return error('Failed to write contractiles README: ${err}') - } - stats.added++ - } else { - stats.preserved++ - } - } else { - stats.added++ // Would add README - } - - return stats -} - -// Merge justfile -fn merge_justfile(repo_path string, preserve bool, dry_run bool) !MergeStats { - mut stats := MergeStats{} - - justfile_path := os.join_path(repo_path, 'justfile') - - if os.exists(justfile_path) && preserve { - stats.preserved++ - } else { - if !dry_run { - content := get_default_justfile(repo_path) or { - return error('Failed to get justfile template: ${err}') - } - os.write_file(justfile_path, content) or { - return error('Failed to write justfile: ${err}') - } - } - - if os.exists(justfile_path) { - stats.updated++ - } else { - stats.added++ - } - } - - return stats -} - -// Merge editorconfig -fn merge_editorconfig(repo_path string, preserve bool, dry_run bool) !MergeStats { - mut stats := MergeStats{} - - editorconfig_path := os.join_path(repo_path, '.editorconfig') - - if os.exists(editorconfig_path) && preserve { - stats.preserved++ - } else { - if !dry_run { - content := get_default_editorconfig() - os.write_file(editorconfig_path, content) or { - return error('Failed to write .editorconfig: ${err}') - } - } - - if os.exists(editorconfig_path) { - stats.updated++ - } else { - stats.added++ - } - } - - return stats -} - -// Get default workflow content -fn get_default_workflow_content(workflow_name string) !string { - return match workflow_name { - 'hypatia-scan.yml' { get_hypatia_workflow() } - 'codeql.yml' { get_codeql_workflow() } - 'scorecard.yml' { get_scorecard_workflow() } - 'quality.yml' { get_quality_workflow() } - 'mirror.yml' { get_mirror_workflow() } - else { error('Unknown workflow: ${workflow_name}') } - } -} - -// Get default SCM content (implementations below) -fn get_default_scm_content(scm_file string, repo_path string) !string { - repo_name := os.file_name(repo_path) - - return match scm_file { - 'META.scm' { get_meta_scm_template(repo_name) } - 'ECOSYSTEM.scm' { get_ecosystem_scm_template(repo_name) } - 'STATE.scm' { get_state_scm_template(repo_name) } - 'CHECKLIST.scm' { get_checklist_scm_template(repo_name) } - 'ROADMAP.scm' { get_roadmap_scm_template(repo_name) } - 'CHANGELOG.scm' { get_changelog_scm_template(repo_name) } - else { error('Unknown SCM file: ${scm_file}') } - } -} - -// Get default bot directive -fn get_default_bot_directive(directive_file string, repo_path string) !string { - repo_name := os.file_name(repo_path) - - return match directive_file { - 'rhodibot.scm' { get_rhodibot_directive(repo_name) } - 'echidnabot.scm' { get_echidnabot_directive(repo_name) } - 'sustainabot.scm' { get_sustainabot_directive(repo_name) } - 'glambot.scm' { get_glambot_directive(repo_name) } - 'seambot.scm' { get_seambot_directive(repo_name) } - 'finishbot.scm' { get_finishbot_directive(repo_name) } - else { error('Unknown bot directive: ${directive_file}') } - } -} - -// Template content functions (to be implemented in separate file) -fn get_hypatia_workflow() string { - return '; SPDX-License-Identifier: PMPL-1.0-or-later -; Hypatia neurosymbolic security scanning -; TODO: Implement full workflow -' -} - -fn get_codeql_workflow() string { - return '# SPDX-License-Identifier: PMPL-1.0-or-later -# CodeQL analysis workflow -# TODO: Implement full workflow -' -} - -fn get_scorecard_workflow() string { - return '# SPDX-License-Identifier: PMPL-1.0-or-later -# OpenSSF Scorecard workflow -# TODO: Implement full workflow -' -} - -fn get_quality_workflow() string { - return '# SPDX-License-Identifier: PMPL-1.0-or-later -# Quality checks workflow -# TODO: Implement full workflow -' -} - -fn get_mirror_workflow() string { - return '# SPDX-License-Identifier: PMPL-1.0-or-later -# Repository mirroring workflow -# TODO: Implement full workflow -' -} - -fn get_contractiles_readme() string { - return '# Contractiles - -This directory contains contractile specifications for this project. - -Contractiles are formal specifications that define project contracts, -interfaces, and guarantees. - -## Structure - -- `api/` - API contracts -- `guarantees/` - Formal guarantees -- `interfaces/` - Interface specifications -' -} - -fn get_default_justfile(repo_path string) !string { - return '# SPDX-License-Identifier: PMPL-1.0-or-later -# Justfile for project automation - -# List available recipes -default: - @just --list - -# Run tests -test: - echo "Running tests..." - -# Build project -build: - echo "Building project..." - -# Format code -fmt: - echo "Formatting code..." -' -} - -fn get_default_editorconfig() string { - return '# SPDX-License-Identifier: PMPL-1.0-or-later -# EditorConfig: https://editorconfig.org - -root = true - -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true -indent_style = space -indent_size = 2 - -[*.{md,adoc}] -trim_trailing_whitespace = false - -[*.{v,go,rs}] -indent_size = 4 - -[Makefile] -indent_style = tab -' -} - -// SCM template functions -fn get_meta_scm_template(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; META.scm - Meta-level information - -(define meta - \'((project-name "${repo_name}") - (license "PMPL-1.0-or-later") - (author "Jonathan D.A. Jewell ") - (description "TODO: Add project description") - (architecture-decisions - ()) - (development-practices - ()))) -' -} - -fn get_ecosystem_scm_template(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; ECOSYSTEM.scm - Ecosystem position - -(define ecosystem - \'((project-name "${repo_name}") - (version "1.0.0") - (type "library") - (purpose "TODO: Define purpose") - (position-in-ecosystem "standalone") - (related-projects - ()))) -' -} - -fn get_state_scm_template(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; STATE.scm - Current project state - -(define state - \'((metadata - (version "1.0.0") - (project "${repo_name}") - (updated "TODO")) - (current-position - (phase "initial") - (overall-completion 0)) - (critical-next-actions - ()))) -' -} - -fn get_checklist_scm_template(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; CHECKLIST.scm - Project checklist - -(define checklist - \'((project "${repo_name}") - (items - ()))) -' -} - -fn get_roadmap_scm_template(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; ROADMAP.scm - Project roadmap - -(define roadmap - \'((project "${repo_name}") - (milestones - ()))) -' -} - -fn get_changelog_scm_template(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; CHANGELOG.scm - Project changelog - -(define changelog - \'((project "${repo_name}") - (entries - ()))) -' -} - -// Bot directive templates -fn get_rhodibot_directive(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; rhodibot.scm - Release optimization directives - -(define rhodibot-config - \'((repo "${repo_name}") - (release-strategy "semantic-versioning") - (auto-release false) - (changelog-generation true))) -' -} - -fn get_echidnabot_directive(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; echidnabot.scm - Ecosystem health directives - -(define echidnabot-config - \'((repo "${repo_name}") - (health-checks - ("dependencies" "security" "performance")) - (monitoring-interval "daily"))) -' -} - -fn get_sustainabot_directive(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; sustainabot.scm - Sustainability directives - -(define sustainabot-config - \'((repo "${repo_name}") - (sustainability-checks - ("dependencies" "maintenance" "documentation")) - (auto-update-deps false))) -' -} - -fn get_glambot_directive(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; glambot.scm - Git log analysis directives - -(define glambot-config - \'((repo "${repo_name}") - (analyze-commits true) - (commit-message-style "conventional-commits") - (report-frequency "weekly"))) -' -} - -fn get_seambot_directive(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; seambot.scm - Security/efficiency analysis directives - -(define seambot-config - \'((repo "${repo_name}") - (security-scans - ("dependencies" "code" "containers")) - (efficiency-analysis true))) -' -} - -fn get_finishbot_directive(repo_name string) string { - return ';; SPDX-License-Identifier: PMPL-1.0-or-later -;; finishbot.scm - Task completion directives - -(define finishbot-config - \'((repo "${repo_name}") - (auto-close-stale-issues true) - (stale-after-days 90) - (track-completion true))) -' -} - -// Batch apply template merge -pub fn apply_template_merge_batch(repos []string, config TemplateMergeConfig, dry_run bool) []TemplateMergeResult { - mut results := []TemplateMergeResult{} - - for repo in repos { - result := apply_template_merge(repo, config, dry_run) or { - results << TemplateMergeResult{ - repo_path: repo - success: false - files_added: 0 - files_preserved: 0 - files_updated: 0 - message: 'Error: ${err}' - } - continue - } - - results << result - } - - return results -} - -// Print merge summary -pub fn print_merge_summary(results []TemplateMergeResult) { - mut total_added := 0 - mut total_preserved := 0 - mut total_updated := 0 - mut success_count := 0 - mut failed_count := 0 - - println('Template Merge Results:') - println('======================') - println('') - - for result in results { - if result.success { - success_count++ - total_added += result.files_added - total_preserved += result.files_preserved - total_updated += result.files_updated - println('✓ ${result.repo_path}') - println(' ${result.message}') - } else { - failed_count++ - println('✗ ${result.repo_path}') - println(' ${result.message}') - } - } - - println('') - println('Summary:') - println(' Success: ${success_count}') - println(' Failed: ${failed_count}') - println(' Files added: ${total_added}') - println(' Files preserved: ${total_preserved}') - println(' Files updated: ${total_updated}') -} diff --git a/scaffoldia/repo-batcher/src/v/utils/repo_scanner.v b/scaffoldia/repo-batcher/src/v/utils/repo_scanner.v deleted file mode 100644 index 7b251cf..0000000 --- a/scaffoldia/repo-batcher/src/v/utils/repo_scanner.v +++ /dev/null @@ -1,119 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Repository Scanner -// Discovers git repositories in directory tree - -module utils - -import os - -// Finds all git repositories in base_dir up to max_depth -pub fn find_git_repos(base_dir string, max_depth int) []string { - mut repos := []string{} - - // Check if base_dir itself is a git repo - if os.is_dir(os.join_path(base_dir, '.git')) { - repos << base_dir - return repos - } - - // Recursively scan for .git directories - scan_for_repos(base_dir, 0, max_depth, mut repos) - - return repos -} - -// Recursively scans for git repositories -fn scan_for_repos(dir string, depth int, max_depth int, mut repos []string) { - if depth > max_depth { - return - } - - // List directory contents - entries := os.ls(dir) or { return } - - for entry in entries { - full_path := os.join_path(dir, entry) - - // Skip if not a directory - if !os.is_dir(full_path) { - continue - } - - // Check if this is a .git directory - if entry == '.git' { - // Parent directory is a git repo - repos << dir - return // Don't scan inside .git - } - - // Recursively scan subdirectory - scan_for_repos(full_path, depth + 1, max_depth, mut repos) - } -} - -// Resolves target specification to list of repository paths -pub fn resolve_targets(targets string, base_dir string, max_depth int) []string { - if targets.starts_with('@') { - // Pattern-based selection - pattern := targets[1..] - - return match pattern { - 'all-repos' { - find_git_repos(base_dir, max_depth) - } - else { - // Pattern matching like @rsr-*, @lithoglyph-* - find_git_repos_matching(base_dir, pattern, max_depth) - } - } - } else if targets.contains(',') { - // Comma-separated list - return targets.split(',').map(it.trim_space()) - } else if os.is_file(targets) { - // File containing repo paths - return read_repos_from_file(targets) - } else if os.is_dir(targets) { - // Single directory (could be single repo or directory of repos) - return find_git_repos(targets, max_depth) - } else { - // Single repository path - return [targets] - } -} - -// Finds repositories matching pattern -fn find_git_repos_matching(base_dir string, pattern string, max_depth int) []string { - all_repos := find_git_repos(base_dir, max_depth) - - // Convert glob pattern to simple matching - // @rsr-* -> match repos starting with "rsr-" - prefix := pattern.replace('*', '') - - return all_repos.filter(fn [prefix] (repo string) bool { - repo_name := os.file_name(repo) - return repo_name.starts_with(prefix) - }) -} - -// Reads repository paths from file -fn read_repos_from_file(path string) []string { - content := os.read_file(path) or { return []string{} } - lines := content.split_into_lines() - - return lines.filter(fn (line string) bool { - trimmed := line.trim_space() - return trimmed.len > 0 && !trimmed.starts_with('#') - }) -} - -// Validates that path is a git repository -pub fn is_git_repo(path string) bool { - git_dir := os.join_path(path, '.git') - return os.is_dir(git_dir) -} - -// Gets repository name from path -pub fn get_repo_name(path string) string { - return os.file_name(path) -} diff --git a/scaffoldia/repo-batcher/src/v/watcher/monitor.v b/scaffoldia/repo-batcher/src/v/watcher/monitor.v deleted file mode 100644 index 1b7be96..0000000 --- a/scaffoldia/repo-batcher/src/v/watcher/monitor.v +++ /dev/null @@ -1,209 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Watch Folder Monitor -// Monitors folder for operation files and executes automatically - -module watcher - -import os -import time -import toml - -// OperationFile represents a TOML operation definition -pub struct OperationFile { -pub mut: - path string - operation_type string - params map[string]string - targets string - dry_run bool - backup bool -} - -// WatchMonitor monitors folder for operation files -pub struct WatchMonitor { -pub mut: - watch_folder string - check_interval int // seconds - auto_delete bool - processed_files map[string]i64 // path -> timestamp -} - -// Creates new watch monitor -pub fn new_watch_monitor(watch_folder string, check_interval int, auto_delete bool) WatchMonitor { - // Create watch folder if needed - os.mkdir_all(watch_folder) or {} - - return WatchMonitor{ - watch_folder: watch_folder - check_interval: check_interval - auto_delete: auto_delete - processed_files: map[string]i64{} - } -} - -// Starts monitoring loop -pub fn (mut monitor WatchMonitor) start() { - println('Starting watch monitor...') - println(' Watch folder: ${monitor.watch_folder}') - println(' Check interval: ${monitor.check_interval}s') - println(' Auto-delete: ${monitor.auto_delete}') - println('') - println('Watching for operation files (*.toml)...') - println('Press Ctrl+C to stop') - println('') - - for { - // Scan for new operation files - monitor.scan_and_process() - - // Wait before next check - time.sleep(monitor.check_interval * time.second) - } -} - -// Scans folder and processes new files -fn (mut monitor WatchMonitor) scan_and_process() { - // List TOML files in watch folder - entries := os.ls(monitor.watch_folder) or { return } - - for entry in entries { - // Only process .toml files - if !entry.ends_with('.toml') { - continue - } - - full_path := os.join_path(monitor.watch_folder, entry) - - // Skip if already processed - if monitor.is_processed(full_path) { - continue - } - - // Parse and execute operation - println('[${time.now().format()}] Found: ${entry}') - monitor.process_operation_file(full_path) - - // Mark as processed - monitor.mark_processed(full_path) - - // Delete if auto-delete enabled - if monitor.auto_delete { - os.rm(full_path) or {} - } - } -} - -// Checks if file already processed -fn (monitor WatchMonitor) is_processed(path string) bool { - // Get file modification time - stat := os.stat(path) or { return true } - mtime := stat.mtime - - // Check if we've processed this version - if processed_time := monitor.processed_files[path] { - return mtime <= processed_time - } - - return false -} - -// Marks file as processed -fn (mut monitor WatchMonitor) mark_processed(path string) { - stat := os.stat(path) or { return } - monitor.processed_files[path] = stat.mtime -} - -// Processes operation file -fn (mut monitor WatchMonitor) process_operation_file(path string) { - // Parse TOML file - op_file := monitor.parse_operation_file(path) or { - println(' ✗ Failed to parse: ${err}') - return - } - - // Execute operation - println(' Operation: ${op_file.operation_type}') - println(' Targets: ${op_file.targets}') - println(' Dry run: ${op_file.dry_run}') - println('') - - // NOTE: This would call the actual operation execution - // For now, just print what would happen - match op_file.operation_type { - 'license-update' { - old := op_file.params['old_license'] or { 'unknown' } - new := op_file.params['new_license'] or { 'unknown' } - println(' Would update licenses: ${old} -> ${new}') - } - 'git-sync' { - parallel := op_file.params['parallel_jobs'] or { '4' } - println(' Would sync repositories (parallel: ${parallel})') - } - 'file-replace' { - pattern := op_file.params['pattern'] or { 'unknown' } - println(' Would replace files matching: ${pattern}') - } - else { - println(' Unknown operation type: ${op_file.operation_type}') - } - } - - println(' ✓ Operation queued') - println('') -} - -// Parses TOML operation file -fn (monitor WatchMonitor) parse_operation_file(path string) !OperationFile { - content := os.read_file(path) or { - return error('Failed to read file: ${err}') - } - - doc := toml.parse_text(content) or { - return error('Failed to parse TOML: ${err}') - } - - // Extract operation type - op_type := doc.value('operation.type').string() or { - return error('Missing operation.type') - } - - // Extract parameters - mut params := map[string]string{} - - // Try to extract common parameters - if val := doc.value('parameters.old_license') { - params['old_license'] = val.string() - } - if val := doc.value('parameters.new_license') { - params['new_license'] = val.string() - } - if val := doc.value('parameters.pattern') { - params['pattern'] = val.string() - } - if val := doc.value('parameters.replacement') { - params['replacement'] = val.string() - } - if val := doc.value('parameters.commit_message') { - params['commit_message'] = val.string() - } - if val := doc.value('parameters.parallel_jobs') { - params['parallel_jobs'] = val.string() - } - - // Extract targets - targets := doc.value('targets.selection').default_to('').string() - - // Extract options - dry_run := doc.value('options.dry_run').default_to(false).bool() - backup := doc.value('options.backup').default_to(true).bool() - - return OperationFile{ - path: path - operation_type: op_type - params: params - targets: targets - dry_run: dry_run - backup: backup - } -} diff --git a/scaffoldia/repo-batcher/tests/github_operations_test.v b/scaffoldia/repo-batcher/tests/github_operations_test.v deleted file mode 100644 index 05a184d..0000000 --- a/scaffoldia/repo-batcher/tests/github_operations_test.v +++ /dev/null @@ -1,352 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// GitHub Operations Tests -// Tests wiki-setup and community-setup operations - -module main - -import os -import github - -const ( - test_dir = '/tmp/repo-batcher-github-tests' - test_repos = ['test-repo-1', 'test-repo-2', 'test-repo-3'] -) - -fn main() { - println('repo-batcher GitHub Operations Tests') - println('=====================================') - println('') - - // Setup test environment - setup_test_repos() or { - eprintln('Failed to setup test repos: ${err}') - exit(1) - } - - // Run tests - mut passed := 0 - mut failed := 0 - - if test_default_home_content() { - passed++ - } else { - failed++ - } - - if test_default_community_files() { - passed++ - } else { - failed++ - } - - if test_community_setup_local() { - passed++ - } else { - failed++ - } - - if test_community_setup_idempotency() { - passed++ - } else { - failed++ - } - - if test_wiki_dry_run() { - passed++ - } else { - failed++ - } - - if test_community_dry_run() { - passed++ - } else { - failed++ - } - - // Cleanup - cleanup_test_repos() - - // Summary - println('') - println('Test Summary') - println('============') - println('Passed: ${passed}') - println('Failed: ${failed}') - println('Total: ${passed + failed}') - - if failed > 0 { - exit(1) - } -} - -// Setup test repositories -fn setup_test_repos() ! { - // Create test directory - os.mkdir_all(test_dir) or { - return error('Failed to create test dir: ${err}') - } - - // Create test repos - for repo in test_repos { - repo_path := os.join_path(test_dir, repo) - os.mkdir_all(repo_path) or { - return error('Failed to create repo: ${err}') - } - - // Initialize git repo - os.execute('cd "${repo_path}" && git init') - - // Create test files - readme := os.join_path(repo_path, 'README.md') - os.write_file(readme, '# ${repo}\n\nTest repository.\n') or {} - } - - println('✓ Test repositories created') -} - -// Cleanup test repositories -fn cleanup_test_repos() { - os.rmdir_all(test_dir) or {} - println('✓ Cleaned up test repositories') -} - -// Test 1: Default home content generation -fn test_default_home_content() bool { - println('Test 1: Default home content generation') - - content := github.default_home_content('test-repo') - - // Check content has expected structure - checks := [ - content.contains('# test-repo Wiki'), - content.contains('Welcome to the test-repo wiki'), - content.contains('## Getting Started'), - content.contains('## Contents'), - content.contains('## Contributing'), - content.len > 100, - ] - - mut passed := true - for check in checks { - if !check { - passed = false - break - } - } - - if passed { - println(' ✓ Default home content looks correct') - } else { - eprintln(' ✗ Default home content missing expected elements') - } - - return passed -} - -// Test 2: Default community files generation -fn test_default_community_files() bool { - println('Test 2: Default community files generation') - - files := github.standard_community_files() - - // Should have 5 files - if files.len != 5 { - eprintln(' ✗ Expected 5 files, got ${files.len}') - return false - } - - // Check all expected files are present - mut found := map[string]bool{} - for file in files { - if file.path.contains('CODE_OF_CONDUCT') { - found['conduct'] = true - } - if file.path.contains('CONTRIBUTING') { - found['contributing'] = true - } - if file.path.contains('SECURITY') { - found['security'] = true - } - if file.path.contains('SUPPORT') { - found['support'] = true - } - if file.path.contains('FUNDING') { - found['funding'] = true - } - } - - if found.len != 5 { - eprintln(' ✗ Not all expected files present') - return false - } - - // Check content is not empty - for file in files { - if file.content.len == 0 { - eprintln(' ✗ File ${file.name} has empty content') - return false - } - } - - println(' ✓ All 5 community files generated correctly') - return true -} - -// Test 3: Community setup on local repository -fn test_community_setup_local() bool { - println('Test 3: Community setup on local repository') - - repo_path := os.join_path(test_dir, test_repos[0]) - files := github.standard_community_files() - - result := github.setup_community_files(github.CommunitySetupParams{ - repo_path: repo_path - files: files - create_pr: false - dry_run: false - }) or { - eprintln(' ✗ Setup failed: ${err}') - return false - } - - if !result.success { - eprintln(' ✗ Setup reported failure: ${result.message}') - return false - } - - if result.files_created != 5 { - eprintln(' ✗ Expected 5 files created, got ${result.files_created}') - return false - } - - // Verify files actually exist - github_dir := os.join_path(repo_path, '.github') - if !os.exists(github_dir) { - eprintln(' ✗ .github directory not created') - return false - } - - expected_files := [ - 'CODE_OF_CONDUCT.md', - 'CONTRIBUTING.md', - 'SECURITY.md', - 'SUPPORT.md', - 'FUNDING.yml', - ] - - for file in expected_files { - file_path := os.join_path(github_dir, file) - if !os.exists(file_path) { - eprintln(' ✗ File not created: ${file}') - return false - } - } - - println(' ✓ Community files deployed successfully') - return true -} - -// Test 4: Community setup idempotency -fn test_community_setup_idempotency() bool { - println('Test 4: Community setup idempotency (skip unchanged files)') - - repo_path := os.join_path(test_dir, test_repos[0]) - files := github.standard_community_files() - - // Run setup again on same repo - result := github.setup_community_files(github.CommunitySetupParams{ - repo_path: repo_path - files: files - create_pr: false - dry_run: false - }) or { - eprintln(' ✗ Second setup failed: ${err}') - return false - } - - if !result.success { - eprintln(' ✗ Second setup reported failure') - return false - } - - // Should have skipped all files (they already exist with same content) - if result.files_skipped != 5 { - eprintln(' ✗ Expected 5 files skipped, got ${result.files_skipped}') - return false - } - - if result.files_created != 0 { - eprintln(' ✗ Expected 0 files created, got ${result.files_created}') - return false - } - - println(' ✓ Idempotency working (skipped unchanged files)') - return true -} - -// Test 5: Wiki setup dry-run -fn test_wiki_dry_run() bool { - println('Test 5: Wiki setup dry-run mode') - - result := github.setup_wiki(github.WikiSetupParams{ - repo_owner: 'test-owner' - repo_name: 'test-repo' - home_content: 'Test content' - dry_run: true - }) or { - eprintln(' ✗ Dry-run failed: ${err}') - return false - } - - if !result.success { - eprintln(' ✗ Dry-run reported failure') - return false - } - - if !result.message.contains('[DRY RUN]') { - eprintln(' ✗ Message does not indicate dry-run') - return false - } - - println(' ✓ Dry-run mode working correctly') - return true -} - -// Test 6: Community setup dry-run -fn test_community_dry_run() bool { - println('Test 6: Community setup dry-run mode') - - repo_path := os.join_path(test_dir, test_repos[1]) - files := github.standard_community_files() - - result := github.setup_community_files(github.CommunitySetupParams{ - repo_path: repo_path - files: files - create_pr: false - dry_run: true - }) or { - eprintln(' ✗ Dry-run failed: ${err}') - return false - } - - if !result.success { - eprintln(' ✗ Dry-run reported failure') - return false - } - - if !result.message.contains('[DRY RUN]') { - eprintln(' ✗ Message does not indicate dry-run') - return false - } - - // Verify no files were actually created - github_dir := os.join_path(repo_path, '.github') - if os.exists(github_dir) { - eprintln(' ✗ Dry-run created files (should not)') - return false - } - - println(' ✓ Dry-run mode prevents file creation') - return true -} diff --git a/scaffoldia/repo-batcher/tests/integration_test.v b/scaffoldia/repo-batcher/tests/integration_test.v deleted file mode 100644 index acf356d..0000000 --- a/scaffoldia/repo-batcher/tests/integration_test.v +++ /dev/null @@ -1,233 +0,0 @@ -// SPDX-License-Identifier: PMPL-1.0-or-later -// -// Integration Tests -// Tests repo-batcher operations on real test repositories - -module main - -import os -import ffi -import utils -import executor - -const ( - test_dir = '/tmp/repo-batcher-tests' - test_repos = ['test-repo-1', 'test-repo-2', 'test-repo-3'] -) - -fn main() { - println('repo-batcher Integration Tests') - println('================================') - println('') - - // Setup test environment - setup_test_repos() or { - eprintln('Failed to setup test repos: ${err}') - exit(1) - } - - // Run tests - mut passed := 0 - mut failed := 0 - - if test_spdx_validation() { - passed++ - } else { - failed++ - } - - if test_repo_scanner() { - passed++ - } else { - failed++ - } - - if test_target_resolution() { - passed++ - } else { - failed++ - } - - if test_git_sync_dry_run() { - passed++ - } else { - failed++ - } - - if test_parallel_execution() { - passed++ - } else { - failed++ - } - - // Cleanup - cleanup_test_repos() - - // Summary - println('') - println('Test Summary') - println('============') - println('Passed: ${passed}') - println('Failed: ${failed}') - println('Total: ${passed + failed}') - - if failed > 0 { - exit(1) - } -} - -// Sets up test repositories -fn setup_test_repos() ! { - // Create test directory - os.mkdir_all(test_dir) or { - return error('Failed to create test dir: ${err}') - } - - // Create test repos - for repo in test_repos { - repo_path := os.join_path(test_dir, repo) - os.mkdir_all(repo_path) or { - return error('Failed to create repo: ${err}') - } - - // Initialize git repo - os.execute('cd ${repo_path} && git init') - - // Create test files - test_file := os.join_path(repo_path, 'README.md') - os.write_file(test_file, '# Test Repository\n\nThis is a test.\n') or {} - - // Create LICENSE file - license_file := os.join_path(repo_path, 'LICENSE') - os.write_file(license_file, 'MIT License\n\nCopyright (c) 2026\n') or {} - - // Initial commit - os.execute('cd ${repo_path} && git add . && git commit -m "Initial commit"') - } - - println('✓ Setup test repositories') -} - -// Cleans up test repositories -fn cleanup_test_repos() { - os.rmdir_all(test_dir) or {} - println('✓ Cleaned up test repositories') -} - -// Test SPDX validation -fn test_spdx_validation() bool { - println('Testing SPDX validation...') - - // Valid identifiers - valid := ['MIT', 'Apache-2.0', 'GPL-3.0-only', 'PMPL-1.0-or-later'] - for id in valid { - if !ffi.validate_spdx(id) { - println(' ✗ Failed: ${id} should be valid') - return false - } - } - - // Invalid identifiers - invalid := ['NotALicense', 'MIT-INVALID', ''] - for id in invalid { - if ffi.validate_spdx(id) { - println(' ✗ Failed: ${id} should be invalid') - return false - } - } - - println(' ✓ SPDX validation works') - return true -} - -// Test repository scanner -fn test_repo_scanner() bool { - println('Testing repository scanner...') - - repos := utils.find_git_repos(test_dir, 2) - - if repos.len != test_repos.len { - println(' ✗ Failed: Expected ${test_repos.len} repos, found ${repos.len}') - return false - } - - for expected in test_repos { - found := false - for repo in repos { - if repo.contains(expected) { - found = true - break - } - } - if !found { - println(' ✗ Failed: Missing repo ${expected}') - return false - } - } - - println(' ✓ Repository scanner works') - return true -} - -// Test target resolution -fn test_target_resolution() bool { - println('Testing target resolution...') - - // Test explicit list - targets := '${os.join_path(test_dir, test_repos[0])},${os.join_path(test_dir, test_repos[1])}' - repos := utils.resolve_targets(targets, test_dir, 2) - - if repos.len != 2 { - println(' ✗ Failed: Expected 2 repos, got ${repos.len}') - return false - } - - // Test @all-repos pattern - all_repos := utils.resolve_targets('@all-repos', test_dir, 2) - if all_repos.len != test_repos.len { - println(' ✗ Failed: @all-repos should find ${test_repos.len} repos, found ${all_repos.len}') - return false - } - - println(' ✓ Target resolution works') - return true -} - -// Test git-sync dry-run -fn test_git_sync_dry_run() bool { - println('Testing git-sync (dry-run)...') - - repos := utils.find_git_repos(test_dir, 2) - - mut pool := executor.new_worker_pool(repos, 2) - result := pool.execute_git_sync('test commit', true) // dry_run = true - - if result.failure_count > 0 { - println(' ✗ Failed: Got ${result.failure_count} failures in dry-run') - return false - } - - println(' ✓ Git-sync dry-run works') - return true -} - -// Test parallel execution -fn test_parallel_execution() bool { - println('Testing parallel execution...') - - repos := utils.find_git_repos(test_dir, 2) - - // Test with 2 workers - mut pool := executor.new_worker_pool(repos, 2) - result := pool.execute_git_sync('parallel test', true) - - // Should process all repos - total := result.success_count + result.failure_count + result.skipped_count - if total != repos.len { - println(' ✗ Failed: Processed ${total} repos, expected ${repos.len}') - return false - } - - println(' ✓ Parallel execution works') - return true -}