fullcourtdefense-cli 1.26.12 → 1.26.13
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.
|
@@ -5,6 +5,12 @@ export interface CmdGuardStatus {
|
|
|
5
5
|
rulesPresent: boolean;
|
|
6
6
|
ruleCount?: number;
|
|
7
7
|
mode?: string;
|
|
8
|
+
/**
|
|
9
|
+
* Whether the doskey macro file exists. Installed-but-missing means the bat
|
|
10
|
+
* fails open and NO command is intercepted, so this must be visible rather
|
|
11
|
+
* than hidden behind `installed: true`.
|
|
12
|
+
*/
|
|
13
|
+
macrosPresent?: boolean;
|
|
8
14
|
}
|
|
9
15
|
export declare function isCmdGuardInstalled(): boolean;
|
|
10
16
|
export declare function getCmdGuardStatus(): CmdGuardStatus;
|
|
@@ -58,6 +58,7 @@ const shellGuard_1 = require("./shellGuard");
|
|
|
58
58
|
const GUARD_RULES_PATH = path.join(os.homedir(), '.fullcourtdefense-shell-guard.json');
|
|
59
59
|
const GUARD_JS_PATH = path.join(os.homedir(), '.fullcourtdefense-cmd-guard.js');
|
|
60
60
|
const AUTORUN_BAT_PATH = path.join(os.homedir(), '.fullcourtdefense-cmd-autorun.bat');
|
|
61
|
+
const MACRO_FILE_PATH = path.join(os.homedir(), '.fullcourtdefense-cmd-macros.txt');
|
|
61
62
|
const CMD_AUTORUN_KEY = 'HKCU\\Software\\Microsoft\\Command Processor';
|
|
62
63
|
const AUTORUN_VALUE = 'AutoRun';
|
|
63
64
|
const AUTORUN_MARKER = 'fullcourtdefense-cmd-autorun.bat';
|
|
@@ -151,10 +152,22 @@ function buildGuardJs(nodePath, cliEntry) {
|
|
|
151
152
|
``,
|
|
152
153
|
].join('\n');
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* The doskey macro definitions, as a file for `doskey /macrofile=`.
|
|
157
|
+
*
|
|
158
|
+
* Deliberately contains NOTHING but macro lines: every line in a macrofile is a
|
|
159
|
+
* definition, so a comment or build stamp here would register a junk macro (a
|
|
160
|
+
* `REM` macro would shadow REM for anything the user types). The build stamp
|
|
161
|
+
* lives in the bat, which is what the staleness check reads.
|
|
162
|
+
*/
|
|
163
|
+
function buildMacroFile(nodePath) {
|
|
164
|
+
const guardJs = batQuote(GUARD_JS_PATH);
|
|
165
|
+
const node = batQuote(nodePath);
|
|
166
|
+
return `${INTERCEPTED_COMMANDS.map(cmd => `${cmd}=${node} ${guardJs} ${cmd} $*`).join('\r\n')}\r\n`;
|
|
167
|
+
}
|
|
154
168
|
function buildAutorunBat(nodePath) {
|
|
155
169
|
const guardJs = batQuote(GUARD_JS_PATH);
|
|
156
170
|
const node = batQuote(nodePath);
|
|
157
|
-
const doskeyLines = INTERCEPTED_COMMANDS.map(cmd => `doskey ${cmd}=${node} ${guardJs} ${cmd} $*`);
|
|
158
171
|
return [
|
|
159
172
|
'@echo off',
|
|
160
173
|
'REM FullCourtDefense cmd.exe guard (installed by fullcourtdefense-cli).',
|
|
@@ -168,7 +181,13 @@ function buildAutorunBat(nodePath) {
|
|
|
168
181
|
// must keep working natively. Never break the customer's tools.
|
|
169
182
|
`if not exist ${node} goto :eof`,
|
|
170
183
|
`if not exist ${guardJs} goto :eof`,
|
|
171
|
-
|
|
184
|
+
`if not exist ${batQuote(MACRO_FILE_PATH)} goto :eof`,
|
|
185
|
+
// ONE doskey call, not one per command. AutoRun runs on every cmd.exe start,
|
|
186
|
+
// and each separate `doskey` statement costs ~8ms, so the previous
|
|
187
|
+
// one-line-per-command form added ~320ms to every cmd.exe launch — paid by
|
|
188
|
+
// build scripts, CI and agents on every single `cmd /c`. Loading the same
|
|
189
|
+
// macros from a file is ~18ms for the whole set.
|
|
190
|
+
`doskey /macrofile=${batQuote(MACRO_FILE_PATH)}`,
|
|
172
191
|
'',
|
|
173
192
|
].join('\r\n');
|
|
174
193
|
}
|
|
@@ -229,6 +248,7 @@ function getCmdGuardStatus() {
|
|
|
229
248
|
rulesPresent,
|
|
230
249
|
ruleCount,
|
|
231
250
|
mode,
|
|
251
|
+
macrosPresent: fs.existsSync(MACRO_FILE_PATH),
|
|
232
252
|
};
|
|
233
253
|
}
|
|
234
254
|
catch {
|
|
@@ -245,6 +265,10 @@ async function installCmdGuardCommand(_args = {}) {
|
|
|
245
265
|
const nodePath = process.execPath;
|
|
246
266
|
const cliEntry = process.argv[1] || '';
|
|
247
267
|
fs.writeFileSync(GUARD_JS_PATH, buildGuardJs(nodePath, cliEntry), 'utf8');
|
|
268
|
+
// Macro file BEFORE the bat: the bat refuses to register macros without it,
|
|
269
|
+
// so this order can never leave a window where cmd.exe sources a bat whose
|
|
270
|
+
// macrofile does not exist yet.
|
|
271
|
+
(0, shellGuard_1.writeGuardArtifact)(MACRO_FILE_PATH, buildMacroFile(nodePath));
|
|
248
272
|
fs.writeFileSync(AUTORUN_BAT_PATH, buildAutorunBat(nodePath), 'utf8');
|
|
249
273
|
console.log(`Guard rules written: ${rules.rules.length} rule(s), mode "${rules.mode}".`);
|
|
250
274
|
const current = readCurrentAutorun();
|
|
@@ -280,6 +304,10 @@ async function uninstallCmdGuardCommand() {
|
|
|
280
304
|
fs.unlinkSync(AUTORUN_BAT_PATH);
|
|
281
305
|
}
|
|
282
306
|
catch { /* not present */ }
|
|
307
|
+
try {
|
|
308
|
+
fs.unlinkSync(MACRO_FILE_PATH);
|
|
309
|
+
}
|
|
310
|
+
catch { /* not present */ }
|
|
283
311
|
console.log('cmd.exe guard uninstalled. Open cmd windows keep doskey until closed.');
|
|
284
312
|
}
|
|
285
313
|
/** Refresh shared rules JSON when cmd guard is installed. */
|
|
@@ -290,21 +318,34 @@ function refreshCmdGuardRules() {
|
|
|
290
318
|
(0, shellGuard_1.writeShellGuardRules)();
|
|
291
319
|
// Self-heal after CLI updates: the planted checker/autorun carry the
|
|
292
320
|
// detection logic of the build that wrote them — rewrite when stale.
|
|
293
|
-
|
|
321
|
+
//
|
|
322
|
+
// A MISSING macro file is also healed here, and matters more than staleness:
|
|
323
|
+
// the bat fails open without it, so the guard would look installed while
|
|
324
|
+
// registering no macros at all. This is the only path that repairs that.
|
|
325
|
+
if ((0, shellGuard_1.guardArtifactStale)(GUARD_JS_PATH) || !fs.existsSync(MACRO_FILE_PATH)) {
|
|
294
326
|
const nodePath = process.execPath;
|
|
295
327
|
(0, shellGuard_1.writeGuardArtifact)(GUARD_JS_PATH, buildGuardJs(nodePath, process.argv[1] || ''));
|
|
328
|
+
(0, shellGuard_1.writeGuardArtifact)(MACRO_FILE_PATH, buildMacroFile(nodePath));
|
|
296
329
|
(0, shellGuard_1.writeGuardArtifact)(AUTORUN_BAT_PATH, buildAutorunBat(nodePath));
|
|
297
330
|
}
|
|
298
331
|
}
|
|
299
332
|
catch { /* best-effort */ }
|
|
300
333
|
}
|
|
301
334
|
/**
|
|
302
|
-
* The node.exe path baked into an installed autorun bat
|
|
303
|
-
*
|
|
335
|
+
* The node.exe path baked into an installed autorun bat.
|
|
336
|
+
*
|
|
337
|
+
* Reads the `if not exist "<node>" goto :eof` fail-open line, which is present
|
|
338
|
+
* in every build. The doskey fallback below covers bats written by builds that
|
|
339
|
+
* predate the macrofile change (`doskey <cmd>="<node>" "<guardJs>" <cmd> $*`):
|
|
340
|
+
* those machines are exactly the ones that still need repairing, so dropping
|
|
341
|
+
* the old shape would strand them.
|
|
304
342
|
*/
|
|
305
343
|
function autorunBatNodePath(batContent) {
|
|
306
|
-
const
|
|
307
|
-
|
|
344
|
+
const existsCheck = batContent.match(/^if not exist\s+"([^"]+node[^"]*)"\s+goto/im);
|
|
345
|
+
if (existsCheck)
|
|
346
|
+
return existsCheck[1];
|
|
347
|
+
const legacyDoskey = batContent.match(/^doskey\s+\S+="([^"]+)"\s/m);
|
|
348
|
+
return legacyDoskey ? legacyDoskey[1] : undefined;
|
|
308
349
|
}
|
|
309
350
|
/**
|
|
310
351
|
* Self-heal a stale/broken cmd-guard AutoRun. Two failure shapes are covered:
|
|
@@ -358,6 +399,10 @@ function repairStaleCmdAutorun() {
|
|
|
358
399
|
fs.unlinkSync(GUARD_JS_PATH);
|
|
359
400
|
}
|
|
360
401
|
catch { /* already gone */ }
|
|
402
|
+
try {
|
|
403
|
+
fs.unlinkSync(MACRO_FILE_PATH);
|
|
404
|
+
}
|
|
405
|
+
catch { /* already gone */ }
|
|
361
406
|
return true;
|
|
362
407
|
}
|
|
363
408
|
catch {
|
package/dist/commands/doctor.js
CHANGED
|
@@ -111,7 +111,10 @@ async function perfCheck(apiUrl, config) {
|
|
|
111
111
|
console.log(`node spawn floor: ${hp.nodeSpawnFloorMs}ms median — OS+AV cost of any per-event process`);
|
|
112
112
|
console.log(`hook end-to-end: ${hp.hookMedianMs}ms median, ${hp.hookMinMs}-${hp.hookMaxMs}ms range — per IDE event (shell/MCP/file)`);
|
|
113
113
|
if (hp.cmdGuardOverheadMs !== undefined) {
|
|
114
|
-
console.log(`cmd guard overhead: +${hp.cmdGuardOverheadMs}ms median — per
|
|
114
|
+
console.log(`cmd guard overhead: +${hp.cmdGuardOverheadMs}ms median — per cmd.exe START (once per terminal window; every time for "cmd /c" callers such as build scripts and CI)`);
|
|
115
|
+
}
|
|
116
|
+
if (hp.cmdGuardCheckerMs !== undefined) {
|
|
117
|
+
console.log(`cmd guard checker: +${hp.cmdGuardCheckerMs}ms median — additional, only when the typed command is one we intercept`);
|
|
115
118
|
}
|
|
116
119
|
if (snapshot.processes.length > 0) {
|
|
117
120
|
console.log(`resident processes: ${snapshot.processes.length} FullCourtDefense process(es), ${snapshot.totalWorkingSetMB}MB working set total`);
|
|
@@ -177,6 +177,7 @@ function collectWindowsChecks() {
|
|
|
177
177
|
'.fullcourtdefense-shell-guard.ps1',
|
|
178
178
|
'.fullcourtdefense-cmd-guard.js',
|
|
179
179
|
'.fullcourtdefense-cmd-autorun.bat',
|
|
180
|
+
'.fullcourtdefense-cmd-macros.txt',
|
|
180
181
|
];
|
|
181
182
|
for (const name of profileFiles) {
|
|
182
183
|
const file = path.join(home, name);
|
package/dist/perfSnapshot.d.ts
CHANGED
|
@@ -40,8 +40,19 @@ export interface PerfHotPathBench {
|
|
|
40
40
|
hookMinMs: number;
|
|
41
41
|
hookMaxMs: number;
|
|
42
42
|
hookRuns: number;
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Cost the cmd guard adds to STARTING a cmd.exe session, measured as a
|
|
45
|
+
* guarded `cmd /c` against the same command with AutoRun suppressed (`/d`).
|
|
46
|
+
*
|
|
47
|
+
* This is what a developer actually pays: AutoRun runs on every cmd.exe
|
|
48
|
+
* launch and registers the doskey macros, whether or not the command being
|
|
49
|
+
* run is one of the intercepted ones. An interactive terminal pays it once
|
|
50
|
+
* when the window opens; anything invoking `cmd /c` per command (build
|
|
51
|
+
* scripts, CI, agents) pays it every time.
|
|
52
|
+
*/
|
|
44
53
|
cmdGuardOverheadMs?: number;
|
|
54
|
+
/** Extra cost when the typed command IS doskey-intercepted and goes through the checker. */
|
|
55
|
+
cmdGuardCheckerMs?: number;
|
|
45
56
|
}
|
|
46
57
|
export interface PerfDiskState {
|
|
47
58
|
hookLogKB?: number;
|
package/dist/perfSnapshot.js
CHANGED
|
@@ -112,15 +112,35 @@ async function benchHotPath(entryJs) {
|
|
|
112
112
|
hookTimes.push((await runTimed(process.execPath, [entry, 'hook', '--event', 'shell', '--fcd-managed', 'true'], { input: hookEvent, timeoutMs: 60_000 })).elapsedMs);
|
|
113
113
|
}
|
|
114
114
|
let cmdGuardOverheadMs;
|
|
115
|
+
let cmdGuardCheckerMs;
|
|
115
116
|
const guardJs = path.join(os.homedir(), '.fullcourtdefense-cmd-guard.js');
|
|
116
117
|
if (process.platform === 'win32' && fs.existsSync(guardJs)) {
|
|
117
|
-
const
|
|
118
|
-
|
|
118
|
+
const comSpec = process.env.ComSpec || 'cmd.exe';
|
|
119
|
+
// What a real command pays: AutoRun fires (registering macros) vs `/d`,
|
|
120
|
+
// which suppresses it. Measuring `node guardJs echo` instead — as this
|
|
121
|
+
// did previously — reports only the checker's cost for an intercepted
|
|
122
|
+
// command and never observes the AutoRun work every cmd.exe start does,
|
|
123
|
+
// which understated the figure developers actually feel by ~5x.
|
|
124
|
+
const withAutoRun = [];
|
|
125
|
+
const withoutAutoRun = [];
|
|
119
126
|
for (let i = 0; i < 4; i++) {
|
|
120
|
-
|
|
121
|
-
|
|
127
|
+
// Alternate order so neither side systematically absorbs warm-up cost.
|
|
128
|
+
if (i % 2 === 0) {
|
|
129
|
+
withAutoRun.push((await runTimed(comSpec, ['/c', 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
130
|
+
withoutAutoRun.push((await runTimed(comSpec, ['/d', '/c', 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
withoutAutoRun.push((await runTimed(comSpec, ['/d', '/c', 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
134
|
+
withAutoRun.push((await runTimed(comSpec, ['/c', 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
cmdGuardOverheadMs = Math.max(0, median(withAutoRun) - median(withoutAutoRun));
|
|
138
|
+
// Kept as a separate number: the checker cost for an intercepted command.
|
|
139
|
+
const checker = [];
|
|
140
|
+
for (let i = 0; i < 4; i++) {
|
|
141
|
+
checker.push((await runTimed(process.execPath, [guardJs, 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
122
142
|
}
|
|
123
|
-
|
|
143
|
+
cmdGuardCheckerMs = Math.max(0, median(checker) - median(withoutAutoRun));
|
|
124
144
|
}
|
|
125
145
|
return {
|
|
126
146
|
nodeSpawnFloorMs: median(spawnTimes),
|
|
@@ -129,6 +149,7 @@ async function benchHotPath(entryJs) {
|
|
|
129
149
|
hookMaxMs: Math.max(...hookTimes),
|
|
130
150
|
hookRuns: hookTimes.length,
|
|
131
151
|
cmdGuardOverheadMs,
|
|
152
|
+
cmdGuardCheckerMs,
|
|
132
153
|
};
|
|
133
154
|
}
|
|
134
155
|
/**
|
|
@@ -246,7 +267,7 @@ async function collectPerfSnapshot(input = {}) {
|
|
|
246
267
|
}
|
|
247
268
|
/** One-line human summary for machine-action result reporting. */
|
|
248
269
|
function summarizePerfSnapshot(s) {
|
|
249
|
-
const guard = s.hotPath.cmdGuardOverheadMs !== undefined ? `, cmd guard +${s.hotPath.cmdGuardOverheadMs}ms` : '';
|
|
270
|
+
const guard = s.hotPath.cmdGuardOverheadMs !== undefined ? `, cmd guard +${s.hotPath.cmdGuardOverheadMs}ms/cmd.exe start` : '';
|
|
250
271
|
return `Hook ${s.hotPath.hookMedianMs}ms median (spawn floor ${s.hotPath.nodeSpawnFloorMs}ms${guard}); `
|
|
251
272
|
+ `${s.processes.length} FCD process(es) using ${s.totalWorkingSetMB}MB; `
|
|
252
273
|
+ `spool backlog ${s.disk.spoolBacklogCount ?? 0}, hook.log ${s.disk.hookLogKB ?? 0}KB${s.enforcement.mode ? `; mode ${s.enforcement.mode}` : ''}.`;
|
package/dist/version.json
CHANGED