logshield-cli 0.4.4 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/README.md +50 -3
- package/dist/cli/index.cjs +52 -18
- package/package.json +36 -8
- package/dist/android-chrome-192x192.png +0 -0
- package/dist/android-chrome-512x512.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/index-B3qxIuiz.css +0 -1
- package/dist/assets/index-DDJ1Wxio.js +0 -29
- package/dist/engine/applyRules.js +0 -27
- package/dist/engine/engine/applyRules.d.ts +0 -5
- package/dist/engine/engine/applyRules.js +0 -30
- package/dist/engine/engine/guard.d.ts +0 -1
- package/dist/engine/engine/guard.js +0 -12
- package/dist/engine/engine/sanitizeLog.d.ts +0 -11
- package/dist/engine/engine/sanitizeLog.js +0 -18
- package/dist/engine/guard.js +0 -9
- package/dist/engine/rules/cloud.d.ts +0 -2
- package/dist/engine/rules/cloud.js +0 -15
- package/dist/engine/rules/credentials.d.ts +0 -2
- package/dist/engine/rules/credentials.js +0 -15
- package/dist/engine/rules/creditCard.d.ts +0 -2
- package/dist/engine/rules/creditCard.js +0 -15
- package/dist/engine/rules/custom.d.ts +0 -2
- package/dist/engine/rules/custom.js +0 -14
- package/dist/engine/rules/index.d.ts +0 -11
- package/dist/engine/rules/index.js +0 -38
- package/dist/engine/rules/tokens.d.ts +0 -2
- package/dist/engine/rules/tokens.js +0 -20
- package/dist/engine/rules/types.d.ts +0 -9
- package/dist/engine/rules/types.js +0 -2
- package/dist/engine/rules/urls.d.ts +0 -2
- package/dist/engine/rules/urls.js +0 -10
- package/dist/engine/sanitizeLog.js +0 -15
- package/dist/engine/utils/luhn.d.ts +0 -1
- package/dist/engine/utils/luhn.js +0 -19
- package/dist/favicon-16x16.png +0 -0
- package/dist/favicon-32x32.png +0 -0
- package/dist/favicon.ico +0 -0
- package/dist/features.jsx +0 -462
- package/dist/robots.txt +0 -4
- package/dist/rules/cloud.js +0 -12
- package/dist/rules/credentials.js +0 -12
- package/dist/rules/creditCard.js +0 -12
- package/dist/rules/custom.js +0 -11
- package/dist/rules/index.js +0 -35
- package/dist/rules/tokens.js +0 -17
- package/dist/rules/types.js +0 -1
- package/dist/rules/urls.js +0 -7
- package/dist/site.webmanifest +0 -20
- package/dist/utils/luhn.js +0 -16
- package/dist/vite.svg +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.5.0
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- Hardened dry-run result shape: dry-run no longer returns the raw input in `output` (safe to serialize and avoids re-leakage in programmatic contexts)
|
|
8
|
+
- `--dry-run` can be combined with `--json` for machine-readable detection without leaking log content (`output` is intentionally empty)
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Added detection-only helper `scanLog(input)` (internal only; safe to serialize)
|
|
13
|
+
|
|
14
|
+
### Compatibility
|
|
15
|
+
|
|
16
|
+
- CLI behavior is unchanged: dry-run still prints a human report and never echoes log content
|
|
17
|
+
- Programmatic consumers: `dryRun` now returns `output: ""` (intentional)
|
|
18
|
+
- npm package ships CLI only; no supported JS API surface is published
|
|
19
|
+
|
|
3
20
|
## v0.4.4
|
|
4
21
|
|
|
5
22
|
### Fixed
|
|
@@ -18,7 +35,7 @@
|
|
|
18
35
|
|
|
19
36
|
- Prevented API key redaction from corrupting header names (`x-api-key`)
|
|
20
37
|
- Preserved key labels when redacting `api_key=...` values
|
|
21
|
-
- Corrected CLI exit code for invalid flag combinations (`--
|
|
38
|
+
- Corrected CLI exit code for invalid flag combinations (e.g., `--summary --json` exits with code 2)
|
|
22
39
|
|
|
23
40
|
### Improved
|
|
24
41
|
|
package/README.md
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/logshield-cli)
|
|
4
4
|
[](https://www.npmjs.com/package/logshield-cli)
|
|
5
5
|
[](https://github.com/afria85/LogShield/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/afria85/LogShield/blob/main/LICENSE)
|
|
7
|
+
[](https://github.com/sponsors/afria85)
|
|
6
8
|
|
|
7
9
|
Your logs already contain secrets. You just don't see them.
|
|
8
10
|
|
|
@@ -95,6 +97,29 @@ It is designed to be **predictable, conservative, and safe for production pipeli
|
|
|
95
97
|
The website and documentation live in the `/docs` directory.
|
|
96
98
|
They are deployed to **https://logshield.dev** via Vercel.
|
|
97
99
|
|
|
100
|
+
## Project links
|
|
101
|
+
|
|
102
|
+
- Website: https://logshield.dev
|
|
103
|
+
- Docs: https://logshield.dev/docs.html
|
|
104
|
+
- GitHub: https://github.com/afria85/LogShield
|
|
105
|
+
- Sponsor: https://github.com/sponsors/afria85
|
|
106
|
+
|
|
107
|
+
## Local preview (website)
|
|
108
|
+
|
|
109
|
+
To preview the website on your computer:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
npm run dev
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
To preview on a phone/tablet on the same Wi-Fi:
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm run dev:lan
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Then open the printed LAN URL on your device.
|
|
122
|
+
|
|
98
123
|
---
|
|
99
124
|
|
|
100
125
|
## Why LogShield exists
|
|
@@ -193,6 +218,8 @@ logshield scan [file]
|
|
|
193
218
|
|
|
194
219
|
If a file is not provided and input is piped, LogShield automatically reads from **STDIN**.
|
|
195
220
|
|
|
221
|
+
Note: the npm package ships the CLI only; there is no supported JS API surface.
|
|
222
|
+
|
|
196
223
|
---
|
|
197
224
|
|
|
198
225
|
## CLI Flags
|
|
@@ -213,7 +240,7 @@ If a file is not provided and input is piped, LogShield automatically reads from
|
|
|
213
240
|
Print a compact redaction summary
|
|
214
241
|
|
|
215
242
|
- `--json`
|
|
216
|
-
JSON output (
|
|
243
|
+
JSON output (can be combined with `--dry-run`; output is empty in dry-run)
|
|
217
244
|
|
|
218
245
|
- `--version`
|
|
219
246
|
Print CLI version
|
|
@@ -371,9 +398,16 @@ Structured output for tooling and automation:
|
|
|
371
398
|
logshield scan --json < logs.txt
|
|
372
399
|
```
|
|
373
400
|
|
|
401
|
+
Detection-only JSON (safe to serialize; no log content):
|
|
402
|
+
|
|
403
|
+
```bash
|
|
404
|
+
logshield scan --json --dry-run < logs.txt
|
|
405
|
+
```
|
|
406
|
+
|
|
374
407
|
Notes:
|
|
375
408
|
|
|
376
|
-
- `--json`
|
|
409
|
+
- `--json` can be combined with `--dry-run` for machine-readable detection
|
|
410
|
+
- In `--dry-run` JSON mode, `output` is intentionally an empty string
|
|
377
411
|
- Usage errors exit with code `2`
|
|
378
412
|
- Output is always newline-terminated
|
|
379
413
|
|
|
@@ -426,7 +460,7 @@ Depending on rules and mode:
|
|
|
426
460
|
LogShield guarantees:
|
|
427
461
|
|
|
428
462
|
- Deterministic output
|
|
429
|
-
- Stable behavior within **v0.
|
|
463
|
+
- Stable behavior within the current minor line **v0.5.x**
|
|
430
464
|
- No runtime dependencies
|
|
431
465
|
- Snapshot-tested and contract-tested
|
|
432
466
|
- No telemetry
|
|
@@ -457,3 +491,16 @@ It is a **last-line safety net**, not a primary defense.
|
|
|
457
491
|
## License
|
|
458
492
|
|
|
459
493
|
Apache-2.0
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Contributing
|
|
497
|
+
|
|
498
|
+
See `CONTRIBUTING.md`.
|
|
499
|
+
|
|
500
|
+
## Security
|
|
501
|
+
|
|
502
|
+
See `SECURITY.md`.
|
|
503
|
+
|
|
504
|
+
## Support
|
|
505
|
+
|
|
506
|
+
See `SUPPORT.md`.
|
package/dist/cli/index.cjs
CHANGED
|
@@ -36,22 +36,24 @@ var readInput_exports = {};
|
|
|
36
36
|
__export(readInput_exports, {
|
|
37
37
|
readInput: () => readInput
|
|
38
38
|
});
|
|
39
|
-
async function readInput(file) {
|
|
39
|
+
async function readInput(file, opts) {
|
|
40
40
|
if (file) {
|
|
41
41
|
if (!import_node_fs.default.existsSync(file)) {
|
|
42
42
|
throw new Error(`File not found: ${file}`);
|
|
43
43
|
}
|
|
44
44
|
return import_node_fs.default.readFileSync(file, "utf8");
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
const stdin = opts?.stdin ?? process.stdin;
|
|
47
|
+
const forceStdin = Boolean(opts?.forceStdin);
|
|
48
|
+
if (!stdin.isTTY || forceStdin) {
|
|
47
49
|
return new Promise((resolve, reject) => {
|
|
48
50
|
let data = "";
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
stdin.setEncoding?.("utf8");
|
|
52
|
+
stdin.on("data", (chunk) => {
|
|
51
53
|
data += chunk;
|
|
52
54
|
});
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
stdin.on("end", () => resolve(data));
|
|
56
|
+
stdin.on("error", reject);
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
throw new Error("No input provided");
|
|
@@ -503,7 +505,7 @@ function sanitizeLog(input, options) {
|
|
|
503
505
|
const matches = [];
|
|
504
506
|
if (ctx.dryRun) {
|
|
505
507
|
applyRules(input, allRules, ctx, matches);
|
|
506
|
-
return { output:
|
|
508
|
+
return { output: "", matches };
|
|
507
509
|
}
|
|
508
510
|
const output = applyRules(input, allRules, ctx, matches);
|
|
509
511
|
return { output, matches };
|
|
@@ -522,8 +524,18 @@ var { writeOutput: writeOutput2 } = (init_writeOutput(), __toCommonJS(writeOutpu
|
|
|
522
524
|
var { printSummary: printSummary2 } = (init_summary(), __toCommonJS(summary_exports));
|
|
523
525
|
var { sanitizeLog: sanitizeLog2 } = (init_sanitizeLog(), __toCommonJS(sanitizeLog_exports));
|
|
524
526
|
var rawArgs = process.argv.slice(2).map((arg) => arg === "-h" ? "--help" : arg);
|
|
527
|
+
var ALLOWED_FLAGS = /* @__PURE__ */ new Set([
|
|
528
|
+
"--strict",
|
|
529
|
+
"--dry-run",
|
|
530
|
+
"--stdin",
|
|
531
|
+
"--fail-on-detect",
|
|
532
|
+
"--json",
|
|
533
|
+
"--summary",
|
|
534
|
+
"--version",
|
|
535
|
+
"--help"
|
|
536
|
+
]);
|
|
525
537
|
function getVersion() {
|
|
526
|
-
return true ? "0.
|
|
538
|
+
return true ? "0.5.0" : "unknown";
|
|
527
539
|
}
|
|
528
540
|
function printHelp() {
|
|
529
541
|
process.stdout.write(`Usage: logshield scan [file]
|
|
@@ -550,14 +562,23 @@ function writeErr(message) {
|
|
|
550
562
|
function parseArgs(args) {
|
|
551
563
|
const flags = /* @__PURE__ */ new Set();
|
|
552
564
|
const positionals = [];
|
|
565
|
+
const unknownFlags = [];
|
|
553
566
|
for (const arg of args) {
|
|
554
|
-
if (arg.startsWith("
|
|
555
|
-
|
|
567
|
+
if (arg.startsWith("-")) {
|
|
568
|
+
if (arg.startsWith("--")) {
|
|
569
|
+
if (!ALLOWED_FLAGS.has(arg)) {
|
|
570
|
+
unknownFlags.push(arg);
|
|
571
|
+
} else {
|
|
572
|
+
flags.add(arg);
|
|
573
|
+
}
|
|
574
|
+
} else {
|
|
575
|
+
unknownFlags.push(arg);
|
|
576
|
+
}
|
|
556
577
|
} else {
|
|
557
578
|
positionals.push(arg);
|
|
558
579
|
}
|
|
559
580
|
}
|
|
560
|
-
return { flags, positionals };
|
|
581
|
+
return { flags, positionals, unknownFlags };
|
|
561
582
|
}
|
|
562
583
|
function isStdinPiped() {
|
|
563
584
|
return !process.stdin.isTTY;
|
|
@@ -596,16 +617,23 @@ function exitUsageError(message) {
|
|
|
596
617
|
process.exit(2);
|
|
597
618
|
}
|
|
598
619
|
async function main() {
|
|
599
|
-
if (rawArgs.length === 0
|
|
620
|
+
if (rawArgs.length === 0) {
|
|
621
|
+
printHelp();
|
|
622
|
+
process.exit(0);
|
|
623
|
+
}
|
|
624
|
+
const { flags, positionals, unknownFlags } = parseArgs(rawArgs);
|
|
625
|
+
if (unknownFlags.length > 0) {
|
|
626
|
+
exitUsageError(`Unknown flag: ${unknownFlags[0]}`);
|
|
627
|
+
}
|
|
628
|
+
if (flags.has("--help")) {
|
|
600
629
|
printHelp();
|
|
601
630
|
process.exit(0);
|
|
602
631
|
}
|
|
603
|
-
if (
|
|
632
|
+
if (flags.has("--version")) {
|
|
604
633
|
process.stdout.write(`logshield v${getVersion()}
|
|
605
634
|
`);
|
|
606
635
|
process.exit(0);
|
|
607
636
|
}
|
|
608
|
-
const { flags, positionals } = parseArgs(rawArgs);
|
|
609
637
|
const command = positionals[0];
|
|
610
638
|
if (command !== "scan") {
|
|
611
639
|
exitUsageError("Unknown command");
|
|
@@ -622,16 +650,22 @@ async function main() {
|
|
|
622
650
|
if (useStdin && file) {
|
|
623
651
|
exitUsageError("Cannot read from both STDIN and file");
|
|
624
652
|
}
|
|
625
|
-
if (dryRun && json) {
|
|
626
|
-
exitUsageError("--dry-run cannot be used with --json");
|
|
627
|
-
}
|
|
628
653
|
if (json && summary) {
|
|
629
654
|
exitUsageError("--summary cannot be used with --json");
|
|
630
655
|
}
|
|
631
656
|
try {
|
|
632
|
-
const input = await readInput2(useStdin ? void 0 : file
|
|
657
|
+
const input = await readInput2(useStdin ? void 0 : file, {
|
|
658
|
+
forceStdin: stdinFlag
|
|
659
|
+
});
|
|
633
660
|
const result = sanitizeLog2(input, { strict, dryRun });
|
|
634
661
|
if (dryRun) {
|
|
662
|
+
if (json) {
|
|
663
|
+
writeOutput2(result, { json: true });
|
|
664
|
+
if (failOnDetect && result.matches.length > 0) {
|
|
665
|
+
process.exit(1);
|
|
666
|
+
}
|
|
667
|
+
process.exit(0);
|
|
668
|
+
}
|
|
635
669
|
renderDryRunReport(result.matches);
|
|
636
670
|
if (failOnDetect && result.matches.length > 0) {
|
|
637
671
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,35 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "logshield-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"bin": {
|
|
7
7
|
"logshield": "dist/cli/index.cjs"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
|
-
"dist",
|
|
10
|
+
"dist/cli",
|
|
11
11
|
"README.md",
|
|
12
12
|
"CHANGELOG.md",
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
15
|
"scripts": {
|
|
16
16
|
"build": "node scripts/build-cli.cjs",
|
|
17
|
-
"build:web": "vite build --outDir dist-web",
|
|
18
17
|
"build:blog": "node scripts/build-blog.js",
|
|
19
|
-
"dev:web": "vite",
|
|
20
18
|
"typecheck": "tsc -p tsconfig.core.json && tsc -p tsconfig.cli.json --noEmit",
|
|
21
19
|
"pretest": "npm run build",
|
|
22
20
|
"test": "vitest",
|
|
21
|
+
"lint": "npm run typecheck",
|
|
22
|
+
"clean:dist": "node scripts/clean-dist.mjs",
|
|
23
|
+
"prepack": "npm run clean:dist && npm run build",
|
|
24
|
+
"pack:check": "npm pack --dry-run",
|
|
25
|
+
"release:check": "npm run prepublish:check && npm run pack:check",
|
|
23
26
|
"prepublish:check": "npm run typecheck && npm test",
|
|
24
|
-
"prepublishOnly": "npm run prepublish:check"
|
|
27
|
+
"prepublishOnly": "npm run prepublish:check",
|
|
28
|
+
"dev": "node scripts/dev-docs-server.mjs",
|
|
29
|
+
"dev:lan": "node scripts/dev-docs-server.mjs --host 0.0.0.0"
|
|
25
30
|
},
|
|
26
31
|
"devDependencies": {
|
|
27
32
|
"@types/node": "^25.0.3",
|
|
28
|
-
"autoprefixer": "^10.4.23",
|
|
29
33
|
"esbuild": "^0.25.0",
|
|
30
|
-
"postcss": "^8.5.6",
|
|
31
|
-
"tailwindcss": "^3.4.19",
|
|
32
34
|
"typescript": "^5.9.3",
|
|
33
35
|
"vitest": "^4.0.0"
|
|
36
|
+
},
|
|
37
|
+
"description": "Deterministic, rule-based CLI to sanitize secrets from logs. No AI. No cloud. No config.",
|
|
38
|
+
"keywords": [
|
|
39
|
+
"log-sanitization",
|
|
40
|
+
"secret-detection",
|
|
41
|
+
"redaction",
|
|
42
|
+
"security",
|
|
43
|
+
"devops",
|
|
44
|
+
"cli",
|
|
45
|
+
"deterministic",
|
|
46
|
+
"no-ai"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/afria85/LogShield.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://logshield.dev",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/afria85/LogShield/issues"
|
|
55
|
+
},
|
|
56
|
+
"funding": {
|
|
57
|
+
"type": "github",
|
|
58
|
+
"url": "https://github.com/sponsors/afria85"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18"
|
|
34
62
|
}
|
|
35
63
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{--color-bg-primary: 248 250 252;--color-bg-secondary: 241 245 249;--color-bg-card: 255 255 255;--color-text-primary: 15 23 42;--color-text-secondary: 71 85 105;--color-text-muted: 148 163 184;--color-border: 226 232 240;--color-accent: 59 130 246;--color-accent-hover: 37 99 235;--gradient-start: 239 246 255;--gradient-mid: 238 242 255;--gradient-end: 248 250 252}*{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}@media (prefers-color-scheme: dark){*{--tw-border-opacity: 1;border-color:rgb(51 65 85 / var(--tw-border-opacity, 1))}}html{scroll-behavior:smooth}body{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.3s}@media (prefers-color-scheme: dark){body{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(241 245 249 / var(--tw-text-opacity, 1))}}body{margin:0;padding:0;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}code{font-family:JetBrains Mono,Fira Code,source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}@media (prefers-color-scheme: dark){::-webkit-scrollbar-track{--tw-bg-opacity: 1;background-color:rgb(30 41 59 / var(--tw-bg-opacity, 1))}}::-webkit-scrollbar-thumb{border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(203 213 225 / var(--tw-bg-opacity, 1))}@media (prefers-color-scheme: dark){::-webkit-scrollbar-thumb{--tw-bg-opacity: 1;background-color:rgb(71 85 105 / var(--tw-bg-opacity, 1))}}::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(148 163 184 / var(--tw-bg-opacity, 1))}@media (prefers-color-scheme: dark){::-webkit-scrollbar-thumb:hover{--tw-bg-opacity: 1;background-color:rgb(100 116 139 / var(--tw-bg-opacity, 1))}}::-moz-selection{background-color:#3b82f64d;--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}::selection{background-color:#3b82f64d;--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}@media (prefers-color-scheme: dark){::-moz-selection{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity, 1))}::selection{--tw-text-opacity: 1;color:rgb(219 234 254 / var(--tw-text-opacity, 1))}}@keyframes fadeIn{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes slideUp{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}@keyframes pulseGlow{0%,to{box-shadow:0 0 #3b82f666}50%{box-shadow:0 0 20px 5px #3b82f633}}@keyframes gradientShift{0%{background-position:0% 50%}50%{background-position:100% 50%}to{background-position:0% 50%}}@keyframes float{0%,to{transform:translateY(0)}50%{transform:translateY(-10px)}}@keyframes shine{0%{background-position:-200% center}to{background-position:200% center}}@keyframes bounceUp{0%,to{transform:translateY(0)}50%{transform:translateY(-4px)}}@media print{.no-print{display:none!important}body{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(0 0 0 / var(--tw-text-opacity, 1))}}:root{--bg: #0f1115;--panel: #151922;--border: #252a36;--text: #e6e8eb;--muted: #9aa1ad;--accent: #ff4d4f}*{box-sizing:border-box}body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,monospace;background:var(--bg);color:var(--text)}.ls-root{min-height:100vh;display:flex;flex-direction:column}.ls-header{padding:16px 24px;border-bottom:1px solid var(--border)}.ls-header h1{margin:0;font-size:20px}.ls-header p{margin:4px 0 0;color:var(--muted);font-size:13px}.ls-controls{display:flex;gap:16px;align-items:center;padding:12px 24px;border-bottom:1px solid var(--border);font-size:13px}.ls-controls label{display:flex;align-items:center;gap:6px;cursor:pointer}.ls-controls button{margin-left:auto;background:transparent;border:1px solid var(--border);color:var(--text);padding:6px 12px;cursor:pointer}.ls-controls button:hover{border-color:var(--accent)}.ls-main{flex:1;display:grid;grid-template-columns:1fr 1fr}.ls-input{width:100%;height:100%;resize:none;border:none;outline:none;padding:16px;background:var(--panel);color:var(--text);border-right:1px solid var(--border);font-size:13px;line-height:1.5}.ls-output{padding:16px;background:#0c0f14;font-size:13px;line-height:1.5;white-space:pre-wrap;overflow:auto}.ls-highlight{background:#ff4d4f40;color:#fff}.ls-footer{padding:8px 24px;border-top:1px solid var(--border);font-size:12px;color:var(--muted)}
|