diff --git a/.gitignore b/.gitignore index c2658d7..f82a4ae 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ node_modules/ +rails diff --git a/AGENTS.md b/AGENTS.md index 567766b..9fe3330 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,8 +30,8 @@ If any required file is missing, you MUST stop and report the missing file. 3. Routine repository operations are NOT escalation triggers. Use escalation triggers only from this contract. 4. For source-code delivery, completion is forbidden at PR-open stage. 5. Completion requires merged PR to `main` + terminal green CI + linked issue/internal task closed. -6. Before push or merge, you MUST run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge`. -7. For issue/PR/milestone operations, you MUST use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`). +6. Before push or merge, you MUST run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge`. +7. For issue/PR/milestone operations, you MUST use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`). 8. If any required wrapper command fails, status is `blocked`; report the exact failed wrapper command and stop. 9. Do NOT stop at "PR created". Do NOT ask "should I merge?" Do NOT ask "should I close the issue?". @@ -63,7 +63,7 @@ If any required file is missing, you MUST stop and report the missing file. 24. Deployment ownership is REQUIRED when deployment is in scope and target access is configured. 25. For container deployments, you MUST use immutable image tags (`sha-*`, `vX.Y.Z-rc.N`) with digest-first promotion; `latest` is forbidden as a deployment reference. 26. If an external git provider is available (Gitea/GitHub/GitLab), you MUST create or update issue(s) and link them in `docs/TASKS.md` before coding; if unavailable, use `TASKS:` internal refs in `docs/TASKS.md`. -27. For provider operations (issue/PR/milestone), you MUST detect platform first and use `~/.config/mosaic/rails/git/*.sh` wrappers before any raw provider CLI/API calls. +27. For provider operations (issue/PR/milestone), you MUST detect platform first and use `~/.config/mosaic/tools/git/*.sh` wrappers before any raw provider CLI/API calls. 28. Direct `gh`/`tea`/`glab` commands are forbidden as first choice when a Mosaic wrapper exists; use raw commands only as documented fallback. 29. If the mission is orchestration-oriented (contains "orchestrate", issue/milestone coordination, or multi-task execution), you MUST load and follow `~/.config/mosaic/guides/ORCHESTRATOR.md` before taking action. 30. At session start, you MUST declare the operating mode in your first response before any tool calls or implementation steps. @@ -72,7 +72,7 @@ If any required file is missing, you MUST stop and report the missing file. 33. For explicit review-only missions, the first line MUST be exactly: `Now initiating Review mode...` 34. For source-code delivery through PR workflow, completion is forbidden until the PR is merged to `main`, CI/pipeline status is terminal green, and linked issue/internal task is closed. 35. If merge/CI/issue-closure operations fail, you MUST report a blocker with the exact failed wrapper command and stop instead of declaring completion. -36. Before push or PR merge, you MUST run CI queue guard and wait if the project has running/queued pipelines: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge`. +36. Before push or PR merge, you MUST run CI queue guard and wait if the project has running/queued pipelines: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge`. ## Mode Declaration Protocol (Hard Rule) diff --git a/AUDIT-2026-02-17-framework-consistency.md b/AUDIT-2026-02-17-framework-consistency.md index 7dde5e5..e7422f5 100644 --- a/AUDIT-2026-02-17-framework-consistency.md +++ b/AUDIT-2026-02-17-framework-consistency.md @@ -24,19 +24,19 @@ Scope: ### MF-001 (QA rails path correction) Updated: -- `rails/qa/qa-hook-wrapper.sh` -- `rails/qa/qa-hook-stdin.sh` -- `rails/qa/qa-hook-handler.sh` -- `rails/qa/remediation-hook-handler.sh` -- `rails/qa/qa-queue-monitor.sh` +- `tools/qa/qa-hook-wrapper.sh` +- `tools/qa/qa-hook-stdin.sh` +- `tools/qa/qa-hook-handler.sh` +- `tools/qa/remediation-hook-handler.sh` +- `tools/qa/qa-queue-monitor.sh` Change: -- Standardized handler paths to `~/.config/mosaic/rails/qa/...`. +- Standardized handler paths to `~/.config/mosaic/tools/qa/...`. ### MF-002 + MF-003 (conditional loading/context detection) Updated: -- `rails/bootstrap/agent-lint.sh` -- `rails/bootstrap/agent-upgrade.sh` +- `tools/bootstrap/agent-lint.sh` +- `tools/bootstrap/agent-upgrade.sh` - `templates/agent/SPEC.md` Change: @@ -58,7 +58,7 @@ Updated: - `skills/pr-reviewer/SKILL.md` Change: -- Replaced all `~/.claude/scripts/git/...` with `~/.config/mosaic/rails/git/...`. +- Replaced all `~/.claude/scripts/git/...` with `~/.config/mosaic/tools/git/...`. - Replaced `~/.claude/skills/...` with `~/.config/mosaic/skills/...`. ### MF-006 (worktree skill docs hierarchy) @@ -109,7 +109,7 @@ These are required to support existing Claude runtime integration while keeping Executed checks: - `rg -n "~/.claude|\\.claude/|agent-guides" ~/src/agent-skills -S` - Result: no matches after remediation. -- `rg -n "~/.config/mosaic/rails/(qa-hook|remediation-hook|qa-queue-monitor)" ~/src/mosaic-bootstrap -S` +- `rg -n "~/.config/mosaic/tools/(qa-hook|remediation-hook|qa-queue-monitor)" ~/src/mosaic-bootstrap -S` - Result: no invalid old-style QA rail paths remain. - Installed runtime validation: - - `~/.config/mosaic` contains `rails/git`, `rails/portainer`, `rails/cicd`, `skills`, and `bin` tooling. + - `~/.config/mosaic` contains `tools/git`, `tools/portainer`, `tools/cicd`, `skills`, and `bin` tooling. diff --git a/README.md b/README.md index 8afd368..9502292 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim ├── bin/ ← CLI tools (mosaic, mosaic-init, mosaic-doctor, etc.) ├── dist/ ← Bundled wizard (mosaic-wizard.mjs) ├── guides/ ← Operational guides -├── rails/ ← Quality rails, git scripts, portainer scripts +├── tools/ ← Tool suites: git, portainer, authentik, coolify, codex, etc. ├── runtime/ ← Runtime adapters + runtime-specific references │ ├── claude/CLAUDE.md │ ├── claude/RUNTIME.md diff --git a/STANDARDS.md b/STANDARDS.md index 9dd4c22..23af20b 100644 --- a/STANDARDS.md +++ b/STANDARDS.md @@ -12,16 +12,16 @@ Master/slave model: 2. Load project-local `AGENTS.md` next. 3. Respect repository-specific tooling and workflows. 4. Use lifecycle scripts when available (`scripts/agent/*.sh`). -5. Use shared rails/guides from `~/.config/mosaic` as canonical references. +5. Use shared tools/guides from `~/.config/mosaic` as canonical references. ## Non-Negotiables - Data files are authoritative; generated views are derived artifacts. - Pull before edits when collaborating in shared repos. - Run validation checks before claiming completion. -- Apply quality rails from `~/.config/mosaic/rails/` when relevant (review, QA, git workflow). -- For project-level mechanical enforcement templates, use `~/.config/mosaic/rails/quality/` via `~/.config/mosaic/bin/mosaic-quality-apply`. -- For runtime-agnostic delegation/orchestration, use `~/.config/mosaic/rails/orchestrator-matrix/` with repo-local `.mosaic/orchestrator/` state. +- Apply quality tools from `~/.config/mosaic/tools/` when relevant (review, QA, git workflow). +- For project-level mechanical enforcement templates, use `~/.config/mosaic/tools/quality/` via `~/.config/mosaic/bin/mosaic-quality-apply`. +- For runtime-agnostic delegation/orchestration, use `~/.config/mosaic/tools/orchestrator-matrix/` with repo-local `.mosaic/orchestrator/` state. - Avoid hardcoded secrets and token leakage in remotes/commits. - Do not perform destructive git/file actions without explicit instruction. - Browser automation (Playwright, Cypress, Puppeteer) MUST run in headless mode. Never launch a visible browser — it collides with the user's display and active session. @@ -50,10 +50,10 @@ All runtime adapters should inject: before task execution. -Runtime-compatible guides and rails are hosted at: +Runtime-compatible guides and tools are hosted at: - `~/.config/mosaic/guides/` -- `~/.config/mosaic/rails/` +- `~/.config/mosaic/tools/` - `~/.config/mosaic/profiles/` (runtime-neutral domain/workflow/stack presets) - `~/.config/mosaic/runtime/` (runtime-specific overlays) - `~/.config/mosaic/skills-local/` (local private skills shared across runtimes) diff --git a/TOOLS.md b/TOOLS.md index b1359bc..2cb9f43 100644 --- a/TOOLS.md +++ b/TOOLS.md @@ -3,34 +3,104 @@ Centralized reference for tools, credentials, and CLI patterns available across all projects. Project-specific tooling belongs in the project's `AGENTS.md`, not here. -## Mosaic Git Wrappers (Use First) +All tool suites are located at `~/.config/mosaic/tools/`. -Mosaic wrappers at `~/.config/mosaic/rails/git/*.sh` handle platform detection and edge cases. Always use these before raw CLI commands. +## Tool Suites + +### Git Wrappers (Use First) + +Mosaic wrappers at `~/.config/mosaic/tools/git/*.sh` handle platform detection and edge cases. Always use these before raw CLI commands. ```bash # Issues -~/.config/mosaic/rails/git/issue-create.sh -~/.config/mosaic/rails/git/issue-close.sh +~/.config/mosaic/tools/git/issue-create.sh +~/.config/mosaic/tools/git/issue-close.sh # PRs -~/.config/mosaic/rails/git/pr-create.sh -~/.config/mosaic/rails/git/pr-merge.sh +~/.config/mosaic/tools/git/pr-create.sh +~/.config/mosaic/tools/git/pr-merge.sh # Milestones -~/.config/mosaic/rails/git/milestone-create.sh +~/.config/mosaic/tools/git/milestone-create.sh # CI queue guard (required before push/merge) -~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge +~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge ``` -## Code Review (Codex) +### Code Review (Codex) ```bash -# Code quality review -~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted +~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted +~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted +``` -# Security review -~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted +### Infrastructure — Portainer + +```bash +~/.config/mosaic/tools/portainer/stack-status.sh -n +~/.config/mosaic/tools/portainer/stack-redeploy.sh -n +~/.config/mosaic/tools/portainer/stack-list.sh +~/.config/mosaic/tools/portainer/endpoint-list.sh +``` + +### Infrastructure — Coolify + +```bash +~/.config/mosaic/tools/coolify/project-list.sh +~/.config/mosaic/tools/coolify/service-list.sh +~/.config/mosaic/tools/coolify/service-status.sh -u +~/.config/mosaic/tools/coolify/deploy.sh -u +~/.config/mosaic/tools/coolify/env-set.sh -u -k KEY -v VALUE +``` + +### Identity — Authentik + +```bash +~/.config/mosaic/tools/authentik/user-list.sh +~/.config/mosaic/tools/authentik/user-create.sh -u -n -e +~/.config/mosaic/tools/authentik/group-list.sh +~/.config/mosaic/tools/authentik/app-list.sh +~/.config/mosaic/tools/authentik/flow-list.sh +~/.config/mosaic/tools/authentik/admin-status.sh +``` + +### CI/CD — Woodpecker + +```bash +~/.config/mosaic/tools/woodpecker/pipeline-list.sh +~/.config/mosaic/tools/woodpecker/pipeline-status.sh +~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh -b +``` + +### IT Service — GLPI + +```bash +~/.config/mosaic/tools/glpi/ticket-list.sh +~/.config/mosaic/tools/glpi/ticket-create.sh -t -c <content> +~/.config/mosaic/tools/glpi/computer-list.sh +~/.config/mosaic/tools/glpi/user-list.sh +``` + +### Health Check + +```bash +# Check all configured services +~/.config/mosaic/tools/health/stack-health.sh + +# Check a specific service +~/.config/mosaic/tools/health/stack-health.sh -s portainer + +# JSON output for automation +~/.config/mosaic/tools/health/stack-health.sh -f json +``` + +### Shared Credential Loader + +```bash +# Source in any script to load service credentials +source ~/.config/mosaic/tools/_lib/credentials.sh +load_credentials <service-name> +# Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker ``` ## Git Providers @@ -42,16 +112,13 @@ Mosaic wrappers at `~/.config/mosaic/rails/git/*.sh` handle platform detection a ## Credentials **Location:** (configure your credential file path) +**Loader:** `source ~/.config/mosaic/tools/_lib/credentials.sh && load_credentials <service>` **Never expose actual values. Never commit credential files.** ## CLI Gotchas -(Add platform-specific CLI gotchas as you discover them. Examples: TTY requirements, default list limits, API fallback patterns.) - -## Custom Tools - -(Add any machine-specific tools, scripts, or workflows here.) +(Add platform-specific CLI gotchas as you discover them.) ## Safety Defaults diff --git a/adapters/claude.md b/adapters/claude.md index ae8d94a..d8abaa5 100644 --- a/adapters/claude.md +++ b/adapters/claude.md @@ -14,4 +14,4 @@ Use wrapper commands from `~/.config/mosaic/bin/` for lifecycle rituals. ## Migration Note Project-local `.claude/commands/*.md` should call `scripts/agent/*.sh` so behavior stays runtime-neutral. -Guides and rails should resolve to `~/.config/mosaic/guides` and `~/.config/mosaic/rails` (linked into `~/.claude` for compatibility). +Guides and tools should resolve to `~/.config/mosaic/guides` and `~/.config/mosaic/tools` (linked into `~/.claude` for compatibility). diff --git a/bin/mosaic-bootstrap-repo b/bin/mosaic-bootstrap-repo index 0a10cd3..b24b0b8 100755 --- a/bin/mosaic-bootstrap-repo +++ b/bin/mosaic-bootstrap-repo @@ -90,10 +90,10 @@ bash scripts/agent/critical.sh bash scripts/agent/session-end.sh ``` -## Shared Rails +## Shared Tools - Quality and orchestration guides: `~/.config/mosaic/guides/` -- Shared automation rails: `~/.config/mosaic/rails/` +- Shared automation tools: `~/.config/mosaic/tools/` ## Repo-Specific Notes @@ -108,7 +108,7 @@ fi echo "[mosaic] Repo bootstrap complete: $TARGET_DIR" echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows" -echo "[mosaic] Optional: apply quality rails via ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR" +echo "[mosaic] Optional: apply quality tools via ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR" echo "[mosaic] Optional: run orchestrator rail via ~/.config/mosaic/bin/mosaic-orchestrator-drain" echo "[mosaic] Optional: run detached orchestrator via bash $TARGET_DIR/scripts/agent/orchestrator-daemon.sh start" @@ -119,8 +119,8 @@ if [[ -n "$QUALITY_TEMPLATE" ]]; then sed -i "s/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml" sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml" fi - echo "[mosaic] Applied quality rails template: $QUALITY_TEMPLATE" + echo "[mosaic] Applied quality tools template: $QUALITY_TEMPLATE" else - echo "[mosaic] WARN: mosaic-quality-apply not found; skipping quality rails apply" >&2 + echo "[mosaic] WARN: mosaic-quality-apply not found; skipping quality tools apply" >&2 fi fi diff --git a/bin/mosaic-doctor b/bin/mosaic-doctor index 2b6f940..6e75ece 100755 --- a/bin/mosaic-doctor +++ b/bin/mosaic-doctor @@ -149,9 +149,9 @@ expect_file "$MOSAIC_HOME/STANDARDS.md" expect_file "$MOSAIC_HOME/USER.md" expect_file "$MOSAIC_HOME/TOOLS.md" expect_dir "$MOSAIC_HOME/guides" -expect_dir "$MOSAIC_HOME/rails" -expect_dir "$MOSAIC_HOME/rails/quality" -expect_dir "$MOSAIC_HOME/rails/orchestrator-matrix" +expect_dir "$MOSAIC_HOME/tools" +expect_dir "$MOSAIC_HOME/tools/quality" +expect_dir "$MOSAIC_HOME/tools/orchestrator-matrix" expect_dir "$MOSAIC_HOME/profiles" expect_dir "$MOSAIC_HOME/templates/agent" expect_dir "$MOSAIC_HOME/skills" @@ -168,10 +168,10 @@ expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-drain" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-publish" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-consume" expect_file "$MOSAIC_HOME/bin/mosaic-orchestrator-matrix-cycle" -expect_file "$MOSAIC_HOME/rails/git/ci-queue-wait.sh" -expect_file "$MOSAIC_HOME/rails/git/pr-ci-wait.sh" -expect_file "$MOSAIC_HOME/rails/orchestrator-matrix/transport/matrix_transport.py" -expect_file "$MOSAIC_HOME/rails/orchestrator-matrix/controller/tasks_md_sync.py" +expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh" +expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh" +expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py" +expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py" expect_file "$MOSAIC_HOME/runtime/mcp/SEQUENTIAL-THINKING.json" expect_file "$MOSAIC_HOME/runtime/claude/RUNTIME.md" expect_file "$MOSAIC_HOME/runtime/codex/RUNTIME.md" diff --git a/bin/mosaic-doctor.ps1 b/bin/mosaic-doctor.ps1 index 7fe6fc1..6a43dd5 100644 --- a/bin/mosaic-doctor.ps1 +++ b/bin/mosaic-doctor.ps1 @@ -138,9 +138,9 @@ Write-Host "[mosaic-doctor] Mosaic home: $MosaicHome" # Canonical Mosaic checks Expect-File (Join-Path $MosaicHome "STANDARDS.md") Expect-Dir (Join-Path $MosaicHome "guides") -Expect-Dir (Join-Path $MosaicHome "rails") -Expect-Dir (Join-Path $MosaicHome "rails\quality") -Expect-Dir (Join-Path $MosaicHome "rails\orchestrator-matrix") +Expect-Dir (Join-Path $MosaicHome "tools") +Expect-Dir (Join-Path $MosaicHome "tools\quality") +Expect-Dir (Join-Path $MosaicHome "tools\orchestrator-matrix") Expect-Dir (Join-Path $MosaicHome "profiles") Expect-Dir (Join-Path $MosaicHome "templates\agent") Expect-Dir (Join-Path $MosaicHome "skills") @@ -157,11 +157,11 @@ Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-drain") Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-publish") Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume") Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-cycle") -Expect-File (Join-Path $MosaicHome "rails\git\ci-queue-wait.ps1") -Expect-File (Join-Path $MosaicHome "rails\git\ci-queue-wait.sh") -Expect-File (Join-Path $MosaicHome "rails\git\pr-ci-wait.sh") -Expect-File (Join-Path $MosaicHome "rails\orchestrator-matrix\transport\matrix_transport.py") -Expect-File (Join-Path $MosaicHome "rails\orchestrator-matrix\controller\tasks_md_sync.py") +Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.ps1") +Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.sh") +Expect-File (Join-Path $MosaicHome "tools\git\pr-ci-wait.sh") +Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\transport\matrix_transport.py") +Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\controller\tasks_md_sync.py") Expect-File (Join-Path $MosaicHome "runtime\mcp\SEQUENTIAL-THINKING.json") Expect-File (Join-Path $MosaicHome "runtime\claude\RUNTIME.md") Expect-File (Join-Path $MosaicHome "runtime\codex\RUNTIME.md") diff --git a/bin/mosaic-orchestrator-matrix-consume b/bin/mosaic-orchestrator-matrix-consume index 9658794..117f9c6 100755 --- a/bin/mosaic-orchestrator-matrix-consume +++ b/bin/mosaic-orchestrator-matrix-consume @@ -2,7 +2,7 @@ set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" -BRIDGE="$MOSAIC_HOME/rails/orchestrator-matrix/transport/matrix_transport.py" +BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py" if [[ ! -f "$BRIDGE" ]]; then echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2 diff --git a/bin/mosaic-orchestrator-matrix-publish b/bin/mosaic-orchestrator-matrix-publish index 005591b..1220776 100755 --- a/bin/mosaic-orchestrator-matrix-publish +++ b/bin/mosaic-orchestrator-matrix-publish @@ -2,7 +2,7 @@ set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" -BRIDGE="$MOSAIC_HOME/rails/orchestrator-matrix/transport/matrix_transport.py" +BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py" if [[ ! -f "$BRIDGE" ]]; then echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2 diff --git a/bin/mosaic-orchestrator-run b/bin/mosaic-orchestrator-run index 178aa6c..2cae49c 100755 --- a/bin/mosaic-orchestrator-run +++ b/bin/mosaic-orchestrator-run @@ -2,7 +2,7 @@ set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" -CTRL="$MOSAIC_HOME/rails/orchestrator-matrix/controller/mosaic_orchestrator.py" +CTRL="$MOSAIC_HOME/tools/orchestrator-matrix/controller/mosaic_orchestrator.py" if [[ ! -f "$CTRL" ]]; then echo "[mosaic-orchestrator] missing controller: $CTRL" >&2 diff --git a/bin/mosaic-orchestrator-sync-tasks b/bin/mosaic-orchestrator-sync-tasks index 6ce419f..0390123 100755 --- a/bin/mosaic-orchestrator-sync-tasks +++ b/bin/mosaic-orchestrator-sync-tasks @@ -2,7 +2,7 @@ set -euo pipefail MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" -SYNC="$MOSAIC_HOME/rails/orchestrator-matrix/controller/tasks_md_sync.py" +SYNC="$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py" if [[ ! -f "$SYNC" ]]; then echo "[mosaic-orchestrator-sync] missing sync script: $SYNC" >&2 diff --git a/bin/mosaic-quality-apply b/bin/mosaic-quality-apply index 98fd1ae..2c2f217 100755 --- a/bin/mosaic-quality-apply +++ b/bin/mosaic-quality-apply @@ -9,7 +9,7 @@ usage() { cat <<USAGE Usage: $(basename "$0") --template <name> [--target <dir>] -Apply Mosaic quality rails templates into a project. +Apply Mosaic quality tools templates into a project. Templates: typescript-node @@ -55,7 +55,7 @@ if [[ ! -d "$TARGET_DIR" ]]; then exit 1 fi -SCRIPT="$MOSAIC_HOME/rails/quality/scripts/install.sh" +SCRIPT="$MOSAIC_HOME/tools/quality/scripts/install.sh" if [[ ! -x "$SCRIPT" ]]; then echo "[mosaic-quality] Missing install script: $SCRIPT" >&2 exit 1 diff --git a/bin/mosaic-quality-verify b/bin/mosaic-quality-verify index c52ca1a..4d18756 100755 --- a/bin/mosaic-quality-verify +++ b/bin/mosaic-quality-verify @@ -39,7 +39,7 @@ if [[ ! -d "$TARGET_DIR" ]]; then exit 1 fi -SCRIPT="$MOSAIC_HOME/rails/quality/scripts/verify.sh" +SCRIPT="$MOSAIC_HOME/tools/quality/scripts/verify.sh" if [[ ! -x "$SCRIPT" ]]; then echo "[mosaic-quality] Missing verify script: $SCRIPT" >&2 exit 1 diff --git a/guides/AUTHENTICATION.md b/guides/AUTHENTICATION.md index 9ae2e8f..e262b3c 100644 --- a/guides/AUTHENTICATION.md +++ b/guides/AUTHENTICATION.md @@ -1,7 +1,7 @@ # Authentication & Authorization Guide ## Before Starting -1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me` +1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me` 2. Review existing auth implementation in codebase 3. Review Vault secrets structure: `docs/vault-secrets-structure.md` @@ -115,6 +115,41 @@ class TestAuthentication: pass ``` +## Authentik SSO Administration + +Authentik is the identity provider for the Mosaic Stack. Use the Authentik tool suite for administration. + +### Tool Suite + +```bash +# System health +~/.config/mosaic/tools/authentik/admin-status.sh + +# User management +~/.config/mosaic/tools/authentik/user-list.sh +~/.config/mosaic/tools/authentik/user-create.sh -u <username> -n <name> -e <email> + +# Group and app management +~/.config/mosaic/tools/authentik/group-list.sh +~/.config/mosaic/tools/authentik/app-list.sh +~/.config/mosaic/tools/authentik/flow-list.sh +``` + +### Registering an OAuth Application + +1. Create an OAuth2 provider in Authentik admin (Applications > Providers) +2. Create an application linked to the provider (Applications > Applications) +3. Configure redirect URIs for the application +4. Store client_id and client_secret in Vault: `secret-{env}/{service}/oauth/authentik/` +5. Verify with: `~/.config/mosaic/tools/authentik/app-list.sh` + +### API Reference + +- Base URL: `https://auth.diversecanvas.com` +- API prefix: `/api/v3/` +- OpenAPI schema: `/api/v3/schema/` +- Auth: Bearer token (obtained via `auth-token.sh`) + ## Common Vulnerabilities to Avoid 1. **Broken Authentication** diff --git a/guides/BACKEND.md b/guides/BACKEND.md index 8099008..a67e798 100644 --- a/guides/BACKEND.md +++ b/guides/BACKEND.md @@ -1,7 +1,7 @@ # Backend Development Guide ## Before Starting -1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me` +1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md` 3. Review API contracts and database schema diff --git a/guides/BOOTSTRAP.md b/guides/BOOTSTRAP.md index 478fda7..e4e9c81 100755 --- a/guides/BOOTSTRAP.md +++ b/guides/BOOTSTRAP.md @@ -19,7 +19,7 @@ This guide covers how to bootstrap a project so AI agents (Claude, Codex, etc.) ```bash # Automated bootstrap (recommended) -~/.config/mosaic/rails/bootstrap/init-project.sh \ +~/.config/mosaic/tools/bootstrap/init-project.sh \ --name "my-project" \ --type "nestjs-nextjs" \ --repo "https://git.mosaicstack.dev/owner/repo" @@ -240,10 +240,10 @@ Documentation root hygiene (HARD RULE): ```bash # Use the init script -~/.config/mosaic/rails/bootstrap/init-repo-labels.sh +~/.config/mosaic/tools/bootstrap/init-repo-labels.sh # Or manually create standard labels -~/.config/mosaic/rails/git/issue-create.sh # (labels are created on first use) +~/.config/mosaic/tools/git/issue-create.sh # (labels are created on first use) ``` ### Standard Labels @@ -264,10 +264,10 @@ Create the first pre-MVP milestone at `0.0.1`. Reserve `0.1.0` for the MVP release milestone. ```bash -~/.config/mosaic/rails/git/milestone-create.sh -t "0.0.1" -d "Pre-MVP - Foundation Sprint" +~/.config/mosaic/tools/git/milestone-create.sh -t "0.0.1" -d "Pre-MVP - Foundation Sprint" # Create when MVP scope is complete and release-ready: -~/.config/mosaic/rails/git/milestone-create.sh -t "0.1.0" -d "MVP - Minimum Viable Product" +~/.config/mosaic/tools/git/milestone-create.sh -t "0.1.0" -d "MVP - Minimum Viable Product" ``` --- @@ -293,8 +293,8 @@ This enforces one merge strategy across human and agent workflows. ```bash # Copy Codex review pipeline mkdir -p .woodpecker/schemas -cp ~/.config/mosaic/rails/codex/woodpecker/codex-review.yml .woodpecker/ -cp ~/.config/mosaic/rails/codex/schemas/*.json .woodpecker/schemas/ +cp ~/.config/mosaic/tools/codex/woodpecker/codex-review.yml .woodpecker/ +cp ~/.config/mosaic/tools/codex/schemas/*.json .woodpecker/schemas/ # Add codex_api_key secret to Woodpecker CI dashboard ``` @@ -366,7 +366,7 @@ fi # (execute the command block under "Quality Gates") # Test Codex review (if configured) -~/.config/mosaic/rails/codex/codex-code-review.sh --help +~/.config/mosaic/tools/codex/codex-code-review.sh --help # Verify sequential-thinking MCP remains configured ~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check @@ -434,7 +434,7 @@ fi Full project bootstrap with interactive and flag-based modes: ```bash -~/.config/mosaic/rails/bootstrap/init-project.sh \ +~/.config/mosaic/tools/bootstrap/init-project.sh \ --name "My Project" \ --type "nestjs-nextjs" \ --repo "https://git.mosaicstack.dev/owner/repo" \ @@ -447,7 +447,7 @@ Full project bootstrap with interactive and flag-based modes: Initialize standard labels and the first pre-MVP milestone: ```bash -~/.config/mosaic/rails/bootstrap/init-repo-labels.sh +~/.config/mosaic/tools/bootstrap/init-repo-labels.sh ``` --- @@ -483,4 +483,4 @@ After bootstrapping, verify: - [ ] `.env.example` exists (if project uses env vars) - [ ] CI/CD pipeline configured (if using Woodpecker/GitHub Actions) - [ ] Python publish path configured in CI (if project ships Python packages) -- [ ] Codex review scripts accessible (`~/.config/mosaic/rails/codex/`) +- [ ] Codex review scripts accessible (`~/.config/mosaic/tools/codex/`) diff --git a/guides/CODE-REVIEW.md b/guides/CODE-REVIEW.md index e96679a..0fc2621 100755 --- a/guides/CODE-REVIEW.md +++ b/guides/CODE-REVIEW.md @@ -12,7 +12,7 @@ Merge strategy enforcement (HARD RULE): - PR target for delivery is `main`. - Direct pushes to `main` are prohibited. - Merge to `main` MUST be squash-only. -- Use `~/.config/mosaic/rails/git/pr-merge.sh -n {PR_NUMBER} -m squash` (or PowerShell equivalent). +- Use `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash` (or PowerShell equivalent). ## Review Checklist @@ -101,7 +101,7 @@ Use `~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md` whenever code/A ### Getting Context ```bash # List the issue being addressed -~/.config/mosaic/rails/git/issue-list.sh -i {issue-number} +~/.config/mosaic/tools/git/issue-list.sh -i {issue-number} # View the changes git diff main...HEAD diff --git a/guides/FRONTEND.md b/guides/FRONTEND.md index a0eb6c3..337944c 100644 --- a/guides/FRONTEND.md +++ b/guides/FRONTEND.md @@ -1,7 +1,7 @@ # Frontend Development Guide ## Before Starting -1. Check assigned issue in git repo: `~/.config/mosaic/rails/git/issue-list.sh -a @me` +1. Check assigned issue in git repo: `~/.config/mosaic/tools/git/issue-list.sh -a @me` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md` 3. Review existing components and patterns in the codebase diff --git a/guides/INFRASTRUCTURE.md b/guides/INFRASTRUCTURE.md index 0f1439b..9367770 100644 --- a/guides/INFRASTRUCTURE.md +++ b/guides/INFRASTRUCTURE.md @@ -1,7 +1,7 @@ # Infrastructure & DevOps Guide ## Before Starting -1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me` +1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md` 3. Review existing infrastructure configuration @@ -97,10 +97,10 @@ readinessProbe: periodSeconds: 3 ``` -## CI/CD Pipelines - -### Pipeline Stages -1. **Lint**: Code style and static analysis +## CI/CD Pipelines + +### Pipeline Stages +1. **Lint**: Code style and static analysis 2. **Test**: Unit and integration tests 3. **Build**: Compile and package 4. **Scan**: Security and vulnerability scanning @@ -109,65 +109,96 @@ readinessProbe: ### Pipeline Security - Use secrets management (not hardcoded) - Pin action/image versions -- Implement approval gates for production -- Audit pipeline access - -## Steered-Autonomous Deployment (Hard Rule) - -In lights-out mode, the agent owns deployment end-to-end when deployment is in scope. -The human is escalation-only for missing access, hard policy conflicts, or irreversible risk. - -### Deployment Target Selection - -1. Use explicit target from `docs/PRD.md` / `docs/PRD.json` or `docs/DEPLOYMENT.md`. -2. If unspecified, infer from existing project config/integration. -3. If multiple targets exist, choose the target already wired in CI/CD and document rationale. - -### Supported Targets - -- **Portainer**: Deploy via configured stack webhook/API, then verify service health and container status. -- **Coolify**: Trigger deployment via Coolify API/webhook, then verify deployment status and endpoint health. -- **Vercel**: Deploy via `vercel` CLI or connected Git integration, then verify preview/production URL health. -- **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates. - -### Image Tagging and Promotion (Hard Rule) - -For containerized deployments: - -1. Build immutable image tags: `sha-<shortsha>` and `v{base-version}-rc.{build}`. -2. Use mutable environment tags only as pointers: `testing`, optional `staging`, and `prod`. -3. Deploy by immutable digest, not by mutable tag alone. -4. Promote the exact tested digest between environments (no rebuild between testing and prod). -5. Do not use `latest` or `dev` as deployment references. - -Blue-green is the default strategy for production promotion. -Canary is allowed only when automated SLO/error-rate gates and auto-rollback triggers are implemented. - -### Post-Deploy Validation (REQUIRED) - -1. Health endpoints return expected status. -2. Critical smoke tests pass in target environment. -3. Running version and digest match the promoted release candidate. -4. Observability signals (errors/latency) are within expected thresholds. - -### Rollback Rule - -If post-deploy validation fails: - -1. Execute rollback/redeploy-safe path immediately. -2. Mark deployment as blocked in `docs/TASKS.md`. -3. Record failure evidence and next remediation step in scratchpad and release notes. - -### Registry Retention and Cleanup - -Cleanup MUST be automated. - -- Keep all final release tags (`vX.Y.Z`) indefinitely. -- Keep active environment digests (`prod`, `testing`, and active blue/green slots). -- Keep recent RC tags (`vX.Y.Z-rc.N`) based on retention window. -- Remove stale `sha-*` and RC tags outside retention window if they are not actively deployed. - -## Monitoring & Logging +- Implement approval gates for production +- Audit pipeline access + +## Steered-Autonomous Deployment (Hard Rule) + +In lights-out mode, the agent owns deployment end-to-end when deployment is in scope. +The human is escalation-only for missing access, hard policy conflicts, or irreversible risk. + +### Deployment Target Selection + +1. Use explicit target from `docs/PRD.md` / `docs/PRD.json` or `docs/DEPLOYMENT.md`. +2. If unspecified, infer from existing project config/integration. +3. If multiple targets exist, choose the target already wired in CI/CD and document rationale. + +### Supported Targets + +- **Portainer**: Deploy via `~/.config/mosaic/tools/portainer/stack-redeploy.sh`, then verify with `stack-status.sh`. +- **Coolify**: Deploy via `~/.config/mosaic/tools/coolify/deploy.sh -u <uuid>`, then verify with `service-status.sh`. +- **Vercel**: Deploy via `vercel` CLI or connected Git integration, then verify preview/production URL health. +- **Other SaaS providers**: Use provider CLI/API/runbook with the same validation and rollback gates. + +### Coolify API Operations + +```bash +# List projects and services +~/.config/mosaic/tools/coolify/project-list.sh +~/.config/mosaic/tools/coolify/service-list.sh + +# Check service status +~/.config/mosaic/tools/coolify/service-status.sh -u <uuid> + +# Set env vars (takes effect on next deploy) +~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k KEY -v VALUE + +# Deploy +~/.config/mosaic/tools/coolify/deploy.sh -u <uuid> +``` + +**Known Coolify Limitations:** +- FQDN updates on compose sub-apps not supported via API (DB workaround required) +- Compose files must be base64-encoded in `docker_compose_raw` field +- Magic variables (`SERVICE_FQDN_*`) require list-style env syntax, not dict-style +- Rate limit: 200 requests per interval + +### Stack Health Check + +Verify all infrastructure services are reachable: + +```bash +~/.config/mosaic/tools/health/stack-health.sh +``` + +### Image Tagging and Promotion (Hard Rule) + +For containerized deployments: + +1. Build immutable image tags: `sha-<shortsha>` and `v{base-version}-rc.{build}`. +2. Use mutable environment tags only as pointers: `testing`, optional `staging`, and `prod`. +3. Deploy by immutable digest, not by mutable tag alone. +4. Promote the exact tested digest between environments (no rebuild between testing and prod). +5. Do not use `latest` or `dev` as deployment references. + +Blue-green is the default strategy for production promotion. +Canary is allowed only when automated SLO/error-rate gates and auto-rollback triggers are implemented. + +### Post-Deploy Validation (REQUIRED) + +1. Health endpoints return expected status. +2. Critical smoke tests pass in target environment. +3. Running version and digest match the promoted release candidate. +4. Observability signals (errors/latency) are within expected thresholds. + +### Rollback Rule + +If post-deploy validation fails: + +1. Execute rollback/redeploy-safe path immediately. +2. Mark deployment as blocked in `docs/TASKS.md`. +3. Record failure evidence and next remediation step in scratchpad and release notes. + +### Registry Retention and Cleanup + +Cleanup MUST be automated. + +- Keep all final release tags (`vX.Y.Z`) indefinitely. +- Keep active environment digests (`prod`, `testing`, and active blue/green slots). +- Keep recent RC tags (`vX.Y.Z-rc.N`) based on retention window. +- Remove stale `sha-*` and RC tags outside retention window if they are not actively deployed. + +## Monitoring & Logging ### Logging Standards - Use structured logging (JSON) diff --git a/guides/QA-TESTING.md b/guides/QA-TESTING.md index 810fe82..3bfcdc8 100644 --- a/guides/QA-TESTING.md +++ b/guides/QA-TESTING.md @@ -2,7 +2,7 @@ ## Before Starting -1. Check assigned issue: `~/.config/mosaic/rails/git/issue-list.sh -a @me` +1. Check assigned issue: `~/.config/mosaic/tools/git/issue-list.sh -a @me` 2. Create scratchpad: `docs/scratchpads/{issue-number}-{short-name}.md` 3. Review `docs/PRD.md` or `docs/PRD.json` as the requirements source. 4. Review acceptance criteria and affected change surfaces. diff --git a/guides/ci-cd-pipelines.md b/guides/ci-cd-pipelines.md index 27f9c0a..332ead7 100644 --- a/guides/ci-cd-pipelines.md +++ b/guides/ci-cd-pipelines.md @@ -870,7 +870,7 @@ Required sequence: 1. Merge PR to `main` (squash) via Mosaic wrapper. 2. Monitor CI to terminal status: ```bash - ~/.config/mosaic/rails/git/pr-ci-wait.sh -n <PR_NUMBER> + ~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER> ``` 3. Require green status before claiming completion. 4. If CI fails, create remediation task(s) and continue until green. @@ -885,8 +885,8 @@ Woodpecker note: Before pushing a branch or merging a PR, guard against overlapping project pipelines: ```bash -~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main -~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main +~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main +~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main ``` Behavior: diff --git a/guides/e2e-delivery.md b/guides/e2e-delivery.md index f494b1d..53e82f9 100644 --- a/guides/e2e-delivery.md +++ b/guides/e2e-delivery.md @@ -25,8 +25,8 @@ First response MUST declare mode before tool calls or implementation steps: 1. For non-trivial work, `docs/TASKS.md` MUST exist before coding. 2. If `docs/TASKS.md` is missing, create it from `~/.config/mosaic/templates/docs/TASKS.md.template`. -3. Detect provider first via `~/.config/mosaic/rails/git/detect-platform.sh`. -4. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/rails/git/*.sh`). +3. Detect provider first via `~/.config/mosaic/tools/git/detect-platform.sh`. +4. For issue/PR/milestone operations, use Mosaic wrappers first (`~/.config/mosaic/tools/git/*.sh`). 5. If external git provider is available (Gitea/GitHub/GitLab), create or update issue(s) before coding. 6. Record provider issue reference(s) in `docs/TASKS.md` (example: `#123`). 7. If no external provider is available, use internal task refs in `docs/TASKS.md` (example: `TASKS:T1`). @@ -73,11 +73,11 @@ For implementation work, you MUST run this cycle in order: 5. `remediate` - fix all findings and any test failures. 6. `review` - re-review remediated changes until blockers are cleared. 7. `commit` - commit only when the logical unit passes tests and review. -8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push`. +8. `pre-push queue guard` - before pushing, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push`. 9. `push` - push immediately after queue guard passes. 10. `PR integration` - if external git provider is available, create/update PR to `main` and merge with required strategy via Mosaic wrappers. -11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines to clear: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge`. -12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/rails/git/pr-ci-wait.sh` for PR-based workflow). +11. `pre-merge queue guard` - before merging PR, wait for running/queued project pipelines to clear: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge`. +12. `CI/pipeline verification` - wait for terminal CI status and require green before completion (`~/.config/mosaic/tools/git/pr-ci-wait.sh` for PR-based workflow). 13. `issue closure` - close linked external issue (or close internal `docs/TASKS.md` task ref when provider is unavailable). 14. `greenfield situational test` - validate required user flows in a clean environment/startup path (post-merge for trunk workflow changes). 15. `deploy + post-deploy validation` - when deployment is in scope, deploy to configured target and run post-deploy health/smoke checks. @@ -85,10 +85,10 @@ For implementation work, you MUST run this cycle in order: ### Post-PR Hard Gate (Execute Sequentially, No Exceptions) -1. `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main` -2. `~/.config/mosaic/rails/git/pr-merge.sh -n <PR_NUMBER> -m squash` -3. `~/.config/mosaic/rails/git/pr-ci-wait.sh -n <PR_NUMBER>` -4. `~/.config/mosaic/rails/git/issue-close.sh -i <ISSUE_NUMBER>` (or close internal `docs/TASKS.md` ref when no provider exists) +1. `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main` +2. `~/.config/mosaic/tools/git/pr-merge.sh -n <PR_NUMBER> -m squash` +3. `~/.config/mosaic/tools/git/pr-ci-wait.sh -n <PR_NUMBER>` +4. `~/.config/mosaic/tools/git/issue-close.sh -i <ISSUE_NUMBER>` (or close internal `docs/TASKS.md` ref when no provider exists) 5. If any step fails: set status `blocked`, report the exact failed wrapper command, and stop. 6. Do not ask the human to perform routine merge/close operations. 7. Do not claim completion before step 4 succeeds. diff --git a/guides/orchestrator.md b/guides/orchestrator.md index d93849d..c2d3861 100644 --- a/guides/orchestrator.md +++ b/guides/orchestrator.md @@ -272,7 +272,7 @@ Provider options: 1. Gitea (preferred when available) via Mosaic helper: ```bash -~/.config/mosaic/rails/git/issue-create.sh \ +~/.config/mosaic/tools/git/issue-create.sh \ -t "Phase 1: Critical Security Fixes" \ -b "$(cat <<'EOF' ## Findings @@ -412,15 +412,15 @@ git push and checklist completed (`~/.config/mosaic/templates/docs/DOCUMENTATION-CHECKLIST.md`) when applicable. 13. **PR + CI + Issue Closure Gate** (HARD RULE for source-code tasks): - Before merging, run queue guard: - `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose merge -B main` + `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose merge -B main` - Ensure PR exists for the task branch (create/update via wrappers if needed): - `~/.config/mosaic/rails/git/pr-create.sh ... -B main` + `~/.config/mosaic/tools/git/pr-create.sh ... -B main` - Merge via wrapper: - `~/.config/mosaic/rails/git/pr-merge.sh -n {PR_NUMBER} -m squash` + `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash` - Wait for terminal CI status: - `~/.config/mosaic/rails/git/pr-ci-wait.sh -n {PR_NUMBER}` + `~/.config/mosaic/tools/git/pr-ci-wait.sh -n {PR_NUMBER}` - Close linked issue after merge + green CI: - `~/.config/mosaic/rails/git/issue-close.sh -i {ISSUE_NUMBER}` + `~/.config/mosaic/tools/git/issue-close.sh -i {ISSUE_NUMBER}` - If any wrapper command fails, mark task `blocked`, record the exact failed wrapper command, report blocker, and STOP. - Do NOT stop at "PR created" or "PR merged pending CI". - Do NOT claim completion before CI is green and issue/internal ref is closed. @@ -463,10 +463,10 @@ Run review when the worker's result includes code changes (commits). Skip for ta cd {project_path} # Code quality review -~/.config/mosaic/rails/codex/codex-code-review.sh -b {base_branch} -o /tmp/review-{task_id}.json +~/.config/mosaic/tools/codex/codex-code-review.sh -b {base_branch} -o /tmp/review-{task_id}.json # Security review -~/.config/mosaic/rails/codex/codex-security-review.sh -b {base_branch} -o /tmp/security-{task_id}.json +~/.config/mosaic/tools/codex/codex-security-review.sh -b {base_branch} -o /tmp/security-{task_id}.json ``` ### Step 2: Parse Review Results @@ -599,19 +599,19 @@ Construct this from the task row and pass to worker via Task tool: 7. If task is bug fix/security/auth/critical business logic, apply REQUIRED TDD discipline per `~/.config/mosaic/guides/QA-TESTING.md`. 8. If gates or required situational tests fail: Fix and retry. Do NOT report success with failures. 9. Commit: `git commit -m "fix({finding_id}): brief description"` -10. Before push, run queue guard: `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push -B main` +10. Before push, run queue guard: `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push -B main` 11. Push: `git push origin {branch}` 12. Report result as JSON (see format below) ## Git Scripts For issue/PR/milestone operations, use scripts (NOT raw tea/gh): -- `~/.config/mosaic/rails/git/issue-view.sh -i {N}` -- `~/.config/mosaic/rails/git/pr-create.sh -t "Title" -b "Desc" -B main` -- `~/.config/mosaic/rails/git/ci-queue-wait.sh --purpose push|merge -B main` -- `~/.config/mosaic/rails/git/pr-merge.sh -n {PR_NUMBER} -m squash` -- `~/.config/mosaic/rails/git/pr-ci-wait.sh -n {PR_NUMBER}` -- `~/.config/mosaic/rails/git/issue-close.sh -i {N}` +- `~/.config/mosaic/tools/git/issue-view.sh -i {N}` +- `~/.config/mosaic/tools/git/pr-create.sh -t "Title" -b "Desc" -B main` +- `~/.config/mosaic/tools/git/ci-queue-wait.sh --purpose push|merge -B main` +- `~/.config/mosaic/tools/git/pr-merge.sh -n {PR_NUMBER} -m squash` +- `~/.config/mosaic/tools/git/pr-ci-wait.sh -n {PR_NUMBER}` +- `~/.config/mosaic/tools/git/issue-close.sh -i {N}` Standard git commands (pull, commit, push, checkout) are fine. @@ -1035,7 +1035,7 @@ When all tasks in `docs/TASKS.md` are `done` (or triaged as `deferred`), you MUS 5. **Close milestone in provider**: - Gitea/GitHub: ```bash - ~/.config/mosaic/rails/git/milestone-close.sh -t "{milestone-name}" + ~/.config/mosaic/tools/git/milestone-close.sh -t "{milestone-name}" ``` - GitLab: close milestone via provider workflow (CLI or web UI). If provider tooling is unavailable, record milestone closure status in `docs/TASKS.md` notes. diff --git a/install.sh b/install.sh index 2e7dd0b..03843d2 100755 --- a/install.sh +++ b/install.sh @@ -144,6 +144,17 @@ mkdir -p "$TARGET_DIR/memory" chmod +x "$TARGET_DIR"/bin/* chmod +x "$TARGET_DIR"/install.sh +# Ensure tool scripts are executable +find "$TARGET_DIR/tools" -name "*.sh" -exec chmod +x {} + 2>/dev/null || true + +# Create backward-compat symlink: rails/ → tools/ +if [[ -d "$TARGET_DIR/tools" ]]; then + if [[ -d "$TARGET_DIR/rails" ]] && [[ ! -L "$TARGET_DIR/rails" ]]; then + rm -rf "$TARGET_DIR/rails" + fi + ln -sfn "tools" "$TARGET_DIR/rails" +fi + ok "Framework installed to $TARGET_DIR" step "Post-install tasks" diff --git a/runtime/claude/RUNTIME.md b/runtime/claude/RUNTIME.md index 91072ba..433ea14 100644 --- a/runtime/claude/RUNTIME.md +++ b/runtime/claude/RUNTIME.md @@ -11,7 +11,7 @@ This file applies only to Claude runtime behavior. 3. Treat sequential-thinking MCP as required. 4. If runtime config conflicts with global rules, global rules win. 5. Documentation rules are inherited from `~/.config/mosaic/AGENTS.md` and `~/.config/mosaic/guides/DOCUMENTATION.md`. -6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/rails/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first. +6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/tools/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first. 7. For orchestration-oriented missions, load `~/.config/mosaic/guides/ORCHESTRATOR.md` before acting. 8. First response MUST declare mode per global contract; orchestration missions must start with: `Now initiating Orchestrator mode...` 9. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates. diff --git a/runtime/claude/settings.json b/runtime/claude/settings.json index a27b562..9fc9df4 100644 --- a/runtime/claude/settings.json +++ b/runtime/claude/settings.json @@ -7,7 +7,7 @@ "hooks": [ { "type": "command", - "command": "~/.config/mosaic/rails/qa/qa-hook-stdin.sh", + "command": "~/.config/mosaic/tools/qa/qa-hook-stdin.sh", "timeout": 60 } ] diff --git a/runtime/codex/RUNTIME.md b/runtime/codex/RUNTIME.md index f5487ea..0ca1486 100644 --- a/runtime/codex/RUNTIME.md +++ b/runtime/codex/RUNTIME.md @@ -11,7 +11,7 @@ This file applies only to Codex runtime behavior. 3. Treat sequential-thinking MCP as required. 4. If runtime config conflicts with global rules, global rules win. 5. Documentation rules are inherited from `~/.config/mosaic/AGENTS.md` and `~/.config/mosaic/guides/DOCUMENTATION.md`. -6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/rails/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first. +6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/tools/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first. 7. For orchestration-oriented missions, load `~/.config/mosaic/guides/ORCHESTRATOR.md` before acting. 8. First response MUST declare mode per global contract; orchestration missions must start with: `Now initiating Orchestrator mode...` 9. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates. diff --git a/runtime/opencode/RUNTIME.md b/runtime/opencode/RUNTIME.md index 6add5c0..a8b743a 100644 --- a/runtime/opencode/RUNTIME.md +++ b/runtime/opencode/RUNTIME.md @@ -11,7 +11,7 @@ This file applies only to OpenCode runtime behavior. 3. Treat sequential-thinking MCP as required. 4. If runtime config conflicts with global rules, global rules win. 5. Documentation rules are inherited from `~/.config/mosaic/AGENTS.md` and `~/.config/mosaic/guides/DOCUMENTATION.md`. -6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/rails/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first. +6. For issue/PR/milestone actions, run Mosaic git wrappers first (`~/.config/mosaic/tools/git/*.sh`) and do not call raw `gh`/`tea`/`glab` first. 7. For orchestration-oriented missions, load `~/.config/mosaic/guides/ORCHESTRATOR.md` before acting. 8. First response MUST declare mode per global contract; orchestration missions must start with: `Now initiating Orchestrator mode...` 9. Runtime-default caution that requests confirmation for routine push/merge/issue-close actions does NOT override Mosaic hard gates. diff --git a/skills-local/mosaic-standards/SKILL.md b/skills-local/mosaic-standards/SKILL.md index 29a3e07..0503fa0 100644 --- a/skills-local/mosaic-standards/SKILL.md +++ b/skills-local/mosaic-standards/SKILL.md @@ -25,7 +25,7 @@ If wrappers are available, you may use: ## Enforcement Rules -- Treat `~/.config/mosaic` as canonical for shared guides, rails, profiles, and skills. +- Treat `~/.config/mosaic` as canonical for shared guides, tools, profiles, and skills. - Do not edit generated project views directly when the repo defines canonical data sources. - Pull/rebase before edits in shared repositories. - Run project verification commands before claiming completion. diff --git a/skills-local/setup-cicd/SKILL.md b/skills-local/setup-cicd/SKILL.md index d4aef25..8c8449d 100644 --- a/skills-local/setup-cicd/SKILL.md +++ b/skills-local/setup-cicd/SKILL.md @@ -140,7 +140,7 @@ Ask these questions with lettered options (user can respond "1A, 2B, 3C"): If the project's `.woodpecker.yml` doesn't already have a `kaniko_setup` anchor in its `variables:` section, add it: ```bash -~/.config/mosaic/rails/cicd/generate-docker-steps.sh --kaniko-setup-only --registry REGISTRY_HOST +~/.config/mosaic/tools/cicd/generate-docker-steps.sh --kaniko-setup-only --registry REGISTRY_HOST ``` This outputs: @@ -158,7 +158,7 @@ Add this to the existing `variables:` block at the top of `.woodpecker.yml`. Use the generator script with the user's answers: ```bash -~/.config/mosaic/rails/cicd/generate-docker-steps.sh \ +~/.config/mosaic/tools/cicd/generate-docker-steps.sh \ --registry REGISTRY \ --org ORG \ --repo REPO \ diff --git a/templates/agent/fragments/code-review.md b/templates/agent/fragments/code-review.md index 407794c..9c94e92 100644 --- a/templates/agent/fragments/code-review.md +++ b/templates/agent/fragments/code-review.md @@ -5,10 +5,10 @@ Run independent reviews: ```bash # Code quality review (Codex) -~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted +~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted # Security review (Codex) -~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted +~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted ``` **Fallback:** If Codex is unavailable, use Claude's built-in review skills. diff --git a/tools/_lib/credentials.sh b/tools/_lib/credentials.sh new file mode 100755 index 0000000..3eadcc2 --- /dev/null +++ b/tools/_lib/credentials.sh @@ -0,0 +1,175 @@ +#!/usr/bin/env bash +# +# credentials.sh — Shared credential loader for Mosaic tool suites +# +# Usage: source ~/.config/mosaic/tools/_lib/credentials.sh +# load_credentials <service-name> +# +# Loads credentials from environment variables first, then falls back +# to ~/src/jarvis-brain/credentials.json (or MOSAIC_CREDENTIALS_FILE). +# +# Supported services: +# portainer, coolify, authentik, glpi, github, +# gitea-mosaicstack, gitea-usc, woodpecker +# +# After loading, service-specific env vars are exported. +# Run `load_credentials --help` for details. + +MOSAIC_CREDENTIALS_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" + +_mosaic_require_jq() { + if ! command -v jq &>/dev/null; then + echo "Error: jq is required but not installed" >&2 + return 1 + fi +} + +_mosaic_read_cred() { + local jq_path="$1" + if [[ ! -f "$MOSAIC_CREDENTIALS_FILE" ]]; then + echo "Error: Credentials file not found: $MOSAIC_CREDENTIALS_FILE" >&2 + return 1 + fi + jq -r "$jq_path // empty" "$MOSAIC_CREDENTIALS_FILE" +} + +load_credentials() { + local service="$1" + + if [[ -z "$service" || "$service" == "--help" ]]; then + cat <<'EOF' +Usage: load_credentials <service> + +Services and exported variables: + portainer → PORTAINER_URL, PORTAINER_API_KEY + coolify → COOLIFY_URL, COOLIFY_TOKEN + authentik → AUTHENTIK_URL, AUTHENTIK_TOKEN, AUTHENTIK_USERNAME, AUTHENTIK_PASSWORD + glpi → GLPI_URL, GLPI_APP_TOKEN, GLPI_USER_TOKEN + github → GITHUB_TOKEN + gitea-mosaicstack → GITEA_URL, GITEA_TOKEN + gitea-usc → GITEA_URL, GITEA_TOKEN + woodpecker → WOODPECKER_URL, WOODPECKER_TOKEN +EOF + return 0 + fi + + _mosaic_require_jq || return 1 + + case "$service" in + portainer) + export PORTAINER_URL="${PORTAINER_URL:-$(_mosaic_read_cred '.portainer.url')}" + export PORTAINER_API_KEY="${PORTAINER_API_KEY:-$(_mosaic_read_cred '.portainer.api_key')}" + PORTAINER_URL="${PORTAINER_URL%/}" + [[ -n "$PORTAINER_URL" ]] || { echo "Error: portainer.url not found" >&2; return 1; } + [[ -n "$PORTAINER_API_KEY" ]] || { echo "Error: portainer.api_key not found" >&2; return 1; } + ;; + coolify) + export COOLIFY_URL="${COOLIFY_URL:-$(_mosaic_read_cred '.coolify.url')}" + export COOLIFY_TOKEN="${COOLIFY_TOKEN:-$(_mosaic_read_cred '.coolify.app_token')}" + COOLIFY_URL="${COOLIFY_URL%/}" + [[ -n "$COOLIFY_URL" ]] || { echo "Error: coolify.url not found" >&2; return 1; } + [[ -n "$COOLIFY_TOKEN" ]] || { echo "Error: coolify.app_token not found" >&2; return 1; } + ;; + authentik) + export AUTHENTIK_URL="${AUTHENTIK_URL:-$(_mosaic_read_cred '.authentik.url')}" + export AUTHENTIK_TOKEN="${AUTHENTIK_TOKEN:-$(_mosaic_read_cred '.authentik.token')}" + export AUTHENTIK_USERNAME="${AUTHENTIK_USERNAME:-$(_mosaic_read_cred '.authentik.username')}" + export AUTHENTIK_PASSWORD="${AUTHENTIK_PASSWORD:-$(_mosaic_read_cred '.authentik.password')}" + AUTHENTIK_URL="${AUTHENTIK_URL%/}" + [[ -n "$AUTHENTIK_URL" ]] || { echo "Error: authentik.url not found" >&2; return 1; } + ;; + glpi) + export GLPI_URL="${GLPI_URL:-$(_mosaic_read_cred '.glpi.url')}" + export GLPI_APP_TOKEN="${GLPI_APP_TOKEN:-$(_mosaic_read_cred '.glpi.app_token')}" + export GLPI_USER_TOKEN="${GLPI_USER_TOKEN:-$(_mosaic_read_cred '.glpi.user_token')}" + GLPI_URL="${GLPI_URL%/}" + [[ -n "$GLPI_URL" ]] || { echo "Error: glpi.url not found" >&2; return 1; } + ;; + github) + export GITHUB_TOKEN="${GITHUB_TOKEN:-$(_mosaic_read_cred '.github.token')}" + [[ -n "$GITHUB_TOKEN" ]] || { echo "Error: github.token not found" >&2; return 1; } + ;; + gitea-mosaicstack) + export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.mosaicstack.url')}" + export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.mosaicstack.token')}" + GITEA_URL="${GITEA_URL%/}" + [[ -n "$GITEA_URL" ]] || { echo "Error: gitea.mosaicstack.url not found" >&2; return 1; } + [[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.mosaicstack.token not found" >&2; return 1; } + ;; + gitea-usc) + export GITEA_URL="${GITEA_URL:-$(_mosaic_read_cred '.gitea.usc.url')}" + export GITEA_TOKEN="${GITEA_TOKEN:-$(_mosaic_read_cred '.gitea.usc.token')}" + GITEA_URL="${GITEA_URL%/}" + [[ -n "$GITEA_URL" ]] || { echo "Error: gitea.usc.url not found" >&2; return 1; } + [[ -n "$GITEA_TOKEN" ]] || { echo "Error: gitea.usc.token not found" >&2; return 1; } + ;; + woodpecker) + export WOODPECKER_URL="${WOODPECKER_URL:-$(_mosaic_read_cred '.woodpecker.url')}" + export WOODPECKER_TOKEN="${WOODPECKER_TOKEN:-$(_mosaic_read_cred '.woodpecker.token')}" + WOODPECKER_URL="${WOODPECKER_URL%/}" + [[ -n "$WOODPECKER_URL" ]] || { echo "Error: woodpecker.url not found" >&2; return 1; } + [[ -n "$WOODPECKER_TOKEN" ]] || { echo "Error: woodpecker.token not found" >&2; return 1; } + ;; + *) + echo "Error: Unknown service '$service'" >&2 + echo "Supported: portainer, coolify, authentik, glpi, github, gitea-mosaicstack, gitea-usc, woodpecker" >&2 + return 1 + ;; + esac +} + +# Common HTTP helper — makes a curl request and separates body from status code +# Usage: mosaic_http GET "/api/v1/endpoint" "Authorization: Bearer $TOKEN" [base_url] +# Returns: body on stdout, sets MOSAIC_HTTP_CODE +mosaic_http() { + local method="$1" + local endpoint="$2" + local auth_header="$3" + local base_url="${4:-}" + + local response + response=$(curl -sk -w "\n%{http_code}" -X "$method" \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + "${base_url}${endpoint}") + + MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1) + echo "$response" | sed '$d' +} + +# POST variant with body +# Usage: mosaic_http_post "/api/v1/endpoint" "Authorization: Bearer $TOKEN" '{"key":"val"}' [base_url] +mosaic_http_post() { + local endpoint="$1" + local auth_header="$2" + local data="$3" + local base_url="${4:-}" + + local response + response=$(curl -sk -w "\n%{http_code}" -X POST \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d "$data" \ + "${base_url}${endpoint}") + + MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1) + echo "$response" | sed '$d' +} + +# PATCH variant with body +mosaic_http_patch() { + local endpoint="$1" + local auth_header="$2" + local data="$3" + local base_url="${4:-}" + + local response + response=$(curl -sk -w "\n%{http_code}" -X PATCH \ + -H "$auth_header" \ + -H "Content-Type: application/json" \ + -d "$data" \ + "${base_url}${endpoint}") + + MOSAIC_HTTP_CODE=$(echo "$response" | tail -n1) + echo "$response" | sed '$d' +} diff --git a/tools/authentik/README.md b/tools/authentik/README.md new file mode 100644 index 0000000..166a744 --- /dev/null +++ b/tools/authentik/README.md @@ -0,0 +1,59 @@ +# Authentik Tool Suite + +Manage Authentik identity provider (SSO, users, groups, applications, flows) via CLI. + +## Prerequisites + +- `jq` installed +- Authentik credentials in `~/src/jarvis-brain/credentials.json` (or `$MOSAIC_CREDENTIALS_FILE`) +- Required fields: `authentik.url`, `authentik.username`, `authentik.password` + +## Authentication + +Scripts use `auth-token.sh` to auto-authenticate via username/password and cache the API token at `~/.cache/mosaic/authentik-token`. The token is validated on each use and refreshed automatically when expired. + +For better security, create a long-lived API token in Authentik admin (Directory > Tokens) and set `$AUTHENTIK_TOKEN` in your environment — the scripts will use it directly. + +## Scripts + +| Script | Purpose | +|--------|---------| +| `auth-token.sh` | Authenticate and cache API token | +| `user-list.sh` | List users (search, filter by group) | +| `user-create.sh` | Create user with optional group assignment | +| `group-list.sh` | List groups | +| `app-list.sh` | List OAuth/SAML applications | +| `flow-list.sh` | List authentication flows | +| `admin-status.sh` | System health and version info | + +## Common Options + +All scripts support: +- `-f json` — JSON output (default: table) +- `-h` — Show help + +## API Reference + +- Base URL: `https://auth.diversecanvas.com` +- API prefix: `/api/v3/` +- OpenAPI schema: `/api/v3/schema/` +- Auth: Bearer token in `Authorization` header + +## Examples + +```bash +# List all users +~/.config/mosaic/tools/authentik/user-list.sh + +# Search for a user +~/.config/mosaic/tools/authentik/user-list.sh -s "jason" + +# Create a user in the admins group +~/.config/mosaic/tools/authentik/user-create.sh -u newuser -n "New User" -e new@example.com -g admins + +# List OAuth applications as JSON +~/.config/mosaic/tools/authentik/app-list.sh -f json + +# Check system health +~/.config/mosaic/tools/authentik/admin-status.sh +``` diff --git a/tools/authentik/admin-status.sh b/tools/authentik/admin-status.sh new file mode 100755 index 0000000..92927b0 --- /dev/null +++ b/tools/authentik/admin-status.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# +# admin-status.sh — Authentik system health and version info +# +# Usage: admin-status.sh [-f format] +# +# Options: +# -f format Output format: table (default), json +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials authentik + +FORMAT="table" + +while getopts "f:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format]" >&2; exit 1 ;; + esac +done + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) + +response=$(curl -sk -w "\n%{http_code}" \ + -H "Authorization: Bearer $TOKEN" \ + "${AUTHENTIK_URL}/api/v3/admin/system/") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to get system status (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "Authentik System Status" +echo "=======================" +echo "$body" | jq -r ' + " URL: \(.http_host // "unknown")\n" + + " Version: \(.runtime.authentik_version // "unknown")\n" + + " Python: \(.runtime.python_version // "unknown")\n" + + " Workers: \(.runtime.gunicorn_workers // "unknown")\n" + + " Build Hash: \(.runtime.build_hash // "unknown")\n" + + " Embedded Outpost: \(.embedded_outpost_host // "unknown")" +' diff --git a/tools/authentik/app-list.sh b/tools/authentik/app-list.sh new file mode 100755 index 0000000..8a6e271 --- /dev/null +++ b/tools/authentik/app-list.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# app-list.sh — List Authentik applications +# +# Usage: app-list.sh [-f format] [-s search] +# +# Options: +# -f format Output format: table (default), json +# -s search Search by application name +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials authentik + +FORMAT="table" +SEARCH="" + +while getopts "f:s:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + s) SEARCH="$OPTARG" ;; + h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-s search]" >&2; exit 1 ;; + esac +done + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) + +PARAMS="ordering=name" +[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}" + +response=$(curl -sk -w "\n%{http_code}" \ + -H "Authorization: Bearer $TOKEN" \ + "${AUTHENTIK_URL}/api/v3/core/applications/?${PARAMS}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list applications (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.results' + exit 0 +fi + +echo "NAME SLUG PROVIDER LAUNCH URL" +echo "---------------------------- ---------------------------- ----------------- ----------------------------------------" +echo "$body" | jq -r '.results[] | [ + .name, + .slug, + (.provider_obj.name // "none"), + (.launch_url // "—") +] | @tsv' | while IFS=$'\t' read -r name slug provider launch_url; do + printf "%-28s %-28s %-17s %s\n" \ + "${name:0:28}" "${slug:0:28}" "${provider:0:17}" "$launch_url" +done diff --git a/tools/authentik/auth-token.sh b/tools/authentik/auth-token.sh new file mode 100755 index 0000000..5f2c78c --- /dev/null +++ b/tools/authentik/auth-token.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# +# auth-token.sh — Obtain and cache Authentik API token +# +# Usage: auth-token.sh [-f] [-q] +# +# Returns a valid Authentik API token. Checks in order: +# 1. Cached token at ~/.cache/mosaic/authentik-token (if valid) +# 2. Pre-configured token from credentials.json (authentik.token) +# 3. Fails with instructions to create a token in the admin UI +# +# Options: +# -f Force re-validation (ignore cached token) +# -q Quiet mode — only output the token +# -h Show this help +# +# Environment variables (or credentials.json): +# AUTHENTIK_URL — Authentik instance URL +# AUTHENTIK_TOKEN — Pre-configured API token (recommended) +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials authentik + +CACHE_DIR="$HOME/.cache/mosaic" +CACHE_FILE="$CACHE_DIR/authentik-token" +FORCE=false +QUIET=false + +while getopts "fqh" opt; do + case $opt in + f) FORCE=true ;; + q) QUIET=true ;; + h) head -20 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f] [-q]" >&2; exit 1 ;; + esac +done + +_validate_token() { + local token="$1" + local http_code + http_code=$(curl -sk -o /dev/null -w "%{http_code}" \ + --connect-timeout 5 --max-time 10 \ + -H "Authorization: Bearer $token" \ + "${AUTHENTIK_URL}/api/v3/core/users/me/") + [[ "$http_code" == "200" ]] +} + +# 1. Check cached token +if [[ "$FORCE" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then + cached_token=$(cat "$CACHE_FILE") + if [[ -n "$cached_token" ]] && _validate_token "$cached_token"; then + [[ "$QUIET" == "false" ]] && echo "Using cached token (valid)" >&2 + echo "$cached_token" + exit 0 + fi + [[ "$QUIET" == "false" ]] && echo "Cached token invalid, checking credentials..." >&2 +fi + +# 2. Use pre-configured token from credentials.json +if [[ -n "${AUTHENTIK_TOKEN:-}" ]]; then + if _validate_token "$AUTHENTIK_TOKEN"; then + # Cache it for faster future access + mkdir -p "$CACHE_DIR" + echo "$AUTHENTIK_TOKEN" > "$CACHE_FILE" + chmod 600 "$CACHE_FILE" + [[ "$QUIET" == "false" ]] && echo "Token validated and cached at $CACHE_FILE" >&2 + echo "$AUTHENTIK_TOKEN" + exit 0 + else + echo "Error: Pre-configured AUTHENTIK_TOKEN is invalid (API returned non-200)" >&2 + exit 1 + fi +fi + +# 3. No token available +echo "Error: No Authentik API token configured" >&2 +echo "" >&2 +echo "To create one:" >&2 +echo " 1. Log into Authentik admin: ${AUTHENTIK_URL}/if/admin/#/core/tokens" >&2 +echo " 2. Click 'Create' → set identifier (e.g., 'mosaic-agent')" >&2 +echo " 3. Select 'API Token' intent, uncheck 'Expiring'" >&2 +echo " 4. Copy the key and add to credentials.json:" >&2 +echo " jq '.authentik.token = \"<your-token>\"' credentials.json > tmp && mv tmp credentials.json" >&2 +exit 1 diff --git a/tools/authentik/flow-list.sh b/tools/authentik/flow-list.sh new file mode 100755 index 0000000..e7bbf93 --- /dev/null +++ b/tools/authentik/flow-list.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# flow-list.sh — List Authentik flows +# +# Usage: flow-list.sh [-f format] [-d designation] +# +# Options: +# -f format Output format: table (default), json +# -d designation Filter by designation (authentication, authorization, enrollment, etc.) +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials authentik + +FORMAT="table" +DESIGNATION="" + +while getopts "f:d:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + d) DESIGNATION="$OPTARG" ;; + h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-d designation]" >&2; exit 1 ;; + esac +done + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) + +PARAMS="ordering=slug" +[[ -n "$DESIGNATION" ]] && PARAMS="${PARAMS}&designation=${DESIGNATION}" + +response=$(curl -sk -w "\n%{http_code}" \ + -H "Authorization: Bearer $TOKEN" \ + "${AUTHENTIK_URL}/api/v3/flows/instances/?${PARAMS}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list flows (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.results' + exit 0 +fi + +echo "NAME SLUG DESIGNATION TITLE" +echo "---------------------------- ---------------------------- ---------------- ----------------------------" +echo "$body" | jq -r '.results[] | [ + .name, + .slug, + .designation, + (.title // "—") +] | @tsv' | while IFS=$'\t' read -r name slug designation title; do + printf "%-28s %-28s %-16s %s\n" \ + "${name:0:28}" "${slug:0:28}" "$designation" "${title:0:28}" +done diff --git a/tools/authentik/group-list.sh b/tools/authentik/group-list.sh new file mode 100755 index 0000000..a3f501d --- /dev/null +++ b/tools/authentik/group-list.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# group-list.sh — List Authentik groups +# +# Usage: group-list.sh [-f format] [-s search] +# +# Options: +# -f format Output format: table (default), json +# -s search Search by group name +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials authentik + +FORMAT="table" +SEARCH="" + +while getopts "f:s:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + s) SEARCH="$OPTARG" ;; + h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-s search]" >&2; exit 1 ;; + esac +done + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) + +PARAMS="ordering=name" +[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}" + +response=$(curl -sk -w "\n%{http_code}" \ + -H "Authorization: Bearer $TOKEN" \ + "${AUTHENTIK_URL}/api/v3/core/groups/?${PARAMS}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list groups (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.results' + exit 0 +fi + +echo "NAME PK MEMBERS SUPERUSER" +echo "---------------------------- ------------------------------------ ------- ---------" +echo "$body" | jq -r '.results[] | [ + .name, + .pk, + (.users | length | tostring), + (if .is_superuser then "yes" else "no" end) +] | @tsv' | while IFS=$'\t' read -r name pk members superuser; do + printf "%-28s %-36s %-7s %s\n" "${name:0:28}" "$pk" "$members" "$superuser" +done diff --git a/tools/authentik/user-create.sh b/tools/authentik/user-create.sh new file mode 100755 index 0000000..e29511a --- /dev/null +++ b/tools/authentik/user-create.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# +# user-create.sh — Create an Authentik user +# +# Usage: user-create.sh -u <username> -n <name> -e <email> [-p password] [-g group] +# +# Options: +# -u username Username (required) +# -n name Display name (required) +# -e email Email address (required) +# -p password Initial password (optional — user gets set-password flow if omitted) +# -g group Group name to add user to (optional) +# -f format Output format: table (default), json +# -h Show this help +# +# Environment variables (or credentials.json): +# AUTHENTIK_URL — Authentik instance URL +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials authentik + +USERNAME="" NAME="" EMAIL="" PASSWORD="" GROUP="" FORMAT="table" + +while getopts "u:n:e:p:g:f:h" opt; do + case $opt in + u) USERNAME="$OPTARG" ;; + n) NAME="$OPTARG" ;; + e) EMAIL="$OPTARG" ;; + p) PASSWORD="$OPTARG" ;; + g) GROUP="$OPTARG" ;; + f) FORMAT="$OPTARG" ;; + h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 -u <username> -n <name> -e <email> [-p password] [-g group]" >&2; exit 1 ;; + esac +done + +if [[ -z "$USERNAME" || -z "$NAME" || -z "$EMAIL" ]]; then + echo "Error: -u username, -n name, and -e email are required" >&2 + exit 1 +fi + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) + +# Build user payload +payload=$(jq -n \ + --arg username "$USERNAME" \ + --arg name "$NAME" \ + --arg email "$EMAIL" \ + '{username: $username, name: $name, email: $email, is_active: true}') + +# Add password if provided +if [[ -n "$PASSWORD" ]]; then + payload=$(echo "$payload" | jq --arg pw "$PASSWORD" '. + {password: $pw}') +fi + +# Add to group if provided +if [[ -n "$GROUP" ]]; then + # Look up group PK by name + group_response=$(curl -sk \ + -H "Authorization: Bearer $TOKEN" \ + "${AUTHENTIK_URL}/api/v3/core/groups/?search=${GROUP}") + group_pk=$(echo "$group_response" | jq -r ".results[] | select(.name == \"$GROUP\") | .pk" | head -1) + if [[ -n "$group_pk" ]]; then + payload=$(echo "$payload" | jq --arg gk "$group_pk" '. + {groups: [$gk]}') + else + echo "Warning: Group '$GROUP' not found — creating user without group" >&2 + fi +fi + +response=$(curl -sk -w "\n%{http_code}" -X POST \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "${AUTHENTIK_URL}/api/v3/core/users/") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "201" ]]; then + echo "Error: Failed to create user (HTTP $http_code)" >&2 + echo "$body" | jq -r '.' 2>/dev/null >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' +else + echo "User created successfully:" + echo "$body" | jq -r '" Username: \(.username)\n Name: \(.name)\n Email: \(.email)\n PK: \(.pk)"' +fi diff --git a/tools/authentik/user-list.sh b/tools/authentik/user-list.sh new file mode 100755 index 0000000..405e099 --- /dev/null +++ b/tools/authentik/user-list.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# +# user-list.sh — List Authentik users +# +# Usage: user-list.sh [-f format] [-s search] [-g group] +# +# Options: +# -f format Output format: table (default), json +# -s search Search term (matches username, name, email) +# -g group Filter by group name +# -h Show this help +# +# Environment variables (or credentials.json): +# AUTHENTIK_URL — Authentik instance URL +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials authentik + +FORMAT="table" +SEARCH="" +GROUP="" + +while getopts "f:s:g:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + s) SEARCH="$OPTARG" ;; + g) GROUP="$OPTARG" ;; + h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-s search] [-g group]" >&2; exit 1 ;; + esac +done + +TOKEN=$("$SCRIPT_DIR/auth-token.sh" -q) + +# Build query params +PARAMS="ordering=username" +[[ -n "$SEARCH" ]] && PARAMS="${PARAMS}&search=${SEARCH}" +[[ -n "$GROUP" ]] && PARAMS="${PARAMS}&groups_by_name=${GROUP}" + +response=$(curl -sk -w "\n%{http_code}" \ + -H "Authorization: Bearer $TOKEN" \ + "${AUTHENTIK_URL}/api/v3/core/users/?${PARAMS}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list users (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.results' + exit 0 +fi + +# Table output +echo "USERNAME NAME EMAIL ACTIVE LAST LOGIN" +echo "-------------------- ---------------------------- ---------------------------- ------ ----------" +echo "$body" | jq -r '.results[] | [ + .username, + .name, + .email, + (if .is_active then "yes" else "no" end), + (.last_login // "never" | split("T")[0]) +] | @tsv' | while IFS=$'\t' read -r username name email active last_login; do + printf "%-20s %-28s %-28s %-6s %s\n" \ + "${username:0:20}" "${name:0:28}" "${email:0:28}" "$active" "$last_login" +done diff --git a/rails/bootstrap/agent-lint.sh b/tools/bootstrap/agent-lint.sh similarity index 98% rename from rails/bootstrap/agent-lint.sh rename to tools/bootstrap/agent-lint.sh index d2947f0..c4be988 100755 --- a/rails/bootstrap/agent-lint.sh +++ b/tools/bootstrap/agent-lint.sh @@ -230,9 +230,9 @@ JSONEOF if $FIX_HINT && ! $JSON_OUTPUT; then if [[ "$has_runtime" == "MISS" || "$has_agents" == "MISS" ]]; then - echo " ${DIM}Fix: ~/.config/mosaic/rails/bootstrap/init-project.sh --name \"$name\" --type auto${NC}" + echo " ${DIM}Fix: ~/.config/mosaic/tools/bootstrap/init-project.sh --name \"$name\" --type auto${NC}" elif [[ "$has_guides" == "MISS" ]]; then - echo " ${DIM}Fix: ~/.config/mosaic/rails/bootstrap/agent-upgrade.sh $dir --section conditional-loading${NC}" + echo " ${DIM}Fix: ~/.config/mosaic/tools/bootstrap/agent-upgrade.sh $dir --section conditional-loading${NC}" fi fi diff --git a/rails/bootstrap/agent-upgrade.sh b/tools/bootstrap/agent-upgrade.sh similarity index 100% rename from rails/bootstrap/agent-upgrade.sh rename to tools/bootstrap/agent-upgrade.sh diff --git a/rails/bootstrap/init-project.sh b/tools/bootstrap/init-project.sh similarity index 99% rename from rails/bootstrap/init-project.sh rename to tools/bootstrap/init-project.sh index 5bb0cac..76a5e22 100755 --- a/rails/bootstrap/init-project.sh +++ b/tools/bootstrap/init-project.sh @@ -9,7 +9,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" TEMPLATE_DIR="$HOME/.config/mosaic/templates/agent" -GIT_SCRIPT_DIR="$HOME/.config/mosaic/rails/git" +GIT_SCRIPT_DIR="$HOME/.config/mosaic/tools/git" SEQUENTIAL_MCP_SCRIPT="$HOME/.config/mosaic/bin/mosaic-ensure-sequential-thinking" # Defaults @@ -403,7 +403,7 @@ echo "Created docs/scratchpads/, docs/reports/*, docs/tasks/, docs/releases/, do # Set up CI/CD pipeline if [[ "$SKIP_CI" != true ]]; then - CODEX_DIR="$HOME/.config/mosaic/rails/codex" + CODEX_DIR="$HOME/.config/mosaic/tools/codex" if [[ -d "$CODEX_DIR/woodpecker" ]]; then mkdir -p .woodpecker/schemas cp "$CODEX_DIR/woodpecker/codex-review.yml" .woodpecker/ @@ -416,7 +416,7 @@ fi # Generate Docker build/push/link pipeline steps if [[ "$CICD_DOCKER" == true ]]; then - CICD_SCRIPT="$HOME/.config/mosaic/rails/cicd/generate-docker-steps.sh" + CICD_SCRIPT="$HOME/.config/mosaic/tools/cicd/generate-docker-steps.sh" if [[ -x "$CICD_SCRIPT" ]]; then # Parse org and repo from git remote CICD_REGISTRY="" diff --git a/rails/bootstrap/init-repo-labels.sh b/tools/bootstrap/init-repo-labels.sh similarity index 98% rename from rails/bootstrap/init-repo-labels.sh rename to tools/bootstrap/init-repo-labels.sh index 6108ac9..36404b1 100755 --- a/rails/bootstrap/init-repo-labels.sh +++ b/tools/bootstrap/init-repo-labels.sh @@ -7,7 +7,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -GIT_SCRIPT_DIR="$HOME/.config/mosaic/rails/git" +GIT_SCRIPT_DIR="$HOME/.config/mosaic/tools/git" source "$GIT_SCRIPT_DIR/detect-platform.sh" SKIP_MILESTONE=false diff --git a/rails/cicd/generate-docker-steps.sh b/tools/cicd/generate-docker-steps.sh similarity index 100% rename from rails/cicd/generate-docker-steps.sh rename to tools/cicd/generate-docker-steps.sh diff --git a/rails/codex/README.md b/tools/codex/README.md similarity index 87% rename from rails/codex/README.md rename to tools/codex/README.md index 5d571f7..1532621 100644 --- a/rails/codex/README.md +++ b/tools/codex/README.md @@ -50,45 +50,45 @@ Security vulnerability review focusing on: ```bash # Code review -~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted +~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted # Security review -~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted +~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted ``` ### Review a Pull Request ```bash # Review and post findings as a PR comment -~/.config/mosaic/rails/codex/codex-code-review.sh -n 42 +~/.config/mosaic/tools/codex/codex-code-review.sh -n 42 # Security review and post to PR -~/.config/mosaic/rails/codex/codex-security-review.sh -n 42 +~/.config/mosaic/tools/codex/codex-security-review.sh -n 42 ``` ### Review Against Base Branch ```bash # Code review changes vs main -~/.config/mosaic/rails/codex/codex-code-review.sh -b main +~/.config/mosaic/tools/codex/codex-code-review.sh -b main # Security review changes vs develop -~/.config/mosaic/rails/codex/codex-security-review.sh -b develop +~/.config/mosaic/tools/codex/codex-security-review.sh -b develop ``` ### Review a Specific Commit ```bash -~/.config/mosaic/rails/codex/codex-code-review.sh -c abc123f -~/.config/mosaic/rails/codex/codex-security-review.sh -c abc123f +~/.config/mosaic/tools/codex/codex-code-review.sh -c abc123f +~/.config/mosaic/tools/codex/codex-security-review.sh -c abc123f ``` ### Save Results to File ```bash # Save JSON output -~/.config/mosaic/rails/codex/codex-code-review.sh --uncommitted -o review-results.json -~/.config/mosaic/rails/codex/codex-security-review.sh --uncommitted -o security-results.json +~/.config/mosaic/tools/codex/codex-code-review.sh --uncommitted -o review-results.json +~/.config/mosaic/tools/codex/codex-security-review.sh --uncommitted -o security-results.json ``` ## Options @@ -113,12 +113,12 @@ Automated PR reviews in CI pipelines. 1. **Copy the pipeline template to your repo:** ```bash - cp ~/.config/mosaic/rails/codex/woodpecker/codex-review.yml your-repo/.woodpecker/ + cp ~/.config/mosaic/tools/codex/woodpecker/codex-review.yml your-repo/.woodpecker/ ``` 2. **Copy the schemas directory:** ```bash - cp -r ~/.config/mosaic/rails/codex/schemas your-repo/.woodpecker/ + cp -r ~/.config/mosaic/tools/codex/schemas your-repo/.woodpecker/ ``` 3. **Add Codex API key to Woodpecker:** @@ -203,7 +203,7 @@ Automated PR reviews in CI pipelines. ## Platform Support -Works with both **GitHub** and **Gitea** via the shared `~/.config/mosaic/rails/git/` infrastructure: +Works with both **GitHub** and **Gitea** via the shared `~/.config/mosaic/tools/git/` infrastructure: - Auto-detects platform from git remote - Posts PR comments using `gh` (GitHub) or `tea` (Gitea) - Unified interface across both platforms @@ -261,5 +261,5 @@ For best results, use `gpt-5.2-codex` or newer for strongest review accuracy. ## See Also - `~/.config/mosaic/guides/CODE-REVIEW.md` — Manual code review checklist -- `~/.config/mosaic/rails/git/` — Git helper scripts (issue/PR management) +- `~/.config/mosaic/tools/git/` — Git helper scripts (issue/PR management) - OpenAI Codex CLI docs: https://developers.openai.com/codex/cli/ diff --git a/rails/codex/codex-code-review.sh b/tools/codex/codex-code-review.sh similarity index 100% rename from rails/codex/codex-code-review.sh rename to tools/codex/codex-code-review.sh diff --git a/rails/codex/codex-security-review.sh b/tools/codex/codex-security-review.sh similarity index 100% rename from rails/codex/codex-security-review.sh rename to tools/codex/codex-security-review.sh diff --git a/rails/codex/common.sh b/tools/codex/common.sh similarity index 100% rename from rails/codex/common.sh rename to tools/codex/common.sh diff --git a/rails/codex/schemas/code-review-schema.json b/tools/codex/schemas/code-review-schema.json similarity index 100% rename from rails/codex/schemas/code-review-schema.json rename to tools/codex/schemas/code-review-schema.json diff --git a/rails/codex/schemas/security-review-schema.json b/tools/codex/schemas/security-review-schema.json similarity index 100% rename from rails/codex/schemas/security-review-schema.json rename to tools/codex/schemas/security-review-schema.json diff --git a/rails/codex/woodpecker/codex-review.yml b/tools/codex/woodpecker/codex-review.yml similarity index 100% rename from rails/codex/woodpecker/codex-review.yml rename to tools/codex/woodpecker/codex-review.yml diff --git a/tools/context/mosaic-context-loader.sh b/tools/context/mosaic-context-loader.sh new file mode 100755 index 0000000..036a5e8 --- /dev/null +++ b/tools/context/mosaic-context-loader.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# mosaic-context-loader.sh — SessionStart hook for Claude Code +# Injects mandatory Mosaic config files into agent context at session init. +# Stdout from this script is added to Claude's context before processing. +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" + +# Mandatory load order (per AGENTS.md contract) +MANDATORY_FILES=( + "$MOSAIC_HOME/SOUL.md" + "$MOSAIC_HOME/USER.md" + "$MOSAIC_HOME/STANDARDS.md" + "$MOSAIC_HOME/AGENTS.md" + "$MOSAIC_HOME/TOOLS.md" +) + +# E2E delivery guide (case-insensitive lookup) +E2E_DELIVERY="" +for candidate in \ + "$MOSAIC_HOME/guides/E2E-DELIVERY.md" \ + "$MOSAIC_HOME/guides/e2e-delivery.md"; do + if [[ -f "$candidate" ]]; then + E2E_DELIVERY="$candidate" + break + fi +done + +# Runtime-specific reference +RUNTIME_FILE="$MOSAIC_HOME/runtime/claude/RUNTIME.md" + +# Project-local AGENTS.md (cwd at session start) +PROJECT_AGENTS="" +if [[ -f "./AGENTS.md" ]]; then + PROJECT_AGENTS="./AGENTS.md" +fi + +emit_file() { + local filepath="$1" + local label="${2:-$(basename "$filepath")}" + if [[ -f "$filepath" ]]; then + echo "=== MOSAIC: $label ===" + cat "$filepath" + echo "" + fi +} + +echo "=== MOSAIC CONTEXT INJECTION (SessionStart) ===" +echo "" + +for f in "${MANDATORY_FILES[@]}"; do + emit_file "$f" +done + +if [[ -n "$E2E_DELIVERY" ]]; then + emit_file "$E2E_DELIVERY" "E2E-DELIVERY.md" +fi + +if [[ -n "$PROJECT_AGENTS" ]]; then + emit_file "$PROJECT_AGENTS" "Project AGENTS.md ($(pwd))" +fi + +emit_file "$RUNTIME_FILE" "Claude RUNTIME.md" + +echo "=== END MOSAIC CONTEXT INJECTION ===" diff --git a/tools/coolify/README.md b/tools/coolify/README.md new file mode 100644 index 0000000..82a68cd --- /dev/null +++ b/tools/coolify/README.md @@ -0,0 +1,65 @@ +# Coolify Tool Suite + +Manage Coolify container deployment platform (projects, services, deployments, environment variables). + +## Prerequisites + +- `jq` and `curl` installed +- Coolify credentials in `~/src/jarvis-brain/credentials.json` (or `$MOSAIC_CREDENTIALS_FILE`) +- Required fields: `coolify.url`, `coolify.app_token` + +## Scripts + +| Script | Purpose | +|--------|---------| +| `team-list.sh` | List teams | +| `project-list.sh` | List projects | +| `service-list.sh` | List all services | +| `service-status.sh` | Get service details and status | +| `deploy.sh` | Trigger service deployment | +| `env-set.sh` | Set environment variable on a service | + +## Common Options + +- `-f json` — JSON output (default: table) +- `-u uuid` — Service UUID (for service-specific operations) +- `-h` — Show help + +## API Reference + +- Base URL: `http://10.1.1.44:8000` +- API prefix: `/api/v1/` +- Auth: Bearer token in `Authorization` header +- Rate limit: 200 requests per interval + +## Known Limitations + +- **FQDN updates on compose sub-apps not supported via API.** Workaround: update directly in Coolify's PostgreSQL DB (`coolify-db` container, `service_applications` table). +- **Compose must be base64-encoded** in `docker_compose_raw` field when creating services via API. +- **Don't send `type` with `docker_compose_raw`** — API rejects payloads with both fields. + +## Coolify Magic Variables + +Coolify reads special env vars from compose files: +- `SERVICE_FQDN_{NAME}_{PORT}` — assigns a domain to a compose service +- `SERVICE_URL_{NAME}_{PORT}` — internal URL reference +- Must use list-style env syntax (`- SERVICE_FQDN_API_3001`), NOT dict-style. + +## Examples + +```bash +# List all projects +~/.config/mosaic/tools/coolify/project-list.sh + +# List services as JSON +~/.config/mosaic/tools/coolify/service-list.sh -f json + +# Check service status +~/.config/mosaic/tools/coolify/service-status.sh -u <uuid> + +# Set an env var +~/.config/mosaic/tools/coolify/env-set.sh -u <uuid> -k DATABASE_URL -v "postgres://..." + +# Deploy a service +~/.config/mosaic/tools/coolify/deploy.sh -u <uuid> +``` diff --git a/tools/coolify/deploy.sh b/tools/coolify/deploy.sh new file mode 100755 index 0000000..1563f43 --- /dev/null +++ b/tools/coolify/deploy.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# deploy.sh — Trigger Coolify service deployment +# +# Usage: deploy.sh -u <uuid> [-f] +# +# Options: +# -u uuid Service UUID (required) +# -f Force restart (stop then start) +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials coolify + +UUID="" +FORCE=false + +while getopts "u:fh" opt; do + case $opt in + u) UUID="$OPTARG" ;; + f) FORCE=true ;; + h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 -u <uuid> [-f]" >&2; exit 1 ;; + esac +done + +if [[ -z "$UUID" ]]; then + echo "Error: -u uuid is required" >&2 + exit 1 +fi + +if [[ "$FORCE" == "true" ]]; then + echo "Stopping service $UUID..." + curl -s -o /dev/null -w "" \ + -X POST \ + -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H "Content-Type: application/json" \ + "${COOLIFY_URL}/api/v1/services/${UUID}/stop" + sleep 2 +fi + +echo "Starting service $UUID..." +response=$(curl -s -w "\n%{http_code}" \ + -X POST \ + -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H "Content-Type: application/json" \ + "${COOLIFY_URL}/api/v1/services/${UUID}/start") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" && "$http_code" != "201" && "$http_code" != "202" ]]; then + echo "Error: Deployment failed (HTTP $http_code)" >&2 + echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2 + exit 1 +fi + +echo "Deployment triggered successfully for service $UUID" +echo "$body" | jq -r '.message // empty' 2>/dev/null || true diff --git a/tools/coolify/env-set.sh b/tools/coolify/env-set.sh new file mode 100755 index 0000000..fd1b3cb --- /dev/null +++ b/tools/coolify/env-set.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# +# env-set.sh — Set environment variable on a Coolify service +# +# Usage: env-set.sh -u <uuid> -k <key> -v <value> [--preview] +# +# Options: +# -u uuid Service UUID (required) +# -k key Environment variable name (required) +# -v value Environment variable value (required) +# --preview Set as preview-only variable +# -h Show this help +# +# Note: Changes take effect on next deploy/restart. +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials coolify + +UUID="" +KEY="" +VALUE="" +IS_PREVIEW="false" + +while [[ $# -gt 0 ]]; do + case $1 in + -u) UUID="$2"; shift 2 ;; + -k) KEY="$2"; shift 2 ;; + -v) VALUE="$2"; shift 2 ;; + --preview) IS_PREVIEW="true"; shift ;; + -h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 -u <uuid> -k <key> -v <value> [--preview]" >&2; exit 1 ;; + esac +done + +if [[ -z "$UUID" || -z "$KEY" || -z "$VALUE" ]]; then + echo "Error: -u uuid, -k key, and -v value are required" >&2 + exit 1 +fi + +payload=$(jq -n \ + --arg key "$KEY" \ + --arg value "$VALUE" \ + --argjson preview "$IS_PREVIEW" \ + '{key: $key, value: $value, is_preview: $preview}') + +response=$(curl -s -w "\n%{http_code}" \ + -X PATCH \ + -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "${COOLIFY_URL}/api/v1/services/${UUID}/envs") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" && "$http_code" != "201" ]]; then + echo "Error: Failed to set environment variable (HTTP $http_code)" >&2 + echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2 + exit 1 +fi + +echo "Set $KEY on service $UUID" +echo "Note: Redeploy the service to apply the change" diff --git a/tools/coolify/project-list.sh b/tools/coolify/project-list.sh new file mode 100755 index 0000000..3faf58f --- /dev/null +++ b/tools/coolify/project-list.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# project-list.sh — List Coolify projects +# +# Usage: project-list.sh [-f format] +# +# Options: +# -f format Output format: table (default), json +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials coolify + +FORMAT="table" + +while getopts "f:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format]" >&2; exit 1 ;; + esac +done + +response=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H "Content-Type: application/json" \ + "${COOLIFY_URL}/api/v1/projects") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list projects (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "UUID NAME DESCRIPTION" +echo "------------------------------------ ---------------------------- ----------------------------------------" +echo "$body" | jq -r '.[] | [ + .uuid, + .name, + (.description // "—") +] | @tsv' | while IFS=$'\t' read -r uuid name desc; do + printf "%-36s %-28s %s\n" "$uuid" "${name:0:28}" "${desc:0:40}" +done diff --git a/tools/coolify/service-list.sh b/tools/coolify/service-list.sh new file mode 100755 index 0000000..91008a0 --- /dev/null +++ b/tools/coolify/service-list.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# +# service-list.sh — List Coolify services +# +# Usage: service-list.sh [-f format] +# +# Options: +# -f format Output format: table (default), json +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials coolify + +FORMAT="table" + +while getopts "f:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format]" >&2; exit 1 ;; + esac +done + +response=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H "Content-Type: application/json" \ + "${COOLIFY_URL}/api/v1/services") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list services (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "UUID NAME TYPE STATUS" +echo "------------------------------------ ---------------------------- ------------ ----------" +echo "$body" | jq -r '.[] | [ + .uuid, + .name, + (.type // "unknown"), + (.status // "unknown") +] | @tsv' | while IFS=$'\t' read -r uuid name type status; do + printf "%-36s %-28s %-12s %s\n" "$uuid" "${name:0:28}" "${type:0:12}" "$status" +done diff --git a/tools/coolify/service-status.sh b/tools/coolify/service-status.sh new file mode 100755 index 0000000..fc5e171 --- /dev/null +++ b/tools/coolify/service-status.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# service-status.sh — Get Coolify service status and details +# +# Usage: service-status.sh -u <uuid> [-f format] +# +# Options: +# -u uuid Service UUID (required) +# -f format Output format: table (default), json +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials coolify + +UUID="" +FORMAT="table" + +while getopts "u:f:h" opt; do + case $opt in + u) UUID="$OPTARG" ;; + f) FORMAT="$OPTARG" ;; + h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 -u <uuid> [-f format]" >&2; exit 1 ;; + esac +done + +if [[ -z "$UUID" ]]; then + echo "Error: -u uuid is required" >&2 + exit 1 +fi + +response=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H "Content-Type: application/json" \ + "${COOLIFY_URL}/api/v1/services/${UUID}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to get service status (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "Service Details" +echo "===============" +echo "$body" | jq -r ' + " UUID: \(.uuid)\n" + + " Name: \(.name)\n" + + " Type: \(.type // "unknown")\n" + + " Status: \(.status // "unknown")\n" + + " FQDN: \(.fqdn // "none")\n" + + " Created: \(.created_at // "unknown")\n" + + " Updated: \(.updated_at // "unknown")" +' diff --git a/tools/coolify/team-list.sh b/tools/coolify/team-list.sh new file mode 100755 index 0000000..d459680 --- /dev/null +++ b/tools/coolify/team-list.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# team-list.sh — List Coolify teams +# +# Usage: team-list.sh [-f format] +# +# Options: +# -f format Output format: table (default), json +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials coolify + +FORMAT="table" + +while getopts "f:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + h) head -10 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format]" >&2; exit 1 ;; + esac +done + +response=$(curl -s -w "\n%{http_code}" \ + -H "Authorization: Bearer $COOLIFY_TOKEN" \ + -H "Content-Type: application/json" \ + "${COOLIFY_URL}/api/v1/teams") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list teams (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "ID NAME DESCRIPTION" +echo "---- ---------------------------- ----------------------------------------" +echo "$body" | jq -r '.[] | [ + (.id | tostring), + .name, + (.description // "—") +] | @tsv' | while IFS=$'\t' read -r id name desc; do + printf "%-4s %-28s %s\n" "$id" "${name:0:28}" "${desc:0:40}" +done diff --git a/rails/git/ci-queue-wait.ps1 b/tools/git/ci-queue-wait.ps1 similarity index 100% rename from rails/git/ci-queue-wait.ps1 rename to tools/git/ci-queue-wait.ps1 diff --git a/rails/git/ci-queue-wait.sh b/tools/git/ci-queue-wait.sh similarity index 100% rename from rails/git/ci-queue-wait.sh rename to tools/git/ci-queue-wait.sh diff --git a/rails/git/detect-platform.ps1 b/tools/git/detect-platform.ps1 similarity index 100% rename from rails/git/detect-platform.ps1 rename to tools/git/detect-platform.ps1 diff --git a/rails/git/detect-platform.sh b/tools/git/detect-platform.sh similarity index 100% rename from rails/git/detect-platform.sh rename to tools/git/detect-platform.sh diff --git a/rails/git/issue-assign.ps1 b/tools/git/issue-assign.ps1 similarity index 100% rename from rails/git/issue-assign.ps1 rename to tools/git/issue-assign.ps1 diff --git a/rails/git/issue-assign.sh b/tools/git/issue-assign.sh similarity index 100% rename from rails/git/issue-assign.sh rename to tools/git/issue-assign.sh diff --git a/rails/git/issue-close.sh b/tools/git/issue-close.sh similarity index 100% rename from rails/git/issue-close.sh rename to tools/git/issue-close.sh diff --git a/rails/git/issue-comment.sh b/tools/git/issue-comment.sh similarity index 100% rename from rails/git/issue-comment.sh rename to tools/git/issue-comment.sh diff --git a/rails/git/issue-create.ps1 b/tools/git/issue-create.ps1 similarity index 100% rename from rails/git/issue-create.ps1 rename to tools/git/issue-create.ps1 diff --git a/rails/git/issue-create.sh b/tools/git/issue-create.sh similarity index 100% rename from rails/git/issue-create.sh rename to tools/git/issue-create.sh diff --git a/rails/git/issue-edit.sh b/tools/git/issue-edit.sh similarity index 100% rename from rails/git/issue-edit.sh rename to tools/git/issue-edit.sh diff --git a/rails/git/issue-list.ps1 b/tools/git/issue-list.ps1 similarity index 100% rename from rails/git/issue-list.ps1 rename to tools/git/issue-list.ps1 diff --git a/rails/git/issue-list.sh b/tools/git/issue-list.sh similarity index 100% rename from rails/git/issue-list.sh rename to tools/git/issue-list.sh diff --git a/rails/git/issue-reopen.sh b/tools/git/issue-reopen.sh similarity index 100% rename from rails/git/issue-reopen.sh rename to tools/git/issue-reopen.sh diff --git a/rails/git/issue-view.sh b/tools/git/issue-view.sh similarity index 100% rename from rails/git/issue-view.sh rename to tools/git/issue-view.sh diff --git a/rails/git/milestone-close.sh b/tools/git/milestone-close.sh similarity index 100% rename from rails/git/milestone-close.sh rename to tools/git/milestone-close.sh diff --git a/rails/git/milestone-create.ps1 b/tools/git/milestone-create.ps1 similarity index 100% rename from rails/git/milestone-create.ps1 rename to tools/git/milestone-create.ps1 diff --git a/rails/git/milestone-create.sh b/tools/git/milestone-create.sh similarity index 100% rename from rails/git/milestone-create.sh rename to tools/git/milestone-create.sh diff --git a/rails/git/milestone-list.sh b/tools/git/milestone-list.sh similarity index 100% rename from rails/git/milestone-list.sh rename to tools/git/milestone-list.sh diff --git a/rails/git/pr-ci-wait.sh b/tools/git/pr-ci-wait.sh similarity index 100% rename from rails/git/pr-ci-wait.sh rename to tools/git/pr-ci-wait.sh diff --git a/rails/git/pr-close.sh b/tools/git/pr-close.sh similarity index 100% rename from rails/git/pr-close.sh rename to tools/git/pr-close.sh diff --git a/rails/git/pr-create.ps1 b/tools/git/pr-create.ps1 similarity index 100% rename from rails/git/pr-create.ps1 rename to tools/git/pr-create.ps1 diff --git a/rails/git/pr-create.sh b/tools/git/pr-create.sh similarity index 100% rename from rails/git/pr-create.sh rename to tools/git/pr-create.sh diff --git a/rails/git/pr-diff.sh b/tools/git/pr-diff.sh similarity index 100% rename from rails/git/pr-diff.sh rename to tools/git/pr-diff.sh diff --git a/rails/git/pr-list.ps1 b/tools/git/pr-list.ps1 similarity index 100% rename from rails/git/pr-list.ps1 rename to tools/git/pr-list.ps1 diff --git a/rails/git/pr-list.sh b/tools/git/pr-list.sh similarity index 100% rename from rails/git/pr-list.sh rename to tools/git/pr-list.sh diff --git a/rails/git/pr-merge.ps1 b/tools/git/pr-merge.ps1 similarity index 100% rename from rails/git/pr-merge.ps1 rename to tools/git/pr-merge.ps1 diff --git a/rails/git/pr-merge.sh b/tools/git/pr-merge.sh similarity index 100% rename from rails/git/pr-merge.sh rename to tools/git/pr-merge.sh diff --git a/rails/git/pr-metadata.sh b/tools/git/pr-metadata.sh similarity index 100% rename from rails/git/pr-metadata.sh rename to tools/git/pr-metadata.sh diff --git a/rails/git/pr-review.sh b/tools/git/pr-review.sh similarity index 100% rename from rails/git/pr-review.sh rename to tools/git/pr-review.sh diff --git a/rails/git/pr-view.sh b/tools/git/pr-view.sh similarity index 100% rename from rails/git/pr-view.sh rename to tools/git/pr-view.sh diff --git a/tools/glpi/README.md b/tools/glpi/README.md new file mode 100644 index 0000000..b97cbc0 --- /dev/null +++ b/tools/glpi/README.md @@ -0,0 +1,55 @@ +# GLPI Tool Suite + +Manage GLPI IT service management (tickets, computers/assets, users). + +## Prerequisites + +- `jq` and `curl` installed +- GLPI credentials in `~/src/jarvis-brain/credentials.json` (or `$MOSAIC_CREDENTIALS_FILE`) +- Required fields: `glpi.url`, `glpi.app_token`, `glpi.user_token` + +## Authentication + +GLPI uses a two-step auth flow: +1. `session-init.sh` exchanges app_token + user_token for a session_token +2. All subsequent calls use the session_token + app_token + +The session token is cached at `~/.cache/mosaic/glpi-session` and auto-refreshed when expired. + +## Scripts + +| Script | Purpose | +|--------|---------| +| `session-init.sh` | Initialize and cache API session | +| `computer-list.sh` | List computers/IT assets | +| `ticket-list.sh` | List tickets (filter by status) | +| `ticket-create.sh` | Create a new ticket | +| `user-list.sh` | List users | + +## Common Options + +- `-f json` — JSON output (default: table) +- `-l limit` — Result count (default: 50) +- `-h` — Show help + +## API Reference + +- Base URL: `https://help.uscllc.com/apirest.php` +- Auth headers: `App-Token` + `Session-Token` +- Pattern: RESTful item-based (`/ItemType/{id}`) + +## Examples + +```bash +# List all tickets +~/.config/mosaic/tools/glpi/ticket-list.sh + +# List only open tickets +~/.config/mosaic/tools/glpi/ticket-list.sh -s new + +# Create a ticket +~/.config/mosaic/tools/glpi/ticket-create.sh -t "Server down" -c "Web server unresponsive" -p 4 + +# List computers as JSON +~/.config/mosaic/tools/glpi/computer-list.sh -f json +``` diff --git a/tools/glpi/computer-list.sh b/tools/glpi/computer-list.sh new file mode 100755 index 0000000..9532531 --- /dev/null +++ b/tools/glpi/computer-list.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# computer-list.sh — List GLPI computers/assets +# +# Usage: computer-list.sh [-f format] [-l limit] +# +# Options: +# -f format Output format: table (default), json +# -l limit Number of results (default: 50) +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials glpi + +FORMAT="table" +LIMIT=50 + +while getopts "f:l:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + l) LIMIT="$OPTARG" ;; + h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-l limit]" >&2; exit 1 ;; + esac +done + +SESSION_TOKEN=$("$SCRIPT_DIR/session-init.sh" -q) + +response=$(curl -sk -w "\n%{http_code}" \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $SESSION_TOKEN" \ + "${GLPI_URL}/Computer?range=0-${LIMIT}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list computers (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "ID NAME SERIAL STATUS" +echo "------ ---------------------------- ------------------ ----------" +echo "$body" | jq -r '.[] | [ + (.id | tostring), + .name, + (.serial // "—"), + (.states_id | tostring) +] | @tsv' | while IFS=$'\t' read -r id name serial states_id; do + printf "%-6s %-28s %-18s %s\n" "$id" "${name:0:28}" "${serial:0:18}" "$states_id" +done diff --git a/tools/glpi/session-init.sh b/tools/glpi/session-init.sh new file mode 100755 index 0000000..e98dd08 --- /dev/null +++ b/tools/glpi/session-init.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# session-init.sh — Initialize GLPI API session +# +# Usage: session-init.sh [-f] [-q] +# +# Authenticates with GLPI and caches the session token at +# ~/.cache/mosaic/glpi-session. +# +# Options: +# -f Force re-authentication (ignore cached session) +# -q Quiet mode — only output the session token +# -h Show this help +# +# Environment variables (or credentials.json): +# GLPI_URL — GLPI API base URL +# GLPI_APP_TOKEN — GLPI application token +# GLPI_USER_TOKEN — GLPI user token +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials glpi + +CACHE_DIR="$HOME/.cache/mosaic" +CACHE_FILE="$CACHE_DIR/glpi-session" +FORCE=false +QUIET=false + +while getopts "fqh" opt; do + case $opt in + f) FORCE=true ;; + q) QUIET=true ;; + h) head -18 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f] [-q]" >&2; exit 1 ;; + esac +done + +# Check cached session validity +if [[ "$FORCE" == "false" ]] && [[ -f "$CACHE_FILE" ]]; then + cached_token=$(cat "$CACHE_FILE") + if [[ -n "$cached_token" ]]; then + # Validate with a lightweight call + http_code=$(curl -sk -o /dev/null -w "%{http_code}" \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $cached_token" \ + "${GLPI_URL}/getMyEntities") + if [[ "$http_code" == "200" ]]; then + [[ "$QUIET" == "false" ]] && echo "Using cached session (valid)" >&2 + echo "$cached_token" + exit 0 + fi + [[ "$QUIET" == "false" ]] && echo "Cached session expired, re-authenticating..." >&2 + fi +fi + +# Initialize session +response=$(curl -sk -w "\n%{http_code}" \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Authorization: user_token $GLPI_USER_TOKEN" \ + "${GLPI_URL}/initSession") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to initialize GLPI session (HTTP $http_code)" >&2 + echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2 + exit 1 +fi + +session_token=$(echo "$body" | jq -r '.session_token // empty') + +if [[ -z "$session_token" ]]; then + echo "Error: No session_token in response" >&2 + exit 1 +fi + +# Cache the session +mkdir -p "$CACHE_DIR" +echo "$session_token" > "$CACHE_FILE" +chmod 600 "$CACHE_FILE" + +[[ "$QUIET" == "false" ]] && echo "Session initialized and cached" >&2 +echo "$session_token" diff --git a/tools/glpi/ticket-create.sh b/tools/glpi/ticket-create.sh new file mode 100755 index 0000000..7a10e7c --- /dev/null +++ b/tools/glpi/ticket-create.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# +# ticket-create.sh — Create a GLPI ticket +# +# Usage: ticket-create.sh -t <title> -c <content> [-p priority] [-y type] +# +# Options: +# -t title Ticket title (required) +# -c content Ticket description (required) +# -p priority 1=VeryLow, 2=Low, 3=Medium (default), 4=High, 5=VeryHigh, 6=Major +# -y type 1=Incident (default), 2=Request +# -f format Output format: table (default), json +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials glpi + +TITLE="" +CONTENT="" +PRIORITY=3 +TYPE=1 +FORMAT="table" + +while getopts "t:c:p:y:f:h" opt; do + case $opt in + t) TITLE="$OPTARG" ;; + c) CONTENT="$OPTARG" ;; + p) PRIORITY="$OPTARG" ;; + y) TYPE="$OPTARG" ;; + f) FORMAT="$OPTARG" ;; + h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 -t <title> -c <content> [-p priority] [-y type]" >&2; exit 1 ;; + esac +done + +if [[ -z "$TITLE" || -z "$CONTENT" ]]; then + echo "Error: -t title and -c content are required" >&2 + exit 1 +fi + +SESSION_TOKEN=$("$SCRIPT_DIR/session-init.sh" -q) + +payload=$(jq -n \ + --arg name "$TITLE" \ + --arg content "$CONTENT" \ + --argjson priority "$PRIORITY" \ + --argjson type "$TYPE" \ + '{input: {name: $name, content: $content, priority: $priority, type: $type}}') + +response=$(curl -sk -w "\n%{http_code}" -X POST \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $SESSION_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$payload" \ + "${GLPI_URL}/Ticket") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "201" && "$http_code" != "200" ]]; then + echo "Error: Failed to create ticket (HTTP $http_code)" >&2 + echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' +else + ticket_id=$(echo "$body" | jq -r '.id // .message // .') + echo "Ticket created: #$ticket_id" + echo " Title: $TITLE" + echo " Priority: $PRIORITY" + echo " Type: $([ "$TYPE" = "1" ] && echo "Incident" || echo "Request")" +fi diff --git a/tools/glpi/ticket-list.sh b/tools/glpi/ticket-list.sh new file mode 100755 index 0000000..7538f5d --- /dev/null +++ b/tools/glpi/ticket-list.sh @@ -0,0 +1,88 @@ +#!/usr/bin/env bash +# +# ticket-list.sh — List GLPI tickets +# +# Usage: ticket-list.sh [-f format] [-l limit] [-s status] +# +# Options: +# -f format Output format: table (default), json +# -l limit Number of results (default: 50) +# -s status Filter: new, processing, pending, solved, closed +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials glpi + +FORMAT="table" +LIMIT=50 +STATUS="" + +while getopts "f:l:s:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + l) LIMIT="$OPTARG" ;; + s) STATUS="$OPTARG" ;; + h) head -13 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-l limit] [-s status]" >&2; exit 1 ;; + esac +done + +SESSION_TOKEN=$("$SCRIPT_DIR/session-init.sh" -q) + +ENDPOINT="${GLPI_URL}/Ticket?range=0-${LIMIT}&order=DESC&sort=date_mod" + +# Map status names to GLPI status IDs +if [[ -n "$STATUS" ]]; then + case "$STATUS" in + new) STATUS_ID=1 ;; + processing|assigned) STATUS_ID=2 ;; + pending|planned) STATUS_ID=3 ;; + solved) STATUS_ID=5 ;; + closed) STATUS_ID=6 ;; + *) echo "Error: Unknown status '$STATUS'. Use: new, processing, pending, solved, closed" >&2; exit 1 ;; + esac + ENDPOINT="${ENDPOINT}&searchText[status]=${STATUS_ID}" +fi + +response=$(curl -sk -w "\n%{http_code}" \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $SESSION_TOKEN" \ + "$ENDPOINT") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list tickets (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "ID PRIORITY STATUS TITLE DATE" +echo "------ -------- ------ ---------------------------------------- ----------" +echo "$body" | jq -r '.[] | [ + (.id | tostring), + (.priority | tostring), + (.status | tostring), + .name, + (.date_mod | split(" ")[0]) +] | @tsv' | while IFS=$'\t' read -r id priority status name date; do + # Map priority numbers + case "$priority" in + 1) pri="VLow" ;; 2) pri="Low" ;; 3) pri="Med" ;; + 4) pri="High" ;; 5) pri="VHigh" ;; 6) pri="Major" ;; *) pri="$priority" ;; + esac + # Map status numbers + case "$status" in + 1) stat="New" ;; 2) stat="Proc" ;; 3) stat="Pend" ;; + 4) stat="Plan" ;; 5) stat="Solv" ;; 6) stat="Clos" ;; *) stat="$status" ;; + esac + printf "%-6s %-8s %-6s %-40s %s\n" "$id" "$pri" "$stat" "${name:0:40}" "$date" +done diff --git a/tools/glpi/user-list.sh b/tools/glpi/user-list.sh new file mode 100755 index 0000000..b62b7cb --- /dev/null +++ b/tools/glpi/user-list.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# user-list.sh — List GLPI users +# +# Usage: user-list.sh [-f format] [-l limit] +# +# Options: +# -f format Output format: table (default), json +# -l limit Number of results (default: 50) +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" +load_credentials glpi + +FORMAT="table" +LIMIT=50 + +while getopts "f:l:h" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + l) LIMIT="$OPTARG" ;; + h) head -11 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-l limit]" >&2; exit 1 ;; + esac +done + +SESSION_TOKEN=$("$SCRIPT_DIR/session-init.sh" -q) + +response=$(curl -sk -w "\n%{http_code}" \ + -H "App-Token: $GLPI_APP_TOKEN" \ + -H "Session-Token: $SESSION_TOKEN" \ + "${GLPI_URL}/User?range=0-${LIMIT}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list users (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "ID USERNAME REALNAME FIRSTNAME ACTIVE" +echo "------ -------------------- -------------------- -------------------- ------" +echo "$body" | jq -r '.[] | [ + (.id | tostring), + (.name // "—"), + (.realname // "—"), + (.firstname // "—"), + (if .is_active == 1 then "yes" else "no" end) +] | @tsv' | while IFS=$'\t' read -r id name realname firstname active; do + printf "%-6s %-20s %-20s %-20s %s\n" \ + "$id" "${name:0:20}" "${realname:0:20}" "${firstname:0:20}" "$active" +done diff --git a/tools/health/stack-health.sh b/tools/health/stack-health.sh new file mode 100755 index 0000000..2175ce1 --- /dev/null +++ b/tools/health/stack-health.sh @@ -0,0 +1,194 @@ +#!/usr/bin/env bash +# +# stack-health.sh — Check health of all configured Mosaic stack services +# +# Usage: stack-health.sh [-f format] [-s service] [-q] +# +# Checks connectivity to all services configured in credentials.json. +# For each service, makes a lightweight API call and reports status. +# +# Options: +# -f format Output format: table (default), json +# -s service Check only a specific service +# -q Quiet — exit code only (0=all healthy, 1=any unhealthy) +# -h Show this help +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" + +FORMAT="table" +SINGLE_SERVICE="" +QUIET=false +CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" + +while getopts "f:s:qh" opt; do + case $opt in + f) FORMAT="$OPTARG" ;; + s) SINGLE_SERVICE="$OPTARG" ;; + q) QUIET=true ;; + h) head -15 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-f format] [-s service] [-q]" >&2; exit 1 ;; + esac +done + +if [[ ! -f "$CRED_FILE" ]]; then + echo "Error: Credentials file not found: $CRED_FILE" >&2 + exit 1 +fi + +# Colors (disabled if not a terminal or quiet mode) +if [[ -t 1 ]] && [[ "$QUIET" == "false" ]]; then + GREEN='\033[0;32m' RED='\033[0;31m' YELLOW='\033[0;33m' RESET='\033[0m' +else + GREEN='' RED='' YELLOW='' RESET='' +fi + +TOTAL=0 +HEALTHY=0 +RESULTS="[]" + +check_service() { + local name="$1" + local display_name="$2" + local url="$3" + local endpoint="$4" + local auth_header="$5" + local insecure="${6:-false}" + + TOTAL=$((TOTAL + 1)) + + local curl_args=(-s -o /dev/null -w "%{http_code} %{time_total}" --connect-timeout 5 --max-time 10) + [[ -n "$auth_header" ]] && curl_args+=(-H "$auth_header") + [[ "$insecure" == "true" ]] && curl_args+=(-k) + + local result + result=$(curl "${curl_args[@]}" "${url}${endpoint}" 2>/dev/null) || result="000 0.000" + + local http_code response_time status_text + http_code=$(echo "$result" | awk '{print $1}') + response_time=$(echo "$result" | awk '{print $2}') + + if [[ "$http_code" -ge 200 && "$http_code" -lt 400 ]]; then + status_text="UP" + HEALTHY=$((HEALTHY + 1)) + elif [[ "$http_code" == "000" ]]; then + status_text="DOWN" + elif [[ "$http_code" == "401" || "$http_code" == "403" ]]; then + # Auth error but service is reachable + status_text="AUTH_ERR" + HEALTHY=$((HEALTHY + 1)) # Service is up, just auth issue + else + status_text="ERROR" + fi + + # Append to JSON results + RESULTS=$(echo "$RESULTS" | jq --arg n "$name" --arg d "$display_name" \ + --arg u "$url" --arg s "$status_text" --arg c "$http_code" --arg t "$response_time" \ + '. + [{name: $n, display_name: $d, url: $u, status: $s, http_code: ($c | tonumber), response_time: $t}]') + + if [[ "$QUIET" == "false" && "$FORMAT" == "table" ]]; then + local color="$GREEN" + [[ "$status_text" == "DOWN" || "$status_text" == "ERROR" ]] && color="$RED" + [[ "$status_text" == "AUTH_ERR" ]] && color="$YELLOW" + printf " %-22s %-35s ${color}%-8s${RESET} %ss\n" \ + "$display_name" "$url" "$status_text" "$response_time" + fi +} + +# Discover and check services +[[ "$QUIET" == "false" && "$FORMAT" == "table" ]] && { + echo "" + echo " SERVICE URL STATUS RESPONSE" + echo " ---------------------- ----------------------------------- -------- --------" +} + +# Portainer +if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "portainer" ]]; then + portainer_url=$(jq -r '.portainer.url // empty' "$CRED_FILE") + portainer_key=$(jq -r '.portainer.api_key // empty' "$CRED_FILE") + if [[ -n "$portainer_url" ]]; then + check_service "portainer" "Portainer" "$portainer_url" "/api/system/status" \ + "X-API-Key: $portainer_key" "true" + fi +fi + +# Coolify +if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "coolify" ]]; then + coolify_url=$(jq -r '.coolify.url // empty' "$CRED_FILE") + coolify_token=$(jq -r '.coolify.app_token // empty' "$CRED_FILE") + if [[ -n "$coolify_url" ]]; then + check_service "coolify" "Coolify" "$coolify_url" "/api/v1/teams" \ + "Authorization: Bearer $coolify_token" "false" + fi +fi + +# Authentik +if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "authentik" ]]; then + authentik_url=$(jq -r '.authentik.url // empty' "$CRED_FILE") + if [[ -n "$authentik_url" ]]; then + check_service "authentik" "Authentik" "$authentik_url" "/-/health/ready/" "" "true" + fi +fi + +# GLPI +if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "glpi" ]]; then + glpi_url=$(jq -r '.glpi.url // empty' "$CRED_FILE") + if [[ -n "$glpi_url" ]]; then + check_service "glpi" "GLPI" "$glpi_url" "/" "" "true" + fi +fi + +# Gitea instances +if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "gitea" ]]; then + for instance in mosaicstack usc; do + gitea_url=$(jq -r ".gitea.${instance}.url // empty" "$CRED_FILE") + if [[ -n "$gitea_url" ]]; then + display="Gitea (${instance})" + check_service "gitea-${instance}" "$display" "$gitea_url" "/api/v1/version" "" "true" + fi + done +fi + +# GitHub +if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "github" ]]; then + github_token=$(jq -r '.github.token // empty' "$CRED_FILE") + if [[ -n "$github_token" ]]; then + check_service "github" "GitHub" "https://api.github.com" "/rate_limit" \ + "Authorization: Bearer $github_token" "false" + fi +fi + +# Woodpecker +if [[ -z "$SINGLE_SERVICE" || "$SINGLE_SERVICE" == "woodpecker" ]]; then + woodpecker_url=$(jq -r '.woodpecker.url // empty' "$CRED_FILE") + woodpecker_token=$(jq -r '.woodpecker.token // empty' "$CRED_FILE") + if [[ -n "$woodpecker_url" && -n "$woodpecker_token" ]]; then + check_service "woodpecker" "Woodpecker CI" "$woodpecker_url" "/api/user" \ + "Authorization: Bearer $woodpecker_token" "true" + elif [[ "$QUIET" == "false" && "$FORMAT" == "table" ]]; then + printf " %-22s %-35s ${YELLOW}%-8s${RESET} %s\n" \ + "Woodpecker CI" "—" "NOTOKEN" "—" + fi +fi + +# Output +if [[ "$FORMAT" == "json" ]]; then + jq -n --argjson results "$RESULTS" --argjson total "$TOTAL" --argjson healthy "$HEALTHY" \ + '{total: $total, healthy: $healthy, results: $results}' + exit 0 +fi + +if [[ "$QUIET" == "false" && "$FORMAT" == "table" ]]; then + echo "" + UNHEALTHY=$((TOTAL - HEALTHY)) + if [[ "$UNHEALTHY" -eq 0 ]]; then + echo -e " ${GREEN}All $TOTAL services healthy${RESET}" + else + echo -e " ${RED}$UNHEALTHY/$TOTAL services unhealthy${RESET}" + fi + echo "" +fi + +# Exit code: 0 if all healthy, 1 if any unhealthy +[[ "$HEALTHY" -eq "$TOTAL" ]] diff --git a/rails/orchestrator-matrix/README.md b/tools/orchestrator-matrix/README.md similarity index 100% rename from rails/orchestrator-matrix/README.md rename to tools/orchestrator-matrix/README.md diff --git a/rails/orchestrator-matrix/adapters/README.md b/tools/orchestrator-matrix/adapters/README.md similarity index 100% rename from rails/orchestrator-matrix/adapters/README.md rename to tools/orchestrator-matrix/adapters/README.md diff --git a/rails/orchestrator-matrix/controller/.gitignore b/tools/orchestrator-matrix/controller/.gitignore similarity index 100% rename from rails/orchestrator-matrix/controller/.gitignore rename to tools/orchestrator-matrix/controller/.gitignore diff --git a/rails/orchestrator-matrix/controller/mosaic_orchestrator.py b/tools/orchestrator-matrix/controller/mosaic_orchestrator.py similarity index 100% rename from rails/orchestrator-matrix/controller/mosaic_orchestrator.py rename to tools/orchestrator-matrix/controller/mosaic_orchestrator.py diff --git a/rails/orchestrator-matrix/controller/tasks_md_sync.py b/tools/orchestrator-matrix/controller/tasks_md_sync.py similarity index 100% rename from rails/orchestrator-matrix/controller/tasks_md_sync.py rename to tools/orchestrator-matrix/controller/tasks_md_sync.py diff --git a/rails/orchestrator-matrix/protocol/event.schema.json b/tools/orchestrator-matrix/protocol/event.schema.json similarity index 100% rename from rails/orchestrator-matrix/protocol/event.schema.json rename to tools/orchestrator-matrix/protocol/event.schema.json diff --git a/rails/orchestrator-matrix/protocol/task.schema.json b/tools/orchestrator-matrix/protocol/task.schema.json similarity index 100% rename from rails/orchestrator-matrix/protocol/task.schema.json rename to tools/orchestrator-matrix/protocol/task.schema.json diff --git a/rails/orchestrator-matrix/transport/.gitignore b/tools/orchestrator-matrix/transport/.gitignore similarity index 100% rename from rails/orchestrator-matrix/transport/.gitignore rename to tools/orchestrator-matrix/transport/.gitignore diff --git a/rails/orchestrator-matrix/transport/matrix_transport.py b/tools/orchestrator-matrix/transport/matrix_transport.py similarity index 100% rename from rails/orchestrator-matrix/transport/matrix_transport.py rename to tools/orchestrator-matrix/transport/matrix_transport.py diff --git a/rails/portainer/README.md b/tools/portainer/README.md similarity index 100% rename from rails/portainer/README.md rename to tools/portainer/README.md diff --git a/rails/portainer/endpoint-list.sh b/tools/portainer/endpoint-list.sh similarity index 100% rename from rails/portainer/endpoint-list.sh rename to tools/portainer/endpoint-list.sh diff --git a/rails/portainer/stack-list.sh b/tools/portainer/stack-list.sh similarity index 100% rename from rails/portainer/stack-list.sh rename to tools/portainer/stack-list.sh diff --git a/rails/portainer/stack-logs.sh b/tools/portainer/stack-logs.sh similarity index 100% rename from rails/portainer/stack-logs.sh rename to tools/portainer/stack-logs.sh diff --git a/rails/portainer/stack-redeploy.sh b/tools/portainer/stack-redeploy.sh similarity index 100% rename from rails/portainer/stack-redeploy.sh rename to tools/portainer/stack-redeploy.sh diff --git a/rails/portainer/stack-start.sh b/tools/portainer/stack-start.sh similarity index 100% rename from rails/portainer/stack-start.sh rename to tools/portainer/stack-start.sh diff --git a/rails/portainer/stack-status.sh b/tools/portainer/stack-status.sh similarity index 100% rename from rails/portainer/stack-status.sh rename to tools/portainer/stack-status.sh diff --git a/rails/portainer/stack-stop.sh b/tools/portainer/stack-stop.sh similarity index 100% rename from rails/portainer/stack-stop.sh rename to tools/portainer/stack-stop.sh diff --git a/rails/qa/debug-hook.sh b/tools/qa/debug-hook.sh similarity index 100% rename from rails/qa/debug-hook.sh rename to tools/qa/debug-hook.sh diff --git a/rails/qa/qa-hook-handler.sh b/tools/qa/qa-hook-handler.sh similarity index 99% rename from rails/qa/qa-hook-handler.sh rename to tools/qa/qa-hook-handler.sh index 5c70492..db0e4f6 100755 --- a/rails/qa/qa-hook-handler.sh +++ b/tools/qa/qa-hook-handler.sh @@ -1,6 +1,6 @@ #!/bin/bash # Universal QA hook handler with robust error handling -# Location: ~/.config/mosaic/rails/qa/qa-hook-handler.sh +# Location: ~/.config/mosaic/tools/qa/qa-hook-handler.sh # Don't exit on unset variables initially to handle missing params gracefully set -eo pipefail diff --git a/rails/qa/qa-hook-stdin.sh b/tools/qa/qa-hook-stdin.sh similarity index 93% rename from rails/qa/qa-hook-stdin.sh rename to tools/qa/qa-hook-stdin.sh index 04dfe51..8c3ce16 100755 --- a/rails/qa/qa-hook-stdin.sh +++ b/tools/qa/qa-hook-stdin.sh @@ -1,6 +1,6 @@ #!/bin/bash # QA Hook handler that reads from stdin -# Location: ~/.config/mosaic/rails/qa/qa-hook-stdin.sh +# Location: ~/.config/mosaic/tools/qa/qa-hook-stdin.sh set -eo pipefail @@ -51,9 +51,9 @@ if ! [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx|mjs|cjs)$ ]]; then fi # Call the main QA handler with extracted parameters -if [ -f ~/.config/mosaic/rails/qa/qa-hook-handler.sh ]; then +if [ -f ~/.config/mosaic/tools/qa/qa-hook-handler.sh ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Calling QA handler for $FILE_PATH" >> "$LOG_FILE" - ~/.config/mosaic/rails/qa/qa-hook-handler.sh "$TOOL_NAME" "$FILE_PATH" 2>&1 | tee -a "$LOG_FILE" + ~/.config/mosaic/tools/qa/qa-hook-handler.sh "$TOOL_NAME" "$FILE_PATH" 2>&1 | tee -a "$LOG_FILE" else echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] QA handler script not found" >> "$LOG_FILE" fi diff --git a/rails/qa/qa-hook-wrapper.sh b/tools/qa/qa-hook-wrapper.sh similarity index 92% rename from rails/qa/qa-hook-wrapper.sh rename to tools/qa/qa-hook-wrapper.sh index c3df942..2117651 100755 --- a/rails/qa/qa-hook-wrapper.sh +++ b/tools/qa/qa-hook-wrapper.sh @@ -13,7 +13,7 @@ echo "[$(date '+%Y-%m-%d %H:%M:%S')] Hook wrapper called: tool=$TOOL_NAME file=$ # Call the actual QA handler if we have a file if [ -n "$FILE_PATH" ]; then - ~/.config/mosaic/rails/qa/qa-hook-handler.sh "$TOOL_NAME" "$FILE_PATH" + ~/.config/mosaic/tools/qa/qa-hook-handler.sh "$TOOL_NAME" "$FILE_PATH" else echo "[$(date '+%Y-%m-%d %H:%M:%S')] No file path available for QA check" >> logs/qa-automation.log 2>/dev/null || true fi diff --git a/rails/qa/qa-queue-monitor.sh b/tools/qa/qa-queue-monitor.sh similarity index 98% rename from rails/qa/qa-queue-monitor.sh rename to tools/qa/qa-queue-monitor.sh index 9145910..10ca6a7 100755 --- a/rails/qa/qa-queue-monitor.sh +++ b/tools/qa/qa-queue-monitor.sh @@ -1,6 +1,6 @@ #!/bin/bash # Monitor QA queues with graceful handling of missing directories -# Location: ~/.config/mosaic/rails/qa/qa-queue-monitor.sh +# Location: ~/.config/mosaic/tools/qa/qa-queue-monitor.sh PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd) diff --git a/rails/qa/remediation-hook-handler.sh b/tools/qa/remediation-hook-handler.sh similarity index 97% rename from rails/qa/remediation-hook-handler.sh rename to tools/qa/remediation-hook-handler.sh index 84a561b..fa4326a 100755 --- a/rails/qa/remediation-hook-handler.sh +++ b/tools/qa/remediation-hook-handler.sh @@ -1,6 +1,6 @@ #!/bin/bash # Universal remediation hook handler with error recovery -# Location: ~/.config/mosaic/rails/qa/remediation-hook-handler.sh +# Location: ~/.config/mosaic/tools/qa/remediation-hook-handler.sh set -euo pipefail diff --git a/rails/quality/PHILOSOPHY.md b/tools/quality/PHILOSOPHY.md similarity index 100% rename from rails/quality/PHILOSOPHY.md rename to tools/quality/PHILOSOPHY.md diff --git a/rails/quality/README.md b/tools/quality/README.md similarity index 100% rename from rails/quality/README.md rename to tools/quality/README.md diff --git a/rails/quality/docs/CI-SETUP.md b/tools/quality/docs/CI-SETUP.md similarity index 100% rename from rails/quality/docs/CI-SETUP.md rename to tools/quality/docs/CI-SETUP.md diff --git a/rails/quality/docs/TYPESCRIPT-SETUP.md b/tools/quality/docs/TYPESCRIPT-SETUP.md similarity index 100% rename from rails/quality/docs/TYPESCRIPT-SETUP.md rename to tools/quality/docs/TYPESCRIPT-SETUP.md diff --git a/rails/quality/scripts/install.ps1 b/tools/quality/scripts/install.ps1 similarity index 100% rename from rails/quality/scripts/install.ps1 rename to tools/quality/scripts/install.ps1 diff --git a/rails/quality/scripts/install.sh b/tools/quality/scripts/install.sh similarity index 100% rename from rails/quality/scripts/install.sh rename to tools/quality/scripts/install.sh diff --git a/rails/quality/scripts/verify.ps1 b/tools/quality/scripts/verify.ps1 similarity index 100% rename from rails/quality/scripts/verify.ps1 rename to tools/quality/scripts/verify.ps1 diff --git a/rails/quality/scripts/verify.sh b/tools/quality/scripts/verify.sh similarity index 100% rename from rails/quality/scripts/verify.sh rename to tools/quality/scripts/verify.sh diff --git a/rails/quality/templates/monorepo/.eslintrc.strict.js b/tools/quality/templates/monorepo/.eslintrc.strict.js similarity index 100% rename from rails/quality/templates/monorepo/.eslintrc.strict.js rename to tools/quality/templates/monorepo/.eslintrc.strict.js diff --git a/rails/quality/templates/monorepo/.husky/pre-commit b/tools/quality/templates/monorepo/.husky/pre-commit similarity index 100% rename from rails/quality/templates/monorepo/.husky/pre-commit rename to tools/quality/templates/monorepo/.husky/pre-commit diff --git a/rails/quality/templates/monorepo/.lintstagedrc.js b/tools/quality/templates/monorepo/.lintstagedrc.js similarity index 100% rename from rails/quality/templates/monorepo/.lintstagedrc.js rename to tools/quality/templates/monorepo/.lintstagedrc.js diff --git a/rails/quality/templates/monorepo/.woodpecker.yml b/tools/quality/templates/monorepo/.woodpecker.yml similarity index 100% rename from rails/quality/templates/monorepo/.woodpecker.yml rename to tools/quality/templates/monorepo/.woodpecker.yml diff --git a/rails/quality/templates/monorepo/README-STRUCTURE.md b/tools/quality/templates/monorepo/README-STRUCTURE.md similarity index 100% rename from rails/quality/templates/monorepo/README-STRUCTURE.md rename to tools/quality/templates/monorepo/README-STRUCTURE.md diff --git a/rails/quality/templates/monorepo/package.json.snippet b/tools/quality/templates/monorepo/package.json.snippet similarity index 100% rename from rails/quality/templates/monorepo/package.json.snippet rename to tools/quality/templates/monorepo/package.json.snippet diff --git a/rails/quality/templates/monorepo/pnpm-workspace.yaml.snippet b/tools/quality/templates/monorepo/pnpm-workspace.yaml.snippet similarity index 100% rename from rails/quality/templates/monorepo/pnpm-workspace.yaml.snippet rename to tools/quality/templates/monorepo/pnpm-workspace.yaml.snippet diff --git a/rails/quality/templates/monorepo/tsconfig.base.json b/tools/quality/templates/monorepo/tsconfig.base.json similarity index 100% rename from rails/quality/templates/monorepo/tsconfig.base.json rename to tools/quality/templates/monorepo/tsconfig.base.json diff --git a/rails/quality/templates/monorepo/turbo.json.snippet b/tools/quality/templates/monorepo/turbo.json.snippet similarity index 100% rename from rails/quality/templates/monorepo/turbo.json.snippet rename to tools/quality/templates/monorepo/turbo.json.snippet diff --git a/rails/quality/templates/typescript-nextjs/.eslintrc.strict.js b/tools/quality/templates/typescript-nextjs/.eslintrc.strict.js similarity index 100% rename from rails/quality/templates/typescript-nextjs/.eslintrc.strict.js rename to tools/quality/templates/typescript-nextjs/.eslintrc.strict.js diff --git a/rails/quality/templates/typescript-nextjs/.husky/pre-commit b/tools/quality/templates/typescript-nextjs/.husky/pre-commit similarity index 100% rename from rails/quality/templates/typescript-nextjs/.husky/pre-commit rename to tools/quality/templates/typescript-nextjs/.husky/pre-commit diff --git a/rails/quality/templates/typescript-nextjs/.lintstagedrc.js b/tools/quality/templates/typescript-nextjs/.lintstagedrc.js similarity index 100% rename from rails/quality/templates/typescript-nextjs/.lintstagedrc.js rename to tools/quality/templates/typescript-nextjs/.lintstagedrc.js diff --git a/rails/quality/templates/typescript-nextjs/.woodpecker.yml b/tools/quality/templates/typescript-nextjs/.woodpecker.yml similarity index 100% rename from rails/quality/templates/typescript-nextjs/.woodpecker.yml rename to tools/quality/templates/typescript-nextjs/.woodpecker.yml diff --git a/rails/quality/templates/typescript-nextjs/next.config.js.snippet b/tools/quality/templates/typescript-nextjs/next.config.js.snippet similarity index 100% rename from rails/quality/templates/typescript-nextjs/next.config.js.snippet rename to tools/quality/templates/typescript-nextjs/next.config.js.snippet diff --git a/rails/quality/templates/typescript-nextjs/package.json.snippet b/tools/quality/templates/typescript-nextjs/package.json.snippet similarity index 100% rename from rails/quality/templates/typescript-nextjs/package.json.snippet rename to tools/quality/templates/typescript-nextjs/package.json.snippet diff --git a/rails/quality/templates/typescript-nextjs/tsconfig.strict.json b/tools/quality/templates/typescript-nextjs/tsconfig.strict.json similarity index 100% rename from rails/quality/templates/typescript-nextjs/tsconfig.strict.json rename to tools/quality/templates/typescript-nextjs/tsconfig.strict.json diff --git a/rails/quality/templates/typescript-node/.eslintrc.strict.js b/tools/quality/templates/typescript-node/.eslintrc.strict.js similarity index 100% rename from rails/quality/templates/typescript-node/.eslintrc.strict.js rename to tools/quality/templates/typescript-node/.eslintrc.strict.js diff --git a/rails/quality/templates/typescript-node/.husky/pre-commit b/tools/quality/templates/typescript-node/.husky/pre-commit similarity index 100% rename from rails/quality/templates/typescript-node/.husky/pre-commit rename to tools/quality/templates/typescript-node/.husky/pre-commit diff --git a/rails/quality/templates/typescript-node/.lintstagedrc.js b/tools/quality/templates/typescript-node/.lintstagedrc.js similarity index 100% rename from rails/quality/templates/typescript-node/.lintstagedrc.js rename to tools/quality/templates/typescript-node/.lintstagedrc.js diff --git a/rails/quality/templates/typescript-node/.woodpecker.yml b/tools/quality/templates/typescript-node/.woodpecker.yml similarity index 100% rename from rails/quality/templates/typescript-node/.woodpecker.yml rename to tools/quality/templates/typescript-node/.woodpecker.yml diff --git a/rails/quality/templates/typescript-node/package.json.snippet b/tools/quality/templates/typescript-node/package.json.snippet similarity index 100% rename from rails/quality/templates/typescript-node/package.json.snippet rename to tools/quality/templates/typescript-node/package.json.snippet diff --git a/rails/quality/templates/typescript-node/tsconfig.strict.json b/tools/quality/templates/typescript-node/tsconfig.strict.json similarity index 100% rename from rails/quality/templates/typescript-node/tsconfig.strict.json rename to tools/quality/templates/typescript-node/tsconfig.strict.json diff --git a/tools/woodpecker/README.md b/tools/woodpecker/README.md new file mode 100644 index 0000000..fff323f --- /dev/null +++ b/tools/woodpecker/README.md @@ -0,0 +1,58 @@ +# Woodpecker CI Tool Suite + +Interact with Woodpecker CI pipelines (list builds, check status, trigger builds). + +## Prerequisites + +- `jq` and `curl` installed +- Woodpecker credentials in `~/src/jarvis-brain/credentials.json` + +## Setup + +A Woodpecker API token is required. To configure: + +1. Go to Woodpecker CI → User Settings → API +2. Generate a personal token +3. Add to `credentials.json`: + +```json +{ + "woodpecker": { + "url": "https://ci.mosaicstack.dev", + "token": "YOUR_TOKEN_HERE" + } +} +``` + +## Scripts + +| Script | Purpose | +|--------|---------| +| `pipeline-list.sh` | List recent pipelines for a repo | +| `pipeline-status.sh` | Get status of a specific or latest pipeline | +| `pipeline-trigger.sh` | Trigger a new pipeline build | + +## Common Options + +- `-r owner/repo` — Repository (auto-detected from git remote if omitted) +- `-f json` — JSON output (default: table) +- `-h` — Show help + +## API Reference + +- Base URL: `https://ci.mosaicstack.dev` +- API prefix: `/api/` +- Auth: Bearer token in `Authorization` header + +## Examples + +```bash +# List recent builds +~/.config/mosaic/tools/woodpecker/pipeline-list.sh + +# Check latest build status +~/.config/mosaic/tools/woodpecker/pipeline-status.sh + +# Trigger a build on a specific branch +~/.config/mosaic/tools/woodpecker/pipeline-trigger.sh -b feature/my-branch +``` diff --git a/tools/woodpecker/pipeline-list.sh b/tools/woodpecker/pipeline-list.sh new file mode 100755 index 0000000..21a380d --- /dev/null +++ b/tools/woodpecker/pipeline-list.sh @@ -0,0 +1,86 @@ +#!/usr/bin/env bash +# +# pipeline-list.sh — List Woodpecker CI pipelines +# +# Usage: pipeline-list.sh [-r owner/repo] [-l limit] [-f format] +# +# Options: +# -r repo Repository in owner/repo format (default: current repo) +# -l limit Number of pipelines to show (default: 20) +# -f format Output format: table (default), json +# -h Show this help +# +# Requires: woodpecker.url and woodpecker.token in credentials.json +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" + +# Check if woodpecker credentials exist before loading +CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" +if ! jq -e '.woodpecker.token // empty | select(. != "")' "$CRED_FILE" &>/dev/null; then + echo "Error: Woodpecker API token not configured in credentials.json" >&2 + echo "" >&2 + echo "To configure:" >&2 + echo " 1. Get your token from Woodpecker CI → User Settings → API" >&2 + echo " 2. Add to credentials.json:" >&2 + echo ' "woodpecker": {"url": "https://ci.mosaicstack.dev", "token": "YOUR_TOKEN"}' >&2 + exit 1 +fi + +load_credentials woodpecker + +REPO="" +LIMIT=20 +FORMAT="table" + +while getopts "r:l:f:h" opt; do + case $opt in + r) REPO="$OPTARG" ;; + l) LIMIT="$OPTARG" ;; + f) FORMAT="$OPTARG" ;; + h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-r owner/repo] [-l limit] [-f format]" >&2; exit 1 ;; + esac +done + +# Auto-detect repo from git remote if not specified +if [[ -z "$REPO" ]]; then + remote_url=$(git remote get-url origin 2>/dev/null || true) + if [[ -n "$remote_url" ]]; then + REPO=$(echo "$remote_url" | sed -E 's|.*[:/]([^/]+/[^/]+?)(\.git)?$|\1|') + else + echo "Error: -r owner/repo required (not in a git repository)" >&2 + exit 1 + fi +fi + +response=$(curl -sk -w "\n%{http_code}" \ + -H "Authorization: Bearer $WOODPECKER_TOKEN" \ + "${WOODPECKER_URL}/api/repos/${REPO}/pipelines?per_page=${LIMIT}") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to list pipelines (HTTP $http_code)" >&2 + exit 1 +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "NUMBER STATUS BRANCH EVENT MESSAGE" +echo "------ -------- -------------------- -------- ----------------------------------------" +echo "$body" | jq -r '.[] | [ + (.number | tostring), + .status, + .branch, + .event, + (.message | split("\n")[0]) +] | @tsv' | while IFS=$'\t' read -r number status branch event message; do + printf "%-6s %-8s %-20s %-8s %s\n" \ + "$number" "$status" "${branch:0:20}" "$event" "${message:0:40}" +done diff --git a/tools/woodpecker/pipeline-status.sh b/tools/woodpecker/pipeline-status.sh new file mode 100755 index 0000000..95e96a9 --- /dev/null +++ b/tools/woodpecker/pipeline-status.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# +# pipeline-status.sh — Check Woodpecker CI pipeline status +# +# Usage: pipeline-status.sh [-r owner/repo] [-n number] [-f format] +# +# Options: +# -r repo Repository in owner/repo format (default: current repo) +# -n number Pipeline number (default: latest) +# -f format Output format: table (default), json +# -h Show this help +# +# Requires: woodpecker.url and woodpecker.token in credentials.json +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" + +CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" +if ! jq -e '.woodpecker.token // empty | select(. != "")' "$CRED_FILE" &>/dev/null; then + echo "Error: Woodpecker API token not configured in credentials.json" >&2 + echo "See: ~/.config/mosaic/tools/woodpecker/README.md" >&2 + exit 1 +fi + +load_credentials woodpecker + +REPO="" +NUMBER="" +FORMAT="table" + +while getopts "r:n:f:h" opt; do + case $opt in + r) REPO="$OPTARG" ;; + n) NUMBER="$OPTARG" ;; + f) FORMAT="$OPTARG" ;; + h) head -14 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-r owner/repo] [-n number] [-f format]" >&2; exit 1 ;; + esac +done + +if [[ -z "$REPO" ]]; then + remote_url=$(git remote get-url origin 2>/dev/null || true) + if [[ -n "$remote_url" ]]; then + REPO=$(echo "$remote_url" | sed -E 's|.*[:/]([^/]+/[^/]+?)(\.git)?$|\1|') + else + echo "Error: -r owner/repo required (not in a git repository)" >&2 + exit 1 + fi +fi + +if [[ -z "$NUMBER" ]]; then + # Get latest pipeline + endpoint="${WOODPECKER_URL}/api/repos/${REPO}/pipelines?per_page=1" +else + endpoint="${WOODPECKER_URL}/api/repos/${REPO}/pipelines/${NUMBER}" +fi + +response=$(curl -sk -w "\n%{http_code}" \ + -H "Authorization: Bearer $WOODPECKER_TOKEN" \ + "$endpoint") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" ]]; then + echo "Error: Failed to get pipeline status (HTTP $http_code)" >&2 + exit 1 +fi + +# If we got a list, extract the first one +if [[ -z "$NUMBER" ]]; then + body=$(echo "$body" | jq '.[0]') +fi + +if [[ "$FORMAT" == "json" ]]; then + echo "$body" | jq '.' + exit 0 +fi + +echo "Pipeline Status" +echo "===============" +echo "$body" | jq -r ' + " Number: \(.number)\n" + + " Status: \(.status)\n" + + " Branch: \(.branch)\n" + + " Event: \(.event)\n" + + " Commit: \(.commit[:12])\n" + + " Message: \(.message | split("\n")[0])\n" + + " Author: \(.author)\n" + + " Started: \(.started_at // "pending")\n" + + " Finished: \(.finished_at // "running")" +' diff --git a/tools/woodpecker/pipeline-trigger.sh b/tools/woodpecker/pipeline-trigger.sh new file mode 100755 index 0000000..24a456f --- /dev/null +++ b/tools/woodpecker/pipeline-trigger.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# +# pipeline-trigger.sh — Trigger a Woodpecker CI pipeline +# +# Usage: pipeline-trigger.sh [-r owner/repo] [-b branch] +# +# Options: +# -r repo Repository in owner/repo format (default: current repo) +# -b branch Branch to build (default: main) +# -h Show this help +# +# Requires: woodpecker.url and woodpecker.token in credentials.json +set -euo pipefail + +MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}" +source "$MOSAIC_HOME/tools/_lib/credentials.sh" + +CRED_FILE="${MOSAIC_CREDENTIALS_FILE:-$HOME/src/jarvis-brain/credentials.json}" +if ! jq -e '.woodpecker.token // empty | select(. != "")' "$CRED_FILE" &>/dev/null; then + echo "Error: Woodpecker API token not configured in credentials.json" >&2 + echo "See: ~/.config/mosaic/tools/woodpecker/README.md" >&2 + exit 1 +fi + +load_credentials woodpecker + +REPO="" +BRANCH="main" + +while getopts "r:b:h" opt; do + case $opt in + r) REPO="$OPTARG" ;; + b) BRANCH="$OPTARG" ;; + h) head -12 "$0" | grep "^#" | sed 's/^# \?//'; exit 0 ;; + *) echo "Usage: $0 [-r owner/repo] [-b branch]" >&2; exit 1 ;; + esac +done + +if [[ -z "$REPO" ]]; then + remote_url=$(git remote get-url origin 2>/dev/null || true) + if [[ -n "$remote_url" ]]; then + REPO=$(echo "$remote_url" | sed -E 's|.*[:/]([^/]+/[^/]+?)(\.git)?$|\1|') + else + echo "Error: -r owner/repo required (not in a git repository)" >&2 + exit 1 + fi +fi + +echo "Triggering pipeline for $REPO on branch $BRANCH..." + +response=$(curl -sk -w "\n%{http_code}" -X POST \ + -H "Authorization: Bearer $WOODPECKER_TOKEN" \ + -H "Content-Type: application/json" \ + -d "$(jq -n --arg b "$BRANCH" '{branch: $b}')" \ + "${WOODPECKER_URL}/api/repos/${REPO}/pipelines") + +http_code=$(echo "$response" | tail -n1) +body=$(echo "$response" | sed '$d') + +if [[ "$http_code" != "200" && "$http_code" != "201" ]]; then + echo "Error: Failed to trigger pipeline (HTTP $http_code)" >&2 + echo "$body" | jq -r '.' 2>/dev/null >&2 || echo "$body" >&2 + exit 1 +fi + +number=$(echo "$body" | jq -r '.number') +echo "Pipeline #$number triggered successfully"