Files
HiveGrid-Wireframes/hivegrid-cloud/docs/contributing/cli-ops.md
T

7.1 KiB

CLI ops (toolctrl ops)

The operator CLI under packages/cli/ ships three ops sub-commands for day-to-day operational work:

  • toolctrl ops doctor — local environment health check
  • toolctrl ops migrate-tenant — resumable tenant migration between envs
  • toolctrl ops impersonate — audit-logged debug magic-link for a tenant user

All three commands accept the --env flag to target a named env file under ~/.toolctrl/envs/. The default is the current working-dir's .env (i.e. whatever is already loaded into process.env).

Env-file layout

Named envs live under:

~/.toolctrl/envs/<name>.env

Example layout:

~/.toolctrl/envs/
├── staging.env       # chmod 600
├── staging-backup.env
└── dev.env
  • File format: standard KEY=VALUE .env syntax with # comments. No shell interpolation, no multi-line values.
  • File permissions must be 0600 (owner-only). The CLI emits a warning when world-readable bits are set; rotate the file with chmod 600 ~/.toolctrl/envs/<name>.env.
  • Production env files must not live in CI. The impersonate command refuses any env named prod, production, or live regardless of file content.

toolctrl ops doctor

Runs eight probes against the resolved env and emits an ok/warn/fail table:

Probe Description Fail hint
node.version Process Node version >=20 nvm use
pnpm.version pnpm --version >=9 corepack enable
docker.reachable docker info succeeds docker compose up -d
postgres.reachable DATABASE_URL ping (injected) Check docker compose ps postgres
redis.reachable REDIS_URL ping (injected) docker compose up -d redis
sentry.dsn HEAD against SENTRY_DSN origin Check Coolify Sentry container
env.required Required env keys present Update your .env
prisma.migrations prisma migrate status (injected) pnpm db:migrate dev

Examples:

# Default — read local .env
node packages/cli/bin/toolctrl-cli.mjs ops doctor

# Read ~/.toolctrl/envs/staging.env
node packages/cli/bin/toolctrl-cli.mjs ops doctor --env=staging

# JSON output for CI
node packages/cli/bin/toolctrl-cli.mjs ops doctor --json

Exit codes: 0 when every probe is ok or warn; 1 when any probe reports fail.

Troubleshooting

Symptom Likely cause
postgres.reachable: fail (DATABASE_URL not set) Forgot to export .env before running CLI
docker.reachable: warn Docker Desktop not running
sentry.dsn: warn SENTRY_DSN intentionally unset — error reporting disabled
env.required: fail Missing required env vars: SESSION_SECRET Bootstrap your .env from .env.example
prisma.migrations: warn N pending Run pnpm db:migrate dev

toolctrl ops migrate-tenant

End-to-end tenant migration between named envs. Reuses the packages/cli/src/commands/tenant/snapshot.ts infrastructure from phase 03 (#44). Resumable via a state file under ~/.toolctrl/migrations/<tenantId>-<timestamp>.state.json.

Steps (each is an atomic resume-point):

  1. preflight — both envs resolve, names validated
  2. snapshot — tenant snapshot exports source tenant to a local archive
  3. restore — archive is restored into the target env
  4. verify — row-counts compared source vs target
  5. cleanup — snapshot kept on disk for audit

Examples:

# Plan a migration (no writes)
node packages/cli/bin/toolctrl-cli.mjs ops migrate-tenant tenant-abc \
  --from=staging --to=staging-backup --dry-run

# Real migration
node packages/cli/bin/toolctrl-cli.mjs ops migrate-tenant tenant-abc \
  --from=staging --to=staging-backup

# Resume after a crash
node packages/cli/bin/toolctrl-cli.mjs ops migrate-tenant tenant-abc \
  --from=staging --to=staging-backup \
  --resume ~/.toolctrl/migrations/tenant-abc-2026-05-17T120000000Z.state.json

The CLI emits a per-step status line:

state-file=/home/op/.toolctrl/migrations/tenant-abc-2026-05-17T120000000Z.state.json
  preflight:  ok — from=staging to=staging-backup
  snapshot:   ok — /home/op/.toolctrl/migrations/tenant-abc-….tar.gz
  restore:    ok — restored 12345 rows
  verify:     ok — row-counts match
  cleanup:    ok — snapshot kept on disk for audit

The Prisma + S3 backends are injected at the bin/toolctrl-cli.mjs entrypoint. The standalone subcommand prints a warning if invoked without that injection (no destructive operations happen).

toolctrl ops impersonate

Generates a 15-minute HMAC-signed magic-link the engineer can paste into a browser to debug a specific tenant user. Every issued link writes an AuditEvent so the action is reviewable.

node packages/cli/bin/toolctrl-cli.mjs ops impersonate \
  tenant-abc user-123 \
  --env=staging \
  --reason="support-ticket-456"

Safety guarantees:

  • Production env names (prod, production, live) are hard-blocked. No --force bypass exists. Use the admin self-service impersonate UI for production.
  • TTL is fixed at 15 minutes. Tokens cannot be longer-lived.
  • HMAC signing key must be at least 16 characters (the CLI throws otherwise).
  • The audit-event captures:
    • actorType = admin
    • action = ops.impersonate.issued
    • targetType = user, targetId = <userId>
    • regulatoryScope = [soc2]
    • dataSensitivity = pii
    • outcome = success
    • metadata = { envName, tokenId, expiresAt, ttlMinutes, reason, userEmail }

The audit-event is written before the link is returned, so even a crashing operator workflow leaves a trail.

Backend route

The corresponding loader at apps/web/app/routes/api.auth.impersonate.tsx (to be wired in a follow-up phase) is responsible for:

  1. Verifying the HMAC signature against the same secret
  2. Looking up the audit-event by tokenId and refusing if absent
  3. Refusing if the audit-event's outcome ≠ success
  4. Setting a session cookie scoped to the impersonated user with a short lifetime

Where things live

packages/cli/src/commands/ops/
├── _env.ts              # env-file resolver (local vs named)
├── _env.test.ts
├── doctor.ts            # doctor command + 8 probes
├── doctor.test.ts
├── migrate-tenant.ts    # 5-step resumable migration
├── migrate-tenant.test.ts
├── impersonate.ts       # HMAC magic-link + audit-event
├── impersonate.test.ts
└── index.ts             # citty subcommand group

The bin entrypoint in packages/cli/bin/toolctrl-cli.mjs is responsible for injecting the live Prisma client + S3 client + URL base into the commands. The standalone files keep zero dependencies on @prisma/client or AWS SDK so the package builds without optional peers.

Security considerations

  • Never commit ~/.toolctrl/envs/*.env. The path is outside the repo on purpose.
  • The migrate-tenant state files contain tenant IDs but no secrets. They are safe to retain for audit.
  • Snapshot archives may contain PII. Apply the standard PII-retention policy (chmod 600, delete after migration audit window).
  • The impersonate audit-event is admin-visible and forms part of the soc2 audit trail. Do not redact it after the fact.