134 lines
4.4 KiB
TypeScript
134 lines
4.4 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import * as p from "@clack/prompts";
|
|
import pc from "picocolors";
|
|
import { onboard } from "./onboard.js";
|
|
import { doctor } from "./doctor.js";
|
|
import { configExists, resolveConfigPath } from "../config/store.js";
|
|
import {
|
|
describeLocalInstancePaths,
|
|
resolvePaperclipHomeDir,
|
|
resolvePaperclipInstanceId,
|
|
} from "../config/home.js";
|
|
|
|
interface RunOptions {
|
|
config?: string;
|
|
instance?: string;
|
|
repair?: boolean;
|
|
yes?: boolean;
|
|
}
|
|
|
|
export async function runCommand(opts: RunOptions): Promise<void> {
|
|
const instanceId = resolvePaperclipInstanceId(opts.instance);
|
|
process.env.PAPERCLIP_INSTANCE_ID = instanceId;
|
|
|
|
const homeDir = resolvePaperclipHomeDir();
|
|
fs.mkdirSync(homeDir, { recursive: true });
|
|
|
|
const paths = describeLocalInstancePaths(instanceId);
|
|
fs.mkdirSync(paths.instanceRoot, { recursive: true });
|
|
|
|
const configPath = resolveConfigPath(opts.config);
|
|
process.env.PAPERCLIP_CONFIG = configPath;
|
|
|
|
p.intro(pc.bgCyan(pc.black(" paperclipai run ")));
|
|
p.log.message(pc.dim(`Home: ${paths.homeDir}`));
|
|
p.log.message(pc.dim(`Instance: ${paths.instanceId}`));
|
|
p.log.message(pc.dim(`Config: ${configPath}`));
|
|
|
|
if (!configExists(configPath)) {
|
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
p.log.error("No config found and terminal is non-interactive.");
|
|
p.log.message(`Run ${pc.cyan("paperclipai onboard")} once, then retry ${pc.cyan("paperclipai run")}.`);
|
|
process.exit(1);
|
|
}
|
|
|
|
p.log.step("No config found. Starting onboarding...");
|
|
await onboard({ config: configPath, invokedByRun: true });
|
|
}
|
|
|
|
p.log.step("Running doctor checks...");
|
|
const summary = await doctor({
|
|
config: configPath,
|
|
repair: opts.repair ?? true,
|
|
yes: opts.yes ?? true,
|
|
});
|
|
|
|
if (summary.failed > 0) {
|
|
p.log.error("Doctor found blocking issues. Not starting server.");
|
|
process.exit(1);
|
|
}
|
|
|
|
p.log.step("Starting Paperclip server...");
|
|
await importServerEntry();
|
|
}
|
|
|
|
function formatError(err: unknown): string {
|
|
if (err instanceof Error) {
|
|
if (err.message && err.message.trim().length > 0) return err.message;
|
|
return err.name;
|
|
}
|
|
if (typeof err === "string") return err;
|
|
try {
|
|
return JSON.stringify(err);
|
|
} catch {
|
|
return String(err);
|
|
}
|
|
}
|
|
|
|
function isModuleNotFoundError(err: unknown): boolean {
|
|
if (!(err instanceof Error)) return false;
|
|
const code = (err as { code?: unknown }).code;
|
|
if (code === "ERR_MODULE_NOT_FOUND") return true;
|
|
return err.message.includes("Cannot find module");
|
|
}
|
|
|
|
function maybeEnableUiDevMiddleware(entrypoint: string): void {
|
|
if (process.env.PAPERCLIP_UI_DEV_MIDDLEWARE !== undefined) return;
|
|
const normalized = entrypoint.replaceAll("\\", "/");
|
|
if (normalized.endsWith("/server/src/index.ts") || normalized.endsWith("@paperclipai/server/src/index.ts")) {
|
|
process.env.PAPERCLIP_UI_DEV_MIDDLEWARE = "true";
|
|
}
|
|
}
|
|
|
|
async function importServerEntry(): Promise<void> {
|
|
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
const fileCandidates = [
|
|
path.resolve(projectRoot, "server/src/index.ts"),
|
|
path.resolve(projectRoot, "server/dist/index.js"),
|
|
];
|
|
const existingFileCandidates = fileCandidates.filter((filePath) => fs.existsSync(filePath));
|
|
if (existingFileCandidates.length > 0) {
|
|
for (const filePath of existingFileCandidates) {
|
|
try {
|
|
maybeEnableUiDevMiddleware(filePath);
|
|
await import(pathToFileURL(filePath).href);
|
|
return;
|
|
} catch (err) {
|
|
throw new Error(`Failed to start Paperclip server from ${filePath}: ${formatError(err)}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const specifierCandidates: string[] = ["@paperclipai/server/dist/index.js", "@paperclipai/server/src/index.ts"];
|
|
const missingErrors: string[] = [];
|
|
for (const specifier of specifierCandidates) {
|
|
try {
|
|
maybeEnableUiDevMiddleware(specifier);
|
|
await import(specifier);
|
|
return;
|
|
} catch (err) {
|
|
if (isModuleNotFoundError(err)) {
|
|
missingErrors.push(`${specifier}: ${formatError(err)}`);
|
|
continue;
|
|
}
|
|
throw new Error(`Failed to start Paperclip server from ${specifier}: ${formatError(err)}`);
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`Could not locate a Paperclip server entrypoint. Tried: ${[...fileCandidates, ...specifierCandidates].join(", ")}\n${missingErrors.join("\n")}`,
|
|
);
|
|
}
|