offwatch 0.5.11 → 0.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +132 -178
- package/bin/offwatch.js +6 -7
- package/lib/downloader.js +112 -0
- package/package.json +17 -7
- package/postinstall.js +21 -0
- package/src/__tests__/agent-jwt-env.test.ts +0 -79
- package/src/__tests__/allowed-hostname.test.ts +0 -80
- package/src/__tests__/auth-command-registration.test.ts +0 -16
- package/src/__tests__/board-auth.test.ts +0 -53
- package/src/__tests__/common.test.ts +0 -98
- package/src/__tests__/company-delete.test.ts +0 -95
- package/src/__tests__/company-import-export-e2e.test.ts +0 -502
- package/src/__tests__/company-import-url.test.ts +0 -74
- package/src/__tests__/company-import-zip.test.ts +0 -44
- package/src/__tests__/company.test.ts +0 -599
- package/src/__tests__/context.test.ts +0 -70
- package/src/__tests__/data-dir.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -102
- package/src/__tests__/feedback.test.ts +0 -177
- package/src/__tests__/helpers/embedded-postgres.ts +0 -6
- package/src/__tests__/helpers/zip.ts +0 -87
- package/src/__tests__/home-paths.test.ts +0 -44
- package/src/__tests__/http.test.ts +0 -106
- package/src/__tests__/network-bind.test.ts +0 -62
- package/src/__tests__/onboard.test.ts +0 -166
- package/src/__tests__/routines.test.ts +0 -249
- package/src/__tests__/telemetry.test.ts +0 -117
- package/src/__tests__/worktree-merge-history.test.ts +0 -492
- package/src/__tests__/worktree.test.ts +0 -982
- package/src/adapters/http/format-event.ts +0 -4
- package/src/adapters/http/index.ts +0 -7
- package/src/adapters/index.ts +0 -2
- package/src/adapters/process/format-event.ts +0 -4
- package/src/adapters/process/index.ts +0 -7
- package/src/adapters/registry.ts +0 -63
- package/src/checks/agent-jwt-secret-check.ts +0 -40
- package/src/checks/config-check.ts +0 -33
- package/src/checks/database-check.ts +0 -59
- package/src/checks/deployment-auth-check.ts +0 -88
- package/src/checks/index.ts +0 -18
- package/src/checks/llm-check.ts +0 -82
- package/src/checks/log-check.ts +0 -30
- package/src/checks/path-resolver.ts +0 -1
- package/src/checks/port-check.ts +0 -24
- package/src/checks/secrets-check.ts +0 -146
- package/src/checks/storage-check.ts +0 -51
- package/src/client/board-auth.ts +0 -282
- package/src/client/command-label.ts +0 -4
- package/src/client/context.ts +0 -175
- package/src/client/http.ts +0 -255
- package/src/commands/allowed-hostname.ts +0 -40
- package/src/commands/auth-bootstrap-ceo.ts +0 -138
- package/src/commands/client/activity.ts +0 -71
- package/src/commands/client/agent.ts +0 -315
- package/src/commands/client/approval.ts +0 -259
- package/src/commands/client/auth.ts +0 -113
- package/src/commands/client/common.ts +0 -221
- package/src/commands/client/company.ts +0 -1578
- package/src/commands/client/context.ts +0 -125
- package/src/commands/client/dashboard.ts +0 -34
- package/src/commands/client/feedback.ts +0 -645
- package/src/commands/client/issue.ts +0 -411
- package/src/commands/client/plugin.ts +0 -374
- package/src/commands/client/zip.ts +0 -129
- package/src/commands/configure.ts +0 -201
- package/src/commands/db-backup.ts +0 -102
- package/src/commands/doctor.ts +0 -203
- package/src/commands/env.ts +0 -411
- package/src/commands/heartbeat-run.ts +0 -344
- package/src/commands/onboard.ts +0 -692
- package/src/commands/routines.ts +0 -352
- package/src/commands/run.ts +0 -216
- package/src/commands/worktree-lib.ts +0 -279
- package/src/commands/worktree-merge-history-lib.ts +0 -764
- package/src/commands/worktree.ts +0 -2876
- package/src/config/data-dir.ts +0 -48
- package/src/config/env.ts +0 -125
- package/src/config/home.ts +0 -80
- package/src/config/hostnames.ts +0 -26
- package/src/config/schema.ts +0 -30
- package/src/config/secrets-key.ts +0 -48
- package/src/config/server-bind.ts +0 -183
- package/src/config/store.ts +0 -120
- package/src/index.ts +0 -182
- package/src/prompts/database.ts +0 -157
- package/src/prompts/llm.ts +0 -43
- package/src/prompts/logging.ts +0 -37
- package/src/prompts/secrets.ts +0 -99
- package/src/prompts/server.ts +0 -221
- package/src/prompts/storage.ts +0 -146
- package/src/telemetry.ts +0 -49
- package/src/utils/banner.ts +0 -24
- package/src/utils/net.ts +0 -18
- package/src/utils/path-resolver.ts +0 -25
- package/src/version.ts +0 -10
package/src/commands/onboard.ts
DELETED
|
@@ -1,692 +0,0 @@
|
|
|
1
|
-
import * as p from "@clack/prompts";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import pc from "picocolors";
|
|
4
|
-
import {
|
|
5
|
-
AUTH_BASE_URL_MODES,
|
|
6
|
-
BIND_MODES,
|
|
7
|
-
DEPLOYMENT_EXPOSURES,
|
|
8
|
-
DEPLOYMENT_MODES,
|
|
9
|
-
SECRET_PROVIDERS,
|
|
10
|
-
STORAGE_PROVIDERS,
|
|
11
|
-
inferBindModeFromHost,
|
|
12
|
-
resolveRuntimeBind,
|
|
13
|
-
type BindMode,
|
|
14
|
-
type AuthBaseUrlMode,
|
|
15
|
-
type DeploymentExposure,
|
|
16
|
-
type DeploymentMode,
|
|
17
|
-
type SecretProvider,
|
|
18
|
-
type StorageProvider,
|
|
19
|
-
} from "@paperclipai/shared";
|
|
20
|
-
import { configExists, readConfig, resolveConfigPath, writeConfig } from "../config/store.js";
|
|
21
|
-
import type { PaperclipConfig } from "../config/schema.js";
|
|
22
|
-
import { ensureAgentJwtSecret, resolveAgentJwtEnvFile } from "../config/env.js";
|
|
23
|
-
import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js";
|
|
24
|
-
import { promptDatabase } from "../prompts/database.js";
|
|
25
|
-
import { promptLlm } from "../prompts/llm.js";
|
|
26
|
-
import { promptLogging } from "../prompts/logging.js";
|
|
27
|
-
import { defaultSecretsConfig } from "../prompts/secrets.js";
|
|
28
|
-
import { defaultStorageConfig, promptStorage } from "../prompts/storage.js";
|
|
29
|
-
import { promptServer } from "../prompts/server.js";
|
|
30
|
-
import { buildPresetServerConfig } from "../config/server-bind.js";
|
|
31
|
-
import {
|
|
32
|
-
describeLocalInstancePaths,
|
|
33
|
-
expandHomePrefix,
|
|
34
|
-
resolveDefaultBackupDir,
|
|
35
|
-
resolveDefaultEmbeddedPostgresDir,
|
|
36
|
-
resolveDefaultLogsDir,
|
|
37
|
-
resolvePaperclipInstanceId,
|
|
38
|
-
} from "../config/home.js";
|
|
39
|
-
import { bootstrapCeoInvite } from "./auth-bootstrap-ceo.js";
|
|
40
|
-
import { printPaperclipCliBanner } from "../utils/banner.js";
|
|
41
|
-
import {
|
|
42
|
-
getTelemetryClient,
|
|
43
|
-
trackInstallStarted,
|
|
44
|
-
trackInstallCompleted,
|
|
45
|
-
} from "../telemetry.js";
|
|
46
|
-
|
|
47
|
-
type SetupMode = "quickstart" | "advanced";
|
|
48
|
-
|
|
49
|
-
type OnboardOptions = {
|
|
50
|
-
config?: string;
|
|
51
|
-
run?: boolean;
|
|
52
|
-
yes?: boolean;
|
|
53
|
-
invokedByRun?: boolean;
|
|
54
|
-
bind?: BindMode;
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
type OnboardDefaults = Pick<PaperclipConfig, "database" | "logging" | "server" | "auth" | "storage" | "secrets">;
|
|
58
|
-
|
|
59
|
-
const TAILNET_BIND_WARNING =
|
|
60
|
-
"No Tailscale address was detected during setup. The saved config will stay on loopback until Tailscale is available or PAPERCLIP_TAILNET_BIND_HOST is set.";
|
|
61
|
-
|
|
62
|
-
const ONBOARD_ENV_KEYS = [
|
|
63
|
-
"PAPERCLIP_PUBLIC_URL",
|
|
64
|
-
"DATABASE_URL",
|
|
65
|
-
"PAPERCLIP_DB_BACKUP_ENABLED",
|
|
66
|
-
"PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES",
|
|
67
|
-
"PAPERCLIP_DB_BACKUP_RETENTION_DAYS",
|
|
68
|
-
"PAPERCLIP_DB_BACKUP_DIR",
|
|
69
|
-
"PAPERCLIP_DEPLOYMENT_MODE",
|
|
70
|
-
"PAPERCLIP_DEPLOYMENT_EXPOSURE",
|
|
71
|
-
"PAPERCLIP_BIND",
|
|
72
|
-
"PAPERCLIP_BIND_HOST",
|
|
73
|
-
"PAPERCLIP_TAILNET_BIND_HOST",
|
|
74
|
-
"HOST",
|
|
75
|
-
"PORT",
|
|
76
|
-
"SERVE_UI",
|
|
77
|
-
"PAPERCLIP_ALLOWED_HOSTNAMES",
|
|
78
|
-
"PAPERCLIP_AUTH_BASE_URL_MODE",
|
|
79
|
-
"PAPERCLIP_AUTH_PUBLIC_BASE_URL",
|
|
80
|
-
"BETTER_AUTH_URL",
|
|
81
|
-
"BETTER_AUTH_BASE_URL",
|
|
82
|
-
"PAPERCLIP_STORAGE_PROVIDER",
|
|
83
|
-
"PAPERCLIP_STORAGE_LOCAL_DIR",
|
|
84
|
-
"PAPERCLIP_STORAGE_S3_BUCKET",
|
|
85
|
-
"PAPERCLIP_STORAGE_S3_REGION",
|
|
86
|
-
"PAPERCLIP_STORAGE_S3_ENDPOINT",
|
|
87
|
-
"PAPERCLIP_STORAGE_S3_PREFIX",
|
|
88
|
-
"PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE",
|
|
89
|
-
"PAPERCLIP_SECRETS_PROVIDER",
|
|
90
|
-
"PAPERCLIP_SECRETS_STRICT_MODE",
|
|
91
|
-
"PAPERCLIP_SECRETS_MASTER_KEY_FILE",
|
|
92
|
-
] as const;
|
|
93
|
-
|
|
94
|
-
function parseBooleanFromEnv(rawValue: string | undefined): boolean | null {
|
|
95
|
-
if (rawValue === undefined) return null;
|
|
96
|
-
const lower = rawValue.trim().toLowerCase();
|
|
97
|
-
if (lower === "true" || lower === "1" || lower === "yes") return true;
|
|
98
|
-
if (lower === "false" || lower === "0" || lower === "no") return false;
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function parseNumberFromEnv(rawValue: string | undefined): number | null {
|
|
103
|
-
if (!rawValue) return null;
|
|
104
|
-
const parsed = Number(rawValue);
|
|
105
|
-
if (!Number.isFinite(parsed)) return null;
|
|
106
|
-
return parsed;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function parseEnumFromEnv<T extends string>(rawValue: string | undefined, allowedValues: readonly T[]): T | null {
|
|
110
|
-
if (!rawValue) return null;
|
|
111
|
-
return allowedValues.includes(rawValue as T) ? (rawValue as T) : null;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function resolvePathFromEnv(rawValue: string | undefined): string | null {
|
|
115
|
-
if (!rawValue || rawValue.trim().length === 0) return null;
|
|
116
|
-
return path.resolve(expandHomePrefix(rawValue.trim()));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function describeServerBinding(server: Pick<PaperclipConfig["server"], "bind" | "customBindHost" | "host" | "port">): string {
|
|
120
|
-
const bind = server.bind ?? inferBindModeFromHost(server.host);
|
|
121
|
-
const detail =
|
|
122
|
-
bind === "custom"
|
|
123
|
-
? server.customBindHost ?? server.host
|
|
124
|
-
: bind === "tailnet"
|
|
125
|
-
? "detected tailscale address"
|
|
126
|
-
: server.host;
|
|
127
|
-
return `${bind}${detail ? ` (${detail})` : ""}:${server.port}`;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function quickstartDefaultsFromEnv(opts?: { preferTrustedLocal?: boolean }): {
|
|
131
|
-
defaults: OnboardDefaults;
|
|
132
|
-
usedEnvKeys: string[];
|
|
133
|
-
ignoredEnvKeys: Array<{ key: string; reason: string }>;
|
|
134
|
-
} {
|
|
135
|
-
const preferTrustedLocal = opts?.preferTrustedLocal ?? false;
|
|
136
|
-
const instanceId = resolvePaperclipInstanceId();
|
|
137
|
-
const defaultStorage = defaultStorageConfig();
|
|
138
|
-
const defaultSecrets = defaultSecretsConfig();
|
|
139
|
-
const databaseUrl = process.env.DATABASE_URL?.trim() || undefined;
|
|
140
|
-
const publicUrl = preferTrustedLocal
|
|
141
|
-
? undefined
|
|
142
|
-
: (
|
|
143
|
-
process.env.PAPERCLIP_PUBLIC_URL?.trim() ||
|
|
144
|
-
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL?.trim() ||
|
|
145
|
-
process.env.BETTER_AUTH_URL?.trim() ||
|
|
146
|
-
process.env.BETTER_AUTH_BASE_URL?.trim() ||
|
|
147
|
-
undefined
|
|
148
|
-
);
|
|
149
|
-
const deploymentMode = preferTrustedLocal
|
|
150
|
-
? "local_trusted"
|
|
151
|
-
: (parseEnumFromEnv<DeploymentMode>(process.env.PAPERCLIP_DEPLOYMENT_MODE, DEPLOYMENT_MODES) ?? "local_trusted");
|
|
152
|
-
const deploymentExposureFromEnv = parseEnumFromEnv<DeploymentExposure>(
|
|
153
|
-
process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE,
|
|
154
|
-
DEPLOYMENT_EXPOSURES,
|
|
155
|
-
);
|
|
156
|
-
const deploymentExposure =
|
|
157
|
-
deploymentMode === "local_trusted" ? "private" : (deploymentExposureFromEnv ?? "private");
|
|
158
|
-
const bindFromEnv = parseEnumFromEnv<BindMode>(process.env.PAPERCLIP_BIND, BIND_MODES);
|
|
159
|
-
const customBindHostFromEnv = process.env.PAPERCLIP_BIND_HOST?.trim() || undefined;
|
|
160
|
-
const hostFromEnv = process.env.HOST?.trim() || undefined;
|
|
161
|
-
const configuredBindHost = customBindHostFromEnv ?? hostFromEnv;
|
|
162
|
-
const bind = preferTrustedLocal
|
|
163
|
-
? "loopback"
|
|
164
|
-
: (
|
|
165
|
-
deploymentMode === "local_trusted"
|
|
166
|
-
? "loopback"
|
|
167
|
-
: (bindFromEnv ?? (configuredBindHost ? inferBindModeFromHost(configuredBindHost) : "lan"))
|
|
168
|
-
);
|
|
169
|
-
const resolvedBind = resolveRuntimeBind({
|
|
170
|
-
bind,
|
|
171
|
-
host: hostFromEnv ?? (bind === "loopback" ? "127.0.0.1" : "0.0.0.0"),
|
|
172
|
-
customBindHost: customBindHostFromEnv,
|
|
173
|
-
tailnetBindHost: process.env.PAPERCLIP_TAILNET_BIND_HOST?.trim(),
|
|
174
|
-
});
|
|
175
|
-
const authPublicBaseUrl = publicUrl;
|
|
176
|
-
const authBaseUrlModeFromEnv = parseEnumFromEnv<AuthBaseUrlMode>(
|
|
177
|
-
process.env.PAPERCLIP_AUTH_BASE_URL_MODE,
|
|
178
|
-
AUTH_BASE_URL_MODES,
|
|
179
|
-
);
|
|
180
|
-
const authBaseUrlMode = authBaseUrlModeFromEnv ?? (authPublicBaseUrl ? "explicit" : "auto");
|
|
181
|
-
const allowedHostnamesFromEnv = process.env.PAPERCLIP_ALLOWED_HOSTNAMES
|
|
182
|
-
? process.env.PAPERCLIP_ALLOWED_HOSTNAMES
|
|
183
|
-
.split(",")
|
|
184
|
-
.map((value) => value.trim().toLowerCase())
|
|
185
|
-
.filter((value) => value.length > 0)
|
|
186
|
-
: [];
|
|
187
|
-
const hostnameFromPublicUrl = publicUrl
|
|
188
|
-
? (() => {
|
|
189
|
-
try {
|
|
190
|
-
return new URL(publicUrl).hostname.trim().toLowerCase();
|
|
191
|
-
} catch {
|
|
192
|
-
return null;
|
|
193
|
-
}
|
|
194
|
-
})()
|
|
195
|
-
: null;
|
|
196
|
-
const storageProvider =
|
|
197
|
-
parseEnumFromEnv<StorageProvider>(process.env.PAPERCLIP_STORAGE_PROVIDER, STORAGE_PROVIDERS) ??
|
|
198
|
-
defaultStorage.provider;
|
|
199
|
-
const secretsProvider =
|
|
200
|
-
parseEnumFromEnv<SecretProvider>(process.env.PAPERCLIP_SECRETS_PROVIDER, SECRET_PROVIDERS) ??
|
|
201
|
-
defaultSecrets.provider;
|
|
202
|
-
const databaseBackupEnabled = parseBooleanFromEnv(process.env.PAPERCLIP_DB_BACKUP_ENABLED) ?? true;
|
|
203
|
-
const databaseBackupIntervalMinutes = Math.max(
|
|
204
|
-
1,
|
|
205
|
-
parseNumberFromEnv(process.env.PAPERCLIP_DB_BACKUP_INTERVAL_MINUTES) ?? 60,
|
|
206
|
-
);
|
|
207
|
-
const databaseBackupRetentionDays = Math.max(
|
|
208
|
-
1,
|
|
209
|
-
parseNumberFromEnv(process.env.PAPERCLIP_DB_BACKUP_RETENTION_DAYS) ?? 30,
|
|
210
|
-
);
|
|
211
|
-
const defaults: OnboardDefaults = {
|
|
212
|
-
database: {
|
|
213
|
-
mode: databaseUrl ? "postgres" : "embedded-postgres",
|
|
214
|
-
...(databaseUrl ? { connectionString: databaseUrl } : {}),
|
|
215
|
-
embeddedPostgresDataDir: resolveDefaultEmbeddedPostgresDir(instanceId),
|
|
216
|
-
embeddedPostgresPort: 54329,
|
|
217
|
-
backup: {
|
|
218
|
-
enabled: databaseBackupEnabled,
|
|
219
|
-
intervalMinutes: databaseBackupIntervalMinutes,
|
|
220
|
-
retentionDays: databaseBackupRetentionDays,
|
|
221
|
-
dir: resolvePathFromEnv(process.env.PAPERCLIP_DB_BACKUP_DIR) ?? resolveDefaultBackupDir(instanceId),
|
|
222
|
-
},
|
|
223
|
-
},
|
|
224
|
-
logging: {
|
|
225
|
-
mode: "file",
|
|
226
|
-
logDir: resolveDefaultLogsDir(instanceId),
|
|
227
|
-
},
|
|
228
|
-
server: {
|
|
229
|
-
deploymentMode,
|
|
230
|
-
exposure: deploymentExposure,
|
|
231
|
-
bind: resolvedBind.bind,
|
|
232
|
-
...(resolvedBind.customBindHost ? { customBindHost: resolvedBind.customBindHost } : {}),
|
|
233
|
-
host: resolvedBind.host,
|
|
234
|
-
port: Number(process.env.PORT) || 3100,
|
|
235
|
-
allowedHostnames: Array.from(new Set([...allowedHostnamesFromEnv, ...(hostnameFromPublicUrl ? [hostnameFromPublicUrl] : [])])),
|
|
236
|
-
serveUi: parseBooleanFromEnv(process.env.SERVE_UI) ?? true,
|
|
237
|
-
},
|
|
238
|
-
auth: {
|
|
239
|
-
baseUrlMode: authBaseUrlMode,
|
|
240
|
-
disableSignUp: false,
|
|
241
|
-
...(authPublicBaseUrl ? { publicBaseUrl: authPublicBaseUrl } : {}),
|
|
242
|
-
},
|
|
243
|
-
storage: {
|
|
244
|
-
provider: storageProvider,
|
|
245
|
-
localDisk: {
|
|
246
|
-
baseDir:
|
|
247
|
-
resolvePathFromEnv(process.env.PAPERCLIP_STORAGE_LOCAL_DIR) ?? defaultStorage.localDisk.baseDir,
|
|
248
|
-
},
|
|
249
|
-
s3: {
|
|
250
|
-
bucket: process.env.PAPERCLIP_STORAGE_S3_BUCKET ?? defaultStorage.s3.bucket,
|
|
251
|
-
region: process.env.PAPERCLIP_STORAGE_S3_REGION ?? defaultStorage.s3.region,
|
|
252
|
-
endpoint: process.env.PAPERCLIP_STORAGE_S3_ENDPOINT ?? defaultStorage.s3.endpoint,
|
|
253
|
-
prefix: process.env.PAPERCLIP_STORAGE_S3_PREFIX ?? defaultStorage.s3.prefix,
|
|
254
|
-
forcePathStyle:
|
|
255
|
-
parseBooleanFromEnv(process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE) ??
|
|
256
|
-
defaultStorage.s3.forcePathStyle,
|
|
257
|
-
},
|
|
258
|
-
},
|
|
259
|
-
secrets: {
|
|
260
|
-
provider: secretsProvider,
|
|
261
|
-
strictMode: parseBooleanFromEnv(process.env.PAPERCLIP_SECRETS_STRICT_MODE) ?? defaultSecrets.strictMode,
|
|
262
|
-
localEncrypted: {
|
|
263
|
-
keyFilePath:
|
|
264
|
-
resolvePathFromEnv(process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE) ??
|
|
265
|
-
defaultSecrets.localEncrypted.keyFilePath,
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
};
|
|
269
|
-
const ignoredEnvKeys: Array<{ key: string; reason: string }> = [];
|
|
270
|
-
if (preferTrustedLocal) {
|
|
271
|
-
const forcedLocalReason = "Ignored because --yes quickstart forces trusted local loopback defaults";
|
|
272
|
-
for (const key of [
|
|
273
|
-
"PAPERCLIP_DEPLOYMENT_MODE",
|
|
274
|
-
"PAPERCLIP_DEPLOYMENT_EXPOSURE",
|
|
275
|
-
"PAPERCLIP_BIND",
|
|
276
|
-
"PAPERCLIP_BIND_HOST",
|
|
277
|
-
"HOST",
|
|
278
|
-
"PAPERCLIP_AUTH_BASE_URL_MODE",
|
|
279
|
-
"PAPERCLIP_AUTH_PUBLIC_BASE_URL",
|
|
280
|
-
"PAPERCLIP_PUBLIC_URL",
|
|
281
|
-
"BETTER_AUTH_URL",
|
|
282
|
-
"BETTER_AUTH_BASE_URL",
|
|
283
|
-
] as const) {
|
|
284
|
-
if (process.env[key] !== undefined) {
|
|
285
|
-
ignoredEnvKeys.push({ key, reason: forcedLocalReason });
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_DEPLOYMENT_EXPOSURE !== undefined) {
|
|
290
|
-
ignoredEnvKeys.push({
|
|
291
|
-
key: "PAPERCLIP_DEPLOYMENT_EXPOSURE",
|
|
292
|
-
reason: "Ignored because deployment mode local_trusted always forces private exposure",
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_BIND !== undefined) {
|
|
296
|
-
ignoredEnvKeys.push({
|
|
297
|
-
key: "PAPERCLIP_BIND",
|
|
298
|
-
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
if (deploymentMode === "local_trusted" && process.env.PAPERCLIP_BIND_HOST !== undefined) {
|
|
302
|
-
ignoredEnvKeys.push({
|
|
303
|
-
key: "PAPERCLIP_BIND_HOST",
|
|
304
|
-
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
if (deploymentMode === "local_trusted" && process.env.HOST !== undefined) {
|
|
308
|
-
ignoredEnvKeys.push({
|
|
309
|
-
key: "HOST",
|
|
310
|
-
reason: "Ignored because deployment mode local_trusted always uses loopback reachability",
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const ignoredKeySet = new Set(ignoredEnvKeys.map((entry) => entry.key));
|
|
315
|
-
const usedEnvKeys = ONBOARD_ENV_KEYS.filter(
|
|
316
|
-
(key) => process.env[key] !== undefined && !ignoredKeySet.has(key),
|
|
317
|
-
);
|
|
318
|
-
return { defaults, usedEnvKeys, ignoredEnvKeys };
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function canCreateBootstrapInviteImmediately(config: Pick<PaperclipConfig, "database" | "server">): boolean {
|
|
322
|
-
return config.server.deploymentMode === "authenticated" && config.database.mode !== "embedded-postgres";
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
export async function onboard(opts: OnboardOptions): Promise<void> {
|
|
326
|
-
if (opts.bind && !["loopback", "lan", "tailnet"].includes(opts.bind)) {
|
|
327
|
-
throw new Error(`Unsupported bind preset for onboard: ${opts.bind}. Use loopback, lan, or tailnet.`);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
printPaperclipCliBanner();
|
|
331
|
-
p.intro(pc.bgCyan(pc.black(" paperclipai onboard ")));
|
|
332
|
-
const configPath = resolveConfigPath(opts.config);
|
|
333
|
-
const instance = describeLocalInstancePaths(resolvePaperclipInstanceId());
|
|
334
|
-
p.log.message(
|
|
335
|
-
pc.dim(
|
|
336
|
-
`Local home: ${instance.homeDir} | instance: ${instance.instanceId} | config: ${configPath}`,
|
|
337
|
-
),
|
|
338
|
-
);
|
|
339
|
-
|
|
340
|
-
let existingConfig: PaperclipConfig | null = null;
|
|
341
|
-
if (configExists(opts.config)) {
|
|
342
|
-
p.log.message(pc.dim(`${configPath} exists`));
|
|
343
|
-
|
|
344
|
-
try {
|
|
345
|
-
existingConfig = readConfig(opts.config);
|
|
346
|
-
} catch (err) {
|
|
347
|
-
p.log.message(
|
|
348
|
-
pc.yellow(
|
|
349
|
-
`Existing config appears invalid and will be updated.\n${err instanceof Error ? err.message : String(err)}`,
|
|
350
|
-
),
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
if (existingConfig) {
|
|
356
|
-
p.log.message(
|
|
357
|
-
pc.dim("Existing Paperclip install detected; keeping the current configuration unchanged."),
|
|
358
|
-
);
|
|
359
|
-
p.log.message(pc.dim(`Use ${pc.cyan("paperclipai configure")} if you want to change settings.`));
|
|
360
|
-
|
|
361
|
-
const jwtSecret = ensureAgentJwtSecret(configPath);
|
|
362
|
-
const envFilePath = resolveAgentJwtEnvFile(configPath);
|
|
363
|
-
if (jwtSecret.created) {
|
|
364
|
-
p.log.success(`Created ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
|
365
|
-
} else if (process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim()) {
|
|
366
|
-
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} from environment`);
|
|
367
|
-
} else {
|
|
368
|
-
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
const keyResult = ensureLocalSecretsKeyFile(existingConfig, configPath);
|
|
372
|
-
if (keyResult.status === "created") {
|
|
373
|
-
p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`);
|
|
374
|
-
} else if (keyResult.status === "existing") {
|
|
375
|
-
p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
p.note(
|
|
379
|
-
[
|
|
380
|
-
"Existing config preserved",
|
|
381
|
-
`Database: ${existingConfig.database.mode}`,
|
|
382
|
-
existingConfig.llm ? `LLM: ${existingConfig.llm.provider}` : "LLM: not configured",
|
|
383
|
-
`Logging: ${existingConfig.logging.mode} -> ${existingConfig.logging.logDir}`,
|
|
384
|
-
`Server: ${existingConfig.server.deploymentMode}/${existingConfig.server.exposure} @ ${describeServerBinding(existingConfig.server)}`,
|
|
385
|
-
`Allowed hosts: ${existingConfig.server.allowedHostnames.length > 0 ? existingConfig.server.allowedHostnames.join(", ") : "(loopback only)"}`,
|
|
386
|
-
`Auth URL mode: ${existingConfig.auth.baseUrlMode}${existingConfig.auth.publicBaseUrl ? ` (${existingConfig.auth.publicBaseUrl})` : ""}`,
|
|
387
|
-
`Storage: ${existingConfig.storage.provider}`,
|
|
388
|
-
`Secrets: ${existingConfig.secrets.provider} (strict mode ${existingConfig.secrets.strictMode ? "on" : "off"})`,
|
|
389
|
-
"Agent auth: PAPERCLIP_AGENT_JWT_SECRET configured",
|
|
390
|
-
].join("\n"),
|
|
391
|
-
"Configuration ready",
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
p.note(
|
|
395
|
-
[
|
|
396
|
-
`Run: ${pc.cyan("paperclipai run")}`,
|
|
397
|
-
`Reconfigure later: ${pc.cyan("paperclipai configure")}`,
|
|
398
|
-
`Diagnose setup: ${pc.cyan("paperclipai doctor")}`,
|
|
399
|
-
].join("\n"),
|
|
400
|
-
"Next commands",
|
|
401
|
-
);
|
|
402
|
-
|
|
403
|
-
let shouldRunNow = opts.run === true || opts.yes === true;
|
|
404
|
-
if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) {
|
|
405
|
-
const answer = await p.confirm({
|
|
406
|
-
message: "Start Paperclip now?",
|
|
407
|
-
initialValue: true,
|
|
408
|
-
});
|
|
409
|
-
if (!p.isCancel(answer)) {
|
|
410
|
-
shouldRunNow = answer;
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
if (shouldRunNow && !opts.invokedByRun) {
|
|
415
|
-
process.env.PAPERCLIP_OPEN_ON_LISTEN = "true";
|
|
416
|
-
const { runCommand } = await import("./run.js");
|
|
417
|
-
await runCommand({ config: configPath, repair: true, yes: true });
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
p.outro("Existing Paperclip setup is ready.");
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
let setupMode: SetupMode = "quickstart";
|
|
426
|
-
if (opts.yes) {
|
|
427
|
-
p.log.message(
|
|
428
|
-
pc.dim(
|
|
429
|
-
opts.bind
|
|
430
|
-
? `\`--yes\` enabled: using Quickstart defaults with bind=${opts.bind}.`
|
|
431
|
-
: "`--yes` enabled: using Quickstart defaults.",
|
|
432
|
-
),
|
|
433
|
-
);
|
|
434
|
-
} else {
|
|
435
|
-
const setupModeChoice = await p.select({
|
|
436
|
-
message: "Choose setup path",
|
|
437
|
-
options: [
|
|
438
|
-
{
|
|
439
|
-
value: "quickstart" as const,
|
|
440
|
-
label: "Quickstart",
|
|
441
|
-
hint: "Recommended: local defaults + ready to run",
|
|
442
|
-
},
|
|
443
|
-
{
|
|
444
|
-
value: "advanced" as const,
|
|
445
|
-
label: "Advanced setup",
|
|
446
|
-
hint: "Customize database, server, storage, and more",
|
|
447
|
-
},
|
|
448
|
-
],
|
|
449
|
-
initialValue: "quickstart",
|
|
450
|
-
});
|
|
451
|
-
if (p.isCancel(setupModeChoice)) {
|
|
452
|
-
p.cancel("Setup cancelled.");
|
|
453
|
-
return;
|
|
454
|
-
}
|
|
455
|
-
setupMode = setupModeChoice as SetupMode;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
const tc = getTelemetryClient();
|
|
459
|
-
if (tc) trackInstallStarted(tc);
|
|
460
|
-
|
|
461
|
-
let llm: PaperclipConfig["llm"] | undefined;
|
|
462
|
-
const { defaults: derivedDefaults, usedEnvKeys, ignoredEnvKeys } = quickstartDefaultsFromEnv({
|
|
463
|
-
preferTrustedLocal: opts.yes === true && !opts.bind,
|
|
464
|
-
});
|
|
465
|
-
let {
|
|
466
|
-
database,
|
|
467
|
-
logging,
|
|
468
|
-
server,
|
|
469
|
-
auth,
|
|
470
|
-
storage,
|
|
471
|
-
secrets,
|
|
472
|
-
} = derivedDefaults;
|
|
473
|
-
|
|
474
|
-
if (opts.bind === "loopback" || opts.bind === "lan" || opts.bind === "tailnet") {
|
|
475
|
-
const preset = buildPresetServerConfig(opts.bind, {
|
|
476
|
-
port: server.port,
|
|
477
|
-
allowedHostnames: server.allowedHostnames,
|
|
478
|
-
serveUi: server.serveUi,
|
|
479
|
-
});
|
|
480
|
-
server = preset.server;
|
|
481
|
-
auth = preset.auth;
|
|
482
|
-
if (opts.bind === "tailnet" && server.host === "127.0.0.1") {
|
|
483
|
-
p.log.warn(TAILNET_BIND_WARNING);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (setupMode === "advanced") {
|
|
488
|
-
p.log.step(pc.bold("Database"));
|
|
489
|
-
database = await promptDatabase(database);
|
|
490
|
-
|
|
491
|
-
if (database.mode === "postgres" && database.connectionString) {
|
|
492
|
-
const s = p.spinner();
|
|
493
|
-
s.start("Testing database connection...");
|
|
494
|
-
try {
|
|
495
|
-
const { createDb } = await import("@paperclipai/db");
|
|
496
|
-
const db = createDb(database.connectionString);
|
|
497
|
-
await db.execute("SELECT 1");
|
|
498
|
-
s.stop("Database connection successful");
|
|
499
|
-
} catch {
|
|
500
|
-
s.stop(pc.yellow("Could not connect to database — you can fix this later with `paperclipai doctor`"));
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
p.log.step(pc.bold("LLM Provider"));
|
|
505
|
-
llm = await promptLlm();
|
|
506
|
-
|
|
507
|
-
if (llm?.apiKey) {
|
|
508
|
-
const s = p.spinner();
|
|
509
|
-
s.start("Validating API key...");
|
|
510
|
-
try {
|
|
511
|
-
if (llm.provider === "claude") {
|
|
512
|
-
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
513
|
-
method: "POST",
|
|
514
|
-
headers: {
|
|
515
|
-
"x-api-key": llm.apiKey,
|
|
516
|
-
"anthropic-version": "2023-06-01",
|
|
517
|
-
"content-type": "application/json",
|
|
518
|
-
},
|
|
519
|
-
body: JSON.stringify({
|
|
520
|
-
model: "claude-sonnet-4-5-20250929",
|
|
521
|
-
max_tokens: 1,
|
|
522
|
-
messages: [{ role: "user", content: "hi" }],
|
|
523
|
-
}),
|
|
524
|
-
});
|
|
525
|
-
if (res.ok || res.status === 400) {
|
|
526
|
-
s.stop("API key is valid");
|
|
527
|
-
} else if (res.status === 401) {
|
|
528
|
-
s.stop(pc.yellow("API key appears invalid — you can update it later"));
|
|
529
|
-
} else {
|
|
530
|
-
s.stop(pc.yellow("Could not validate API key — continuing anyway"));
|
|
531
|
-
}
|
|
532
|
-
} else {
|
|
533
|
-
const res = await fetch("https://api.openai.com/v1/models", {
|
|
534
|
-
headers: { Authorization: `Bearer ${llm.apiKey}` },
|
|
535
|
-
});
|
|
536
|
-
if (res.ok) {
|
|
537
|
-
s.stop("API key is valid");
|
|
538
|
-
} else if (res.status === 401) {
|
|
539
|
-
s.stop(pc.yellow("API key appears invalid — you can update it later"));
|
|
540
|
-
} else {
|
|
541
|
-
s.stop(pc.yellow("Could not validate API key — continuing anyway"));
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
} catch {
|
|
545
|
-
s.stop(pc.yellow("Could not reach API — continuing anyway"));
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
p.log.step(pc.bold("Logging"));
|
|
550
|
-
logging = await promptLogging();
|
|
551
|
-
|
|
552
|
-
p.log.step(pc.bold("Server"));
|
|
553
|
-
({ server, auth } = await promptServer({ currentServer: server, currentAuth: auth }));
|
|
554
|
-
|
|
555
|
-
p.log.step(pc.bold("Storage"));
|
|
556
|
-
storage = await promptStorage(storage);
|
|
557
|
-
|
|
558
|
-
p.log.step(pc.bold("Secrets"));
|
|
559
|
-
const secretsDefaults = defaultSecretsConfig();
|
|
560
|
-
secrets = {
|
|
561
|
-
provider: secrets.provider ?? secretsDefaults.provider,
|
|
562
|
-
strictMode: secrets.strictMode ?? secretsDefaults.strictMode,
|
|
563
|
-
localEncrypted: {
|
|
564
|
-
keyFilePath: secrets.localEncrypted?.keyFilePath ?? secretsDefaults.localEncrypted.keyFilePath,
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
p.log.message(
|
|
568
|
-
pc.dim(
|
|
569
|
-
`Using defaults: provider=${secrets.provider}, strictMode=${secrets.strictMode}, keyFile=${secrets.localEncrypted.keyFilePath}`,
|
|
570
|
-
),
|
|
571
|
-
);
|
|
572
|
-
} else {
|
|
573
|
-
p.log.step(pc.bold("Quickstart"));
|
|
574
|
-
p.log.message(
|
|
575
|
-
pc.dim(
|
|
576
|
-
opts.bind
|
|
577
|
-
? `Using quickstart defaults with bind=${opts.bind}.`
|
|
578
|
-
: `Using quickstart defaults: ${server.deploymentMode}/${server.exposure} @ ${describeServerBinding(server)}.`,
|
|
579
|
-
),
|
|
580
|
-
);
|
|
581
|
-
if (usedEnvKeys.length > 0) {
|
|
582
|
-
p.log.message(pc.dim(`Environment-aware defaults active (${usedEnvKeys.length} env var(s) detected).`));
|
|
583
|
-
} else {
|
|
584
|
-
p.log.message(
|
|
585
|
-
pc.dim("No environment overrides detected: embedded database, file storage, local encrypted secrets."),
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
for (const ignored of ignoredEnvKeys) {
|
|
589
|
-
p.log.message(pc.dim(`Ignored ${ignored.key}: ${ignored.reason}`));
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
const jwtSecret = ensureAgentJwtSecret(configPath);
|
|
594
|
-
const envFilePath = resolveAgentJwtEnvFile(configPath);
|
|
595
|
-
if (jwtSecret.created) {
|
|
596
|
-
p.log.success(`Created ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
|
597
|
-
} else if (process.env.PAPERCLIP_AGENT_JWT_SECRET?.trim()) {
|
|
598
|
-
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} from environment`);
|
|
599
|
-
} else {
|
|
600
|
-
p.log.info(`Using existing ${pc.cyan("PAPERCLIP_AGENT_JWT_SECRET")} in ${pc.dim(envFilePath)}`);
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
const config: PaperclipConfig = {
|
|
604
|
-
$meta: {
|
|
605
|
-
version: 1,
|
|
606
|
-
updatedAt: new Date().toISOString(),
|
|
607
|
-
source: "onboard",
|
|
608
|
-
},
|
|
609
|
-
...(llm && { llm }),
|
|
610
|
-
database,
|
|
611
|
-
logging,
|
|
612
|
-
server,
|
|
613
|
-
auth,
|
|
614
|
-
telemetry: {
|
|
615
|
-
enabled: true,
|
|
616
|
-
},
|
|
617
|
-
storage,
|
|
618
|
-
secrets,
|
|
619
|
-
};
|
|
620
|
-
|
|
621
|
-
const keyResult = ensureLocalSecretsKeyFile(config, configPath);
|
|
622
|
-
if (keyResult.status === "created") {
|
|
623
|
-
p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`);
|
|
624
|
-
} else if (keyResult.status === "existing") {
|
|
625
|
-
p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`));
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
writeConfig(config, opts.config);
|
|
629
|
-
|
|
630
|
-
if (tc) trackInstallCompleted(tc, {
|
|
631
|
-
adapterType: server.deploymentMode,
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
p.note(
|
|
635
|
-
[
|
|
636
|
-
`Database: ${database.mode}`,
|
|
637
|
-
llm ? `LLM: ${llm.provider}` : "LLM: not configured",
|
|
638
|
-
`Logging: ${logging.mode} -> ${logging.logDir}`,
|
|
639
|
-
`Server: ${server.deploymentMode}/${server.exposure} @ ${describeServerBinding(server)}`,
|
|
640
|
-
`Allowed hosts: ${server.allowedHostnames.length > 0 ? server.allowedHostnames.join(", ") : "(loopback only)"}`,
|
|
641
|
-
`Auth URL mode: ${auth.baseUrlMode}${auth.publicBaseUrl ? ` (${auth.publicBaseUrl})` : ""}`,
|
|
642
|
-
`Storage: ${storage.provider}`,
|
|
643
|
-
`Secrets: ${secrets.provider} (strict mode ${secrets.strictMode ? "on" : "off"})`,
|
|
644
|
-
"Agent auth: PAPERCLIP_AGENT_JWT_SECRET configured",
|
|
645
|
-
].join("\n"),
|
|
646
|
-
"Configuration saved",
|
|
647
|
-
);
|
|
648
|
-
|
|
649
|
-
p.note(
|
|
650
|
-
[
|
|
651
|
-
`Run: ${pc.cyan("paperclipai run")}`,
|
|
652
|
-
`Reconfigure later: ${pc.cyan("paperclipai configure")}`,
|
|
653
|
-
`Diagnose setup: ${pc.cyan("paperclipai doctor")}`,
|
|
654
|
-
].join("\n"),
|
|
655
|
-
"Next commands",
|
|
656
|
-
);
|
|
657
|
-
|
|
658
|
-
if (canCreateBootstrapInviteImmediately({ database, server })) {
|
|
659
|
-
p.log.step("Generating bootstrap CEO invite");
|
|
660
|
-
await bootstrapCeoInvite({ config: configPath });
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
let shouldRunNow = opts.run === true || opts.yes === true;
|
|
664
|
-
if (!shouldRunNow && !opts.invokedByRun && process.stdin.isTTY && process.stdout.isTTY) {
|
|
665
|
-
const answer = await p.confirm({
|
|
666
|
-
message: "Start Paperclip now?",
|
|
667
|
-
initialValue: true,
|
|
668
|
-
});
|
|
669
|
-
if (!p.isCancel(answer)) {
|
|
670
|
-
shouldRunNow = answer;
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
if (shouldRunNow && !opts.invokedByRun) {
|
|
675
|
-
process.env.PAPERCLIP_OPEN_ON_LISTEN = "true";
|
|
676
|
-
const { runCommand } = await import("./run.js");
|
|
677
|
-
await runCommand({ config: configPath, repair: true, yes: true });
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
if (server.deploymentMode === "authenticated" && database.mode === "embedded-postgres") {
|
|
682
|
-
p.log.info(
|
|
683
|
-
[
|
|
684
|
-
"Bootstrap CEO invite will be created after the server starts.",
|
|
685
|
-
`Next: ${pc.cyan("paperclipai run")}`,
|
|
686
|
-
`Then: ${pc.cyan("paperclipai auth bootstrap-ceo")}`,
|
|
687
|
-
].join("\n"),
|
|
688
|
-
);
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
p.outro("You're all set!");
|
|
692
|
-
}
|