Set up the project structure per the new-project guide, adapted from the Python skeleton to the Node/TS stack fixed in SPEC.md (reuses docmost-mcp). Scaffold only — the sync engine is not implemented yet. - src/settings.ts: single config layer on zod, schema keyed by real ENV names; credentials and own-service address have no default (fail fast). - src/config-errors.ts: loadSettingsOrExit — clear startup message naming the missing/invalid env var instead of a raw stack trace; exit(1). - src/index.ts: thin entry point that validates config and logs (stub). - test/: vitest unit tests for settings parsing and config errors (10 tests). - Makefile (install/env/build/test/run/dev/clean), strict tsconfig, vitest. - Dockerfile (single-stage, no EXPOSE, prunes dev deps), docker-compose (daemon, volume on /app/data, watchtower), ghcr CI with build needs test. - .env.example, .gitignore/.dockerignore, AGENTS.md, README.md. - Pinned deps (dotenv, zod) + committed package-lock.json.
67 lines
2.5 KiB
TypeScript
67 lines
2.5 KiB
TypeScript
import { config as loadDotenv } from 'dotenv';
|
|
import { z } from 'zod';
|
|
import { loadSettingsOrExit } from './config-errors.js';
|
|
|
|
// Schema keyed by the real ENV variable names so validation errors name the
|
|
// exact variable. Credentials and the address of our OWN Docmost instance have
|
|
// NO default — a missing value must fail at startup, never silently fall back.
|
|
export const envSchema = z.object({
|
|
// Docmost connection — address of our own instance, no default.
|
|
DOCMOST_API_URL: z.string().url(),
|
|
// Credentials for /auth/login — no default, never hardcoded.
|
|
DOCMOST_EMAIL: z.string().min(1),
|
|
DOCMOST_PASSWORD: z.string().min(1),
|
|
// Which Docmost space to mirror.
|
|
DOCMOST_SPACE_ID: z.string().min(1),
|
|
|
|
// Local git vault (state store) — kept under data/ so the volume persists it.
|
|
VAULT_PATH: z.string().min(1).default('data/vault'),
|
|
// Optional git remote the vault pushes to. Empty string is treated as unset.
|
|
GIT_REMOTE: z.preprocess(
|
|
(v) => (v === '' ? undefined : v),
|
|
z.string().min(1).optional(),
|
|
),
|
|
|
|
// Non-secret tunables — sensible defaults are fine.
|
|
POLL_INTERVAL_MS: z.coerce.number().int().positive().default(15000),
|
|
DEBOUNCE_MS: z.coerce.number().int().positive().default(2000),
|
|
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
});
|
|
|
|
export type Settings = {
|
|
docmostApiUrl: string;
|
|
docmostEmail: string;
|
|
docmostPassword: string;
|
|
docmostSpaceId: string;
|
|
vaultPath: string;
|
|
gitRemote?: string;
|
|
pollIntervalMs: number;
|
|
debounceMs: number;
|
|
logLevel: 'debug' | 'info' | 'warn' | 'error';
|
|
};
|
|
|
|
// Pure: validate a raw environment object and map it to a typed Settings.
|
|
// Throws ZodError on bad config. No side effects — safe to import in tests.
|
|
export function parseSettings(env: NodeJS.ProcessEnv): Settings {
|
|
const e = envSchema.parse(env);
|
|
return {
|
|
docmostApiUrl: e.DOCMOST_API_URL,
|
|
docmostEmail: e.DOCMOST_EMAIL,
|
|
docmostPassword: e.DOCMOST_PASSWORD,
|
|
docmostSpaceId: e.DOCMOST_SPACE_ID,
|
|
vaultPath: e.VAULT_PATH,
|
|
gitRemote: e.GIT_REMOTE,
|
|
pollIntervalMs: e.POLL_INTERVAL_MS,
|
|
debounceMs: e.DEBOUNCE_MS,
|
|
logLevel: e.LOG_LEVEL,
|
|
};
|
|
}
|
|
|
|
// Load .env (if present; absent in prod where env comes from docker-compose),
|
|
// then build validated settings, failing fast with a clear message instead of a
|
|
// raw stack trace. Call once at startup from the entry point.
|
|
export function loadSettings(): Settings {
|
|
loadDotenv();
|
|
return loadSettingsOrExit(() => parseSettings(process.env));
|
|
}
|