- Install @libsql/client@^0.17.2 to server package - Create skill-registry-schema.ts with 4 sqliteTable definitions (skills, skillVersions, skillFiles, communityRatings) - Create skill-registry-db.ts with lazy singleton getSkillRegistryDb() and resetSkillRegistryDb() - Add resolveSkillRegistryDbPath() and resolveSkillCacheDir() to home-paths.ts - Add skill-registry-schema.test.ts with 8 passing tests (TDD green)
124 lines
4.2 KiB
TypeScript
124 lines
4.2 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const DEFAULT_INSTANCE_ID = "default";
|
|
const INSTANCE_ID_RE = /^[a-zA-Z0-9_-]+$/;
|
|
const PATH_SEGMENT_RE = /^[a-zA-Z0-9_-]+$/;
|
|
const FRIENDLY_PATH_SEGMENT_RE = /[^a-zA-Z0-9._-]+/g;
|
|
|
|
function expandHomePrefix(value: string): string {
|
|
if (value === "~") return os.homedir();
|
|
if (value.startsWith("~/")) return path.resolve(os.homedir(), value.slice(2));
|
|
return value;
|
|
}
|
|
|
|
// [nexus] Read ~/.nexus pointer file for custom home directory
|
|
function resolveNexusPointerFile(): string | null {
|
|
const pointerPath = path.resolve(os.homedir(), ".nexus");
|
|
try {
|
|
const raw = fs.readFileSync(pointerPath, "utf-8").trim();
|
|
if (raw.length > 0) {
|
|
return path.resolve(expandHomePrefix(raw));
|
|
}
|
|
} catch {
|
|
// ~/.nexus does not exist or is unreadable — fall through
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function resolvePaperclipHomeDir(): string {
|
|
// [nexus] Pointer-file: ~/.nexus overrides all other home resolution
|
|
const nexusRoot = resolveNexusPointerFile();
|
|
if (nexusRoot) return nexusRoot;
|
|
|
|
const envHome = process.env.PAPERCLIP_HOME?.trim();
|
|
if (envHome) return path.resolve(expandHomePrefix(envHome));
|
|
return path.resolve(os.homedir(), ".paperclip");
|
|
}
|
|
|
|
export function resolvePaperclipInstanceId(): string {
|
|
const raw = process.env.PAPERCLIP_INSTANCE_ID?.trim() || DEFAULT_INSTANCE_ID;
|
|
if (!INSTANCE_ID_RE.test(raw)) {
|
|
throw new Error(`Invalid PAPERCLIP_INSTANCE_ID '${raw}'.`);
|
|
}
|
|
return raw;
|
|
}
|
|
|
|
export function resolvePaperclipInstanceRoot(): string {
|
|
return path.resolve(resolvePaperclipHomeDir(), "instances", resolvePaperclipInstanceId());
|
|
}
|
|
|
|
export function resolveDefaultConfigPath(): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "config.json");
|
|
}
|
|
|
|
export function resolveDefaultEmbeddedPostgresDir(): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "db");
|
|
}
|
|
|
|
export function resolveDefaultLogsDir(): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "logs");
|
|
}
|
|
|
|
export function resolveDefaultSecretsKeyFilePath(): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "secrets", "master.key");
|
|
}
|
|
|
|
export function resolveDefaultStorageDir(): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "data", "storage");
|
|
}
|
|
|
|
export function resolveDefaultBackupDir(): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "data", "backups");
|
|
}
|
|
|
|
// [nexus] Accept agent object for human-readable slugified workspace dirs
|
|
export function resolveDefaultAgentWorkspaceDir(agent: { id: string; name?: string | null }): string {
|
|
// Use slugified name for human-readable dirs; fall back to sanitized id
|
|
const segment = agent.name?.trim()
|
|
? sanitizeFriendlyPathSegment(agent.name, agent.id)
|
|
: sanitizeFriendlyPathSegment(agent.id, agent.id);
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "workspaces", segment);
|
|
}
|
|
|
|
function sanitizeFriendlyPathSegment(value: string | null | undefined, fallback = "_default"): string {
|
|
const trimmed = value?.trim() ?? "";
|
|
if (!trimmed) return fallback;
|
|
const sanitized = trimmed
|
|
.replace(FRIENDLY_PATH_SEGMENT_RE, "-")
|
|
.replace(/^-+|-+$/g, "");
|
|
return sanitized || fallback;
|
|
}
|
|
|
|
export function resolveManagedProjectWorkspaceDir(input: {
|
|
companyId: string;
|
|
projectId: string;
|
|
repoName?: string | null;
|
|
}): string {
|
|
const companyId = input.companyId.trim();
|
|
const projectId = input.projectId.trim();
|
|
if (!companyId || !projectId) {
|
|
throw new Error("Managed project workspace path requires companyId and projectId.");
|
|
}
|
|
return path.resolve(
|
|
resolvePaperclipInstanceRoot(),
|
|
"projects",
|
|
sanitizeFriendlyPathSegment(companyId, "company"),
|
|
sanitizeFriendlyPathSegment(projectId, "project"),
|
|
sanitizeFriendlyPathSegment(input.repoName, "_default"),
|
|
);
|
|
}
|
|
|
|
export function resolveHomeAwarePath(value: string): string {
|
|
return path.resolve(expandHomePrefix(value));
|
|
}
|
|
|
|
// [nexus] Skill registry paths
|
|
export function resolveSkillRegistryDbPath(): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "skills", "registry.db");
|
|
}
|
|
|
|
export function resolveSkillCacheDir(skillId: string, versionId: string): string {
|
|
return path.resolve(resolvePaperclipInstanceRoot(), "skills", "cache", skillId, versionId);
|
|
}
|