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
4 changes: 0 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ test-report.junit.xml
.env
.env.*
!.env.example
# Checked in like the examples above: it configures `dev:solo`, whose whole point is that it holds
# nothing worth keeping out of the repository.
!.env.solo
.code-zero/
.data/
*.log
Expand All @@ -32,4 +29,3 @@ test-report.junit.xml
/vscode_cli.tar.gz
code-zero.deployment.yml
!code-zero.deployment.example.yml
!apps/dashboard/code-zero.deployment.solo.yml
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,6 @@ cp apps/dashboard/.env.example apps/dashboard/.env

```bash
aube run dev # watch workspace development tasks
aube run dev:solo # dashboard alone, no database (apps/dashboard/.env.solo)
aube run dev:docs # docs site alone
aube run dev:marketing # marketing site alone
aube run zero doctor # inspect the local environment
Expand Down
28 changes: 16 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,22 +103,26 @@ aube run dev
The root `.env` configures the CLI. Each app loads its own file: the dashboard uses
`apps/dashboard/.env`, while the docs app optionally uses `apps/docs/.env` for `NUXT_APP_BASE_URL`.

To see the dashboard before configuring anything, start it on its own instead:
The dashboard needs Postgres. It stores accounts, sessions, and the repositories a task may
target, so point `DATABASE_URL` in `apps/dashboard/.env` at a database and apply the schema before
the first start — there is no in-memory mode to fall back on:

```bash
mise install
aube ci
aube run dev:solo # http://localhost:3000, then sign up at /signup
aube run db:migrate
aube run dev # http://localhost:3000, then sign up at /signup
```

Set `AUTH_ENABLE_SIGNUP=true` in `apps/dashboard/.env` to create the first account, then turn it
back off. Self-registration grants the `user` role, not `admin`, and configuring repositories
(`repositories.save`, reached from the dashboard) requires `admin` — the same gate `audit.list`
uses — so promote that first account once, directly in the database:

```sql
UPDATE "user" SET role = 'admin' WHERE email = 'you@example.com';
```

`dev:solo` is `nuxt dev` reading [`apps/dashboard/.env.solo`](./apps/dashboard/.env.solo) in
place of `.env`: Better Auth runs on an in-memory store, so there is no Postgres to install and no
migration to apply, and the account you create lives until you stop the process. Nothing else
about the app changes — it is the same UI, the same router, and the same authentication endpoints
a deployment serves. Tasks still need a checkout to target, so add one to
`CODE_ZERO_SOLO_REPOSITORIES` in that file; `observe` runs no model, so a task can be
created and inspected without a provider credential. Use `aube run dev` and `apps/dashboard/.env`
for anything that has to persist.
From there, add a repository from the dashboard to give tasks a checkout to target; `observe`
runs no model, so a task can be created and inspected without a provider credential.
Comment thread
RedStar071 marked this conversation as resolved.

`dev:docs` and `dev:marketing` start those two apps on their own the same way `mail:preview`
already does for `apps/mail-preview` — a plain `turbo run dev` filtered to one app, with no
Expand Down
36 changes: 0 additions & 36 deletions apps/dashboard/.env.solo

This file was deleted.

9 changes: 0 additions & 9 deletions apps/dashboard/code-zero.deployment.solo.yml

This file was deleted.

1 change: 0 additions & 1 deletion apps/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
"build": "nuxt build",
"clean": "nuxt cleanup && tsc -b --clean",
"dev": "nuxt dev",
"dev:solo": "nuxt dev --dotenv .env.solo",
"lint": "oxlint --config ../../tooling/oxc/apps.oxlintrc.json --type-aware --type-check --ignore-pattern \"test/nuxt/components/**\" --ignore-pattern \"test/nuxt/pages/**\" app config modules server test",
"lint:fix": "oxlint --fix --config ../../tooling/oxc/apps.oxlintrc.json --type-aware --type-check --ignore-pattern \"test/nuxt/components/**\" --ignore-pattern \"test/nuxt/pages/**\" app config modules server test",
"prelint": "nuxt prepare",
Expand Down
17 changes: 9 additions & 8 deletions apps/dashboard/server/auth.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,22 +75,23 @@ const options = authBetterAuthOptions({
});

/**
* `AUTH_E2E_MEMORY` swaps the Postgres adapter for an in-memory one. Two callers set it, both of
* which own the whole server process and throw its store away when they exit: the Playwright
* preview server (`start:playwright:webserver`, see `playwright.config.ts`), so the e2e suite in
* `AUTH_E2E_MEMORY` swaps the Postgres adapter for an in-memory one. One caller sets it, and it
* owns the whole server process and throws its store away when it exits: the Playwright preview
* server (`start:playwright:webserver`, see `playwright.config.ts`), so the e2e suite in
* `test/e2e/test-utils.ts` can sign up and sign in its own throwaway account through the real
* `/api/auth/**` endpoints without a live database; and `dev:solo` (`.env.solo`), so the
* dashboard starts from a fresh clone without one either. Both stay off the network and off mutable
* `/api/auth/**` endpoints without a live database. It stays off the network and off mutable
* external state. `AUTH_DATABASE_URL` still has to resolve to build `options` above, but nothing
* ever queries it once `database` is overridden here.
*
* Running the dashboard itself is not one of those callers: `aube run dev` and a deployment alike
* need Postgres, and there is no env file in the repository that turns this on.
*
* Deliberately not guarded by `NODE_ENV`: `nuxt preview` — the command this app's own e2e suite
* runs, per `start:playwright:webserver` above — sets `NODE_ENV=production` whenever it isn't
* already set (`@nuxt/cli`'s `preview` command), identically to a real deployment's built output.
* A `NODE_ENV === 'production'` check would therefore reject every e2e run, not just a leaked
* flag. Keep this variable out of any shared `.env`/CI template that a real deployment also reads —
* `.env.solo` is not one: `nuxt` loads it only when a command names it with `--dotenv`, which is
* how `dev:solo` alone reaches it.
* flag. Keep this variable out of every `.env`/CI template a real deployment also reads;
* `playwright.config.ts` passes it to the one server it starts and nowhere else.
*/
export default defineServerAuth(
process.env.AUTH_E2E_MEMORY === 'true'
Expand Down
50 changes: 12 additions & 38 deletions apps/dashboard/server/utils/repositories.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { resolve } from 'node:path';

import {
deleteRepository,
isAllowedCheckout,
Expand All @@ -16,10 +14,10 @@ import { database } from './database.js';
/**
* The repositories this deployment may act on.
*
* A contract rather than the Drizzle functions directly, so the process can run on an in-memory
* store when it has no database — the same shape `server/auth.config.ts` takes for the session
* store, and for the same reason: `dev:solo` and the Playwright preview both own the whole process
* and throw its state away when they exit.
* A contract rather than the Drizzle functions directly, so the Playwright preview server can run
* on an in-memory store — the same shape `server/auth.config.ts` takes for the session store, and
* for the same reason: that process owns its whole state and throws it away when it exits. Every
* other process, a deployment and `aube run dev` alike, reads Postgres.
*/
export interface RepositoryStore {
list(): Promise<RepositoryRecord[]>;
Expand All @@ -43,18 +41,13 @@ function postgresRepositoryStore(): RepositoryStore {
}

/**
* The in-memory stand-in, for a process running without a database.
*
* Seeded from `CODE_ZERO_SOLO_REPOSITORIES` because the store starts empty on every boot and the
* procedures that would fill it require an administrator, which a freshly created throwaway
* account is not. That variable is read here and nowhere else: it configures a fixture, not a
* deployment, which is why the three variables this table replaced are gone rather than joined by
* a fourth.
* The in-memory stand-in, for the Playwright preview server, which runs without a database.
*
* Entries are `owner/name=/path` or bare `/path`; the first form is watched, the second is only
* allow-listed. A malformed entry is dropped, the same way the poller's own configuration was.
* Starts empty and stays in the process: the e2e suite creates whatever it needs through the same
* procedures an operator uses, and nothing seeds it from the environment, because a store that
* outlives no process is a fixture rather than a deployment's configuration.
*/
export function memoryRepositoryStore(seed: string | undefined): RepositoryStore {
export function memoryRepositoryStore(): RepositoryStore {
const records = new Map<string, RepositoryRecord>();
let sequence = 0;

Expand Down Expand Up @@ -85,24 +78,6 @@ export function memoryRepositoryStore(seed: string | undefined): RepositoryStore
return record;
};

for (const entry of (seed ?? '').split(',')) {
const trimmed = entry.trim();
if (trimmed === '') continue;
const [slug, checkoutPath] = trimmed.includes('=')
? trimmed.split('=', 2).map((part) => part.trim())
: [undefined, trimmed];
if (!checkoutPath) continue;
const [owner, name] = (slug ?? '').split('/', 2).map((part) => part.trim());
put({
// Resolved the same way `mayTargetRepository` resolves the path a task creation names
// (`context.ts`) and `repositories.save` resolves an operator-supplied one (`router.ts`):
// a relative or trailing-slash entry here must still string-equal what a task creation
// compares it against, or every task creation against it is refused as not allow-listed.
checkoutPath: resolve(checkoutPath),
...(owner && name ? { owner, name, pollEnabled: true } : {}),
});
}

return {
list: () => Promise.resolve([...records.values()]),
watched: () =>
Expand All @@ -123,9 +98,8 @@ export function memoryRepositoryStore(seed: string | undefined): RepositoryStore
* The store this process uses.
*
* `AUTH_E2E_MEMORY` selects the in-memory one, the same flag `server/auth.config.ts` reads: it
* marks a process whose stores live and die with it, which is true of both stores or neither.
* marks a process whose stores live and die with it, which is true of both stores or neither. Only
* `playwright.config.ts` sets it, so everything else here talks to Postgres.
*/
export const repositoryStore: RepositoryStore =
process.env.AUTH_E2E_MEMORY === 'true'
? memoryRepositoryStore(process.env.CODE_ZERO_SOLO_REPOSITORIES)
: postgresRepositoryStore();
process.env.AUTH_E2E_MEMORY === 'true' ? memoryRepositoryStore() : postgresRepositoryStore();
29 changes: 25 additions & 4 deletions apps/dashboard/test/unit/repositories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,18 @@ import { memoryRepositoryStore } from '../../server/utils/repositories.js';
const CHECKOUT = resolve('/srv/widget');

describe('memoryRepositoryStore', () => {
it('watches a seeded `owner/name=/path` entry and only allow-lists a bare path', async () => {
const store = memoryRepositoryStore(`acme/widget=${CHECKOUT},/srv/other`);
it('starts empty, so nothing is allow-listed until it is saved', async () => {
const store = memoryRepositoryStore();

expect(await store.list()).toEqual([]);
expect(await store.allows(CHECKOUT)).toBe(false);
});

it('watches a saved repository with coordinates and polling on, and only allow-lists a bare path', async () => {
const store = memoryRepositoryStore();

await store.save({ owner: 'acme', name: 'widget', checkoutPath: CHECKOUT, pollEnabled: true });
await store.save({ checkoutPath: resolve('/srv/other') });

expect(await store.allows(CHECKOUT)).toBe(true);
expect(await store.allows(resolve('/srv/other'))).toBe(true);
Expand All @@ -18,7 +28,8 @@ describe('memoryRepositoryStore', () => {
});

it('keeps the fields a save leaves out, the way the Postgres upsert does', async () => {
const store = memoryRepositoryStore(`acme/widget=${CHECKOUT}`);
const store = memoryRepositoryStore();
await store.save({ owner: 'acme', name: 'widget', checkoutPath: CHECKOUT, pollEnabled: true });

const saved = await store.save({ checkoutPath: CHECKOUT });

Expand All @@ -36,7 +47,8 @@ describe('memoryRepositoryStore', () => {
});

it('writes the fields a save does name, and defaults them for a repository it has never seen', async () => {
const store = memoryRepositoryStore(`acme/widget=${CHECKOUT}`);
const store = memoryRepositoryStore();
await store.save({ owner: 'acme', name: 'widget', checkoutPath: CHECKOUT, pollEnabled: true });

const updated = await store.save({
checkoutPath: CHECKOUT,
Expand All @@ -54,4 +66,13 @@ describe('memoryRepositoryStore', () => {
pollEnabled: false,
});
});

it('removes a record by id', async () => {
const store = memoryRepositoryStore();
const saved = await store.save({ checkoutPath: CHECKOUT });

expect(await store.remove(saved.id)).toBe(true);
expect(await store.list()).toEqual([]);
expect(await store.remove(saved.id)).toBe(false);
});
});
11 changes: 10 additions & 1 deletion docs/PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ Fasi 0-5 eseguite. Cosa è cambiato rispetto al piano, e perché:
Trovato strada facendo: l'allow-list dei repository esisteva solo se erano configurati anche i
token operatore, quindi un deployment con sole sessioni non poteva creare nessun task. Corretto.

## Aggiornamento al 2026-09-12

`dev:solo` e `apps/dashboard/.env.solo` sono stati rimossi, insieme a
`code-zero.deployment.solo.yml` e a `CODE_ZERO_SOLO_REPOSITORIES`: la dashboard richiede Postgres,
come ogni deployment, e non esiste più un avvio senza database. L'adattatore Better Auth in memoria
resta, ma lo accende solo il server di anteprima di Playwright (`playwright.config.ts`), e lo store
dei repository in memoria parte vuoto. Le righe sopra che citano `dev:solo` restano come cronaca
della Fase 0, non come istruzioni.

## Cosa resta

| Cosa | Perché non è stato fatto |
Expand All @@ -160,7 +169,7 @@ token operatore, quindi un deployment con sole sessioni non poteva creare nessun

## Rischi

- **Run lunghi dentro `nuxt dev`**: HMR riavvia Nitro e uccide il run. Mitigazione: `dev:solo` in
- **Run lunghi dentro `nuxt dev`**: HMR riavvia Nitro e uccide il run. Mitigazione: `aube run dev` in
`observe`, run veri solo su `.output/` o con `nuxt dev --no-fork`.
- **KV `fs-lite` senza scrittura atomica**: `list()` legge tutte le chiavi a ogni overview. Va bene
fino a qualche migliaio di task; poi Postgres (già in repo per l'auth).
Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
"dev": "turbo run dev",
"dev:docs": "turbo run dev --filter=@code-zero/docs",
"dev:marketing": "turbo run dev --filter=@code-zero/marketing",
"dev:solo": "turbo run dev:solo --filter=@code-zero/dashboard",
"format": "oxfmt -c tooling/oxc/.oxfmtrc.json --ignore-path .oxfmtignore .",
"format:check": "oxfmt -c tooling/oxc/.oxfmtrc.json --ignore-path .oxfmtignore --check .",
"i18n:report": "turbo run i18n:report",
Expand Down
8 changes: 0 additions & 8 deletions turbo.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -197,14 +197,6 @@
"cache": false,
"persistent": true,
},
// The dashboard on its own, reading apps/dashboard/.env.solo instead of .env: no database, no
// model credentials, no other app in the graph. Same task shape as `dev`, since it is the same
// command with a different env file.
"dev:solo": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true,
},
"clean": {
"cache": false,
},
Expand Down
Loading