Repository navigation
Expand file tree
/
Copy pathbootstrap.ts
More file actions
90 lines (80 loc) · 2.83 KB
/
Copy pathbootstrap.ts
File metadata and controls
90 lines (80 loc) · 2.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import { createKeychainStore, resolveConfiguredProvider } from "@procni/ai";
import {
createAnalyzeJobHandler,
createDraftJobHandler,
createJobWorker,
openDb,
readProjectConfig,
resolveProjectDir,
sweepStaleJobsOnStartup,
type JobWorker,
} from "@procni/core";
import { ProcniError } from "@procni/shared";
import { join } from "node:path";
import { buildServer } from "./app.js";
import { listenOnAvailablePort } from "./listen.js";
import { assertNotAlreadyRunning, releaseLock, writeLock } from "./pidLock.js";
export interface DevServerOptions {
/** Preferred port; `0` lets the OS choose (what tests use to avoid collisions). Defaults to 4173. */
port?: number | undefined;
/** Where the built web SPA lives. Static-serving is skipped if this directory doesn't exist. */
webDistDir: string;
}
export interface DevServerHandle {
url: string;
port: number;
stop: () => Promise<void>;
}
/**
* Everything `procni dev` needs to go from "a project on disk" to "a running
* local server": confirm `procni init` has already run, refuse to start a
* second instance for the same project, open (and migrate) its database,
* resolve a configured AI provider if one exists (the server still starts
* without one — see `resolveConfiguredProvider` — since browsing existing
* data never needs AI), sweep any jobs left stuck `"running"` by a crashed
* previous instance, start the job worker, bind the API server, then record
* the lock so the *next* `procni dev` can find this one.
*/
export async function startDevServer(
repoRoot: string,
options: DevServerOptions,
): Promise<DevServerHandle> {
const projectDir = resolveProjectDir(repoRoot);
const config = await readProjectConfig(repoRoot);
if (!config) {
throw new ProcniError(
"NOT_INITIALIZED",
`${repoRoot} has not been initialized. Run "procni init" first.`,
);
}
await assertNotAlreadyRunning(projectDir);
const db = openDb(join(projectDir, "procni.db"));
const resolved = await resolveConfiguredProvider(createKeychainStore());
await sweepStaleJobsOnStartup(db);
const worker: JobWorker = createJobWorker({
db,
handlers: resolved
? { analyze: createAnalyzeJobHandler(db, resolved.provider), draft: createDraftJobHandler(db, resolved.provider) }
: {},
});
worker.start();
const app = buildServer(options.webDistDir, {
db,
repoRoot,
projectName: config.name,
providerId: resolved?.id ?? null,
defaultModel: resolved?.model ?? "",
});
const port = await listenOnAvailablePort(app, options.port ?? 4173);
await writeLock(projectDir, { pid: process.pid, port, startedAt: new Date().toISOString() });
return {
url: `http://127.0.0.1:${port}`,
port,
stop: async () => {
worker.stop();
await app.close();
await db.destroy();
await releaseLock(projectDir);
},
};
}