@parley-im/parley 0.1.2 → 0.2.0
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 +19 -17
- package/bin/parley +7 -0
- package/dist/access.js +57 -0
- package/dist/account.js +5 -1
- package/dist/agents/claude.js +20 -11
- package/dist/agents/codex.js +10 -5
- package/dist/agents/process.js +35 -4
- package/dist/api.js +21 -10
- package/dist/autostart.js +86 -0
- package/dist/charter.js +4 -4
- package/dist/cli.js +62 -16
- package/dist/config.js +49 -9
- package/dist/control.js +16 -10
- package/dist/daemon.js +34 -10
- package/dist/db.js +111 -27
- package/dist/mcp.js +20 -20
- package/dist/models.js +8 -2
- package/dist/relay.js +25 -9
- package/dist/runner.js +9 -7
- package/dist/seal.js +7 -5
- package/dist/server.js +30 -16
- package/dist/state.js +7 -4
- package/dist/transcript.js +10 -6
- package/dist/version.js +15 -0
- package/package.json +2 -2
- package/web/app.css +21 -13
- package/web/chat.html +1 -1
- package/web/chat.js +120 -54
- package/web/dashboard.js +41 -0
- package/web/ended.html +1 -1
- package/web/missing.html +1 -1
- package/web/panel.html +4 -4
- package/web/panel.js +6 -6
package/dist/cli.js
CHANGED
|
@@ -6,17 +6,19 @@ import { fileURLToPath } from "node:url";
|
|
|
6
6
|
import { loadConfig, paths, cleanEnv } from "./config.js";
|
|
7
7
|
import { Store } from "./db.js";
|
|
8
8
|
import { ensureServer, stopServer } from "./daemon.js";
|
|
9
|
+
import { enableAutostart, disableAutostart } from "./autostart.js";
|
|
10
|
+
import { VERSION } from "./version.js";
|
|
9
11
|
import { readState, linkFor, dashboardUrl, isServerUp } from "./state.js";
|
|
10
12
|
import { transcript, summaryLine } from "./transcript.js";
|
|
11
13
|
import { startSession, endSession, deleteSession, setReplyLimit } from "./control.js";
|
|
12
14
|
import { login, readCredentials, clearCredentials, relay as registry } from "./account.js";
|
|
13
15
|
const HELP = `parley: someone chats with a read-only Claude or Codex running in this project, on this
|
|
14
|
-
computer, and you get everything they said. Good for feedback from
|
|
16
|
+
computer, and you get everything they said. Good for feedback from guests, or any interview.
|
|
15
17
|
|
|
16
18
|
If you're an agent and the user asks for "a parley" (or to get someone's take on this project):
|
|
17
19
|
|
|
18
20
|
1. From the project folder, make one and give the user the printed link to send:
|
|
19
|
-
parley new --
|
|
21
|
+
parley new --guest "Sam" --brief-file - <<'BRIEF'
|
|
20
22
|
What this is and how they'll try it, what the user most wants to learn, known issues to
|
|
21
23
|
skip. Written to the interviewer, in the terms a user sees (no file paths).
|
|
22
24
|
BRIEF
|
|
@@ -28,7 +30,7 @@ With the parley MCP tools (create_session, list_sessions, get_transcript, end_se
|
|
|
28
30
|
use those instead; they do the same.
|
|
29
31
|
|
|
30
32
|
Commands
|
|
31
|
-
parley new --
|
|
33
|
+
parley new --guest NAME (--brief TEXT | --brief-file FILE|-) Make a session here; prints its link
|
|
32
34
|
[--dir PATH] [--greeting TEXT] [--agent claude|codex] [--model MODEL] [--replies N]
|
|
33
35
|
parley list [--ended | --all] [--dir PATH] Sessions, with new-message counts and replies used
|
|
34
36
|
parley show ID [--new] Transcript as Markdown; --new for what's unread
|
|
@@ -38,8 +40,10 @@ Commands
|
|
|
38
40
|
parley login | logout | whoami Link this computer to parley.im, or unlink it
|
|
39
41
|
parley start | stop | status The background server (holds the parley.im connection)
|
|
40
42
|
parley panel Open the control panel (localhost)
|
|
41
|
-
parley install Add
|
|
43
|
+
parley install Add parley's MCP tools to Claude Code and Codex; start at login
|
|
44
|
+
parley uninstall Undo install (your conversations stay)
|
|
42
45
|
parley serve | mcp Run the server in the foreground | MCP over stdio
|
|
46
|
+
parley --version
|
|
43
47
|
|
|
44
48
|
Conversations and screenshots live in each project's .parley folder (kept out of git), with a
|
|
45
49
|
readable transcript.md per session. Settings and your parley.im login live in ${paths.home}.`;
|
|
@@ -93,7 +97,7 @@ async function main() {
|
|
|
93
97
|
return;
|
|
94
98
|
}
|
|
95
99
|
case "stop": {
|
|
96
|
-
console.log((await stopServer()) ? "Stopped." : "Not running.");
|
|
100
|
+
console.log((await stopServer(config)) ? "Stopped." : "Not running.");
|
|
97
101
|
return;
|
|
98
102
|
}
|
|
99
103
|
case "status": {
|
|
@@ -111,7 +115,7 @@ async function main() {
|
|
|
111
115
|
args: rest,
|
|
112
116
|
options: {
|
|
113
117
|
dir: { type: "string" },
|
|
114
|
-
|
|
118
|
+
guest: { type: "string" },
|
|
115
119
|
brief: { type: "string" },
|
|
116
120
|
"brief-file": { type: "string" },
|
|
117
121
|
greeting: { type: "string" },
|
|
@@ -123,22 +127,25 @@ async function main() {
|
|
|
123
127
|
// "-" reads the brief from stdin, so a long one can come in a heredoc.
|
|
124
128
|
const file = values["brief-file"];
|
|
125
129
|
const brief = file ? readFileSync(file === "-" ? 0 : file, "utf8") : values.brief;
|
|
126
|
-
if (!values.
|
|
127
|
-
fail("Need --
|
|
130
|
+
if (!values.guest || !brief)
|
|
131
|
+
fail("Need --guest and --brief (or --brief-file).");
|
|
128
132
|
if (values.agent && values.agent !== "claude" && values.agent !== "codex")
|
|
129
133
|
fail("--agent is claude or codex.");
|
|
134
|
+
const replies = values.replies === undefined ? null : Number(values.replies);
|
|
135
|
+
if (replies !== null && !(Number.isInteger(replies) && replies > 0))
|
|
136
|
+
fail("--replies is a whole number above zero.");
|
|
130
137
|
const store = new Store();
|
|
131
138
|
const made = await startSession(store, config, {
|
|
132
139
|
projectDir: resolve(values.dir ?? process.cwd()),
|
|
133
|
-
|
|
140
|
+
guest: values.guest,
|
|
134
141
|
brief,
|
|
135
142
|
greeting: values.greeting,
|
|
136
|
-
agent: values.agent ??
|
|
143
|
+
agent: values.agent ?? null,
|
|
137
144
|
model: values.model,
|
|
138
|
-
replyLimit:
|
|
145
|
+
replyLimit: replies,
|
|
139
146
|
}).catch((error) => fail(error.message));
|
|
140
147
|
const { session } = made;
|
|
141
|
-
console.log(`Session ${session.id} for ${session.
|
|
148
|
+
console.log(`Session ${session.id} for ${session.guest} in ${session.project_dir}`);
|
|
142
149
|
console.log(`Link: ${linkFor(session, config)}`);
|
|
143
150
|
console.log(`Replies allowed: ${session.reply_limit}`);
|
|
144
151
|
console.log(`Watch: ${dashboardUrl(config, session)}`);
|
|
@@ -197,11 +204,12 @@ async function main() {
|
|
|
197
204
|
if (session.status !== "open")
|
|
198
205
|
return console.log(`${session.id} had already ended.`);
|
|
199
206
|
await endSession(config, store, session.id);
|
|
200
|
-
console.log(`Ended ${session.id} (${session.
|
|
207
|
+
console.log(`Ended ${session.id} (${session.guest} on ${session.project_name}). Its link no longer works.`);
|
|
201
208
|
}
|
|
202
209
|
else {
|
|
203
210
|
await deleteSession(config, store, session.id);
|
|
204
|
-
console.log(`Deleted ${session.id} (${session.
|
|
211
|
+
console.log(`Deleted ${session.id} (${session.guest} on ${session.project_name}) and its screenshots.`);
|
|
212
|
+
console.log(`${session.agent === "codex" ? "Codex" : "Claude Code"} keeps its own log of the conversation (in ${session.agent === "codex" ? "~/.codex/sessions" : "~/.claude/projects"}); delete it there too if you want it gone everywhere.`);
|
|
205
213
|
}
|
|
206
214
|
return;
|
|
207
215
|
}
|
|
@@ -212,6 +220,13 @@ async function main() {
|
|
|
212
220
|
}
|
|
213
221
|
case "install":
|
|
214
222
|
return install(config);
|
|
223
|
+
case "uninstall":
|
|
224
|
+
return uninstall(config);
|
|
225
|
+
case "version":
|
|
226
|
+
case "--version":
|
|
227
|
+
case "-v":
|
|
228
|
+
console.log(VERSION);
|
|
229
|
+
return;
|
|
215
230
|
case undefined:
|
|
216
231
|
case "help":
|
|
217
232
|
case "--help":
|
|
@@ -222,7 +237,7 @@ async function main() {
|
|
|
222
237
|
fail(`Unknown command: ${command}\n\n${HELP}`);
|
|
223
238
|
}
|
|
224
239
|
}
|
|
225
|
-
function install(config) {
|
|
240
|
+
async function install(config) {
|
|
226
241
|
const launch = [process.execPath, "--no-warnings", CLI_PATH, "mcp"];
|
|
227
242
|
const env = cleanEnv();
|
|
228
243
|
const run = (bin, args) => {
|
|
@@ -248,8 +263,34 @@ function install(config) {
|
|
|
248
263
|
}
|
|
249
264
|
else
|
|
250
265
|
console.log("Codex: not found, skipped.");
|
|
266
|
+
// The server holds your links; it should come back after a restart.
|
|
267
|
+
await stopServer(config);
|
|
268
|
+
console.log(`Server: ${enableAutostart(CLI_PATH)}`);
|
|
269
|
+
if (!(await ensureServer(config)))
|
|
270
|
+
console.log(`The server didn't come up. See ${paths.log}`);
|
|
251
271
|
console.log(`\nStart a new Claude or Codex session in any project and ask it to make a parley for someone.`);
|
|
252
272
|
}
|
|
273
|
+
// Undoes `parley install`. Your conversations stay in each project's .parley
|
|
274
|
+
// folder, and settings and login in ~/.parley.
|
|
275
|
+
async function uninstall(config) {
|
|
276
|
+
const run = (bin, args) => {
|
|
277
|
+
try {
|
|
278
|
+
execFileSync(bin, args, { env: cleanEnv(), stdio: "pipe" });
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
catch {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
if (config.claude.bin.startsWith("/") && run(config.claude.bin, ["mcp", "remove", "--scope", "user", "parley"]))
|
|
286
|
+
console.log("Claude Code: removed the parley MCP server.");
|
|
287
|
+
if (config.codex.bin.startsWith("/") && run(config.codex.bin, ["mcp", "remove", "parley"]))
|
|
288
|
+
console.log("Codex: removed the parley MCP server.");
|
|
289
|
+
console.log(disableAutostart() ?? "Wasn't set to start at login.");
|
|
290
|
+
if (await stopServer(config))
|
|
291
|
+
console.log("Stopped the server.");
|
|
292
|
+
console.log(`\nYour conversations are still in each project's .parley folder, and settings and login in ${paths.home}.`);
|
|
293
|
+
}
|
|
253
294
|
async function printRelay(config) {
|
|
254
295
|
const creds = readCredentials(config);
|
|
255
296
|
if (!creds)
|
|
@@ -270,4 +311,9 @@ function fail(message) {
|
|
|
270
311
|
console.error(message);
|
|
271
312
|
process.exit(1);
|
|
272
313
|
}
|
|
273
|
-
|
|
314
|
+
// Mistakes in how parley was called get a sentence, not a stack trace.
|
|
315
|
+
main().catch((error) => {
|
|
316
|
+
if (typeof error?.code === "string" && error.code.startsWith("ERR_PARSE_ARGS"))
|
|
317
|
+
fail(`${error.message}\nRun parley on its own for help.`);
|
|
318
|
+
fail(process.env.PARLEY_DEBUG ? String(error?.stack ?? error) : String(error?.message ?? error));
|
|
319
|
+
});
|
package/dist/config.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, accessSync, constants } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { join, delimiter } from "node:path";
|
|
3
|
+
import { join, delimiter, basename } from "node:path";
|
|
4
4
|
export const HOME = process.env.PARLEY_HOME ?? join(homedir(), ".parley");
|
|
5
5
|
// Settings, your parley.im login and the server's state. Conversations live
|
|
6
6
|
// in each project's own .parley folder (see db.ts); projects.json lists them.
|
|
@@ -13,6 +13,41 @@ export const paths = {
|
|
|
13
13
|
// Your computer's link to a parley.im account, from `parley login`.
|
|
14
14
|
credentials: join(HOME, "credentials.json"),
|
|
15
15
|
};
|
|
16
|
+
export const BLOCKED_BY_DEFAULT = [
|
|
17
|
+
// Environment files and other places secrets usually live.
|
|
18
|
+
"**/.env",
|
|
19
|
+
"**/.env.*",
|
|
20
|
+
"**/.dev.vars",
|
|
21
|
+
"**/secrets/**",
|
|
22
|
+
"**/secrets.json",
|
|
23
|
+
"**/secrets.yml",
|
|
24
|
+
"**/secrets.yaml",
|
|
25
|
+
"**/credentials.json",
|
|
26
|
+
"**/*.tfstate",
|
|
27
|
+
"**/*.tfvars",
|
|
28
|
+
// Keys and certificates.
|
|
29
|
+
"**/*.pem",
|
|
30
|
+
"**/*.key",
|
|
31
|
+
"**/*.p12",
|
|
32
|
+
"**/*.pfx",
|
|
33
|
+
"**/*.jks",
|
|
34
|
+
"**/*.keystore",
|
|
35
|
+
"**/id_rsa*",
|
|
36
|
+
"**/id_dsa*",
|
|
37
|
+
"**/id_ecdsa*",
|
|
38
|
+
"**/id_ed25519*",
|
|
39
|
+
// Tool logins.
|
|
40
|
+
"**/.ssh/**",
|
|
41
|
+
"**/.aws/**",
|
|
42
|
+
"**/.gnupg/**",
|
|
43
|
+
"**/.kube/**",
|
|
44
|
+
"**/.docker/**",
|
|
45
|
+
"**/.npmrc",
|
|
46
|
+
"**/.pypirc",
|
|
47
|
+
"**/.netrc",
|
|
48
|
+
"**/.git-credentials",
|
|
49
|
+
"**/.git/config",
|
|
50
|
+
];
|
|
16
51
|
const defaults = {
|
|
17
52
|
relayUrl: "https://parley.im",
|
|
18
53
|
port: 4747,
|
|
@@ -21,6 +56,7 @@ const defaults = {
|
|
|
21
56
|
replyLimit: 100,
|
|
22
57
|
claude: { bin: "claude", model: null, effort: null },
|
|
23
58
|
codex: { bin: "codex", model: null, effort: null },
|
|
59
|
+
interviewer: { readConversations: false, blocked: BLOCKED_BY_DEFAULT, allowHomeFolder: false },
|
|
24
60
|
};
|
|
25
61
|
export function ensureHome() {
|
|
26
62
|
mkdirSync(paths.home, { recursive: true });
|
|
@@ -40,18 +76,20 @@ export function loadConfig() {
|
|
|
40
76
|
replyLimit: pick("replyLimit"),
|
|
41
77
|
claude: { ...defaults.claude, ...stored.claude },
|
|
42
78
|
codex: { ...defaults.codex, ...stored.codex },
|
|
79
|
+
interviewer: { ...defaults.interviewer, ...stored.interviewer },
|
|
43
80
|
};
|
|
44
81
|
// The server is often started from inside an MCP server with a thin PATH,
|
|
45
|
-
// so binaries are pinned to absolute paths the first time they are found
|
|
46
|
-
|
|
82
|
+
// so binaries are pinned to absolute paths the first time they are found,
|
|
83
|
+
// and found again if they move (say, from npm to a native installer).
|
|
84
|
+
let changed = !existsSync(paths.config) || Object.keys(stored).some((key) => !(key in defaults)) || !stored.interviewer;
|
|
47
85
|
for (const [get, set] of [
|
|
48
86
|
[() => config.claude.bin, (v) => (config.claude.bin = v)],
|
|
49
87
|
[() => config.codex.bin, (v) => (config.codex.bin = v)],
|
|
50
88
|
]) {
|
|
51
89
|
const current = get();
|
|
52
|
-
if (current.startsWith("/"))
|
|
90
|
+
if (current.startsWith("/") && existsSync(current))
|
|
53
91
|
continue;
|
|
54
|
-
const found = findBin(current);
|
|
92
|
+
const found = findBin(basename(current));
|
|
55
93
|
if (found) {
|
|
56
94
|
set(found);
|
|
57
95
|
changed = true;
|
|
@@ -84,13 +122,15 @@ export function findBin(name) {
|
|
|
84
122
|
}
|
|
85
123
|
return null;
|
|
86
124
|
}
|
|
87
|
-
// Agents run as you, on your own subscription.
|
|
88
|
-
//
|
|
89
|
-
// CLIs fall back to your normal login.
|
|
125
|
+
// Agents run as you, on your own subscription. What a host session (Claude
|
|
126
|
+
// Code itself, an MCP client) put in the environment is dropped, API keys
|
|
127
|
+
// included, so the CLIs fall back to your normal login. Settings that choose
|
|
128
|
+
// where that login lives (a config folder, Bedrock, Vertex, a proxy) stay.
|
|
129
|
+
const KEEP = new Set(["CLAUDE_CONFIG_DIR", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "ANTHROPIC_BASE_URL", "ANTHROPIC_VERTEX_PROJECT_ID"]);
|
|
90
130
|
export function cleanEnv() {
|
|
91
131
|
const env = {};
|
|
92
132
|
for (const [key, value] of Object.entries(process.env)) {
|
|
93
|
-
if (/^(CLAUDE|ANTHROPIC_|MCP_)/.test(key))
|
|
133
|
+
if (/^(CLAUDE|ANTHROPIC_|MCP_)/.test(key) && !KEEP.has(key))
|
|
94
134
|
continue;
|
|
95
135
|
env[key] = value;
|
|
96
136
|
}
|
package/dist/control.js
CHANGED
|
@@ -1,28 +1,34 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { isAbsolute
|
|
1
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
3
|
import { isServerUp } from "./state.js";
|
|
4
4
|
import { readCredentials, relay as registry } from "./account.js";
|
|
5
5
|
import { ensureServer } from "./daemon.js";
|
|
6
|
+
import { agentProblem, checkProjectFolder, defaultAgent } from "./access.js";
|
|
6
7
|
// The one way sessions get made, from the CLI, MCP tools or your control panel.
|
|
7
8
|
export function createSession(store, config, input) {
|
|
8
9
|
if (!isAbsolute(input.projectDir))
|
|
9
10
|
throw new Error(`Use the full path of the project folder, not ${input.projectDir}.`);
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
if (!existsSync(input.projectDir) || !statSync(input.projectDir).isDirectory())
|
|
12
|
+
throw new Error(`Not a folder: ${input.projectDir}`);
|
|
13
|
+
const projectDir = realpathSync(input.projectDir);
|
|
14
|
+
checkProjectFolder(projectDir, config);
|
|
15
|
+
const guest = input.guest.trim();
|
|
14
16
|
const brief = input.brief.trim();
|
|
15
|
-
if (!
|
|
17
|
+
if (!guest)
|
|
16
18
|
throw new Error("Say who the session is for.");
|
|
17
19
|
if (!brief)
|
|
18
20
|
throw new Error("Write a brief for the interviewer.");
|
|
19
21
|
const replyLimit = input.replyLimit ?? config.replyLimit;
|
|
20
22
|
if (!Number.isInteger(replyLimit) || replyLimit < 1)
|
|
21
23
|
throw new Error("The reply limit has to be a whole number above zero.");
|
|
22
|
-
|
|
24
|
+
const agent = input.agent ?? defaultAgent(config);
|
|
25
|
+
const problem = agentProblem(config, agent);
|
|
26
|
+
if (problem)
|
|
27
|
+
throw new Error(problem);
|
|
28
|
+
return store.createSession({ ...input, projectDir, guest, brief, replyLimit, agent });
|
|
23
29
|
}
|
|
24
30
|
// Changes go through the server when it's running, so a reply in progress
|
|
25
|
-
// stops, any open
|
|
31
|
+
// stops, any open guest page updates at once, and parley.im hears about it.
|
|
26
32
|
async function viaServer(config, path, body) {
|
|
27
33
|
const res = await fetch(`http://localhost:${config.dashboardPort}${path}`, {
|
|
28
34
|
method: "POST",
|
|
@@ -39,7 +45,7 @@ async function viaServer(config, path, body) {
|
|
|
39
45
|
export async function startSession(store, config, input) {
|
|
40
46
|
if (await ensureServer(config)) {
|
|
41
47
|
const made = await viaServer(config, "/api/sessions", input);
|
|
42
|
-
return { session: store.session(made.id), shared: made.shared, serverUp: true };
|
|
48
|
+
return { session: made.session ?? store.session(made.id), shared: made.shared, serverUp: true };
|
|
43
49
|
}
|
|
44
50
|
const session = createSession(store, config, input);
|
|
45
51
|
return { session, shared: false, serverUp: false };
|
package/dist/daemon.js
CHANGED
|
@@ -1,15 +1,30 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { openSync } from "node:fs";
|
|
2
|
+
import { openSync, rmSync } from "node:fs";
|
|
3
3
|
import { join, dirname } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { paths, cleanEnv } from "./config.js";
|
|
6
|
-
import { readState,
|
|
6
|
+
import { readState, serverHealth } from "./state.js";
|
|
7
|
+
import { VERSION, compareVersions } from "./version.js";
|
|
7
8
|
// cli.ts in a checkout, cli.js once built for npm.
|
|
8
9
|
const CLI = join(dirname(fileURLToPath(import.meta.url)), import.meta.url.endsWith(".ts") ? "cli.ts" : "cli.js");
|
|
9
10
|
// Starts the server in the background if it isn't running, and waits until
|
|
10
|
-
// it answers. It holds this computer's connection to parley.im.
|
|
11
|
+
// it answers. It holds this computer's connection to parley.im. A server from
|
|
12
|
+
// an older parley is replaced; one newer than this process means this process
|
|
13
|
+
// is the stale one (an MCP server started before an update).
|
|
11
14
|
export async function ensureServer(config, waitMs = 20_000) {
|
|
12
|
-
|
|
15
|
+
const running = await serverHealth(config);
|
|
16
|
+
if (running?.version && running.version !== VERSION) {
|
|
17
|
+
if (compareVersions(running.version, VERSION) > 0) {
|
|
18
|
+
throw new Error(`parley was updated to ${running.version}, but this process is still ${VERSION}. Restart this Claude Code or Codex session (or the parley command) to use the new version.`);
|
|
19
|
+
}
|
|
20
|
+
await stopServer(config);
|
|
21
|
+
await waitUntilDown(config);
|
|
22
|
+
}
|
|
23
|
+
else if (running && !running.version) {
|
|
24
|
+
await stopServer(config);
|
|
25
|
+
await waitUntilDown(config);
|
|
26
|
+
}
|
|
27
|
+
if (!(await serverHealth(config))) {
|
|
13
28
|
const log = openSync(paths.log, "a");
|
|
14
29
|
const child = spawn(process.execPath, ["--no-warnings", CLI, "serve"], {
|
|
15
30
|
detached: true,
|
|
@@ -22,18 +37,27 @@ export async function ensureServer(config, waitMs = 20_000) {
|
|
|
22
37
|
const deadline = Date.now() + waitMs;
|
|
23
38
|
while (Date.now() < deadline) {
|
|
24
39
|
const state = readState();
|
|
25
|
-
if (state && (await
|
|
40
|
+
if (state && (await serverHealth(config)))
|
|
26
41
|
return state;
|
|
27
42
|
await new Promise((r) => setTimeout(r, 400));
|
|
28
43
|
}
|
|
29
|
-
return (await
|
|
44
|
+
return (await serverHealth(config)) ? readState() : null;
|
|
30
45
|
}
|
|
31
|
-
|
|
32
|
-
const
|
|
33
|
-
|
|
46
|
+
async function waitUntilDown(config, waitMs = 10_000) {
|
|
47
|
+
const deadline = Date.now() + waitMs;
|
|
48
|
+
while (Date.now() < deadline && (await serverHealth(config)))
|
|
49
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
50
|
+
}
|
|
51
|
+
// Stops the server, but only a process that answers as parley's server: the
|
|
52
|
+
// pid in server.json may be stale and belong to something else by now.
|
|
53
|
+
export async function stopServer(config) {
|
|
54
|
+
const running = await serverHealth(config);
|
|
55
|
+
if (!running) {
|
|
56
|
+
rmSync(paths.state, { force: true });
|
|
34
57
|
return false;
|
|
58
|
+
}
|
|
35
59
|
try {
|
|
36
|
-
process.kill(
|
|
60
|
+
process.kill(running.pid, "SIGTERM");
|
|
37
61
|
return true;
|
|
38
62
|
}
|
|
39
63
|
catch {
|