chore(scaffold): bootstrap docmost-sync Node/TS project skeleton
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.
This commit is contained in:
36
src/config-errors.ts
Normal file
36
src/config-errors.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { ZodError } from 'zod';
|
||||
|
||||
// Turn a ZodError from settings validation into a clear, actionable startup
|
||||
// message that names the offending env var(s), then exit(1) — no raw stack
|
||||
// trace. Mirrors the Python new-project skeleton's load_settings_or_exit.
|
||||
// A non-ZodError is left to propagate unchanged.
|
||||
export function loadSettingsOrExit<T>(factory: () => T): T {
|
||||
try {
|
||||
return factory();
|
||||
} catch (err) {
|
||||
if (!(err instanceof ZodError)) throw err;
|
||||
const missing: string[] = [];
|
||||
const invalid: string[] = [];
|
||||
for (const issue of err.issues) {
|
||||
const name = issue.path.length ? String(issue.path[0]) : '?';
|
||||
const isMissing =
|
||||
issue.code === 'invalid_type' &&
|
||||
(issue as { received?: unknown }).received === 'undefined';
|
||||
if (isMissing) missing.push(name);
|
||||
else invalid.push(`${name}: ${issue.message}`);
|
||||
}
|
||||
const lines = ['Configuration error in environment / .env:'];
|
||||
if (missing.length) {
|
||||
lines.push(' Missing required variable(s):');
|
||||
for (const n of [...new Set(missing)]) lines.push(` - ${n}`);
|
||||
}
|
||||
if (invalid.length) {
|
||||
lines.push(' Invalid value(s):');
|
||||
for (const item of invalid) lines.push(` - ${item}`);
|
||||
}
|
||||
lines.push('');
|
||||
lines.push('Set them in .env (see .env.example) and try again.');
|
||||
process.stderr.write(lines.join('\n') + '\n');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
14
src/index.ts
Normal file
14
src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { loadSettings } from './settings.js';
|
||||
|
||||
// Thin entry point. loadSettings() validates the environment and exits with a
|
||||
// clear message if anything is missing/invalid. The sync engine is not
|
||||
// implemented yet — see SPEC.md for the design and the phased plan.
|
||||
function main(): void {
|
||||
const settings = loadSettings();
|
||||
console.log(`docmost-sync starting (log level: ${settings.logLevel})`);
|
||||
console.log(`Docmost: ${settings.docmostApiUrl} (space ${settings.docmostSpaceId})`);
|
||||
console.log(`Vault: ${settings.vaultPath}`);
|
||||
console.log('Engine not implemented yet — scaffold only. See SPEC.md.');
|
||||
}
|
||||
|
||||
main();
|
||||
66
src/settings.ts
Normal file
66
src/settings.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user