mainflow 0.44.0-alpha.1 → 0.44.0-alpha.3
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 +35 -6
- package/dist/facts.d.ts +2 -2
- package/dist/facts.js +2 -2
- package/dist/grant-check.js +2 -2
- package/dist/hooks.js +17 -8
- package/dist/{host-C74j2Qox.js → host-CsZY5trM.js} +107 -5
- package/dist/host.js +1 -1
- package/dist/launch.js +2 -2
- package/dist/{operation-BmCeGKHw.js → operation-MKNwQkAY.js} +167 -26
- package/dist/{ui-8NTeAfC7.js → ui-Bv29R0WG.js} +2 -3
- package/package.json +1 -1
- package/dist/progress-1BfPg2zN.js +0 -10
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { P as UNTIL_FORMS, X as parseUntil, Y as parseRef, Z as refExample, ct as stampMs, it as untilProblem, n as CLOSED_STATUS } from "./dist-CXrUfRQp.js";
|
|
3
|
-
import { $t as
|
|
4
|
-
import { t as
|
|
5
|
-
import { A as hubName, C as readRemote, D as Unreachable, E as ServerError, F as syncPaths, M as SETTINGS_FILE, O as ENV, T as ServerApi, _ as DaemonUnavailable, a as versionGate, b as readDaemon, c as oversizedFiles, d as planAnnounce, f as planFetch, g as DaemonRunning, h as startDaemon, i as requireWritableVersion, j as syncSettings, k as earlierToken, l as fetchDenial, m as toolCallOf, n as hubWriter, o as queuedCount, p as planFlush, r as writtenLines, s as remoteMode, t as CONFIRM_MS, u as hookAnswer, v as callDaemon, w as HubRewritingError, x as silentHint, y as probeDaemon } from "./operation-BmCeGKHw.js";
|
|
2
|
+
import { D as SESSION_ID, P as UNTIL_FORMS, X as parseUntil, Y as parseRef, Z as refExample, ct as stampMs, it as untilProblem, n as CLOSED_STATUS } from "./dist-CXrUfRQp.js";
|
|
3
|
+
import { $ as applyOperation, $t as dataFile, A as unhandledNotices, Bt as hubHoldingFile, C as duplicateClaimLines, Ft as MAINLINE_FILE, Gt as registerHub, Ht as listHubs, Jt as resolveHub, Kt as registeredName, Lt as TASK_FILE, M as readSession, Rt as requireSlug, S as untilState, St as MAX_FILE_BYTES, Ut as notRegisteredMessage, Wt as readRegistry, Xt as userDataDir, Yt as unregisterHub, Zt as nodeStorage, _ as receiverFromEnv, an as DATA_DIR, d as groupPlan, et as checkHubName, f as listProgress, ft as remoteMembers, g as retroReportFromReceiver, h as retroReport, ht as nowMinute, i as hostName, it as loadHub, j as loadSessions, m as migrateHub, nn as realPath, on as WriteError, ot as sectionText, p as migrateHubData, pt as unavailableHubs, qt as registryFile, tt as HubError, u as sessionView, ut as memberOfGroup, v as RETRY_RULES, w as sessionBriefing, x as initHub, xt as MAX_CHANGE_PAGE, yt as BLOB_HASH, zt as hubAtRoot } from "./host-CsZY5trM.js";
|
|
4
|
+
import { A as hubName, C as readRemote, D as Unreachable, E as ServerError, F as syncPaths, M as SETTINGS_FILE, O as ENV, T as ServerApi, _ as DaemonUnavailable, a as versionGate, b as readDaemon, c as oversizedFiles, d as planAnnounce, f as planFetch, g as DaemonRunning, h as startDaemon, i as requireWritableVersion, j as syncSettings, k as earlierToken, l as fetchDenial, m as toolCallOf, n as hubWriter, o as queuedCount, p as planFlush, r as writtenLines, s as remoteMode, t as CONFIRM_MS, u as hookAnswer, v as callDaemon, w as HubRewritingError, x as silentHint, y as probeDaemon } from "./operation-MKNwQkAY.js";
|
|
6
5
|
import { COMMAND_SPECS, OPTIONS } from "./facts.js";
|
|
7
6
|
import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
|
|
8
|
-
import { hookFallback, parsePayload, runHook } from "./hooks.js";
|
|
7
|
+
import { claudeCodeSessionId, hookFallback, openLink, parsePayload, runHook } from "./hooks.js";
|
|
9
8
|
import { runGrantCheck } from "./grant-check.js";
|
|
10
9
|
import { exitCodeFor, launchUi, networkProbe, readUiRecord, startUiInBackground, watchHubGone } from "./launch.js";
|
|
11
10
|
import { randomBytes } from "node:crypto";
|
|
@@ -13,6 +12,35 @@ import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync }
|
|
|
13
12
|
import { basename, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
13
|
import { fileURLToPath } from "node:url";
|
|
15
14
|
import { parseArgs } from "node:util";
|
|
15
|
+
//#region src/session-json.ts
|
|
16
|
+
/** The mainflow session a host's session id stands for: the id itself when such a session exists, else Claude Code's. */
|
|
17
|
+
function sessionIdOf(dirs, raw) {
|
|
18
|
+
return SESSION_ID.test(raw) && readSession(dirs, raw) ? raw : claudeCodeSessionId(raw);
|
|
19
|
+
}
|
|
20
|
+
/** The sync client's state; null when the hub is not synced, that is no client runs and writes go to the files. */
|
|
21
|
+
async function syncState(dirs) {
|
|
22
|
+
const probe = await probeDaemon(dirs);
|
|
23
|
+
if (!probe && !remoteMode(dirs)) return null;
|
|
24
|
+
const running = probe?.state === "running";
|
|
25
|
+
return {
|
|
26
|
+
running,
|
|
27
|
+
caughtUp: running && probe.status?.caughtUp === true,
|
|
28
|
+
queued: queuedCount(dirs),
|
|
29
|
+
notices: unhandledNotices(dirs).length
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** What `session show --json` prints for a mainflow or host session id; a missing session gives `session: null`. */
|
|
33
|
+
async function sessionJson(dirs, raw) {
|
|
34
|
+
const hub = loadHub(dirs, { remotes: false });
|
|
35
|
+
return sessionView({
|
|
36
|
+
hub,
|
|
37
|
+
session: readSession(dirs, sessionIdOf(dirs, raw)),
|
|
38
|
+
url: `https://${hostName(hub.config.hub)}`,
|
|
39
|
+
link: openLink(hub.config.hub),
|
|
40
|
+
sync: await syncState(dirs)
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
16
44
|
//#region src/migration.ts
|
|
17
45
|
/**
|
|
18
46
|
* What `mainflow migrate` prints of a migration's report, the same whether the hub's files are here or on the sync
|
|
@@ -1604,8 +1632,9 @@ const RUNS = {
|
|
|
1604
1632
|
});
|
|
1605
1633
|
return 0;
|
|
1606
1634
|
},
|
|
1607
|
-
"session show": ({ dirs, args, opts }) => {
|
|
1635
|
+
"session show": async ({ dirs, args, opts }) => {
|
|
1608
1636
|
const id = sessionOf(opts, args[0]);
|
|
1637
|
+
if (opts.json) return done(JSON.stringify(await sessionJson(dirs, id)));
|
|
1609
1638
|
const session = readSession(dirs, id);
|
|
1610
1639
|
if (!session) throw new WriteError(`session ${id} not found; start one with "mainflow session start"`);
|
|
1611
1640
|
return done(sessionBriefing(dirs, session, { cli: CLI }));
|
package/dist/facts.d.ts
CHANGED
|
@@ -97,8 +97,8 @@ export declare const COMMAND_SPECS: {
|
|
|
97
97
|
readonly needsHub: true;
|
|
98
98
|
};
|
|
99
99
|
readonly 'session show': {
|
|
100
|
-
readonly usage: 'session show [session]';
|
|
101
|
-
readonly summary: 'Print the briefing a hook would give this session';
|
|
100
|
+
readonly usage: 'session show [session] [--json]';
|
|
101
|
+
readonly summary: 'Print the briefing a hook would give this session. --json prints one object for a host to read: {"hub","session","focus","tasks","mainlines","sync"}; session is null, and the exit code 0, when the session does not exist. The session is a mainflow session id or the host\'s own (a Claude Code UUID)';
|
|
102
102
|
readonly args: 0;
|
|
103
103
|
readonly optionalArgs: 1;
|
|
104
104
|
readonly needsHub: true;
|
package/dist/facts.js
CHANGED
|
@@ -87,8 +87,8 @@ const COMMAND_SPECS = {
|
|
|
87
87
|
needsHub: true
|
|
88
88
|
},
|
|
89
89
|
"session show": {
|
|
90
|
-
usage: "session show [session]",
|
|
91
|
-
summary: "Print the briefing a hook would give this session",
|
|
90
|
+
usage: "session show [session] [--json]",
|
|
91
|
+
summary: "Print the briefing a hook would give this session. --json prints one object for a host to read: {\"hub\",\"session\",\"focus\",\"tasks\",\"mainlines\",\"sync\"}; session is null, and the exit code 0, when the session does not exist. The session is a mainflow session id or the host's own (a Claude Code UUID)",
|
|
92
92
|
args: 0,
|
|
93
93
|
optionalArgs: 1,
|
|
94
94
|
needsHub: true
|
package/dist/grant-check.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { D as SESSION_ID, I as configSchema, ft as describeRequest, gt as hasControl, tt as repoPaths } from "./dist-CXrUfRQp.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as hubWriter, r as writtenLines } from "./operation-
|
|
2
|
+
import { F as matchGrants, Jt as resolveHub, M as readSession, Mt as hubFormatVersion, P as grantScope, Qt as currentBranch, Zt as nodeStorage, _t as readYaml, en as gitCheckout, jt as formatMismatch, nn as realPath } from "./host-CsZY5trM.js";
|
|
3
|
+
import { n as hubWriter, r as writtenLines } from "./operation-MKNwQkAY.js";
|
|
4
4
|
import { OWNER_REPO, githubOrigin, quote, readManifest, resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
|
|
5
5
|
import { claudeCodeSessionId, parsePayload } from "./hooks.js";
|
|
6
6
|
import { readFileSync } from "node:fs";
|
package/dist/hooks.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { I as configSchema, Y as parseRef, ft as describeRequest, rt as taskSchema, tt as repoPaths } from "./dist-CXrUfRQp.js";
|
|
2
|
-
import { $ as
|
|
3
|
-
import { n as hubWriter, r as writtenLines } from "./operation-
|
|
2
|
+
import { $t as dataFile, Bt as hubHoldingFile, F as matchGrants, I as decorateReply, It as SESSIONS_DIR, Jt as resolveHub, L as endsWithMarker, M as readSession, Mt as hubFormatVersion, P as grantScope, Qt as currentBranch, R as headerLine, T as turnReminder, Ut as notRegisteredMessage, Vt as inRepositoryHubAt, Zt as nodeStorage, _t as readYaml, an as DATA_DIR, at as loadMainlines, gt as readFrontMatter, i as hostName, it as loadHub, j as loadSessions, jt as formatMismatch, mt as taskFile, nn as realPath, nt as isOpenMainline, rn as worktreesOf, tn as hubPath, tt as HubError, w as sessionBriefing, z as mayDecorate } from "./host-CsZY5trM.js";
|
|
3
|
+
import { n as hubWriter, r as writtenLines } from "./operation-MKNwQkAY.js";
|
|
4
4
|
import { SCENARIO_LINES, SCENARIO_SKIP_KINDS } from "./facts.js";
|
|
5
5
|
import { resolveFixed, scanCommand, scopedCwd } from "./grant-commands.js";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
@@ -102,13 +102,17 @@ const settleOnEnd = async (_root, { payload, write }) => {
|
|
|
102
102
|
};
|
|
103
103
|
/** How a hub's interface is linked from a reply: through its /open route, which shows the address in the app. */
|
|
104
104
|
const openLink = (hub) => (path) => `https://${hostName(hub)}/open${path}`;
|
|
105
|
+
/** Whether a host's display mod is loaded, which it says by setting MAINFLOW_DISPLAY_MOD; it then shows the current action itself. */
|
|
106
|
+
const displayModOn = (env = process.env) => !!env.MAINFLOW_DISPLAY_MOD;
|
|
105
107
|
/**
|
|
106
108
|
* MessageDisplay: link the references in the lines about to be shown and expand the trailing header line into
|
|
107
109
|
* the mainline › task › action its ids name, titles included, whatever state that action is in now. Display
|
|
108
110
|
* only, so the model keeps reading what it wrote. Text with nothing to change
|
|
109
|
-
* returns before the hub is loaded, because Claude Code holds every batch until the hook answers.
|
|
111
|
+
* returns before the hub is loaded, because Claude Code holds every batch until the hook answers. With a display
|
|
112
|
+
* mod loaded it returns null at once.
|
|
110
113
|
*/
|
|
111
|
-
const displayReply = (dirs, { payload }) => {
|
|
114
|
+
const displayReply = (dirs, { payload, env }) => {
|
|
115
|
+
if (displayModOn(env)) return null;
|
|
112
116
|
const text = payload.delta;
|
|
113
117
|
if (!text || !mayDecorate(text)) return null;
|
|
114
118
|
const hub = loadHub(dirs, { remotes: false });
|
|
@@ -183,18 +187,22 @@ function turnTouchedMainflow(transcript, cli, env) {
|
|
|
183
187
|
* an action first when the queue is empty, the reply talks about an open mainline and the turn ran a mainflow
|
|
184
188
|
* command or wrote a file of the hub, since the header only names the session's own action in progress. A reply
|
|
185
189
|
* that only mentions a mainline, in a draft or a question, asks for nothing. Null when there is nothing to ask for.
|
|
190
|
+
* With a display mod loaded the mod shows the current action, so no header is asked for, but an empty queue
|
|
191
|
+
* still is: the action is what grants and the hub's tracking hang on.
|
|
186
192
|
*/
|
|
187
193
|
function missingHeaderRequest(dirs, { payload, cli, env }) {
|
|
188
194
|
if (!payload.session_id || payload.stop_hook_active) return null;
|
|
195
|
+
const displayMod = displayModOn(env);
|
|
189
196
|
const message = payload.last_assistant_message ?? "";
|
|
190
|
-
if (endsWithMarker(message)) return null;
|
|
197
|
+
if (!displayMod && endsWithMarker(message)) return null;
|
|
191
198
|
const session = readSession(dirs, claudeCodeSessionId(payload.session_id));
|
|
192
199
|
const header = headerLine(session);
|
|
193
|
-
if (header) return `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`;
|
|
200
|
+
if (header) return displayMod ? null : `[mainflow] 這一輪最後一則回覆的最後一行不是標頭。請只回一則內容為「${header}」的訊息,不加其他文字。`;
|
|
194
201
|
if (session && session.actions.length > 0) return null;
|
|
195
202
|
const mainlines = openMainlinesIn(dirs, message);
|
|
196
203
|
if (mainlines.length === 0 || !turnTouchedMainflow(payload.transcript_path, cli, env)) return null;
|
|
197
|
-
|
|
204
|
+
const list = `先用 ${cli} action add "<做法>" --serves <m/t#c>(還沒拆任務時用解決條件 <m#r>)列出行動並用 ${cli} action set <a編號> --status doing 標為進行中,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動`;
|
|
205
|
+
return displayMod ? `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")},也執行了 mainflow 指令或寫入 hub 的資料目錄,但這場會話沒有行動。這一輪若在推進它,${list}。只是提到、沒有推進時,不必處理。` : `[mainflow] 這一輪的回覆提到 ${mainlines.join("、")},也執行了 mainflow 指令或寫入 hub 的資料目錄,但這場會話沒有行動,所以回覆結尾沒有標頭。這一輪若在推進它,${list};再只回一則內容為「▸ <m/t> · <a編號>」的訊息,不加其他文字。只是提到、沒有推進時,不必處理。`;
|
|
198
206
|
}
|
|
199
207
|
/**
|
|
200
208
|
* Stop: ask for what the turn's header needs, then settle the session. Only the model knows which message closes a
|
|
@@ -231,12 +239,13 @@ const CLAUDE_CODE_EVENTS = {
|
|
|
231
239
|
envNote: envFile ? "已設定環境變數 MAINFLOW_SESSION,指令不必再帶 --session" : `執行指令時帶 --session ${id}`
|
|
232
240
|
});
|
|
233
241
|
},
|
|
234
|
-
"user-prompt-submit": async (dirs, { payload, cli, write }) => {
|
|
242
|
+
"user-prompt-submit": async (dirs, { payload, cli, write, env }) => {
|
|
235
243
|
if (!payload.session_id) return null;
|
|
236
244
|
const session = await recordSession(write, dirs, payload, claudeCodeSessionId(payload.session_id), false);
|
|
237
245
|
const saved = [];
|
|
238
246
|
const text = turnReminder(dirs, session, {
|
|
239
247
|
cli,
|
|
248
|
+
displayMod: displayModOn(env),
|
|
240
249
|
saveDigest: (digest) => saved.push(write("session.reminder", {
|
|
241
250
|
id: session.id,
|
|
242
251
|
digest
|
|
@@ -4435,6 +4435,17 @@ function pathOf(ref, target) {
|
|
|
4435
4435
|
}
|
|
4436
4436
|
}
|
|
4437
4437
|
const sessionPath = (id) => `/sessions/${id}`;
|
|
4438
|
+
/** Where a mainline's file shows on the hub's interface, under the mainline's own address. */
|
|
4439
|
+
const mainlineFilePath = (mainline, file) => `/mainlines/${mainline}/files/${file}`;
|
|
4440
|
+
/** The interface address of every reference the hub has a page or panel for, keyed by the reference. */
|
|
4441
|
+
function refPaths(hub) {
|
|
4442
|
+
const paths = {};
|
|
4443
|
+
for (const [ref, target] of refIndex(hub)) {
|
|
4444
|
+
const path = pathOf(ref, target);
|
|
4445
|
+
if (path) paths[ref] = path;
|
|
4446
|
+
}
|
|
4447
|
+
return paths;
|
|
4448
|
+
}
|
|
4438
4449
|
/** Fenced blocks, inline code, markdown links and bare URLs: text inside them is left as written. */
|
|
4439
4450
|
const PROTECTED = /(```[\s\S]*?(?:```|$)|`[^`\n]*`|\[[^\]\n]*\]\([^)\n]*\)|https?:\/\/\S+)/;
|
|
4440
4451
|
/** A file under a mainline directory, or a reference to a mainline, task, decision, issue, finding or group. */
|
|
@@ -4443,7 +4454,7 @@ const MENTION = /\.mainflow\/(?:[a-z0-9-]+\/)?mainlines\/(m\d{3})-[\w-]+\/([^\s`
|
|
|
4443
4454
|
function linkMentions(text, context) {
|
|
4444
4455
|
const index = refIndex(context.hub);
|
|
4445
4456
|
return text.split(PROTECTED).map((part, i) => i % 2 === 1 ? part : part.replace(MENTION, (all, m, file, ref) => {
|
|
4446
|
-
if (m && file) return index.has(m) ? `[${m}/${file}](${context.link(
|
|
4457
|
+
if (m && file) return index.has(m) ? `[${m}/${file}](${context.link(mainlineFilePath(m, file))})` : all;
|
|
4447
4458
|
const target = ref ? index.get(ref) : void 0;
|
|
4448
4459
|
const path = ref && target ? pathOf(ref, target) : null;
|
|
4449
4460
|
return path ? `[${ref}](${context.link(path)})` : all;
|
|
@@ -5145,9 +5156,10 @@ function sessionBriefing(dirs, session, options) {
|
|
|
5145
5156
|
* The reminder for every user turn; null when there is nothing to say, or when it would be the same text as the
|
|
5146
5157
|
* reminder the model was last given. Records a digest of the whole text in the session file, only when it changes;
|
|
5147
5158
|
* `saveDigest` records it instead of writing the file here, as a host hook in remote mode sends it to the sync server.
|
|
5159
|
+
* `displayMod` leaves out the sentence that asks for the header line, for a host whose mod shows the current action.
|
|
5148
5160
|
*/
|
|
5149
5161
|
function turnReminder(dirs, session, options) {
|
|
5150
|
-
const text = reminderText(dirs, session, options.cli);
|
|
5162
|
+
const text = reminderText(dirs, session, options.cli, options.displayMod === true);
|
|
5151
5163
|
if (text === null) return null;
|
|
5152
5164
|
const digest = contentHash(text);
|
|
5153
5165
|
if (session.reminderDigest === digest) return null;
|
|
@@ -5157,13 +5169,13 @@ function turnReminder(dirs, session, options) {
|
|
|
5157
5169
|
} catch {}
|
|
5158
5170
|
return text;
|
|
5159
5171
|
}
|
|
5160
|
-
function reminderText(dirs, session, cli) {
|
|
5172
|
+
function reminderText(dirs, session, cli, displayMod) {
|
|
5161
5173
|
if (session.actions.length === 0) return openMainlines(dirs).length > 0 ? `[mainflow] ${session.id} 還沒有行動。這一輪要推進主脈絡時,先用 ${cli} action add 列出行動並標為 doing,或用 ${cli} session handoff <會話> 接手上一場沒做完的行動,再開始做。回覆結尾的標頭只看這場會話進行中的行動。` : null;
|
|
5162
5174
|
const doing = current(session);
|
|
5163
5175
|
const open = session.actions.filter(isOpen);
|
|
5164
5176
|
const blocked = session.actions.filter((a) => a.status === "blocked");
|
|
5165
5177
|
const head = doing ? `[mainflow] 目前行動 ${doing.id} ${doing.do},服務 ${doing.serves.join("、") || "(未指定)"}。這一輪的工作若不屬於它,先調整佇列再做。` : open.length > 0 ? `[mainflow] ${session.id} 目前沒有進行中的行動。` : blocked.length > 0 ? `[mainflow] ${session.id} 的行動在等外部條件:${blocked.map(waitingLine).join(";")}。有條件(until)的可以在背景執行 ${cli} wait <a編號> 等它成立。條件出現後用 ${cli} action set <a編號> --status doing 繼續;要先做別的事,用 ${cli} action add 列出行動。` : `[mainflow] ${session.id} 的行動都已結束。繼續工作前先用 ${cli} action add 列出下一個行動。`;
|
|
5166
|
-
const header = headerLine(session);
|
|
5178
|
+
const header = displayMod ? null : headerLine(session);
|
|
5167
5179
|
const marker = header ? [`- 這一輪最後一則回覆的最後一行寫「${header}」,回覆範例等其他內容都放在它前面。有顯示 hook 時,它會換成標題與連結。`] : [];
|
|
5168
5180
|
const grants = grantLines(dirs, session);
|
|
5169
5181
|
const granted = grants.length > 0 ? ["- 目前行動服務的主脈絡與所屬的組預先授權了這些動作:", ...grants.map((g) => ` ${g}`)] : [];
|
|
@@ -9330,6 +9342,15 @@ function copyData(source, target) {
|
|
|
9330
9342
|
return target;
|
|
9331
9343
|
}
|
|
9332
9344
|
//#endregion
|
|
9345
|
+
//#region ../core/dist/progress.js
|
|
9346
|
+
function listProgress(items) {
|
|
9347
|
+
const counted = items.filter((item) => !item.dropped);
|
|
9348
|
+
return {
|
|
9349
|
+
done: counted.filter((item) => item.done).length,
|
|
9350
|
+
total: counted.length
|
|
9351
|
+
};
|
|
9352
|
+
}
|
|
9353
|
+
//#endregion
|
|
9333
9354
|
//#region ../core/dist/group-plan.js
|
|
9334
9355
|
/** Sessions that can still hold work: not archived, and active or paused. */
|
|
9335
9356
|
const live = (s) => !s.archivedAt && (s.status === "active" || s.status === "paused");
|
|
@@ -9401,6 +9422,87 @@ function groupPlan(hub, sessions, groupId) {
|
|
|
9401
9422
|
};
|
|
9402
9423
|
}
|
|
9403
9424
|
//#endregion
|
|
9425
|
+
//#region ../core/dist/session-view.js
|
|
9426
|
+
const unfinished = (a) => a.status === "todo" || a.status === "doing" || a.status === "blocked";
|
|
9427
|
+
/** The mainline and task a served reference points into; criteria of a group name neither. */
|
|
9428
|
+
function targetOf(ref) {
|
|
9429
|
+
const parsed = parseRef(ref);
|
|
9430
|
+
if (parsed?.kind === "checklistItem") return {
|
|
9431
|
+
mainline: parsed.mainline,
|
|
9432
|
+
task: parsed.task
|
|
9433
|
+
};
|
|
9434
|
+
if (parsed?.kind === "criterion") return { mainline: parsed.mainline };
|
|
9435
|
+
return null;
|
|
9436
|
+
}
|
|
9437
|
+
const taskProgress = (m) => {
|
|
9438
|
+
const parts = m.tasks.map((t) => listProgress(t.checklist));
|
|
9439
|
+
return {
|
|
9440
|
+
done: parts.reduce((n, p) => n + p.done, 0),
|
|
9441
|
+
total: parts.reduce((n, p) => n + p.total, 0)
|
|
9442
|
+
};
|
|
9443
|
+
};
|
|
9444
|
+
function sessionView({ hub, session, url, link, sync }) {
|
|
9445
|
+
const actions = session?.actions ?? [];
|
|
9446
|
+
const lead = actions.find((a) => a.status === "doing") ?? actions.find(unfinished);
|
|
9447
|
+
const doing = actions.find((a) => a.status === "doing");
|
|
9448
|
+
const focusTarget = lead?.serves.map(targetOf).find((t) => t !== null) ?? null;
|
|
9449
|
+
const focusMainline = focusTarget ? hub.mainlines.find((m) => m.id === focusTarget.mainline) : void 0;
|
|
9450
|
+
const serving = new Set((doing?.serves ?? []).flatMap((s) => targetOf(s) ?? []).filter((t) => t.task).map((t) => `${t.mainline}/${t.task}`));
|
|
9451
|
+
const mainlinePath = (id) => `/mainlines/${id}`;
|
|
9452
|
+
return {
|
|
9453
|
+
hub: {
|
|
9454
|
+
name: hub.config.hub,
|
|
9455
|
+
url,
|
|
9456
|
+
open: link("")
|
|
9457
|
+
},
|
|
9458
|
+
refs: refPaths(hub),
|
|
9459
|
+
session: session ? {
|
|
9460
|
+
id: session.id,
|
|
9461
|
+
actions: actions.map((a) => ({
|
|
9462
|
+
id: a.id,
|
|
9463
|
+
do: a.do,
|
|
9464
|
+
status: a.status,
|
|
9465
|
+
serves: a.serves,
|
|
9466
|
+
waiting: a.blockedBy ? {
|
|
9467
|
+
reason: a.blockedBy.reason,
|
|
9468
|
+
waitingFor: a.blockedBy.waitingFor ?? null,
|
|
9469
|
+
until: a.blockedBy.until ?? null
|
|
9470
|
+
} : null
|
|
9471
|
+
})),
|
|
9472
|
+
progress: {
|
|
9473
|
+
done: actions.filter((a) => a.status === "done").length,
|
|
9474
|
+
total: actions.filter((a) => a.status !== "dropped").length
|
|
9475
|
+
}
|
|
9476
|
+
} : null,
|
|
9477
|
+
focus: focusMainline ? {
|
|
9478
|
+
id: focusMainline.id,
|
|
9479
|
+
title: focusMainline.title,
|
|
9480
|
+
status: focusMainline.status,
|
|
9481
|
+
criteria: listProgress(focusMainline.criteria),
|
|
9482
|
+
checklist: taskProgress(focusMainline),
|
|
9483
|
+
link: link(mainlinePath(focusMainline.id))
|
|
9484
|
+
} : null,
|
|
9485
|
+
tasks: (focusMainline?.tasks ?? []).map((t) => ({
|
|
9486
|
+
mainline: focusMainline.id,
|
|
9487
|
+
id: t.id,
|
|
9488
|
+
title: t.title,
|
|
9489
|
+
status: t.status,
|
|
9490
|
+
checklist: listProgress(t.checklist),
|
|
9491
|
+
link: link(`${mainlinePath(focusMainline.id)}/tasks/${t.id}`),
|
|
9492
|
+
serving: serving.has(`${focusMainline.id}/${t.id}`)
|
|
9493
|
+
})),
|
|
9494
|
+
mainlines: hub.mainlines.filter(isOpenMainline).map((m) => ({
|
|
9495
|
+
id: m.id,
|
|
9496
|
+
title: m.title,
|
|
9497
|
+
status: m.status,
|
|
9498
|
+
group: m.group ?? null,
|
|
9499
|
+
checklist: taskProgress(m),
|
|
9500
|
+
link: link(mainlinePath(m.id))
|
|
9501
|
+
})),
|
|
9502
|
+
sync
|
|
9503
|
+
};
|
|
9504
|
+
}
|
|
9505
|
+
//#endregion
|
|
9404
9506
|
//#region src/host.ts
|
|
9405
9507
|
const DEFAULT_UI_PORT = 4320;
|
|
9406
9508
|
const hostName = (hub) => `${hub}.mainflow.localhost`;
|
|
@@ -9599,4 +9701,4 @@ function openInApp(hub, target, closeAfterMs = 150) {
|
|
|
9599
9701
|
return true;
|
|
9600
9702
|
}
|
|
9601
9703
|
//#endregion
|
|
9602
|
-
export {
|
|
9704
|
+
export { applyOperation as $, dataFile as $t, unhandledNotices as A, textBytes as At, attributeTrace as B, hubHoldingFile as Bt, duplicateClaimLines as C, ROUTES as Ct, markNoticesHandled as D, exceedsFileLimit as Dt, HANDLED_FILE as E, comparePaths as Et, matchGrants as F, MAINLINE_FILE as Ft, renumberFiles as G, registerHub as Gt, transcriptTitle as H, listHubs as Ht, decorateReply as I, SESSIONS_DIR as It, scopeOfPath as J, resolveHub as Jt, renumberPath as K, registeredName as Kt, endsWithMarker as L, TASK_FILE as Lt, readSession as M, hubFormatVersion as Mt, transcriptPath as N, hubRequirements as Nt, parseWarn as O, readsAsText as Ot, grantScope as P, versionMismatch as Pt, syncedPath as Q, currentBranch as Qt, headerLine as R, requireSlug as Rt, untilState as S, MAX_FILE_BYTES as St, turnReminder as T, VERSION_HEADER as Tt, applyOffline as U, notRegisteredMessage as Ut, traceTranscript as V, inRepositoryHubAt as Vt, renumberRequest as W, readRegistry as Wt, mayHoldSynced as X, userDataDir as Xt, hubDigest as Y, unregisterHub as Yt, syncedDir as Z, nodeStorage as Zt, receiverFromEnv as _, readYaml as _t, openInApp as a, DATA_DIR as an, loadMainlines as at, setArchivedMany as b, MAX_BLOB_BYTES as bt, siteBlock as c, splitFrontMatter as cn, mainlineEdges as ct, groupPlan as d, refIndex as dt, gitCheckout as en, checkHubName as et, listProgress as f, remoteMembers as ft, retroReportFromReceiver as g, readFrontMatter as gt, retroReport as h, nowMinute as ht, hostName as i, ConflictError as in, loadHub as it, loadSessions as j, formatMismatch as jt, readNotices as k, requestTime as kt, syncHost as l, linksInto as lt, migrateHub as m, taskFile as mt, DEFAULT_UI_PORT as n, realPath as nn, isOpenMainline as nt, removeDanglingLinks as o, WriteError as on, sectionText as ot, migrateHubData as p, unavailableHubs as pt, renumberText as q, registryFile as qt, caddySites as r, worktreesOf as rn, loadConfig as rt, removeHostLink as s, contentHash as sn, dependencyGraph as st, APP_BUNDLE_ID as t, hubPath as tn, HubError as tt, sessionView as u, memberOfGroup as ut, RETRY_RULES as v, ACCESS_HEADERS as vt, sessionBriefing as w, SOCKET_MAX_AGE_CLOSE as wt, initHub as x, MAX_CHANGE_PAGE as xt, ARCHIVE_KINDS as y, BLOB_HASH as yt, mayDecorate as z, hubAtRoot as zt };
|
package/dist/host.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-
|
|
1
|
+
import { a as openInApp, c as siteBlock, i as hostName, l as syncHost, n as DEFAULT_UI_PORT, o as removeDanglingLinks, r as caddySites, s as removeHostLink, t as APP_BUNDLE_ID } from "./host-CsZY5trM.js";
|
|
2
2
|
export { APP_BUNDLE_ID, DEFAULT_UI_PORT, caddySites, hostName, openInApp, removeDanglingLinks, removeHostLink, siteBlock, syncHost };
|
package/dist/launch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as hostName, l as syncHost, n as DEFAULT_UI_PORT,
|
|
1
|
+
import { i as hostName, l as syncHost, n as DEFAULT_UI_PORT, on as WriteError, s as removeHostLink } from "./host-CsZY5trM.js";
|
|
2
2
|
import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
@@ -273,7 +273,7 @@ async function launchUi(dirs, hub, configured, options) {
|
|
|
273
273
|
}
|
|
274
274
|
let server;
|
|
275
275
|
if (!reuse) {
|
|
276
|
-
const { serveUi } = await import("./ui-
|
|
276
|
+
const { serveUi } = await import("./ui-Bv29R0WG.js");
|
|
277
277
|
server = await serveUi(dirs, choice.port);
|
|
278
278
|
writeUiRecord(dirs, {
|
|
279
279
|
port: choice.port,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as applyOperation, At as textBytes, Ct as ROUTES, Dt as exceedsFileLimit, Et as comparePaths, G as renumberFiles, J as scopeOfPath, K as renumberPath, Kt as registeredName, Nt as hubRequirements, Ot as readsAsText, Pt as versionMismatch, Q as syncedPath, St as MAX_FILE_BYTES, Tt as VERSION_HEADER, U as applyOffline, W as renumberRequest, X as mayHoldSynced, Y as hubDigest, Z as syncedDir, Zt as nodeStorage, bt as MAX_BLOB_BYTES, cn as splitFrontMatter, ht as nowMinute, in as ConflictError, kt as requestTime, on as WriteError, q as renumberText, sn as contentHash, vt as ACCESS_HEADERS, wt as SOCKET_MAX_AGE_CLOSE, yt as BLOB_HASH } from "./host-CsZY5trM.js";
|
|
2
2
|
import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
|
|
3
3
|
import { createHash, randomBytes } from "node:crypto";
|
|
4
4
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants, copyFileSync, createReadStream, createWriteStream, existsSync, linkSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, watch, writeFileSync } from "node:fs";
|
|
@@ -920,6 +920,11 @@ const readKnownDirs = (hub) => {
|
|
|
920
920
|
const dirs = readJson(syncPaths(hub).known, {}).dirs;
|
|
921
921
|
return Array.isArray(dirs) ? dirs.filter((dir) => typeof dir === "string") : [];
|
|
922
922
|
};
|
|
923
|
+
/** The hub's seq known.json names (KnownFile.seq); undefined when it names none, or none that is a seq. */
|
|
924
|
+
const readKnownSeq = (hub) => {
|
|
925
|
+
const seq = readJson(syncPaths(hub).known, {}).seq;
|
|
926
|
+
return Number.isInteger(seq) && seq >= 0 ? seq : void 0;
|
|
927
|
+
};
|
|
923
928
|
/** What known.json writes before a binary file's sha256, apart from a text's content hash. */
|
|
924
929
|
const BLOB_PREFIX = "blob:";
|
|
925
930
|
/** The text of the version known.json names by `hash`; undefined when it was not kept, as by a sync client before known/. */
|
|
@@ -936,9 +941,9 @@ function readKnownText(dirs, hash) {
|
|
|
936
941
|
* there yet; the texts of versions it no longer names are removed. A version whose text the client does not have keeps
|
|
937
942
|
* the text known/ holds, if any. The new texts go first, then known.json, then the removals, so a client killed at any
|
|
938
943
|
* step leaves a known.json whose every text is in known/. A binary file is given as its sha256 with BLOB_PREFIX, and no text.
|
|
939
|
-
* `dirs` are the directories it names.
|
|
944
|
+
* `dirs` are the directories it names, and `seq` the hub's seq they are as of, written with them (KnownFile.seq).
|
|
940
945
|
*/
|
|
941
|
-
function writeKnown(hub, files, dirs = []) {
|
|
946
|
+
function writeKnown(hub, files, dirs = [], seq) {
|
|
942
947
|
const dir = syncPaths(hub).knownTexts;
|
|
943
948
|
mkdirSync(dir, { recursive: true });
|
|
944
949
|
const named = new Set(Object.values(files).map((file) => file.hash));
|
|
@@ -951,7 +956,8 @@ function writeKnown(hub, files, dirs = []) {
|
|
|
951
956
|
}
|
|
952
957
|
const known = {
|
|
953
958
|
files: Object.fromEntries(Object.entries(files).map(([path, file]) => [path, file.hash])),
|
|
954
|
-
...dirs.length > 0 ? { dirs } : {}
|
|
959
|
+
...dirs.length > 0 ? { dirs } : {},
|
|
960
|
+
...seq === void 0 ? {} : { seq }
|
|
955
961
|
};
|
|
956
962
|
writeJson(syncPaths(hub).known, known);
|
|
957
963
|
for (const name of held) if (!named.has(name)) rmSync(join(dir, name), { force: true });
|
|
@@ -1656,7 +1662,8 @@ var SyncClient = class extends EventEmitter {
|
|
|
1656
1662
|
* every directory, but for the paths changed after it: a whole snapshot with binary files was taken in this run, and
|
|
1657
1663
|
* nothing since made the client unsure of what it holds, as a server that stopped keeping binary files, or a hub
|
|
1658
1664
|
* reloaded. Only then is a snapshot asked for what changed since hubSeq (`since`), as a path the answer does not name
|
|
1659
|
-
* holds what the client takes the server to hold.
|
|
1665
|
+
* holds what the client takes the server to hold. A run after it starts from known.json instead, which names the seq
|
|
1666
|
+
* (knownSeq, resumeSeq).
|
|
1660
1667
|
*/
|
|
1661
1668
|
holdsHub = false;
|
|
1662
1669
|
/**
|
|
@@ -1664,6 +1671,20 @@ var SyncClient = class extends EventEmitter {
|
|
|
1664
1671
|
* not leave the client holding the hub (holdsHub), whatever it took in, so the next one is whole.
|
|
1665
1672
|
*/
|
|
1666
1673
|
reloads = 0;
|
|
1674
|
+
/**
|
|
1675
|
+
* The hub's seq known.json named when this run started (readKnownSeq): the files it names, its directories and
|
|
1676
|
+
* remote.json are the hub as of it, so the run's first snapshot asks for what changed since, as one after a reconnect
|
|
1677
|
+
* does (holdsHub). Undefined once a snapshot was taken, or when known.json names none or the run cannot take it, as
|
|
1678
|
+
* when a text it names is not in known/.
|
|
1679
|
+
*/
|
|
1680
|
+
resumeSeq;
|
|
1681
|
+
/**
|
|
1682
|
+
* For each path, a seq at or before which the hub held another version than the one the client knows of now (in
|
|
1683
|
+
* `cloud`, or `remote`): a broadcast's own seq, or, for a snapshot's, one after the seq of the version before it. A
|
|
1684
|
+
* path whose known.json version lags behind is changed on the server after the seq known.json is written with
|
|
1685
|
+
* (knownSeq), so what changed since names it.
|
|
1686
|
+
*/
|
|
1687
|
+
changedAt = /* @__PURE__ */ new Map();
|
|
1667
1688
|
/** The seqs of the broadcasts on the current connection above hubSeq with a change between that never arrived */
|
|
1668
1689
|
ahead = /* @__PURE__ */ new Set();
|
|
1669
1690
|
waiters = /* @__PURE__ */ new Set();
|
|
@@ -1861,6 +1882,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
1861
1882
|
async restoreKnown() {
|
|
1862
1883
|
this.copied = !existsSync(this.paths.known);
|
|
1863
1884
|
this.blobsCompared = !this.copied && !existsSync(this.paths.copyBlobs);
|
|
1885
|
+
this.resumeSeq = this.blobsCompared ? readKnownSeq(this.dirs) : void 0;
|
|
1864
1886
|
for (const dir of readKnownDirs(this.dirs)) this.syncedDirs.add(dir);
|
|
1865
1887
|
for (const [path, hash] of Object.entries(readKnown(this.dirs))) {
|
|
1866
1888
|
if (hash.startsWith("blob:")) {
|
|
@@ -1884,7 +1906,9 @@ var SyncClient = class extends EventEmitter {
|
|
|
1884
1906
|
text,
|
|
1885
1907
|
server: text
|
|
1886
1908
|
});
|
|
1909
|
+
else if (this.syncs(path)) this.resumeSeq = void 0;
|
|
1887
1910
|
}
|
|
1911
|
+
if (this.resumeSeq !== void 0) this.log(`known.json is of the hub's seq ${this.resumeSeq}: the first snapshot asks for what changed since`);
|
|
1888
1912
|
}
|
|
1889
1913
|
/**
|
|
1890
1914
|
* The refusals a run killed before it settled them left in the outbox (src/state.ts): each text not saved yet is saved,
|
|
@@ -2118,12 +2142,13 @@ var SyncClient = class extends EventEmitter {
|
|
|
2118
2142
|
if ("reload" in message && message.reload) {
|
|
2119
2143
|
this.reloads++;
|
|
2120
2144
|
this.holdsHub = false;
|
|
2145
|
+
this.resumeSeq = void 0;
|
|
2121
2146
|
this.track(this.resync());
|
|
2122
2147
|
return;
|
|
2123
2148
|
}
|
|
2124
2149
|
for (const event of message.files) if (event.kind === "dir") this.receiveDir(event.path, seq, true);
|
|
2125
2150
|
else if (event.kind === "dirRemoved") this.receiveDirRemoved(event.path, seq);
|
|
2126
|
-
else this.receive(event, seq);
|
|
2151
|
+
else this.receive(event, seq, true);
|
|
2127
2152
|
if (this.ahead.size > 0 && this.synced && this.resyncing === null) {
|
|
2128
2153
|
this.log(`missed the hub's changes ${this.hubSeq + 1} to ${Math.min(...this.ahead) - 1}; reloading the snapshot`);
|
|
2129
2154
|
this.track(this.resync());
|
|
@@ -2144,7 +2169,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2144
2169
|
* that may stay on the server, as a new screenshot of a closed mainline, goes into `remote` instead of being downloaded,
|
|
2145
2170
|
* as a listed text does in a snapshot.
|
|
2146
2171
|
*/
|
|
2147
|
-
receive(event, seq) {
|
|
2172
|
+
receive(event, seq, live = false) {
|
|
2148
2173
|
if (!this.syncs(event.path)) return;
|
|
2149
2174
|
const file = event.kind === "file" ? event.text : event.kind === "blob" ? {
|
|
2150
2175
|
kind: "blob",
|
|
@@ -2157,6 +2182,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2157
2182
|
this.trace(`older ${event.path} ${label} seq ${seq}, holding seq ${held}`);
|
|
2158
2183
|
return;
|
|
2159
2184
|
}
|
|
2185
|
+
this.noteChange(event.path, file === null ? null : serverHash(file), live ? seq : held + 1);
|
|
2160
2186
|
this.fileSeq.set(event.path, seq);
|
|
2161
2187
|
if (this.downloadErrors.has(event.path)) {
|
|
2162
2188
|
const before = this.cloud.get(event.path);
|
|
@@ -2238,6 +2264,16 @@ var SyncClient = class extends EventEmitter {
|
|
|
2238
2264
|
forgetDir(path) {
|
|
2239
2265
|
if (this.syncedDirs.delete(path)) this.saveKnown();
|
|
2240
2266
|
}
|
|
2267
|
+
/** The hash the client knows the server's file at `path` by: of its version in `cloud`, or `remote`; null for none. */
|
|
2268
|
+
serverView(path) {
|
|
2269
|
+
const file = this.cloud.get(path);
|
|
2270
|
+
if (file !== void 0 && file !== null) return serverHash(file);
|
|
2271
|
+
return this.remote.get(path) ?? null;
|
|
2272
|
+
}
|
|
2273
|
+
/** The server's file at `path` is to be known by `hash` (null for none), a change of the hub at seq `at` or later (changedAt). */
|
|
2274
|
+
noteChange(path, hash, at) {
|
|
2275
|
+
if (this.serverView(path) !== hash) this.changedAt.set(path, at);
|
|
2276
|
+
}
|
|
2241
2277
|
/** Whether the client holds `path` as of the hub's `seq` or later. */
|
|
2242
2278
|
reached(path, seq) {
|
|
2243
2279
|
return this.hubSeq >= seq || (this.fileSeq.get(path) ?? -1) >= seq;
|
|
@@ -2381,7 +2417,8 @@ var SyncClient = class extends EventEmitter {
|
|
|
2381
2417
|
* since hubSeq alone (`since`). A server whose change log tells answers with the paths changed (SnapshotDelta): taken
|
|
2382
2418
|
* in over what the client holds once they make the hub's digest (changesHold, takeChanges), and else dropped for the
|
|
2383
2419
|
* whole snapshot. A server that cannot tell, or one from before it, answers the first batch, and the snapshot goes on
|
|
2384
|
-
* whole as ever.
|
|
2420
|
+
* whole as ever. A run's first snapshot asks the same way from the seq known.json names (resumeSeq), taking known.json,
|
|
2421
|
+
* remote.json and the directories known.json names for what the server held as of it (knownCopy).
|
|
2385
2422
|
*/
|
|
2386
2423
|
takeSnapshot() {
|
|
2387
2424
|
return this.exclusive(() => this.snapshotNow());
|
|
@@ -2406,13 +2443,15 @@ var SyncClient = class extends EventEmitter {
|
|
|
2406
2443
|
/** The reload broadcasts that came before this snapshot: one that comes while it runs leaves the next one whole */
|
|
2407
2444
|
const reloads = this.reloads;
|
|
2408
2445
|
/**
|
|
2409
|
-
* Whether the snapshot may ask for what changed since hubSeq
|
|
2446
|
+
* Whether the snapshot may ask for what changed since a seq: hubSeq while the client holds the hub, or the seq
|
|
2447
|
+
* known.json names before the run's first snapshot (resumeSeq). Read once the probe told whether the server keeps
|
|
2410
2448
|
* binary files; not once an answer of it failed the digest
|
|
2411
2449
|
*/
|
|
2412
|
-
let askChanges = this.holdsHub;
|
|
2450
|
+
let askChanges = this.holdsHub || this.resumeSeq !== void 0;
|
|
2413
2451
|
for (;;) {
|
|
2414
2452
|
before = this.hubSeq;
|
|
2415
|
-
const
|
|
2453
|
+
const from = !askChanges || this.blobSupport !== true ? null : this.holdsHub ? before >= 0 ? before : null : this.resumeSeq ?? null;
|
|
2454
|
+
const base = from === null ? null : this.holdsHub ? this.serverCopy() : this.knownCopy();
|
|
2416
2455
|
({texts: held, blobs: heldBlobs} = await this.holdings());
|
|
2417
2456
|
let dropped = false;
|
|
2418
2457
|
for (const path of this.remote.keys()) {
|
|
@@ -2433,7 +2472,7 @@ var SyncClient = class extends EventEmitter {
|
|
|
2433
2472
|
value: hubDigest(have, dirs),
|
|
2434
2473
|
dirs
|
|
2435
2474
|
} : void 0;
|
|
2436
|
-
since = base !== null && withBlobs && !dropped ?
|
|
2475
|
+
since = base !== null && from !== null && withBlobs && !dropped ? from : void 0;
|
|
2437
2476
|
const taken = await this.api.snapshot(have, {
|
|
2438
2477
|
listOnly: this.listOnly(this.copied && !this.synced),
|
|
2439
2478
|
blobs: withBlobs,
|
|
@@ -2583,6 +2622,8 @@ var SyncClient = class extends EventEmitter {
|
|
|
2583
2622
|
this.takenUpTo(seq);
|
|
2584
2623
|
this.synced = true;
|
|
2585
2624
|
this.holdsHub = withBlobs && this.reloads === reloads;
|
|
2625
|
+
this.resumeSeq = void 0;
|
|
2626
|
+
this.saveKnown();
|
|
2586
2627
|
if (withBlobs && !this.blobsCompared) {
|
|
2587
2628
|
this.blobsCompared = true;
|
|
2588
2629
|
rmSync(this.paths.copyBlobs, { force: true });
|
|
@@ -2597,6 +2638,32 @@ var SyncClient = class extends EventEmitter {
|
|
|
2597
2638
|
await this.queueRemovedDirs();
|
|
2598
2639
|
}
|
|
2599
2640
|
/**
|
|
2641
|
+
* What the client takes the server to hold as of the seq known.json names, before the run's first snapshot
|
|
2642
|
+
* (resumeSeq): the version known.json names of each file, but one only on the server (remote.json), and the
|
|
2643
|
+
* directories it names; with what a broadcast taken in since the run started holds, which is of a later seq.
|
|
2644
|
+
*/
|
|
2645
|
+
knownCopy() {
|
|
2646
|
+
const files = /* @__PURE__ */ new Map();
|
|
2647
|
+
for (const [path, disk] of this.confirmed) {
|
|
2648
|
+
if (!disk || !this.syncs(path) || this.remote.has(path)) continue;
|
|
2649
|
+
if (disk.blob) files.set(path, {
|
|
2650
|
+
kind: "blob",
|
|
2651
|
+
hash: disk.hash
|
|
2652
|
+
});
|
|
2653
|
+
else if (disk.text !== void 0) files.set(path, disk.text);
|
|
2654
|
+
}
|
|
2655
|
+
for (const [path, file] of this.cloud) {
|
|
2656
|
+
if (!this.syncs(path)) continue;
|
|
2657
|
+
if (file === null) files.delete(path);
|
|
2658
|
+
else files.set(path, file);
|
|
2659
|
+
}
|
|
2660
|
+
return {
|
|
2661
|
+
files,
|
|
2662
|
+
remote: new Map(this.remote),
|
|
2663
|
+
dirs: new Set(this.syncedDirs)
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2600
2667
|
* What the client takes the server to hold, while it holds the hub (holdsHub): the server's version of each file it
|
|
2601
2668
|
* took in, a binary file's included, the hash of each file only on the server, and the directories the server holds.
|
|
2602
2669
|
* A file here the server never had is not among them, though a snapshot request names it.
|
|
@@ -3237,7 +3304,9 @@ var SyncClient = class extends EventEmitter {
|
|
|
3237
3304
|
}
|
|
3238
3305
|
/** Note `path` as only on the server, with the hash of the server's text as of `seq`, unless a newer version came. */
|
|
3239
3306
|
keepRemote(path, hash, seq) {
|
|
3240
|
-
|
|
3307
|
+
const held = this.fileSeq.get(path) ?? -1;
|
|
3308
|
+
if (seq <= held) return;
|
|
3309
|
+
this.noteChange(path, hash, held + 1);
|
|
3241
3310
|
this.fileSeq.set(path, seq);
|
|
3242
3311
|
this.remote.set(path, hash);
|
|
3243
3312
|
this.cloud.delete(path);
|
|
@@ -5688,12 +5757,39 @@ var SyncClient = class extends EventEmitter {
|
|
|
5688
5757
|
this.knownTimer = void 0;
|
|
5689
5758
|
const files = this.knownFiles();
|
|
5690
5759
|
const dirs = this.knownDirs();
|
|
5760
|
+
const seq = this.knownSeq();
|
|
5761
|
+
if (seq !== void 0) this.saveRemoteNow(final);
|
|
5691
5762
|
try {
|
|
5692
|
-
underLock(this.dirs, () => writeKnown(this.dirs, files, dirs));
|
|
5763
|
+
underLock(this.dirs, () => writeKnown(this.dirs, files, dirs, seq));
|
|
5693
5764
|
} catch (error) {
|
|
5694
5765
|
this.log(`could not save local/sync/known.json: ${error.message}`);
|
|
5695
5766
|
}
|
|
5696
5767
|
}
|
|
5768
|
+
/**
|
|
5769
|
+
* The hub's seq known.json is written with (KnownFile.seq): one as of which the files it names, its directories and
|
|
5770
|
+
* remote.json are the hub's, but for paths changed on the server after it, so that what changed since names every
|
|
5771
|
+
* path whose version there differs. Only while the client holds the hub (holdsHub) and no snapshot is under way, as
|
|
5772
|
+
* the files it is taking in are of later seqs; and none while a listed file is being downloaded. It is hubSeq, or
|
|
5773
|
+
* lower when a file's version known.json names is not the server's the client knows of, as one the outbox holds a
|
|
5774
|
+
* write about while the server changed it: one before that change (changedAt). Undefined when there is none.
|
|
5775
|
+
*/
|
|
5776
|
+
knownSeq() {
|
|
5777
|
+
if (!this.holdsHub || this.hubSeq < 0 || this.resyncing !== null || this.awaiting.size > 0) return void 0;
|
|
5778
|
+
const listed = this.listedRemote();
|
|
5779
|
+
let seq = this.hubSeq;
|
|
5780
|
+
for (const path of /* @__PURE__ */ new Set([
|
|
5781
|
+
...this.cloud.keys(),
|
|
5782
|
+
...this.confirmed.keys(),
|
|
5783
|
+
...this.remote.keys()
|
|
5784
|
+
])) {
|
|
5785
|
+
if (!this.syncs(path)) continue;
|
|
5786
|
+
const disk = this.confirmed.get(path);
|
|
5787
|
+
if ((listed.get(path) ?? (disk ? disk.hash : null)) === this.serverView(path)) continue;
|
|
5788
|
+
seq = Math.min(seq, (this.changedAt.get(path) ?? 0) - 1);
|
|
5789
|
+
if (seq < 0) return void 0;
|
|
5790
|
+
}
|
|
5791
|
+
return seq;
|
|
5792
|
+
}
|
|
5697
5793
|
};
|
|
5698
5794
|
//#endregion
|
|
5699
5795
|
//#region ../sync/dist/ipc.js
|
|
@@ -5724,30 +5820,75 @@ var DaemonUnavailable = class extends Error {
|
|
|
5724
5820
|
};
|
|
5725
5821
|
/** Another sync client already runs for the hub. */
|
|
5726
5822
|
var DaemonRunning = class extends Error {};
|
|
5823
|
+
/** The answer's body could not be read to its end, after its status came. */
|
|
5824
|
+
var BodyUnread = class extends Error {};
|
|
5825
|
+
/**
|
|
5826
|
+
* One request to the sync client on the loopback interface, with the status and the whole body of its answer. It uses
|
|
5827
|
+
* node:http, taken when first called, rather than fetch: fetch loads undici and with it tls, and a process with
|
|
5828
|
+
* NODE_USE_SYSTEM_CA set reads the system's certificates when tls loads, which costs every hook that writes about 140 ms.
|
|
5829
|
+
* `agent: false` opens a connection for this request alone, so no kept-alive socket holds a short-lived command open.
|
|
5830
|
+
* No answer within `timeoutMs` rejects with an error named TimeoutError, which callDaemon takes as a silent sync client;
|
|
5831
|
+
* the time running out while the body is read rejects with BodyUnread, as does any failure reading it.
|
|
5832
|
+
*/
|
|
5833
|
+
function loopbackRequest(port, path, headers, body, timeoutMs) {
|
|
5834
|
+
return new Promise((resolve, reject) => {
|
|
5835
|
+
let answered = false;
|
|
5836
|
+
const request = process.getBuiltinModule("node:http").request({
|
|
5837
|
+
host: "127.0.0.1",
|
|
5838
|
+
port,
|
|
5839
|
+
path,
|
|
5840
|
+
method: body === void 0 ? "GET" : "POST",
|
|
5841
|
+
headers,
|
|
5842
|
+
agent: false
|
|
5843
|
+
}, (response) => {
|
|
5844
|
+
answered = true;
|
|
5845
|
+
const chunks = [];
|
|
5846
|
+
response.on("data", (chunk) => chunks.push(chunk));
|
|
5847
|
+
response.on("end", () => {
|
|
5848
|
+
clearTimeout(timer);
|
|
5849
|
+
resolve({
|
|
5850
|
+
status: response.statusCode ?? 0,
|
|
5851
|
+
text: Buffer.concat(chunks).toString("utf8")
|
|
5852
|
+
});
|
|
5853
|
+
});
|
|
5854
|
+
response.on("error", (error) => {
|
|
5855
|
+
clearTimeout(timer);
|
|
5856
|
+
reject(new BodyUnread(error.message));
|
|
5857
|
+
});
|
|
5858
|
+
});
|
|
5859
|
+
const timer = setTimeout(() => {
|
|
5860
|
+
const error = answered ? new BodyUnread("the answer did not end in time") : Object.assign(/* @__PURE__ */ new Error("The operation was aborted due to timeout"), { name: "TimeoutError" });
|
|
5861
|
+
request.destroy(error);
|
|
5862
|
+
reject(error);
|
|
5863
|
+
}, timeoutMs);
|
|
5864
|
+
request.on("error", (error) => {
|
|
5865
|
+
clearTimeout(timer);
|
|
5866
|
+
reject(answered ? new BodyUnread(error.message) : error);
|
|
5867
|
+
});
|
|
5868
|
+
request.end(body);
|
|
5869
|
+
});
|
|
5870
|
+
}
|
|
5727
5871
|
async function callDaemon(daemon, route, body, timeoutMs = 6e4) {
|
|
5872
|
+
const notSync = () => new DaemonUnavailable(`what answers on port ${daemon.port} is not mainflow sync (pid ${daemon.pid})`);
|
|
5728
5873
|
let response;
|
|
5729
5874
|
try {
|
|
5730
|
-
response = await
|
|
5731
|
-
|
|
5732
|
-
|
|
5733
|
-
|
|
5734
|
-
"content-type": "application/json"
|
|
5735
|
-
},
|
|
5736
|
-
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
5737
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
5738
|
-
});
|
|
5875
|
+
response = await loopbackRequest(daemon.port, route, {
|
|
5876
|
+
[SECRET_HEADER]: daemon.secret,
|
|
5877
|
+
"content-type": "application/json"
|
|
5878
|
+
}, body === void 0 ? void 0 : JSON.stringify(body), timeoutMs);
|
|
5739
5879
|
} catch (error) {
|
|
5880
|
+
if (error instanceof BodyUnread) throw notSync();
|
|
5740
5881
|
const silent = error.name === "TimeoutError";
|
|
5741
5882
|
throw new DaemonUnavailable(`mainflow sync (pid ${daemon.pid}) does not answer: ${error.message}`, silent);
|
|
5742
5883
|
}
|
|
5743
5884
|
let parsed;
|
|
5744
5885
|
try {
|
|
5745
|
-
parsed =
|
|
5886
|
+
parsed = JSON.parse(response.text);
|
|
5746
5887
|
} catch {
|
|
5747
|
-
throw
|
|
5888
|
+
throw notSync();
|
|
5748
5889
|
}
|
|
5749
5890
|
if (response.status === 403) throw new DaemonUnavailable(`what answers on port ${daemon.port} is not this hub's mainflow sync (pid ${daemon.pid})`);
|
|
5750
|
-
if (
|
|
5891
|
+
if (response.status < 200 || response.status > 299) throw new Error(`mainflow sync failed: ${parsed.error}`);
|
|
5751
5892
|
return parsed;
|
|
5752
5893
|
}
|
|
5753
5894
|
/** How long a probe waits for the sync client's answer; a client holding the write lock answers after it. */
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { I as configSchema, tt as repoPaths } from "./dist-CXrUfRQp.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { C as readRemote, F as syncPaths, N as SYNC_DIR, P as isSynced, S as watchTree, _ as DaemonUnavailable, b as readDaemon, n as hubWriter, r as writtenLines, s as remoteMode, v as callDaemon } from "./operation-BmCeGKHw.js";
|
|
2
|
+
import { $t as dataFile, B as attributeTrace, D as markNoticesHandled, E as HANDLED_FILE, H as transcriptTitle, M as readSession, N as transcriptPath, O as parseWarn, V as traceTranscript, _t as readYaml, a as openInApp, an as DATA_DIR, b as setArchivedMany, ct as mainlineEdges, d as groupPlan, dt as refIndex, f as listProgress, ft as remoteMembers, in as ConflictError, it as loadHub, j as loadSessions, k as readNotices$1, lt as linksInto, on as WriteError, ot as sectionText, rt as loadConfig, st as dependencyGraph, tn as hubPath, tt as HubError, y as ARCHIVE_KINDS, zt as hubAtRoot } from "./host-CsZY5trM.js";
|
|
3
|
+
import { C as readRemote, F as syncPaths, N as SYNC_DIR, P as isSynced, S as watchTree, _ as DaemonUnavailable, b as readDaemon, n as hubWriter, r as writtenLines, s as remoteMode, v as callDaemon } from "./operation-MKNwQkAY.js";
|
|
5
4
|
import { t as CLI_VERSION } from "./version-Dvv4jwF1.js";
|
|
6
5
|
import { readUiRecord, realRoot } from "./launch.js";
|
|
7
6
|
import { createRequire } from "node:module";
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
//#region ../core/dist/progress.js
|
|
2
|
-
function listProgress(items) {
|
|
3
|
-
const counted = items.filter((item) => !item.dropped);
|
|
4
|
-
return {
|
|
5
|
-
done: counted.filter((item) => item.done).length,
|
|
6
|
-
total: counted.length
|
|
7
|
-
};
|
|
8
|
-
}
|
|
9
|
-
//#endregion
|
|
10
|
-
export { listProgress as t };
|