privacy-brush 0.0.4 → 1.0.1

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/src/cli.mjs CHANGED
@@ -1,154 +1,174 @@
1
- import { parseArgs } from "node:util"
2
- import { PrivacyBrush } from "./index.mjs"
3
-
4
- // 流式处理:将 stdin 通过 masker Transform 流处理后输出到 stdout
5
- // Usage: some_command | node src/cli.mjs
6
- // Example 1: echo "password" | node src/cli.mjs
7
- // Example 2: flutter devices | node src/cli.mjs
8
- // Example 3: echo 'Microsoft Windows [版本 10.0.12345.6785]' | node src/cli.mjs
9
- // => Microsoft Windows [版本 10.█.█████.████]
10
- // Example 4: ❯ node src/cli.mjs
11
- // Input: Microsoft Windows [版本 10.0.12345.6785]
12
- // Output: Microsoft Windows [版本 10.█.█████.████]
13
-
14
- // get config from command line arguments use native `parseArgs`
15
- // node src/cli.mjs --mask X --preserve-first false
16
- const args = process.argv.slice(2)
17
- // console.log("args:", args) // args: [ '--mask', 'X', '--preserve-first', 'false' ]
18
-
19
- const verbose = args.includes("--verbose")
20
-
21
- const [err, result] = safeCall(() =>
22
- parseArgs({
23
- allowPositionals: true,
24
- allowNegative: true,
25
-
26
- args,
27
-
28
- options: {
29
- mask: {
30
- type: "string",
31
- short: "m",
32
- default: "█",
33
- },
34
- "preserve-first": {
35
- type: "boolean",
36
- short: "p",
37
- default: true,
38
- },
39
- // help
40
- help: {
41
- type: "boolean",
42
- short: "h",
43
- },
44
- // verbose
45
- verbose: {
46
- type: "boolean",
47
- },
48
- // version
49
- version: {
50
- type: "boolean",
51
- short: "v",
52
- },
53
- },
54
- }),
55
- )
56
-
57
- await main()
58
-
59
- async function main() {
60
- if (err) {
61
- console.error(verbose ? err : String(err))
62
- console.error()
63
- await printHelp()
64
-
65
- process.exit(1)
66
- return
67
- }
68
-
69
- const { values } = result
70
-
71
- if (values.help) {
72
- await printHelp()
73
- return
74
- }
75
-
76
- if (values.version) {
77
- await printVersion()
78
- return
79
- }
80
-
81
- // console.log("values:", values)
82
- // console.log("positionals:", positionals)
83
-
84
- const config = values
85
-
86
- const masker = new PrivacyBrush({
87
- maskChar: config.mask,
88
- preserveFirstPart: config["preserve-first"],
89
- })
90
- const maskStream = await masker.createMaskStream()
91
-
92
- // 检查 stdin 是否连接到管道(有数据输入)
93
- const isPipedInput = !process.stdin.isTTY
94
-
95
- // 如果不是管道输入(交互模式),才显示提示
96
- if (!isPipedInput) {
97
- process.stdout.write("Input (Press Ctrl+C to exit...):\n")
98
- }
99
-
100
- // 处理所有数据
101
- process.stdin.pipe(maskStream).pipe(process.stdout)
102
- }
103
- /**
104
- * @template T
105
- * @param {(...args: any[]) => T} fn
106
- * @returns {[null, T] | [Error, null]}
107
- */
108
- function safeCall(fn) {
109
- try {
110
- const result = fn()
111
- return [null, result]
112
- } catch (error) {
113
- // @ts-expect-error
114
- return [error, null]
115
- }
116
- }
117
-
118
- async function printHelp() {
119
- console.log(`
120
- ${await getNameVersion()}
121
-
122
- Usage: pnpx privacy-brush [options]
123
-
124
- Options:
125
- --mask, -m Character to use for masking (default: "█")
126
- --preserve-first, -p Whether to preserve the first part of version numbers (default: true, \`--no-preserve-first\` to false)
127
- --help, -h Show this help message (default: false)
128
- --verbose Enable verbose output (default: false)
129
- --version, -v Show version information (default: false)
130
-
131
- Examples:
132
- echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush
133
- echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush --mask "X" --no-preserve-first
134
- `)
135
- }
136
-
137
- async function parsePackageJSON() {
138
- const fs = await import("node:fs")
139
- const pkgPath = new URL("../package.json", import.meta.url)
140
-
141
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
142
-
143
- return pkg
144
- }
145
-
146
- async function getNameVersion() {
147
- const pkg = await parsePackageJSON()
148
-
149
- return `${pkg.name}@${pkg.version}`
150
- }
151
-
152
- async function printVersion() {
153
- console.log(await getNameVersion())
154
- }
1
+ #!/usr/bin/env node
2
+
3
+ import { PrivacyBrush } from "./index.mjs"
4
+ import { parsedResult, verbose } from "./lib/parse-args.mjs"
5
+
6
+ // 流式处理:将 stdin 通过 masker Transform 流处理后输出到 stdout
7
+ // Usage: some_command | node src/cli.mjs
8
+ // Example 1: echo "password" | node src/cli.mjs
9
+ // Example 2: flutter devices | node src/cli.mjs
10
+ // Example 3: ❯ echo 'Microsoft Windows [版本 10.0.12345.6785]' | node src/cli.mjs
11
+ // => Microsoft Windows [版本 10.█.█████.████]
12
+ // Example 4: node src/cli.mjs
13
+ // Input: Microsoft Windows [版本 10.0.12345.6785]
14
+ // Output: Microsoft Windows [版本 10.█.█████.████]
15
+
16
+ await main()
17
+
18
+ async function main() {
19
+ const [err, result] = parsedResult
20
+
21
+ if (err) {
22
+ console.error(verbose ? err : String(err))
23
+ console.error()
24
+ await printHelp()
25
+
26
+ process.exit(1)
27
+ return
28
+ }
29
+
30
+ const { values } = result
31
+ verbose && console.log("values:", values)
32
+
33
+ if (values.help) {
34
+ await printHelp()
35
+ return
36
+ }
37
+
38
+ if (values.version) {
39
+ await printVersion()
40
+ return
41
+ }
42
+
43
+ const config = values
44
+
45
+ const masker = new PrivacyBrush({
46
+ maskChar: config.mask,
47
+ preserveFirstPart: config["preserve-first"],
48
+ customPatterns: config.pattern,
49
+ })
50
+
51
+ if (values["list-patterns"]) {
52
+ // instantiate PrivacyBrush to access default patterns
53
+ const patterns = masker.defaultSensitivePatterns
54
+
55
+ console.log("\nBuilt-in patterns:")
56
+ console.table(patterns.map(({ replacer: _, ...args }) => args))
57
+
58
+ if (masker.customSensitivePatterns.length) {
59
+ console.log("Custom patterns:")
60
+ console.table(
61
+ masker.customSensitivePatterns.map(({ replacer: _, ...args }) => args),
62
+ )
63
+ }
64
+
65
+ return
66
+ }
67
+
68
+ // If an input file is provided, process it. If --output-file is provided write there, otherwise print to stdout.
69
+ if (config["input-file"]) {
70
+ try {
71
+ if (config["output-file"]) {
72
+ // maskFile will write to the output path
73
+ masker.maskFile(config["input-file"], config["output-file"])
74
+ return
75
+ }
76
+
77
+ const masked = masker.maskFile(config["input-file"])
78
+
79
+ process.stdout.write(masked)
80
+ return
81
+ } catch {
82
+ process.exitCode = 1
83
+ return
84
+ }
85
+ }
86
+
87
+ // If an output file was requested for piped or interactive stdin, collect all input, mask and write to file.
88
+ if (config["output-file"]) {
89
+ try {
90
+ const chunks = []
91
+ for await (const chunk of process.stdin) {
92
+ chunks.push(String(chunk))
93
+ }
94
+
95
+ const inputText = chunks.join("")
96
+ console.log(`inputText: |${inputText}|`)
97
+ const masked = masker.maskText(inputText)
98
+
99
+ const fs = await import("node:fs/promises")
100
+ await fs.writeFile(config["output-file"], masked, "utf8")
101
+ return
102
+ } catch (error) {
103
+ console.error(error)
104
+ process.exitCode = 1
105
+ return
106
+ }
107
+ }
108
+
109
+ // 检查 stdin 是否连接到管道(有数据输入)
110
+ const isPipedInput = !process.stdin.isTTY
111
+
112
+ // 如果不是管道输入(交互模式),才显示提示
113
+ if (!isPipedInput) {
114
+ process.stdout.write("Input (Press Ctrl+C to exit...):\n")
115
+ }
116
+
117
+ const maskStream = await masker.createMaskStream()
118
+
119
+ // Default: stream masking to stdout
120
+ process.stdin.pipe(maskStream).pipe(process.stdout)
121
+ }
122
+
123
+ async function printHelp() {
124
+ console.log(`
125
+ # ${await getNameVersion()}
126
+
127
+ ## Usage:
128
+
129
+ pnpx privacy-brush [options]
130
+
131
+
132
+ ## Options:
133
+ --input-file, -i Path to input file to mask and print to stdout
134
+ --output-file, -o Path to write masked output (if given, write to this file)
135
+ --mask, -m Character to use for masking (default: "█")
136
+ --preserve-first, -p Whether to preserve the first part of version numbers (default: true, \`--no-preserve-first\` to false)
137
+ --pattern, -r Custom regex pattern(s) to mask (can be used multiple times. E.g. --pattern '\\d{2,}' --pattern 'sk-([0-9a-z]{20,})')
138
+ --list-patterns List all built-in patterns
139
+
140
+ --help, -h Show this help message (default: false)
141
+ --verbose Enable verbose output (default: false)
142
+ --version, -v Show version information (default: false)
143
+
144
+
145
+ ## Examples:
146
+
147
+ flutter devices | pnpx privacy-brush
148
+
149
+ echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush
150
+ echo "Microsoft Windows [Version 10.0.12345.6785]" | pnpx privacy-brush --mask "X" --no-preserve-first
151
+
152
+ pnpx privacy-brush --input-file test/fixtures/terminal_log.md # mask and print to stdout
153
+ pnpx privacy-brush --input-file test/fixtures/terminal_log.md --output-file masked_log.md
154
+ `)
155
+ }
156
+
157
+ async function parsePackageJSON() {
158
+ const fs = await import("node:fs")
159
+ const pkgPath = new URL("../package.json", import.meta.url)
160
+
161
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"))
162
+
163
+ return pkg
164
+ }
165
+
166
+ async function getNameVersion() {
167
+ const pkg = await parsePackageJSON()
168
+
169
+ return `${pkg.name}@${pkg.version}`
170
+ }
171
+
172
+ async function printVersion() {
173
+ console.log(await getNameVersion())
174
+ }
package/src/cli.test.mjs CHANGED
@@ -1,28 +1,169 @@
1
- // 流式处理
2
-
3
- import { strict as assert } from "node:assert"
4
- import { execSync } from "node:child_process"
5
- import { test } from "node:test"
6
-
7
- test("❯ echo 'Microsoft Windows [Version 10.0.12345.6785]' | node src/cli.mjs", async () => {
8
- const actual = execSync(
9
- "echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs",
10
- ).toString("utf8")
11
-
12
- // console.log(`actual:|${actual}|`)
13
-
14
- const expected = "Microsoft Windows [Version 10.█.█████.████] \r\n"
15
- assert.strictEqual(actual, expected)
16
- })
17
-
18
- test("--mask", async () => {
19
- const actual = execSync(
20
- 'echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs --mask "🔒" --no-preserve-first',
21
- ).toString("utf8")
22
-
23
- // console.log(`actual:|${actual}|`)
24
-
25
- const expected =
26
- "Microsoft Windows [Version 🔒🔒.🔒.🔒🔒🔒🔒🔒.🔒🔒🔒🔒] \r\n"
27
- assert.strictEqual(actual, expected)
28
- })
1
+ // 流式处理
2
+
3
+ import { strict as assert } from "node:assert"
4
+ import { execSync, spawnSync } from "node:child_process"
5
+ import { test } from "node:test"
6
+
7
+ test("❯ echo | node src/cli.mjs", async () => {
8
+ const text = "Microsoft Windows [Version 10.0.12345.6785]"
9
+
10
+ // `echo "${text}"` will print text + newline to stdout
11
+ // `echo -n "${text}"` will print -n and text + newline to stdout
12
+ // So we use spawn with args [`-n`, text] to avoid the extra newline
13
+ function pipeCommands() {
14
+ const node = spawnSync("node", ["src/cli.mjs"], {
15
+ input: text,
16
+ })
17
+
18
+ return node.stdout.toString("utf8")
19
+ }
20
+
21
+ const actual = pipeCommands()
22
+
23
+ const expected = "Microsoft Windows [Version 10.█.█████.████]"
24
+ assert.strictEqual(actual, expected)
25
+ })
26
+
27
+ test("--mask", async () => {
28
+ const actual = execSync(
29
+ 'echo Microsoft Windows [Version 10.0.12345.6785] | node src/cli.mjs --mask "🔒" --no-preserve-first',
30
+ )
31
+ .toString("utf8")
32
+ .trim()
33
+
34
+ // console.log(`actual:|${actual}|`)
35
+
36
+ const expected = "Microsoft Windows [Version 🔒🔒.🔒.🔒🔒🔒🔒🔒.🔒🔒🔒🔒]"
37
+ assert.strictEqual(actual, expected)
38
+ })
39
+
40
+ test("--input-file outputs masked file content", async () => {
41
+ const { execSync } = await import("node:child_process")
42
+
43
+ const inputPath = "test/fixtures/terminal_log.md"
44
+
45
+ const actual = execSync(
46
+ `node src/cli.mjs --input-file ${inputPath}`,
47
+ ).toString("utf8")
48
+
49
+ // compute expected via masker.maskFile and normalize EOLs
50
+ const expected = `# h1
51
+
52
+ ## flutter devices
53
+
54
+ ❯ flutter devices
55
+ Flutter assets will be downloaded from <https://storage.flutter-io.cn>. Make sure you trust this source!
56
+ Found 4 connected devices:
57
+ sdk gphone64 x86 64 (mobile) • emulator-5554 • android-x64 • Android 16 (API 36) (emulator)
58
+ Windows (desktop) • windows • windows-x64 • Microsoft Windows [版本 10.█.█████.████]
59
+ Chrome (web) • chrome • web-javascript • Google Chrome 144.█.████.██
60
+ Edge (web) • edge • web-javascript • Microsoft Edge 144.█.████.██
61
+
62
+ Run "flutter emulators" to list and start any available device emulators.
63
+
64
+ If you expected another device to be detected, please run "flutter doctor" to diagnose potential issues. You may also try
65
+ increasing the time to wait for connected devices with the "--device-timeout" flag. Visit <https://flutter.dev/setup/> for
66
+ troubleshooting tips.
67
+
68
+ ## log
69
+
70
+ DEBUG: User login from IP 192.███.█.███
71
+ DEBUG: Session ID: abc123def456
72
+ DEBUG: Browser: Chrome/144.0.1234.60
73
+ DEBUG: OS: Windows 10.0.12345
74
+ `
75
+
76
+ assert.strictEqual(actual, expected)
77
+ })
78
+
79
+ test(`custom patterns`, () => {
80
+ const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
81
+
82
+ const actual = spawnSync(
83
+ "node",
84
+ ["src/cli.mjs", "--pattern", "/sk-([a-z0-9]{20,})/"],
85
+ { input },
86
+ ).stdout.toString("utf8")
87
+
88
+ const expected = "DEEPSEEK_API_KEY=sk-████████████████████████████████"
89
+ assert.strictEqual(actual, expected)
90
+ })
91
+
92
+ test(`custom patterns without ()`, () => {
93
+ const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
94
+
95
+ const actual = spawnSync(
96
+ "node",
97
+ ["src/cli.mjs", "--pattern", "/sk-[a-z0-9]{20,}/"],
98
+ { input },
99
+ )
100
+ .stdout.toString("utf8")
101
+ .trim()
102
+
103
+ const expected = "DEEPSEEK_API_KEY=███████████████████████████████████"
104
+ assert.strictEqual(actual, expected)
105
+ })
106
+
107
+ test(`custom patterns`, () => {
108
+ const input = `DEEPSEEK_API_KEY=sk-af75149812524eb08eb302bf9604c8e8`
109
+
110
+ const actual = spawnSync(
111
+ "node",
112
+ ["src/cli.mjs", "--pattern", "/([0-9]{2,})/"],
113
+ { input },
114
+ ).stdout.toString("utf8")
115
+
116
+ const expected = "DEEPSEEK_API_KEY=sk-af███████████eb██eb███bf████c8e8"
117
+ assert.strictEqual(actual, expected)
118
+ })
119
+
120
+ /**
121
+ * 将两个或多个命令通过管道连接起来执行,并返回最后一个命令的输出结果
122
+ * @param {string} cmdStrWithPipes
123
+ * @returns {string}
124
+ */
125
+ function pipeCommands(cmdStrWithPipes) {
126
+ // const echo = spawnSync("echo", ["-n", text])
127
+ // const node = spawnSync("node", ["src/cli.mjs"], {
128
+ // input: echo.stdout,
129
+ // })
130
+ // 以下代码是以上代码的泛化版本
131
+ const commands = cmdStrWithPipes
132
+ .split("|")
133
+ .map(cmd => cmd.trim())
134
+ .filter(Boolean)
135
+
136
+ if (commands.length < 2) {
137
+ throw new Error("At least two commands are required for piping.")
138
+ }
139
+
140
+ const [first, ...restCommand] = commands
141
+ const [cmdName, args] = splitCmdAndArgs(first)
142
+
143
+ const lastCommand = restCommand.reduce(
144
+ (prevProcess, cmdStr) => {
145
+ const [cmd, args] = splitCmdAndArgs(cmdStr)
146
+
147
+ const currentProcess = spawnSync(cmd, args, {
148
+ input: prevProcess.stdout,
149
+ })
150
+
151
+ return currentProcess
152
+ },
153
+ spawnSync(cmdName, args),
154
+ )
155
+
156
+ return lastCommand.stdout.toString("utf8")
157
+ }
158
+
159
+ /**
160
+ *
161
+ * @param {string} cmd
162
+ * @returns {[cmdName: string, args: string[]]}
163
+ */
164
+ function splitCmdAndArgs(cmd) {
165
+ const [cmdName, ...args] = cmd.split(" ").filter(Boolean)
166
+ // console.log(`cmdName:|${cmdName}|`)
167
+ // console.log("args:", args)
168
+ return [cmdName, args]
169
+ }