Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,14 +234,15 @@ Durable, non-obvious notes for working in the Cursor Cloud VM. The startup updat
- `ubuntu` is in the `docker` group, so `docker`/`docker compose` work without `sudo` once the daemon is up. On a brand-new session the socket may be `root:docker`; if you hit a permission error, `sudo chmod 666 /var/run/docker.sock`.

### Secrets / Infisical
- The Infisical CLI is NOT installed. `scripts/with-env.sh` calls `infisical export` and prints `infisical: command not found`, then continues (the script has no `set -e`) using `.env` + `.env.local`. This is expected in the VM — local dev only needs `DATABASE_URL`, `CLICKHOUSE_URL`, `REDIS_URL`, which `pnpm compose:up` writes into `.env.local`.
- The Infisical CLI is NOT installed. [`scripts/with-env.sh`](scripts/with-env.sh) deliberately fails before running its command when Infisical export is unavailable. For VM commands that only need the local `DATABASE_URL`, `CLICKHOUSE_URL`, and `REDIS_URL` written by `pnpm compose:up`, source `.env.local` explicitly and run the underlying command directly.
- Provider OAuth/credentials and other secret-gated features are unavailable without Infisical. Use the dev-login bypass below instead of real auth.

### Bring up the full local stack (web + API, no PeerDB needed)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Run from the repo root, in order:
1. `pnpm compose:up` — starts Postgres/TimescaleDB + ClickHouse + Redis on random host ports and writes `.env.local`.
2. `pnpm setup-db` — applies Postgres + ClickHouse migrations. The ClickHouse migrations pre-create the `analytics.*` serving tables that the API boot waits for, so PeerDB/Temporal and the Redpanda metric-stream stack are NOT required for dev.
3. `./scripts/with-env.sh pnpm seed` — seeds demo data and creates the `dev-session`. Note `pnpm seed` does not wrap `with-env.sh` itself, so it must be run through `with-env.sh` (or with `DATABASE_URL` exported) to pick up `.env.local`.
3. `set -a; . ./.env.local; set +a; pnpm seed` — exports the local service URLs, seeds demo data, and creates the `dev-session` without requiring unavailable provider secrets.
4. `pnpm analytics:build` (optional) — dbt build via `uv`; populates ClickHouse activity/sleep read models from seeded Postgres data. The API boots without it (tables exist empty), but activity-stream analytics need it.
5. API: `cd packages/server && pnpm dev` (Express + tRPC on `:3000`). Web: `cd packages/web && pnpm dev` (Vite on `:5173`, proxies `/api`, `/auth`, `/callback` to `:3000`).

Expand Down
1 change: 1 addition & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Utility and maintenance scripts for development, infrastructure, and reverse eng

- `with-env.sh`: Wrapper script that loads environment variables from `.env`, `.env.local`, and **Infisical**.
- Automatically constructs OpenTelemetry auth headers from `AXIOM_API_TOKEN`.
- Exits before running the command when Infisical export fails or no command is provided.
- Usage: `./scripts/with-env.sh <command>`
- `make-admin.sh`: Promotes a user to admin in the production database via SSH.
- Resolves server IP via Infisical, finds the `dofek-db` container, and executes `UPDATE fitness.user_profile SET is_admin = true ...`.
Expand Down
11 changes: 10 additions & 1 deletion scripts/with-env.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
# Requires: infisical CLI installed and authenticated (run `infisical login` first)
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"

if [ "$#" -eq 0 ]; then
echo "Usage: $(basename "$0") <command>" >&2
exit 64
fi

# Load .env as defaults (don't overwrite existing vars)
if [ -f "$REPO_ROOT/.env" ]; then
while IFS='=' read -r key value; do
Expand All @@ -24,7 +29,11 @@ if [ -f "$REPO_ROOT/.env.local" ]; then
fi

# Fetch secrets from Infisical and export them
eval "$(infisical export --env=prod --format=dotenv-export)"
if ! infisical_exports="$(infisical export --env=prod --format=dotenv-export)"; then
echo "Failed to export secrets from Infisical" >&2
exit 1
fi
eval "$infisical_exports"

# Construct OTEL auth headers from Axiom API token (config concern, not a secret)
if [ -n "$AXIOM_API_TOKEN" ]; then
Expand Down
116 changes: 116 additions & 0 deletions scripts/with-env.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { spawnSync } from "node:child_process";
import {
chmodSync,
copyFileSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { afterEach, describe, expect, it } from "vitest";

const fixtureRoots: string[] = [];

function writeExecutable(path: string, contents: string): void {
writeFileSync(path, contents);
chmodSync(path, 0o755);
}

function createFixture(infisicalScript: string): {
fixtureRoot: string;
scriptPath: string;
} {
const fixtureRoot = mkdtempSync(join(tmpdir(), "with-env-test-"));
const scriptsDirectory = join(fixtureRoot, "scripts");
const binDirectory = join(fixtureRoot, "bin");
mkdirSync(scriptsDirectory);
mkdirSync(binDirectory);
fixtureRoots.push(fixtureRoot);

const scriptPath = join(scriptsDirectory, "with-env.sh");
copyFileSync(resolve("scripts/with-env.sh"), scriptPath);
chmodSync(scriptPath, 0o755);
writeExecutable(join(binDirectory, "infisical"), infisicalScript);

return { fixtureRoot, scriptPath };
}

function runWithEnv(scriptPath: string, fixtureRoot: string, command: string[] = []) {
return spawnSync("bash", [scriptPath, ...command], {
encoding: "utf8",
env: {
...process.env,
PATH: `${join(fixtureRoot, "bin")}:${process.env.PATH ?? ""}`,
},
});
}

afterEach(() => {
for (const fixtureRoot of fixtureRoots.splice(0)) {
rmSync(fixtureRoot, { recursive: true, force: true });
}
});

describe("with-env", () => {
it("fails before running the command when Infisical export fails", () => {
const { fixtureRoot, scriptPath } = createFixture(`#!/bin/sh
printf '%s\n' 'authentication expired' >&2
exit 42
`);
const commandMarker = join(fixtureRoot, "command-ran");
const wrappedCommand = join(fixtureRoot, "bin", "wrapped-command");
writeExecutable(
wrappedCommand,
`#!/bin/sh
touch "$1"
`,
);

const result = runWithEnv(scriptPath, fixtureRoot, [wrappedCommand, commandMarker]);

expect(result.status).not.toBe(0);
expect(result.stderr).toContain("Failed to export secrets from Infisical");
expect(existsSync(commandMarker)).toBe(false);
});

it("fails when no command is provided", () => {
const { fixtureRoot, scriptPath } = createFixture(`#!/bin/sh
exit 0
`);

const result = runWithEnv(scriptPath, fixtureRoot);

expect(result.status).not.toBe(0);
expect(result.stderr).toContain("Usage: with-env.sh <command>");
});

it("exports shell-quoted Infisical values before running the command", () => {
const injectionMarker = join(tmpdir(), `with-env-injection-${process.pid}`);
rmSync(injectionMarker, { force: true });
const secretValue = `value with spaces;$(touch "${injectionMarker}")'quoted`;
const shellQuotedSecret = `'${secretValue.replaceAll("'", `'"'"'`)}'`;
const { fixtureRoot, scriptPath } = createFixture(`#!/bin/sh
cat <<'EXPORT_OUTPUT'
export WITH_ENV_TEST_SECRET=${shellQuotedSecret}
EXPORT_OUTPUT
`);
const capturedValuePath = join(fixtureRoot, "captured-value");
const wrappedCommand = join(fixtureRoot, "bin", "wrapped-command");
writeExecutable(
wrappedCommand,
`#!/bin/sh
printf '%s' "$WITH_ENV_TEST_SECRET" > "$1"
`,
);

const result = runWithEnv(scriptPath, fixtureRoot, [wrappedCommand, capturedValuePath]);

expect(result.status).toBe(0);
expect(readFileSync(capturedValuePath, "utf8")).toBe(secretValue);
expect(existsSync(injectionMarker)).toBe(false);
});
});