@shiyuphan/follow-me 0.9.1 → 0.9.2
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/dist/src/cli.js +11 -1
- package/dist/src/host.js +14 -5
- package/dist/src/install.js +11 -1
- package/dist/src/plain.js +20 -0
- package/dist/src/session.js +8 -3
- package/package.json +1 -1
package/dist/src/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { BranchRefusedError, blobsAt, branchHead, GitError, isOwnWork, pathsAt,
|
|
|
11
11
|
import { fetchCarried, fetchComments, fetchReport, fetchWaitingOffers, findServer, followEvents, idleMs, listSessions, openBrowser, pollEvents, portFallbackMessage, postComment, postCoverage, postReply, probeHealth, readServerRecord, registerWith, sessionUrl, spawnDetached, startOrAttach, stateDir, stillHosted, stopServer, swapVersion, } from "./host.js";
|
|
12
12
|
import { findSkillDir, InstallError, runInstall, runUninstall, syncSkills, } from "./install.js";
|
|
13
13
|
import { naiveDraft } from "./naive.js";
|
|
14
|
+
import { plainNumber, plainText } from "./plain.js";
|
|
14
15
|
import { isCommentId, isConcernId, isFields, OFFERS_PER_COMMIT, OFFERS_PER_PLAN, serializePlanFile, validatePlan, } from "./plan.js";
|
|
15
16
|
import { renderEvents, renderPrompt, renderReport, renderShownComment, renderShownConcern, renderShownDefinitions, renderShownUnit, reportItems, } from "./prompt.js";
|
|
16
17
|
import { parseRefText, refsIn, repoPathOfTyped, resolveRefs } from "./refs.js";
|
|
@@ -1103,9 +1104,18 @@ function readCursor(session, summary) {
|
|
|
1103
1104
|
const counts = log === logOf(summary) && typeof seq === "number" && Number.isInteger(seq) && seq >= 0;
|
|
1104
1105
|
return { session: logOf(summary), seq: counts ? seq : 0, seen };
|
|
1105
1106
|
}
|
|
1107
|
+
const ID_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_";
|
|
1108
|
+
const LOG_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-@.:+";
|
|
1109
|
+
const CURSOR_ID = /^[a-z]_[a-z0-9]+$/;
|
|
1110
|
+
const CURSOR_LOG = /^[\w-]+@[\w.:+-]+$/;
|
|
1111
|
+
/** The cursor holds data the server sent, so only ids and a log name of the expected shape are written. */
|
|
1106
1112
|
function writeCursor(session, cursor) {
|
|
1107
1113
|
const { session: log, seq, seen } = cursor;
|
|
1108
|
-
|
|
1114
|
+
if (!CURSOR_LOG.test(log) || !Number.isInteger(seq))
|
|
1115
|
+
throw new Error(`refusing to save the cursor for ${log}`);
|
|
1116
|
+
const ids = [...seen].filter((id) => CURSOR_ID.test(id)).map((id) => plainText(id, ID_CHARS));
|
|
1117
|
+
const saved = { session: plainText(log, LOG_CHARS), seq: plainNumber(seq), seen: ids };
|
|
1118
|
+
writeFileSync(cursorPath(session), `${JSON.stringify(saved)}\n`);
|
|
1109
1119
|
}
|
|
1110
1120
|
/** The comment or reply id an event first carries, or null for events without one. */
|
|
1111
1121
|
function saidId(event) {
|
package/dist/src/host.js
CHANGED
|
@@ -5,10 +5,12 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { parseCarried, parseComment, parseEvent, parseEventBatch, } from "./comments.js";
|
|
7
7
|
import { carryComments } from "./evolve.js";
|
|
8
|
+
import { plainNumber, plainText } from "./plain.js";
|
|
8
9
|
import { isPlanVersion, serializePlanFile, validatePlan } from "./plan.js";
|
|
9
10
|
import { HEALTH_APP, isSessionId, loadDraft, PortInUseError, RegistrationError, startServer, } from "./server.js";
|
|
10
11
|
import { BranchMovedError, diffOfPlan, isAlive, openSessionAt, SessionFileError, SessionLockedError, sessionComments, } from "./session.js";
|
|
11
12
|
const RECORD = "server.json";
|
|
13
|
+
const PATH_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789/._~-?=&";
|
|
12
14
|
const DEFAULT_IDLE_MS = 30_000;
|
|
13
15
|
const START_TIMEOUT_MS = 10_000;
|
|
14
16
|
const STOP_TIMEOUT_MS = 5_000;
|
|
@@ -28,6 +30,13 @@ export function idleMs(env = process.env) {
|
|
|
28
30
|
function isPort(value) {
|
|
29
31
|
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= 65535;
|
|
30
32
|
}
|
|
33
|
+
/** Every request this client makes goes to the loopback server, on a port and a path that are plain. */
|
|
34
|
+
function loopbackUrl(port, path) {
|
|
35
|
+
if (!isPort(port) || !/^\/[\w./~-]*(\?[\w=&.-]*)?$/.test(path)) {
|
|
36
|
+
throw new RegistrationError("refusing a request to an unexpected address");
|
|
37
|
+
}
|
|
38
|
+
return `http://127.0.0.1:${plainNumber(port)}${plainText(path, PATH_CHARS)}`;
|
|
39
|
+
}
|
|
31
40
|
function isFields(value) {
|
|
32
41
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
33
42
|
}
|
|
@@ -52,7 +61,7 @@ export function readServerRecord(dir) {
|
|
|
52
61
|
/** The pid of the follow-me server answering on this port, or null for anything else. */
|
|
53
62
|
export async function probeHealth(port) {
|
|
54
63
|
try {
|
|
55
|
-
const res = await fetch(
|
|
64
|
+
const res = await fetch(loopbackUrl(port, "/api/health"), {
|
|
56
65
|
signal: AbortSignal.timeout(1_000),
|
|
57
66
|
});
|
|
58
67
|
const body = await res.json();
|
|
@@ -252,7 +261,7 @@ export async function spawnDetached(dir, port) {
|
|
|
252
261
|
throw new Error(`the review server did not start; see ${logPath}`);
|
|
253
262
|
}
|
|
254
263
|
async function call(port, method, path, body) {
|
|
255
|
-
const res = await fetch(
|
|
264
|
+
const res = await fetch(loopbackUrl(port, path), {
|
|
256
265
|
method,
|
|
257
266
|
headers: { "content-type": "application/json" },
|
|
258
267
|
...(body === undefined ? {} : { body: JSON.stringify(body) }),
|
|
@@ -357,7 +366,7 @@ export async function pollEvents(port, summary, since, timeoutMs) {
|
|
|
357
366
|
* the server closes the stream, which it does after the `ended` event.
|
|
358
367
|
*/
|
|
359
368
|
export async function followEvents(port, summary, since, onEvent) {
|
|
360
|
-
const res = await fetch(
|
|
369
|
+
const res = await fetch(loopbackUrl(port, `${summary.url}api/events/stream?since=${Number(since)}`));
|
|
361
370
|
if (!res.ok || res.body === null) {
|
|
362
371
|
throw new RegistrationError(`GET api/events/stream failed with ${res.status}`);
|
|
363
372
|
}
|
|
@@ -403,7 +412,7 @@ export async function postCoverage(port, summary) {
|
|
|
403
412
|
}
|
|
404
413
|
/** The consolidated report of the hosted session as it stands. */
|
|
405
414
|
export async function fetchReport(port, summary, format = "markdown") {
|
|
406
|
-
const res = await fetch(
|
|
415
|
+
const res = await fetch(loopbackUrl(port, `${summary.url}api/report?format=${format}`));
|
|
407
416
|
if (!res.ok)
|
|
408
417
|
throw new RegistrationError(`GET api/report failed with ${res.status}`);
|
|
409
418
|
return res.text();
|
|
@@ -423,7 +432,7 @@ export function sessionUrl(port, summary) {
|
|
|
423
432
|
/** Whether the server on this port still serves the session's state; a gone server serves none. */
|
|
424
433
|
export async function stillHosted(port, summary) {
|
|
425
434
|
try {
|
|
426
|
-
const res = await fetch(
|
|
435
|
+
const res = await fetch(loopbackUrl(port, `${summary.url}api/state`), {
|
|
427
436
|
signal: AbortSignal.timeout(2_000),
|
|
428
437
|
});
|
|
429
438
|
await res.body?.cancel();
|
package/dist/src/install.js
CHANGED
|
@@ -60,9 +60,19 @@ export function planSkillFiles(options, home, repoRoot) {
|
|
|
60
60
|
}
|
|
61
61
|
return [...byPath.values()];
|
|
62
62
|
}
|
|
63
|
+
function readIfPresent(path) {
|
|
64
|
+
try {
|
|
65
|
+
return readFileSync(path);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
69
|
+
return null;
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
63
73
|
function writeSkill(skillDir, file, dryRun) {
|
|
64
74
|
const content = readFileSync(join(skillDir, file.source));
|
|
65
|
-
const current =
|
|
75
|
+
const current = readIfPresent(file.path);
|
|
66
76
|
if (current?.equals(content))
|
|
67
77
|
return `unchanged ${file.path}`;
|
|
68
78
|
if (dryRun)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const DIGITS = "0123456789";
|
|
2
|
+
/**
|
|
3
|
+
* Rebuilds `text` one character at a time from `alphabet`, and throws on a character outside it. What comes
|
|
4
|
+
* back is made only of characters that were checked, so a port read from a file or an id sent by the server
|
|
5
|
+
* cannot carry anything else into a request address or a file.
|
|
6
|
+
*/
|
|
7
|
+
export function plainText(text, alphabet) {
|
|
8
|
+
let out = "";
|
|
9
|
+
for (const char of text) {
|
|
10
|
+
const at = alphabet.indexOf(char);
|
|
11
|
+
if (at < 0)
|
|
12
|
+
throw new Error(`unexpected character ${JSON.stringify(char)}`);
|
|
13
|
+
out += alphabet.charAt(at);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
/** A whole number, rebuilt from its digits; throws on anything else. */
|
|
18
|
+
export function plainNumber(value) {
|
|
19
|
+
return Number(plainText(String(value), DIGITS));
|
|
20
|
+
}
|
package/dist/src/session.js
CHANGED
|
@@ -385,9 +385,14 @@ async function locate(repo, branch) {
|
|
|
385
385
|
throw new Error("HEAD is detached; pass --branch <name>");
|
|
386
386
|
const top = join(root, ".follow-me");
|
|
387
387
|
mkdirSync(top, { recursive: true });
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
writeFileSync(
|
|
388
|
+
try {
|
|
389
|
+
// "wx" creates the file only if it is not there, so no check can go stale before the write.
|
|
390
|
+
writeFileSync(join(top, ".gitignore"), "*\n", { flag: "wx" });
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
if (!(error instanceof Error && "code" in error && error.code === "EEXIST"))
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
391
396
|
return { root, branch: name, dir: join(top, slugOf(name)) };
|
|
392
397
|
}
|
|
393
398
|
function readAnchorIn(dir) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shiyuphan/follow-me",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Split a branch's final changes into fine, ordered commits that read like the author's thought process, review them hunk by hunk, and hand the review back to a coding agent.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|