204 lines
7.1 KiB
Markdown
204 lines
7.1 KiB
Markdown
# CLI ops (`hivegrid ops`)
|
|
|
|
The operator CLI under `packages/cli/` ships three ops sub-commands
|
|
for day-to-day operational work:
|
|
|
|
- `hivegrid ops doctor` — local environment health check
|
|
- `hivegrid ops migrate-tenant` — resumable tenant migration between envs
|
|
- `hivegrid 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 `~/.hivegrid/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:
|
|
|
|
```
|
|
~/.hivegrid/envs/<name>.env
|
|
```
|
|
|
|
Example layout:
|
|
|
|
```
|
|
~/.hivegrid/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 ~/.hivegrid/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.
|
|
|
|
## `hivegrid 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:
|
|
|
|
```bash
|
|
# Default — read local .env
|
|
node packages/cli/bin/hivegrid-cli.mjs ops doctor
|
|
|
|
# Read ~/.hivegrid/envs/staging.env
|
|
node packages/cli/bin/hivegrid-cli.mjs ops doctor --env=staging
|
|
|
|
# JSON output for CI
|
|
node packages/cli/bin/hivegrid-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` |
|
|
|
|
## `hivegrid 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
|
|
`~/.hivegrid/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:
|
|
|
|
```bash
|
|
# Plan a migration (no writes)
|
|
node packages/cli/bin/hivegrid-cli.mjs ops migrate-tenant tenant-abc \
|
|
--from=staging --to=staging-backup --dry-run
|
|
|
|
# Real migration
|
|
node packages/cli/bin/hivegrid-cli.mjs ops migrate-tenant tenant-abc \
|
|
--from=staging --to=staging-backup
|
|
|
|
# Resume after a crash
|
|
node packages/cli/bin/hivegrid-cli.mjs ops migrate-tenant tenant-abc \
|
|
--from=staging --to=staging-backup \
|
|
--resume ~/.hivegrid/migrations/tenant-abc-2026-05-17T120000000Z.state.json
|
|
```
|
|
|
|
The CLI emits a per-step status line:
|
|
|
|
```
|
|
state-file=/home/op/.hivegrid/migrations/tenant-abc-2026-05-17T120000000Z.state.json
|
|
preflight: ok — from=staging to=staging-backup
|
|
snapshot: ok — /home/op/.hivegrid/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/hivegrid-cli.mjs`
|
|
entrypoint. The standalone subcommand prints a warning if invoked
|
|
without that injection (no destructive operations happen).
|
|
|
|
## `hivegrid 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.
|
|
|
|
```bash
|
|
node packages/cli/bin/hivegrid-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/hivegrid-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 `~/.hivegrid/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.
|