feat(cli): add client commands and home-based local runtime defaults

This commit is contained in:
Forgotten
2026-02-20 07:10:58 -06:00
parent 8e3c2fae35
commit 8f3fc077fa
40 changed files with 2284 additions and 138 deletions

View File

@@ -8,8 +8,6 @@ const JWT_SECRET_ENV_KEY = "PAPERCLIP_AGENT_JWT_SECRET";
function resolveEnvFilePath() {
return path.resolve(path.dirname(resolveConfigPath()), ".env");
}
const ENV_FILE_PATH = resolveEnvFilePath();
const loadedEnvFiles = new Set<string>();
function isNonEmpty(value: unknown): value is string {
@@ -35,10 +33,10 @@ function renderEnvFile(entries: Record<string, string>) {
}
export function resolveAgentJwtEnvFile(): string {
return ENV_FILE_PATH;
return resolveEnvFilePath();
}
export function loadAgentJwtEnvFile(filePath = ENV_FILE_PATH): void {
export function loadAgentJwtEnvFile(filePath = resolveEnvFilePath()): void {
if (loadedEnvFiles.has(filePath)) return;
if (!fs.existsSync(filePath)) return;
@@ -52,7 +50,7 @@ export function readAgentJwtSecretFromEnv(): string | null {
return isNonEmpty(raw) ? raw!.trim() : null;
}
export function readAgentJwtSecretFromEnvFile(filePath = ENV_FILE_PATH): string | null {
export function readAgentJwtSecretFromEnvFile(filePath = resolveEnvFilePath()): string | null {
if (!fs.existsSync(filePath)) return null;
const raw = fs.readFileSync(filePath, "utf-8");
@@ -78,7 +76,7 @@ export function ensureAgentJwtSecret(): { secret: string; created: boolean } {
return { secret, created };
}
export function writeAgentJwtEnv(secret: string, filePath = ENV_FILE_PATH): void {
export function writeAgentJwtEnv(secret: string, filePath = resolveEnvFilePath()): void {
const dir = path.dirname(filePath);
fs.mkdirSync(dir, { recursive: true });

66
cli/src/config/home.ts Normal file
View File

@@ -0,0 +1,66 @@
import os from "node:os";
import path from "node:path";
const DEFAULT_INSTANCE_ID = "default";
const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
export function resolvePaperclipHomeDir(): string {
const envHome = process.env.PAPERCLIP_HOME?.trim();
if (envHome) return path.resolve(expandHomePrefix(envHome));
return path.resolve(os.homedir(), ".paperclip");
}
export function resolvePaperclipInstanceId(override?: string): string {
const raw = override?.trim() || process.env.PAPERCLIP_INSTANCE_ID?.trim() || DEFAULT_INSTANCE_ID;
if (!INSTANCE_ID_RE.test(raw)) {
throw new Error(
`Invalid instance id '${raw}'. Allowed characters: letters, numbers, '_' and '-'.`,
);
}
return raw;
}
export function resolvePaperclipInstanceRoot(instanceId?: string): string {
const id = resolvePaperclipInstanceId(instanceId);
return path.resolve(resolvePaperclipHomeDir(), "instances", id);
}
export function resolveDefaultConfigPath(instanceId?: string): string {
return path.resolve(resolvePaperclipInstanceRoot(instanceId), "config.json");
}
export function resolveDefaultContextPath(): string {
return path.resolve(resolvePaperclipHomeDir(), "context.json");
}
export function resolveDefaultEmbeddedPostgresDir(instanceId?: string): string {
return path.resolve(resolvePaperclipInstanceRoot(instanceId), "db");
}
export function resolveDefaultLogsDir(instanceId?: string): string {
return path.resolve(resolvePaperclipInstanceRoot(instanceId), "logs");
}
export function resolveDefaultSecretsKeyFilePath(instanceId?: string): string {
return path.resolve(resolvePaperclipInstanceRoot(instanceId), "secrets", "master.key");
}
export function expandHomePrefix(value: string): string {
if (value === "~") return os.homedir();
if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2));
return value;
}
export function describeLocalInstancePaths(instanceId?: string) {
const resolvedInstanceId = resolvePaperclipInstanceId(instanceId);
const instanceRoot = resolvePaperclipInstanceRoot(resolvedInstanceId);
return {
homeDir: resolvePaperclipHomeDir(),
instanceId: resolvedInstanceId,
instanceRoot,
configPath: resolveDefaultConfigPath(resolvedInstanceId),
embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(resolvedInstanceId),
logDir: resolveDefaultLogsDir(resolvedInstanceId),
secretsKeyFilePath: resolveDefaultSecretsKeyFilePath(resolvedInstanceId),
};
}

View File

@@ -1,8 +1,11 @@
import fs from "node:fs";
import path from "node:path";
import { paperclipConfigSchema, type PaperclipConfig } from "./schema.js";
import {
resolveDefaultConfigPath,
resolvePaperclipInstanceId,
} from "./home.js";
const DEFAULT_CONFIG_PATH = ".paperclip/config.json";
const DEFAULT_CONFIG_BASENAME = "config.json";
function findConfigFileFromAncestors(startDir: string): string | null {
@@ -26,7 +29,7 @@ function findConfigFileFromAncestors(startDir: string): string | null {
export function resolveConfigPath(overridePath?: string): string {
if (overridePath) return path.resolve(overridePath);
if (process.env.PAPERCLIP_CONFIG) return path.resolve(process.env.PAPERCLIP_CONFIG);
return findConfigFileFromAncestors(process.cwd()) ?? path.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
return findConfigFileFromAncestors(process.cwd()) ?? resolveDefaultConfigPath(resolvePaperclipInstanceId());
}
function parseJson(filePath: string): unknown {