Merge pull request #427 from paperclipai/dotta-releases

Dotta releases
This commit is contained in:
Dotta
2026-03-09 11:41:11 -05:00
committed by GitHub
7 changed files with 652 additions and 538 deletions

View File

@@ -3,6 +3,7 @@ import * as p from "@clack/prompts";
import pc from "picocolors";
import { and, eq, gt, isNull } from "drizzle-orm";
import { createDb, instanceUserRoles, invites } from "@paperclipai/db";
import { loadPaperclipEnvFile } from "../config/env.js";
import { readConfig, resolveConfigPath } from "../config/store.js";
function hashToken(token: string) {
@@ -13,7 +14,8 @@ function createInviteToken() {
return `pcp_bootstrap_${randomBytes(24).toString("hex")}`;
}
function resolveDbUrl(configPath?: string) {
function resolveDbUrl(configPath?: string, explicitDbUrl?: string) {
if (explicitDbUrl) return explicitDbUrl;
const config = readConfig(configPath);
if (process.env.DATABASE_URL) return process.env.DATABASE_URL;
if (config?.database.mode === "postgres" && config.database.connectionString) {
@@ -49,8 +51,10 @@ export async function bootstrapCeoInvite(opts: {
force?: boolean;
expiresHours?: number;
baseUrl?: string;
dbUrl?: string;
}) {
const configPath = resolveConfigPath(opts.config);
loadPaperclipEnvFile(configPath);
const config = readConfig(configPath);
if (!config) {
p.log.error(`No config found at ${configPath}. Run ${pc.cyan("paperclip onboard")} first.`);
@@ -62,7 +66,7 @@ export async function bootstrapCeoInvite(opts: {
return;
}
const dbUrl = resolveDbUrl(configPath);
const dbUrl = resolveDbUrl(configPath, opts.dbUrl);
if (!dbUrl) {
p.log.error(
"Could not resolve database connection for bootstrap.",

View File

@@ -14,6 +14,7 @@ import {
storageCheck,
type CheckResult,
} from "../checks/index.js";
import { loadPaperclipEnvFile } from "../config/env.js";
import { printPaperclipCliBanner } from "../utils/banner.js";
const STATUS_ICON = {
@@ -31,6 +32,7 @@ export async function doctor(opts: {
p.intro(pc.bgCyan(pc.black(" paperclip doctor ")));
const configPath = resolveConfigPath(opts.config);
loadPaperclipEnvFile(configPath);
const results: CheckResult[] = [];
// 1. Config check (must pass before others)

View File

@@ -229,6 +229,10 @@ function quickstartDefaultsFromEnv(): {
return { defaults, usedEnvKeys, ignoredEnvKeys };
}
function canCreateBootstrapInviteImmediately(config: Pick<PaperclipConfig, "database" | "server">): boolean {
return config.server.deploymentMode === "authenticated" && config.database.mode !== "embedded-postgres";
}
export async function onboard(opts: OnboardOptions): Promise<void> {
printPaperclipCliBanner();
p.intro(pc.bgCyan(pc.black(" paperclipai onboard ")));
@@ -450,7 +454,7 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
"Next commands",
);
if (server.deploymentMode === "authenticated") {
if (canCreateBootstrapInviteImmediately({ database, server })) {
p.log.step("Generating bootstrap CEO invite");
await bootstrapCeoInvite({ config: configPath });
}
@@ -473,5 +477,15 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
return;
}
if (server.deploymentMode === "authenticated" && database.mode === "embedded-postgres") {
p.log.info(
[
"Bootstrap CEO invite will be created after the server starts.",
`Next: ${pc.cyan("paperclipai run")}`,
`Then: ${pc.cyan("paperclipai auth bootstrap-ceo")}`,
].join("\n"),
);
}
p.outro("You're all set!");
}

View File

@@ -3,9 +3,13 @@ import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import * as p from "@clack/prompts";
import pc from "picocolors";
import { bootstrapCeoInvite } from "./auth-bootstrap-ceo.js";
import { onboard } from "./onboard.js";
import { doctor } from "./doctor.js";
import { loadPaperclipEnvFile } from "../config/env.js";
import { configExists, resolveConfigPath } from "../config/store.js";
import type { PaperclipConfig } from "../config/schema.js";
import { readConfig } from "../config/store.js";
import {
describeLocalInstancePaths,
resolvePaperclipHomeDir,
@@ -19,6 +23,13 @@ interface RunOptions {
yes?: boolean;
}
interface StartedServer {
apiUrl: string;
databaseUrl: string;
host: string;
listenPort: number;
}
export async function runCommand(opts: RunOptions): Promise<void> {
const instanceId = resolvePaperclipInstanceId(opts.instance);
process.env.PAPERCLIP_INSTANCE_ID = instanceId;
@@ -31,6 +42,7 @@ export async function runCommand(opts: RunOptions): Promise<void> {
const configPath = resolveConfigPath(opts.config);
process.env.PAPERCLIP_CONFIG = configPath;
loadPaperclipEnvFile(configPath);
p.intro(pc.bgCyan(pc.black(" paperclipai run ")));
p.log.message(pc.dim(`Home: ${paths.homeDir}`));
@@ -60,8 +72,23 @@ export async function runCommand(opts: RunOptions): Promise<void> {
process.exit(1);
}
const config = readConfig(configPath);
if (!config) {
p.log.error(`No config found at ${configPath}.`);
process.exit(1);
}
p.log.step("Starting Paperclip server...");
await importServerEntry();
const startedServer = await importServerEntry();
if (shouldGenerateBootstrapInviteAfterStart(config)) {
p.log.step("Generating bootstrap CEO invite");
await bootstrapCeoInvite({
config: configPath,
dbUrl: startedServer.databaseUrl,
baseUrl: startedServer.apiUrl.replace(/\/api$/, ""),
});
}
}
function formatError(err: unknown): string {
@@ -101,19 +128,20 @@ function maybeEnableUiDevMiddleware(entrypoint: string): void {
}
}
async function importServerEntry(): Promise<void> {
async function importServerEntry(): Promise<StartedServer> {
// Dev mode: try local workspace path (monorepo with tsx)
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
const devEntry = path.resolve(projectRoot, "server/src/index.ts");
if (fs.existsSync(devEntry)) {
maybeEnableUiDevMiddleware(devEntry);
await import(pathToFileURL(devEntry).href);
return;
const mod = await import(pathToFileURL(devEntry).href);
return await startServerFromModule(mod, devEntry);
}
// Production mode: import the published @paperclipai/server package
try {
await import("@paperclipai/server");
const mod = await import("@paperclipai/server");
return await startServerFromModule(mod, "@paperclipai/server");
} catch (err) {
const missingSpecifier = getMissingModuleSpecifier(err);
const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@paperclipai/server";
@@ -130,3 +158,15 @@ async function importServerEntry(): Promise<void> {
);
}
}
function shouldGenerateBootstrapInviteAfterStart(config: PaperclipConfig): boolean {
return config.server.deploymentMode === "authenticated" && config.database.mode === "embedded-postgres";
}
async function startServerFromModule(mod: unknown, label: string): Promise<StartedServer> {
const startServer = (mod as { startServer?: () => Promise<StartedServer> }).startServer;
if (typeof startServer !== "function") {
throw new Error(`Paperclip server entrypoint did not export startServer(): ${label}`);
}
return await startServer();
}

View File

@@ -36,6 +36,10 @@ export function resolveAgentJwtEnvFile(configPath?: string): string {
return resolveEnvFilePath(configPath);
}
export function loadPaperclipEnvFile(configPath?: string): void {
loadAgentJwtEnvFile(resolveEnvFilePath(configPath));
}
export function loadAgentJwtEnvFile(filePath = resolveEnvFilePath()): void {
if (loadedEnvFiles.has(filePath)) return;

View File

@@ -126,6 +126,8 @@ This is the best existing fit when you want:
- a dedicated host port
- an end-to-end `npx paperclipai ... onboard` check
In authenticated/private mode, the expected result is a full authenticated onboarding flow, including printing the bootstrap CEO invite once startup completes.
If you want to exercise onboarding from a fresh local checkout rather than npm, use:
```bash

File diff suppressed because it is too large Load Diff