offwatch 0.5.9 → 0.5.10
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/bin/offwatch.js +7 -6
- package/package.json +4 -3
- package/src/__tests__/agent-jwt-env.test.ts +79 -0
- package/src/__tests__/allowed-hostname.test.ts +80 -0
- package/src/__tests__/auth-command-registration.test.ts +16 -0
- package/src/__tests__/board-auth.test.ts +53 -0
- package/src/__tests__/common.test.ts +98 -0
- package/src/__tests__/company-delete.test.ts +95 -0
- package/src/__tests__/company-import-export-e2e.test.ts +502 -0
- package/src/__tests__/company-import-url.test.ts +74 -0
- package/src/__tests__/company-import-zip.test.ts +44 -0
- package/src/__tests__/company.test.ts +599 -0
- package/src/__tests__/context.test.ts +70 -0
- package/src/__tests__/data-dir.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +102 -0
- package/src/__tests__/feedback.test.ts +177 -0
- package/src/__tests__/helpers/embedded-postgres.ts +6 -0
- package/src/__tests__/helpers/zip.ts +87 -0
- package/src/__tests__/home-paths.test.ts +44 -0
- package/src/__tests__/http.test.ts +106 -0
- package/src/__tests__/network-bind.test.ts +62 -0
- package/src/__tests__/onboard.test.ts +166 -0
- package/src/__tests__/routines.test.ts +249 -0
- package/src/__tests__/telemetry.test.ts +117 -0
- package/src/__tests__/worktree-merge-history.test.ts +492 -0
- package/src/__tests__/worktree.test.ts +982 -0
- package/src/adapters/http/format-event.ts +4 -0
- package/src/adapters/http/index.ts +7 -0
- package/src/adapters/index.ts +2 -0
- package/src/adapters/process/format-event.ts +4 -0
- package/src/adapters/process/index.ts +7 -0
- package/src/adapters/registry.ts +63 -0
- package/src/checks/agent-jwt-secret-check.ts +40 -0
- package/src/checks/config-check.ts +33 -0
- package/src/checks/database-check.ts +59 -0
- package/src/checks/deployment-auth-check.ts +88 -0
- package/src/checks/index.ts +18 -0
- package/src/checks/llm-check.ts +82 -0
- package/src/checks/log-check.ts +30 -0
- package/src/checks/path-resolver.ts +1 -0
- package/src/checks/port-check.ts +24 -0
- package/src/checks/secrets-check.ts +146 -0
- package/src/checks/storage-check.ts +51 -0
- package/src/client/board-auth.ts +282 -0
- package/src/client/command-label.ts +4 -0
- package/src/client/context.ts +175 -0
- package/src/client/http.ts +255 -0
- package/src/commands/allowed-hostname.ts +40 -0
- package/src/commands/auth-bootstrap-ceo.ts +138 -0
- package/src/commands/client/activity.ts +71 -0
- package/src/commands/client/agent.ts +315 -0
- package/src/commands/client/approval.ts +259 -0
- package/src/commands/client/auth.ts +113 -0
- package/src/commands/client/common.ts +221 -0
- package/src/commands/client/company.ts +1578 -0
- package/src/commands/client/context.ts +125 -0
- package/src/commands/client/dashboard.ts +34 -0
- package/src/commands/client/feedback.ts +645 -0
- package/src/commands/client/issue.ts +411 -0
- package/src/commands/client/plugin.ts +374 -0
- package/src/commands/client/zip.ts +129 -0
- package/src/commands/configure.ts +201 -0
- package/src/commands/db-backup.ts +102 -0
- package/src/commands/doctor.ts +203 -0
- package/src/commands/env.ts +411 -0
- package/src/commands/heartbeat-run.ts +344 -0
- package/src/commands/onboard.ts +692 -0
- package/src/commands/routines.ts +352 -0
- package/src/commands/run.ts +216 -0
- package/src/commands/worktree-lib.ts +279 -0
- package/src/commands/worktree-merge-history-lib.ts +764 -0
- package/src/commands/worktree.ts +2876 -0
- package/src/config/data-dir.ts +48 -0
- package/src/config/env.ts +125 -0
- package/src/config/home.ts +80 -0
- package/src/config/hostnames.ts +26 -0
- package/src/config/schema.ts +30 -0
- package/src/config/secrets-key.ts +48 -0
- package/src/config/server-bind.ts +183 -0
- package/src/config/store.ts +120 -0
- package/src/index.ts +182 -0
- package/src/prompts/database.ts +157 -0
- package/src/prompts/llm.ts +43 -0
- package/src/prompts/logging.ts +37 -0
- package/src/prompts/secrets.ts +99 -0
- package/src/prompts/server.ts +221 -0
- package/src/prompts/storage.ts +146 -0
- package/src/telemetry.ts +49 -0
- package/src/utils/banner.ts +24 -0
- package/src/utils/net.ts +18 -0
- package/src/utils/path-resolver.ts +25 -0
- package/src/version.ts +10 -0
- package/lib/downloader.js +0 -112
- package/postinstall.js +0 -23
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import pc from "picocolors";
|
|
3
|
+
import type { PaperclipConfig } from "../config/schema.js";
|
|
4
|
+
import { configExists, readConfig, resolveConfigPath } from "../config/store.js";
|
|
5
|
+
import {
|
|
6
|
+
readAgentJwtSecretFromEnv,
|
|
7
|
+
readAgentJwtSecretFromEnvFile,
|
|
8
|
+
resolveAgentJwtEnvFile,
|
|
9
|
+
} from "../config/env.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveDefaultSecretsKeyFilePath,
|
|
12
|
+
resolveDefaultStorageDir,
|
|
13
|
+
resolvePaperclipInstanceId,
|
|
14
|
+
} from "../config/home.js";
|
|
15
|
+
|
|
16
|
+
type EnvSource = "env" | "config" | "file" | "default" | "missing";
|
|
17
|
+
|
|
18
|
+
type EnvVarRow = {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string;
|
|
21
|
+
source: EnvSource;
|
|
22
|
+
required: boolean;
|
|
23
|
+
note: string;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const DEFAULT_AGENT_JWT_TTL_SECONDS = "172800";
|
|
27
|
+
const DEFAULT_AGENT_JWT_ISSUER = "paperclip";
|
|
28
|
+
const DEFAULT_AGENT_JWT_AUDIENCE = "paperclip-api";
|
|
29
|
+
const DEFAULT_HEARTBEAT_SCHEDULER_INTERVAL_MS = "30000";
|
|
30
|
+
const DEFAULT_SECRETS_PROVIDER = "local_encrypted";
|
|
31
|
+
const DEFAULT_STORAGE_PROVIDER = "local_disk";
|
|
32
|
+
function defaultSecretsKeyFilePath(): string {
|
|
33
|
+
return resolveDefaultSecretsKeyFilePath(resolvePaperclipInstanceId());
|
|
34
|
+
}
|
|
35
|
+
function defaultStorageBaseDir(): string {
|
|
36
|
+
return resolveDefaultStorageDir(resolvePaperclipInstanceId());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function envCommand(opts: { config?: string }): Promise<void> {
|
|
40
|
+
p.intro(pc.bgCyan(pc.black(" paperclip env ")));
|
|
41
|
+
|
|
42
|
+
const configPath = resolveConfigPath(opts.config);
|
|
43
|
+
let config: PaperclipConfig | null = null;
|
|
44
|
+
let configReadError: string | null = null;
|
|
45
|
+
|
|
46
|
+
if (configExists(opts.config)) {
|
|
47
|
+
p.log.message(pc.dim(`Config file: ${configPath}`));
|
|
48
|
+
try {
|
|
49
|
+
config = readConfig(opts.config);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
configReadError = err instanceof Error ? err.message : String(err);
|
|
52
|
+
p.log.message(pc.yellow(`Could not parse config: ${configReadError}`));
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
p.log.message(pc.dim(`Config file missing: ${configPath}`));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const rows = collectDeploymentEnvRows(config, configPath);
|
|
59
|
+
const missingRequired = rows.filter((row) => row.required && row.source === "missing");
|
|
60
|
+
const sortedRows = rows.sort((a, b) => Number(b.required) - Number(a.required) || a.key.localeCompare(b.key));
|
|
61
|
+
|
|
62
|
+
const requiredRows = sortedRows.filter((row) => row.required);
|
|
63
|
+
const optionalRows = sortedRows.filter((row) => !row.required);
|
|
64
|
+
|
|
65
|
+
const formatSection = (title: string, entries: EnvVarRow[]) => {
|
|
66
|
+
if (entries.length === 0) return;
|
|
67
|
+
|
|
68
|
+
p.log.message(pc.bold(title));
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
const status = entry.source === "missing" ? pc.red("missing") : entry.source === "default" ? pc.yellow("default") : pc.green("set");
|
|
71
|
+
const sourceNote = {
|
|
72
|
+
env: "environment",
|
|
73
|
+
config: "config",
|
|
74
|
+
file: "file",
|
|
75
|
+
default: "default",
|
|
76
|
+
missing: "missing",
|
|
77
|
+
}[entry.source];
|
|
78
|
+
p.log.message(
|
|
79
|
+
`${pc.cyan(entry.key)} ${status.padEnd(7)} ${pc.dim(`[${sourceNote}] ${entry.note}`)}${entry.source === "missing" ? "" : ` ${pc.dim("=>")} ${pc.white(quoteShellValue(entry.value))}`}`,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
formatSection("Required environment variables", requiredRows);
|
|
85
|
+
formatSection("Optional environment variables", optionalRows);
|
|
86
|
+
|
|
87
|
+
const exportRows = rows.map((row) => (row.source === "missing" ? { ...row, value: "<set-this-value>" } : row));
|
|
88
|
+
const uniqueRows = uniqueByKey(exportRows);
|
|
89
|
+
const exportBlock = uniqueRows.map((row) => `export ${row.key}=${quoteShellValue(row.value)}`).join("\n");
|
|
90
|
+
|
|
91
|
+
if (configReadError) {
|
|
92
|
+
p.log.error(`Could not load config cleanly: ${configReadError}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
p.note(
|
|
96
|
+
exportBlock || "No values detected. Set required variables manually.",
|
|
97
|
+
"Deployment export block",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (missingRequired.length > 0) {
|
|
101
|
+
p.log.message(
|
|
102
|
+
pc.yellow(
|
|
103
|
+
`Missing required values: ${missingRequired.map((row) => row.key).join(", ")}. Set these before deployment.`,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
} else {
|
|
107
|
+
p.log.message(pc.green("All required deployment variables are present."));
|
|
108
|
+
}
|
|
109
|
+
p.outro("Done");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function collectDeploymentEnvRows(config: PaperclipConfig | null, configPath: string): EnvVarRow[] {
|
|
113
|
+
const agentJwtEnvFile = resolveAgentJwtEnvFile(configPath);
|
|
114
|
+
const jwtEnv = readAgentJwtSecretFromEnv(configPath);
|
|
115
|
+
const jwtFile = jwtEnv ? null : readAgentJwtSecretFromEnvFile(agentJwtEnvFile);
|
|
116
|
+
const jwtSource = jwtEnv ? "env" : jwtFile ? "file" : "missing";
|
|
117
|
+
|
|
118
|
+
const dbUrl = process.env.DATABASE_URL ?? config?.database?.connectionString ?? "";
|
|
119
|
+
const databaseMode = config?.database?.mode ?? "embedded-postgres";
|
|
120
|
+
const dbUrlSource: EnvSource = process.env.DATABASE_URL ? "env" : config?.database?.connectionString ? "config" : "missing";
|
|
121
|
+
const publicUrl =
|
|
122
|
+
process.env.PAPERCLIP_PUBLIC_URL ??
|
|
123
|
+
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ??
|
|
124
|
+
process.env.BETTER_AUTH_URL ??
|
|
125
|
+
process.env.BETTER_AUTH_BASE_URL ??
|
|
126
|
+
config?.auth?.publicBaseUrl ??
|
|
127
|
+
"";
|
|
128
|
+
const publicUrlSource: EnvSource =
|
|
129
|
+
process.env.PAPERCLIP_PUBLIC_URL
|
|
130
|
+
? "env"
|
|
131
|
+
: process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL || process.env.BETTER_AUTH_URL || process.env.BETTER_AUTH_BASE_URL
|
|
132
|
+
? "env"
|
|
133
|
+
: config?.auth?.publicBaseUrl
|
|
134
|
+
? "config"
|
|
135
|
+
: "missing";
|
|
136
|
+
let trustedOriginsDefault = "";
|
|
137
|
+
if (publicUrl) {
|
|
138
|
+
try {
|
|
139
|
+
trustedOriginsDefault = new URL(publicUrl).origin;
|
|
140
|
+
} catch {
|
|
141
|
+
trustedOriginsDefault = "";
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const heartbeatInterval = process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS ?? DEFAULT_HEARTBEAT_SCHEDULER_INTERVAL_MS;
|
|
146
|
+
const heartbeatEnabled = process.env.HEARTBEAT_SCHEDULER_ENABLED ?? "true";
|
|
147
|
+
const secretsProvider =
|
|
148
|
+
process.env.PAPERCLIP_SECRETS_PROVIDER ??
|
|
149
|
+
config?.secrets?.provider ??
|
|
150
|
+
DEFAULT_SECRETS_PROVIDER;
|
|
151
|
+
const secretsStrictMode =
|
|
152
|
+
process.env.PAPERCLIP_SECRETS_STRICT_MODE ??
|
|
153
|
+
String(config?.secrets?.strictMode ?? false);
|
|
154
|
+
const secretsKeyFilePath =
|
|
155
|
+
process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE ??
|
|
156
|
+
config?.secrets?.localEncrypted?.keyFilePath ??
|
|
157
|
+
defaultSecretsKeyFilePath();
|
|
158
|
+
const storageProvider =
|
|
159
|
+
process.env.PAPERCLIP_STORAGE_PROVIDER ??
|
|
160
|
+
config?.storage?.provider ??
|
|
161
|
+
DEFAULT_STORAGE_PROVIDER;
|
|
162
|
+
const storageLocalDir =
|
|
163
|
+
process.env.PAPERCLIP_STORAGE_LOCAL_DIR ??
|
|
164
|
+
config?.storage?.localDisk?.baseDir ??
|
|
165
|
+
defaultStorageBaseDir();
|
|
166
|
+
const storageS3Bucket =
|
|
167
|
+
process.env.PAPERCLIP_STORAGE_S3_BUCKET ??
|
|
168
|
+
config?.storage?.s3?.bucket ??
|
|
169
|
+
"paperclip";
|
|
170
|
+
const storageS3Region =
|
|
171
|
+
process.env.PAPERCLIP_STORAGE_S3_REGION ??
|
|
172
|
+
config?.storage?.s3?.region ??
|
|
173
|
+
"us-east-1";
|
|
174
|
+
const storageS3Endpoint =
|
|
175
|
+
process.env.PAPERCLIP_STORAGE_S3_ENDPOINT ??
|
|
176
|
+
config?.storage?.s3?.endpoint ??
|
|
177
|
+
"";
|
|
178
|
+
const storageS3Prefix =
|
|
179
|
+
process.env.PAPERCLIP_STORAGE_S3_PREFIX ??
|
|
180
|
+
config?.storage?.s3?.prefix ??
|
|
181
|
+
"";
|
|
182
|
+
const storageS3ForcePathStyle =
|
|
183
|
+
process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE ??
|
|
184
|
+
String(config?.storage?.s3?.forcePathStyle ?? false);
|
|
185
|
+
|
|
186
|
+
const rows: EnvVarRow[] = [
|
|
187
|
+
{
|
|
188
|
+
key: "PAPERCLIP_AGENT_JWT_SECRET",
|
|
189
|
+
value: jwtEnv ?? jwtFile ?? "",
|
|
190
|
+
source: jwtSource,
|
|
191
|
+
required: true,
|
|
192
|
+
note:
|
|
193
|
+
jwtSource === "missing"
|
|
194
|
+
? "Generate during onboard or set manually (required for local adapter authentication)"
|
|
195
|
+
: jwtSource === "env"
|
|
196
|
+
? "Set in process environment"
|
|
197
|
+
: `Set in ${agentJwtEnvFile}`,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
key: "DATABASE_URL",
|
|
201
|
+
value: dbUrl,
|
|
202
|
+
source: dbUrlSource,
|
|
203
|
+
required: true,
|
|
204
|
+
note:
|
|
205
|
+
databaseMode === "postgres"
|
|
206
|
+
? "Configured for postgres mode (required)"
|
|
207
|
+
: "Required for live deployment with managed PostgreSQL",
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
key: "PORT",
|
|
211
|
+
value:
|
|
212
|
+
process.env.PORT ??
|
|
213
|
+
(config?.server?.port !== undefined ? String(config.server.port) : "3100"),
|
|
214
|
+
source: process.env.PORT ? "env" : config?.server?.port !== undefined ? "config" : "default",
|
|
215
|
+
required: false,
|
|
216
|
+
note: "HTTP listen port",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
key: "PAPERCLIP_PUBLIC_URL",
|
|
220
|
+
value: publicUrl,
|
|
221
|
+
source: publicUrlSource,
|
|
222
|
+
required: false,
|
|
223
|
+
note: "Canonical public URL for auth/callback/invite origin wiring",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
key: "BETTER_AUTH_TRUSTED_ORIGINS",
|
|
227
|
+
value: process.env.BETTER_AUTH_TRUSTED_ORIGINS ?? trustedOriginsDefault,
|
|
228
|
+
source: process.env.BETTER_AUTH_TRUSTED_ORIGINS
|
|
229
|
+
? "env"
|
|
230
|
+
: trustedOriginsDefault
|
|
231
|
+
? "default"
|
|
232
|
+
: "missing",
|
|
233
|
+
required: false,
|
|
234
|
+
note: "Comma-separated auth origin allowlist (auto-derived from PAPERCLIP_PUBLIC_URL when possible)",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
key: "PAPERCLIP_AGENT_JWT_TTL_SECONDS",
|
|
238
|
+
value: process.env.PAPERCLIP_AGENT_JWT_TTL_SECONDS ?? DEFAULT_AGENT_JWT_TTL_SECONDS,
|
|
239
|
+
source: process.env.PAPERCLIP_AGENT_JWT_TTL_SECONDS ? "env" : "default",
|
|
240
|
+
required: false,
|
|
241
|
+
note: "JWT lifetime in seconds",
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
key: "PAPERCLIP_AGENT_JWT_ISSUER",
|
|
245
|
+
value: process.env.PAPERCLIP_AGENT_JWT_ISSUER ?? DEFAULT_AGENT_JWT_ISSUER,
|
|
246
|
+
source: process.env.PAPERCLIP_AGENT_JWT_ISSUER ? "env" : "default",
|
|
247
|
+
required: false,
|
|
248
|
+
note: "JWT issuer",
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
key: "PAPERCLIP_AGENT_JWT_AUDIENCE",
|
|
252
|
+
value: process.env.PAPERCLIP_AGENT_JWT_AUDIENCE ?? DEFAULT_AGENT_JWT_AUDIENCE,
|
|
253
|
+
source: process.env.PAPERCLIP_AGENT_JWT_AUDIENCE ? "env" : "default",
|
|
254
|
+
required: false,
|
|
255
|
+
note: "JWT audience",
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
key: "HEARTBEAT_SCHEDULER_INTERVAL_MS",
|
|
259
|
+
value: heartbeatInterval,
|
|
260
|
+
source: process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS ? "env" : "default",
|
|
261
|
+
required: false,
|
|
262
|
+
note: "Heartbeat worker interval in ms",
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
key: "HEARTBEAT_SCHEDULER_ENABLED",
|
|
266
|
+
value: heartbeatEnabled,
|
|
267
|
+
source: process.env.HEARTBEAT_SCHEDULER_ENABLED ? "env" : "default",
|
|
268
|
+
required: false,
|
|
269
|
+
note: "Set to `false` to disable timer scheduling",
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
key: "PAPERCLIP_SECRETS_PROVIDER",
|
|
273
|
+
value: secretsProvider,
|
|
274
|
+
source: process.env.PAPERCLIP_SECRETS_PROVIDER
|
|
275
|
+
? "env"
|
|
276
|
+
: config?.secrets?.provider
|
|
277
|
+
? "config"
|
|
278
|
+
: "default",
|
|
279
|
+
required: false,
|
|
280
|
+
note: "Default provider for new secrets",
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
key: "PAPERCLIP_SECRETS_STRICT_MODE",
|
|
284
|
+
value: secretsStrictMode,
|
|
285
|
+
source: process.env.PAPERCLIP_SECRETS_STRICT_MODE
|
|
286
|
+
? "env"
|
|
287
|
+
: config?.secrets?.strictMode !== undefined
|
|
288
|
+
? "config"
|
|
289
|
+
: "default",
|
|
290
|
+
required: false,
|
|
291
|
+
note: "Require secret refs for sensitive env keys",
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
key: "PAPERCLIP_SECRETS_MASTER_KEY_FILE",
|
|
295
|
+
value: secretsKeyFilePath,
|
|
296
|
+
source: process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE
|
|
297
|
+
? "env"
|
|
298
|
+
: config?.secrets?.localEncrypted?.keyFilePath
|
|
299
|
+
? "config"
|
|
300
|
+
: "default",
|
|
301
|
+
required: false,
|
|
302
|
+
note: "Path to local encrypted secrets key file",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
key: "PAPERCLIP_STORAGE_PROVIDER",
|
|
306
|
+
value: storageProvider,
|
|
307
|
+
source: process.env.PAPERCLIP_STORAGE_PROVIDER
|
|
308
|
+
? "env"
|
|
309
|
+
: config?.storage?.provider
|
|
310
|
+
? "config"
|
|
311
|
+
: "default",
|
|
312
|
+
required: false,
|
|
313
|
+
note: "Storage provider (local_disk or s3)",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
key: "PAPERCLIP_STORAGE_LOCAL_DIR",
|
|
317
|
+
value: storageLocalDir,
|
|
318
|
+
source: process.env.PAPERCLIP_STORAGE_LOCAL_DIR
|
|
319
|
+
? "env"
|
|
320
|
+
: config?.storage?.localDisk?.baseDir
|
|
321
|
+
? "config"
|
|
322
|
+
: "default",
|
|
323
|
+
required: false,
|
|
324
|
+
note: "Local storage base directory for local_disk provider",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
key: "PAPERCLIP_STORAGE_S3_BUCKET",
|
|
328
|
+
value: storageS3Bucket,
|
|
329
|
+
source: process.env.PAPERCLIP_STORAGE_S3_BUCKET
|
|
330
|
+
? "env"
|
|
331
|
+
: config?.storage?.s3?.bucket
|
|
332
|
+
? "config"
|
|
333
|
+
: "default",
|
|
334
|
+
required: false,
|
|
335
|
+
note: "S3 bucket name for s3 provider",
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
key: "PAPERCLIP_STORAGE_S3_REGION",
|
|
339
|
+
value: storageS3Region,
|
|
340
|
+
source: process.env.PAPERCLIP_STORAGE_S3_REGION
|
|
341
|
+
? "env"
|
|
342
|
+
: config?.storage?.s3?.region
|
|
343
|
+
? "config"
|
|
344
|
+
: "default",
|
|
345
|
+
required: false,
|
|
346
|
+
note: "S3 region for s3 provider",
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
key: "PAPERCLIP_STORAGE_S3_ENDPOINT",
|
|
350
|
+
value: storageS3Endpoint,
|
|
351
|
+
source: process.env.PAPERCLIP_STORAGE_S3_ENDPOINT
|
|
352
|
+
? "env"
|
|
353
|
+
: config?.storage?.s3?.endpoint
|
|
354
|
+
? "config"
|
|
355
|
+
: "default",
|
|
356
|
+
required: false,
|
|
357
|
+
note: "Optional custom endpoint for S3-compatible providers",
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
key: "PAPERCLIP_STORAGE_S3_PREFIX",
|
|
361
|
+
value: storageS3Prefix,
|
|
362
|
+
source: process.env.PAPERCLIP_STORAGE_S3_PREFIX
|
|
363
|
+
? "env"
|
|
364
|
+
: config?.storage?.s3?.prefix
|
|
365
|
+
? "config"
|
|
366
|
+
: "default",
|
|
367
|
+
required: false,
|
|
368
|
+
note: "Optional object key prefix",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
key: "PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE",
|
|
372
|
+
value: storageS3ForcePathStyle,
|
|
373
|
+
source: process.env.PAPERCLIP_STORAGE_S3_FORCE_PATH_STYLE
|
|
374
|
+
? "env"
|
|
375
|
+
: config?.storage?.s3?.forcePathStyle !== undefined
|
|
376
|
+
? "config"
|
|
377
|
+
: "default",
|
|
378
|
+
required: false,
|
|
379
|
+
note: "Set true for path-style access on compatible providers",
|
|
380
|
+
},
|
|
381
|
+
];
|
|
382
|
+
|
|
383
|
+
const defaultConfigPath = resolveConfigPath();
|
|
384
|
+
if (process.env.PAPERCLIP_CONFIG || configPath !== defaultConfigPath) {
|
|
385
|
+
rows.push({
|
|
386
|
+
key: "PAPERCLIP_CONFIG",
|
|
387
|
+
value: process.env.PAPERCLIP_CONFIG ?? configPath,
|
|
388
|
+
source: process.env.PAPERCLIP_CONFIG ? "env" : "default",
|
|
389
|
+
required: false,
|
|
390
|
+
note: "Optional path override for config file",
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return rows;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function uniqueByKey(rows: EnvVarRow[]): EnvVarRow[] {
|
|
398
|
+
const seen = new Set<string>();
|
|
399
|
+
const result: EnvVarRow[] = [];
|
|
400
|
+
for (const row of rows) {
|
|
401
|
+
if (seen.has(row.key)) continue;
|
|
402
|
+
seen.add(row.key);
|
|
403
|
+
result.push(row);
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function quoteShellValue(value: string): string {
|
|
409
|
+
if (value === "") return "\"\"";
|
|
410
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
411
|
+
}
|