mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 02:44:50 +01:00
fix(cli): decline the service install for configs the service never reads
The supervisor starts run --instance with only the home baked in, so the service always reads the default instance config path. When onboarding selected a custom config (--config, PAPERCLIP_CONFIG, or an ancestor file), it still installed a service that would serve the default config or exit. Decline the install for non-default configs with the reason and the foreground alternative, mirroring the custom-shim decline. The announcement no longer needs a mismatch branch: an installed service implies the default config.
This commit is contained in:
1 parent
c664a5de76
commit
55872ca83c
3 files changed
+56
-25
No files matched your search
@@ -44,6 +44,39 @@ describe("onboard service policy", () => {
|
||||
expect(info).toHaveBeenCalledWith(expect.stringContaining("--install-service"));
|
||||
});
|
||||
|
||||
it("declines a custom config path the service would never read", async () => {
|
||||
const warn = vi.fn();
|
||||
const detect = vi.fn();
|
||||
const installed = await handleOnboardService(
|
||||
{ yes: true, installService: true, configPath: "/custom/paperclip.json" },
|
||||
{
|
||||
detect,
|
||||
warn,
|
||||
serviceConfigPath: () => "/home/user/.paperclip/instances/default/config.json",
|
||||
ensureServiceShim: vi.fn(async () => ({ ok: true, installedNow: false })),
|
||||
},
|
||||
);
|
||||
expect(installed).toBe(false);
|
||||
expect(detect).not.toHaveBeenCalled();
|
||||
const message = warn.mock.calls[0]?.[0] as string;
|
||||
expect(message).toContain("/home/user/.paperclip/instances/default/config.json");
|
||||
expect(message).toContain("/custom/paperclip.json");
|
||||
});
|
||||
|
||||
it("installs when the config path is the one the service reads", async () => {
|
||||
const detection = supportedDetection();
|
||||
const installed = await handleOnboardService(
|
||||
{ yes: true, installService: true, configPath: "/home/user/.paperclip/instances/default/config.json" },
|
||||
{
|
||||
detect: vi.fn(async () => detection),
|
||||
isInteractive: () => false,
|
||||
serviceConfigPath: () => "/home/user/.paperclip/instances/default/config.json",
|
||||
ensureServiceShim: vi.fn(async () => ({ ok: true, installedNow: false })),
|
||||
},
|
||||
);
|
||||
expect(installed).toBe(true);
|
||||
});
|
||||
|
||||
it("installs when --yes explicitly opts in", async () => {
|
||||
const detection = supportedDetection();
|
||||
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
resolveDefaultBackupDir,
|
||||
resolveDefaultEmbeddedPostgresDir,
|
||||
resolveDefaultLogsDir,
|
||||
resolveDefaultConfigPath,
|
||||
resolvePaperclipInstanceId,
|
||||
} from "../config/home.js";
|
||||
import { bootstrapCeoInvite } from "./auth-bootstrap-ceo.js";
|
||||
@@ -130,23 +129,6 @@ function resolvePathFromEnv(rawValue: string | undefined): string | null {
|
||||
return path.resolve(expandHomePrefix(rawValue.trim()));
|
||||
}
|
||||
|
||||
async function announceInstalledService(options: { configPath: string; baseUrl: string }): Promise<void> {
|
||||
const serviceConfigPath = resolveDefaultConfigPath(resolvePaperclipInstanceId());
|
||||
if (path.resolve(options.configPath) === path.resolve(serviceConfigPath)) {
|
||||
await announceServiceReady({
|
||||
baseUrl: options.baseUrl,
|
||||
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
||||
});
|
||||
return;
|
||||
}
|
||||
p.log.info(
|
||||
[
|
||||
`The background service reads ${serviceConfigPath}, not ${options.configPath}.`,
|
||||
`Check ${pc.cyan("paperclipai service status")} for the served address.`,
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
|
||||
function describeServerBinding(server: Pick<PaperclipConfig["server"], "bind" | "customBindHost" | "host" | "port">): string {
|
||||
const bind = server.bind ?? inferBindModeFromHost(server.host);
|
||||
const detail =
|
||||
@@ -475,11 +457,11 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
|
||||
);
|
||||
|
||||
printManagedInstallHint();
|
||||
const serviceInstalled = await handleOnboardService(opts);
|
||||
const serviceInstalled = await handleOnboardService({ ...opts, configPath });
|
||||
if (serviceInstalled) {
|
||||
await announceInstalledService({
|
||||
configPath,
|
||||
await announceServiceReady({
|
||||
baseUrl: buildLocalBaseUrl(existingConfig.server.host, existingConfig.server.port),
|
||||
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -747,11 +729,11 @@ export async function onboard(opts: OnboardOptions): Promise<void> {
|
||||
await bootstrapCeoInvite({ config: configPath });
|
||||
}
|
||||
|
||||
const serviceInstalled = await handleOnboardService(opts);
|
||||
const serviceInstalled = await handleOnboardService({ ...opts, configPath });
|
||||
if (serviceInstalled) {
|
||||
await announceInstalledService({
|
||||
configPath,
|
||||
await announceServiceReady({
|
||||
baseUrl: buildLocalBaseUrl(server.host, server.port),
|
||||
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import path from "node:path";
|
||||
import * as p from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { installCommand } from "./commands/install.js";
|
||||
import { resolvePaperclipInstanceId } from "./config/home.js";
|
||||
import { resolveDefaultConfigPath, resolvePaperclipInstanceId } from "./config/home.js";
|
||||
import {
|
||||
readInstallManifest,
|
||||
resolveInstallStorePaths,
|
||||
@@ -19,6 +19,7 @@ import { packageVersion } from "./version.js";
|
||||
export type OnboardServiceOptions = {
|
||||
yes?: boolean;
|
||||
installService?: boolean;
|
||||
configPath?: string;
|
||||
};
|
||||
|
||||
type EnsureShimResult = { ok: boolean; installedNow: boolean; reason?: string };
|
||||
@@ -36,6 +37,7 @@ type OnboardServiceDependencies = {
|
||||
confirm: () => Promise<boolean>;
|
||||
confirmLinger: () => Promise<boolean>;
|
||||
isInteractive: () => boolean;
|
||||
serviceConfigPath: (instanceId: string) => string;
|
||||
info: (message: string) => void;
|
||||
success: (message: string) => void;
|
||||
warn: (message: string) => void;
|
||||
@@ -112,6 +114,7 @@ const defaultDependencies: OnboardServiceDependencies = {
|
||||
return !p.isCancel(answer) && answer === true;
|
||||
},
|
||||
isInteractive: () => process.stdin.isTTY === true && process.stdout.isTTY === true,
|
||||
serviceConfigPath: (instanceId) => resolveDefaultConfigPath(instanceId),
|
||||
info: (message) => p.log.message(pc.dim(message)),
|
||||
success: (message) => p.log.success(message),
|
||||
warn: (message) => p.log.warn(message),
|
||||
@@ -134,6 +137,19 @@ export async function handleOnboardService(
|
||||
}
|
||||
|
||||
const instanceId = resolvePaperclipInstanceId();
|
||||
|
||||
// The supervisor starts `run --instance <id>` with only the home baked in,
|
||||
// so the service always reads the instance's default config path. Installing
|
||||
// it for a custom config would leave that config unserved.
|
||||
const serviceConfigPath = deps.serviceConfigPath(instanceId);
|
||||
if (options.configPath && path.resolve(options.configPath) !== path.resolve(serviceConfigPath)) {
|
||||
const reason =
|
||||
`the background service reads ${serviceConfigPath}, not ${options.configPath}. ` +
|
||||
"Move the configuration to the default path, or run the server in the foreground with `paperclipai run`.";
|
||||
if (explicitlyRequested) deps.warn(`Background service not installed: ${reason}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const detection = await deps.detect(instanceId);
|
||||
if (!detection.supported) {
|
||||
if (explicitlyRequested) deps.warn(detection.reason);
|
||||
|
||||
Reference in new issue
Block a user