amalgm 0.1.38 → 0.1.40
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/README.md +4 -3
- package/lib/cli.js +188 -4
- package/lib/paths.js +10 -0
- package/lib/service.js +823 -0
- package/lib/supervisor.js +43 -5
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -20,9 +20,9 @@ If you start from the Amalgm web app or are setting up a remote/headless machine
|
|
|
20
20
|
amalgm login --setup-code ABCD-EFGH-JKLM-NPQR
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
-
`amalgm doctor` checks the installed runtime, login record, required local ports, daemon state, and whether an HMAC proxy token is available.
|
|
23
|
+
`amalgm doctor` checks the installed runtime, login record, service supervisor, required local ports, daemon state, and whether an HMAC proxy token is available.
|
|
24
24
|
|
|
25
|
-
`amalgm start` runs the local essentials:
|
|
25
|
+
`amalgm start` installs/starts the best available service supervisor for the machine, then the supervisor runs `amalgm run` and keeps the local essentials alive:
|
|
26
26
|
|
|
27
27
|
- port monitor on `8081`
|
|
28
28
|
- filesystem/auth watcher on `8082`
|
|
@@ -31,7 +31,7 @@ amalgm login --setup-code ABCD-EFGH-JKLM-NPQR
|
|
|
31
31
|
- events/previews/artifact tunnel to `wire.events.amalgm.ai`
|
|
32
32
|
- chat tunnel to `amalgm-chat-gateway`
|
|
33
33
|
|
|
34
|
-
The npm supervisor binds local services to `127.0.0.1` by default and connects them outward through the registered Amalgm tunnels. For development without cloud registration, use `amalgm start --local-only`.
|
|
34
|
+
The npm supervisor binds local services to `127.0.0.1` by default and connects them outward through the registered Amalgm tunnels. For development without cloud registration, use `amalgm start --local-only`. For direct foreground debugging, use `amalgm start --foreground`.
|
|
35
35
|
|
|
36
36
|
Useful commands:
|
|
37
37
|
|
|
@@ -40,6 +40,7 @@ amalgm status
|
|
|
40
40
|
amalgm update
|
|
41
41
|
amalgm logs
|
|
42
42
|
amalgm logs chat-server
|
|
43
|
+
amalgm service status
|
|
43
44
|
amalgm stop
|
|
44
45
|
amalgm logout
|
|
45
46
|
```
|
package/lib/cli.js
CHANGED
|
@@ -18,8 +18,16 @@ const {
|
|
|
18
18
|
RUNTIME_DIR,
|
|
19
19
|
RUNTIME_STATE_FILE,
|
|
20
20
|
RUNTIME_TOKEN_FILE,
|
|
21
|
+
SHUTDOWN_REASON_FILE,
|
|
21
22
|
} = require('./paths');
|
|
22
23
|
const { startSupervisor } = require('./supervisor');
|
|
24
|
+
const {
|
|
25
|
+
getServiceStatus,
|
|
26
|
+
installService,
|
|
27
|
+
startService,
|
|
28
|
+
stopService,
|
|
29
|
+
uninstallService,
|
|
30
|
+
} = require('./service');
|
|
23
31
|
const {
|
|
24
32
|
deleteComputerAuth,
|
|
25
33
|
loadComputerRecord,
|
|
@@ -79,6 +87,7 @@ function usage() {
|
|
|
79
87
|
' amalgm stop',
|
|
80
88
|
' amalgm status',
|
|
81
89
|
' amalgm doctor',
|
|
90
|
+
' amalgm service [install|start|stop|status|uninstall] [--mode auto|systemd|launchd|portable]',
|
|
82
91
|
' amalgm update [--tag latest|canary]',
|
|
83
92
|
' amalgm logs [service]',
|
|
84
93
|
' amalgm logout',
|
|
@@ -824,11 +833,16 @@ async function start(options) {
|
|
|
824
833
|
const pid = readPid();
|
|
825
834
|
if (isPidRunning(pid)) {
|
|
826
835
|
const restartReason = runningDaemonRestartReason(pid, record, localOnly);
|
|
827
|
-
|
|
836
|
+
const serviceStatus = getServiceStatus();
|
|
837
|
+
if (!restartReason && (options['no-service'] || serviceStatus.running)) {
|
|
828
838
|
console.log(`Amalgm is already running (pid ${pid}).`);
|
|
829
839
|
return;
|
|
830
840
|
}
|
|
831
|
-
|
|
841
|
+
if (!restartReason) {
|
|
842
|
+
console.log(`Amalgm is already running (pid ${pid}) without a service supervisor. Moving it under service supervision.`);
|
|
843
|
+
} else {
|
|
844
|
+
console.log(`Amalgm is already running (pid ${pid}), but ${restartReason}. Restarting.`);
|
|
845
|
+
}
|
|
832
846
|
await stop();
|
|
833
847
|
}
|
|
834
848
|
|
|
@@ -843,6 +857,27 @@ async function start(options) {
|
|
|
843
857
|
return;
|
|
844
858
|
}
|
|
845
859
|
|
|
860
|
+
if (!options['no-service']) {
|
|
861
|
+
const statusBefore = getServiceStatus();
|
|
862
|
+
const statusAfter = startService({
|
|
863
|
+
localOnly,
|
|
864
|
+
mode: options.mode || process.env.AMALGM_SERVICE_MODE || 'auto',
|
|
865
|
+
});
|
|
866
|
+
const backend = statusAfter.backend || statusBefore.backend || 'service';
|
|
867
|
+
console.log(`Amalgm service ${statusBefore.installed ? 'started' : 'installed and started'} (${backend}).`);
|
|
868
|
+
const serviceDetail = statusAfter.detail || statusAfter.state?.detail;
|
|
869
|
+
if (serviceDetail) console.log(`Service: ${serviceDetail}`);
|
|
870
|
+
|
|
871
|
+
const ready = await waitForServices(8000);
|
|
872
|
+
const pending = ready.filter(([, ok]) => !ok).map(([name]) => name);
|
|
873
|
+
if (pending.length === 0) {
|
|
874
|
+
console.log('Services: all local essentials are up.');
|
|
875
|
+
} else {
|
|
876
|
+
console.log(`Services still warming up: ${pending.join(', ')}. Run \`amalgm status\` or \`amalgm logs\` if they stay down.`);
|
|
877
|
+
}
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
846
881
|
ensureDir(LOG_DIR);
|
|
847
882
|
const logPath = path.join(LOG_DIR, 'daemon.log');
|
|
848
883
|
const logFd = fs.openSync(logPath, 'a');
|
|
@@ -869,6 +904,23 @@ async function start(options) {
|
|
|
869
904
|
}
|
|
870
905
|
|
|
871
906
|
async function stop() {
|
|
907
|
+
const serviceStatus = getServiceStatus();
|
|
908
|
+
if (serviceStatus.installed || serviceStatus.running) {
|
|
909
|
+
const result = stopService();
|
|
910
|
+
if (result.service.backend) {
|
|
911
|
+
console.log(`Amalgm service stopped (${result.service.backend}).`);
|
|
912
|
+
}
|
|
913
|
+
if (!isPidRunning(readPid())) {
|
|
914
|
+
try {
|
|
915
|
+
fs.unlinkSync(PID_FILE);
|
|
916
|
+
} catch {
|
|
917
|
+
// noop
|
|
918
|
+
}
|
|
919
|
+
console.log('Amalgm stopped.');
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
872
924
|
const pid = readPid();
|
|
873
925
|
if (!isPidRunning(pid)) {
|
|
874
926
|
try {
|
|
@@ -944,10 +996,24 @@ async function status() {
|
|
|
944
996
|
const pid = readPid();
|
|
945
997
|
const record = loadComputerRecord({ migrate: true });
|
|
946
998
|
const runtimeState = readJson(RUNTIME_STATE_FILE, null);
|
|
999
|
+
const serviceStatus = getServiceStatus();
|
|
1000
|
+
const lastShutdown = readJson(SHUTDOWN_REASON_FILE, null);
|
|
947
1001
|
|
|
948
1002
|
console.log(`Daemon: ${isPidRunning(pid) ? `running (pid ${pid})` : 'stopped'}`);
|
|
1003
|
+
console.log(
|
|
1004
|
+
`Service: ${
|
|
1005
|
+
serviceStatus.installed
|
|
1006
|
+
? `${serviceStatus.running ? 'running' : 'stopped'} (${serviceStatus.backend || 'unknown'})`
|
|
1007
|
+
: 'not installed'
|
|
1008
|
+
}`,
|
|
1009
|
+
);
|
|
949
1010
|
console.log(`State: ${AMALGM_DIR}`);
|
|
950
1011
|
if (runtimeState?.gateway_url) console.log(`Gateway: ${runtimeState.gateway_url}`);
|
|
1012
|
+
if (lastShutdown?.signal || lastShutdown?.reason) {
|
|
1013
|
+
console.log(
|
|
1014
|
+
`Last shutdown: ${lastShutdown.signal || lastShutdown.reason} at ${lastShutdown.at || lastShutdown.time || 'unknown time'}`,
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
951
1017
|
console.log(
|
|
952
1018
|
`Computer: ${
|
|
953
1019
|
record?.computer_id
|
|
@@ -1016,7 +1082,33 @@ async function doctor() {
|
|
|
1016
1082
|
|
|
1017
1083
|
const pid = readPid();
|
|
1018
1084
|
const daemonRunning = isPidRunning(pid);
|
|
1085
|
+
const serviceStatus = getServiceStatus();
|
|
1086
|
+
const lastShutdown = readJson(SHUTDOWN_REASON_FILE, null);
|
|
1087
|
+
add(
|
|
1088
|
+
'service',
|
|
1089
|
+
serviceStatus.installed && serviceStatus.running,
|
|
1090
|
+
serviceStatus.installed
|
|
1091
|
+
? `${serviceStatus.running ? 'running' : 'stopped'} (${serviceStatus.backend || 'unknown'})`
|
|
1092
|
+
: 'not installed; `amalgm start` will install one',
|
|
1093
|
+
serviceStatus.installed && serviceStatus.running ? 'ok' : 'warn',
|
|
1094
|
+
);
|
|
1095
|
+
if (serviceStatus.backend === 'portable' && /container/i.test(serviceStatus.detail || '')) {
|
|
1096
|
+
add(
|
|
1097
|
+
'service durability',
|
|
1098
|
+
true,
|
|
1099
|
+
'portable watchdog is best-effort inside managed containers; platform entrypoint/service is stronger',
|
|
1100
|
+
'warn',
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1019
1103
|
add('daemon', daemonRunning, daemonRunning ? `pid ${pid}` : 'stopped', daemonRunning ? 'ok' : 'warn');
|
|
1104
|
+
if (lastShutdown?.signal || lastShutdown?.reason) {
|
|
1105
|
+
add(
|
|
1106
|
+
'last shutdown',
|
|
1107
|
+
true,
|
|
1108
|
+
`${lastShutdown.signal || lastShutdown.reason} at ${lastShutdown.at || 'unknown time'}`,
|
|
1109
|
+
'warn',
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1020
1112
|
|
|
1021
1113
|
for (const [name, port] of servicePorts()) {
|
|
1022
1114
|
const envName = {
|
|
@@ -1057,13 +1149,104 @@ function readLastLines(file, maxLines = 120) {
|
|
|
1057
1149
|
return buffer.toString('utf8').trimEnd().split('\n').slice(-maxLines).join('\n');
|
|
1058
1150
|
}
|
|
1059
1151
|
|
|
1152
|
+
function readNativeServiceLogs(maxLines = 120) {
|
|
1153
|
+
const serviceStatus = getServiceStatus();
|
|
1154
|
+
const label = serviceStatus.state?.label || 'ai.amalgm.runtime';
|
|
1155
|
+
const chunks = [];
|
|
1156
|
+
|
|
1157
|
+
if (serviceStatus.backend === 'systemd') {
|
|
1158
|
+
const statusResult = spawnSync('systemctl', ['--user', 'status', label, '--no-pager', '-l'], {
|
|
1159
|
+
encoding: 'utf8',
|
|
1160
|
+
timeout: 5000,
|
|
1161
|
+
});
|
|
1162
|
+
const statusOutput = [statusResult.stdout, statusResult.stderr].filter(Boolean).join('\n').trim();
|
|
1163
|
+
if (statusOutput) chunks.push(`===== systemctl --user status ${label} =====\n${statusOutput}`);
|
|
1164
|
+
|
|
1165
|
+
const journalResult = spawnSync('journalctl', ['--user', '-u', label, '-n', String(maxLines), '--no-pager'], {
|
|
1166
|
+
encoding: 'utf8',
|
|
1167
|
+
timeout: 5000,
|
|
1168
|
+
});
|
|
1169
|
+
const journalOutput = [journalResult.stdout, journalResult.stderr].filter(Boolean).join('\n').trim();
|
|
1170
|
+
if (journalOutput) chunks.push(`===== journalctl --user -u ${label} =====\n${journalOutput}`);
|
|
1171
|
+
} else if (serviceStatus.backend === 'launchd') {
|
|
1172
|
+
const domain = `gui/${process.getuid ? process.getuid() : ''}`;
|
|
1173
|
+
const result = spawnSync('launchctl', ['print', `${domain}/${label}`], {
|
|
1174
|
+
encoding: 'utf8',
|
|
1175
|
+
timeout: 5000,
|
|
1176
|
+
});
|
|
1177
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
1178
|
+
if (output) chunks.push(`===== launchctl print ${domain}/${label} =====\n${output}`);
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
return chunks.join('\n\n');
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1060
1184
|
async function logs(options, positionals) {
|
|
1061
1185
|
const service = String(options.service || positionals[0] || 'daemon').replace(/[^a-z0-9_-]/gi, '');
|
|
1186
|
+
const maxLines = Number(options.lines || 120);
|
|
1187
|
+
if (service === 'service') {
|
|
1188
|
+
const nativeLogs = readNativeServiceLogs(maxLines);
|
|
1189
|
+
if (nativeLogs) {
|
|
1190
|
+
console.log(nativeLogs);
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1062
1194
|
const logPath = path.join(LOG_DIR, `${service}.log`);
|
|
1063
1195
|
if (!fs.existsSync(logPath)) {
|
|
1064
1196
|
throw new Error(`No log file found for "${service}" at ${logPath}`);
|
|
1065
1197
|
}
|
|
1066
|
-
console.log(readLastLines(logPath,
|
|
1198
|
+
console.log(readLastLines(logPath, maxLines));
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function printServiceStatus(statusInfo = getServiceStatus()) {
|
|
1202
|
+
console.log(
|
|
1203
|
+
`Service: ${
|
|
1204
|
+
statusInfo.installed
|
|
1205
|
+
? `${statusInfo.running ? 'running' : 'stopped'} (${statusInfo.backend || 'unknown'})`
|
|
1206
|
+
: 'not installed'
|
|
1207
|
+
}`,
|
|
1208
|
+
);
|
|
1209
|
+
if (statusInfo.detail) console.log(`Backend: ${statusInfo.detail}`);
|
|
1210
|
+
if (statusInfo.pid) console.log(`Service pid: ${statusInfo.pid}`);
|
|
1211
|
+
if (statusInfo.stopRequested) console.log('Stop request: pending');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
async function serviceCommand(options, positionals) {
|
|
1215
|
+
const action = positionals[0] || 'status';
|
|
1216
|
+
const localOnly = !!options['local-only'];
|
|
1217
|
+
const mode = options.mode || process.env.AMALGM_SERVICE_MODE || 'auto';
|
|
1218
|
+
|
|
1219
|
+
if (action === 'install') {
|
|
1220
|
+
const state = installService({ localOnly, mode });
|
|
1221
|
+
console.log(`Amalgm service installed (${state.backend}).`);
|
|
1222
|
+
if (state.detail) console.log(`Service: ${state.detail}`);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
if (action === 'start') {
|
|
1227
|
+
const statusInfo = startService({ localOnly, mode });
|
|
1228
|
+
printServiceStatus(statusInfo);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
if (action === 'stop') {
|
|
1233
|
+
stopService();
|
|
1234
|
+
printServiceStatus();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (action === 'status') {
|
|
1239
|
+
printServiceStatus();
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (action === 'uninstall') {
|
|
1244
|
+
uninstallService();
|
|
1245
|
+
console.log('Amalgm service uninstalled.');
|
|
1246
|
+
return;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
throw new Error(`Unknown service action "${action}". Use install, start, stop, status, or uninstall.`);
|
|
1067
1250
|
}
|
|
1068
1251
|
|
|
1069
1252
|
async function logout() {
|
|
@@ -1113,12 +1296,13 @@ async function main(argv) {
|
|
|
1113
1296
|
if (command === 'run') {
|
|
1114
1297
|
return startSupervisor({
|
|
1115
1298
|
foreground: true,
|
|
1116
|
-
localOnly: process.env.AMALGM_LOCAL_ONLY === 'true',
|
|
1299
|
+
localOnly: !!options['local-only'] || process.env.AMALGM_LOCAL_ONLY === 'true',
|
|
1117
1300
|
});
|
|
1118
1301
|
}
|
|
1119
1302
|
if (command === 'stop') return stop();
|
|
1120
1303
|
if (command === 'status') return status();
|
|
1121
1304
|
if (command === 'doctor') return doctor();
|
|
1305
|
+
if (command === 'service') return serviceCommand(options, positionals);
|
|
1122
1306
|
if (command === 'update') return update(options);
|
|
1123
1307
|
if (command === 'logs') return logs(options, positionals);
|
|
1124
1308
|
if (command === 'logout') return logout();
|
package/lib/paths.js
CHANGED
|
@@ -13,6 +13,11 @@ const RUNTIME_TOKEN_FILE = path.join(AMALGM_DIR, 'runtime-token.json');
|
|
|
13
13
|
const PID_FILE = path.join(AMALGM_DIR, 'amalgm.pid');
|
|
14
14
|
const RUNTIME_STATE_FILE = path.join(AMALGM_DIR, 'runtime-state.json');
|
|
15
15
|
const LOG_DIR = path.join(AMALGM_DIR, 'logs');
|
|
16
|
+
const SERVICE_DIR = path.join(AMALGM_DIR, 'service');
|
|
17
|
+
const SERVICE_PID_FILE = path.join(AMALGM_DIR, 'amalgm-service.pid');
|
|
18
|
+
const SERVICE_STATE_FILE = path.join(AMALGM_DIR, 'service-state.json');
|
|
19
|
+
const SERVICE_STOP_FILE = path.join(AMALGM_DIR, 'amalgm-service.stop');
|
|
20
|
+
const SHUTDOWN_REASON_FILE = path.join(AMALGM_DIR, 'shutdown-reason.json');
|
|
16
21
|
const DEFAULT_APP_URL = process.env.AMALGM_APP_URL || 'https://amalgm.ai';
|
|
17
22
|
|
|
18
23
|
module.exports = {
|
|
@@ -27,4 +32,9 @@ module.exports = {
|
|
|
27
32
|
RUNTIME_DIR,
|
|
28
33
|
RUNTIME_STATE_FILE,
|
|
29
34
|
RUNTIME_TOKEN_FILE,
|
|
35
|
+
SERVICE_DIR,
|
|
36
|
+
SERVICE_PID_FILE,
|
|
37
|
+
SERVICE_STATE_FILE,
|
|
38
|
+
SERVICE_STOP_FILE,
|
|
39
|
+
SHUTDOWN_REASON_FILE,
|
|
30
40
|
};
|
package/lib/service.js
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawn, spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
AMALGM_DIR,
|
|
10
|
+
LOG_DIR,
|
|
11
|
+
PACKAGE_ROOT,
|
|
12
|
+
PID_FILE,
|
|
13
|
+
SERVICE_DIR,
|
|
14
|
+
SERVICE_PID_FILE,
|
|
15
|
+
SERVICE_STATE_FILE,
|
|
16
|
+
SERVICE_STOP_FILE,
|
|
17
|
+
} = require('./paths');
|
|
18
|
+
|
|
19
|
+
const PACKAGE_VERSION = require('../package.json').version;
|
|
20
|
+
const SERVICE_LABEL = 'ai.amalgm.runtime';
|
|
21
|
+
const SERVICE_SCRIPT_FILE = path.join(SERVICE_DIR, 'amalgm-watchdog.sh');
|
|
22
|
+
const SERVICE_NODE_SCRIPT_FILE = path.join(SERVICE_DIR, 'amalgm-watchdog.cjs');
|
|
23
|
+
const SERVICE_LOG_FILE = path.join(LOG_DIR, 'service.log');
|
|
24
|
+
const AMALGM_BIN = path.join(PACKAGE_ROOT, 'bin', 'amalgm.js');
|
|
25
|
+
|
|
26
|
+
const SERVICE_ENV_KEYS = [
|
|
27
|
+
'AMALGM_APP_URL',
|
|
28
|
+
'AMALGM_AUTH_BACKUP_ENABLED',
|
|
29
|
+
'AMALGM_BIND_HOST',
|
|
30
|
+
'AMALGM_CREATE_AUTH_WATCH_DIRS',
|
|
31
|
+
'AMALGM_DEFAULT_CWD',
|
|
32
|
+
'AMALGM_GATEWAY_PORT',
|
|
33
|
+
'AMALGM_NATIVE_NODE_MODULES',
|
|
34
|
+
'AMALGM_PROJECTS_DIR',
|
|
35
|
+
'AMALGM_SKIP_NATIVE_INSTALL',
|
|
36
|
+
'AMALGM_WORKSPACES_DIR',
|
|
37
|
+
'CHAT_SERVER_PORT',
|
|
38
|
+
'ELECTRON_RUN_AS_NODE',
|
|
39
|
+
'FS_WATCHER_PORT',
|
|
40
|
+
'LANG',
|
|
41
|
+
'LC_ALL',
|
|
42
|
+
'NODE_PATH',
|
|
43
|
+
'PATH',
|
|
44
|
+
'PORT_MONITOR_PORT',
|
|
45
|
+
'SHELL',
|
|
46
|
+
'TERM',
|
|
47
|
+
'TMP',
|
|
48
|
+
'TMPDIR',
|
|
49
|
+
'TEMP',
|
|
50
|
+
'XDG_CACHE_HOME',
|
|
51
|
+
'XDG_CONFIG_HOME',
|
|
52
|
+
'XDG_DATA_HOME',
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
function ensureDir(dir, mode = 0o700) {
|
|
56
|
+
fs.mkdirSync(dir, { recursive: true, mode });
|
|
57
|
+
try {
|
|
58
|
+
fs.chmodSync(dir, mode);
|
|
59
|
+
} catch {
|
|
60
|
+
// Best effort on non-POSIX filesystems.
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function readJson(file, fallback = null) {
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
67
|
+
} catch {
|
|
68
|
+
return fallback;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function writeJson(file, data, mode = 0o600) {
|
|
73
|
+
ensureDir(path.dirname(file), 0o700);
|
|
74
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
75
|
+
fs.writeFileSync(temp, `${JSON.stringify(data, null, 2)}\n`, { mode });
|
|
76
|
+
fs.renameSync(temp, file);
|
|
77
|
+
try {
|
|
78
|
+
fs.chmodSync(file, mode);
|
|
79
|
+
} catch {
|
|
80
|
+
// Best effort.
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function removeFile(file) {
|
|
85
|
+
try {
|
|
86
|
+
fs.unlinkSync(file);
|
|
87
|
+
} catch {
|
|
88
|
+
// noop
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function shellQuote(value) {
|
|
93
|
+
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function plistEscape(value) {
|
|
97
|
+
return String(value)
|
|
98
|
+
.replace(/&/g, '&')
|
|
99
|
+
.replace(/</g, '<')
|
|
100
|
+
.replace(/>/g, '>')
|
|
101
|
+
.replace(/"/g, '"')
|
|
102
|
+
.replace(/'/g, ''');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function systemdQuote(value) {
|
|
106
|
+
return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isPidRunning(pid) {
|
|
110
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
111
|
+
try {
|
|
112
|
+
process.kill(pid, 0);
|
|
113
|
+
return true;
|
|
114
|
+
} catch {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function readPidFile(file) {
|
|
120
|
+
const raw = readJson(file, null);
|
|
121
|
+
if (raw && Number.isInteger(raw.pid)) return raw.pid;
|
|
122
|
+
try {
|
|
123
|
+
const parsed = Number(fs.readFileSync(file, 'utf8').trim());
|
|
124
|
+
return Number.isInteger(parsed) ? parsed : null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function commandExists(command) {
|
|
131
|
+
const pathValue = process.env.PATH || '';
|
|
132
|
+
const extensions = process.platform === 'win32' ? ['.exe', '.cmd', '.bat', ''] : [''];
|
|
133
|
+
for (const dir of pathValue.split(path.delimiter)) {
|
|
134
|
+
if (!dir) continue;
|
|
135
|
+
for (const ext of extensions) {
|
|
136
|
+
const candidate = path.join(dir, `${command}${ext}`);
|
|
137
|
+
try {
|
|
138
|
+
fs.accessSync(candidate, fs.constants.X_OK);
|
|
139
|
+
return true;
|
|
140
|
+
} catch {
|
|
141
|
+
// keep looking
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function systemdUnavailableOutput(output) {
|
|
149
|
+
return /systemd"? is not running|Failed to connect to bus|No medium found|Host is down|Connection refused/i.test(String(output || ''));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function systemdAvailable() {
|
|
153
|
+
if (process.platform !== 'linux' || !commandExists('systemctl')) return false;
|
|
154
|
+
const showEnvironment = spawnSync('systemctl', ['--user', 'show-environment'], {
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
timeout: 2500,
|
|
157
|
+
});
|
|
158
|
+
const showEnvironmentOutput = [showEnvironment.stderr, showEnvironment.stdout].filter(Boolean).join('\n');
|
|
159
|
+
if (showEnvironment.status !== 0 || systemdUnavailableOutput(showEnvironmentOutput)) return false;
|
|
160
|
+
|
|
161
|
+
const showTarget = spawnSync('systemctl', ['--user', 'show', 'default.target', '--property=LoadState', '--property=ActiveState'], {
|
|
162
|
+
encoding: 'utf8',
|
|
163
|
+
timeout: 2500,
|
|
164
|
+
});
|
|
165
|
+
const showTargetOutput = [showTarget.stderr, showTarget.stdout].filter(Boolean).join('\n');
|
|
166
|
+
return showTarget.status === 0 && !systemdUnavailableOutput(showTargetOutput);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function launchdAvailable() {
|
|
170
|
+
return process.platform === 'darwin' && commandExists('launchctl');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function runningInContainer() {
|
|
174
|
+
if (fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv')) return true;
|
|
175
|
+
try {
|
|
176
|
+
const cgroup = fs.readFileSync('/proc/1/cgroup', 'utf8');
|
|
177
|
+
return /docker|kubepods|containerd|libpod|lxc|daytona|e2b/i.test(cgroup);
|
|
178
|
+
} catch {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function normalizeMode(mode) {
|
|
184
|
+
const value = String(mode || 'auto').trim().toLowerCase();
|
|
185
|
+
if (!value || value === 'auto') return 'auto';
|
|
186
|
+
if (value === 'watchdog') return 'portable';
|
|
187
|
+
if (['portable', 'systemd', 'launchd'].includes(value)) return value;
|
|
188
|
+
throw new Error(`Unsupported service mode "${mode}". Use auto, systemd, launchd, or portable.`);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function detectServiceBackend(mode = 'auto') {
|
|
192
|
+
const requested = normalizeMode(mode);
|
|
193
|
+
const inContainer = runningInContainer();
|
|
194
|
+
if (requested === 'launchd') {
|
|
195
|
+
if (!launchdAvailable()) throw new Error('launchd is not available on this machine.');
|
|
196
|
+
return { backend: 'launchd', detail: 'macOS LaunchAgent' };
|
|
197
|
+
}
|
|
198
|
+
if (requested === 'systemd') {
|
|
199
|
+
if (!systemdAvailable()) throw new Error('systemd --user is not available in this session.');
|
|
200
|
+
return { backend: 'systemd', detail: 'systemd user service' };
|
|
201
|
+
}
|
|
202
|
+
if (requested === 'portable') {
|
|
203
|
+
return {
|
|
204
|
+
backend: 'portable',
|
|
205
|
+
detail: inContainer ? 'portable watchdog in container' : 'portable watchdog',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
if (launchdAvailable()) return { backend: 'launchd', detail: 'macOS LaunchAgent' };
|
|
209
|
+
if (inContainer) {
|
|
210
|
+
return {
|
|
211
|
+
backend: 'portable',
|
|
212
|
+
detail: 'portable watchdog in container',
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (systemdAvailable()) return { backend: 'systemd', detail: 'systemd user service' };
|
|
216
|
+
return {
|
|
217
|
+
backend: 'portable',
|
|
218
|
+
detail: inContainer ? 'portable watchdog in container' : 'portable watchdog',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function buildServiceEnv(localOnly) {
|
|
223
|
+
const env = {
|
|
224
|
+
AMALGM_DIR,
|
|
225
|
+
AMALGM_LOCAL_ONLY: localOnly ? 'true' : 'false',
|
|
226
|
+
AMALGM_SERVICE_MANAGED: 'true',
|
|
227
|
+
AMALGM_SERVICE_LABEL: SERVICE_LABEL,
|
|
228
|
+
ELECTRON_RUN_AS_NODE: process.env.ELECTRON_RUN_AS_NODE || '1',
|
|
229
|
+
PATH: process.env.PATH || '/usr/local/bin:/usr/bin:/bin',
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
for (const key of SERVICE_ENV_KEYS) {
|
|
233
|
+
if (process.env[key] && !env[key]) env[key] = process.env[key];
|
|
234
|
+
}
|
|
235
|
+
return env;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function writePortableScript(localOnly) {
|
|
239
|
+
ensureDir(SERVICE_DIR, 0o700);
|
|
240
|
+
ensureDir(LOG_DIR, 0o700);
|
|
241
|
+
const env = buildServiceEnv(localOnly);
|
|
242
|
+
const lines = [
|
|
243
|
+
'#!/bin/sh',
|
|
244
|
+
'set -u',
|
|
245
|
+
`AMALGM_DIR=${shellQuote(AMALGM_DIR)}`,
|
|
246
|
+
`LOG_DIR=${shellQuote(LOG_DIR)}`,
|
|
247
|
+
`PID_FILE=${shellQuote(SERVICE_PID_FILE)}`,
|
|
248
|
+
`WORKER_PID_FILE=${shellQuote(PID_FILE)}`,
|
|
249
|
+
`STOP_FILE=${shellQuote(SERVICE_STOP_FILE)}`,
|
|
250
|
+
`LOCK_DIR=${shellQuote(path.join(SERVICE_DIR, 'lock'))}`,
|
|
251
|
+
`NODE_BIN=${shellQuote(process.execPath)}`,
|
|
252
|
+
`AMALGM_BIN=${shellQuote(AMALGM_BIN)}`,
|
|
253
|
+
`DAEMON_LOG=${shellQuote(path.join(LOG_DIR, 'daemon.log'))}`,
|
|
254
|
+
`SERVICE_LOG=${shellQuote(SERVICE_LOG_FILE)}`,
|
|
255
|
+
`RESTART_DELAY=${shellQuote(String(process.env.AMALGM_SERVICE_RESTART_DELAY || '5'))}`,
|
|
256
|
+
];
|
|
257
|
+
|
|
258
|
+
for (const [key, value] of Object.entries(env)) {
|
|
259
|
+
lines.push(`export ${key}=${shellQuote(value)}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
lines.push(
|
|
263
|
+
'mkdir -p "$AMALGM_DIR" "$LOG_DIR"',
|
|
264
|
+
'if ! mkdir "$LOCK_DIR" 2>/dev/null; then',
|
|
265
|
+
' existing="$(cat "$PID_FILE" 2>/dev/null | sed -n \'s/.*"pid"[[:space:]]*:[[:space:]]*\\([0-9][0-9]*\\).*/\\1/p\')"',
|
|
266
|
+
' [ -n "$existing" ] || existing="$(cat "$PID_FILE" 2>/dev/null || true)"',
|
|
267
|
+
' if [ -n "$existing" ] && kill -0 "$existing" 2>/dev/null; then',
|
|
268
|
+
' exit 0',
|
|
269
|
+
' fi',
|
|
270
|
+
' rm -rf "$LOCK_DIR"',
|
|
271
|
+
' mkdir "$LOCK_DIR" || exit 1',
|
|
272
|
+
'fi',
|
|
273
|
+
'child_pid=""',
|
|
274
|
+
'cleanup() {',
|
|
275
|
+
' touch "$STOP_FILE" 2>/dev/null || true',
|
|
276
|
+
' if [ -n "$child_pid" ] && kill -0 "$child_pid" 2>/dev/null; then',
|
|
277
|
+
' kill "$child_pid" 2>/dev/null || true',
|
|
278
|
+
' wait "$child_pid" 2>/dev/null || true',
|
|
279
|
+
' fi',
|
|
280
|
+
' rm -f "$PID_FILE"',
|
|
281
|
+
' rm -rf "$LOCK_DIR"',
|
|
282
|
+
' exit 0',
|
|
283
|
+
'}',
|
|
284
|
+
'trap cleanup TERM INT HUP',
|
|
285
|
+
'printf \'{"pid":%s,"started_at":"%s","backend":"portable"}\\n\' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$PID_FILE"',
|
|
286
|
+
'rm -f "$STOP_FILE"',
|
|
287
|
+
'while [ ! -f "$STOP_FILE" ]; do',
|
|
288
|
+
' printf \'[%s] [service] starting amalgm run\\n\' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$SERVICE_LOG"',
|
|
289
|
+
' "$NODE_BIN" "$AMALGM_BIN" run >> "$DAEMON_LOG" 2>&1 &',
|
|
290
|
+
' child_pid=$!',
|
|
291
|
+
' wait "$child_pid"',
|
|
292
|
+
' code=$?',
|
|
293
|
+
' child_pid=""',
|
|
294
|
+
' printf \'[%s] [service] amalgm run exited code=%s\\n\' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$code" >> "$SERVICE_LOG"',
|
|
295
|
+
' [ -f "$STOP_FILE" ] && break',
|
|
296
|
+
' sleep "$RESTART_DELAY"',
|
|
297
|
+
'done',
|
|
298
|
+
'rm -f "$PID_FILE"',
|
|
299
|
+
'rm -rf "$LOCK_DIR"',
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
fs.writeFileSync(SERVICE_SCRIPT_FILE, `${lines.join('\n')}\n`, { mode: 0o700 });
|
|
303
|
+
try {
|
|
304
|
+
fs.chmodSync(SERVICE_SCRIPT_FILE, 0o700);
|
|
305
|
+
} catch {
|
|
306
|
+
// Best effort.
|
|
307
|
+
}
|
|
308
|
+
return SERVICE_SCRIPT_FILE;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function writePortableNodeScript(localOnly) {
|
|
312
|
+
ensureDir(SERVICE_DIR, 0o700);
|
|
313
|
+
ensureDir(LOG_DIR, 0o700);
|
|
314
|
+
const config = {
|
|
315
|
+
amalgmDir: AMALGM_DIR,
|
|
316
|
+
logDir: LOG_DIR,
|
|
317
|
+
pidFile: SERVICE_PID_FILE,
|
|
318
|
+
workerPidFile: PID_FILE,
|
|
319
|
+
stopFile: SERVICE_STOP_FILE,
|
|
320
|
+
lockDir: path.join(SERVICE_DIR, 'lock'),
|
|
321
|
+
nodeBin: process.execPath,
|
|
322
|
+
amalgmBin: AMALGM_BIN,
|
|
323
|
+
daemonLog: path.join(LOG_DIR, 'daemon.log'),
|
|
324
|
+
serviceLog: SERVICE_LOG_FILE,
|
|
325
|
+
restartDelayMs: Math.max(1000, Number(process.env.AMALGM_SERVICE_RESTART_DELAY || 5) * 1000),
|
|
326
|
+
env: buildServiceEnv(localOnly),
|
|
327
|
+
};
|
|
328
|
+
const source = `'use strict';
|
|
329
|
+
|
|
330
|
+
const fs = require('fs');
|
|
331
|
+
const path = require('path');
|
|
332
|
+
const { spawn } = require('child_process');
|
|
333
|
+
|
|
334
|
+
const config = ${JSON.stringify(config, null, 2)};
|
|
335
|
+
let child = null;
|
|
336
|
+
let stopping = false;
|
|
337
|
+
let exiting = false;
|
|
338
|
+
|
|
339
|
+
function ensureDir(dir) {
|
|
340
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function append(file, line) {
|
|
344
|
+
try {
|
|
345
|
+
fs.appendFileSync(file, \`[\${new Date().toISOString()}] [service] \${line}\\n\`);
|
|
346
|
+
} catch {
|
|
347
|
+
// noop
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readPid(file) {
|
|
352
|
+
try {
|
|
353
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
354
|
+
if (Number.isInteger(parsed.pid)) return parsed.pid;
|
|
355
|
+
} catch {
|
|
356
|
+
// fall back to raw pid
|
|
357
|
+
}
|
|
358
|
+
try {
|
|
359
|
+
const raw = Number(fs.readFileSync(file, 'utf8').trim());
|
|
360
|
+
return Number.isInteger(raw) ? raw : null;
|
|
361
|
+
} catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function isPidRunning(pid) {
|
|
367
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
368
|
+
try {
|
|
369
|
+
process.kill(pid, 0);
|
|
370
|
+
return true;
|
|
371
|
+
} catch {
|
|
372
|
+
return false;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function acquireLock() {
|
|
377
|
+
try {
|
|
378
|
+
fs.mkdirSync(config.lockDir);
|
|
379
|
+
return true;
|
|
380
|
+
} catch {
|
|
381
|
+
const existing = readPid(config.pidFile);
|
|
382
|
+
if (isPidRunning(existing)) return false;
|
|
383
|
+
try { fs.rmSync(config.lockDir, { recursive: true, force: true }); } catch {}
|
|
384
|
+
fs.mkdirSync(config.lockDir);
|
|
385
|
+
return true;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function writePid() {
|
|
390
|
+
fs.writeFileSync(config.pidFile, JSON.stringify({
|
|
391
|
+
pid: process.pid,
|
|
392
|
+
backend: 'portable',
|
|
393
|
+
started_at: new Date().toISOString(),
|
|
394
|
+
}, null, 2) + '\\n', { mode: 0o600 });
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function finish(exitCode = 0) {
|
|
398
|
+
try { fs.rmSync(config.pidFile, { force: true }); } catch {}
|
|
399
|
+
try { fs.rmSync(config.lockDir, { recursive: true, force: true }); } catch {}
|
|
400
|
+
process.exit(exitCode);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function cleanup(exitCode = 0) {
|
|
404
|
+
if (exiting) return;
|
|
405
|
+
exiting = true;
|
|
406
|
+
stopping = true;
|
|
407
|
+
try { fs.writeFileSync(config.stopFile, new Date().toISOString() + '\\n', { mode: 0o600 }); } catch {}
|
|
408
|
+
if (child && !child.killed) {
|
|
409
|
+
try { child.kill('SIGTERM'); } catch {}
|
|
410
|
+
const timer = setTimeout(() => {
|
|
411
|
+
if (child && !child.killed) {
|
|
412
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
413
|
+
}
|
|
414
|
+
finish(exitCode);
|
|
415
|
+
}, 5000);
|
|
416
|
+
child.once('exit', () => {
|
|
417
|
+
clearTimeout(timer);
|
|
418
|
+
finish(exitCode);
|
|
419
|
+
});
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
finish(exitCode);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function launch() {
|
|
426
|
+
if (stopping || fs.existsSync(config.stopFile)) return cleanup(0);
|
|
427
|
+
append(config.serviceLog, 'starting amalgm run');
|
|
428
|
+
const fd = fs.openSync(config.daemonLog, 'a');
|
|
429
|
+
child = spawn(config.nodeBin, [config.amalgmBin, 'run'], {
|
|
430
|
+
cwd: process.env.HOME || process.cwd(),
|
|
431
|
+
env: { ...process.env, ...config.env },
|
|
432
|
+
stdio: ['ignore', fd, fd],
|
|
433
|
+
windowsHide: true,
|
|
434
|
+
});
|
|
435
|
+
try { fs.closeSync(fd); } catch {}
|
|
436
|
+
child.on('exit', (code, signal) => {
|
|
437
|
+
append(config.serviceLog, \`amalgm run exited code=\${code ?? ''} signal=\${signal ?? ''}\`);
|
|
438
|
+
child = null;
|
|
439
|
+
if (stopping || fs.existsSync(config.stopFile)) return cleanup(0);
|
|
440
|
+
setTimeout(launch, config.restartDelayMs);
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
ensureDir(config.amalgmDir);
|
|
445
|
+
ensureDir(config.logDir);
|
|
446
|
+
if (!acquireLock()) process.exit(0);
|
|
447
|
+
writePid();
|
|
448
|
+
try { fs.rmSync(config.stopFile, { force: true }); } catch {}
|
|
449
|
+
process.on('SIGTERM', () => cleanup(0));
|
|
450
|
+
process.on('SIGINT', () => cleanup(0));
|
|
451
|
+
if (process.platform !== 'win32') process.on('SIGHUP', () => cleanup(0));
|
|
452
|
+
launch();
|
|
453
|
+
`;
|
|
454
|
+
fs.writeFileSync(SERVICE_NODE_SCRIPT_FILE, source, { mode: 0o700 });
|
|
455
|
+
try {
|
|
456
|
+
fs.chmodSync(SERVICE_NODE_SCRIPT_FILE, 0o700);
|
|
457
|
+
} catch {
|
|
458
|
+
// Best effort.
|
|
459
|
+
}
|
|
460
|
+
return SERVICE_NODE_SCRIPT_FILE;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function launchdPlistPath() {
|
|
464
|
+
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${SERVICE_LABEL}.plist`);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function writeLaunchdPlist(localOnly) {
|
|
468
|
+
const plistPath = launchdPlistPath();
|
|
469
|
+
ensureDir(path.dirname(plistPath), 0o700);
|
|
470
|
+
ensureDir(LOG_DIR, 0o700);
|
|
471
|
+
const env = buildServiceEnv(localOnly);
|
|
472
|
+
const envXml = Object.entries(env)
|
|
473
|
+
.map(([key, value]) => ` <key>${plistEscape(key)}</key>\n <string>${plistEscape(value)}</string>`)
|
|
474
|
+
.join('\n');
|
|
475
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
476
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
477
|
+
<plist version="1.0">
|
|
478
|
+
<dict>
|
|
479
|
+
<key>Label</key>
|
|
480
|
+
<string>${plistEscape(SERVICE_LABEL)}</string>
|
|
481
|
+
<key>ProgramArguments</key>
|
|
482
|
+
<array>
|
|
483
|
+
<string>${plistEscape(process.execPath)}</string>
|
|
484
|
+
<string>${plistEscape(AMALGM_BIN)}</string>
|
|
485
|
+
<string>run</string>
|
|
486
|
+
</array>
|
|
487
|
+
<key>EnvironmentVariables</key>
|
|
488
|
+
<dict>
|
|
489
|
+
${envXml}
|
|
490
|
+
</dict>
|
|
491
|
+
<key>WorkingDirectory</key>
|
|
492
|
+
<string>${plistEscape(os.homedir())}</string>
|
|
493
|
+
<key>KeepAlive</key>
|
|
494
|
+
<true/>
|
|
495
|
+
<key>RunAtLoad</key>
|
|
496
|
+
<true/>
|
|
497
|
+
<key>StandardOutPath</key>
|
|
498
|
+
<string>${plistEscape(path.join(LOG_DIR, 'daemon.log'))}</string>
|
|
499
|
+
<key>StandardErrorPath</key>
|
|
500
|
+
<string>${plistEscape(path.join(LOG_DIR, 'daemon.log'))}</string>
|
|
501
|
+
</dict>
|
|
502
|
+
</plist>
|
|
503
|
+
`;
|
|
504
|
+
fs.writeFileSync(plistPath, plist, { mode: 0o600 });
|
|
505
|
+
return plistPath;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function systemdUnitPath() {
|
|
509
|
+
return path.join(os.homedir(), '.config', 'systemd', 'user', `${SERVICE_LABEL}.service`);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function writeSystemdUnit(localOnly) {
|
|
513
|
+
const unitPath = systemdUnitPath();
|
|
514
|
+
ensureDir(path.dirname(unitPath), 0o700);
|
|
515
|
+
ensureDir(LOG_DIR, 0o700);
|
|
516
|
+
const env = {
|
|
517
|
+
...buildServiceEnv(localOnly),
|
|
518
|
+
AMALGM_NODE: process.execPath,
|
|
519
|
+
AMALGM_BIN,
|
|
520
|
+
AMALGM_DAEMON_LOG: path.join(LOG_DIR, 'daemon.log'),
|
|
521
|
+
};
|
|
522
|
+
const envLines = Object.entries(env)
|
|
523
|
+
.map(([key, value]) => `Environment=${systemdQuote(`${key}=${value}`)}`)
|
|
524
|
+
.join('\n');
|
|
525
|
+
const unit = `[Unit]
|
|
526
|
+
Description=Amalgm local runtime
|
|
527
|
+
After=network-online.target
|
|
528
|
+
Wants=network-online.target
|
|
529
|
+
|
|
530
|
+
[Service]
|
|
531
|
+
Type=simple
|
|
532
|
+
WorkingDirectory=${systemdQuote(os.homedir())}
|
|
533
|
+
${envLines}
|
|
534
|
+
ExecStart=/bin/sh -lc ${systemdQuote('exec "$AMALGM_NODE" "$AMALGM_BIN" run >> "$AMALGM_DAEMON_LOG" 2>&1')}
|
|
535
|
+
Restart=always
|
|
536
|
+
RestartSec=5
|
|
537
|
+
KillSignal=SIGTERM
|
|
538
|
+
TimeoutStopSec=20
|
|
539
|
+
|
|
540
|
+
[Install]
|
|
541
|
+
WantedBy=default.target
|
|
542
|
+
`;
|
|
543
|
+
fs.writeFileSync(unitPath, unit, { mode: 0o600 });
|
|
544
|
+
return unitPath;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function serviceStateFor(backend, localOnly, files = {}) {
|
|
548
|
+
return {
|
|
549
|
+
backend,
|
|
550
|
+
label: SERVICE_LABEL,
|
|
551
|
+
local_only: !!localOnly,
|
|
552
|
+
package_version: PACKAGE_VERSION,
|
|
553
|
+
package_root: PACKAGE_ROOT,
|
|
554
|
+
node: process.execPath,
|
|
555
|
+
bin: AMALGM_BIN,
|
|
556
|
+
files,
|
|
557
|
+
updated_at: new Date().toISOString(),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function installService(options = {}) {
|
|
562
|
+
ensureDir(AMALGM_DIR, 0o700);
|
|
563
|
+
ensureDir(LOG_DIR, 0o700);
|
|
564
|
+
const localOnly = !!options.localOnly;
|
|
565
|
+
const { backend, detail } = detectServiceBackend(options.mode || 'auto');
|
|
566
|
+
let files = {};
|
|
567
|
+
|
|
568
|
+
if (backend === 'launchd') {
|
|
569
|
+
files.plist = writeLaunchdPlist(localOnly);
|
|
570
|
+
} else if (backend === 'systemd') {
|
|
571
|
+
files.unit = writeSystemdUnit(localOnly);
|
|
572
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore', timeout: 5000 });
|
|
573
|
+
} else {
|
|
574
|
+
files.script = writePortableScript(localOnly);
|
|
575
|
+
files.nodeScript = writePortableNodeScript(localOnly);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
const state = {
|
|
579
|
+
...serviceStateFor(backend, localOnly, files),
|
|
580
|
+
detail,
|
|
581
|
+
installed_at: new Date().toISOString(),
|
|
582
|
+
};
|
|
583
|
+
writeJson(SERVICE_STATE_FILE, state);
|
|
584
|
+
return state;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function stateMatchesRequest(state, options = {}) {
|
|
588
|
+
if (!state) return false;
|
|
589
|
+
if (state.package_root !== PACKAGE_ROOT) return false;
|
|
590
|
+
if (state.package_version !== PACKAGE_VERSION) return false;
|
|
591
|
+
if (!!state.local_only !== !!options.localOnly) return false;
|
|
592
|
+
const requested = normalizeMode(options.mode || 'auto');
|
|
593
|
+
if (requested === 'auto') {
|
|
594
|
+
const desired = detectServiceBackend('auto');
|
|
595
|
+
if (desired.backend !== state.backend) return false;
|
|
596
|
+
} else if (requested !== state.backend) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function ensureServiceInstalled(options = {}) {
|
|
603
|
+
const state = readJson(SERVICE_STATE_FILE, null);
|
|
604
|
+
if (stateMatchesRequest(state, options)) return state;
|
|
605
|
+
return installService(options);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
function runCommand(command, args, options = {}) {
|
|
609
|
+
const result = spawnSync(command, args, {
|
|
610
|
+
encoding: 'utf8',
|
|
611
|
+
timeout: options.timeout || 10000,
|
|
612
|
+
stdio: options.stdio || 'pipe',
|
|
613
|
+
});
|
|
614
|
+
if (result.status !== 0) {
|
|
615
|
+
const detail = [result.stderr, result.stdout].filter(Boolean).join('\n').trim();
|
|
616
|
+
throw new Error(`${command} ${args.join(' ')} failed${detail ? `: ${detail}` : ''}`);
|
|
617
|
+
}
|
|
618
|
+
return result;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function launchdDomain() {
|
|
622
|
+
return `gui/${process.getuid ? process.getuid() : ''}`;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function startLaunchd(state) {
|
|
626
|
+
const plist = state?.files?.plist || launchdPlistPath();
|
|
627
|
+
const domain = launchdDomain();
|
|
628
|
+
spawnSync('launchctl', ['bootout', domain, plist], { stdio: 'ignore', timeout: 5000 });
|
|
629
|
+
runCommand('launchctl', ['bootstrap', domain, plist], { timeout: 10000 });
|
|
630
|
+
spawnSync('launchctl', ['enable', `${domain}/${SERVICE_LABEL}`], { stdio: 'ignore', timeout: 5000 });
|
|
631
|
+
spawnSync('launchctl', ['kickstart', '-k', `${domain}/${SERVICE_LABEL}`], { stdio: 'ignore', timeout: 5000 });
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function stopLaunchd(state) {
|
|
635
|
+
const plist = state?.files?.plist || launchdPlistPath();
|
|
636
|
+
spawnSync('launchctl', ['bootout', launchdDomain(), plist], { stdio: 'ignore', timeout: 10000 });
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function startSystemd() {
|
|
640
|
+
runCommand('systemctl', ['--user', 'daemon-reload'], { timeout: 10000 });
|
|
641
|
+
spawnSync('systemctl', ['--user', 'enable', SERVICE_LABEL], { stdio: 'ignore', timeout: 10000 });
|
|
642
|
+
runCommand('systemctl', ['--user', 'start', SERVICE_LABEL], { timeout: 10000 });
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function stopSystemd() {
|
|
646
|
+
spawnSync('systemctl', ['--user', 'stop', SERVICE_LABEL], { stdio: 'ignore', timeout: 10000 });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function startPortable() {
|
|
650
|
+
const state = readJson(SERVICE_STATE_FILE, null);
|
|
651
|
+
writePortableScript(state?.local_only);
|
|
652
|
+
writePortableNodeScript(state?.local_only);
|
|
653
|
+
const pid = readPidFile(SERVICE_PID_FILE);
|
|
654
|
+
if (isPidRunning(pid)) return;
|
|
655
|
+
removeFile(SERVICE_STOP_FILE);
|
|
656
|
+
ensureDir(LOG_DIR, 0o700);
|
|
657
|
+
const logFd = fs.openSync(SERVICE_LOG_FILE, 'a');
|
|
658
|
+
const command = process.platform === 'win32' ? process.execPath : '/bin/sh';
|
|
659
|
+
const args = process.platform === 'win32'
|
|
660
|
+
? [state?.files?.nodeScript || SERVICE_NODE_SCRIPT_FILE]
|
|
661
|
+
: [state?.files?.script || SERVICE_SCRIPT_FILE];
|
|
662
|
+
const child = spawn(command, args, {
|
|
663
|
+
detached: true,
|
|
664
|
+
env: {
|
|
665
|
+
...process.env,
|
|
666
|
+
...buildServiceEnv(state?.local_only),
|
|
667
|
+
},
|
|
668
|
+
cwd: os.homedir(),
|
|
669
|
+
stdio: ['ignore', logFd, logFd],
|
|
670
|
+
windowsHide: true,
|
|
671
|
+
});
|
|
672
|
+
child.unref();
|
|
673
|
+
fs.closeSync(logFd);
|
|
674
|
+
writeJson(SERVICE_PID_FILE, {
|
|
675
|
+
pid: child.pid,
|
|
676
|
+
backend: 'portable',
|
|
677
|
+
started_at: new Date().toISOString(),
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function stopDaemonProcess() {
|
|
682
|
+
const pid = readPidFile(PID_FILE);
|
|
683
|
+
if (!isPidRunning(pid)) {
|
|
684
|
+
removeFile(PID_FILE);
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
try {
|
|
688
|
+
process.kill(pid, 'SIGTERM');
|
|
689
|
+
} catch {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
const deadline = Date.now() + 5000;
|
|
693
|
+
while (Date.now() < deadline && isPidRunning(pid)) {
|
|
694
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
695
|
+
}
|
|
696
|
+
if (isPidRunning(pid)) {
|
|
697
|
+
try {
|
|
698
|
+
process.kill(pid, 'SIGKILL');
|
|
699
|
+
} catch {
|
|
700
|
+
// noop
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
removeFile(PID_FILE);
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function stopPortable() {
|
|
708
|
+
ensureDir(AMALGM_DIR, 0o700);
|
|
709
|
+
fs.writeFileSync(SERVICE_STOP_FILE, `${new Date().toISOString()}\n`, { mode: 0o600 });
|
|
710
|
+
const pid = readPidFile(SERVICE_PID_FILE);
|
|
711
|
+
if (isPidRunning(pid)) {
|
|
712
|
+
try {
|
|
713
|
+
process.kill(pid, 'SIGTERM');
|
|
714
|
+
} catch {
|
|
715
|
+
// noop
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
const deadline = Date.now() + 5000;
|
|
719
|
+
while (Date.now() < deadline && isPidRunning(pid)) {
|
|
720
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
721
|
+
}
|
|
722
|
+
removeFile(SERVICE_PID_FILE);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function startService(options = {}) {
|
|
726
|
+
const state = ensureServiceInstalled(options);
|
|
727
|
+
removeFile(SERVICE_STOP_FILE);
|
|
728
|
+
if (state.backend === 'launchd') startLaunchd(state);
|
|
729
|
+
else if (state.backend === 'systemd') startSystemd(state);
|
|
730
|
+
else startPortable(state);
|
|
731
|
+
return getServiceStatus();
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function stopService(options = {}) {
|
|
735
|
+
const state = readJson(SERVICE_STATE_FILE, null);
|
|
736
|
+
ensureDir(AMALGM_DIR, 0o700);
|
|
737
|
+
fs.writeFileSync(SERVICE_STOP_FILE, `${new Date().toISOString()}\n`, { mode: 0o600 });
|
|
738
|
+
|
|
739
|
+
if (state?.backend === 'launchd') stopLaunchd(state);
|
|
740
|
+
else if (state?.backend === 'systemd') stopSystemd(state);
|
|
741
|
+
else stopPortable();
|
|
742
|
+
|
|
743
|
+
const stoppedDaemon = stopDaemonProcess();
|
|
744
|
+
if (!options.preserveStopFile) removeFile(SERVICE_STOP_FILE);
|
|
745
|
+
return { service: getServiceStatus(), stoppedDaemon };
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function uninstallService() {
|
|
749
|
+
const state = readJson(SERVICE_STATE_FILE, null);
|
|
750
|
+
stopService({ preserveStopFile: true });
|
|
751
|
+
if (state?.backend === 'systemd') {
|
|
752
|
+
spawnSync('systemctl', ['--user', 'disable', SERVICE_LABEL], { stdio: 'ignore', timeout: 10000 });
|
|
753
|
+
removeFile(state?.files?.unit || systemdUnitPath());
|
|
754
|
+
spawnSync('systemctl', ['--user', 'daemon-reload'], { stdio: 'ignore', timeout: 5000 });
|
|
755
|
+
} else if (state?.backend === 'launchd') {
|
|
756
|
+
removeFile(state?.files?.plist || launchdPlistPath());
|
|
757
|
+
} else {
|
|
758
|
+
removeFile(SERVICE_SCRIPT_FILE);
|
|
759
|
+
removeFile(SERVICE_NODE_SCRIPT_FILE);
|
|
760
|
+
}
|
|
761
|
+
removeFile(SERVICE_STATE_FILE);
|
|
762
|
+
removeFile(SERVICE_STOP_FILE);
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function systemdRunning() {
|
|
767
|
+
if (!systemdAvailable()) return false;
|
|
768
|
+
const result = spawnSync('systemctl', ['--user', 'is-active', '--quiet', SERVICE_LABEL], {
|
|
769
|
+
encoding: 'utf8',
|
|
770
|
+
timeout: 3000,
|
|
771
|
+
});
|
|
772
|
+
const output = [result.stderr, result.stdout].filter(Boolean).join('\n');
|
|
773
|
+
return result.status === 0 && !systemdUnavailableOutput(output);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function launchdRunning() {
|
|
777
|
+
const result = spawnSync('launchctl', ['print', `${launchdDomain()}/${SERVICE_LABEL}`], {
|
|
778
|
+
stdio: 'ignore',
|
|
779
|
+
timeout: 3000,
|
|
780
|
+
});
|
|
781
|
+
return result.status === 0;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function getServiceStatus() {
|
|
785
|
+
const state = readJson(SERVICE_STATE_FILE, null);
|
|
786
|
+
const backend = state?.backend || null;
|
|
787
|
+
let installed = false;
|
|
788
|
+
let running = false;
|
|
789
|
+
let pid = null;
|
|
790
|
+
|
|
791
|
+
if (backend === 'systemd') {
|
|
792
|
+
installed = fs.existsSync(state?.files?.unit || systemdUnitPath());
|
|
793
|
+
running = installed && systemdRunning();
|
|
794
|
+
} else if (backend === 'launchd') {
|
|
795
|
+
installed = fs.existsSync(state?.files?.plist || launchdPlistPath());
|
|
796
|
+
running = installed && launchdRunning();
|
|
797
|
+
} else if (backend === 'portable') {
|
|
798
|
+
installed = fs.existsSync(state?.files?.script || SERVICE_SCRIPT_FILE)
|
|
799
|
+
|| fs.existsSync(state?.files?.nodeScript || SERVICE_NODE_SCRIPT_FILE);
|
|
800
|
+
pid = readPidFile(SERVICE_PID_FILE);
|
|
801
|
+
running = isPidRunning(pid);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
return {
|
|
805
|
+
backend,
|
|
806
|
+
detail: state?.detail || null,
|
|
807
|
+
installed,
|
|
808
|
+
running,
|
|
809
|
+
pid,
|
|
810
|
+
state,
|
|
811
|
+
stopRequested: fs.existsSync(SERVICE_STOP_FILE),
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
module.exports = {
|
|
816
|
+
detectServiceBackend,
|
|
817
|
+
ensureServiceInstalled,
|
|
818
|
+
getServiceStatus,
|
|
819
|
+
installService,
|
|
820
|
+
startService,
|
|
821
|
+
stopService,
|
|
822
|
+
uninstallService,
|
|
823
|
+
};
|
package/lib/supervisor.js
CHANGED
|
@@ -4,7 +4,7 @@ const crypto = require('crypto');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const path = require('path');
|
|
7
|
-
const { spawn } = require('child_process');
|
|
7
|
+
const { spawn, spawnSync } = require('child_process');
|
|
8
8
|
const net = require('net');
|
|
9
9
|
|
|
10
10
|
const {
|
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
RUNTIME_DIR,
|
|
15
15
|
RUNTIME_STATE_FILE,
|
|
16
16
|
RUNTIME_TOKEN_FILE,
|
|
17
|
+
SHUTDOWN_REASON_FILE,
|
|
17
18
|
} = require('./paths');
|
|
18
19
|
const {
|
|
19
20
|
loadComputerRecord,
|
|
@@ -56,6 +57,42 @@ function writeJsonSecret(file, data) {
|
|
|
56
57
|
}
|
|
57
58
|
}
|
|
58
59
|
|
|
60
|
+
function readProcessCommand(pid) {
|
|
61
|
+
if (!Number.isInteger(pid) || pid <= 0) return '';
|
|
62
|
+
try {
|
|
63
|
+
const raw = fs.readFileSync(`/proc/${pid}/cmdline`);
|
|
64
|
+
const cmd = raw.toString('utf8').replace(/\0/g, ' ').trim();
|
|
65
|
+
if (cmd) return cmd;
|
|
66
|
+
} catch {
|
|
67
|
+
// /proc is Linux-only.
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const result = spawnSync('ps', ['-p', String(pid), '-o', 'command='], {
|
|
71
|
+
encoding: 'utf8',
|
|
72
|
+
timeout: 1000,
|
|
73
|
+
});
|
|
74
|
+
return String(result.stdout || '').trim();
|
|
75
|
+
} catch {
|
|
76
|
+
return '';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function writeShutdownReason(signal) {
|
|
81
|
+
const at = new Date().toISOString();
|
|
82
|
+
const reason = {
|
|
83
|
+
at,
|
|
84
|
+
signal,
|
|
85
|
+
reason: signal ? `received ${signal}` : 'cleanup requested',
|
|
86
|
+
pid: process.pid,
|
|
87
|
+
ppid: process.ppid,
|
|
88
|
+
parent_command: readProcessCommand(process.ppid) || null,
|
|
89
|
+
uptime_seconds: Math.round(process.uptime()),
|
|
90
|
+
package_version: PACKAGE_VERSION,
|
|
91
|
+
};
|
|
92
|
+
writeJsonSecret(SHUTDOWN_REASON_FILE, reason);
|
|
93
|
+
console.log(`[supervisor] shutdown requested signal=${signal || 'unknown'} pid=${process.pid} ppid=${process.ppid} parent=${reason.parent_command || 'unknown'} uptime=${reason.uptime_seconds}s`);
|
|
94
|
+
}
|
|
95
|
+
|
|
59
96
|
function runtimeEntry(relativePath) {
|
|
60
97
|
return path.join(RUNTIME_DIR, relativePath);
|
|
61
98
|
}
|
|
@@ -447,9 +484,10 @@ async function startSupervisor(options = {}) {
|
|
|
447
484
|
|
|
448
485
|
await new Promise((resolve) => {
|
|
449
486
|
let cleaned = false;
|
|
450
|
-
const cleanup = () => {
|
|
487
|
+
const cleanup = (signal) => {
|
|
451
488
|
if (cleaned) return;
|
|
452
489
|
cleaned = true;
|
|
490
|
+
writeShutdownReason(signal);
|
|
453
491
|
for (const tunnel of tunnels) tunnel.stop();
|
|
454
492
|
for (const proc of managed) proc.stop();
|
|
455
493
|
try {
|
|
@@ -467,9 +505,9 @@ async function startSupervisor(options = {}) {
|
|
|
467
505
|
resolve();
|
|
468
506
|
};
|
|
469
507
|
|
|
470
|
-
process.once('SIGINT', cleanup);
|
|
471
|
-
process.once('SIGTERM', cleanup);
|
|
472
|
-
process.once('SIGHUP', cleanup);
|
|
508
|
+
process.once('SIGINT', () => cleanup('SIGINT'));
|
|
509
|
+
process.once('SIGTERM', () => cleanup('SIGTERM'));
|
|
510
|
+
process.once('SIGHUP', () => cleanup('SIGHUP'));
|
|
473
511
|
});
|
|
474
512
|
}
|
|
475
513
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amalgm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"description": "Amalgm local computer runtime: login, MCP, chat, events, previews, and tunnels.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"private": false,
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"sync-runtime": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
18
18
|
"prepack": "node ../../scripts/sync-npm-package-runtime.mjs",
|
|
19
19
|
"pack:dry": "npm pack --dry-run",
|
|
20
|
-
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/fs-watcher.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
20
|
+
"check": "node --check bin/amalgm.js && node --check lib/auth-store.js && node --check lib/cli.js && node --check lib/paths.js && node --check lib/service.js && node --check lib/supervisor.js && node --check lib/tunnel-chat.js && node --check lib/tunnel-events.js && node --check runtime/scripts/runtime-auth.js && node --check runtime/scripts/proxy-token-store.js && node --check runtime/scripts/local-gateway.js && node --check runtime/scripts/port-monitor.js && node --check runtime/scripts/fs-watcher.js && node --check runtime/scripts/chat-server.js && node --check runtime/scripts/chat-server/index.js && node --check runtime/scripts/chat-server/config.js && node --check runtime/scripts/chat-core/tooling/native-binaries.js && node --check runtime/scripts/chat-core/tooling/package-import.js && node --check runtime/scripts/amalgm-mcp/index.js && node --check runtime/scripts/amalgm-mcp/config.js"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
23
|
"node": ">=20"
|