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.
- 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,279 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { PaperclipConfig } from "../config/schema.js";
|
|
4
|
+
import { expandHomePrefix } from "../config/home.js";
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_WORKTREE_HOME = "~/.paperclip-worktrees";
|
|
7
|
+
export const WORKTREE_SEED_MODES = ["minimal", "full"] as const;
|
|
8
|
+
|
|
9
|
+
export type WorktreeSeedMode = (typeof WORKTREE_SEED_MODES)[number];
|
|
10
|
+
|
|
11
|
+
export type WorktreeSeedPlan = {
|
|
12
|
+
mode: WorktreeSeedMode;
|
|
13
|
+
excludedTables: string[];
|
|
14
|
+
nullifyColumns: Record<string, string[]>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const MINIMAL_WORKTREE_EXCLUDED_TABLES = [
|
|
18
|
+
"activity_log",
|
|
19
|
+
"agent_runtime_state",
|
|
20
|
+
"agent_task_sessions",
|
|
21
|
+
"agent_wakeup_requests",
|
|
22
|
+
"cost_events",
|
|
23
|
+
"heartbeat_run_events",
|
|
24
|
+
"heartbeat_runs",
|
|
25
|
+
"workspace_runtime_services",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const MINIMAL_WORKTREE_NULLIFIED_COLUMNS: Record<string, string[]> = {
|
|
29
|
+
issues: ["checkout_run_id", "execution_run_id"],
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type WorktreeLocalPaths = {
|
|
33
|
+
cwd: string;
|
|
34
|
+
repoConfigDir: string;
|
|
35
|
+
configPath: string;
|
|
36
|
+
envPath: string;
|
|
37
|
+
homeDir: string;
|
|
38
|
+
instanceId: string;
|
|
39
|
+
instanceRoot: string;
|
|
40
|
+
contextPath: string;
|
|
41
|
+
embeddedPostgresDataDir: string;
|
|
42
|
+
backupDir: string;
|
|
43
|
+
logDir: string;
|
|
44
|
+
secretsKeyFilePath: string;
|
|
45
|
+
storageDir: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type WorktreeUiBranding = {
|
|
49
|
+
name: string;
|
|
50
|
+
color: string;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function isWorktreeSeedMode(value: string): value is WorktreeSeedMode {
|
|
54
|
+
return (WORKTREE_SEED_MODES as readonly string[]).includes(value);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function resolveWorktreeSeedPlan(mode: WorktreeSeedMode): WorktreeSeedPlan {
|
|
58
|
+
if (mode === "full") {
|
|
59
|
+
return {
|
|
60
|
+
mode,
|
|
61
|
+
excludedTables: [],
|
|
62
|
+
nullifyColumns: {},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
mode,
|
|
67
|
+
excludedTables: [...MINIMAL_WORKTREE_EXCLUDED_TABLES],
|
|
68
|
+
nullifyColumns: {
|
|
69
|
+
...MINIMAL_WORKTREE_NULLIFIED_COLUMNS,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function nonEmpty(value: string | null | undefined): string | null {
|
|
75
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function isLoopbackHost(hostname: string): boolean {
|
|
79
|
+
const value = hostname.trim().toLowerCase();
|
|
80
|
+
return value === "127.0.0.1" || value === "localhost" || value === "::1";
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function sanitizeWorktreeInstanceId(rawValue: string): string {
|
|
84
|
+
const trimmed = rawValue.trim().toLowerCase();
|
|
85
|
+
const normalized = trimmed
|
|
86
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
87
|
+
.replace(/-+/g, "-")
|
|
88
|
+
.replace(/^[-_]+|[-_]+$/g, "");
|
|
89
|
+
return normalized || "worktree";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function resolveSuggestedWorktreeName(cwd: string, explicitName?: string): string {
|
|
93
|
+
return nonEmpty(explicitName) ?? path.basename(path.resolve(cwd));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function hslComponentToHex(n: number): string {
|
|
97
|
+
return Math.round(Math.max(0, Math.min(255, n)))
|
|
98
|
+
.toString(16)
|
|
99
|
+
.padStart(2, "0");
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hslToHex(hue: number, saturation: number, lightness: number): string {
|
|
103
|
+
const s = Math.max(0, Math.min(100, saturation)) / 100;
|
|
104
|
+
const l = Math.max(0, Math.min(100, lightness)) / 100;
|
|
105
|
+
const c = (1 - Math.abs((2 * l) - 1)) * s;
|
|
106
|
+
const h = ((hue % 360) + 360) % 360;
|
|
107
|
+
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
108
|
+
const m = l - (c / 2);
|
|
109
|
+
|
|
110
|
+
let r = 0;
|
|
111
|
+
let g = 0;
|
|
112
|
+
let b = 0;
|
|
113
|
+
|
|
114
|
+
if (h < 60) {
|
|
115
|
+
r = c;
|
|
116
|
+
g = x;
|
|
117
|
+
} else if (h < 120) {
|
|
118
|
+
r = x;
|
|
119
|
+
g = c;
|
|
120
|
+
} else if (h < 180) {
|
|
121
|
+
g = c;
|
|
122
|
+
b = x;
|
|
123
|
+
} else if (h < 240) {
|
|
124
|
+
g = x;
|
|
125
|
+
b = c;
|
|
126
|
+
} else if (h < 300) {
|
|
127
|
+
r = x;
|
|
128
|
+
b = c;
|
|
129
|
+
} else {
|
|
130
|
+
r = c;
|
|
131
|
+
b = x;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return `#${hslComponentToHex((r + m) * 255)}${hslComponentToHex((g + m) * 255)}${hslComponentToHex((b + m) * 255)}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function generateWorktreeColor(): string {
|
|
138
|
+
return hslToHex(randomInt(0, 360), 68, 56);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function resolveWorktreeLocalPaths(opts: {
|
|
142
|
+
cwd: string;
|
|
143
|
+
homeDir?: string;
|
|
144
|
+
instanceId: string;
|
|
145
|
+
}): WorktreeLocalPaths {
|
|
146
|
+
const cwd = path.resolve(opts.cwd);
|
|
147
|
+
const homeDir = path.resolve(expandHomePrefix(opts.homeDir ?? DEFAULT_WORKTREE_HOME));
|
|
148
|
+
const instanceRoot = path.resolve(homeDir, "instances", opts.instanceId);
|
|
149
|
+
const repoConfigDir = path.resolve(cwd, ".paperclip");
|
|
150
|
+
return {
|
|
151
|
+
cwd,
|
|
152
|
+
repoConfigDir,
|
|
153
|
+
configPath: path.resolve(repoConfigDir, "config.json"),
|
|
154
|
+
envPath: path.resolve(repoConfigDir, ".env"),
|
|
155
|
+
homeDir,
|
|
156
|
+
instanceId: opts.instanceId,
|
|
157
|
+
instanceRoot,
|
|
158
|
+
contextPath: path.resolve(homeDir, "context.json"),
|
|
159
|
+
embeddedPostgresDataDir: path.resolve(instanceRoot, "db"),
|
|
160
|
+
backupDir: path.resolve(instanceRoot, "data", "backups"),
|
|
161
|
+
logDir: path.resolve(instanceRoot, "logs"),
|
|
162
|
+
secretsKeyFilePath: path.resolve(instanceRoot, "secrets", "master.key"),
|
|
163
|
+
storageDir: path.resolve(instanceRoot, "data", "storage"),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function rewriteLocalUrlPort(rawUrl: string | undefined, port: number): string | undefined {
|
|
168
|
+
if (!rawUrl) return undefined;
|
|
169
|
+
try {
|
|
170
|
+
const parsed = new URL(rawUrl);
|
|
171
|
+
if (!isLoopbackHost(parsed.hostname)) return rawUrl;
|
|
172
|
+
parsed.port = String(port);
|
|
173
|
+
return parsed.toString();
|
|
174
|
+
} catch {
|
|
175
|
+
return rawUrl;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function buildWorktreeConfig(input: {
|
|
180
|
+
sourceConfig: PaperclipConfig | null;
|
|
181
|
+
paths: WorktreeLocalPaths;
|
|
182
|
+
serverPort: number;
|
|
183
|
+
databasePort: number;
|
|
184
|
+
now?: Date;
|
|
185
|
+
}): PaperclipConfig {
|
|
186
|
+
const { sourceConfig, paths, serverPort, databasePort } = input;
|
|
187
|
+
const nowIso = (input.now ?? new Date()).toISOString();
|
|
188
|
+
|
|
189
|
+
const source = sourceConfig;
|
|
190
|
+
const authPublicBaseUrl = rewriteLocalUrlPort(source?.auth.publicBaseUrl, serverPort);
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
$meta: {
|
|
194
|
+
version: 1,
|
|
195
|
+
updatedAt: nowIso,
|
|
196
|
+
source: "configure",
|
|
197
|
+
},
|
|
198
|
+
...(source?.llm ? { llm: source.llm } : {}),
|
|
199
|
+
database: {
|
|
200
|
+
mode: "embedded-postgres",
|
|
201
|
+
embeddedPostgresDataDir: paths.embeddedPostgresDataDir,
|
|
202
|
+
embeddedPostgresPort: databasePort,
|
|
203
|
+
backup: {
|
|
204
|
+
enabled: source?.database.backup.enabled ?? true,
|
|
205
|
+
intervalMinutes: source?.database.backup.intervalMinutes ?? 60,
|
|
206
|
+
retentionDays: source?.database.backup.retentionDays ?? 30,
|
|
207
|
+
dir: paths.backupDir,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
logging: {
|
|
211
|
+
mode: source?.logging.mode ?? "file",
|
|
212
|
+
logDir: paths.logDir,
|
|
213
|
+
},
|
|
214
|
+
server: {
|
|
215
|
+
deploymentMode: source?.server.deploymentMode ?? "local_trusted",
|
|
216
|
+
exposure: source?.server.exposure ?? "private",
|
|
217
|
+
...(source?.server.bind ? { bind: source.server.bind } : {}),
|
|
218
|
+
...(source?.server.customBindHost ? { customBindHost: source.server.customBindHost } : {}),
|
|
219
|
+
host: source?.server.host ?? "127.0.0.1",
|
|
220
|
+
port: serverPort,
|
|
221
|
+
allowedHostnames: source?.server.allowedHostnames ?? [],
|
|
222
|
+
serveUi: source?.server.serveUi ?? true,
|
|
223
|
+
},
|
|
224
|
+
auth: {
|
|
225
|
+
baseUrlMode: source?.auth.baseUrlMode ?? "auto",
|
|
226
|
+
...(authPublicBaseUrl ? { publicBaseUrl: authPublicBaseUrl } : {}),
|
|
227
|
+
disableSignUp: source?.auth.disableSignUp ?? false,
|
|
228
|
+
},
|
|
229
|
+
telemetry: {
|
|
230
|
+
enabled: source?.telemetry?.enabled ?? true,
|
|
231
|
+
},
|
|
232
|
+
storage: {
|
|
233
|
+
provider: source?.storage.provider ?? "local_disk",
|
|
234
|
+
localDisk: {
|
|
235
|
+
baseDir: paths.storageDir,
|
|
236
|
+
},
|
|
237
|
+
s3: {
|
|
238
|
+
bucket: source?.storage.s3.bucket ?? "paperclip",
|
|
239
|
+
region: source?.storage.s3.region ?? "us-east-1",
|
|
240
|
+
endpoint: source?.storage.s3.endpoint,
|
|
241
|
+
prefix: source?.storage.s3.prefix ?? "",
|
|
242
|
+
forcePathStyle: source?.storage.s3.forcePathStyle ?? false,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
secrets: {
|
|
246
|
+
provider: source?.secrets.provider ?? "local_encrypted",
|
|
247
|
+
strictMode: source?.secrets.strictMode ?? false,
|
|
248
|
+
localEncrypted: {
|
|
249
|
+
keyFilePath: paths.secretsKeyFilePath,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function buildWorktreeEnvEntries(
|
|
256
|
+
paths: WorktreeLocalPaths,
|
|
257
|
+
branding?: WorktreeUiBranding,
|
|
258
|
+
): Record<string, string> {
|
|
259
|
+
return {
|
|
260
|
+
PAPERCLIP_HOME: paths.homeDir,
|
|
261
|
+
PAPERCLIP_INSTANCE_ID: paths.instanceId,
|
|
262
|
+
PAPERCLIP_CONFIG: paths.configPath,
|
|
263
|
+
PAPERCLIP_CONTEXT: paths.contextPath,
|
|
264
|
+
PAPERCLIP_IN_WORKTREE: "true",
|
|
265
|
+
...(branding?.name ? { PAPERCLIP_WORKTREE_NAME: branding.name } : {}),
|
|
266
|
+
...(branding?.color ? { PAPERCLIP_WORKTREE_COLOR: branding.color } : {}),
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function shellEscape(value: string): string {
|
|
271
|
+
return `'${value.replaceAll("'", `'\"'\"'`)}'`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export function formatShellExports(entries: Record<string, string>): string {
|
|
275
|
+
return Object.entries(entries)
|
|
276
|
+
.filter(([, value]) => typeof value === "string" && value.trim().length > 0)
|
|
277
|
+
.map(([key, value]) => `export ${key}=${shellEscape(value)}`)
|
|
278
|
+
.join("\n");
|
|
279
|
+
}
|