Add CLI package, config file support, and workspace setup

Add cli/ package with initial scaffolding. Add config-schema to shared
package for typed configuration. Add server config-file loader for
paperclip.config.ts support. Register cli in pnpm workspace. Add
.paperclip/ and .pnpm-store/ to gitignore. Minor Companies page fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-17 13:39:47 -06:00
parent 0975907121
commit 5306142542
28 changed files with 1091 additions and 7 deletions

View File

@@ -0,0 +1,33 @@
import { readConfig, configExists, resolveConfigPath } from "../config/store.js";
import type { CheckResult } from "./index.js";
export function configCheck(configPath?: string): CheckResult {
const filePath = resolveConfigPath(configPath);
if (!configExists(configPath)) {
return {
name: "Config file",
status: "fail",
message: `Config file not found at ${filePath}`,
canRepair: false,
repairHint: "Run `paperclip onboard` to create one",
};
}
try {
readConfig(configPath);
return {
name: "Config file",
status: "pass",
message: `Valid config at ${filePath}`,
};
} catch (err) {
return {
name: "Config file",
status: "fail",
message: `Invalid config: ${err instanceof Error ? err.message : String(err)}`,
canRepair: false,
repairHint: "Run `paperclip onboard` to recreate",
};
}
}

View File

@@ -0,0 +1,57 @@
import fs from "node:fs";
import path from "node:path";
import type { PaperclipConfig } from "../config/schema.js";
import type { CheckResult } from "./index.js";
export async function databaseCheck(config: PaperclipConfig): Promise<CheckResult> {
if (config.database.mode === "postgres") {
if (!config.database.connectionString) {
return {
name: "Database",
status: "fail",
message: "PostgreSQL mode selected but no connection string configured",
canRepair: false,
repairHint: "Run `paperclip configure --section database`",
};
}
try {
const { createDb } = await import("@paperclip/db");
const db = createDb(config.database.connectionString);
await db.execute("SELECT 1");
return {
name: "Database",
status: "pass",
message: "PostgreSQL connection successful",
};
} catch (err) {
return {
name: "Database",
status: "fail",
message: `Cannot connect to PostgreSQL: ${err instanceof Error ? err.message : String(err)}`,
canRepair: false,
repairHint: "Check your connection string and ensure PostgreSQL is running",
};
}
}
// PGlite mode — check data dir
const dataDir = path.resolve(config.database.pgliteDataDir);
if (!fs.existsSync(dataDir)) {
return {
name: "Database",
status: "warn",
message: `PGlite data directory does not exist: ${dataDir}`,
canRepair: true,
repair: () => {
fs.mkdirSync(dataDir, { recursive: true });
},
};
}
return {
name: "Database",
status: "pass",
message: `PGlite data directory exists: ${dataDir}`,
};
}

14
cli/src/checks/index.ts Normal file
View File

@@ -0,0 +1,14 @@
export interface CheckResult {
name: string;
status: "pass" | "warn" | "fail";
message: string;
canRepair?: boolean;
repair?: () => void | Promise<void>;
repairHint?: string;
}
export { configCheck } from "./config-check.js";
export { databaseCheck } from "./database-check.js";
export { llmCheck } from "./llm-check.js";
export { logCheck } from "./log-check.js";
export { portCheck } from "./port-check.js";

View File

@@ -0,0 +1,86 @@
import type { PaperclipConfig } from "../config/schema.js";
import type { CheckResult } from "./index.js";
export async function llmCheck(config: PaperclipConfig): Promise<CheckResult> {
if (!config.llm) {
return {
name: "LLM provider",
status: "warn",
message: "No LLM provider configured",
canRepair: false,
repairHint: "Run `paperclip configure --section llm` to set one up",
};
}
if (!config.llm.apiKey) {
return {
name: "LLM provider",
status: "warn",
message: `${config.llm.provider} configured but no API key set`,
canRepair: false,
repairHint: "Run `paperclip configure --section llm`",
};
}
try {
if (config.llm.provider === "claude") {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": config.llm.apiKey,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-5-20250929",
max_tokens: 1,
messages: [{ role: "user", content: "hi" }],
}),
});
if (res.ok || res.status === 400) {
return { name: "LLM provider", status: "pass", message: "Claude API key is valid" };
}
if (res.status === 401) {
return {
name: "LLM provider",
status: "fail",
message: "Claude API key is invalid (401)",
canRepair: false,
repairHint: "Run `paperclip configure --section llm`",
};
}
return {
name: "LLM provider",
status: "warn",
message: `Claude API returned status ${res.status}`,
};
} else {
const res = await fetch("https://api.openai.com/v1/models", {
headers: { Authorization: `Bearer ${config.llm.apiKey}` },
});
if (res.ok) {
return { name: "LLM provider", status: "pass", message: "OpenAI API key is valid" };
}
if (res.status === 401) {
return {
name: "LLM provider",
status: "fail",
message: "OpenAI API key is invalid (401)",
canRepair: false,
repairHint: "Run `paperclip configure --section llm`",
};
}
return {
name: "LLM provider",
status: "warn",
message: `OpenAI API returned status ${res.status}`,
};
}
} catch {
return {
name: "LLM provider",
status: "warn",
message: "Could not reach API to validate key",
};
}
}

View File

@@ -0,0 +1,37 @@
import fs from "node:fs";
import path from "node:path";
import type { PaperclipConfig } from "../config/schema.js";
import type { CheckResult } from "./index.js";
export function logCheck(config: PaperclipConfig): CheckResult {
const logDir = path.resolve(config.logging.logDir);
if (!fs.existsSync(logDir)) {
return {
name: "Log directory",
status: "warn",
message: `Log directory does not exist: ${logDir}`,
canRepair: true,
repair: () => {
fs.mkdirSync(logDir, { recursive: true });
},
};
}
try {
fs.accessSync(logDir, fs.constants.W_OK);
return {
name: "Log directory",
status: "pass",
message: `Log directory is writable: ${logDir}`,
};
} catch {
return {
name: "Log directory",
status: "fail",
message: `Log directory is not writable: ${logDir}`,
canRepair: false,
repairHint: "Check file permissions on the log directory",
};
}
}

View File

@@ -0,0 +1,24 @@
import type { PaperclipConfig } from "../config/schema.js";
import { checkPort } from "../utils/net.js";
import type { CheckResult } from "./index.js";
export async function portCheck(config: PaperclipConfig): Promise<CheckResult> {
const port = config.server.port;
const result = await checkPort(port);
if (result.available) {
return {
name: "Server port",
status: "pass",
message: `Port ${port} is available`,
};
}
return {
name: "Server port",
status: "warn",
message: result.error ?? `Port ${port} is not available`,
canRepair: false,
repairHint: `Check what's using port ${port} with: lsof -i :${port}`,
};
}