offwatch 0.5.12 → 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 +18 -11
- 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/routines.ts
DELETED
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import net from "node:net";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { Command } from "commander";
|
|
5
|
-
import pc from "picocolors";
|
|
6
|
-
import {
|
|
7
|
-
applyPendingMigrations,
|
|
8
|
-
createDb,
|
|
9
|
-
createEmbeddedPostgresLogBuffer,
|
|
10
|
-
ensurePostgresDatabase,
|
|
11
|
-
formatEmbeddedPostgresError,
|
|
12
|
-
routines,
|
|
13
|
-
} from "@paperclipai/db";
|
|
14
|
-
import { eq, inArray } from "drizzle-orm";
|
|
15
|
-
import { loadPaperclipEnvFile } from "../config/env.js";
|
|
16
|
-
import { readConfig, resolveConfigPath } from "../config/store.js";
|
|
17
|
-
|
|
18
|
-
type RoutinesDisableAllOptions = {
|
|
19
|
-
config?: string;
|
|
20
|
-
dataDir?: string;
|
|
21
|
-
companyId?: string;
|
|
22
|
-
json?: boolean;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type DisableAllRoutinesResult = {
|
|
26
|
-
companyId: string;
|
|
27
|
-
totalRoutines: number;
|
|
28
|
-
pausedCount: number;
|
|
29
|
-
alreadyPausedCount: number;
|
|
30
|
-
archivedCount: number;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
type EmbeddedPostgresInstance = {
|
|
34
|
-
initialise(): Promise<void>;
|
|
35
|
-
start(): Promise<void>;
|
|
36
|
-
stop(): Promise<void>;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
type EmbeddedPostgresCtor = new (opts: {
|
|
40
|
-
databaseDir: string;
|
|
41
|
-
user: string;
|
|
42
|
-
password: string;
|
|
43
|
-
port: number;
|
|
44
|
-
persistent: boolean;
|
|
45
|
-
initdbFlags?: string[];
|
|
46
|
-
onLog?: (message: unknown) => void;
|
|
47
|
-
onError?: (message: unknown) => void;
|
|
48
|
-
}) => EmbeddedPostgresInstance;
|
|
49
|
-
|
|
50
|
-
type EmbeddedPostgresHandle = {
|
|
51
|
-
port: number;
|
|
52
|
-
startedByThisProcess: boolean;
|
|
53
|
-
stop: () => Promise<void>;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
type ClosableDb = ReturnType<typeof createDb> & {
|
|
57
|
-
$client?: {
|
|
58
|
-
end?: (options?: { timeout?: number }) => Promise<void>;
|
|
59
|
-
};
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
function nonEmpty(value: string | null | undefined): string | null {
|
|
63
|
-
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function isPortAvailable(port: number): Promise<boolean> {
|
|
67
|
-
return await new Promise<boolean>((resolve) => {
|
|
68
|
-
const server = net.createServer();
|
|
69
|
-
server.unref();
|
|
70
|
-
server.once("error", () => resolve(false));
|
|
71
|
-
server.listen(port, "127.0.0.1", () => {
|
|
72
|
-
server.close(() => resolve(true));
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function findAvailablePort(preferredPort: number): Promise<number> {
|
|
78
|
-
let port = Math.max(1, Math.trunc(preferredPort));
|
|
79
|
-
while (!(await isPortAvailable(port))) {
|
|
80
|
-
port += 1;
|
|
81
|
-
}
|
|
82
|
-
return port;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function readPidFilePort(postmasterPidFile: string): number | null {
|
|
86
|
-
if (!fs.existsSync(postmasterPidFile)) return null;
|
|
87
|
-
try {
|
|
88
|
-
const lines = fs.readFileSync(postmasterPidFile, "utf8").split("\n");
|
|
89
|
-
const port = Number(lines[3]?.trim());
|
|
90
|
-
return Number.isInteger(port) && port > 0 ? port : null;
|
|
91
|
-
} catch {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function readRunningPostmasterPid(postmasterPidFile: string): number | null {
|
|
97
|
-
if (!fs.existsSync(postmasterPidFile)) return null;
|
|
98
|
-
try {
|
|
99
|
-
const pid = Number(fs.readFileSync(postmasterPidFile, "utf8").split("\n")[0]?.trim());
|
|
100
|
-
if (!Number.isInteger(pid) || pid <= 0) return null;
|
|
101
|
-
process.kill(pid, 0);
|
|
102
|
-
return pid;
|
|
103
|
-
} catch {
|
|
104
|
-
return null;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
async function ensureEmbeddedPostgres(dataDir: string, preferredPort: number): Promise<EmbeddedPostgresHandle> {
|
|
109
|
-
const moduleName = "embedded-postgres";
|
|
110
|
-
let EmbeddedPostgres: EmbeddedPostgresCtor;
|
|
111
|
-
try {
|
|
112
|
-
const mod = await import(moduleName);
|
|
113
|
-
EmbeddedPostgres = mod.default as EmbeddedPostgresCtor;
|
|
114
|
-
} catch {
|
|
115
|
-
throw new Error(
|
|
116
|
-
"Embedded PostgreSQL support requires dependency `embedded-postgres`. Reinstall dependencies and try again.",
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const postmasterPidFile = path.resolve(dataDir, "postmaster.pid");
|
|
121
|
-
const runningPid = readRunningPostmasterPid(postmasterPidFile);
|
|
122
|
-
if (runningPid) {
|
|
123
|
-
return {
|
|
124
|
-
port: readPidFilePort(postmasterPidFile) ?? preferredPort,
|
|
125
|
-
startedByThisProcess: false,
|
|
126
|
-
stop: async () => {},
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
const port = await findAvailablePort(preferredPort);
|
|
131
|
-
const logBuffer = createEmbeddedPostgresLogBuffer();
|
|
132
|
-
const instance = new EmbeddedPostgres({
|
|
133
|
-
databaseDir: dataDir,
|
|
134
|
-
user: "paperclip",
|
|
135
|
-
password: "paperclip",
|
|
136
|
-
port,
|
|
137
|
-
persistent: true,
|
|
138
|
-
initdbFlags: ["--encoding=UTF8", "--locale=C", "--lc-messages=C"],
|
|
139
|
-
onLog: logBuffer.append,
|
|
140
|
-
onError: logBuffer.append,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
if (!fs.existsSync(path.resolve(dataDir, "PG_VERSION"))) {
|
|
144
|
-
try {
|
|
145
|
-
await instance.initialise();
|
|
146
|
-
} catch (error) {
|
|
147
|
-
throw formatEmbeddedPostgresError(error, {
|
|
148
|
-
fallbackMessage: `Failed to initialize embedded PostgreSQL cluster in ${dataDir} on port ${port}`,
|
|
149
|
-
recentLogs: logBuffer.getRecentLogs(),
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (fs.existsSync(postmasterPidFile)) {
|
|
155
|
-
fs.rmSync(postmasterPidFile, { force: true });
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
try {
|
|
159
|
-
await instance.start();
|
|
160
|
-
} catch (error) {
|
|
161
|
-
throw formatEmbeddedPostgresError(error, {
|
|
162
|
-
fallbackMessage: `Failed to start embedded PostgreSQL on port ${port}`,
|
|
163
|
-
recentLogs: logBuffer.getRecentLogs(),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
port,
|
|
169
|
-
startedByThisProcess: true,
|
|
170
|
-
stop: async () => {
|
|
171
|
-
await instance.stop();
|
|
172
|
-
},
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function closeDb(db: ClosableDb): Promise<void> {
|
|
177
|
-
await db.$client?.end?.({ timeout: 5 }).catch(() => undefined);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
async function openConfiguredDb(configPath: string): Promise<{
|
|
181
|
-
db: ClosableDb;
|
|
182
|
-
stop: () => Promise<void>;
|
|
183
|
-
}> {
|
|
184
|
-
const config = readConfig(configPath);
|
|
185
|
-
if (!config) {
|
|
186
|
-
throw new Error(`Config not found at ${configPath}.`);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
let embeddedHandle: EmbeddedPostgresHandle | null = null;
|
|
190
|
-
try {
|
|
191
|
-
if (config.database.mode === "embedded-postgres") {
|
|
192
|
-
embeddedHandle = await ensureEmbeddedPostgres(
|
|
193
|
-
config.database.embeddedPostgresDataDir,
|
|
194
|
-
config.database.embeddedPostgresPort,
|
|
195
|
-
);
|
|
196
|
-
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/postgres`;
|
|
197
|
-
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
|
198
|
-
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/paperclip`;
|
|
199
|
-
await applyPendingMigrations(connectionString);
|
|
200
|
-
const db = createDb(connectionString) as ClosableDb;
|
|
201
|
-
return {
|
|
202
|
-
db,
|
|
203
|
-
stop: async () => {
|
|
204
|
-
await closeDb(db);
|
|
205
|
-
if (embeddedHandle?.startedByThisProcess) {
|
|
206
|
-
await embeddedHandle.stop().catch(() => undefined);
|
|
207
|
-
}
|
|
208
|
-
},
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
const connectionString = nonEmpty(config.database.connectionString);
|
|
213
|
-
if (!connectionString) {
|
|
214
|
-
throw new Error(`Config at ${configPath} does not define a database connection string.`);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
await applyPendingMigrations(connectionString);
|
|
218
|
-
const db = createDb(connectionString) as ClosableDb;
|
|
219
|
-
return {
|
|
220
|
-
db,
|
|
221
|
-
stop: async () => {
|
|
222
|
-
await closeDb(db);
|
|
223
|
-
},
|
|
224
|
-
};
|
|
225
|
-
} catch (error) {
|
|
226
|
-
if (embeddedHandle?.startedByThisProcess) {
|
|
227
|
-
await embeddedHandle.stop().catch(() => undefined);
|
|
228
|
-
}
|
|
229
|
-
throw error;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export async function disableAllRoutinesInConfig(
|
|
234
|
-
options: Pick<RoutinesDisableAllOptions, "config" | "companyId">,
|
|
235
|
-
): Promise<DisableAllRoutinesResult> {
|
|
236
|
-
const configPath = resolveConfigPath(options.config);
|
|
237
|
-
loadPaperclipEnvFile(configPath);
|
|
238
|
-
const companyId =
|
|
239
|
-
nonEmpty(options.companyId)
|
|
240
|
-
?? nonEmpty(process.env.PAPERCLIP_COMPANY_ID)
|
|
241
|
-
?? null;
|
|
242
|
-
if (!companyId) {
|
|
243
|
-
throw new Error("Company ID is required. Pass --company-id or set PAPERCLIP_COMPANY_ID.");
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
const config = readConfig(configPath);
|
|
247
|
-
if (!config) {
|
|
248
|
-
throw new Error(`Config not found at ${configPath}.`);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
let embeddedHandle: EmbeddedPostgresHandle | null = null;
|
|
252
|
-
let db: ClosableDb | null = null;
|
|
253
|
-
try {
|
|
254
|
-
if (config.database.mode === "embedded-postgres") {
|
|
255
|
-
embeddedHandle = await ensureEmbeddedPostgres(
|
|
256
|
-
config.database.embeddedPostgresDataDir,
|
|
257
|
-
config.database.embeddedPostgresPort,
|
|
258
|
-
);
|
|
259
|
-
const adminConnectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/postgres`;
|
|
260
|
-
await ensurePostgresDatabase(adminConnectionString, "paperclip");
|
|
261
|
-
const connectionString = `postgres://paperclip:paperclip@127.0.0.1:${embeddedHandle.port}/paperclip`;
|
|
262
|
-
await applyPendingMigrations(connectionString);
|
|
263
|
-
db = createDb(connectionString) as ClosableDb;
|
|
264
|
-
} else {
|
|
265
|
-
const connectionString = nonEmpty(config.database.connectionString);
|
|
266
|
-
if (!connectionString) {
|
|
267
|
-
throw new Error(`Config at ${configPath} does not define a database connection string.`);
|
|
268
|
-
}
|
|
269
|
-
await applyPendingMigrations(connectionString);
|
|
270
|
-
db = createDb(connectionString) as ClosableDb;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
const existing = await db
|
|
274
|
-
.select({
|
|
275
|
-
id: routines.id,
|
|
276
|
-
status: routines.status,
|
|
277
|
-
})
|
|
278
|
-
.from(routines)
|
|
279
|
-
.where(eq(routines.companyId, companyId));
|
|
280
|
-
|
|
281
|
-
const alreadyPausedCount = existing.filter((routine) => routine.status === "paused").length;
|
|
282
|
-
const archivedCount = existing.filter((routine) => routine.status === "archived").length;
|
|
283
|
-
const idsToPause = existing
|
|
284
|
-
.filter((routine) => routine.status !== "paused" && routine.status !== "archived")
|
|
285
|
-
.map((routine) => routine.id);
|
|
286
|
-
|
|
287
|
-
if (idsToPause.length > 0) {
|
|
288
|
-
await db
|
|
289
|
-
.update(routines)
|
|
290
|
-
.set({
|
|
291
|
-
status: "paused",
|
|
292
|
-
updatedAt: new Date(),
|
|
293
|
-
})
|
|
294
|
-
.where(inArray(routines.id, idsToPause));
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
companyId,
|
|
299
|
-
totalRoutines: existing.length,
|
|
300
|
-
pausedCount: idsToPause.length,
|
|
301
|
-
alreadyPausedCount,
|
|
302
|
-
archivedCount,
|
|
303
|
-
};
|
|
304
|
-
} finally {
|
|
305
|
-
if (db) {
|
|
306
|
-
await closeDb(db);
|
|
307
|
-
}
|
|
308
|
-
if (embeddedHandle?.startedByThisProcess) {
|
|
309
|
-
await embeddedHandle.stop().catch(() => undefined);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
export async function disableAllRoutinesCommand(options: RoutinesDisableAllOptions): Promise<void> {
|
|
315
|
-
const result = await disableAllRoutinesInConfig(options);
|
|
316
|
-
|
|
317
|
-
if (options.json) {
|
|
318
|
-
console.log(JSON.stringify(result, null, 2));
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (result.totalRoutines === 0) {
|
|
323
|
-
console.log(pc.dim(`No routines found for company ${result.companyId}.`));
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
console.log(
|
|
328
|
-
`Paused ${result.pausedCount} routine(s) for company ${result.companyId} ` +
|
|
329
|
-
`(${result.alreadyPausedCount} already paused, ${result.archivedCount} archived).`,
|
|
330
|
-
);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
export function registerRoutineCommands(program: Command): void {
|
|
334
|
-
const routinesCommand = program.command("routines").description("Local routine maintenance commands");
|
|
335
|
-
|
|
336
|
-
routinesCommand
|
|
337
|
-
.command("disable-all")
|
|
338
|
-
.description("Pause all non-archived routines in the configured local instance for one company")
|
|
339
|
-
.option("-c, --config <path>", "Path to config file")
|
|
340
|
-
.option("-d, --data-dir <path>", "Paperclip data directory root (isolates state from ~/.paperclip)")
|
|
341
|
-
.option("-C, --company-id <id>", "Company ID")
|
|
342
|
-
.option("--json", "Output raw JSON")
|
|
343
|
-
.action(async (opts: RoutinesDisableAllOptions) => {
|
|
344
|
-
try {
|
|
345
|
-
await disableAllRoutinesCommand(opts);
|
|
346
|
-
} catch (error) {
|
|
347
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
348
|
-
console.error(pc.red(message));
|
|
349
|
-
process.exit(1);
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
}
|
package/src/commands/run.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
import * as p from "@clack/prompts";
|
|
6
|
-
import pc from "picocolors";
|
|
7
|
-
import { bootstrapCeoInvite } from "./auth-bootstrap-ceo.js";
|
|
8
|
-
import { onboard } from "./onboard.js";
|
|
9
|
-
import { doctor } from "./doctor.js";
|
|
10
|
-
import { loadPaperclipEnvFile } from "../config/env.js";
|
|
11
|
-
import { configExists, resolveConfigPath } from "../config/store.js";
|
|
12
|
-
import type { PaperclipConfig } from "../config/schema.js";
|
|
13
|
-
import { readConfig } from "../config/store.js";
|
|
14
|
-
import {
|
|
15
|
-
describeLocalInstancePaths,
|
|
16
|
-
resolvePaperclipHomeDir,
|
|
17
|
-
resolvePaperclipInstanceId,
|
|
18
|
-
} from "../config/home.js";
|
|
19
|
-
|
|
20
|
-
interface RunOptions {
|
|
21
|
-
config?: string;
|
|
22
|
-
instance?: string;
|
|
23
|
-
repair?: boolean;
|
|
24
|
-
yes?: boolean;
|
|
25
|
-
bind?: "loopback" | "lan" | "tailnet";
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
interface StartedServer {
|
|
29
|
-
apiUrl: string;
|
|
30
|
-
databaseUrl: string;
|
|
31
|
-
host: string;
|
|
32
|
-
listenPort: number;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export async function runCommand(opts: RunOptions): Promise<void> {
|
|
36
|
-
const instanceId = resolvePaperclipInstanceId(opts.instance);
|
|
37
|
-
process.env.PAPERCLIP_INSTANCE_ID = instanceId;
|
|
38
|
-
|
|
39
|
-
const homeDir = resolvePaperclipHomeDir();
|
|
40
|
-
fs.mkdirSync(homeDir, { recursive: true });
|
|
41
|
-
|
|
42
|
-
const paths = describeLocalInstancePaths(instanceId);
|
|
43
|
-
fs.mkdirSync(paths.instanceRoot, { recursive: true });
|
|
44
|
-
|
|
45
|
-
const configPath = resolveConfigPath(opts.config);
|
|
46
|
-
process.env.PAPERCLIP_CONFIG = configPath;
|
|
47
|
-
loadPaperclipEnvFile(configPath);
|
|
48
|
-
|
|
49
|
-
p.intro(pc.bgCyan(pc.black(" paperclipai run ")));
|
|
50
|
-
p.log.message(pc.dim(`Home: ${paths.homeDir}`));
|
|
51
|
-
p.log.message(pc.dim(`Instance: ${paths.instanceId}`));
|
|
52
|
-
p.log.message(pc.dim(`Config: ${configPath}`));
|
|
53
|
-
|
|
54
|
-
if (!configExists(configPath)) {
|
|
55
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
56
|
-
p.log.error("No config found and terminal is non-interactive.");
|
|
57
|
-
p.log.message(`Run ${pc.cyan("paperclipai onboard")} once, then retry ${pc.cyan("paperclipai run")}.`);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
p.log.step("No config found. Starting onboarding...");
|
|
62
|
-
await onboard({ config: configPath, invokedByRun: true, bind: opts.bind });
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
p.log.step("Running doctor checks...");
|
|
66
|
-
const summary = await doctor({
|
|
67
|
-
config: configPath,
|
|
68
|
-
repair: opts.repair ?? true,
|
|
69
|
-
yes: opts.yes ?? true,
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
if (summary.failed > 0) {
|
|
73
|
-
p.log.error("Doctor found blocking issues. Not starting server.");
|
|
74
|
-
process.exit(1);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const config = readConfig(configPath);
|
|
78
|
-
if (!config) {
|
|
79
|
-
p.log.error(`No config found at ${configPath}.`);
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
p.log.step("Starting Paperclip server...");
|
|
84
|
-
const startedServer = await importServerEntry();
|
|
85
|
-
|
|
86
|
-
if (shouldGenerateBootstrapInviteAfterStart(config)) {
|
|
87
|
-
p.log.step("Generating bootstrap CEO invite");
|
|
88
|
-
await bootstrapCeoInvite({
|
|
89
|
-
config: configPath,
|
|
90
|
-
dbUrl: startedServer.databaseUrl,
|
|
91
|
-
baseUrl: resolveBootstrapInviteBaseUrl(config, startedServer),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function resolveBootstrapInviteBaseUrl(
|
|
97
|
-
config: PaperclipConfig,
|
|
98
|
-
startedServer: StartedServer,
|
|
99
|
-
): string {
|
|
100
|
-
const explicitBaseUrl =
|
|
101
|
-
process.env.PAPERCLIP_PUBLIC_URL ??
|
|
102
|
-
process.env.PAPERCLIP_AUTH_PUBLIC_BASE_URL ??
|
|
103
|
-
process.env.BETTER_AUTH_URL ??
|
|
104
|
-
process.env.BETTER_AUTH_BASE_URL ??
|
|
105
|
-
(config.auth.baseUrlMode === "explicit" ? config.auth.publicBaseUrl : undefined);
|
|
106
|
-
|
|
107
|
-
if (typeof explicitBaseUrl === "string" && explicitBaseUrl.trim().length > 0) {
|
|
108
|
-
return explicitBaseUrl.trim().replace(/\/+$/, "");
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return startedServer.apiUrl.replace(/\/api$/, "");
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function formatError(err: unknown): string {
|
|
115
|
-
if (err instanceof Error) {
|
|
116
|
-
if (err.message && err.message.trim().length > 0) return err.message;
|
|
117
|
-
return err.name;
|
|
118
|
-
}
|
|
119
|
-
if (typeof err === "string") return err;
|
|
120
|
-
try {
|
|
121
|
-
return JSON.stringify(err);
|
|
122
|
-
} catch {
|
|
123
|
-
return String(err);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function isModuleNotFoundError(err: unknown): boolean {
|
|
128
|
-
if (!(err instanceof Error)) return false;
|
|
129
|
-
const code = (err as { code?: unknown }).code;
|
|
130
|
-
if (code === "ERR_MODULE_NOT_FOUND") return true;
|
|
131
|
-
return err.message.includes("Cannot find module");
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function getMissingModuleSpecifier(err: unknown): string | null {
|
|
135
|
-
if (!(err instanceof Error)) return null;
|
|
136
|
-
const packageMatch = err.message.match(/Cannot find package '([^']+)' imported from/);
|
|
137
|
-
if (packageMatch?.[1]) return packageMatch[1];
|
|
138
|
-
const moduleMatch = err.message.match(/Cannot find module '([^']+)'/);
|
|
139
|
-
if (moduleMatch?.[1]) return moduleMatch[1];
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function maybeEnableUiDevMiddleware(entrypoint: string): void {
|
|
144
|
-
if (process.env.PAPERCLIP_UI_DEV_MIDDLEWARE !== undefined) return;
|
|
145
|
-
const normalized = entrypoint.replaceAll("\\", "/");
|
|
146
|
-
if (normalized.endsWith("/server/src/index.ts") || normalized.endsWith("@paperclipai/server/src/index.ts")) {
|
|
147
|
-
process.env.PAPERCLIP_UI_DEV_MIDDLEWARE = "true";
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function ensureDevWorkspaceBuildDeps(projectRoot: string): void {
|
|
152
|
-
const buildScript = path.resolve(projectRoot, "scripts/ensure-plugin-build-deps.mjs");
|
|
153
|
-
if (!fs.existsSync(buildScript)) return;
|
|
154
|
-
|
|
155
|
-
const result = spawnSync(process.execPath, [buildScript], {
|
|
156
|
-
cwd: projectRoot,
|
|
157
|
-
stdio: "inherit",
|
|
158
|
-
timeout: 120_000,
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
if (result.error) {
|
|
162
|
-
throw new Error(
|
|
163
|
-
`Failed to prepare workspace build artifacts before starting the Paperclip dev server.\n${formatError(result.error)}`,
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if ((result.status ?? 1) !== 0) {
|
|
168
|
-
throw new Error(
|
|
169
|
-
"Failed to prepare workspace build artifacts before starting the Paperclip dev server.",
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
async function importServerEntry(): Promise<StartedServer> {
|
|
175
|
-
// Dev mode: try local workspace path (monorepo with tsx)
|
|
176
|
-
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
177
|
-
const devEntry = path.resolve(projectRoot, "server/src/index.ts");
|
|
178
|
-
if (fs.existsSync(devEntry)) {
|
|
179
|
-
ensureDevWorkspaceBuildDeps(projectRoot);
|
|
180
|
-
maybeEnableUiDevMiddleware(devEntry);
|
|
181
|
-
const mod = await import(pathToFileURL(devEntry).href);
|
|
182
|
-
return await startServerFromModule(mod, devEntry);
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
// Production mode: import the published @paperclipai/server package
|
|
186
|
-
try {
|
|
187
|
-
const mod = await import("@paperclipai/server");
|
|
188
|
-
return await startServerFromModule(mod, "@paperclipai/server");
|
|
189
|
-
} catch (err) {
|
|
190
|
-
const missingSpecifier = getMissingModuleSpecifier(err);
|
|
191
|
-
const missingServerEntrypoint = !missingSpecifier || missingSpecifier === "@paperclipai/server";
|
|
192
|
-
if (isModuleNotFoundError(err) && missingServerEntrypoint) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
`Could not locate a Paperclip server entrypoint.\n` +
|
|
195
|
-
`Tried: ${devEntry}, @paperclipai/server\n` +
|
|
196
|
-
`${formatError(err)}`,
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Paperclip server failed to start.\n` +
|
|
201
|
-
`${formatError(err)}`,
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function shouldGenerateBootstrapInviteAfterStart(config: PaperclipConfig): boolean {
|
|
207
|
-
return config.server.deploymentMode === "authenticated" && config.database.mode === "embedded-postgres";
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async function startServerFromModule(mod: unknown, label: string): Promise<StartedServer> {
|
|
211
|
-
const startServer = (mod as { startServer?: () => Promise<StartedServer> }).startServer;
|
|
212
|
-
if (typeof startServer !== "function") {
|
|
213
|
-
throw new Error(`Paperclip server entrypoint did not export startServer(): ${label}`);
|
|
214
|
-
}
|
|
215
|
-
return await startServer();
|
|
216
|
-
}
|