Make onboarding reruns preserve existing config
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
f83a77f41f
commit
54b05d6d68
6 changed files with 188 additions and 2 deletions
|
|
@ -177,6 +177,8 @@ Open source. Self-hosted. No Paperclip account required.
|
||||||
npx paperclipai onboard --yes
|
npx paperclipai onboard --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings.
|
||||||
|
|
||||||
Or manually:
|
Or manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,8 @@ Open source. Self-hosted. No Paperclip account required.
|
||||||
npx paperclipai onboard --yes
|
npx paperclipai onboard --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you already have Paperclip configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to edit settings.
|
||||||
|
|
||||||
Or manually:
|
Or manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
105
cli/src/__tests__/onboard.test.ts
Normal file
105
cli/src/__tests__/onboard.test.ts
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
|
import { onboard } from "../commands/onboard.js";
|
||||||
|
import type { PaperclipConfig } from "../config/schema.js";
|
||||||
|
|
||||||
|
const ORIGINAL_ENV = { ...process.env };
|
||||||
|
|
||||||
|
function createExistingConfigFixture() {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-onboard-"));
|
||||||
|
const runtimeRoot = path.join(root, "runtime");
|
||||||
|
const configPath = path.join(root, ".paperclip", "config.json");
|
||||||
|
const config: PaperclipConfig = {
|
||||||
|
$meta: {
|
||||||
|
version: 1,
|
||||||
|
updatedAt: "2026-03-29T00:00:00.000Z",
|
||||||
|
source: "configure",
|
||||||
|
},
|
||||||
|
database: {
|
||||||
|
mode: "embedded-postgres",
|
||||||
|
embeddedPostgresDataDir: path.join(runtimeRoot, "db"),
|
||||||
|
embeddedPostgresPort: 54329,
|
||||||
|
backup: {
|
||||||
|
enabled: true,
|
||||||
|
intervalMinutes: 60,
|
||||||
|
retentionDays: 30,
|
||||||
|
dir: path.join(runtimeRoot, "backups"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logging: {
|
||||||
|
mode: "file",
|
||||||
|
logDir: path.join(runtimeRoot, "logs"),
|
||||||
|
},
|
||||||
|
server: {
|
||||||
|
deploymentMode: "local_trusted",
|
||||||
|
exposure: "private",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 3100,
|
||||||
|
allowedHostnames: [],
|
||||||
|
serveUi: true,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
baseUrlMode: "auto",
|
||||||
|
disableSignUp: false,
|
||||||
|
},
|
||||||
|
storage: {
|
||||||
|
provider: "local_disk",
|
||||||
|
localDisk: {
|
||||||
|
baseDir: path.join(runtimeRoot, "storage"),
|
||||||
|
},
|
||||||
|
s3: {
|
||||||
|
bucket: "paperclip",
|
||||||
|
region: "us-east-1",
|
||||||
|
prefix: "",
|
||||||
|
forcePathStyle: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
provider: "local_encrypted",
|
||||||
|
strictMode: false,
|
||||||
|
localEncrypted: {
|
||||||
|
keyFilePath: path.join(runtimeRoot, "secrets", "master.key"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
||||||
|
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 });
|
||||||
|
|
||||||
|
return { configPath, configText: fs.readFileSync(configPath, "utf8") };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("onboard", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV };
|
||||||
|
delete process.env.PAPERCLIP_AGENT_JWT_SECRET;
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY;
|
||||||
|
delete process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
process.env = { ...ORIGINAL_ENV };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves an existing config when rerun without flags", async () => {
|
||||||
|
const fixture = createExistingConfigFixture();
|
||||||
|
|
||||||
|
await onboard({ config: fixture.configPath });
|
||||||
|
|
||||||
|
expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText);
|
||||||
|
expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("preserves an existing config when rerun with --yes", async () => {
|
||||||
|
const fixture = createExistingConfigFixture();
|
||||||
|
|
||||||
|
await onboard({ config: fixture.configPath, yes: true, invokedByRun: true });
|
||||||
|
|
||||||
|
expect(fs.readFileSync(fixture.configPath, "utf8")).toBe(fixture.configText);
|
||||||
|
expect(fs.existsSync(`${fixture.configPath}.backup`)).toBe(false);
|
||||||
|
expect(fs.existsSync(path.join(path.dirname(fixture.configPath), ".env"))).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -244,11 +244,12 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let existingConfig: PaperclipConfig | null = null;
|
||||||
if (configExists(opts.config)) {
|
if (configExists(opts.config)) {
|
||||||
p.log.message(pc.dim(`${configPath} exists, updating config`));
|
p.log.message(pc.dim(`${configPath} exists`));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
readConfig(opts.config);
|
existingConfig = readConfig(opts.config);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
p.log.message(
|
p.log.message(
|
||||||
pc.yellow(
|
pc.yellow(
|
||||||
|
|
@ -258,6 +259,76 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (existingConfig) {
|
||||||
|
p.log.message(
|
||||||
|
pc.dim("Existing Paperclip install detected; keeping the current configuration unchanged."),
|
||||||
|
);
|
||||||
|
p.log.message(pc.dim(`Use ${pc.cyan("paperclipai configure")} if you want to change settings.`));
|
||||||
|
|
||||||
|
const jwtSecret = ensureAgentJwtSecret(configPath);
|
||||||
|
const envFilePath = resolveAgentJwtEnvFile(configPath);
|
||||||
|
if (jwtSecret.created) {
|
||||||
|
p.log.success(`Created ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
||||||
|
} else if (process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim()) {
|
||||||
|
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} from environment`);
|
||||||
|
} else {
|
||||||
|
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyResult = ensureLocalSecretsKeyFile(existingConfig, configPath);
|
||||||
|
if (keyResult.status === "created") {
|
||||||
|
p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`);
|
||||||
|
} else if (keyResult.status === "existing") {
|
||||||
|
p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
p.note(
|
||||||
|
[
|
||||||
|
"Existing config preserved",
|
||||||
|
`Database: ${existingConfig.database.mode}`,
|
||||||
|
existingConfig.llm ? `LLM: ${existingConfig.llm.provider}` : "LLM: not configured",
|
||||||
|
`Logging: ${existingConfig.logging.mode} -> ${existingConfig.logging.logDir}`,
|
||||||
|
`Server: ${existingConfig.server.deploymentMode}/${existingConfig.server.exposure} @ ${existingConfig.server.host}:${existingConfig.server.port}`,
|
||||||
|
`Allowed hosts: ${existingConfig.server.allowedHostnames.length > 0 ? existingConfig.server.allowedHostnames.join(", ") : "(loopback only)"}`,
|
||||||
|
`Auth URL mode: ${existingConfig.auth.baseUrlMode}${existingConfig.auth.publicBaseUrl ? ` (${existingConfig.auth.publicBaseUrl})` : ""}`,
|
||||||
|
`Storage: ${existingConfig.storage.provider}`,
|
||||||
|
`Secrets: ${existingConfig.secrets.provider} (strict mode ${existingConfig.secrets.strictMode ? "on" : "off"})`,
|
||||||
|
"Agent auth: PAPERCLIP_AGENT_JWT_SECRET configured",
|
||||||
|
].join("\n"),
|
||||||
|
"Configuration ready",
|
||||||
|
);
|
||||||
|
|
||||||
|
p.note(
|
||||||
|
[
|
||||||
|
`Run: ${pc.cyan("paperclipai run")}`,
|
||||||
|
`Reconfigure later: ${pc.cyan("paperclipai configure")}`,
|
||||||
|
`Diagnose setup: ${pc.cyan("paperclipai doctor")}`,
|
||||||
|
].join("\n"),
|
||||||
|
"Next commands",
|
||||||
|
);
|
||||||
|
|
||||||
|
let shouldRunNow = opts.run === true || opts.yes === true;
|
||||||
|
if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) {
|
||||||
|
const answer = await p.confirm({
|
||||||
|
message: "Start Paperclip now?",
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
if (!p.isCancel(answer)) {
|
||||||
|
shouldRunNow = answer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldRunNow && !opts.invokedByRun) {
|
||||||
|
process.env.PAPERCLIP_OPEN_ON_LISTEN = "true";
|
||||||
|
const { runCommand } = await import("./run.js");
|
||||||
|
await runCommand({ config: configPath, repair: true, yes: true });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.outro("Existing Paperclip setup is ready.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let setupMode: SetupMode = "quickstart";
|
let setupMode: SetupMode = "quickstart";
|
||||||
if (opts.yes) {
|
if (opts.yes) {
|
||||||
p.log.message(pc.dim("`--yes` enabled: using Quickstart defaults."));
|
p.log.message(pc.dim("`--yes` enabled: using Quickstart defaults."));
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,8 @@ Interactive first-time setup:
|
||||||
pnpm paperclipai onboard
|
pnpm paperclipai onboard
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If Paperclip is already configured, rerunning `onboard` keeps the existing config in place. Use `paperclipai configure` to change settings on an existing install.
|
||||||
|
|
||||||
First prompt:
|
First prompt:
|
||||||
|
|
||||||
1. `Quickstart` (recommended): local defaults (embedded database, no LLM provider, local disk storage, default secrets)
|
1. `Quickstart` (recommended): local defaults (embedded database, no LLM provider, local disk storage, default secrets)
|
||||||
|
|
@ -50,6 +52,8 @@ Non-interactive defaults + immediate start (opens browser on server listen):
|
||||||
pnpm paperclipai onboard --yes
|
pnpm paperclipai onboard --yes
|
||||||
```
|
```
|
||||||
|
|
||||||
|
On an existing install, `--yes` now preserves the current config and just starts Paperclip with that setup.
|
||||||
|
|
||||||
## `paperclipai doctor`
|
## `paperclipai doctor`
|
||||||
|
|
||||||
Health checks with optional auto-repair:
|
Health checks with optional auto-repair:
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ npx paperclipai onboard --yes
|
||||||
|
|
||||||
This walks you through setup, configures your environment, and gets Paperclip running.
|
This walks you through setup, configures your environment, and gets Paperclip running.
|
||||||
|
|
||||||
|
If you already have a Paperclip install, rerunning `onboard` keeps your current config and data paths intact. Use `paperclipai configure` if you want to edit settings.
|
||||||
|
|
||||||
To start Paperclip again later:
|
To start Paperclip again later:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue