baychat 0.29.9 → 0.31.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/.claude-plugin/plugin.json +1 -1
- package/README.md +43 -10
- package/dist/approval-wait.js +143 -0
- package/dist/approve-hook.js +68 -133
- package/dist/auto-update/describe.js +66 -0
- package/dist/auto-update/notice.js +40 -0
- package/dist/auto-update/policy.js +110 -0
- package/dist/auto-update/relay-glue.js +143 -0
- package/dist/auto-update/store.js +140 -0
- package/dist/auto-update/updater.js +324 -0
- package/dist/channel/permission.js +6 -0
- package/dist/cli-help.js +30 -3
- package/dist/denied/after-hook.js +46 -0
- package/dist/denied/confirm.js +131 -0
- package/dist/denied/grants.js +292 -0
- package/dist/denied/hook-log.js +75 -0
- package/dist/denied/live-session.js +154 -0
- package/dist/denied/notice.js +29 -0
- package/dist/denied/opt-in.js +68 -0
- package/dist/denied/permission-rule.js +186 -0
- package/dist/denied/settings-rules.js +221 -0
- package/dist/denied/transcript.js +173 -0
- package/dist/denied-hook.js +431 -0
- package/dist/doctor-command.js +7 -1
- package/dist/doctor.js +28 -0
- package/dist/help-topics.js +12 -4
- package/dist/hook-stdin.js +35 -0
- package/dist/index.js +13 -1
- package/dist/main.js +20 -2
- package/dist/permissions-command.js +104 -0
- package/dist/protocol-content.js +1 -1
- package/dist/refresh-clients.js +36 -0
- package/dist/relay/acp/runner.js +4 -0
- package/dist/relay/commands.js +19 -3
- package/dist/relay/daemon.js +37 -1
- package/dist/relay/launcher/approval-settings.js +56 -2
- package/dist/relay/launcher/loop.js +4 -1
- package/dist/relay/self-restart.js +149 -0
- package/dist/relay/terminal-pane.js +74 -2
- package/dist/runtimes.js +2 -2
- package/dist/self-upgrade.js +92 -27
- package/dist/start-command.js +15 -1
- package/dist/update-command.js +51 -31
- package/dist/waiting-cards.js +107 -0
- package/package.json +4 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HELD_FRESH_MS = exports.QUIET_MS = exports.CARD_POLL_MS = exports.ASK_AGAIN_MS = exports.CHECK_INTERVAL_MS = exports.AUTO_UPDATE_ENV = void 0;
|
|
4
|
+
exports.planUpdate = planUpdate;
|
|
5
|
+
exports.autoUpdateOffReason = autoUpdateOffReason;
|
|
6
|
+
exports.nextCheckAt = nextCheckAt;
|
|
7
|
+
exports.installAllowedAt = installAllowedAt;
|
|
8
|
+
exports.busyReason = busyReason;
|
|
9
|
+
const self_upgrade_1 = require("../self-upgrade");
|
|
10
|
+
/** Longest reason a card or terminal line carries when several releases each said something. */
|
|
11
|
+
const REASON_MAX_CHARS = 300;
|
|
12
|
+
function planUpdate(current, release) {
|
|
13
|
+
const order = (0, self_upgrade_1.compareVersions)(current, release.version);
|
|
14
|
+
// Already there, newer (a local build), or a version that does not parse (a dev build like
|
|
15
|
+
// 0.31.0-dev): never install over it.
|
|
16
|
+
if (order === undefined || order >= 0)
|
|
17
|
+
return { kind: "current" };
|
|
18
|
+
// Every release this machine would cross that asked for the person, newest first.
|
|
19
|
+
const crossed = Object.entries(release.attention)
|
|
20
|
+
.filter(([version]) => (0, self_upgrade_1.compareVersions)(version, current) === 1 && ((0, self_upgrade_1.compareVersions)(version, release.version) ?? 1) <= 0)
|
|
21
|
+
.sort(([a], [b]) => (0, self_upgrade_1.compareVersions)(b, a) ?? 0)
|
|
22
|
+
.map(([, reason]) => reason);
|
|
23
|
+
const major = (v) => Number(v.split(".")[0]);
|
|
24
|
+
if (major(release.version) > major(current))
|
|
25
|
+
crossed.push(`a new major version (${major(release.version)}.x)`);
|
|
26
|
+
if (crossed.length === 0)
|
|
27
|
+
return { kind: "auto", version: release.version };
|
|
28
|
+
return { kind: "ask", version: release.version, reason: clip(crossed.join("; "), REASON_MAX_CHARS) };
|
|
29
|
+
}
|
|
30
|
+
/** Environment values that switch automatic updates off (for CI and companies). */
|
|
31
|
+
const ENV_OFF = new Set(["0", "off", "false", "no"]);
|
|
32
|
+
exports.AUTO_UPDATE_ENV = "BAYCHAT_AUTO_UPDATE";
|
|
33
|
+
/** Why automatic updates are off, or undefined when they are on. The environment wins. */
|
|
34
|
+
function autoUpdateOffReason(env, setting) {
|
|
35
|
+
const raw = env[exports.AUTO_UPDATE_ENV];
|
|
36
|
+
if (raw !== undefined && ENV_OFF.has(raw.trim().toLowerCase()))
|
|
37
|
+
return `${exports.AUTO_UPDATE_ENV}=${raw.trim()}`;
|
|
38
|
+
if (setting === "off")
|
|
39
|
+
return "baychat update --auto off";
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
// ─── Timing ───────────────────────────────────────────────────────────────────────────────
|
|
43
|
+
const MINUTE = 60_000;
|
|
44
|
+
const HOUR = 60 * MINUTE;
|
|
45
|
+
const DAY = 24 * HOUR;
|
|
46
|
+
/** About once a day — invisible on the registry, and soon enough that nobody runs `update`. */
|
|
47
|
+
exports.CHECK_INTERVAL_MS = DAY;
|
|
48
|
+
/** Spread across an hour so a fleet of relays started together never asks npm together. */
|
|
49
|
+
const CHECK_SPREAD_MS = HOUR;
|
|
50
|
+
/** The first check after a start: soon, but not in the busy first minutes. */
|
|
51
|
+
const FIRST_CHECK_MIN_MS = 5 * MINUTE;
|
|
52
|
+
const FIRST_CHECK_SPREAD_MS = 10 * MINUTE;
|
|
53
|
+
/** A check that failed (offline, npm down) is tried again sooner than a day, then backs off. */
|
|
54
|
+
const CHECK_RETRY_MS = HOUR;
|
|
55
|
+
/** A failed install of one version waits a day, then two, … up to a week. A newer version resets it. */
|
|
56
|
+
const INSTALL_RETRY_MS = DAY;
|
|
57
|
+
const INSTALL_RETRY_MAX_MS = 7 * DAY;
|
|
58
|
+
/** "Later" on the card: asked again after this long. */
|
|
59
|
+
exports.ASK_AGAIN_MS = 3 * DAY;
|
|
60
|
+
/** How often a waiting card is looked at. A tap on "Update now" lands within this. */
|
|
61
|
+
exports.CARD_POLL_MS = 5 * MINUTE;
|
|
62
|
+
/** No message for this long before a restart: an agent woken just now is likely mid-turn. */
|
|
63
|
+
exports.QUIET_MS = 10 * MINUTE;
|
|
64
|
+
/** A held message younger than this is a session mid-turn; an older one is a session that died. */
|
|
65
|
+
exports.HELD_FRESH_MS = 30 * MINUTE;
|
|
66
|
+
/**
|
|
67
|
+
* When the next registry check is due. `random` is in [0, 1) and spreads relays apart; it is a
|
|
68
|
+
* parameter so a test can pin it.
|
|
69
|
+
*/
|
|
70
|
+
function nextCheckAt(timing, startedAt, random) {
|
|
71
|
+
const firstAfterStart = startedAt + FIRST_CHECK_MIN_MS + random * FIRST_CHECK_SPREAD_MS;
|
|
72
|
+
if (timing.lastCheckAt === undefined)
|
|
73
|
+
return firstAfterStart;
|
|
74
|
+
const failures = timing.checkFailures ?? 0;
|
|
75
|
+
const wait = failures > 0 ? Math.min(CHECK_RETRY_MS * 2 ** (failures - 1), exports.CHECK_INTERVAL_MS) : exports.CHECK_INTERVAL_MS + random * CHECK_SPREAD_MS;
|
|
76
|
+
return Math.max(timing.lastCheckAt + wait, firstAfterStart);
|
|
77
|
+
}
|
|
78
|
+
/** When installing `version` may be tried again after failing; 0 when nothing holds it back. */
|
|
79
|
+
function installAllowedAt(failure, version) {
|
|
80
|
+
if (!failure || failure.version !== version || failure.count < 1)
|
|
81
|
+
return 0;
|
|
82
|
+
return failure.at + Math.min(INSTALL_RETRY_MS * 2 ** (failure.count - 1), INSTALL_RETRY_MAX_MS);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Why the relay must not restart now, or undefined when it may. Fail closed on everything the
|
|
86
|
+
* relay can see: a restart is never worth an interrupted turn, a lost session command, or a card
|
|
87
|
+
* whose answer would arrive at a relay that is not there.
|
|
88
|
+
*/
|
|
89
|
+
function busyReason({ activity, cardsWaiting, dependents, now }) {
|
|
90
|
+
if (activity.deliveries.length)
|
|
91
|
+
return `delivering to ${activity.deliveries.join(", ")}`;
|
|
92
|
+
if (activity.headless.length)
|
|
93
|
+
return `a headless turn is running for ${activity.headless.join(", ")}`;
|
|
94
|
+
if (activity.acp.length)
|
|
95
|
+
return `an agent turn is running for ${activity.acp.join(", ")}`;
|
|
96
|
+
if (activity.launching)
|
|
97
|
+
return "a session is being started from BayChat";
|
|
98
|
+
if (cardsWaiting > 0)
|
|
99
|
+
return `${cardsWaiting} card(s) on the phone are waiting for an answer`;
|
|
100
|
+
if (activity.heldAt.some((at) => now - Date.parse(at) < exports.HELD_FRESH_MS))
|
|
101
|
+
return "a session is mid-turn with messages waiting for it";
|
|
102
|
+
if (activity.lastActivityAt !== undefined && now - activity.lastActivityAt < exports.QUIET_MS)
|
|
103
|
+
return "messages arrived in the last 10 minutes";
|
|
104
|
+
if (dependents.length)
|
|
105
|
+
return `processes the relay started are still running (pid ${dependents.slice(0, 3).join(", ")})`;
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
function clip(text, max) {
|
|
109
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
110
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.startRelayUpdater = startRelayUpdater;
|
|
4
|
+
const api_1 = require("../api");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
const self_restart_1 = require("../relay/self-restart");
|
|
7
|
+
const session_reply_1 = require("../relay/session-reply");
|
|
8
|
+
const self_upgrade_1 = require("../self-upgrade");
|
|
9
|
+
const refresh_clients_1 = require("../refresh-clients");
|
|
10
|
+
const waiting_cards_1 = require("../waiting-cards");
|
|
11
|
+
const policy_1 = require("./policy");
|
|
12
|
+
const store_1 = require("./store");
|
|
13
|
+
const updater_1 = require("./updater");
|
|
14
|
+
/**
|
|
15
|
+
* The automatic update wired to a real relay: npm, the disk, the device API for the card, and the
|
|
16
|
+
* init system for the restart. Everything that decides is in `updater.ts` and `policy.ts`; this
|
|
17
|
+
* file only connects it. Run by the SUPERVISING relay only — a per-Bay child restarts with it.
|
|
18
|
+
*/
|
|
19
|
+
function startRelayUpdater(opts) {
|
|
20
|
+
const supervisor = (0, self_restart_1.detectSupervisor)(self_restart_1.realSupervisorProbe);
|
|
21
|
+
const install = (0, self_upgrade_1.classifyInstall)((0, self_upgrade_1.currentEntry)());
|
|
22
|
+
const current = (0, self_upgrade_1.ownVersion)();
|
|
23
|
+
const deps = {
|
|
24
|
+
now: Date.now,
|
|
25
|
+
random: Math.random,
|
|
26
|
+
log: opts.log,
|
|
27
|
+
current,
|
|
28
|
+
ability: { install, supervised: supervisor !== undefined, root: process.getuid?.() === 0, platform: process.platform },
|
|
29
|
+
env: process.env,
|
|
30
|
+
setting: store_1.loadAutoSetting,
|
|
31
|
+
loadState: store_1.loadAutoUpdateState,
|
|
32
|
+
saveState: store_1.saveAutoUpdateState,
|
|
33
|
+
notify: store_1.writeUpdateNotice,
|
|
34
|
+
lookupLatest: () => (0, self_upgrade_1.lookupLatest)(self_upgrade_1.realNpm),
|
|
35
|
+
installVersion: (version) => installGlobal(install, version, current, opts.log),
|
|
36
|
+
busyReason: () => (0, policy_1.busyReason)({
|
|
37
|
+
activity: opts.daemon.activity(),
|
|
38
|
+
cardsWaiting: (0, waiting_cards_1.countWaitingCards)(),
|
|
39
|
+
dependents: supervisor === "systemd" ? (0, self_restart_1.processesEndedByRestart)(self_restart_1.realSupervisorProbe) : [],
|
|
40
|
+
now: Date.now(),
|
|
41
|
+
}),
|
|
42
|
+
restart: async () => restartUnder(supervisor, opts.shutdown),
|
|
43
|
+
refreshClients: async () => {
|
|
44
|
+
const device = (0, config_1.loadDeviceCredentials)();
|
|
45
|
+
if (device)
|
|
46
|
+
for (const line of (0, refresh_clients_1.refreshConnectedClients)(device, opts.log))
|
|
47
|
+
opts.log(`auto-update: ${line}`);
|
|
48
|
+
},
|
|
49
|
+
cards: deviceCards(opts.daemon, opts.log),
|
|
50
|
+
};
|
|
51
|
+
const updater = new updater_1.AutoUpdater(deps);
|
|
52
|
+
updater.start();
|
|
53
|
+
return updater;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Install `version` over the running global copy, holding the install lock. If npm failed in a way
|
|
57
|
+
* that left something other than the running version on disk, put the running version back, so a
|
|
58
|
+
* relay that restarts later (a reboot) still starts working code.
|
|
59
|
+
*/
|
|
60
|
+
async function installGlobal(install, version, current, log) {
|
|
61
|
+
if (install.kind !== "global")
|
|
62
|
+
return { ok: false, reason: `a ${install.kind} copy is never updated automatically` };
|
|
63
|
+
const unlock = (0, store_1.acquireUpdateLock)();
|
|
64
|
+
if (!unlock)
|
|
65
|
+
return { ok: false, reason: "another update of baychat is running" };
|
|
66
|
+
try {
|
|
67
|
+
const deps = { npm: self_upgrade_1.realNpm, readVersion: self_upgrade_1.readPackageVersion, install };
|
|
68
|
+
const installed = await (0, self_upgrade_1.installRelease)(deps, version);
|
|
69
|
+
if (installed.ok || (0, self_upgrade_1.readPackageVersion)(install.packageRoot) === current)
|
|
70
|
+
return installed;
|
|
71
|
+
const restored = await (0, self_upgrade_1.installRelease)(deps, current);
|
|
72
|
+
log(`auto-update: ${restored.ok ? `put ${current} back on disk` : `could not put ${current} back (${restored.reason})`}`);
|
|
73
|
+
return installed;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
unlock();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function restartUnder(supervisor, shutdown) {
|
|
80
|
+
// `mode` is never "auto" without a supervisor, so this is an invariant, not a branch.
|
|
81
|
+
if (!supervisor)
|
|
82
|
+
throw new Error("refusing to restart: no init system would start the relay again");
|
|
83
|
+
(0, self_restart_1.arrangeRestart)(supervisor);
|
|
84
|
+
shutdown();
|
|
85
|
+
}
|
|
86
|
+
/** The decision card, over the device API — the same endpoint the hooks use, owner-only answers. */
|
|
87
|
+
function deviceCards(daemon, log) {
|
|
88
|
+
const device = () => {
|
|
89
|
+
const creds = (0, config_1.loadDeviceCredentials)();
|
|
90
|
+
if (!creds)
|
|
91
|
+
throw new Error("this computer is not signed in to BayChat");
|
|
92
|
+
return creds;
|
|
93
|
+
};
|
|
94
|
+
return {
|
|
95
|
+
async ask(question) {
|
|
96
|
+
const session = daemon.hostedLiveSessions()[0];
|
|
97
|
+
if (!session)
|
|
98
|
+
return undefined; // A card goes to the private chat with a session; none is live.
|
|
99
|
+
try {
|
|
100
|
+
const reply = await (0, api_1.apiRequest)(device(), "POST", "/api/device-api/approvals", {
|
|
101
|
+
session,
|
|
102
|
+
question,
|
|
103
|
+
options: [...updater_1.UPDATE_OPTIONS],
|
|
104
|
+
direct: true,
|
|
105
|
+
wait: 1,
|
|
106
|
+
});
|
|
107
|
+
if (typeof reply.requestId !== "string" || !reply.requestId)
|
|
108
|
+
return undefined;
|
|
109
|
+
return {
|
|
110
|
+
requestId: reply.requestId,
|
|
111
|
+
conversationId: typeof reply.conversationId === "string" ? reply.conversationId : undefined,
|
|
112
|
+
session: typeof reply.sessionName === "string" ? reply.sessionName : session,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
log(`auto-update: could not put the update card up (${err instanceof Error ? err.message : String(err)})`);
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
async answer(requestId) {
|
|
121
|
+
try {
|
|
122
|
+
const reply = await (0, api_1.apiRequest)(device(), "GET", `/api/device-api/approvals/${encodeURIComponent(requestId)}?wait=1`);
|
|
123
|
+
if (reply.status === "waiting")
|
|
124
|
+
return { kind: "waiting" };
|
|
125
|
+
if (reply.status === "answered")
|
|
126
|
+
return { kind: reply.answerIndex === updater_1.UPDATE_NOW_INDEX ? "update" : "later" };
|
|
127
|
+
return { kind: "gone" };
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
// The card no longer exists for us: gone. A blip: ask again on the next poll.
|
|
131
|
+
if (err instanceof api_1.ApiError && (err.status === 404 || err.status === 403))
|
|
132
|
+
return { kind: "gone" };
|
|
133
|
+
return { kind: "waiting" };
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async withdraw(requestId) {
|
|
137
|
+
await (0, api_1.apiRequest)(device(), "POST", `/api/device-api/approvals/${encodeURIComponent(requestId)}/withdraw`, {});
|
|
138
|
+
},
|
|
139
|
+
async say(conversationId, session, text) {
|
|
140
|
+
await (0, session_reply_1.postSessionReply)(device(), conversationId, session, text);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadAutoUpdateState = loadAutoUpdateState;
|
|
37
|
+
exports.saveAutoUpdateState = saveAutoUpdateState;
|
|
38
|
+
exports.loadAutoSetting = loadAutoSetting;
|
|
39
|
+
exports.saveAutoSetting = saveAutoSetting;
|
|
40
|
+
exports.writeUpdateNotice = writeUpdateNotice;
|
|
41
|
+
exports.takeUpdateNotice = takeUpdateNotice;
|
|
42
|
+
exports.acquireUpdateLock = acquireUpdateLock;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const config_1 = require("../config");
|
|
46
|
+
const parent_watch_1 = require("../relay/parent-watch");
|
|
47
|
+
const STATE_FILE = "auto-update.json";
|
|
48
|
+
const SETTING_FILE = "auto-update-setting.json";
|
|
49
|
+
const NOTICE_FILE = "update-notice.json";
|
|
50
|
+
const LOCK_FILE = "update.lock";
|
|
51
|
+
function loadAutoUpdateState() {
|
|
52
|
+
const raw = readJson(STATE_FILE);
|
|
53
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
54
|
+
}
|
|
55
|
+
function saveAutoUpdateState(state) {
|
|
56
|
+
writeJsonAtomic(STATE_FILE, state);
|
|
57
|
+
}
|
|
58
|
+
function loadAutoSetting() {
|
|
59
|
+
const raw = readJson(SETTING_FILE);
|
|
60
|
+
return raw?.auto === "off" ? "off" : "on";
|
|
61
|
+
}
|
|
62
|
+
function saveAutoSetting(auto) {
|
|
63
|
+
writeJsonAtomic(SETTING_FILE, { auto });
|
|
64
|
+
}
|
|
65
|
+
/** Leave one line for the next `baychat` command to print. Replaces any earlier one. */
|
|
66
|
+
function writeUpdateNotice(text) {
|
|
67
|
+
writeJsonAtomic(NOTICE_FILE, { text, at: Date.now() });
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The waiting notice, taken: read and deleted, so it is printed exactly once. Undefined when
|
|
71
|
+
* there is none, or when it cannot be deleted (printing it again on every command would be worse
|
|
72
|
+
* than not printing it).
|
|
73
|
+
*/
|
|
74
|
+
function takeUpdateNotice() {
|
|
75
|
+
const file = path.join((0, config_1.configDir)(), NOTICE_FILE);
|
|
76
|
+
const raw = readJson(NOTICE_FILE);
|
|
77
|
+
if (typeof raw?.text !== "string" || !raw.text.trim())
|
|
78
|
+
return undefined;
|
|
79
|
+
try {
|
|
80
|
+
fs.rmSync(file);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return raw.text.trim();
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Hold the install lock: one npm install over this computer's baychat at a time, whether the
|
|
89
|
+
* relay or a person started it. Returns the release, or undefined when a live process holds it.
|
|
90
|
+
* A lock whose holder is gone (a crash) is taken over.
|
|
91
|
+
*/
|
|
92
|
+
function acquireUpdateLock(pid = process.pid) {
|
|
93
|
+
const file = path.join((0, config_1.configDir)(), LOCK_FILE);
|
|
94
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync((0, config_1.configDir)(), { recursive: true });
|
|
97
|
+
fs.writeFileSync(file, String(pid), { flag: "wx", mode: 0o600 });
|
|
98
|
+
return () => fs.rmSync(file, { force: true });
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (err.code !== "EEXIST")
|
|
102
|
+
return undefined;
|
|
103
|
+
const holder = Number(readText(file));
|
|
104
|
+
if (Number.isInteger(holder) && holder > 1 && holder !== pid && (0, parent_watch_1.processIsAlive)(holder))
|
|
105
|
+
return undefined;
|
|
106
|
+
// The holder is gone: the lock is stale. Removed and tried once more.
|
|
107
|
+
fs.rmSync(file, { force: true });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
function readJson(name) {
|
|
113
|
+
const text = readText(path.join((0, config_1.configDir)(), name));
|
|
114
|
+
if (text === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(text);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// A torn or hand-edited file reads as absent: every reader has a safe default.
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function readText(file) {
|
|
125
|
+
try {
|
|
126
|
+
return fs.readFileSync(file, "utf8");
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Missing is the ordinary first-run case; unreadable reads the same way.
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Write via a temporary file and a rename, so a reader never sees half a file. */
|
|
134
|
+
function writeJsonAtomic(name, value) {
|
|
135
|
+
const file = path.join((0, config_1.configDir)(), name);
|
|
136
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
137
|
+
fs.mkdirSync((0, config_1.configDir)(), { recursive: true });
|
|
138
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
|
|
139
|
+
fs.renameSync(tmp, file);
|
|
140
|
+
}
|