@jungjaehoon/mama-os 0.77.0 → 0.78.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/CHANGELOG.md +135 -0
- package/README.md +25 -10
- package/dist/api/security-events.d.ts.map +1 -1
- package/dist/api/security-events.js +10 -1
- package/dist/api/security-events.js.map +1 -1
- package/dist/api/viewer-server.d.ts +1 -0
- package/dist/api/viewer-server.d.ts.map +1 -1
- package/dist/api/viewer-server.js +34 -0
- package/dist/api/viewer-server.js.map +1 -1
- package/dist/api/work-actions.d.ts.map +1 -1
- package/dist/api/work-actions.js +27 -2
- package/dist/api/work-actions.js.map +1 -1
- package/dist/cli/commands/backfill.d.ts.map +1 -1
- package/dist/cli/commands/backfill.js +1 -0
- package/dist/cli/commands/backfill.js.map +1 -1
- package/dist/cli/commands/backup.d.ts +13 -0
- package/dist/cli/commands/backup.d.ts.map +1 -0
- package/dist/cli/commands/backup.js +137 -0
- package/dist/cli/commands/backup.js.map +1 -0
- package/dist/cli/commands/control.d.ts +26 -0
- package/dist/cli/commands/control.d.ts.map +1 -0
- package/dist/cli/commands/control.js +193 -0
- package/dist/cli/commands/control.js.map +1 -0
- package/dist/cli/commands/daemon.d.ts +49 -3
- package/dist/cli/commands/daemon.d.ts.map +1 -1
- package/dist/cli/commands/daemon.js +244 -74
- package/dist/cli/commands/daemon.js.map +1 -1
- package/dist/cli/commands/doctor.d.ts +13 -0
- package/dist/cli/commands/doctor.d.ts.map +1 -0
- package/dist/cli/commands/doctor.js +216 -0
- package/dist/cli/commands/doctor.js.map +1 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +3 -2
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/logs.d.ts +9 -0
- package/dist/cli/commands/logs.d.ts.map +1 -0
- package/dist/cli/commands/logs.js +109 -0
- package/dist/cli/commands/logs.js.map +1 -0
- package/dist/cli/commands/register-owner.d.ts.map +1 -1
- package/dist/cli/commands/register-owner.js +12 -0
- package/dist/cli/commands/register-owner.js.map +1 -1
- package/dist/cli/commands/restore.d.ts +14 -0
- package/dist/cli/commands/restore.d.ts.map +1 -0
- package/dist/cli/commands/restore.js +130 -0
- package/dist/cli/commands/restore.js.map +1 -0
- package/dist/cli/commands/service.d.ts +28 -0
- package/dist/cli/commands/service.d.ts.map +1 -0
- package/dist/cli/commands/service.js +278 -0
- package/dist/cli/commands/service.js.map +1 -0
- package/dist/cli/commands/uninstall.d.ts +7 -0
- package/dist/cli/commands/uninstall.d.ts.map +1 -0
- package/dist/cli/commands/uninstall.js +262 -0
- package/dist/cli/commands/uninstall.js.map +1 -0
- package/dist/cli/index.js +67 -20
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/launch-files.d.ts +83 -0
- package/dist/cli/launch-files.d.ts.map +1 -1
- package/dist/cli/launch-files.js +231 -15
- package/dist/cli/launch-files.js.map +1 -1
- package/dist/cli/runtime/backend-probe.d.ts +34 -0
- package/dist/cli/runtime/backend-probe.d.ts.map +1 -0
- package/dist/cli/runtime/backend-probe.js +83 -0
- package/dist/cli/runtime/backend-probe.js.map +1 -0
- package/dist/cli/runtime/backup.d.ts +114 -0
- package/dist/cli/runtime/backup.d.ts.map +1 -0
- package/dist/cli/runtime/backup.js +564 -0
- package/dist/cli/runtime/backup.js.map +1 -0
- package/dist/cli/runtime/daemon-control.d.ts +84 -0
- package/dist/cli/runtime/daemon-control.d.ts.map +1 -0
- package/dist/cli/runtime/daemon-control.js +266 -0
- package/dist/cli/runtime/daemon-control.js.map +1 -0
- package/dist/cli/runtime/diagnostics.d.ts +43 -0
- package/dist/cli/runtime/diagnostics.d.ts.map +1 -0
- package/dist/cli/runtime/diagnostics.js +953 -0
- package/dist/cli/runtime/diagnostics.js.map +1 -0
- package/dist/cli/runtime/restore.d.ts +98 -0
- package/dist/cli/runtime/restore.d.ts.map +1 -0
- package/dist/cli/runtime/restore.js +756 -0
- package/dist/cli/runtime/restore.js.map +1 -0
- package/dist/cli/runtime/systemd-service.d.ts +13 -0
- package/dist/cli/runtime/systemd-service.d.ts.map +1 -0
- package/dist/cli/runtime/systemd-service.js +32 -0
- package/dist/cli/runtime/systemd-service.js.map +1 -0
- package/dist/connectors/chatwork/index.js +1 -1
- package/dist/connectors/chatwork/index.js.map +1 -1
- package/dist/connectors/config-loader.d.ts +2 -1
- package/dist/connectors/config-loader.d.ts.map +1 -1
- package/dist/connectors/config-loader.js +3 -2
- package/dist/connectors/config-loader.js.map +1 -1
- package/dist/connectors/framework/polling-scheduler.d.ts.map +1 -1
- package/dist/connectors/framework/polling-scheduler.js +12 -11
- package/dist/connectors/framework/polling-scheduler.js.map +1 -1
- package/dist/connectors/imessage/index.d.ts.map +1 -1
- package/dist/connectors/imessage/index.js +2 -1
- package/dist/connectors/imessage/index.js.map +1 -1
- package/dist/connectors/slack/index.js +1 -1
- package/dist/connectors/slack/index.js.map +1 -1
- package/dist/runtime/backend-security.d.ts +3 -0
- package/dist/runtime/backend-security.d.ts.map +1 -1
- package/dist/runtime/backend-security.js +13 -5
- package/dist/runtime/backend-security.js.map +1 -1
- package/dist/runtime/config.d.ts +3 -0
- package/dist/runtime/config.d.ts.map +1 -1
- package/dist/runtime/config.js +4 -4
- package/dist/runtime/config.js.map +1 -1
- package/dist/runtime/connectors.d.ts +21 -1
- package/dist/runtime/connectors.d.ts.map +1 -1
- package/dist/runtime/connectors.js +28 -17
- package/dist/runtime/connectors.js.map +1 -1
- package/dist/runtime/core-db.d.ts.map +1 -1
- package/dist/runtime/core-db.js +2 -0
- package/dist/runtime/core-db.js.map +1 -1
- package/dist/runtime/owner-system-prompt.d.ts.map +1 -1
- package/dist/runtime/owner-system-prompt.js +7 -5
- package/dist/runtime/owner-system-prompt.js.map +1 -1
- package/dist/runtime/rotating-log.d.ts +44 -0
- package/dist/runtime/rotating-log.d.ts.map +1 -0
- package/dist/runtime/rotating-log.js +124 -0
- package/dist/runtime/rotating-log.js.map +1 -0
- package/dist/tools/document-toolkit.d.ts +2 -0
- package/dist/tools/document-toolkit.d.ts.map +1 -1
- package/dist/tools/document-toolkit.js +2 -0
- package/dist/tools/document-toolkit.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,953 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runDiagnostics = runDiagnostics;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_module_1 = require("node:module");
|
|
6
|
+
const node_net_1 = require("node:net");
|
|
7
|
+
const node_os_1 = require("node:os");
|
|
8
|
+
const node_path_1 = require("node:path");
|
|
9
|
+
const viewer_server_js_1 = require("../../api/viewer-server.js");
|
|
10
|
+
const index_js_1 = require("../../connectors/index.js");
|
|
11
|
+
const config_js_1 = require("../../runtime/config.js");
|
|
12
|
+
const backend_security_js_1 = require("../../runtime/backend-security.js");
|
|
13
|
+
const connectors_js_1 = require("../../runtime/connectors.js");
|
|
14
|
+
const timezone_js_1 = require("../../runtime/timezone.js");
|
|
15
|
+
const document_toolkit_js_1 = require("../../tools/document-toolkit.js");
|
|
16
|
+
const launch_files_js_1 = require("../launch-files.js");
|
|
17
|
+
const rotating_log_js_1 = require("../../runtime/rotating-log.js");
|
|
18
|
+
const secrets_js_1 = require("../secrets.js");
|
|
19
|
+
const daemon_js_1 = require("../commands/daemon.js");
|
|
20
|
+
const backend_probe_js_1 = require("./backend-probe.js");
|
|
21
|
+
const daemon_control_js_1 = require("./daemon-control.js");
|
|
22
|
+
const db_manager_1 = require("@jungjaehoon/mama-core/db-manager");
|
|
23
|
+
const migrations_source_js_1 = require("../../storage/migrations-source.js");
|
|
24
|
+
const systemd_service_js_1 = require("./systemd-service.js");
|
|
25
|
+
/** The PATH a service manager gives a unit that does not set one ($PATH in start.sh). */
|
|
26
|
+
const SERVICE_DEFAULT_PATH = {
|
|
27
|
+
darwin: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'],
|
|
28
|
+
linux: ['/usr/local/sbin', '/usr/local/bin', '/usr/sbin', '/usr/bin', '/sbin', '/bin'],
|
|
29
|
+
};
|
|
30
|
+
/** systemd states of an installed unit that can be started or is running. */
|
|
31
|
+
const SYSTEMD_USABLE = new Set(['active', 'inactive', 'activating', 'deactivating', 'reloading']);
|
|
32
|
+
function findIn(name, dirs) {
|
|
33
|
+
for (const dir of dirs) {
|
|
34
|
+
const path = (0, node_path_1.join)(dir, name);
|
|
35
|
+
if ((0, node_fs_1.existsSync)(path))
|
|
36
|
+
return path;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
function message(error) {
|
|
41
|
+
return error instanceof Error ? error.message : String(error);
|
|
42
|
+
}
|
|
43
|
+
/** auth.env's assignments, unquoted as sh reads them; values are never printed. */
|
|
44
|
+
function readAuthEnv(root) {
|
|
45
|
+
const path = (0, node_path_1.join)(root, 'auth.env');
|
|
46
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
47
|
+
return {};
|
|
48
|
+
const values = {};
|
|
49
|
+
(0, node_fs_1.readFileSync)(path, 'utf8')
|
|
50
|
+
.split('\n')
|
|
51
|
+
.forEach((line, index) => {
|
|
52
|
+
if (line.trim() === '' || line.trim().startsWith('#'))
|
|
53
|
+
return;
|
|
54
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)=(.*)$/.exec(line);
|
|
55
|
+
// start.sh sources the file under `set -e`, so any other line runs as a command.
|
|
56
|
+
if (!match)
|
|
57
|
+
throw new Error(`line ${index + 1} is not an assignment`);
|
|
58
|
+
try {
|
|
59
|
+
values[match[1]] = (0, launch_files_js_1.shellWords)(match[2])[0] ?? '';
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
throw new Error(`line ${index + 1}: ${message(error)}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
return values;
|
|
66
|
+
}
|
|
67
|
+
async function defaultTelegramCall(token, method, params = {}) {
|
|
68
|
+
const query = new URLSearchParams(params).toString();
|
|
69
|
+
const response = await fetch(`https://api.telegram.org/bot${token}/${method}${query ? `?${query}` : ''}`, { signal: AbortSignal.timeout(8_000) });
|
|
70
|
+
return (await response.json());
|
|
71
|
+
}
|
|
72
|
+
/** True when something listens; a refusal means free; any other error (a host that cannot be
|
|
73
|
+
* reached or bound) rejects, since the viewer would fail on it too. */
|
|
74
|
+
function portAnswers(port, host) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const socket = (0, node_net_1.connect)({ port, host });
|
|
77
|
+
socket.setTimeout(1_000, () => {
|
|
78
|
+
socket.destroy();
|
|
79
|
+
resolve(false);
|
|
80
|
+
});
|
|
81
|
+
socket.once('connect', () => {
|
|
82
|
+
socket.destroy();
|
|
83
|
+
resolve(true);
|
|
84
|
+
});
|
|
85
|
+
socket.once('error', (error) => error.code === 'ECONNREFUSED' ? resolve(false) : reject(error));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function secretFix(name) {
|
|
89
|
+
return secrets_js_1.SECRET_NAMES.includes(name) || (0, secrets_js_1.isIcalSecret)(name)
|
|
90
|
+
? `mama secret set ${name}`
|
|
91
|
+
: `add ${name} to auth.env`;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Check an install as its service would run it. Nothing here writes MAMA's files or starts a
|
|
95
|
+
* service; the database is opened read-only (which can leave its -wal and -shm beside it), and the
|
|
96
|
+
* backend's own status command may update the backend's config.
|
|
97
|
+
* Each check names what it found and, when it is not ok, what to do.
|
|
98
|
+
*/
|
|
99
|
+
async function runDiagnostics(options = {}) {
|
|
100
|
+
const home = options.home ?? (0, node_os_1.homedir)();
|
|
101
|
+
const configPath = options.configPath ?? (0, node_path_1.join)(home, '.mama', 'config.yaml');
|
|
102
|
+
const root = (0, node_path_1.dirname)(configPath);
|
|
103
|
+
const platform = options.platform ?? process.platform;
|
|
104
|
+
const run = options.run ?? backend_probe_js_1.defaultProbeRun;
|
|
105
|
+
const locate = options.findIn ?? findIn;
|
|
106
|
+
const checks = [];
|
|
107
|
+
if ((0, node_fs_1.existsSync)((0, daemon_control_js_1.restoreJournalPath)(root))) {
|
|
108
|
+
checks.push({
|
|
109
|
+
name: 'restore',
|
|
110
|
+
status: 'fail',
|
|
111
|
+
detail: 'an interrupted restore is unfinished; boot refuses until it is resolved',
|
|
112
|
+
fix: 'mama restore --rollback',
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
let config = null;
|
|
116
|
+
// Setting names can be anything the owner typed, so they stay out of the report.
|
|
117
|
+
let ignored = [];
|
|
118
|
+
try {
|
|
119
|
+
config = (0, config_js_1.loadConfig)({ home, path: configPath, onIgnored: (names) => (ignored = names) });
|
|
120
|
+
(0, daemon_js_1.validateDeliveryRoutes)(config);
|
|
121
|
+
(0, daemon_js_1.pathsFor)(configPath, config);
|
|
122
|
+
checks.push({
|
|
123
|
+
name: 'config',
|
|
124
|
+
status: 'ok',
|
|
125
|
+
detail: ignored.length > 0 ? `loads; ${ignored.length} setting(s) it ignores` : 'loads',
|
|
126
|
+
context: [configPath, ...(ignored.length > 0 ? [`ignored: ${ignored.join(', ')}`] : [])].join('; '),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
checks.push({
|
|
131
|
+
name: 'config',
|
|
132
|
+
status: 'fail',
|
|
133
|
+
detail: 'does not load as boot reads it',
|
|
134
|
+
context: message(error),
|
|
135
|
+
fix: `correct ${configPath}; the daemon refuses to boot until this passes`,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (config)
|
|
139
|
+
checks.push(schemaCheck(config.database.path));
|
|
140
|
+
if (config) {
|
|
141
|
+
checks.push(config.member_root
|
|
142
|
+
? { name: 'member_root', status: 'ok', detail: 'set', context: config.member_root }
|
|
143
|
+
: {
|
|
144
|
+
name: 'member_root',
|
|
145
|
+
status: 'warn',
|
|
146
|
+
detail: 'not set; the daemon needs it once a person is registered',
|
|
147
|
+
fix: 'set member_root to an absolute folder outside HOME in config.yaml',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
// What the service runs: the start script's Node, PATH and exports, not this shell's.
|
|
151
|
+
const startPath = (0, node_path_1.join)(root, 'start.sh');
|
|
152
|
+
let servicePath = SERVICE_DEFAULT_PATH[platform] ?? [];
|
|
153
|
+
let exports = {};
|
|
154
|
+
// A script mama did not write is assumed to load auth.env; a generated one must still.
|
|
155
|
+
let loadsAuth = true;
|
|
156
|
+
if (!(0, node_fs_1.existsSync)(startPath)) {
|
|
157
|
+
checks.push({
|
|
158
|
+
name: 'start.sh',
|
|
159
|
+
status: 'fail',
|
|
160
|
+
detail: 'does not exist',
|
|
161
|
+
fix: 'mama service install writes it',
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
// launchd and systemd run the script itself.
|
|
166
|
+
try {
|
|
167
|
+
(0, node_fs_1.accessSync)(startPath, node_fs_1.constants.X_OK);
|
|
168
|
+
}
|
|
169
|
+
catch {
|
|
170
|
+
checks.push({
|
|
171
|
+
name: 'start.sh',
|
|
172
|
+
status: 'fail',
|
|
173
|
+
detail: 'is not executable, so the service cannot run it',
|
|
174
|
+
fix: `chmod 700 ${startPath}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const reading = (0, launch_files_js_1.readStartScript)((0, node_fs_1.readFileSync)(startPath, 'utf8'), home);
|
|
179
|
+
exports = (0, launch_files_js_1.startExports)(reading);
|
|
180
|
+
if (reading.generated && !reading.loadsAuthEnv) {
|
|
181
|
+
loadsAuth = false;
|
|
182
|
+
checks.push({
|
|
183
|
+
name: 'start.sh',
|
|
184
|
+
status: 'fail',
|
|
185
|
+
detail: 'no longer loads auth.env, so the daemon gets none of its secrets',
|
|
186
|
+
fix: 'mama service install --replace rewrites it',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
if (reading.path !== null) {
|
|
190
|
+
servicePath = [
|
|
191
|
+
...(0, launch_files_js_1.startPathValue)(reading.path, home),
|
|
192
|
+
...(/\$\{?PATH\}?/.test(reading.path) ? servicePath : []),
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
const [, node, cli] = reading.exec === null ? [] : (0, launch_files_js_1.shellWords)(reading.exec);
|
|
196
|
+
if (!reading.generated) {
|
|
197
|
+
checks.push({
|
|
198
|
+
name: 'start.sh',
|
|
199
|
+
status: 'warn',
|
|
200
|
+
detail: 'not written by mama; its PATH and exports are read, its Node is not checked',
|
|
201
|
+
fix: 'mama service install shows what it would change; --replace rewrites it',
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
else if (node === undefined || cli === undefined) {
|
|
205
|
+
checks.push({
|
|
206
|
+
name: 'start.sh',
|
|
207
|
+
status: 'fail',
|
|
208
|
+
detail: 'has no exec line naming Node and the MAMA command',
|
|
209
|
+
fix: 'mama service install --replace rewrites it',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
checks.push(nodeCheck(node, cli, run));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
checks.push({
|
|
218
|
+
name: 'start.sh',
|
|
219
|
+
status: 'fail',
|
|
220
|
+
detail: 'does not parse',
|
|
221
|
+
context: message(error),
|
|
222
|
+
fix: 'mama service install --replace rewrites it',
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// start.sh loads auth.env with `set -a`, then its own exports override it.
|
|
227
|
+
let auth = {};
|
|
228
|
+
try {
|
|
229
|
+
if (loadsAuth)
|
|
230
|
+
auth = readAuthEnv(root);
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
checks.push({
|
|
234
|
+
name: 'auth.env',
|
|
235
|
+
status: 'fail',
|
|
236
|
+
detail: 'does not parse; start.sh stops when it loads it',
|
|
237
|
+
context: message(error),
|
|
238
|
+
fix: `correct the quoting in ${(0, node_path_1.join)(root, 'auth.env')}`,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
const serviceEnv = {
|
|
242
|
+
HOME: home,
|
|
243
|
+
USER: process.env.USER,
|
|
244
|
+
LOGNAME: process.env.LOGNAME,
|
|
245
|
+
...auth,
|
|
246
|
+
...exports,
|
|
247
|
+
PATH: servicePath.join(':'),
|
|
248
|
+
};
|
|
249
|
+
const secret = (name) => Boolean(serviceEnv[name]?.trim());
|
|
250
|
+
if (config) {
|
|
251
|
+
const backend = config.agent.backend;
|
|
252
|
+
const executable = locate(backend, servicePath);
|
|
253
|
+
if (!executable) {
|
|
254
|
+
checks.push({
|
|
255
|
+
name: 'backend',
|
|
256
|
+
status: 'fail',
|
|
257
|
+
detail: `${backend} is not on the service's PATH`,
|
|
258
|
+
fix: `install ${backend}, then run mama service install so start.sh includes it`,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
// The daemon's backend gets its environment without credential-shaped variables.
|
|
263
|
+
const env = (0, backend_security_js_1.backendEnvironment)(serviceEnv);
|
|
264
|
+
const codexHome = config.agent.codex_home ?? (0, node_path_1.join)(root, '.codex');
|
|
265
|
+
const probe = (0, backend_probe_js_1.probeBackend)(backend, executable, { codexHome, run, env });
|
|
266
|
+
if (probe.problem || !probe.versionOk) {
|
|
267
|
+
checks.push({
|
|
268
|
+
name: 'backend',
|
|
269
|
+
status: 'fail',
|
|
270
|
+
detail: probe.problem ?? `${backend} ${probe.version} is older than ${probe.minimum}`,
|
|
271
|
+
fix: `update ${backend} to ${probe.minimum} or newer`,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
else if (!probe.loggedIn) {
|
|
275
|
+
checks.push({
|
|
276
|
+
name: 'backend',
|
|
277
|
+
status: 'fail',
|
|
278
|
+
detail: `${backend} ${probe.version} is not signed in for the service`,
|
|
279
|
+
fix: backend === 'claude' ? 'claude auth login' : `CODEX_HOME='${codexHome}' codex login`,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
checks.push({
|
|
284
|
+
name: 'backend',
|
|
285
|
+
status: 'ok',
|
|
286
|
+
detail: `${backend} ${probe.version}, signed in`,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// The shell sandbox is required, so it must actually start, not only exist.
|
|
291
|
+
if (backend === 'claude')
|
|
292
|
+
checks.push(sandboxCheck(platform, servicePath, locate, run));
|
|
293
|
+
for (const messenger of ['telegram', 'discord', 'slack']) {
|
|
294
|
+
if (!config[messenger]?.enabled)
|
|
295
|
+
continue;
|
|
296
|
+
const missing = daemon_js_1.MESSENGER_SECRETS[messenger].filter((name) => !secret(name));
|
|
297
|
+
if (missing.length > 0) {
|
|
298
|
+
checks.push({
|
|
299
|
+
name: messenger,
|
|
300
|
+
status: 'fail',
|
|
301
|
+
detail: `${missing.join(' and ')} not set; boot stops at the messengers stage`,
|
|
302
|
+
fix: missing.map(secretFix).join('; '),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
else if (messenger === 'telegram' && !options.offline) {
|
|
306
|
+
checks.push(await telegramCheck(serviceEnv.MAMA_TELEGRAM_TOKEN, config.telegram.owner_chat_id, options.telegramCall ?? defaultTelegramCall));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
checks.push({ name: messenger, status: 'ok', detail: 'secrets set' });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
checks.push(...(await sourceChecks(config, configPath, servicePath, locate, secret, {
|
|
313
|
+
env: serviceEnv,
|
|
314
|
+
online: !options.offline,
|
|
315
|
+
load: options.loadSource ?? index_js_1.loadConnector,
|
|
316
|
+
timeoutMs: options.sourceTimeoutMs ?? SOURCE_PROBE_MS,
|
|
317
|
+
})));
|
|
318
|
+
checks.push(toolkitCheck(config));
|
|
319
|
+
}
|
|
320
|
+
let port = null;
|
|
321
|
+
try {
|
|
322
|
+
port = (0, viewer_server_js_1.resolveApiPort)(serviceEnv.MAMA_API_PORT);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
checks.push({
|
|
326
|
+
name: 'port',
|
|
327
|
+
status: 'fail',
|
|
328
|
+
detail: 'MAMA_API_PORT is not a usable port',
|
|
329
|
+
context: message(error),
|
|
330
|
+
fix: 'set MAMA_API_PORT in start.sh to an integer between 1024 and 65535',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
checks.push(...(await daemonChecks(options.askStatus ?? (() => (0, daemon_js_1.askDaemon)('status', { home, configPath })), port, serviceEnv.MAMA_API_HOST ?? '127.0.0.1', options.portInUse ?? portAnswers)));
|
|
334
|
+
checks.push(serviceCheck(platform, home, options.serviceRun ?? systemd_service_js_1.defaultServiceRun));
|
|
335
|
+
// Each log against its own cap: the daemon rotates daemon.log and security-events.jsonl before
|
|
336
|
+
// they pass theirs, and start.sh empties crash.log at start past its own.
|
|
337
|
+
for (const [name, path, cap] of [
|
|
338
|
+
// The daemon's own log is wherever the config puts it.
|
|
339
|
+
[
|
|
340
|
+
'daemon.log',
|
|
341
|
+
config?.logging.file ?? (0, node_path_1.join)(root, 'logs', 'daemon.log'),
|
|
342
|
+
rotating_log_js_1.DAEMON_LOG_LIMITS.maxBytes,
|
|
343
|
+
],
|
|
344
|
+
[
|
|
345
|
+
'security-events.jsonl',
|
|
346
|
+
(0, node_path_1.join)(root, 'logs', 'security-events.jsonl'),
|
|
347
|
+
rotating_log_js_1.SECURITY_LOG_LIMITS.maxBytes,
|
|
348
|
+
],
|
|
349
|
+
[launch_files_js_1.CRASH_LOG_NAME, (0, node_path_1.join)(root, 'logs', launch_files_js_1.CRASH_LOG_NAME), launch_files_js_1.CRASH_LOG_CAP_BYTES],
|
|
350
|
+
]) {
|
|
351
|
+
if (!(0, node_fs_1.existsSync)(path))
|
|
352
|
+
continue;
|
|
353
|
+
try {
|
|
354
|
+
(0, node_fs_1.accessSync)(path, node_fs_1.constants.R_OK);
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
checks.push({
|
|
358
|
+
name: 'logs',
|
|
359
|
+
status: 'warn',
|
|
360
|
+
detail: `${name} cannot be read`,
|
|
361
|
+
context: message(error),
|
|
362
|
+
fix: `give ${name} back to the user that runs MAMA`,
|
|
363
|
+
});
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
const size = (0, node_fs_1.statSync)(path).size;
|
|
367
|
+
if (size > cap) {
|
|
368
|
+
checks.push({
|
|
369
|
+
name: 'logs',
|
|
370
|
+
status: 'warn',
|
|
371
|
+
detail: `${name} is ${(size / 1024 / 1024).toFixed(1)} MB, past its ${cap / 1024 / 1024} MB cap`,
|
|
372
|
+
fix: name === launch_files_js_1.CRASH_LOG_NAME
|
|
373
|
+
? 'start.sh empties it at the next start; read it first, it holds what the daemon could not write to its own log'
|
|
374
|
+
: 'either an older daemon writes it without rotating (restart the daemon on this version) or its rotation fails, and the log says why',
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return checks;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* The database's schema against what this version knows, per migration source, read-only. A
|
|
382
|
+
* newer one, or a source this version does not know, is what boot refuses; an older one migrates
|
|
383
|
+
* at the next start.
|
|
384
|
+
*/
|
|
385
|
+
function schemaCheck(databasePath) {
|
|
386
|
+
try {
|
|
387
|
+
(0, node_fs_1.statSync)(databasePath);
|
|
388
|
+
}
|
|
389
|
+
catch (error) {
|
|
390
|
+
// Only a confirmed absence is a fresh install; a path the service cannot reach is not.
|
|
391
|
+
if (error.code === 'ENOENT') {
|
|
392
|
+
return {
|
|
393
|
+
name: 'schema',
|
|
394
|
+
status: 'ok',
|
|
395
|
+
detail: 'no database yet; the first start creates it',
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return {
|
|
399
|
+
name: 'schema',
|
|
400
|
+
status: 'fail',
|
|
401
|
+
detail: 'the database cannot be read',
|
|
402
|
+
context: message(error),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
// The same read boot makes before it connects, so the two cannot disagree.
|
|
407
|
+
const states = (0, db_manager_1.readSchemaState)(databasePath, [
|
|
408
|
+
{ name: db_manager_1.CORE_MIGRATION_SOURCE, dir: db_manager_1.MIGRATIONS_DIR },
|
|
409
|
+
migrations_source_js_1.STANDALONE_MIGRATION_SOURCE,
|
|
410
|
+
]);
|
|
411
|
+
const summary = states
|
|
412
|
+
.map((state) => `${state.name} ${state.stored} of ${state.highest}`)
|
|
413
|
+
.join(', ');
|
|
414
|
+
if (states.some((state) => state.stored > state.highest)) {
|
|
415
|
+
return {
|
|
416
|
+
name: 'schema',
|
|
417
|
+
status: 'fail',
|
|
418
|
+
detail: `the database is newer than this MAMA (${summary}); boot refuses it`,
|
|
419
|
+
fix: 'install the version that wrote it, or mama restore a backup taken with this one',
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
const pending = states.reduce((sum, state) => sum + (state.highest - state.stored), 0);
|
|
423
|
+
return {
|
|
424
|
+
name: 'schema',
|
|
425
|
+
status: 'ok',
|
|
426
|
+
detail: pending > 0 ? `${summary}; the next start migrates it` : summary,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
catch (error) {
|
|
430
|
+
// A file that is not a database fails at its first query, not at open.
|
|
431
|
+
return {
|
|
432
|
+
name: 'schema',
|
|
433
|
+
status: 'fail',
|
|
434
|
+
detail: 'the database cannot be read',
|
|
435
|
+
context: message(error),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
/** start.sh's Node: still there, outside one version's folder, and able to load MAMA's SQLite. */
|
|
440
|
+
function nodeCheck(node, cli, run) {
|
|
441
|
+
const rewrite = 'mama service install rewrites start.sh with a Node path that survives upgrades';
|
|
442
|
+
if (!(0, node_fs_1.existsSync)(node)) {
|
|
443
|
+
return {
|
|
444
|
+
name: 'node',
|
|
445
|
+
status: 'fail',
|
|
446
|
+
detail: 'the Node start.sh runs is gone',
|
|
447
|
+
context: node,
|
|
448
|
+
fix: rewrite,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
if (!(0, node_fs_1.existsSync)(cli)) {
|
|
452
|
+
return {
|
|
453
|
+
name: 'node',
|
|
454
|
+
status: 'fail',
|
|
455
|
+
detail: 'the MAMA command start.sh runs is gone',
|
|
456
|
+
context: cli,
|
|
457
|
+
fix: 'mama service install from the current install rewrites start.sh',
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
// A Node upgrade across a major version cannot load a native module built for the old one.
|
|
461
|
+
let binding;
|
|
462
|
+
try {
|
|
463
|
+
binding = (0, node_path_1.dirname)((0, node_module_1.createRequire)(cli).resolve('better-sqlite3/package.json'));
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
return {
|
|
467
|
+
name: 'node',
|
|
468
|
+
status: 'fail',
|
|
469
|
+
detail: 'the MAMA command start.sh runs has no SQLite module installed',
|
|
470
|
+
context: message(error),
|
|
471
|
+
fix: 'reinstall MAMA',
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
const load = run(node, [
|
|
475
|
+
'-e',
|
|
476
|
+
"new (require(process.argv[1]))(':memory:').close(); process.stdout.write(process.version)",
|
|
477
|
+
binding,
|
|
478
|
+
]);
|
|
479
|
+
const version = /^v\d+\.\d+\.\d+$/.test(`${load.stdout ?? ''}`) ? `${load.stdout}` : 'Node';
|
|
480
|
+
if (load.error || load.status !== 0) {
|
|
481
|
+
return {
|
|
482
|
+
name: 'node',
|
|
483
|
+
status: 'fail',
|
|
484
|
+
detail: 'the Node start.sh runs cannot load the SQLite module MAMA was installed with',
|
|
485
|
+
context: node,
|
|
486
|
+
fix: 'reinstall MAMA with this Node, or point start.sh at the Node it was installed with',
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
if ((0, launch_files_js_1.versionedNodePath)(node)) {
|
|
490
|
+
return {
|
|
491
|
+
name: 'node',
|
|
492
|
+
status: 'warn',
|
|
493
|
+
detail: `start.sh runs ${version} from one version's folder; an upgrade or a switch removes it`,
|
|
494
|
+
context: node,
|
|
495
|
+
fix: node.includes('/Cellar/')
|
|
496
|
+
? rewrite
|
|
497
|
+
: 'run mama service install from a shell whose node is not an nvm, volta or fnm version',
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
return { name: 'node', status: 'ok', detail: `${version} loads SQLite`, context: node };
|
|
501
|
+
}
|
|
502
|
+
function sandboxCheck(platform, servicePath, locate, run) {
|
|
503
|
+
if (platform === 'darwin') {
|
|
504
|
+
const result = run('/usr/bin/sandbox-exec', [
|
|
505
|
+
'-p',
|
|
506
|
+
'(version 1)(allow default)',
|
|
507
|
+
'/usr/bin/true',
|
|
508
|
+
]);
|
|
509
|
+
return result.error || result.status !== 0
|
|
510
|
+
? {
|
|
511
|
+
name: 'sandbox',
|
|
512
|
+
status: 'fail',
|
|
513
|
+
detail: 'sandbox-exec could not start a sandbox',
|
|
514
|
+
fix: 'sandbox-exec ships with macOS; check that nothing blocks it',
|
|
515
|
+
}
|
|
516
|
+
: { name: 'sandbox', status: 'ok', detail: 'starts' };
|
|
517
|
+
}
|
|
518
|
+
// Claude's Linux sandbox runs bwrap without network and relays through socat.
|
|
519
|
+
const bwrap = locate('bwrap', servicePath);
|
|
520
|
+
const socat = locate('socat', servicePath);
|
|
521
|
+
if (!bwrap || !socat) {
|
|
522
|
+
return {
|
|
523
|
+
name: 'sandbox',
|
|
524
|
+
status: 'fail',
|
|
525
|
+
detail: `${[!bwrap && 'bwrap', !socat && 'socat'].filter(Boolean).join(' and ')} not on the service's PATH`,
|
|
526
|
+
fix: 'install bubblewrap and socat',
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const relay = run(socat, ['-V']);
|
|
530
|
+
if (relay.error || relay.status !== 0) {
|
|
531
|
+
return {
|
|
532
|
+
name: 'sandbox',
|
|
533
|
+
status: 'fail',
|
|
534
|
+
detail: 'socat does not run',
|
|
535
|
+
context: socat,
|
|
536
|
+
fix: 'reinstall socat',
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
const result = run(bwrap, ['--ro-bind', '/', '/', '--dev', '/dev', '--unshare-net', '/bin/true']);
|
|
540
|
+
return result.error || result.status !== 0
|
|
541
|
+
? {
|
|
542
|
+
name: 'sandbox',
|
|
543
|
+
status: 'fail',
|
|
544
|
+
detail: 'bwrap could not start a sandbox without network',
|
|
545
|
+
fix: 'allow unprivileged user namespaces (kernel.unprivileged_userns_clone, or the AppArmor userns rule)',
|
|
546
|
+
}
|
|
547
|
+
: { name: 'sandbox', status: 'ok', detail: 'starts' };
|
|
548
|
+
}
|
|
549
|
+
async function telegramCheck(token, commonChat, call) {
|
|
550
|
+
let me;
|
|
551
|
+
try {
|
|
552
|
+
const reply = await call(token ?? '', 'getMe');
|
|
553
|
+
me = reply.ok === true ? reply.result : undefined;
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
return {
|
|
557
|
+
name: 'telegram',
|
|
558
|
+
status: 'warn',
|
|
559
|
+
detail: `could not reach Telegram (${error instanceof Error ? error.name : 'error'})`,
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
if (!me) {
|
|
563
|
+
return {
|
|
564
|
+
name: 'telegram',
|
|
565
|
+
status: 'fail',
|
|
566
|
+
detail: 'Telegram refused the bot token',
|
|
567
|
+
fix: 'mama secret set MAMA_TELEGRAM_TOKEN with the token BotFather gave',
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (commonChat === undefined || commonChat === '') {
|
|
571
|
+
return { name: 'telegram', status: 'ok', detail: 'token valid; no common group configured' };
|
|
572
|
+
}
|
|
573
|
+
// Removed from the group, the bot hears nothing there whatever its privacy setting.
|
|
574
|
+
let status;
|
|
575
|
+
try {
|
|
576
|
+
const member = await call(token ?? '', 'getChatMember', {
|
|
577
|
+
chat_id: String(commonChat),
|
|
578
|
+
user_id: String(me.id),
|
|
579
|
+
});
|
|
580
|
+
if (member.ok !== true && member.error_code !== 400 && member.error_code !== 403) {
|
|
581
|
+
return {
|
|
582
|
+
name: 'telegram',
|
|
583
|
+
status: 'warn',
|
|
584
|
+
detail: `token valid; Telegram did not answer about the common group (${String(member.error_code)})`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
status =
|
|
588
|
+
member.ok === true ? member.result?.status : undefined;
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
return {
|
|
592
|
+
name: 'telegram',
|
|
593
|
+
status: 'warn',
|
|
594
|
+
detail: `token valid; could not ask about the common group (${error instanceof Error ? error.name : 'error'})`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
if (typeof status !== 'string' || status === 'left' || status === 'kicked') {
|
|
598
|
+
return {
|
|
599
|
+
name: 'telegram',
|
|
600
|
+
status: 'fail',
|
|
601
|
+
detail: 'the bot is not in the common group',
|
|
602
|
+
fix: 'add the bot to the group owner_chat_id names',
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
const admin = status === 'administrator' || status === 'creator';
|
|
606
|
+
if (me.can_read_all_group_messages !== true && !admin) {
|
|
607
|
+
return {
|
|
608
|
+
name: 'telegram',
|
|
609
|
+
status: 'warn',
|
|
610
|
+
detail: 'the bot cannot read group messages',
|
|
611
|
+
fix: 'turn privacy off for the bot in BotFather, then remove and re-add it to the group',
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
name: 'telegram',
|
|
616
|
+
status: 'ok',
|
|
617
|
+
detail: admin
|
|
618
|
+
? 'token valid; an administrator of the common group, so it reads its messages'
|
|
619
|
+
: 'token valid; a member of the common group that reads its messages',
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
/** connectors.json as the connector runtime reads it, and what each enabled source needs. */
|
|
623
|
+
async function sourceChecks(config, configPath, servicePath, locate, secret, start) {
|
|
624
|
+
const paths = (0, daemon_js_1.pathsFor)(configPath, config);
|
|
625
|
+
let plan;
|
|
626
|
+
try {
|
|
627
|
+
plan = (0, connectors_js_1.planConnectors)({
|
|
628
|
+
configPath: paths.connectorsConfigPath,
|
|
629
|
+
// Channel keys can be owner names; they stay out of the terminal and the report.
|
|
630
|
+
onIgnored: () => { },
|
|
631
|
+
...(config.wiki?.enabled
|
|
632
|
+
? { wikiRoot: (0, node_path_1.resolve)(config.wiki.vaultPath, config.wiki.wikiDir) }
|
|
633
|
+
: {}),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
catch (error) {
|
|
637
|
+
return [
|
|
638
|
+
{
|
|
639
|
+
name: 'sources',
|
|
640
|
+
status: 'fail',
|
|
641
|
+
detail: 'connectors.json does not load as boot reads it',
|
|
642
|
+
context: message(error),
|
|
643
|
+
fix: `correct ${paths.connectorsConfigPath}`,
|
|
644
|
+
},
|
|
645
|
+
];
|
|
646
|
+
}
|
|
647
|
+
const checks = [];
|
|
648
|
+
const timeZone = (0, timezone_js_1.createTimeZoneSetting)(config.timezone);
|
|
649
|
+
for (const name of plan.enabledConnectorNames) {
|
|
650
|
+
let connector;
|
|
651
|
+
try {
|
|
652
|
+
connector = await start.load(name, plan.config.config[name], (0, connectors_js_1.connectorLoadPaths)(name, {
|
|
653
|
+
statePath: paths.connectorsRoot,
|
|
654
|
+
kagemushaDbPath: paths.kagemushaDbPath,
|
|
655
|
+
timeZone,
|
|
656
|
+
}));
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
checks.push({
|
|
660
|
+
name: `source ${name}`,
|
|
661
|
+
status: 'fail',
|
|
662
|
+
detail: 'cannot be built from connectors.json; boot stops at the connectors stage',
|
|
663
|
+
context: message(error),
|
|
664
|
+
});
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
const needs = connector.getAuthRequirements();
|
|
668
|
+
// A secret's name can carry an owner's channel key (iCal), so it stays out of the report.
|
|
669
|
+
const secrets = needs.flatMap((need) => need.type === 'token' && need.tokenName && !secret(need.tokenName) ? [need.tokenName] : []);
|
|
670
|
+
const commands = needs.flatMap((need) => need.type === 'cli' && need.cli && !locate(need.cli, servicePath)
|
|
671
|
+
? [{ cli: need.cli, login: need.cliAuthCommand }]
|
|
672
|
+
: []);
|
|
673
|
+
if (secrets.length > 0 || commands.length > 0) {
|
|
674
|
+
checks.push({
|
|
675
|
+
name: `source ${name}`,
|
|
676
|
+
status: 'fail',
|
|
677
|
+
detail: [
|
|
678
|
+
...(secrets.length > 0 ? [`${secrets.length} secret(s) it needs are not set`] : []),
|
|
679
|
+
...commands.map(({ cli }) => `the ${cli} command is not on the service's PATH`),
|
|
680
|
+
].join('; '),
|
|
681
|
+
...(secrets.length > 0 ? { context: secrets.join(', ') } : {}),
|
|
682
|
+
fix: [
|
|
683
|
+
...secrets.map(secretFix),
|
|
684
|
+
...commands.map(({ cli, login }) => `install ${cli}${login ? ` and run ${login}` : ''}`),
|
|
685
|
+
].join('; '),
|
|
686
|
+
});
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (!start.online)
|
|
690
|
+
continue;
|
|
691
|
+
// Boot starts each source in the daemon's environment; no source's start writes anything.
|
|
692
|
+
// Boot only starts a source; a poll then needs its credentials accepted, so ask that too.
|
|
693
|
+
let failure = null;
|
|
694
|
+
try {
|
|
695
|
+
await withEnvironment(start.env, () => bounded(start.timeoutMs, async () => {
|
|
696
|
+
try {
|
|
697
|
+
await connector.init();
|
|
698
|
+
}
|
|
699
|
+
catch (error) {
|
|
700
|
+
failure = {
|
|
701
|
+
name: `source ${name}`,
|
|
702
|
+
status: 'fail',
|
|
703
|
+
detail: 'does not start; boot stops at the connectors stage',
|
|
704
|
+
context: message(error),
|
|
705
|
+
};
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
if (!(await connector.authenticate())) {
|
|
709
|
+
failure = {
|
|
710
|
+
name: `source ${name}`,
|
|
711
|
+
status: 'fail',
|
|
712
|
+
detail: 'starts, but its credentials or sign-in are refused, so its polls fail',
|
|
713
|
+
fix: needs
|
|
714
|
+
.map((need) => need.cliAuthCommand ?? (need.tokenName ? secretFix(need.tokenName) : ''))
|
|
715
|
+
.filter(Boolean)
|
|
716
|
+
.join('; ') || undefined,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
}));
|
|
720
|
+
}
|
|
721
|
+
catch (error) {
|
|
722
|
+
failure = {
|
|
723
|
+
name: `source ${name}`,
|
|
724
|
+
status: 'fail',
|
|
725
|
+
detail: 'could not be asked whether its credentials work',
|
|
726
|
+
context: message(error),
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
finally {
|
|
730
|
+
await connector.dispose();
|
|
731
|
+
}
|
|
732
|
+
if (failure)
|
|
733
|
+
checks.push(failure);
|
|
734
|
+
}
|
|
735
|
+
return checks;
|
|
736
|
+
}
|
|
737
|
+
/** Some sources fetch without a deadline; doctor waits this long for each before moving on. */
|
|
738
|
+
const SOURCE_PROBE_MS = 20_000;
|
|
739
|
+
function bounded(ms, run) {
|
|
740
|
+
let timer;
|
|
741
|
+
return Promise.race([
|
|
742
|
+
run(),
|
|
743
|
+
new Promise((_, reject) => {
|
|
744
|
+
timer = setTimeout(() => reject(new Error(`no answer within ${ms / 1000} s`)), ms);
|
|
745
|
+
}),
|
|
746
|
+
]).finally(() => clearTimeout(timer));
|
|
747
|
+
}
|
|
748
|
+
/** Run with process.env as the daemon's process has it, then put this shell's back. */
|
|
749
|
+
async function withEnvironment(env, run) {
|
|
750
|
+
const saved = { ...process.env };
|
|
751
|
+
const replace = (next) => {
|
|
752
|
+
for (const name of Object.keys(process.env))
|
|
753
|
+
delete process.env[name];
|
|
754
|
+
for (const [name, value] of Object.entries(next)) {
|
|
755
|
+
if (value !== undefined)
|
|
756
|
+
process.env[name] = value;
|
|
757
|
+
}
|
|
758
|
+
};
|
|
759
|
+
replace(env);
|
|
760
|
+
try {
|
|
761
|
+
return await run();
|
|
762
|
+
}
|
|
763
|
+
finally {
|
|
764
|
+
replace(saved);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function toolkitCheck(config) {
|
|
768
|
+
if (!config.tools_root) {
|
|
769
|
+
return {
|
|
770
|
+
name: 'toolkit',
|
|
771
|
+
status: 'warn',
|
|
772
|
+
detail: 'tools_root is not set; document work has no toolkit',
|
|
773
|
+
fix: 'set tools_root outside HOME in config.yaml, then mama tools install',
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
// Boot reads the toolkit the same way; an unsafe tools_root stops it at the config stage.
|
|
777
|
+
let present;
|
|
778
|
+
try {
|
|
779
|
+
present = (0, document_toolkit_js_1.readDocumentToolkit)(config.tools_root, () => { }) !== null;
|
|
780
|
+
}
|
|
781
|
+
catch (error) {
|
|
782
|
+
return {
|
|
783
|
+
name: 'toolkit',
|
|
784
|
+
status: 'fail',
|
|
785
|
+
detail: 'tools_root is unsafe; boot refuses it',
|
|
786
|
+
context: message(error),
|
|
787
|
+
fix: 'make tools_root and the folders above it writable only by you (or sticky), and owned by you',
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
return present
|
|
791
|
+
? { name: 'toolkit', status: 'ok', detail: 'installed', context: config.tools_root }
|
|
792
|
+
: {
|
|
793
|
+
name: 'toolkit',
|
|
794
|
+
status: 'warn',
|
|
795
|
+
detail: 'not installed under tools_root',
|
|
796
|
+
context: config.tools_root,
|
|
797
|
+
fix: 'mama tools install',
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
async function daemonChecks(ask, port, host, portInUse) {
|
|
801
|
+
let daemon;
|
|
802
|
+
try {
|
|
803
|
+
daemon = await ask();
|
|
804
|
+
}
|
|
805
|
+
catch (error) {
|
|
806
|
+
return [
|
|
807
|
+
{
|
|
808
|
+
name: 'daemon',
|
|
809
|
+
status: 'fail',
|
|
810
|
+
detail: 'holds the install but does not answer on its control socket',
|
|
811
|
+
context: message(error),
|
|
812
|
+
fix: 'daemon.log shows what it is doing (a migration can hold it); if it stays silent, mama stop and mama service start',
|
|
813
|
+
},
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
if (daemon.state === 'stopped') {
|
|
817
|
+
const checks = [
|
|
818
|
+
{
|
|
819
|
+
name: 'daemon',
|
|
820
|
+
status: 'warn',
|
|
821
|
+
detail: daemon.holder ? `stopped; ${daemon.holder.command} holds the install` : 'stopped',
|
|
822
|
+
fix: 'mama service start',
|
|
823
|
+
},
|
|
824
|
+
];
|
|
825
|
+
if (port === null)
|
|
826
|
+
return checks;
|
|
827
|
+
let used;
|
|
828
|
+
try {
|
|
829
|
+
used = await portInUse(port, host);
|
|
830
|
+
}
|
|
831
|
+
catch (error) {
|
|
832
|
+
checks.push({
|
|
833
|
+
name: 'port',
|
|
834
|
+
status: 'fail',
|
|
835
|
+
detail: 'MAMA_API_HOST is not an address this machine can bind',
|
|
836
|
+
context: message(error),
|
|
837
|
+
fix: 'set MAMA_API_HOST in start.sh to a local address, or remove it for 127.0.0.1',
|
|
838
|
+
});
|
|
839
|
+
return checks;
|
|
840
|
+
}
|
|
841
|
+
if (used) {
|
|
842
|
+
checks.push({
|
|
843
|
+
name: 'port',
|
|
844
|
+
status: 'fail',
|
|
845
|
+
detail: `port ${port} is in use by another process`,
|
|
846
|
+
fix: `free port ${port} or set MAMA_API_PORT in start.sh`,
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
return checks;
|
|
850
|
+
}
|
|
851
|
+
if (daemon.reply.ok !== true) {
|
|
852
|
+
return [
|
|
853
|
+
{
|
|
854
|
+
name: 'daemon',
|
|
855
|
+
status: 'fail',
|
|
856
|
+
detail: 'running, but it refused status',
|
|
857
|
+
context: String(daemon.reply.error),
|
|
858
|
+
fix: 'daemon.log shows why',
|
|
859
|
+
},
|
|
860
|
+
];
|
|
861
|
+
}
|
|
862
|
+
const status = (daemon.reply.status ?? {});
|
|
863
|
+
const checks = [];
|
|
864
|
+
const stage = typeof status.stage === 'string' ? status.stage : 'unknown';
|
|
865
|
+
const startedAt = typeof status.startedAt === 'number' ? status.startedAt : Date.now();
|
|
866
|
+
// A replay or backfill does its work inside the boot, for as long as it takes.
|
|
867
|
+
if (status.command === 'replay' || status.command === 'backfill') {
|
|
868
|
+
return [
|
|
869
|
+
{
|
|
870
|
+
name: 'daemon',
|
|
871
|
+
status: 'warn',
|
|
872
|
+
detail: `stopped; ${status.command} holds the install (stage ${stage})`,
|
|
873
|
+
fix: `mama service start after the ${status.command} ends`,
|
|
874
|
+
},
|
|
875
|
+
];
|
|
876
|
+
}
|
|
877
|
+
if (stage === 'running') {
|
|
878
|
+
checks.push({ name: 'daemon', status: 'ok', detail: 'running' });
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
// A first poll of many sources can hold the connectors stage for minutes; time alone
|
|
882
|
+
// does not make a boot stuck, so this names the stage and how long it has been there.
|
|
883
|
+
const minutes = Math.floor((Date.now() - startedAt) / 60_000);
|
|
884
|
+
checks.push({
|
|
885
|
+
name: 'daemon',
|
|
886
|
+
status: 'warn',
|
|
887
|
+
detail: `at boot stage ${stage} for ${minutes} min`,
|
|
888
|
+
fix: 'daemon.log shows what that stage is doing',
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
// Each source as the running daemon last polled it.
|
|
892
|
+
for (const connector of (Array.isArray(status.connectors) ? status.connectors : [])) {
|
|
893
|
+
const lastPoll = typeof connector.lastPoll === 'string' ? connector.lastPoll : 'never';
|
|
894
|
+
checks.push(connector.healthy === true
|
|
895
|
+
? { name: `source ${connector.name}`, status: 'ok', detail: `last poll ${lastPoll}` }
|
|
896
|
+
: {
|
|
897
|
+
name: `source ${connector.name}`,
|
|
898
|
+
status: 'warn',
|
|
899
|
+
detail: `unhealthy; last poll ${lastPoll}`,
|
|
900
|
+
...(typeof connector.error === 'string' ? { context: connector.error } : {}),
|
|
901
|
+
fix: 'daemon.log shows this source; the daemon keeps polling it',
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
return checks;
|
|
905
|
+
}
|
|
906
|
+
function serviceCheck(platform, home, serviceRun) {
|
|
907
|
+
let launchd;
|
|
908
|
+
let unit;
|
|
909
|
+
try {
|
|
910
|
+
launchd = platform === 'darwin' ? (0, daemon_js_1.launchdService)() : { loaded: false, pid: null };
|
|
911
|
+
unit = (0, systemd_service_js_1.systemdUnitState)(home, serviceRun, platform);
|
|
912
|
+
}
|
|
913
|
+
catch (error) {
|
|
914
|
+
return {
|
|
915
|
+
name: 'service',
|
|
916
|
+
status: 'warn',
|
|
917
|
+
detail: `could not ask ${platform === 'darwin' ? 'launchd' : 'systemd'}`,
|
|
918
|
+
context: message(error),
|
|
919
|
+
};
|
|
920
|
+
}
|
|
921
|
+
if (launchd.loaded) {
|
|
922
|
+
return {
|
|
923
|
+
name: 'service',
|
|
924
|
+
status: 'ok',
|
|
925
|
+
detail: `launchd${launchd.pid === null ? ' (loaded, not running)' : ''}`,
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
if (unit === null) {
|
|
929
|
+
return {
|
|
930
|
+
name: 'service',
|
|
931
|
+
status: 'warn',
|
|
932
|
+
detail: 'no service manager runs the daemon',
|
|
933
|
+
fix: 'mama service install, then mama service start',
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
if (unit === 'failed') {
|
|
937
|
+
return {
|
|
938
|
+
name: 'service',
|
|
939
|
+
status: 'fail',
|
|
940
|
+
detail: 'systemd (failed)',
|
|
941
|
+
fix: 'journalctl --user -u mama.service shows why',
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
return SYSTEMD_USABLE.has(unit)
|
|
945
|
+
? { name: 'service', status: 'ok', detail: `systemd (${unit})` }
|
|
946
|
+
: {
|
|
947
|
+
name: 'service',
|
|
948
|
+
status: 'warn',
|
|
949
|
+
detail: 'systemctl --user did not answer; no user session manager is running',
|
|
950
|
+
fix: 'log in on a session, or mama service install --enable-linger',
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
//# sourceMappingURL=diagnostics.js.map
|