Replace PGlite with embedded-postgres and add startup banner
Switch from PGlite (WebAssembly) to embedded-postgres for zero-config local development — provides a real PostgreSQL server with full compatibility. Add startup banner with config summary on server boot. Improve server bootstrap with auto port detection, database creation, and migration on startup. Update DATABASE.md, DEVELOPING.md, and SPEC-implementation.md to reflect the change. Update CLI database check and prompts. Simplify OnboardingWizard database options. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -35,23 +35,32 @@ export async function databaseCheck(config: PaperclipConfig): Promise<CheckResul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// PGlite mode — check data dir
|
if (config.database.mode === "embedded-postgres") {
|
||||||
const dataDir = path.resolve(config.database.pgliteDataDir);
|
const dataDir = path.resolve(config.database.embeddedPostgresDataDir);
|
||||||
if (!fs.existsSync(dataDir)) {
|
if (!fs.existsSync(dataDir)) {
|
||||||
|
return {
|
||||||
|
name: "Database",
|
||||||
|
status: "warn",
|
||||||
|
message: `Embedded PostgreSQL data directory does not exist: ${dataDir}`,
|
||||||
|
canRepair: true,
|
||||||
|
repair: () => {
|
||||||
|
fs.mkdirSync(dataDir, { recursive: true });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "Database",
|
name: "Database",
|
||||||
status: "warn",
|
status: "pass",
|
||||||
message: `PGlite data directory does not exist: ${dataDir}`,
|
message: `Embedded PostgreSQL configured at ${dataDir} (port ${config.database.embeddedPostgresPort})`,
|
||||||
canRepair: true,
|
|
||||||
repair: () => {
|
|
||||||
fs.mkdirSync(dataDir, { recursive: true });
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: "Database",
|
name: "Database",
|
||||||
status: "pass",
|
status: "fail",
|
||||||
message: `PGlite data directory exists: ${dataDir}`,
|
message: `Unknown database mode: ${String(config.database.mode)}`,
|
||||||
|
canRepair: false,
|
||||||
|
repairHint: "Run `paperclip configure --section database`",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export async function promptDatabase(): Promise<DatabaseConfig> {
|
|||||||
const mode = await p.select({
|
const mode = await p.select({
|
||||||
message: "Database mode",
|
message: "Database mode",
|
||||||
options: [
|
options: [
|
||||||
{ value: "pglite" as const, label: "PGlite (embedded, no setup needed)", hint: "recommended" },
|
{ value: "embedded-postgres" as const, label: "Embedded PostgreSQL (managed locally)", hint: "recommended" },
|
||||||
{ value: "postgres" as const, label: "PostgreSQL (external server)" },
|
{ value: "postgres" as const, label: "PostgreSQL (external server)" },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -30,19 +30,43 @@ export async function promptDatabase(): Promise<DatabaseConfig> {
|
|||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mode: "postgres", connectionString, pgliteDataDir: "./data/pglite" };
|
return {
|
||||||
|
mode: "postgres",
|
||||||
|
connectionString,
|
||||||
|
embeddedPostgresDataDir: "./data/embedded-postgres",
|
||||||
|
embeddedPostgresPort: 54329,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const pgliteDataDir = await p.text({
|
const embeddedPostgresDataDir = await p.text({
|
||||||
message: "PGlite data directory",
|
message: "Embedded PostgreSQL data directory",
|
||||||
defaultValue: "./data/pglite",
|
defaultValue: "./data/embedded-postgres",
|
||||||
placeholder: "./data/pglite",
|
placeholder: "./data/embedded-postgres",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (p.isCancel(pgliteDataDir)) {
|
if (p.isCancel(embeddedPostgresDataDir)) {
|
||||||
p.cancel("Setup cancelled.");
|
p.cancel("Setup cancelled.");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { mode: "pglite", pgliteDataDir: pgliteDataDir || "./data/pglite" };
|
const embeddedPostgresPort = await p.text({
|
||||||
|
message: "Embedded PostgreSQL port",
|
||||||
|
defaultValue: "54329",
|
||||||
|
placeholder: "54329",
|
||||||
|
validate: (val) => {
|
||||||
|
const n = Number(val);
|
||||||
|
if (!Number.isInteger(n) || n < 1 || n > 65535) return "Port must be an integer between 1 and 65535";
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (p.isCancel(embeddedPostgresPort)) {
|
||||||
|
p.cancel("Setup cancelled.");
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
mode: "embedded-postgres",
|
||||||
|
embeddedPostgresDataDir: embeddedPostgresDataDir || "./data/embedded-postgres",
|
||||||
|
embeddedPostgresPort: Number(embeddedPostgresPort || "54329"),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
Paperclip uses PostgreSQL via [Drizzle ORM](https://orm.drizzle.team/). There are three ways to run the database, from simplest to most production-ready.
|
Paperclip uses PostgreSQL via [Drizzle ORM](https://orm.drizzle.team/). There are three ways to run the database, from simplest to most production-ready.
|
||||||
|
|
||||||
## 1. Embedded (PGlite) — zero config
|
## 1. Embedded PostgreSQL — zero config
|
||||||
|
|
||||||
If you don't set `DATABASE_URL`, the server automatically starts an embedded PostgreSQL instance powered by [PGlite](https://pglite.dev/). No installation, no Docker, no setup.
|
If you don't set `DATABASE_URL`, the server automatically starts an embedded PostgreSQL instance and manages a local data directory.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm dev
|
pnpm dev
|
||||||
@@ -12,19 +12,14 @@ pnpm dev
|
|||||||
|
|
||||||
That's it. On first start the server:
|
That's it. On first start the server:
|
||||||
|
|
||||||
1. Creates a `./data/pglite/` directory for storage
|
1. Creates a `./server/data/embedded-postgres/` directory for storage
|
||||||
2. Pushes the Drizzle schema to create all tables
|
2. Ensures the `paperclip` database exists
|
||||||
3. Starts serving requests
|
3. Runs migrations automatically for empty databases
|
||||||
|
4. Starts serving requests
|
||||||
|
|
||||||
Data persists across restarts in the `./data/pglite/` directory. To reset the database, delete that directory.
|
Data persists across restarts in `./server/data/embedded-postgres/`. To reset local dev data, delete that directory.
|
||||||
|
|
||||||
**Limitations:**
|
This mode is ideal for local development and one-command installs.
|
||||||
|
|
||||||
- Single connection only (no concurrent access from multiple processes)
|
|
||||||
- Slower than native PostgreSQL (runs in WebAssembly)
|
|
||||||
- Not suitable for production
|
|
||||||
|
|
||||||
This mode is ideal for getting started, local development, and demos.
|
|
||||||
|
|
||||||
## 2. Local PostgreSQL (Docker)
|
## 2. Local PostgreSQL (Docker)
|
||||||
|
|
||||||
@@ -116,11 +111,11 @@ See [Supabase pricing](https://supabase.com/pricing) for current details.
|
|||||||
|
|
||||||
## Switching between modes
|
## Switching between modes
|
||||||
|
|
||||||
The database mode is controlled entirely by the `DATABASE_URL` environment variable:
|
The database mode is controlled by `DATABASE_URL`:
|
||||||
|
|
||||||
| `DATABASE_URL` | Mode |
|
| `DATABASE_URL` | Mode |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Not set | Embedded PGlite (`./data/pglite/`) |
|
| Not set | Embedded PostgreSQL (`./server/data/embedded-postgres/`) |
|
||||||
| `postgres://...localhost...` | Local Docker PostgreSQL |
|
| `postgres://...localhost...` | Local Docker PostgreSQL |
|
||||||
| `postgres://...supabase.com...` | Hosted Supabase |
|
| `postgres://...supabase.com...` | Hosted Supabase |
|
||||||
|
|
||||||
|
|||||||
@@ -19,14 +19,14 @@ pnpm dev
|
|||||||
This starts:
|
This starts:
|
||||||
|
|
||||||
- API server: `http://localhost:3100`
|
- API server: `http://localhost:3100`
|
||||||
- UI: `http://localhost:5173`
|
- UI: served by the API server in dev middleware mode (same origin as API)
|
||||||
|
|
||||||
## Database in Dev (Auto-Handled)
|
## Database in Dev (Auto-Handled)
|
||||||
|
|
||||||
For local development, leave `DATABASE_URL` unset.
|
For local development, leave `DATABASE_URL` unset.
|
||||||
The server will automatically use embedded PGlite and persist data at:
|
The server will automatically use embedded PostgreSQL and persist data at:
|
||||||
|
|
||||||
- `./data/pglite`
|
- `./data/embedded-postgres`
|
||||||
|
|
||||||
No Docker or external database is required for this mode.
|
No Docker or external database is required for this mode.
|
||||||
|
|
||||||
@@ -49,10 +49,10 @@ Expected:
|
|||||||
To wipe local dev data and start fresh:
|
To wipe local dev data and start fresh:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
rm -rf data/pglite
|
rm -rf server/data/embedded-postgres
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## Optional: Use External Postgres
|
## Optional: Use External Postgres
|
||||||
|
|
||||||
If you set `DATABASE_URL`, the server will use that instead of embedded PGlite.
|
If you set `DATABASE_URL`, the server will use that instead of embedded PostgreSQL.
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ These decisions close open questions from `SPEC.md` for V1.
|
|||||||
| Auth | Session auth for board, API keys for agents |
|
| Auth | Session auth for board, API keys for agents |
|
||||||
| Budget period | Monthly UTC calendar window |
|
| Budget period | Monthly UTC calendar window |
|
||||||
| Budget enforcement | Soft alerts + hard limit auto-pause |
|
| Budget enforcement | Soft alerts + hard limit auto-pause |
|
||||||
| Deployment modes | Embedded PGlite default; Docker/hosted Postgres supported |
|
| Deployment modes | Embedded PostgreSQL default; Docker/hosted Postgres supported |
|
||||||
|
|
||||||
## 4. Current Baseline (Repo Snapshot)
|
## 4. Current Baseline (Repo Snapshot)
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ As of 2026-02-17, the repo already includes:
|
|||||||
|
|
||||||
- Node + TypeScript backend with REST CRUD for `agents`, `projects`, `goals`, `issues`, `activity`
|
- Node + TypeScript backend with REST CRUD for `agents`, `projects`, `goals`, `issues`, `activity`
|
||||||
- React UI pages for dashboard/agents/projects/goals/issues lists
|
- React UI pages for dashboard/agents/projects/goals/issues lists
|
||||||
- PostgreSQL schema via Drizzle with embedded PGlite fallback when `DATABASE_URL` is unset
|
- PostgreSQL schema via Drizzle with embedded PostgreSQL fallback when `DATABASE_URL` is unset
|
||||||
|
|
||||||
V1 implementation extends this baseline into a company-centric, governance-aware control plane.
|
V1 implementation extends this baseline into a company-centric, governance-aware control plane.
|
||||||
|
|
||||||
@@ -86,13 +86,13 @@ V1 implementation extends this baseline into a company-centric, governance-aware
|
|||||||
|
|
||||||
- `server/`: REST API, auth, orchestration services
|
- `server/`: REST API, auth, orchestration services
|
||||||
- `ui/`: Board operator interface
|
- `ui/`: Board operator interface
|
||||||
- `packages/db/`: Drizzle schema, migrations, DB clients (Postgres and PGlite)
|
- `packages/db/`: Drizzle schema, migrations, DB clients (Postgres)
|
||||||
- `packages/shared/`: Shared API types, validators, constants
|
- `packages/shared/`: Shared API types, validators, constants
|
||||||
|
|
||||||
## 6.2 Data Stores
|
## 6.2 Data Stores
|
||||||
|
|
||||||
- Primary: PostgreSQL
|
- Primary: PostgreSQL
|
||||||
- Local default: embedded PGlite at `./data/pglite`
|
- Local default: embedded PostgreSQL at `./server/data/embedded-postgres`
|
||||||
- Optional local prod-like: Docker Postgres
|
- Optional local prod-like: Docker Postgres
|
||||||
- Optional hosted: Supabase/Postgres-compatible
|
- Optional hosted: Supabase/Postgres-compatible
|
||||||
|
|
||||||
@@ -754,7 +754,7 @@ V1 is complete only when all criteria are true:
|
|||||||
6. Budget hard limit auto-pauses an agent and prevents new invocations.
|
6. Budget hard limit auto-pauses an agent and prevents new invocations.
|
||||||
7. Dashboard shows accurate counts/spend from live DB data.
|
7. Dashboard shows accurate counts/spend from live DB data.
|
||||||
8. Every mutation is auditable in activity log.
|
8. Every mutation is auditable in activity log.
|
||||||
9. App runs with embedded PGlite by default and with external Postgres via `DATABASE_URL`.
|
9. App runs with embedded PostgreSQL by default and with external Postgres via `DATABASE_URL`.
|
||||||
|
|
||||||
## 20. Post-V1 Backlog (Explicitly Deferred)
|
## 20. Post-V1 Backlog (Explicitly Deferred)
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "pnpm --parallel --filter @paperclip/server --filter @paperclip/ui dev",
|
"dev": "PAPERCLIP_UI_DEV_MIDDLEWARE=true pnpm --filter @paperclip/server dev",
|
||||||
"dev:server": "pnpm --filter @paperclip/server dev",
|
"dev:server": "pnpm --filter @paperclip/server dev",
|
||||||
"dev:ui": "pnpm --filter @paperclip/ui dev",
|
"dev:ui": "pnpm --filter @paperclip/ui dev",
|
||||||
"build": "pnpm -r build",
|
"build": "pnpm -r build",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"seed": "tsx src/seed.ts"
|
"seed": "tsx src/seed.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electric-sql/pglite": "^0.3.15",
|
|
||||||
"@paperclip/shared": "workspace:*",
|
"@paperclip/shared": "workspace:*",
|
||||||
"drizzle-orm": "^0.38.4",
|
"drizzle-orm": "^0.38.4",
|
||||||
"postgres": "^3.4.5"
|
"postgres": "^3.4.5"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { mkdirSync } from "node:fs";
|
|
||||||
import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
|
import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
|
||||||
import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
|
import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import { PGlite } from "@electric-sql/pglite";
|
|
||||||
import * as schema from "./schema/index.js";
|
import * as schema from "./schema/index.js";
|
||||||
|
|
||||||
export function createDb(url: string) {
|
export function createDb(url: string) {
|
||||||
@@ -10,17 +8,66 @@ export function createDb(url: string) {
|
|||||||
return drizzlePg(sql, { schema });
|
return drizzlePg(sql, { schema });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createPgliteDb(dataDir: string) {
|
export type MigrationBootstrapResult =
|
||||||
mkdirSync(dataDir, { recursive: true });
|
| { migrated: true; reason: "migrated-empty-db"; tableCount: 0 }
|
||||||
const client = new PGlite(dataDir);
|
| { migrated: false; reason: "already-migrated"; tableCount: number }
|
||||||
const db = drizzlePglite({ client, schema });
|
| { migrated: false; reason: "not-empty-no-migration-journal"; tableCount: number };
|
||||||
|
|
||||||
// Auto-push schema to PGlite on startup (like drizzle-kit push)
|
export async function migratePostgresIfEmpty(url: string): Promise<MigrationBootstrapResult> {
|
||||||
const { pushSchema } = await import("drizzle-kit/api");
|
const sql = postgres(url, { max: 1 });
|
||||||
const { apply } = await pushSchema(schema, db as any);
|
|
||||||
await apply();
|
|
||||||
|
|
||||||
return db;
|
try {
|
||||||
|
const journal = await sql<{ regclass: string | null }[]>`
|
||||||
|
select to_regclass('public.__drizzle_migrations') as regclass
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tableCountResult = await sql<{ count: number }[]>`
|
||||||
|
select count(*)::int as count
|
||||||
|
from information_schema.tables
|
||||||
|
where table_schema = 'public'
|
||||||
|
and table_type = 'BASE TABLE'
|
||||||
|
`;
|
||||||
|
|
||||||
|
const tableCount = tableCountResult[0]?.count ?? 0;
|
||||||
|
|
||||||
|
if (journal[0]?.regclass) {
|
||||||
|
return { migrated: false, reason: "already-migrated", tableCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableCount > 0) {
|
||||||
|
return { migrated: false, reason: "not-empty-no-migration-journal", tableCount };
|
||||||
|
}
|
||||||
|
|
||||||
|
const db = drizzlePg(sql);
|
||||||
|
const migrationsFolder = new URL("./migrations", import.meta.url).pathname;
|
||||||
|
await migratePg(db, { migrationsFolder });
|
||||||
|
|
||||||
|
return { migrated: true, reason: "migrated-empty-db", tableCount: 0 };
|
||||||
|
} finally {
|
||||||
|
await sql.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensurePostgresDatabase(
|
||||||
|
url: string,
|
||||||
|
databaseName: string,
|
||||||
|
): Promise<"created" | "exists"> {
|
||||||
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(databaseName)) {
|
||||||
|
throw new Error(`Unsafe database name: ${databaseName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = postgres(url, { max: 1 });
|
||||||
|
try {
|
||||||
|
const existing = await sql<{ one: number }[]>`
|
||||||
|
select 1 as one from pg_database where datname = ${databaseName} limit 1
|
||||||
|
`;
|
||||||
|
if (existing.length > 0) return "exists";
|
||||||
|
|
||||||
|
await sql.unsafe(`create database "${databaseName}"`);
|
||||||
|
return "created";
|
||||||
|
} finally {
|
||||||
|
await sql.end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Db = ReturnType<typeof createDb>;
|
export type Db = ReturnType<typeof createDb>;
|
||||||
|
|||||||
@@ -1,2 +1,8 @@
|
|||||||
export { createDb, createPgliteDb, type Db } from "./client.js";
|
export {
|
||||||
|
createDb,
|
||||||
|
ensurePostgresDatabase,
|
||||||
|
migratePostgresIfEmpty,
|
||||||
|
type MigrationBootstrapResult,
|
||||||
|
type Db,
|
||||||
|
} from "./client.js";
|
||||||
export * from "./schema/index.js";
|
export * from "./schema/index.js";
|
||||||
|
|||||||
@@ -1,23 +1,17 @@
|
|||||||
import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
|
import { migrate as migratePg } from "drizzle-orm/postgres-js/migrator";
|
||||||
import { migrate as migratePglite } from "drizzle-orm/pglite/migrator";
|
|
||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import { PGlite } from "@electric-sql/pglite";
|
|
||||||
import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
|
import { drizzle as drizzlePg } from "drizzle-orm/postgres-js";
|
||||||
import { drizzle as drizzlePglite } from "drizzle-orm/pglite";
|
|
||||||
|
|
||||||
const migrationsFolder = new URL("./migrations", import.meta.url).pathname;
|
const migrationsFolder = new URL("./migrations", import.meta.url).pathname;
|
||||||
const url = process.env.DATABASE_URL;
|
const url = process.env.DATABASE_URL;
|
||||||
|
|
||||||
if (url) {
|
if (!url) {
|
||||||
const sql = postgres(url, { max: 1 });
|
throw new Error("DATABASE_URL is required for db:migrate");
|
||||||
const db = drizzlePg(sql);
|
|
||||||
await migratePg(db, { migrationsFolder });
|
|
||||||
await sql.end();
|
|
||||||
} else {
|
|
||||||
const client = new PGlite("./data/pglite");
|
|
||||||
const db = drizzlePglite({ client });
|
|
||||||
await migratePglite(db, { migrationsFolder });
|
|
||||||
await client.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sql = postgres(url, { max: 1 });
|
||||||
|
const db = drizzlePg(sql);
|
||||||
|
await migratePg(db, { migrationsFolder });
|
||||||
|
await sql.end();
|
||||||
|
|
||||||
console.log("Migrations complete");
|
console.log("Migrations complete");
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ export const llmConfigSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const databaseConfigSchema = z.object({
|
export const databaseConfigSchema = z.object({
|
||||||
mode: z.enum(["pglite", "postgres"]),
|
mode: z.enum(["embedded-postgres", "postgres"]).default("embedded-postgres"),
|
||||||
connectionString: z.string().optional(),
|
connectionString: z.string().optional(),
|
||||||
pgliteDataDir: z.string().default("./data/pglite"),
|
embeddedPostgresDataDir: z.string().default("./data/embedded-postgres"),
|
||||||
|
embeddedPostgresPort: z.number().int().min(1).max(65535).default(54329),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const loggingConfigSchema = z.object({
|
export const loggingConfigSchema = z.object({
|
||||||
|
|||||||
251
pnpm-lock.yaml
generated
251
pnpm-lock.yaml
generated
@@ -45,15 +45,12 @@ importers:
|
|||||||
|
|
||||||
packages/db:
|
packages/db:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@electric-sql/pglite':
|
|
||||||
specifier: ^0.3.15
|
|
||||||
version: 0.3.15
|
|
||||||
'@paperclip/shared':
|
'@paperclip/shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../shared
|
version: link:../shared
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.38.4
|
specifier: ^0.38.4
|
||||||
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4)
|
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
|
||||||
postgres:
|
postgres:
|
||||||
specifier: ^3.4.5
|
specifier: ^3.4.5
|
||||||
version: 3.4.8
|
version: 3.4.8
|
||||||
@@ -89,9 +86,12 @@ importers:
|
|||||||
'@paperclip/shared':
|
'@paperclip/shared':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../packages/shared
|
version: link:../packages/shared
|
||||||
|
detect-port:
|
||||||
|
specifier: ^2.1.0
|
||||||
|
version: 2.1.0
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.38.4
|
specifier: ^0.38.4
|
||||||
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4)
|
version: 0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4)
|
||||||
express:
|
express:
|
||||||
specifier: ^5.1.0
|
specifier: ^5.1.0
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
@@ -107,6 +107,10 @@ importers:
|
|||||||
zod:
|
zod:
|
||||||
specifier: ^3.24.2
|
specifier: ^3.24.2
|
||||||
version: 3.25.76
|
version: 3.25.76
|
||||||
|
optionalDependencies:
|
||||||
|
embedded-postgres:
|
||||||
|
specifier: ^18.1.0-beta.16
|
||||||
|
version: 18.1.0-beta.16
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
@@ -126,6 +130,9 @@ importers:
|
|||||||
typescript:
|
typescript:
|
||||||
specifier: ^5.7.3
|
specifier: ^5.7.3
|
||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
|
vite:
|
||||||
|
specifier: ^6.1.0
|
||||||
|
version: 6.4.1(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.0.5
|
specifier: ^3.0.5
|
||||||
version: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)
|
version: 3.2.4(@types/node@25.2.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)
|
||||||
@@ -294,6 +301,54 @@ packages:
|
|||||||
'@electric-sql/pglite@0.3.15':
|
'@electric-sql/pglite@0.3.15':
|
||||||
resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==}
|
resolution: {integrity: sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==}
|
||||||
|
|
||||||
|
'@embedded-postgres/darwin-arm64@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-tU/syLOamFZdXMC+p7AYczmFKIiolFlZ8y3Qb7KonX37O07ezc/OSDiQ641sheV3X0WPf9V10qyK8c81rleDdw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@embedded-postgres/darwin-x64@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-4zHNCscGJt/3pmkpLCuU/IpMJzwENM6OqSHE+WWkOoNqYid49ZnmgB1ltOelgZgRoPRIy/HDEnrMeuVxQHBhEw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-arm64@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-G0f/reVFc7svqncDQL7blwKulzYIYsz+o/3TEtAJOaGMXYkD8Swzv9RFKfEJOr9+IVRwCmoFFppMeBnUwMoGZg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-arm@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-aB1t95YGnqay8swh70s7zo587Nog90UL9yUYrzMMNMq40Qfrq9aWiBn0+6vCxp1rSFaPyJMPvW62/rIKLFBkKg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-ia32@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-gMTIryUMnwyLancs34gXqaiuXIFMgn8RGYZ2wJZEuXpW0SQ/cE07kFINmoQO1sN+5T/IR0KvMXPzxo867wO3ew==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-ppc64@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-wTglX0bZVBretiUJrZUO/EmEP8w7jC+i8ZAEKesHHIqIDXEV2F9l+6aTjWt2wRu5SAJ6gpe2RbcKvJ6x+6m1Qw==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-x64@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-+GIabpHh7QV2AcYBuzyQC41AYczSFphxfHy4ccTPIPrp6OSthZXH+A9fymjQzOiDHg9+1UeYET00Aj7sScjXrg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@embedded-postgres/windows-x64@18.1.0-beta.16':
|
||||||
|
resolution: {integrity: sha512-v6AXH1zi6YyoqPM6U7mX08prJ33yD9gqsbo3YdtPi8FDx0C/y9sYa3aQVf/3blPJvHyERYbY8fZnYXbb79Lo0Q==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
|
||||||
deprecated: 'Merged into tsx: https://tsx.is'
|
deprecated: 'Merged into tsx: https://tsx.is'
|
||||||
@@ -1825,6 +1880,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
|
address@2.0.3:
|
||||||
|
resolution: {integrity: sha512-XNAb/a6TCqou+TufU8/u11HCu9x1gYvOoxLwtlXgIqmkrYQADVv6ljyW2zwiPhHz9R1gItAWpuDrdJMmrOBFEA==}
|
||||||
|
engines: {node: '>= 16.0.0'}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1836,6 +1895,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
async-exit-hook@2.0.1:
|
||||||
|
resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==}
|
||||||
|
engines: {node: '>=0.12.0'}
|
||||||
|
|
||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
@@ -1967,6 +2030,11 @@ packages:
|
|||||||
detect-node-es@1.1.0:
|
detect-node-es@1.1.0:
|
||||||
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
|
||||||
|
|
||||||
|
detect-port@2.1.0:
|
||||||
|
resolution: {integrity: sha512-epZuWb/6Q62L+nDHJc/hQAqf8pylsqgk3BpZXVBx1CDnr3nkrVNn73Uu1rXcFzkNcc+hkP3whuOg7JZYaQB65Q==}
|
||||||
|
engines: {node: '>= 16.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
dezalgo@1.0.4:
|
dezalgo@1.0.4:
|
||||||
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
resolution: {integrity: sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==}
|
||||||
|
|
||||||
@@ -2076,6 +2144,10 @@ packages:
|
|||||||
electron-to-chromium@1.5.286:
|
electron-to-chromium@1.5.286:
|
||||||
resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
|
resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==}
|
||||||
|
|
||||||
|
embedded-postgres@18.1.0-beta.16:
|
||||||
|
resolution: {integrity: sha512-TDp7Ld0h84x5fzIIZFyreYWqZrxUNjuXB6OxqJCmV6PodB2vzQ+1hlL6n4uK1de7bIAxYt5OkDykwcuIONQdQg==}
|
||||||
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
encodeurl@2.0.0:
|
encodeurl@2.0.0:
|
||||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@@ -2430,6 +2502,40 @@ packages:
|
|||||||
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==}
|
||||||
engines: {node: '>= 14.16'}
|
engines: {node: '>= 14.16'}
|
||||||
|
|
||||||
|
pg-cloudflare@1.3.0:
|
||||||
|
resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==}
|
||||||
|
|
||||||
|
pg-connection-string@2.11.0:
|
||||||
|
resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==}
|
||||||
|
|
||||||
|
pg-int8@1.0.1:
|
||||||
|
resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
|
pg-pool@3.11.0:
|
||||||
|
resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==}
|
||||||
|
peerDependencies:
|
||||||
|
pg: '>=8.0'
|
||||||
|
|
||||||
|
pg-protocol@1.11.0:
|
||||||
|
resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==}
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
pg@8.18.0:
|
||||||
|
resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==}
|
||||||
|
engines: {node: '>= 16.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
pg-native: '>=3.0.1'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
pg-native:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==}
|
||||||
|
|
||||||
picocolors@1.1.1:
|
picocolors@1.1.1:
|
||||||
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
|
||||||
|
|
||||||
@@ -2454,6 +2560,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
postgres-array@2.0.0:
|
||||||
|
resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
|
postgres-bytea@1.0.1:
|
||||||
|
resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-date@1.0.7:
|
||||||
|
resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
postgres@3.4.8:
|
postgres@3.4.8:
|
||||||
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
resolution: {integrity: sha512-d+JFcLM17njZaOLkv6SCev7uoLaBtfK86vMUXhW1Z4glPWh4jozno9APvW/XKFJ3CCxVoC7OL38BqRydtu5nGg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2892,6 +3014,10 @@ packages:
|
|||||||
utf-8-validate:
|
utf-8-validate:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
xtend@4.0.2:
|
||||||
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
|
engines: {node: '>=0.4'}
|
||||||
|
|
||||||
yallist@3.1.1:
|
yallist@3.1.1:
|
||||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||||
|
|
||||||
@@ -3025,7 +3151,32 @@ snapshots:
|
|||||||
|
|
||||||
'@drizzle-team/brocli@0.10.2': {}
|
'@drizzle-team/brocli@0.10.2': {}
|
||||||
|
|
||||||
'@electric-sql/pglite@0.3.15': {}
|
'@electric-sql/pglite@0.3.15':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/darwin-arm64@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/darwin-x64@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-arm64@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-arm@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-ia32@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-ppc64@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/linux-x64@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@embedded-postgres/windows-x64@18.1.0-beta.16':
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild-kit/core-utils@3.3.2':
|
'@esbuild-kit/core-utils@3.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -4367,6 +4518,8 @@ snapshots:
|
|||||||
mime-types: 3.0.2
|
mime-types: 3.0.2
|
||||||
negotiator: 1.0.0
|
negotiator: 1.0.0
|
||||||
|
|
||||||
|
address@2.0.3: {}
|
||||||
|
|
||||||
aria-hidden@1.2.6:
|
aria-hidden@1.2.6:
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
@@ -4375,6 +4528,9 @@ snapshots:
|
|||||||
|
|
||||||
assertion-error@2.0.1: {}
|
assertion-error@2.0.1: {}
|
||||||
|
|
||||||
|
async-exit-hook@2.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
atomic-sleep@1.0.0: {}
|
atomic-sleep@1.0.0: {}
|
||||||
@@ -4487,6 +4643,10 @@ snapshots:
|
|||||||
|
|
||||||
detect-node-es@1.1.0: {}
|
detect-node-es@1.1.0: {}
|
||||||
|
|
||||||
|
detect-port@2.1.0:
|
||||||
|
dependencies:
|
||||||
|
address: 2.0.3
|
||||||
|
|
||||||
dezalgo@1.0.4:
|
dezalgo@1.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
asap: 2.0.6
|
asap: 2.0.6
|
||||||
@@ -4501,10 +4661,11 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4):
|
drizzle-orm@0.38.4(@electric-sql/pglite@0.3.15)(@types/react@19.2.14)(pg@8.18.0)(postgres@3.4.8)(react@19.2.4):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@electric-sql/pglite': 0.3.15
|
'@electric-sql/pglite': 0.3.15
|
||||||
'@types/react': 19.2.14
|
'@types/react': 19.2.14
|
||||||
|
pg: 8.18.0
|
||||||
postgres: 3.4.8
|
postgres: 3.4.8
|
||||||
react: 19.2.4
|
react: 19.2.4
|
||||||
|
|
||||||
@@ -4518,6 +4679,23 @@ snapshots:
|
|||||||
|
|
||||||
electron-to-chromium@1.5.286: {}
|
electron-to-chromium@1.5.286: {}
|
||||||
|
|
||||||
|
embedded-postgres@18.1.0-beta.16:
|
||||||
|
dependencies:
|
||||||
|
async-exit-hook: 2.0.1
|
||||||
|
pg: 8.18.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@embedded-postgres/darwin-arm64': 18.1.0-beta.16
|
||||||
|
'@embedded-postgres/darwin-x64': 18.1.0-beta.16
|
||||||
|
'@embedded-postgres/linux-arm': 18.1.0-beta.16
|
||||||
|
'@embedded-postgres/linux-arm64': 18.1.0-beta.16
|
||||||
|
'@embedded-postgres/linux-ia32': 18.1.0-beta.16
|
||||||
|
'@embedded-postgres/linux-ppc64': 18.1.0-beta.16
|
||||||
|
'@embedded-postgres/linux-x64': 18.1.0-beta.16
|
||||||
|
'@embedded-postgres/windows-x64': 18.1.0-beta.16
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- pg-native
|
||||||
|
optional: true
|
||||||
|
|
||||||
encodeurl@2.0.0: {}
|
encodeurl@2.0.0: {}
|
||||||
|
|
||||||
enhanced-resolve@5.19.0:
|
enhanced-resolve@5.19.0:
|
||||||
@@ -4900,6 +5078,48 @@ snapshots:
|
|||||||
|
|
||||||
pathval@2.0.1: {}
|
pathval@2.0.1: {}
|
||||||
|
|
||||||
|
pg-cloudflare@1.3.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-connection-string@2.11.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-int8@1.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-pool@3.11.0(pg@8.18.0):
|
||||||
|
dependencies:
|
||||||
|
pg: 8.18.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-protocol@1.11.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg-types@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
pg-int8: 1.0.1
|
||||||
|
postgres-array: 2.0.0
|
||||||
|
postgres-bytea: 1.0.1
|
||||||
|
postgres-date: 1.0.7
|
||||||
|
postgres-interval: 1.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pg@8.18.0:
|
||||||
|
dependencies:
|
||||||
|
pg-connection-string: 2.11.0
|
||||||
|
pg-pool: 3.11.0(pg@8.18.0)
|
||||||
|
pg-protocol: 1.11.0
|
||||||
|
pg-types: 2.2.0
|
||||||
|
pgpass: 1.0.5
|
||||||
|
optionalDependencies:
|
||||||
|
pg-cloudflare: 1.3.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
pgpass@1.0.5:
|
||||||
|
dependencies:
|
||||||
|
split2: 4.2.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
picocolors@1.1.1: {}
|
picocolors@1.1.1: {}
|
||||||
|
|
||||||
picomatch@4.0.3: {}
|
picomatch@4.0.3: {}
|
||||||
@@ -4937,6 +5157,20 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
postgres-array@2.0.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
postgres-bytea@1.0.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
postgres-date@1.0.7:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
postgres-interval@1.2.0:
|
||||||
|
dependencies:
|
||||||
|
xtend: 4.0.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
postgres@3.4.8: {}
|
postgres@3.4.8: {}
|
||||||
|
|
||||||
process-warning@5.0.0: {}
|
process-warning@5.0.0: {}
|
||||||
@@ -5413,6 +5647,9 @@ snapshots:
|
|||||||
|
|
||||||
ws@8.19.0: {}
|
ws@8.19.0: {}
|
||||||
|
|
||||||
|
xtend@4.0.2:
|
||||||
|
optional: true
|
||||||
|
|
||||||
yallist@3.1.1: {}
|
yallist@3.1.1: {}
|
||||||
|
|
||||||
zod@3.25.76: {}
|
zod@3.25.76: {}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tsx watch src/index.ts",
|
"dev": "tsx watch --ignore ../ui/node_modules --ignore ../ui/.vite --ignore ../ui/dist src/index.ts",
|
||||||
"build": "tsc",
|
"build": "tsc",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@paperclip/db": "workspace:*",
|
"@paperclip/db": "workspace:*",
|
||||||
"@paperclip/shared": "workspace:*",
|
"@paperclip/shared": "workspace:*",
|
||||||
|
"detect-port": "^2.1.0",
|
||||||
"drizzle-orm": "^0.38.4",
|
"drizzle-orm": "^0.38.4",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
"pino": "^9.6.0",
|
"pino": "^9.6.0",
|
||||||
@@ -19,6 +20,9 @@
|
|||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"embedded-postgres": "^18.1.0-beta.16"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/express-serve-static-core": "^5.0.0",
|
"@types/express-serve-static-core": "^5.0.0",
|
||||||
@@ -26,6 +30,7 @@
|
|||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"tsx": "^4.19.2",
|
"tsx": "^4.19.2",
|
||||||
"typescript": "^5.7.3",
|
"typescript": "^5.7.3",
|
||||||
|
"vite": "^6.1.0",
|
||||||
"vitest": "^3.0.5"
|
"vitest": "^3.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import express, { Router } from "express";
|
import express, { Router } from "express";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import fs from "node:fs";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import type { Db } from "@paperclip/db";
|
import type { Db } from "@paperclip/db";
|
||||||
import { httpLogger, errorHandler } from "./middleware/index.js";
|
import { httpLogger, errorHandler } from "./middleware/index.js";
|
||||||
@@ -15,7 +16,9 @@ import { costRoutes } from "./routes/costs.js";
|
|||||||
import { activityRoutes } from "./routes/activity.js";
|
import { activityRoutes } from "./routes/activity.js";
|
||||||
import { dashboardRoutes } from "./routes/dashboard.js";
|
import { dashboardRoutes } from "./routes/dashboard.js";
|
||||||
|
|
||||||
export function createApp(db: Db, opts: { serveUi: boolean }) {
|
type UiMode = "none" | "static" | "vite-dev";
|
||||||
|
|
||||||
|
export async function createApp(db: Db, opts: { uiMode: UiMode }) {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
@@ -36,16 +39,40 @@ export function createApp(db: Db, opts: { serveUi: boolean }) {
|
|||||||
api.use(dashboardRoutes(db));
|
api.use(dashboardRoutes(db));
|
||||||
app.use("/api", api);
|
app.use("/api", api);
|
||||||
|
|
||||||
// SPA fallback for serving the UI build
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
if (opts.serveUi) {
|
if (opts.uiMode === "static") {
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
// Serve built UI from ui/dist in production.
|
||||||
const uiDist = path.resolve(__dirname, "../../ui/dist");
|
const uiDist = path.resolve(__dirname, "../../ui/dist");
|
||||||
app.use(express.static(uiDist));
|
app.use(express.static(uiDist));
|
||||||
app.get("*", (_req, res) => {
|
app.get(/.*/, (_req, res) => {
|
||||||
res.sendFile(path.join(uiDist, "index.html"));
|
res.sendFile(path.join(uiDist, "index.html"));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts.uiMode === "vite-dev") {
|
||||||
|
const uiRoot = path.resolve(__dirname, "../../ui");
|
||||||
|
const { createServer: createViteServer } = await import("vite");
|
||||||
|
const vite = await createViteServer({
|
||||||
|
root: uiRoot,
|
||||||
|
appType: "spa",
|
||||||
|
server: {
|
||||||
|
middlewareMode: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
app.use(vite.middlewares);
|
||||||
|
app.get(/.*/, async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const templatePath = path.resolve(uiRoot, "index.html");
|
||||||
|
const template = fs.readFileSync(templatePath, "utf-8");
|
||||||
|
const html = await vite.transformIndexHtml(req.originalUrl, template);
|
||||||
|
res.status(200).set({ "Content-Type": "text/html" }).end(html);
|
||||||
|
} catch (err) {
|
||||||
|
next(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
app.use(errorHandler);
|
app.use(errorHandler);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -1,28 +1,40 @@
|
|||||||
import { readConfigFile } from "./config-file.js";
|
import { readConfigFile } from "./config-file.js";
|
||||||
|
|
||||||
|
type DatabaseMode = "embedded-postgres" | "postgres";
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
port: number;
|
port: number;
|
||||||
|
databaseMode: DatabaseMode;
|
||||||
databaseUrl: string | undefined;
|
databaseUrl: string | undefined;
|
||||||
|
embeddedPostgresDataDir: string;
|
||||||
|
embeddedPostgresPort: number;
|
||||||
serveUi: boolean;
|
serveUi: boolean;
|
||||||
|
uiDevMiddleware: boolean;
|
||||||
heartbeatSchedulerEnabled: boolean;
|
heartbeatSchedulerEnabled: boolean;
|
||||||
heartbeatSchedulerIntervalMs: number;
|
heartbeatSchedulerIntervalMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadConfig(): Config {
|
export function loadConfig(): Config {
|
||||||
const fileConfig = readConfigFile();
|
const fileConfig = readConfigFile();
|
||||||
|
const fileDatabaseMode =
|
||||||
|
(fileConfig?.database.mode === "postgres" ? "postgres" : "embedded-postgres") as DatabaseMode;
|
||||||
|
|
||||||
const fileDbUrl =
|
const fileDbUrl =
|
||||||
fileConfig?.database.mode === "postgres"
|
fileDatabaseMode === "postgres"
|
||||||
? fileConfig.database.connectionString
|
? fileConfig?.database.connectionString
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port: Number(process.env.PORT) || fileConfig?.server.port || 3100,
|
port: Number(process.env.PORT) || fileConfig?.server.port || 3100,
|
||||||
|
databaseMode: fileDatabaseMode,
|
||||||
databaseUrl: process.env.DATABASE_URL ?? fileDbUrl,
|
databaseUrl: process.env.DATABASE_URL ?? fileDbUrl,
|
||||||
|
embeddedPostgresDataDir: fileConfig?.database.embeddedPostgresDataDir ?? "./data/embedded-postgres",
|
||||||
|
embeddedPostgresPort: fileConfig?.database.embeddedPostgresPort ?? 54329,
|
||||||
serveUi:
|
serveUi:
|
||||||
process.env.SERVE_UI !== undefined
|
process.env.SERVE_UI !== undefined
|
||||||
? process.env.SERVE_UI === "true"
|
? process.env.SERVE_UI === "true"
|
||||||
: fileConfig?.server.serveUi ?? false,
|
: fileConfig?.server.serveUi ?? false,
|
||||||
|
uiDevMiddleware: process.env.PAPERCLIP_UI_DEV_MIDDLEWARE === "true",
|
||||||
heartbeatSchedulerEnabled: process.env.HEARTBEAT_SCHEDULER_ENABLED !== "false",
|
heartbeatSchedulerEnabled: process.env.HEARTBEAT_SCHEDULER_ENABLED !== "false",
|
||||||
heartbeatSchedulerIntervalMs: Math.max(10000, Number(process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS) || 30000),
|
heartbeatSchedulerIntervalMs: Math.max(10000, Number(process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS) || 30000),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,26 +1,163 @@
|
|||||||
|
import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||||
import { createServer } from "node:http";
|
import { createServer } from "node:http";
|
||||||
import { resolve } from "node:path";
|
import { resolve } from "node:path";
|
||||||
import { createDb, createPgliteDb } from "@paperclip/db";
|
import {
|
||||||
|
createDb,
|
||||||
|
ensurePostgresDatabase,
|
||||||
|
migratePostgresIfEmpty,
|
||||||
|
} from "@paperclip/db";
|
||||||
|
import detectPort from "detect-port";
|
||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { loadConfig } from "./config.js";
|
import { loadConfig } from "./config.js";
|
||||||
import { logger } from "./middleware/logger.js";
|
import { logger } from "./middleware/logger.js";
|
||||||
import { setupLiveEventsWebSocketServer } from "./realtime/live-events-ws.js";
|
import { setupLiveEventsWebSocketServer } from "./realtime/live-events-ws.js";
|
||||||
import { heartbeatService } from "./services/index.js";
|
import { heartbeatService } from "./services/index.js";
|
||||||
|
import { printStartupBanner } from "./startup-banner.js";
|
||||||
|
|
||||||
|
type EmbeddedPostgresInstance = {
|
||||||
|
initialise(): Promise<void>;
|
||||||
|
start(): Promise<void>;
|
||||||
|
stop(): Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedPostgresCtor = new (opts: {
|
||||||
|
databaseDir: string;
|
||||||
|
user: string;
|
||||||
|
password: string;
|
||||||
|
port: number;
|
||||||
|
persistent: boolean;
|
||||||
|
}) => EmbeddedPostgresInstance;
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
let db;
|
let db;
|
||||||
|
let embeddedPostgres: EmbeddedPostgresInstance | null = null;
|
||||||
|
let embeddedPostgresStartedByThisProcess = false;
|
||||||
|
let migrationSummary = "skipped";
|
||||||
|
let startupDbInfo:
|
||||||
|
| { mode: "external-postgres"; connectionString: string }
|
||||||
|
| { mode: "embedded-postgres"; dataDir: string; port: number };
|
||||||
if (config.databaseUrl) {
|
if (config.databaseUrl) {
|
||||||
|
const migration = await migratePostgresIfEmpty(config.databaseUrl);
|
||||||
|
if (migration.migrated) {
|
||||||
|
logger.info("Empty PostgreSQL database detected; applied migrations");
|
||||||
|
migrationSummary = "applied (empty database)";
|
||||||
|
} else if (migration.reason === "not-empty-no-migration-journal") {
|
||||||
|
logger.warn(
|
||||||
|
{ tableCount: migration.tableCount },
|
||||||
|
"PostgreSQL has existing tables but no migration journal; skipped auto-migrate",
|
||||||
|
);
|
||||||
|
migrationSummary = "skipped (existing schema, no migration journal)";
|
||||||
|
} else {
|
||||||
|
migrationSummary = "already applied";
|
||||||
|
}
|
||||||
|
|
||||||
db = createDb(config.databaseUrl);
|
db = createDb(config.databaseUrl);
|
||||||
|
logger.info("Using external PostgreSQL via DATABASE_URL/config");
|
||||||
|
startupDbInfo = { mode: "external-postgres", connectionString: config.databaseUrl };
|
||||||
} else {
|
} else {
|
||||||
const dataDir = resolve("./data/pglite");
|
const moduleName = "embedded-postgres";
|
||||||
logger.info(`No DATABASE_URL set — using embedded PGlite (${dataDir})`);
|
let EmbeddedPostgres: EmbeddedPostgresCtor;
|
||||||
db = await createPgliteDb(dataDir);
|
try {
|
||||||
logger.info("PGlite ready, schema pushed");
|
const mod = await import(moduleName);
|
||||||
|
EmbeddedPostgres = mod.default as EmbeddedPostgresCtor;
|
||||||
|
} catch {
|
||||||
|
throw new Error(
|
||||||
|
"Embedded PostgreSQL mode requires optional dependency `embedded-postgres`. Install optional dependencies or set DATABASE_URL for external Postgres.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataDir = resolve(config.embeddedPostgresDataDir);
|
||||||
|
const port = config.embeddedPostgresPort;
|
||||||
|
|
||||||
|
if (config.databaseMode === "postgres") {
|
||||||
|
logger.warn("Database mode is postgres but no connection string was set; falling back to embedded PostgreSQL");
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`No DATABASE_URL set — using embedded PostgreSQL (${dataDir}) on port ${port}`);
|
||||||
|
embeddedPostgres = new EmbeddedPostgres({
|
||||||
|
databaseDir: dataDir,
|
||||||
|
user: "paperclip",
|
||||||
|
password: "paperclip",
|
||||||
|
port,
|
||||||
|
persistent: true,
|
||||||
|
});
|
||||||
|
const clusterVersionFile = resolve(dataDir, "PG_VERSION");
|
||||||
|
if (!existsSync(clusterVersionFile)) {
|
||||||
|
await embeddedPostgres.initialise();
|
||||||
|
} else {
|
||||||
|
logger.info(`Embedded PostgreSQL cluster already exists (${clusterVersionFile}); skipping init`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const postmasterPidFile = resolve(dataDir, "postmaster.pid");
|
||||||
|
const isPidRunning = (pid: number): boolean => {
|
||||||
|
try {
|
||||||
|
process.kill(pid, 0);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRunningPid = (): number | null => {
|
||||||
|
if (!existsSync(postmasterPidFile)) return null;
|
||||||
|
try {
|
||||||
|
const pidLine = readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim();
|
||||||
|
const pid = Number(pidLine);
|
||||||
|
if (!Number.isInteger(pid) || pid <= 0) return null;
|
||||||
|
if (!isPidRunning(pid)) return null;
|
||||||
|
return pid;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const runningPid = getRunningPid();
|
||||||
|
if (runningPid) {
|
||||||
|
logger.warn({ pid: runningPid }, "Embedded PostgreSQL already running; reusing existing process");
|
||||||
|
} else {
|
||||||
|
if (existsSync(postmasterPidFile)) {
|
||||||
|
logger.warn("Removing stale embedded PostgreSQL lock file");
|
||||||
|
rmSync(postmasterPidFile, { force: true });
|
||||||
|
}
|
||||||
|
await embeddedPostgres.start();
|
||||||
|
embeddedPostgresStartedByThisProcess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeddedAdminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/postgres`;
|
||||||
|
const dbStatus = await ensurePostgresDatabase(embeddedAdminConnectionString, "paperclip");
|
||||||
|
if (dbStatus === "created") {
|
||||||
|
logger.info("Created embedded PostgreSQL database: paperclip");
|
||||||
|
}
|
||||||
|
|
||||||
|
const embeddedConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${port}/paperclip`;
|
||||||
|
const migration = await migratePostgresIfEmpty(embeddedConnectionString);
|
||||||
|
if (migration.migrated) {
|
||||||
|
logger.info("Empty embedded PostgreSQL database detected; applied migrations");
|
||||||
|
migrationSummary = "applied (empty database)";
|
||||||
|
} else if (migration.reason === "not-empty-no-migration-journal") {
|
||||||
|
logger.warn(
|
||||||
|
{ tableCount: migration.tableCount },
|
||||||
|
"Embedded PostgreSQL has existing tables but no migration journal; skipped auto-migrate",
|
||||||
|
);
|
||||||
|
migrationSummary = "skipped (existing schema, no migration journal)";
|
||||||
|
} else {
|
||||||
|
migrationSummary = "already applied";
|
||||||
|
}
|
||||||
|
|
||||||
|
db = createDb(embeddedConnectionString);
|
||||||
|
logger.info("Embedded PostgreSQL ready");
|
||||||
|
startupDbInfo = { mode: "embedded-postgres", dataDir, port };
|
||||||
}
|
}
|
||||||
|
|
||||||
const app = createApp(db as any, { serveUi: config.serveUi });
|
const uiMode = config.uiDevMiddleware ? "vite-dev" : config.serveUi ? "static" : "none";
|
||||||
|
const app = await createApp(db as any, { uiMode });
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
|
const listenPort = await detectPort(config.port);
|
||||||
|
|
||||||
|
if (listenPort !== config.port) {
|
||||||
|
logger.warn({ requestedPort: config.port, selectedPort: listenPort }, "Requested port is busy; using next free port");
|
||||||
|
}
|
||||||
|
|
||||||
setupLiveEventsWebSocketServer(server, db as any);
|
setupLiveEventsWebSocketServer(server, db as any);
|
||||||
|
|
||||||
@@ -40,6 +177,35 @@ if (config.heartbeatSchedulerEnabled) {
|
|||||||
}, config.heartbeatSchedulerIntervalMs);
|
}, config.heartbeatSchedulerIntervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
server.listen(config.port, () => {
|
server.listen(listenPort, () => {
|
||||||
logger.info(`Server listening on :${config.port}`);
|
logger.info(`Server listening on :${listenPort}`);
|
||||||
|
printStartupBanner({
|
||||||
|
requestedPort: config.port,
|
||||||
|
listenPort,
|
||||||
|
uiMode,
|
||||||
|
db: startupDbInfo,
|
||||||
|
migrationSummary,
|
||||||
|
heartbeatSchedulerEnabled: config.heartbeatSchedulerEnabled,
|
||||||
|
heartbeatSchedulerIntervalMs: config.heartbeatSchedulerIntervalMs,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (embeddedPostgres && embeddedPostgresStartedByThisProcess) {
|
||||||
|
const shutdown = async (signal: "SIGINT" | "SIGTERM") => {
|
||||||
|
logger.info({ signal }, "Stopping embedded PostgreSQL");
|
||||||
|
try {
|
||||||
|
await embeddedPostgres?.stop();
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "Failed to stop embedded PostgreSQL cleanly");
|
||||||
|
} finally {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
process.once("SIGINT", () => {
|
||||||
|
void shutdown("SIGINT");
|
||||||
|
});
|
||||||
|
process.once("SIGTERM", () => {
|
||||||
|
void shutdown("SIGTERM");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
115
server/src/startup-banner.ts
Normal file
115
server/src/startup-banner.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
type UiMode = "none" | "static" | "vite-dev";
|
||||||
|
|
||||||
|
type ExternalPostgresInfo = {
|
||||||
|
mode: "external-postgres";
|
||||||
|
connectionString: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type EmbeddedPostgresInfo = {
|
||||||
|
mode: "embedded-postgres";
|
||||||
|
dataDir: string;
|
||||||
|
port: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StartupBannerOptions = {
|
||||||
|
requestedPort: number;
|
||||||
|
listenPort: number;
|
||||||
|
uiMode: UiMode;
|
||||||
|
db: ExternalPostgresInfo | EmbeddedPostgresInfo;
|
||||||
|
migrationSummary: string;
|
||||||
|
heartbeatSchedulerEnabled: boolean;
|
||||||
|
heartbeatSchedulerIntervalMs: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ansi = {
|
||||||
|
reset: "\x1b[0m",
|
||||||
|
bold: "\x1b[1m",
|
||||||
|
dim: "\x1b[2m",
|
||||||
|
cyan: "\x1b[36m",
|
||||||
|
green: "\x1b[32m",
|
||||||
|
yellow: "\x1b[33m",
|
||||||
|
magenta: "\x1b[35m",
|
||||||
|
blue: "\x1b[34m",
|
||||||
|
};
|
||||||
|
|
||||||
|
function color(text: string, c: keyof typeof ansi): string {
|
||||||
|
return `${ansi[c]}${text}${ansi.reset}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function row(label: string, value: string): string {
|
||||||
|
return `${color(label.padEnd(16), "dim")} ${value}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function redactConnectionString(raw: string): string {
|
||||||
|
try {
|
||||||
|
const u = new URL(raw);
|
||||||
|
const user = u.username || "user";
|
||||||
|
const auth = `${user}:***@`;
|
||||||
|
return `${u.protocol}//${auth}${u.host}${u.pathname}`;
|
||||||
|
} catch {
|
||||||
|
return "<invalid DATABASE_URL>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function printStartupBanner(opts: StartupBannerOptions): void {
|
||||||
|
const baseUrl = `http://localhost:${opts.listenPort}`;
|
||||||
|
const apiUrl = `${baseUrl}/api`;
|
||||||
|
const uiUrl = opts.uiMode === "none" ? "disabled" : baseUrl;
|
||||||
|
const configPath = process.env.PAPERCLIP_CONFIG
|
||||||
|
? resolve(process.env.PAPERCLIP_CONFIG)
|
||||||
|
: resolve(process.cwd(), ".paperclip/config.json");
|
||||||
|
|
||||||
|
const dbMode =
|
||||||
|
opts.db.mode === "embedded-postgres"
|
||||||
|
? color("embedded-postgres", "green")
|
||||||
|
: color("external-postgres", "yellow");
|
||||||
|
const uiMode =
|
||||||
|
opts.uiMode === "vite-dev"
|
||||||
|
? color("vite-dev-middleware", "cyan")
|
||||||
|
: opts.uiMode === "static"
|
||||||
|
? color("static-ui", "magenta")
|
||||||
|
: color("headless-api", "yellow");
|
||||||
|
|
||||||
|
const portValue =
|
||||||
|
opts.requestedPort === opts.listenPort
|
||||||
|
? `${opts.listenPort}`
|
||||||
|
: `${opts.listenPort} ${color(`(requested ${opts.requestedPort})`, "dim")}`;
|
||||||
|
|
||||||
|
const dbDetails =
|
||||||
|
opts.db.mode === "embedded-postgres"
|
||||||
|
? `${opts.db.dataDir} ${color(`(pg:${opts.db.port})`, "dim")}`
|
||||||
|
: redactConnectionString(opts.db.connectionString);
|
||||||
|
|
||||||
|
const heartbeat = opts.heartbeatSchedulerEnabled
|
||||||
|
? `enabled ${color(`(${opts.heartbeatSchedulerIntervalMs}ms)`, "dim")}`
|
||||||
|
: color("disabled", "yellow");
|
||||||
|
|
||||||
|
const art = [
|
||||||
|
color("██████╗ █████╗ ██████╗ ███████╗██████╗ ██████╗██╗ ██╗██████╗ ", "cyan"),
|
||||||
|
color("██╔══██╗██╔══██╗██╔══██╗██╔════╝██╔══██╗██╔════╝██║ ██║██╔══██╗", "cyan"),
|
||||||
|
color("██████╔╝███████║██████╔╝█████╗ ██████╔╝██║ ██║ ██║██████╔╝", "cyan"),
|
||||||
|
color("██╔═══╝ ██╔══██║██╔═══╝ ██╔══╝ ██╔══██╗██║ ██║ ██║██╔═══╝ ", "cyan"),
|
||||||
|
color("██║ ██║ ██║██║ ███████╗██║ ██║╚██████╗███████╗██║██║ ", "cyan"),
|
||||||
|
color("╚═╝ ╚═╝ ╚═╝╚═╝ ╚══════╝╚═╝ ╚═╝ ╚═════╝╚══════╝╚═╝╚═╝ ", "cyan"),
|
||||||
|
];
|
||||||
|
|
||||||
|
const lines = [
|
||||||
|
"",
|
||||||
|
...art,
|
||||||
|
color(" ───────────────────────────────────────────────────────", "blue"),
|
||||||
|
row("Mode", `${dbMode} | ${uiMode}`),
|
||||||
|
row("Server", portValue),
|
||||||
|
row("API", `${apiUrl} ${color(`(health: ${apiUrl}/health)`, "dim")}`),
|
||||||
|
row("UI", uiUrl),
|
||||||
|
row("Database", dbDetails),
|
||||||
|
row("Migrations", opts.migrationSummary),
|
||||||
|
row("Heartbeat", heartbeat),
|
||||||
|
row("Config", configPath),
|
||||||
|
color(" ───────────────────────────────────────────────────────", "blue"),
|
||||||
|
"",
|
||||||
|
];
|
||||||
|
|
||||||
|
console.log(lines.join("\n"));
|
||||||
|
}
|
||||||
@@ -60,8 +60,8 @@ export function OnboardingWizard() {
|
|||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
|
|
||||||
// Step 3
|
// Step 3
|
||||||
const [taskTitle, setTaskTitle] = useState("");
|
const [taskTitle, setTaskTitle] = useState("Create your CEO HEARTBEAT.md");
|
||||||
const [taskDescription, setTaskDescription] = useState("");
|
const [taskDescription, setTaskDescription] = useState("You're the CEO of the company, make sure you have a file agents/ceo/HEARTBEAT.md that tells you your core loop. You MUST use the Paperclip SKILL.");
|
||||||
|
|
||||||
// Created entity IDs
|
// Created entity IDs
|
||||||
const [createdCompanyId, setCreatedCompanyId] = useState<string | null>(null);
|
const [createdCompanyId, setCreatedCompanyId] = useState<string | null>(null);
|
||||||
@@ -88,8 +88,8 @@ export function OnboardingWizard() {
|
|||||||
setCommand("");
|
setCommand("");
|
||||||
setArgs("");
|
setArgs("");
|
||||||
setUrl("");
|
setUrl("");
|
||||||
setTaskTitle("");
|
setTaskTitle("Create your CEO HEARTBEAT.md");
|
||||||
setTaskDescription("");
|
setTaskDescription("You're the CEO of the company, make sure you have a file agents/ceo/HEARTBEAT.md that tells you your core loop. You MUST use the Paperclip SKILL.");
|
||||||
setCreatedCompanyId(null);
|
setCreatedCompanyId(null);
|
||||||
setCreatedAgentId(null);
|
setCreatedAgentId(null);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user