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:
33
cli/src/checks/config-check.ts
Normal file
33
cli/src/checks/config-check.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
57
cli/src/checks/database-check.ts
Normal file
57
cli/src/checks/database-check.ts
Normal 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
14
cli/src/checks/index.ts
Normal 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";
|
||||
86
cli/src/checks/llm-check.ts
Normal file
86
cli/src/checks/llm-check.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
37
cli/src/checks/log-check.ts
Normal file
37
cli/src/checks/log-check.ts
Normal 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",
|
||||
};
|
||||
}
|
||||
}
|
||||
24
cli/src/checks/port-check.ts
Normal file
24
cli/src/checks/port-check.ts
Normal 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}`,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user