fullcourtdefense-cli 1.15.9 → 1.15.11
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 +34 -3
- package/dist/commands/discover.js +11 -6
- package/dist/commands/discoverSecrets.d.ts +1 -0
- package/dist/commands/discoverSecrets.js +3 -2
- package/dist/runtimeConfig.d.ts +11 -3
- package/dist/runtimeConfig.js +21 -7
- package/dist/version.json +1 -1
- package/package.json +1 -1
package/dist/commands/daemon.js
CHANGED
|
@@ -53,6 +53,7 @@ const COLOR = {
|
|
|
53
53
|
red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
54
54
|
};
|
|
55
55
|
const TASK_NAME = 'FullCourtDefense Daemon';
|
|
56
|
+
const WATCHDOG_TASK_NAME = 'FullCourtDefense Daemon Watchdog';
|
|
56
57
|
const CRON_MARKER = '# FCD_DAEMON';
|
|
57
58
|
const LAUNCHD_LABEL = 'ai.fullcourtdefense.daemon';
|
|
58
59
|
const SYSTEMD_UNIT = 'fullcourtdefense-daemon.service';
|
|
@@ -262,6 +263,16 @@ async function runDaemon(args, config) {
|
|
|
262
263
|
console.log(`${COLOR.yellow}Another FullCourtDefense daemon is already running (pid file: ${pidFile()}).${COLOR.reset}`);
|
|
263
264
|
return;
|
|
264
265
|
}
|
|
266
|
+
// The resident loop must never die silently from one bad tick (fs watcher
|
|
267
|
+
// callback, fetch, timer). Log and keep running — the loop is timer-driven,
|
|
268
|
+
// so surviving a failed tick is safe, and on Windows there is no supervisor
|
|
269
|
+
// that would restart a crashed process instantly.
|
|
270
|
+
process.on('uncaughtException', error => {
|
|
271
|
+
log(`Uncaught exception (daemon continues): ${error?.stack || String(error)}`);
|
|
272
|
+
});
|
|
273
|
+
process.on('unhandledRejection', reason => {
|
|
274
|
+
log(`Unhandled rejection (daemon continues): ${reason?.stack || String(reason)}`);
|
|
275
|
+
});
|
|
265
276
|
const creds = (0, config_1.resolveCliCredentials)(config, {
|
|
266
277
|
shieldId: args.shieldId,
|
|
267
278
|
shieldKey: args.shieldKey,
|
|
@@ -696,12 +707,23 @@ function installWindows() {
|
|
|
696
707
|
], { stdio: 'ignore', windowsHide: true });
|
|
697
708
|
ok = reg.status === 0;
|
|
698
709
|
}
|
|
710
|
+
// Watchdog: logon triggers only START the daemon — nothing restarts it if it
|
|
711
|
+
// is killed or crashes mid-session. A time-based per-user task (no elevation
|
|
712
|
+
// needed, unlike ONLOGON) re-launches the daemon every 5 minutes;
|
|
713
|
+
// acquirePidLock() makes each tick a cheap no-op while the daemon is alive,
|
|
714
|
+
// and revives it within one tick when it is not. Best-effort: a missing
|
|
715
|
+
// watchdog never fails the install.
|
|
716
|
+
(0, child_process_1.spawnSync)('schtasks', [
|
|
717
|
+
'/Create', '/TN', WATCHDOG_TASK_NAME, '/TR', `wscript.exe "${vbs}"`,
|
|
718
|
+
'/SC', 'MINUTE', '/MO', '5', '/F',
|
|
719
|
+
], { stdio: 'ignore', windowsHide: true });
|
|
699
720
|
if (ok)
|
|
700
721
|
startDaemonNowWindows(vbs, taskOk);
|
|
701
722
|
return ok;
|
|
702
723
|
}
|
|
703
724
|
function uninstallWindows() {
|
|
704
725
|
const task = (0, child_process_1.spawnSync)('schtasks', ['/Delete', '/TN', TASK_NAME, '/F'], { stdio: 'ignore', windowsHide: true });
|
|
726
|
+
(0, child_process_1.spawnSync)('schtasks', ['/Delete', '/TN', WATCHDOG_TASK_NAME, '/F'], { stdio: 'ignore', windowsHide: true });
|
|
705
727
|
const reg = (0, child_process_1.spawnSync)('reg', ['delete', WINDOWS_RUN_KEY, '/v', WINDOWS_RUN_VALUE, '/f'], { stdio: 'ignore', windowsHide: true });
|
|
706
728
|
const vbs = path.join(process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local'), 'FullCourtDefense', 'daemon.vbs');
|
|
707
729
|
try {
|
|
@@ -790,11 +812,16 @@ WantedBy=default.target
|
|
|
790
812
|
return true;
|
|
791
813
|
}
|
|
792
814
|
catch { /* fall back to cron */ }
|
|
793
|
-
// Fallback: cron
|
|
815
|
+
// Fallback: cron for boxes without a systemd user session. @reboot starts
|
|
816
|
+
// the daemon at boot; the 5-min line is the watchdog (pid lock makes each
|
|
817
|
+
// tick a no-op while the daemon is alive, and revives it after a kill/crash).
|
|
794
818
|
const command = `"${process.execPath}" "${cliEntry()}" daemon >/dev/null 2>&1`;
|
|
795
819
|
const list = (0, child_process_1.spawnSync)('crontab', ['-l'], { encoding: 'utf8' });
|
|
796
820
|
const existing = (list.status === 0 ? (list.stdout || '') : '').split('\n').filter(l => l && !l.includes(CRON_MARKER));
|
|
797
|
-
const write = (0, child_process_1.spawnSync)('crontab', ['-'], {
|
|
821
|
+
const write = (0, child_process_1.spawnSync)('crontab', ['-'], {
|
|
822
|
+
input: [...existing, `@reboot ${command} ${CRON_MARKER}`, `*/5 * * * * ${command} ${CRON_MARKER}`].join('\n') + '\n',
|
|
823
|
+
encoding: 'utf8',
|
|
824
|
+
});
|
|
798
825
|
return write.status === 0;
|
|
799
826
|
}
|
|
800
827
|
function uninstallLinux() {
|
|
@@ -854,6 +881,10 @@ function statusCommand() {
|
|
|
854
881
|
else {
|
|
855
882
|
console.log(' not installed — run: fullcourtdefense daemon --install true');
|
|
856
883
|
}
|
|
884
|
+
const watchdog = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', WATCHDOG_TASK_NAME], { stdio: 'ignore', windowsHide: true, timeout: 5_000 });
|
|
885
|
+
console.log(watchdog.status === 0
|
|
886
|
+
? ` Watchdog task "${WATCHDOG_TASK_NAME}" installed — revives a killed daemon within 5 min`
|
|
887
|
+
: ` ${COLOR.yellow}watchdog not installed${COLOR.reset} — a killed daemon stays down until next logon; re-run: fullcourtdefense daemon --install true`);
|
|
857
888
|
}
|
|
858
889
|
else if (process.platform === 'darwin') {
|
|
859
890
|
console.log(fs.existsSync(launchdPlistPath()) ? ` launchd agent installed (${launchdPlistPath()})` : ' not installed');
|
|
@@ -897,7 +928,7 @@ async function daemonCommand(args, config) {
|
|
|
897
928
|
: installLinux();
|
|
898
929
|
if (ok) {
|
|
899
930
|
console.log(`${COLOR.green}${COLOR.bold}Daemon autostart installed.${COLOR.reset}`);
|
|
900
|
-
console.log(`${COLOR.gray}Trigger:${COLOR.reset} ${process.platform === 'win32' ? `at logon (Scheduled Task "${TASK_NAME}")` : process.platform === 'darwin' ? 'launchd (RunAtLoad + KeepAlive)' : 'systemd user unit (or @reboot cron)'}`);
|
|
931
|
+
console.log(`${COLOR.gray}Trigger:${COLOR.reset} ${process.platform === 'win32' ? `at logon (Scheduled Task "${TASK_NAME}") + 5-min watchdog ("${WATCHDOG_TASK_NAME}") that revives a killed daemon` : process.platform === 'darwin' ? 'launchd (RunAtLoad + KeepAlive)' : 'systemd user unit (or @reboot + 5-min watchdog cron)'}`);
|
|
901
932
|
console.log(`${COLOR.gray}Start now:${COLOR.reset} fullcourtdefense daemon`);
|
|
902
933
|
console.log(`${COLOR.gray}Status:${COLOR.reset} fullcourtdefense daemon --status true`);
|
|
903
934
|
console.log(`${COLOR.gray}Remove:${COLOR.reset} fullcourtdefense daemon --uninstall true`);
|
|
@@ -824,9 +824,13 @@ async function discoverCommand(args, config) {
|
|
|
824
824
|
// and behavior is exactly the known-locations scan.
|
|
825
825
|
// Admin-chosen folders (picked in the console per machine, delivered via the
|
|
826
826
|
// runtime bundle) are always merged in, so every scheduled/daemon/manual scan
|
|
827
|
-
// covers them without extra flags.
|
|
828
|
-
|
|
829
|
-
const
|
|
827
|
+
// covers them without extra flags. Default folders the admin explicitly
|
|
828
|
+
// removed in the console are dropped from the sweep.
|
|
829
|
+
const { extraScanRoots: adminScanRoots, disabledScanRoots } = (0, runtimeConfig_1.getCachedScanRootOverrides)();
|
|
830
|
+
const disabledRootSet = new Set(disabledScanRoots.map(root => path.resolve(root).toLowerCase()));
|
|
831
|
+
const isDisabledRoot = (root) => disabledRootSet.has(path.resolve(root).toLowerCase());
|
|
832
|
+
const scanRoots = [...new Set([...(0, discoverPaths_1.resolveScanRoots)(args.scanRoot), ...adminScanRoots])]
|
|
833
|
+
.filter(root => !isDisabledRoot(root));
|
|
830
834
|
const sweep = scanRoots.length > 0 ? (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots) : { candidates: [], scannedDirs: 0 };
|
|
831
835
|
// Admin roots are scanned for env/secrets even when no MCP config lives there.
|
|
832
836
|
const sweepProjectRoots = [...new Set([...deriveProjectRoots(sweep.candidates), ...adminScanRoots])];
|
|
@@ -877,7 +881,7 @@ async function discoverCommand(args, config) {
|
|
|
877
881
|
}
|
|
878
882
|
}
|
|
879
883
|
const secrets = surfaces.has('secrets') || surfaces.has('posture')
|
|
880
|
-
? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, configFiles: secretConfigFiles })
|
|
884
|
+
? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, disabledRoots: disabledScanRoots, configFiles: secretConfigFiles })
|
|
881
885
|
: undefined;
|
|
882
886
|
const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
|
|
883
887
|
? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd, extraRoots: sweepProjectRoots })
|
|
@@ -893,15 +897,16 @@ async function discoverCommand(args, config) {
|
|
|
893
897
|
const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
|
|
894
898
|
const home = os.homedir();
|
|
895
899
|
// Effective roots the posture scan covered — proof of coverage for the console.
|
|
900
|
+
// Admin-removed defaults are excluded here too, so the console mirrors reality.
|
|
896
901
|
const defaultEnvRoots = [home, cwd, path.join(home, 'dev'), path.join(home, 'repos'), path.join(home, 'projects'), path.join(home, 'Documents'), path.join(home, 'code')]
|
|
897
902
|
.map(root => path.resolve(root));
|
|
898
903
|
const adminRootSet = new Set(adminScanRoots.map(root => path.resolve(root).toLowerCase()));
|
|
899
904
|
const scannedRoots = [
|
|
900
|
-
...[...new Set(defaultEnvRoots)].filter(root => fs.existsSync(root)).map(root => ({ path: root, source: 'default' })),
|
|
905
|
+
...[...new Set(defaultEnvRoots)].filter(root => fs.existsSync(root) && !isDisabledRoot(root)).map(root => ({ path: root, source: 'default' })),
|
|
901
906
|
...adminScanRoots.map(root => path.resolve(root)).filter(root => fs.existsSync(root)).map(root => ({ path: root, source: 'admin' })),
|
|
902
907
|
...(0, discoverPaths_1.resolveScanRoots)(args.scanRoot)
|
|
903
908
|
.map(root => path.resolve(root))
|
|
904
|
-
.filter(root => fs.existsSync(root) && !adminRootSet.has(root.toLowerCase()))
|
|
909
|
+
.filter(root => fs.existsSync(root) && !adminRootSet.has(root.toLowerCase()) && !isDisabledRoot(root))
|
|
905
910
|
.map(root => ({ path: root, source: 'default' })),
|
|
906
911
|
].filter((entry, index, list) => list.findIndex(other => other.path.toLowerCase() === entry.path.toLowerCase()) === index);
|
|
907
912
|
// Shallow folder tree for the console folder picker (names only, no contents).
|
|
@@ -478,7 +478,8 @@ function scanSecrets(options = {}) {
|
|
|
478
478
|
const home = os.homedir();
|
|
479
479
|
const cwd = options.cwd || process.cwd();
|
|
480
480
|
const maxDepth = options.maxEnvDepth ?? 4;
|
|
481
|
-
const
|
|
481
|
+
const disabledRoots = new Set((options.disabledRoots ?? []).map(root => path.resolve(root).toLowerCase()));
|
|
482
|
+
const extraRoots = (options.extraRoots ?? []).filter(root => !disabledRoots.has(path.resolve(root).toLowerCase()));
|
|
482
483
|
const findings = [];
|
|
483
484
|
const seen = new Set();
|
|
484
485
|
let scannedFiles = 0;
|
|
@@ -513,7 +514,7 @@ function scanSecrets(options = {}) {
|
|
|
513
514
|
path.join(home, 'projects'),
|
|
514
515
|
path.join(home, 'Documents'),
|
|
515
516
|
path.join(home, 'code'),
|
|
516
|
-
].filter(root => fs.existsSync(root))));
|
|
517
|
+
].filter(root => fs.existsSync(root) && !disabledRoots.has(path.resolve(root).toLowerCase()))));
|
|
517
518
|
for (const envFile of collectEnvFiles(envRoots, maxDepth)) {
|
|
518
519
|
if (findings.length >= MAX_FINDINGS)
|
|
519
520
|
break;
|
package/dist/runtimeConfig.d.ts
CHANGED
|
@@ -26,6 +26,8 @@ export interface RuntimeBundle {
|
|
|
26
26
|
suspended?: boolean;
|
|
27
27
|
/** Admin-chosen extra posture scan folders for this machine (from the console). */
|
|
28
28
|
extraScanRoots?: string[];
|
|
29
|
+
/** Default scan folders the admin explicitly removed from this machine's posture sweep. */
|
|
30
|
+
disabledScanRoots?: string[];
|
|
29
31
|
/** One constrained, auditable action queued for the resident daemon. */
|
|
30
32
|
machineAction?: {
|
|
31
33
|
id: string;
|
|
@@ -58,8 +60,14 @@ export interface EffectiveBundle extends RuntimeBundle {
|
|
|
58
60
|
*/
|
|
59
61
|
export declare function getRuntimeBundle(input: FetchBundleInput): Promise<EffectiveBundle>;
|
|
60
62
|
/**
|
|
61
|
-
* Admin
|
|
62
|
-
*
|
|
63
|
-
*
|
|
63
|
+
* Admin scan-folder choices from the most recent cached bundle (any shield):
|
|
64
|
+
* extra folders to add and default folders the admin removed. Read-only and
|
|
65
|
+
* offline — used by `discover` so scheduled/daemon scans honor the console
|
|
66
|
+
* selection without needing credentials plumbed in.
|
|
64
67
|
*/
|
|
68
|
+
export declare function getCachedScanRootOverrides(): {
|
|
69
|
+
extraScanRoots: string[];
|
|
70
|
+
disabledScanRoots: string[];
|
|
71
|
+
};
|
|
72
|
+
/** @deprecated Use getCachedScanRootOverrides(). */
|
|
65
73
|
export declare function getCachedExtraScanRoots(): string[];
|
package/dist/runtimeConfig.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.getRuntimeBundle = getRuntimeBundle;
|
|
37
|
+
exports.getCachedScanRootOverrides = getCachedScanRootOverrides;
|
|
37
38
|
exports.getCachedExtraScanRoots = getCachedExtraScanRoots;
|
|
38
39
|
const fs = __importStar(require("fs"));
|
|
39
40
|
const os = __importStar(require("os"));
|
|
@@ -107,6 +108,9 @@ async function getRuntimeBundle(input) {
|
|
|
107
108
|
extraScanRoots: Array.isArray(body.data.extraScanRoots)
|
|
108
109
|
? body.data.extraScanRoots.filter((root) => typeof root === 'string').slice(0, 20)
|
|
109
110
|
: undefined,
|
|
111
|
+
disabledScanRoots: Array.isArray(body.data.disabledScanRoots)
|
|
112
|
+
? body.data.disabledScanRoots.filter((root) => typeof root === 'string').slice(0, 20)
|
|
113
|
+
: undefined,
|
|
110
114
|
machineAction: body.data.machineAction,
|
|
111
115
|
fetchedAt: Date.now(),
|
|
112
116
|
};
|
|
@@ -123,18 +127,28 @@ async function getRuntimeBundle(input) {
|
|
|
123
127
|
return { mode: 'block', version: '', source: 'default' };
|
|
124
128
|
}
|
|
125
129
|
/**
|
|
126
|
-
* Admin
|
|
127
|
-
*
|
|
128
|
-
*
|
|
130
|
+
* Admin scan-folder choices from the most recent cached bundle (any shield):
|
|
131
|
+
* extra folders to add and default folders the admin removed. Read-only and
|
|
132
|
+
* offline — used by `discover` so scheduled/daemon scans honor the console
|
|
133
|
+
* selection without needing credentials plumbed in.
|
|
129
134
|
*/
|
|
130
|
-
function
|
|
135
|
+
function getCachedScanRootOverrides() {
|
|
131
136
|
const cache = readCacheFile();
|
|
132
|
-
const
|
|
137
|
+
const extra = new Set();
|
|
138
|
+
const disabled = new Set();
|
|
133
139
|
for (const entry of Object.values(cache)) {
|
|
134
140
|
for (const root of entry.extraScanRoots || []) {
|
|
135
141
|
if (typeof root === 'string' && root.trim())
|
|
136
|
-
|
|
142
|
+
extra.add(root.trim());
|
|
143
|
+
}
|
|
144
|
+
for (const root of entry.disabledScanRoots || []) {
|
|
145
|
+
if (typeof root === 'string' && root.trim())
|
|
146
|
+
disabled.add(root.trim());
|
|
137
147
|
}
|
|
138
148
|
}
|
|
139
|
-
return [...
|
|
149
|
+
return { extraScanRoots: [...extra], disabledScanRoots: [...disabled] };
|
|
150
|
+
}
|
|
151
|
+
/** @deprecated Use getCachedScanRootOverrides(). */
|
|
152
|
+
function getCachedExtraScanRoots() {
|
|
153
|
+
return getCachedScanRootOverrides().extraScanRoots;
|
|
140
154
|
}
|
package/dist/version.json
CHANGED