offwatch 0.5.8 → 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.
Files changed (94) hide show
  1. package/bin/offwatch.js +7 -6
  2. package/package.json +4 -3
  3. package/src/__tests__/agent-jwt-env.test.ts +79 -0
  4. package/src/__tests__/allowed-hostname.test.ts +80 -0
  5. package/src/__tests__/auth-command-registration.test.ts +16 -0
  6. package/src/__tests__/board-auth.test.ts +53 -0
  7. package/src/__tests__/common.test.ts +98 -0
  8. package/src/__tests__/company-delete.test.ts +95 -0
  9. package/src/__tests__/company-import-export-e2e.test.ts +502 -0
  10. package/src/__tests__/company-import-url.test.ts +74 -0
  11. package/src/__tests__/company-import-zip.test.ts +44 -0
  12. package/src/__tests__/company.test.ts +599 -0
  13. package/src/__tests__/context.test.ts +70 -0
  14. package/src/__tests__/data-dir.test.ts +79 -0
  15. package/src/__tests__/doctor.test.ts +102 -0
  16. package/src/__tests__/feedback.test.ts +177 -0
  17. package/src/__tests__/helpers/embedded-postgres.ts +6 -0
  18. package/src/__tests__/helpers/zip.ts +87 -0
  19. package/src/__tests__/home-paths.test.ts +44 -0
  20. package/src/__tests__/http.test.ts +106 -0
  21. package/src/__tests__/network-bind.test.ts +62 -0
  22. package/src/__tests__/onboard.test.ts +166 -0
  23. package/src/__tests__/routines.test.ts +249 -0
  24. package/src/__tests__/telemetry.test.ts +117 -0
  25. package/src/__tests__/worktree-merge-history.test.ts +492 -0
  26. package/src/__tests__/worktree.test.ts +982 -0
  27. package/src/adapters/http/format-event.ts +4 -0
  28. package/src/adapters/http/index.ts +7 -0
  29. package/src/adapters/index.ts +2 -0
  30. package/src/adapters/process/format-event.ts +4 -0
  31. package/src/adapters/process/index.ts +7 -0
  32. package/src/adapters/registry.ts +63 -0
  33. package/src/checks/agent-jwt-secret-check.ts +40 -0
  34. package/src/checks/config-check.ts +33 -0
  35. package/src/checks/database-check.ts +59 -0
  36. package/src/checks/deployment-auth-check.ts +88 -0
  37. package/src/checks/index.ts +18 -0
  38. package/src/checks/llm-check.ts +82 -0
  39. package/src/checks/log-check.ts +30 -0
  40. package/src/checks/path-resolver.ts +1 -0
  41. package/src/checks/port-check.ts +24 -0
  42. package/src/checks/secrets-check.ts +146 -0
  43. package/src/checks/storage-check.ts +51 -0
  44. package/src/client/board-auth.ts +282 -0
  45. package/src/client/command-label.ts +4 -0
  46. package/src/client/context.ts +175 -0
  47. package/src/client/http.ts +255 -0
  48. package/src/commands/allowed-hostname.ts +40 -0
  49. package/src/commands/auth-bootstrap-ceo.ts +138 -0
  50. package/src/commands/client/activity.ts +71 -0
  51. package/src/commands/client/agent.ts +315 -0
  52. package/src/commands/client/approval.ts +259 -0
  53. package/src/commands/client/auth.ts +113 -0
  54. package/src/commands/client/common.ts +221 -0
  55. package/src/commands/client/company.ts +1578 -0
  56. package/src/commands/client/context.ts +125 -0
  57. package/src/commands/client/dashboard.ts +34 -0
  58. package/src/commands/client/feedback.ts +645 -0
  59. package/src/commands/client/issue.ts +411 -0
  60. package/src/commands/client/plugin.ts +374 -0
  61. package/src/commands/client/zip.ts +129 -0
  62. package/src/commands/configure.ts +201 -0
  63. package/src/commands/db-backup.ts +102 -0
  64. package/src/commands/doctor.ts +203 -0
  65. package/src/commands/env.ts +411 -0
  66. package/src/commands/heartbeat-run.ts +344 -0
  67. package/src/commands/onboard.ts +692 -0
  68. package/src/commands/routines.ts +352 -0
  69. package/src/commands/run.ts +216 -0
  70. package/src/commands/worktree-lib.ts +279 -0
  71. package/src/commands/worktree-merge-history-lib.ts +764 -0
  72. package/src/commands/worktree.ts +2876 -0
  73. package/src/config/data-dir.ts +48 -0
  74. package/src/config/env.ts +125 -0
  75. package/src/config/home.ts +80 -0
  76. package/src/config/hostnames.ts +26 -0
  77. package/src/config/schema.ts +30 -0
  78. package/src/config/secrets-key.ts +48 -0
  79. package/src/config/server-bind.ts +183 -0
  80. package/src/config/store.ts +120 -0
  81. package/src/index.ts +182 -0
  82. package/src/prompts/database.ts +157 -0
  83. package/src/prompts/llm.ts +43 -0
  84. package/src/prompts/logging.ts +37 -0
  85. package/src/prompts/secrets.ts +99 -0
  86. package/src/prompts/server.ts +221 -0
  87. package/src/prompts/storage.ts +146 -0
  88. package/src/telemetry.ts +49 -0
  89. package/src/utils/banner.ts +24 -0
  90. package/src/utils/net.ts +18 -0
  91. package/src/utils/path-resolver.ts +25 -0
  92. package/src/version.ts +10 -0
  93. package/lib/downloader.js +0 -112
  94. 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
+ }