fullcourtdefense-cli 1.15.3 → 1.15.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/daemon.js +145 -36
- package/dist/runtimeConfig.d.ts +2 -0
- package/dist/runtimeConfig.js +2 -0
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -77,6 +77,55 @@ function daemonDir() {
|
|
|
77
77
|
function pidFile() {
|
|
78
78
|
return path.join(daemonDir(), 'daemon.pid');
|
|
79
79
|
}
|
|
80
|
+
/** Sidecar next to the pid lock recording WHICH build owns the daemon. */
|
|
81
|
+
function metaFile() {
|
|
82
|
+
return path.join(daemonDir(), 'daemon.meta.json');
|
|
83
|
+
}
|
|
84
|
+
function readDaemonMeta() {
|
|
85
|
+
try {
|
|
86
|
+
const meta = JSON.parse(fs.readFileSync(metaFile(), 'utf8'));
|
|
87
|
+
return meta && typeof meta.pid === 'number' ? meta : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function writeDaemonMeta() {
|
|
94
|
+
try {
|
|
95
|
+
const meta = {
|
|
96
|
+
pid: process.pid,
|
|
97
|
+
version: cliVersion(),
|
|
98
|
+
entry: cliEntry(),
|
|
99
|
+
startedAt: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
fs.writeFileSync(metaFile(), JSON.stringify(meta, null, 2), 'utf8');
|
|
102
|
+
}
|
|
103
|
+
catch { /* best-effort */ }
|
|
104
|
+
}
|
|
105
|
+
/** Kill a process and wait (up to ~5s) for it to actually exit. */
|
|
106
|
+
function stopPid(pid) {
|
|
107
|
+
try {
|
|
108
|
+
process.kill(pid);
|
|
109
|
+
}
|
|
110
|
+
catch { /* may already be gone */ }
|
|
111
|
+
const deadline = Date.now() + 5_000;
|
|
112
|
+
const sleeper = new Int32Array(new SharedArrayBuffer(4));
|
|
113
|
+
while (Date.now() < deadline) {
|
|
114
|
+
if (!isPidAlive(pid))
|
|
115
|
+
return true;
|
|
116
|
+
Atomics.wait(sleeper, 0, 0, 250);
|
|
117
|
+
}
|
|
118
|
+
if (process.platform === 'win32') {
|
|
119
|
+
(0, child_process_1.spawnSync)('taskkill', ['/PID', String(pid), '/F'], { stdio: 'ignore', windowsHide: true, timeout: 5_000 });
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
try {
|
|
123
|
+
process.kill(pid, 'SIGKILL');
|
|
124
|
+
}
|
|
125
|
+
catch { /* ignore */ }
|
|
126
|
+
}
|
|
127
|
+
return !isPidAlive(pid);
|
|
128
|
+
}
|
|
80
129
|
function logFile() {
|
|
81
130
|
return path.join(daemonDir(), 'daemon.log');
|
|
82
131
|
}
|
|
@@ -117,24 +166,56 @@ function isPidAlive(pid) {
|
|
|
117
166
|
return error.code === 'EPERM';
|
|
118
167
|
}
|
|
119
168
|
}
|
|
120
|
-
/**
|
|
169
|
+
/** Lexicographic-free semver compare; missing/unparseable sorts oldest. */
|
|
170
|
+
function compareVersions(a, b) {
|
|
171
|
+
const parse = (value) => String(value || '0').split('.').map(part => parseInt(part, 10) || 0);
|
|
172
|
+
const [pa, pb] = [parse(a), parse(b)];
|
|
173
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
174
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
175
|
+
if (diff !== 0)
|
|
176
|
+
return diff;
|
|
177
|
+
}
|
|
178
|
+
return 0;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Take the single-instance lock. A NEWER build supersedes a running older
|
|
182
|
+
* daemon (e.g. the MSI-bundled copy kept running after an npm update): the old
|
|
183
|
+
* process is stopped and this one takes over, so the fleet never keeps running
|
|
184
|
+
* stale enforcement code silently. Same-or-newer versions keep the lock.
|
|
185
|
+
*/
|
|
121
186
|
function acquirePidLock() {
|
|
122
187
|
fs.mkdirSync(daemonDir(), { recursive: true });
|
|
123
188
|
try {
|
|
124
189
|
const existing = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
125
190
|
if (Number.isFinite(existing) && existing > 0 && existing !== process.pid && isPidAlive(existing)) {
|
|
126
|
-
|
|
191
|
+
const meta = readDaemonMeta();
|
|
192
|
+
const runningVersion = meta && meta.pid === existing ? meta.version : undefined;
|
|
193
|
+
// No meta = pre-1.15.4 build (never wrote one) → treated as older.
|
|
194
|
+
if (compareVersions(cliVersion(), runningVersion) > 0) {
|
|
195
|
+
log(`Superseding older daemon (pid ${existing}, version ${runningVersion || 'unknown'}) with ${cliVersion() || 'this build'}.`);
|
|
196
|
+
if (!stopPid(existing))
|
|
197
|
+
return false; // could not stop it — yield rather than double-run
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
127
202
|
}
|
|
128
203
|
}
|
|
129
204
|
catch { /* no pidfile — free to start */ }
|
|
130
205
|
fs.writeFileSync(pidFile(), String(process.pid), 'utf8');
|
|
206
|
+
writeDaemonMeta();
|
|
131
207
|
return true;
|
|
132
208
|
}
|
|
133
209
|
function releasePidLock() {
|
|
134
210
|
try {
|
|
135
211
|
const recorded = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
136
|
-
if (recorded === process.pid)
|
|
212
|
+
if (recorded === process.pid) {
|
|
137
213
|
fs.unlinkSync(pidFile());
|
|
214
|
+
try {
|
|
215
|
+
fs.unlinkSync(metaFile());
|
|
216
|
+
}
|
|
217
|
+
catch { /* best-effort */ }
|
|
218
|
+
}
|
|
138
219
|
}
|
|
139
220
|
catch { /* best-effort */ }
|
|
140
221
|
}
|
|
@@ -356,6 +437,7 @@ async function runDaemon(args, config) {
|
|
|
356
437
|
shieldKey: creds.shieldKey,
|
|
357
438
|
developerName: identity.developerName,
|
|
358
439
|
machineName: identity.hostname,
|
|
440
|
+
machineId: identity.machineId,
|
|
359
441
|
force: true,
|
|
360
442
|
});
|
|
361
443
|
if (bundle.suspended && !suspended) {
|
|
@@ -467,8 +549,15 @@ function isWindowsRunKeyInstalled() {
|
|
|
467
549
|
function startDaemonNowWindows(vbs) {
|
|
468
550
|
try {
|
|
469
551
|
const existing = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
470
|
-
if (Number.isFinite(existing) && existing > 0 && isPidAlive(existing))
|
|
471
|
-
|
|
552
|
+
if (Number.isFinite(existing) && existing > 0 && isPidAlive(existing)) {
|
|
553
|
+
// Skip only when the running daemon is the same version or newer; an
|
|
554
|
+
// older one (e.g. stale MSI copy) gets superseded by the spawned daemon,
|
|
555
|
+
// whose acquirePidLock() stops it and takes over.
|
|
556
|
+
const meta = readDaemonMeta();
|
|
557
|
+
const runningVersion = meta && meta.pid === existing ? meta.version : undefined;
|
|
558
|
+
if (compareVersions(cliVersion(), runningVersion) <= 0)
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
472
561
|
}
|
|
473
562
|
catch { /* not running */ }
|
|
474
563
|
const escaped = vbs.replace(/'/g, "''");
|
|
@@ -513,23 +602,23 @@ function launchdPlistPath() {
|
|
|
513
602
|
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${LAUNCHD_LABEL}.plist`);
|
|
514
603
|
}
|
|
515
604
|
function installMacos() {
|
|
516
|
-
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
517
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
518
|
-
<plist version="1.0">
|
|
519
|
-
<dict>
|
|
520
|
-
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
521
|
-
<key>ProgramArguments</key>
|
|
522
|
-
<array>
|
|
523
|
-
<string>${process.execPath}</string>
|
|
524
|
-
<string>${cliEntry()}</string>
|
|
525
|
-
<string>daemon</string>
|
|
526
|
-
</array>
|
|
527
|
-
<key>RunAtLoad</key><true/>
|
|
528
|
-
<key>KeepAlive</key><true/>
|
|
529
|
-
<key>StandardOutPath</key><string>${logFile()}</string>
|
|
530
|
-
<key>StandardErrorPath</key><string>${logFile()}</string>
|
|
531
|
-
</dict>
|
|
532
|
-
</plist>
|
|
605
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
606
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
607
|
+
<plist version="1.0">
|
|
608
|
+
<dict>
|
|
609
|
+
<key>Label</key><string>${LAUNCHD_LABEL}</string>
|
|
610
|
+
<key>ProgramArguments</key>
|
|
611
|
+
<array>
|
|
612
|
+
<string>${process.execPath}</string>
|
|
613
|
+
<string>${cliEntry()}</string>
|
|
614
|
+
<string>daemon</string>
|
|
615
|
+
</array>
|
|
616
|
+
<key>RunAtLoad</key><true/>
|
|
617
|
+
<key>KeepAlive</key><true/>
|
|
618
|
+
<key>StandardOutPath</key><string>${logFile()}</string>
|
|
619
|
+
<key>StandardErrorPath</key><string>${logFile()}</string>
|
|
620
|
+
</dict>
|
|
621
|
+
</plist>
|
|
533
622
|
`;
|
|
534
623
|
const file = launchdPlistPath();
|
|
535
624
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -567,16 +656,16 @@ function isDaemonAutostartInstalled() {
|
|
|
567
656
|
return fs.existsSync(systemdUnitPath());
|
|
568
657
|
}
|
|
569
658
|
function installLinux() {
|
|
570
|
-
const unit = `[Unit]
|
|
571
|
-
Description=FullCourtDefense resident daemon (config watch + heartbeat)
|
|
572
|
-
|
|
573
|
-
[Service]
|
|
574
|
-
ExecStart=${JSON.stringify(process.execPath)} ${JSON.stringify(cliEntry())} daemon
|
|
575
|
-
Restart=always
|
|
576
|
-
RestartSec=10
|
|
577
|
-
|
|
578
|
-
[Install]
|
|
579
|
-
WantedBy=default.target
|
|
659
|
+
const unit = `[Unit]
|
|
660
|
+
Description=FullCourtDefense resident daemon (config watch + heartbeat)
|
|
661
|
+
|
|
662
|
+
[Service]
|
|
663
|
+
ExecStart=${JSON.stringify(process.execPath)} ${JSON.stringify(cliEntry())} daemon
|
|
664
|
+
Restart=always
|
|
665
|
+
RestartSec=10
|
|
666
|
+
|
|
667
|
+
[Install]
|
|
668
|
+
WantedBy=default.target
|
|
580
669
|
`;
|
|
581
670
|
const file = systemdUnitPath();
|
|
582
671
|
try {
|
|
@@ -621,9 +710,20 @@ function statusCommand() {
|
|
|
621
710
|
try {
|
|
622
711
|
const pid = Number(fs.readFileSync(pidFile(), 'utf8').trim());
|
|
623
712
|
running = Number.isFinite(pid) && pid > 0 && isPidAlive(pid);
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
713
|
+
if (running) {
|
|
714
|
+
const meta = readDaemonMeta();
|
|
715
|
+
const runningVersion = meta && meta.pid === pid ? meta.version : undefined;
|
|
716
|
+
const mine = cliVersion();
|
|
717
|
+
console.log(`${COLOR.green}Running${COLOR.reset} (pid ${pid}, version ${runningVersion || 'unknown — pre-1.15.4 build'})`);
|
|
718
|
+
if (meta?.entry)
|
|
719
|
+
console.log(`${COLOR.gray}Binary:${COLOR.reset} ${meta.entry}`);
|
|
720
|
+
if (mine && compareVersions(mine, runningVersion) > 0) {
|
|
721
|
+
console.log(`${COLOR.yellow}Outdated:${COLOR.reset} this CLI is ${mine} — run ${COLOR.bold}fullcourtdefense daemon${COLOR.reset} to supersede the old daemon.`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
console.log(`${COLOR.yellow}Not running${COLOR.reset} (stale pid file: ${pidFile()})`);
|
|
726
|
+
}
|
|
627
727
|
}
|
|
628
728
|
catch {
|
|
629
729
|
console.log(`${COLOR.yellow}Not running${COLOR.reset} (no pid file)`);
|
|
@@ -631,7 +731,16 @@ function statusCommand() {
|
|
|
631
731
|
console.log(`${COLOR.gray}Log:${COLOR.reset} ${logFile()}`);
|
|
632
732
|
console.log(`${COLOR.gray}Autostart:${COLOR.reset}`);
|
|
633
733
|
if (process.platform === 'win32') {
|
|
634
|
-
(0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', TASK_NAME], { stdio: '
|
|
734
|
+
const task = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', TASK_NAME], { stdio: 'ignore', windowsHide: true, timeout: 5_000 });
|
|
735
|
+
if (task.status === 0) {
|
|
736
|
+
console.log(` Scheduled Task "${TASK_NAME}" installed`);
|
|
737
|
+
}
|
|
738
|
+
else if (isWindowsRunKeyInstalled()) {
|
|
739
|
+
console.log(` Run key installed (HKCU\\...\\Run\\${WINDOWS_RUN_VALUE}) — starts at logon`);
|
|
740
|
+
}
|
|
741
|
+
else {
|
|
742
|
+
console.log(' not installed — run: fullcourtdefense daemon --install true');
|
|
743
|
+
}
|
|
635
744
|
}
|
|
636
745
|
else if (process.platform === 'darwin') {
|
|
637
746
|
console.log(fs.existsSync(launchdPlistPath()) ? ` launchd agent installed (${launchdPlistPath()})` : ' not installed');
|
package/dist/runtimeConfig.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface FetchBundleInput {
|
|
|
39
39
|
shieldKey?: string;
|
|
40
40
|
developerName?: string;
|
|
41
41
|
machineName?: string;
|
|
42
|
+
/** Exact fleet machine ID — makes the server's pending-action lookup deterministic. */
|
|
43
|
+
machineId?: string;
|
|
42
44
|
/** Override cache TTL (ms). */
|
|
43
45
|
ttlMs?: number;
|
|
44
46
|
/** Force a network refresh regardless of cache freshness. */
|
package/dist/runtimeConfig.js
CHANGED
|
@@ -83,6 +83,8 @@ async function getRuntimeBundle(input) {
|
|
|
83
83
|
params.set('developerName', input.developerName);
|
|
84
84
|
if (input.machineName)
|
|
85
85
|
params.set('machineName', input.machineName);
|
|
86
|
+
if (input.machineId)
|
|
87
|
+
params.set('machineId', input.machineId);
|
|
86
88
|
const resp = await fetch(`${input.apiUrl}/api/cli/bundle?${params.toString()}`, { method: 'GET', headers, signal: AbortSignal.timeout(REFRESH_TIMEOUT_MS) });
|
|
87
89
|
// 304 Not Modified — cache is still valid; refresh its timestamp.
|
|
88
90
|
if (resp.status === 304 && cached) {
|
package/dist/version.json
CHANGED