mainflow 0.8.0 → 0.9.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/dist/cli.js +3 -3
- package/dist/{dist-DCafRe8c.js → dist-_ArTiNbL.js} +10 -1
- package/dist/hooks.js +8 -5
- package/dist/host.js +1 -1
- package/dist/{launch-V6oqJDGG.js → launch-D2WJRvPD.js} +80 -24
- package/dist/launch.d.ts +20 -4
- package/dist/launch.js +2 -2
- package/dist/web/assets/{index-oh5tFaO9.js → index-CZjPKVxY.js} +6 -6
- package/dist/web/assets/index-miwi9q2z.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-C3g64Xyp.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as
|
|
3
|
-
import { r as launchUi, s as
|
|
2
|
+
import { A as appendOps, B as loadHub, C as setSessionStatus, D as updateAction, F as reopenIssue, I as setFindingArchived, K as WriteError, L as setFindingStatus, M as createFinding, N as createIssue, O as sessionBriefing, P as planIssue, R as setIssueArchived, T as startSession, U as MAINLINE_FILE, W as TASK_FILE, Y as findHubRoot, _ as addAction, a as createDecision, b as readSession, c as dropChecklistItem, d as setChecklistItem, f as setCriterion, g as updateTask, h as supersedeDecision, i as addRepo, j as closeIssue, l as initHub, n as acceptDecision, o as createMainline, p as setMainlineStatus, r as addHub, s as createTask, t as migrateHub, u as setArchived, x as setSessionArchived, z as HubError } from "./dist-_ArTiNbL.js";
|
|
3
|
+
import { c as listProgress, r as launchUi, s as CLI_VERSION } from "./launch-D2WJRvPD.js";
|
|
4
4
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
5
5
|
import { runHook } from "./hooks.js";
|
|
6
6
|
import { readFileSync, realpathSync } from "node:fs";
|
|
@@ -249,7 +249,7 @@ function run(argv, cwd) {
|
|
|
249
249
|
return fail(`${error.message}\n\n${USAGE}`);
|
|
250
250
|
}
|
|
251
251
|
const { values: opts, positionals } = parsed;
|
|
252
|
-
if (opts.version) return done(
|
|
252
|
+
if (opts.version) return done(CLI_VERSION);
|
|
253
253
|
if (opts.help || positionals.length === 0) {
|
|
254
254
|
console.log(USAGE);
|
|
255
255
|
return opts.help ? 0 : 1;
|
|
@@ -1324,6 +1324,15 @@ function setMainlineStatus(root, id, status, expectedVersion) {
|
|
|
1324
1324
|
stampClosed(m, closes("mainline", next), today());
|
|
1325
1325
|
}, expectedVersion);
|
|
1326
1326
|
}
|
|
1327
|
+
/** Rename a mainline. References use its ID, so they keep resolving and show the new title. */
|
|
1328
|
+
function setMainlineTitle(root, id, title, expectedVersion) {
|
|
1329
|
+
requireRef(id, "mainline");
|
|
1330
|
+
const next = title.replace(/\s+/g, " ").trim();
|
|
1331
|
+
if (!next) throw new WriteError(`${id} needs a title, e.g. "mainflow 改發布到 npm 公開套件"`);
|
|
1332
|
+
return mutateFrontMatter(root, mainlineFile(root, id), mainlineSchema, (m) => {
|
|
1333
|
+
m.title = next;
|
|
1334
|
+
}, expectedVersion);
|
|
1335
|
+
}
|
|
1327
1336
|
function setArchived(root, id, archived, expectedVersion) {
|
|
1328
1337
|
requireRef(id, "mainline");
|
|
1329
1338
|
return mutateFrontMatter(root, mainlineFile(root, id), mainlineSchema, (m) => {
|
|
@@ -1644,4 +1653,4 @@ function migrateHub(root, options = {}) {
|
|
|
1644
1653
|
});
|
|
1645
1654
|
}
|
|
1646
1655
|
//#endregion
|
|
1647
|
-
export {
|
|
1656
|
+
export { appendOps as A, loadHub as B, setSessionStatus as C, updateAction as D, transcriptPath as E, reopenIssue as F, ConflictError as G, refIndex as H, setFindingArchived as I, currentBranch as J, WriteError as K, setFindingStatus as L, createFinding as M, createIssue as N, sessionBriefing as O, planIssue as P, setIssueArchived as R, setSessionInfo as S, startSession as T, MAINLINE_FILE as U, linksInto as V, TASK_FILE as W, worktreesOf as X, findHubRoot as Y, addAction as _, createDecision as a, readSession as b, dropChecklistItem as c, setChecklistItem as d, setCriterion as f, updateTask as g, supersedeDecision as h, addRepo as i, closeIssue as j, turnReminder as k, initHub as l, setMainlineTitle as m, acceptDecision as n, createMainline as o, setMainlineStatus as p, DATA_DIR as q, addHub as r, createTask as s, migrateHub as t, setArchived as u, ensureSession as v, settleSession as w, setSessionArchived as x, loadSessions as y, HubError as z };
|
package/dist/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { B as loadHub, C as setSessionStatus, J as currentBranch, O as sessionBriefing, S as setSessionInfo, X as worktreesOf, Y as findHubRoot, b as readSession, k as turnReminder, q as DATA_DIR, v as ensureSession, w as settleSession, z as HubError } from "./dist-_ArTiNbL.js";
|
|
2
2
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
4
|
//#region src/hooks.ts
|
|
@@ -46,6 +46,11 @@ function recordSession(root, payload, id, refresh) {
|
|
|
46
46
|
effort: payload.effort?.level
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
+
/** Settle the main session by the rules settleSession states; prints nothing. */
|
|
50
|
+
const settleOnEnd = (root, { payload }) => {
|
|
51
|
+
if (payload.session_id) settleSession(root, claudeCodeSessionId(payload.session_id));
|
|
52
|
+
return null;
|
|
53
|
+
};
|
|
49
54
|
const CLAUDE_CODE_EVENTS = {
|
|
50
55
|
"session-start": (root, { payload, envFile, cli }) => {
|
|
51
56
|
if (!payload.session_id) return null;
|
|
@@ -61,10 +66,8 @@ const CLAUDE_CODE_EVENTS = {
|
|
|
61
66
|
if (!payload.session_id) return null;
|
|
62
67
|
return turnReminder(root, recordSession(root, payload, claudeCodeSessionId(payload.session_id), false), { cli });
|
|
63
68
|
},
|
|
64
|
-
stop:
|
|
65
|
-
|
|
66
|
-
return null;
|
|
67
|
-
},
|
|
69
|
+
stop: settleOnEnd,
|
|
70
|
+
"session-end": settleOnEnd,
|
|
68
71
|
"subagent-start": (root, { payload }) => {
|
|
69
72
|
if (!payload.agent_id) return null;
|
|
70
73
|
const parent = payload.session_id ? claudeCodeSessionId(payload.session_id) : null;
|
package/dist/host.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { K as WriteError, q as DATA_DIR } from "./dist-_ArTiNbL.js";
|
|
2
2
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
4
|
import { spawnSync } from "node:child_process";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { B as
|
|
1
|
+
import { B as loadHub, E as transcriptPath, G as ConflictError, H as refIndex, I as setFindingArchived, K as WriteError, R as setIssueArchived, V as linksInto, b as readSession, d as setChecklistItem, f as setCriterion, g as updateTask, m as setMainlineTitle, p as setMainlineStatus, q as DATA_DIR, u as setArchived, x as setSessionArchived, y as loadSessions } from "./dist-_ArTiNbL.js";
|
|
2
2
|
import { DEFAULT_UI_PORT, syncHost } from "./host.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, watch, writeFileSync } from "node:fs";
|
|
@@ -214,6 +214,10 @@ function listProgress(items) {
|
|
|
214
214
|
};
|
|
215
215
|
}
|
|
216
216
|
//#endregion
|
|
217
|
+
//#region src/version.ts
|
|
218
|
+
/** This CLI's own version, from the package manifest one level above both src/ and dist/. */
|
|
219
|
+
const CLI_VERSION = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
220
|
+
//#endregion
|
|
217
221
|
//#region src/server.ts
|
|
218
222
|
/** Every reference with a hub prefix in the documents of `hub`: links, origins and replaced items. */
|
|
219
223
|
function prefixedRefs(hub) {
|
|
@@ -323,6 +327,7 @@ function createApp(root) {
|
|
|
323
327
|
const hub = loadHub(root);
|
|
324
328
|
const body = {
|
|
325
329
|
hub: hub.config.hub,
|
|
330
|
+
version: CLI_VERSION,
|
|
326
331
|
repos: hub.config.repos,
|
|
327
332
|
mainlines: hub.mainlines.map(summarize),
|
|
328
333
|
openIssues: hub.issues.filter((i) => i.status !== "closed").length,
|
|
@@ -415,6 +420,10 @@ function createApp(root) {
|
|
|
415
420
|
const { archived, version } = await c.req.json();
|
|
416
421
|
return c.json(setArchived(root, c.req.param("m"), archived, version));
|
|
417
422
|
});
|
|
423
|
+
app.post("/api/mainlines/:m/title", async (c) => {
|
|
424
|
+
const { title, version } = await c.req.json();
|
|
425
|
+
return c.json(setMainlineTitle(root, c.req.param("m"), title, version));
|
|
426
|
+
});
|
|
418
427
|
app.post("/api/mainlines/:m/status", async (c) => {
|
|
419
428
|
const { status, version } = await c.req.json();
|
|
420
429
|
return c.json(setMainlineStatus(root, c.req.param("m"), status, version));
|
|
@@ -476,19 +485,24 @@ function serveUi(root, port) {
|
|
|
476
485
|
//#region src/launch.ts
|
|
477
486
|
/** How many ports above DEFAULT_UI_PORT are tried before giving up. */
|
|
478
487
|
const PORT_SPAN = 100;
|
|
479
|
-
/** Where the last
|
|
480
|
-
const
|
|
481
|
-
function
|
|
488
|
+
/** Where the last server started is remembered: machine-specific, so under the unversioned local/ folder. */
|
|
489
|
+
const uiRecordFile = (root) => join(root, DATA_DIR, "local", "ui.json");
|
|
490
|
+
function readUiRecord(root) {
|
|
482
491
|
try {
|
|
483
|
-
const
|
|
484
|
-
|
|
492
|
+
const record = JSON.parse(readFileSync(uiRecordFile(root), "utf8"));
|
|
493
|
+
if (!Number.isInteger(record.port)) return void 0;
|
|
494
|
+
return {
|
|
495
|
+
port: record.port,
|
|
496
|
+
...Number.isInteger(record.pid) && { pid: record.pid },
|
|
497
|
+
...typeof record.version === "string" && { version: record.version }
|
|
498
|
+
};
|
|
485
499
|
} catch {
|
|
486
500
|
return;
|
|
487
501
|
}
|
|
488
502
|
}
|
|
489
|
-
function
|
|
503
|
+
function writeUiRecord(root, record) {
|
|
490
504
|
mkdirSync(join(root, DATA_DIR, "local"), { recursive: true });
|
|
491
|
-
writeFileSync(
|
|
505
|
+
writeFileSync(uiRecordFile(root), `${JSON.stringify(record)}\n`);
|
|
492
506
|
}
|
|
493
507
|
/**
|
|
494
508
|
* Ports to try, in order. An explicit --port is the only candidate: the user asked for that port. Otherwise
|
|
@@ -503,13 +517,18 @@ function portCandidates(options) {
|
|
|
503
517
|
...range
|
|
504
518
|
].filter((p) => p !== void 0))];
|
|
505
519
|
}
|
|
520
|
+
const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
|
|
506
521
|
const networkProbe = {
|
|
507
|
-
async
|
|
522
|
+
async serverAt(port) {
|
|
508
523
|
try {
|
|
509
524
|
const res = await fetch(`http://localhost:${port}/api/hub`, { signal: AbortSignal.timeout(500) });
|
|
510
525
|
if (!res.ok) return null;
|
|
511
526
|
const body = await res.json();
|
|
512
|
-
|
|
527
|
+
if (typeof body.hub !== "string") return null;
|
|
528
|
+
return {
|
|
529
|
+
hub: body.hub,
|
|
530
|
+
...typeof body.version === "string" && { version: body.version }
|
|
531
|
+
};
|
|
513
532
|
} catch {
|
|
514
533
|
return null;
|
|
515
534
|
}
|
|
@@ -517,7 +536,29 @@ const networkProbe = {
|
|
|
517
536
|
isFree: (port) => new Promise((done) => {
|
|
518
537
|
const probe = createServer().once("error", () => done(false));
|
|
519
538
|
probe.listen(port, () => probe.close(() => done(true)));
|
|
520
|
-
})
|
|
539
|
+
}),
|
|
540
|
+
listenerOf(port) {
|
|
541
|
+
const out = spawnSync("lsof", [
|
|
542
|
+
"-nP",
|
|
543
|
+
`-iTCP:${port}`,
|
|
544
|
+
"-sTCP:LISTEN",
|
|
545
|
+
"-t"
|
|
546
|
+
], { encoding: "utf8" });
|
|
547
|
+
const pid = Number(out.stdout?.split("\n")[0]);
|
|
548
|
+
return out.status === 0 && Number.isInteger(pid) && pid > 0 ? pid : null;
|
|
549
|
+
},
|
|
550
|
+
async stop(pid, port) {
|
|
551
|
+
try {
|
|
552
|
+
process.kill(pid, "SIGTERM");
|
|
553
|
+
} catch {
|
|
554
|
+
return false;
|
|
555
|
+
}
|
|
556
|
+
for (let i = 0; i < 25; i++) {
|
|
557
|
+
await sleep(200);
|
|
558
|
+
if (await networkProbe.isFree(port)) return true;
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
521
562
|
};
|
|
522
563
|
/**
|
|
523
564
|
* A candidate already serving this hub wins wherever it sits in the order, so a lost last-port record or a
|
|
@@ -525,15 +566,15 @@ const networkProbe = {
|
|
|
525
566
|
* probe timeout. Otherwise the first free candidate; ports held by another hub or program are skipped.
|
|
526
567
|
*/
|
|
527
568
|
async function choosePort(hub, candidates, probe = networkProbe) {
|
|
528
|
-
const
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
531
|
-
port:
|
|
532
|
-
running:
|
|
569
|
+
const servers = await Promise.all(candidates.map((port) => probe.serverAt(port)));
|
|
570
|
+
const at = candidates.findIndex((_, i) => servers[i]?.hub === hub);
|
|
571
|
+
if (at >= 0) return {
|
|
572
|
+
port: candidates[at],
|
|
573
|
+
running: servers[at].version ?? ""
|
|
533
574
|
};
|
|
534
|
-
for (const [i, port] of candidates.entries()) if (
|
|
575
|
+
for (const [i, port] of candidates.entries()) if (servers[i] === null && await probe.isFree(port)) return {
|
|
535
576
|
port,
|
|
536
|
-
running:
|
|
577
|
+
running: void 0
|
|
537
578
|
};
|
|
538
579
|
const tried = candidates.length === 1 ? `port ${candidates[0]} is` : `ports ${candidates[0]} to ${candidates.at(-1)} are`;
|
|
539
580
|
throw new WriteError(`${tried} taken by other programs; free one, or pass --port <port>`);
|
|
@@ -560,20 +601,35 @@ async function launchUi(root, hub, configured, options) {
|
|
|
560
601
|
explicit = Number(options.port);
|
|
561
602
|
if (!Number.isInteger(explicit) || explicit < 1024 || explicit > 65535) throw new WriteError("--port must be an integer from 1024 to 65535, e.g. --port 4320");
|
|
562
603
|
}
|
|
604
|
+
const probe = options.probe ?? networkProbe;
|
|
605
|
+
const record = readUiRecord(root);
|
|
563
606
|
const choice = await choosePort(hub, portCandidates({
|
|
564
607
|
explicit,
|
|
565
608
|
configured,
|
|
566
|
-
last:
|
|
567
|
-
}),
|
|
609
|
+
last: record?.port
|
|
610
|
+
}), probe);
|
|
611
|
+
let reuse = choice.running !== void 0;
|
|
612
|
+
if (reuse && choice.running !== CLI_VERSION) {
|
|
613
|
+
const from = choice.running || "unknown";
|
|
614
|
+
const pid = record?.port === choice.port && record.pid !== void 0 ? record.pid : probe.listenerOf(choice.port);
|
|
615
|
+
if (pid !== null && await probe.stop(pid, choice.port)) {
|
|
616
|
+
log(`! restarted: mainflow ${from} → ${CLI_VERSION}`);
|
|
617
|
+
reuse = false;
|
|
618
|
+
} else log(`! localhost:${choice.port} still runs mainflow ${from}, not ${CLI_VERSION}; stop it with \`kill $(lsof -ti tcp:${choice.port})\` and run mainflow ui again`);
|
|
619
|
+
}
|
|
568
620
|
let server;
|
|
569
|
-
if (!
|
|
621
|
+
if (!reuse) {
|
|
570
622
|
server = await serveUi(root, choice.port);
|
|
571
|
-
|
|
623
|
+
writeUiRecord(root, {
|
|
624
|
+
port: choice.port,
|
|
625
|
+
pid: process.pid,
|
|
626
|
+
version: CLI_VERSION
|
|
627
|
+
});
|
|
572
628
|
}
|
|
573
629
|
const host = await syncHost(root, hub, choice.port, options.host);
|
|
574
630
|
if (host.url === null) log(`! host name not available: ${host.reason}`);
|
|
575
631
|
const url = host.url ?? `http://localhost:${choice.port}`;
|
|
576
|
-
log(`${
|
|
632
|
+
log(`${reuse ? "already running" : "mainflow ui"}: ${url} (localhost:${choice.port})`);
|
|
577
633
|
if (options.open) openUrl(url);
|
|
578
634
|
return {
|
|
579
635
|
url,
|
|
@@ -581,4 +637,4 @@ async function launchUi(root, hub, configured, options) {
|
|
|
581
637
|
};
|
|
582
638
|
}
|
|
583
639
|
//#endregion
|
|
584
|
-
export { portCandidates as a, networkProbe as i, choosePort as n,
|
|
640
|
+
export { portCandidates as a, listProgress as c, networkProbe as i, choosePort as n, readUiRecord as o, launchUi as r, CLI_VERSION as s, PORT_SPAN as t };
|
package/dist/launch.d.ts
CHANGED
|
@@ -3,7 +3,13 @@ import { ServerType } from "@hono/node-server";
|
|
|
3
3
|
//#region src/launch.d.ts
|
|
4
4
|
/** How many ports above DEFAULT_UI_PORT are tried before giving up. */
|
|
5
5
|
export declare const PORT_SPAN = 100;
|
|
6
|
-
|
|
6
|
+
/** The server this hub last started: its port, and the process and version when it recorded them. */
|
|
7
|
+
export interface UiRecord {
|
|
8
|
+
port: number;
|
|
9
|
+
pid?: number;
|
|
10
|
+
version?: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function readUiRecord(root: string): UiRecord | undefined;
|
|
7
13
|
/**
|
|
8
14
|
* Ports to try, in order. An explicit --port is the only candidate: the user asked for that port. Otherwise
|
|
9
15
|
* the hub's configured port, then the last one used, then the default range.
|
|
@@ -13,15 +19,25 @@ export declare function portCandidates(options: {
|
|
|
13
19
|
configured?: number;
|
|
14
20
|
last?: number;
|
|
15
21
|
}): number[];
|
|
22
|
+
/** What a mainflow interface on a port reports; servers older than 0.8.1 report no version. */
|
|
23
|
+
export interface ServerInfo {
|
|
24
|
+
hub: string;
|
|
25
|
+
version?: string;
|
|
26
|
+
}
|
|
16
27
|
export interface PortProbe {
|
|
17
|
-
/** The
|
|
18
|
-
|
|
28
|
+
/** The mainflow interface on this port, or null when nothing mainflow answers. */
|
|
29
|
+
serverAt: (port: number) => Promise<ServerInfo | null>;
|
|
19
30
|
isFree: (port: number) => Promise<boolean>;
|
|
31
|
+
/** The process listening on this port, or null when it cannot be found. */
|
|
32
|
+
listenerOf: (port: number) => number | null;
|
|
33
|
+
/** Stop a process and wait until the port is free; false when it did not free up. */
|
|
34
|
+
stop: (pid: number, port: number) => Promise<boolean>;
|
|
20
35
|
}
|
|
21
36
|
export declare const networkProbe: PortProbe;
|
|
37
|
+
/** A port to serve on; `running` is the version already serving this hub there, '' when it reports none. */
|
|
22
38
|
export type PortChoice = {
|
|
23
39
|
port: number;
|
|
24
|
-
running
|
|
40
|
+
running?: string;
|
|
25
41
|
};
|
|
26
42
|
/**
|
|
27
43
|
* A candidate already serving this hub wins wherever it sits in the order, so a lost last-port record or a
|
package/dist/launch.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as portCandidates, i as networkProbe, n as choosePort, o as
|
|
2
|
-
export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates,
|
|
1
|
+
import { a as portCandidates, i as networkProbe, n as choosePort, o as readUiRecord, r as launchUi, t as PORT_SPAN } from "./launch-D2WJRvPD.js";
|
|
2
|
+
export { PORT_SPAN, choosePort, launchUi, networkProbe, portCandidates, readUiRecord };
|