@stll/anonymize-cli 1.5.0 → 2.0.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/README.md +61 -17
- package/dist/cli.mjs +293 -60
- package/dist/cli.mjs.map +1 -1
- package/package.json +15 -6
package/README.md
CHANGED
|
@@ -26,23 +26,60 @@ anonymize -d key.json reply.txt
|
|
|
26
26
|
|
|
27
27
|
## Options
|
|
28
28
|
|
|
29
|
-
| Flag | Meaning
|
|
30
|
-
| ------------------------- |
|
|
31
|
-
| `-o, --output <path>` | Output file, or directory for
|
|
32
|
-
| `-m, --mode <mode>` | `replace` (reversible placeholders) or `redact`
|
|
33
|
-
| `-k, --key <path>` | Write the redaction key JSON (replace mode)
|
|
34
|
-
| `-d, --deanonymise <key>` | Restore text using a redaction key
|
|
35
|
-
| `--
|
|
36
|
-
|
|
|
37
|
-
| `--
|
|
38
|
-
| `--
|
|
39
|
-
| `--
|
|
40
|
-
| `--
|
|
41
|
-
| `--
|
|
29
|
+
| Flag | Meaning |
|
|
30
|
+
| ------------------------- | ------------------------------------------------ |
|
|
31
|
+
| `-o, --output <path>` | Output file, or directory for batch input |
|
|
32
|
+
| `-m, --mode <mode>` | `replace` (reversible placeholders) or `redact` |
|
|
33
|
+
| `-k, --key <path>` | Write the redaction key JSON (replace mode) |
|
|
34
|
+
| `-d, --deanonymise <key>` | Restore text using a redaction key |
|
|
35
|
+
| `--revert <term>` | With `-d`, restore only this entity (repeatable) |
|
|
36
|
+
| `-r, --recursive` | Descend into subdirectories for a directory arg |
|
|
37
|
+
| `--workers <n>` | Batch files processed concurrently (min(4,cpus)) |
|
|
38
|
+
| `--labels <list>` | Entity labels to detect (default: all) |
|
|
39
|
+
| `--languages <list>` | Name-corpus languages, e.g. `cs,de,en` |
|
|
40
|
+
| `--countries <list>` | ISO 3166-1 alpha-2 deny-list/city scope |
|
|
41
|
+
| `--threshold <n>` | Minimum confidence score 0-1 (default 0.3) |
|
|
42
|
+
| `--redact-string <s>` | Replacement text in redact mode |
|
|
43
|
+
| `--json` | Emit entities + redacted text as JSON |
|
|
44
|
+
| `--quiet` | Suppress the stderr summary |
|
|
42
45
|
|
|
43
46
|
Run `anonymize --help` for the full reference, including the
|
|
44
47
|
`--json` schema and exit codes.
|
|
45
48
|
|
|
49
|
+
## Batch processing
|
|
50
|
+
|
|
51
|
+
A directory argument anonymizes the text files inside it,
|
|
52
|
+
mirroring the input tree into the `--output` directory:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Non-recursive: only files directly under docs/
|
|
56
|
+
anonymize -o out/ docs/
|
|
57
|
+
|
|
58
|
+
# Recursive, 8 files in flight at a time
|
|
59
|
+
anonymize --recursive --workers 8 -o out/ docs/
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Directory walks process regular files only and skip likely-binary
|
|
63
|
+
files (a NUL byte in the first 8 KiB); explicitly named files are
|
|
64
|
+
always processed. The stderr summary reports how many files were
|
|
65
|
+
processed, failed, and skipped; any failure sets exit code 1.
|
|
66
|
+
`--key` and `--json` apply to single inputs only.
|
|
67
|
+
|
|
68
|
+
`--workers` overlaps file I/O across files; redaction itself is a
|
|
69
|
+
synchronous native call, so it is serialized on the JS thread and
|
|
70
|
+
the shared pipeline is reused across all workers (identical output
|
|
71
|
+
regardless of the worker count).
|
|
72
|
+
|
|
73
|
+
## Selective de-anonymisation
|
|
74
|
+
|
|
75
|
+
`--revert` restores only the entities you name, leaving the rest
|
|
76
|
+
redacted. A term matches either a placeholder token or an original
|
|
77
|
+
value, case-sensitive and exact; it is repeatable:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
anonymize -d key.json --revert "[PERSON_1]" --revert "Jan Novák" reply.txt
|
|
81
|
+
```
|
|
82
|
+
|
|
46
83
|
## Scripting and agents
|
|
47
84
|
|
|
48
85
|
- Exit codes: `0` success, `1` runtime error, `2` usage error.
|
|
@@ -55,10 +92,17 @@ Run `anonymize --help` for the full reference, including the
|
|
|
55
92
|
|
|
56
93
|
## Standalone binary
|
|
57
94
|
|
|
58
|
-
`bun
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
95
|
+
The single-file `bun build --compile` binary is temporarily
|
|
96
|
+
unavailable. It embedded the previous in-process TS pipeline;
|
|
97
|
+
that engine has been replaced by the `@stll/anonymize-wasm`
|
|
98
|
+
native binding, which instantiates through the napi-rs
|
|
99
|
+
`wasm32-wasip1-threads` glue (`node:wasi` + worker threads).
|
|
100
|
+
Bun's `node:wasi` does not yet implement `WASI.prototype.initialize`,
|
|
101
|
+
so the binding cannot instantiate under the Bun runtime that a
|
|
102
|
+
compiled binary ships with. The binary will return once Bun
|
|
103
|
+
implements the missing `node:wasi` surface (or a non-threaded
|
|
104
|
+
single-file wasm artifact is available). The npm CLI above is
|
|
105
|
+
the supported distribution in the meantime.
|
|
62
106
|
|
|
63
107
|
## License
|
|
64
108
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1,27 +1,34 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as anonymize from "@stll/anonymize";
|
|
3
3
|
import { ALL_DICTIONARY_IDS, DICTIONARY_META, loadCityDictionary, loadDictionary, loadNameDictionaries } from "@stll/anonymize-data";
|
|
4
|
+
import { availableParallelism } from "node:os";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
5
6
|
import { realpathSync } from "node:fs";
|
|
6
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
-
import { basename, join, resolve } from "node:path";
|
|
7
|
+
import { mkdir, open, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
|
8
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
8
9
|
import { createInterface } from "node:readline/promises";
|
|
9
10
|
import { DEFAULT_ENTITY_LABELS } from "@stll/anonymize/constants";
|
|
10
11
|
//#region src/args.ts
|
|
11
12
|
const CLI_MODES = ["replace", "redact"];
|
|
12
13
|
const DEFAULT_THRESHOLD = .3;
|
|
13
14
|
const DEFAULT_REDACT_STRING = "[REDACTED]";
|
|
15
|
+
/** Default batch concurrency: min(4, cores). Workers overlap
|
|
16
|
+
* file reads/writes; the shared native pipeline runs each
|
|
17
|
+
* redaction to completion on the single JS thread. */
|
|
18
|
+
const defaultWorkerCount = () => Math.max(1, Math.min(4, availableParallelism()));
|
|
14
19
|
/** Invalid invocation; printed with usage hint, exit code 2. */
|
|
15
20
|
var UsageError = class extends Error {};
|
|
16
|
-
const HELP = `Usage: anonymize [options] [file ...]
|
|
21
|
+
const HELP = `Usage: anonymize [options] [file|dir ...]
|
|
17
22
|
|
|
18
23
|
Detect and anonymize PII in text. Reads the given files, or stdin
|
|
19
|
-
when no files are given.
|
|
24
|
+
when no files are given. A directory argument processes the text
|
|
25
|
+
files inside it (add --recursive to descend into subdirectories).
|
|
26
|
+
Writes to stdout, or to --output.
|
|
20
27
|
All processing is local; the CLI makes no network calls.
|
|
21
28
|
|
|
22
29
|
Options:
|
|
23
|
-
-o, --output <path> Output file, or directory
|
|
24
|
-
input files
|
|
30
|
+
-o, --output <path> Output file, or directory for batch
|
|
31
|
+
input (multiple files or a directory)
|
|
25
32
|
-m, --mode <mode> "replace" (reversible [PERSON_1]
|
|
26
33
|
placeholders) or "redact"
|
|
27
34
|
(default: replace)
|
|
@@ -29,6 +36,17 @@ Options:
|
|
|
29
36
|
(single input, replace mode)
|
|
30
37
|
-d, --deanonymise <path> Restore redacted text using the
|
|
31
38
|
redaction key at <path>
|
|
39
|
+
--revert <term> With --deanonymise, restore only the
|
|
40
|
+
given entity. Match a placeholder token
|
|
41
|
+
("[PERSON_1]") or an original value
|
|
42
|
+
("Jan Novák"), case-sensitive exact.
|
|
43
|
+
Repeatable; others stay redacted
|
|
44
|
+
-r, --recursive Descend into subdirectories when a
|
|
45
|
+
directory is given as input
|
|
46
|
+
--workers <n> Batch files to process concurrently
|
|
47
|
+
(default: min(4, CPU cores)). Overlaps
|
|
48
|
+
file I/O; redaction is serialized on
|
|
49
|
+
the JS thread
|
|
32
50
|
--labels <list> Comma-separated entity labels to detect
|
|
33
51
|
(default: all). Accepts canonical labels
|
|
34
52
|
("email address"), short aliases (email,
|
|
@@ -52,6 +70,15 @@ Options:
|
|
|
52
70
|
--list-labels List detectable entity labels and the
|
|
53
71
|
short aliases accepted by --labels
|
|
54
72
|
|
|
73
|
+
Batch input (directory or multiple files):
|
|
74
|
+
Requires --output <directory>. The input tree is mirrored
|
|
75
|
+
into the output directory. Directory walks process regular
|
|
76
|
+
files only and skip likely-binary files (a NUL byte in the
|
|
77
|
+
first 8 KiB); explicitly named files are always processed.
|
|
78
|
+
The stderr summary reports how many files were processed,
|
|
79
|
+
failed, and skipped; any failure sets exit code 1.
|
|
80
|
+
--key and --json apply to single inputs only.
|
|
81
|
+
|
|
55
82
|
Interactive prompt:
|
|
56
83
|
When run on files from a terminal without --countries or
|
|
57
84
|
--languages, the CLI asks once which country scope to load.
|
|
@@ -77,6 +104,8 @@ Examples:
|
|
|
77
104
|
anonymize contract.txt > contract.anon.txt
|
|
78
105
|
anonymize -k contract.key.json -o contract.anon.txt contract.txt
|
|
79
106
|
anonymize -d contract.key.json contract.anon.txt
|
|
107
|
+
anonymize -r --workers 8 -o out/ docs/
|
|
108
|
+
anonymize -d key.json --revert "[PERSON_1]" contract.anon.txt
|
|
80
109
|
cat notes.md | anonymize --countries CZ,SK --languages cs,sk
|
|
81
110
|
anonymize --json --quiet input.txt | jq '.entities[].label'
|
|
82
111
|
`;
|
|
@@ -86,6 +115,11 @@ const parseThreshold = (raw) => {
|
|
|
86
115
|
if (!Number.isFinite(value) || value < 0 || value > 1) throw new UsageError(`--threshold must be a number between 0 and 1, got "${raw}"`);
|
|
87
116
|
return value;
|
|
88
117
|
};
|
|
118
|
+
const parseWorkers = (raw) => {
|
|
119
|
+
const value = Number(raw);
|
|
120
|
+
if (!Number.isInteger(value) || value < 1) throw new UsageError(`--workers must be a positive integer, got "${raw}"`);
|
|
121
|
+
return value;
|
|
122
|
+
};
|
|
89
123
|
const parseMode = (raw) => {
|
|
90
124
|
const mode = CLI_MODES.find((candidate) => candidate === raw);
|
|
91
125
|
if (!mode) throw new UsageError(`--mode must be one of: ${CLI_MODES.join(", ")}; got "${raw}"`);
|
|
@@ -115,6 +149,9 @@ const parseCliArgs = (argv) => {
|
|
|
115
149
|
mode: values.mode === void 0 ? "replace" : parseMode(values.mode),
|
|
116
150
|
keyPath: values.key,
|
|
117
151
|
deanonymiseKeyPath: values.deanonymise,
|
|
152
|
+
revert: values.revert === void 0 || values.revert.length === 0 ? void 0 : values.revert,
|
|
153
|
+
recursive: values.recursive === true,
|
|
154
|
+
workers: values.workers === void 0 ? defaultWorkerCount() : parseWorkers(values.workers),
|
|
118
155
|
labels: values.labels === void 0 ? void 0 : splitList(values.labels),
|
|
119
156
|
languages: values.languages === void 0 ? void 0 : splitList(values.languages),
|
|
120
157
|
countries: values.countries === void 0 ? void 0 : parseCountries(values.countries),
|
|
@@ -147,6 +184,15 @@ const PARSE_CONFIG = {
|
|
|
147
184
|
type: "string",
|
|
148
185
|
short: "d"
|
|
149
186
|
},
|
|
187
|
+
revert: {
|
|
188
|
+
type: "string",
|
|
189
|
+
multiple: true
|
|
190
|
+
},
|
|
191
|
+
recursive: {
|
|
192
|
+
type: "boolean",
|
|
193
|
+
short: "r"
|
|
194
|
+
},
|
|
195
|
+
workers: { type: "string" },
|
|
150
196
|
labels: { type: "string" },
|
|
151
197
|
languages: { type: "string" },
|
|
152
198
|
countries: { type: "string" },
|
|
@@ -261,7 +307,7 @@ const loadCliDictionaries = async ({ languages, countries }) => {
|
|
|
261
307
|
};
|
|
262
308
|
//#endregion
|
|
263
309
|
//#region package.json
|
|
264
|
-
var version = "
|
|
310
|
+
var version = "2.0.0";
|
|
265
311
|
//#endregion
|
|
266
312
|
//#region src/main.ts
|
|
267
313
|
const cliVersion = () => version;
|
|
@@ -296,6 +342,108 @@ const readInputs = async (files) => {
|
|
|
296
342
|
text: await readFile(path, "utf8")
|
|
297
343
|
})));
|
|
298
344
|
};
|
|
345
|
+
const TEXT_SNIFF_BYTES = 8192;
|
|
346
|
+
/**
|
|
347
|
+
* True when the file's first {@link TEXT_SNIFF_BYTES} bytes
|
|
348
|
+
* contain no NUL byte. Used to skip binaries discovered by a
|
|
349
|
+
* directory walk without reading the whole file.
|
|
350
|
+
*/
|
|
351
|
+
const looksTextual = async (path) => {
|
|
352
|
+
const handle = await open(path, "r");
|
|
353
|
+
try {
|
|
354
|
+
const buffer = Buffer.alloc(TEXT_SNIFF_BYTES);
|
|
355
|
+
const { bytesRead } = await handle.read(buffer, 0, TEXT_SNIFF_BYTES, 0);
|
|
356
|
+
return buffer.subarray(0, bytesRead).indexOf(0) === -1;
|
|
357
|
+
} finally {
|
|
358
|
+
await handle.close();
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
/**
|
|
362
|
+
* Collect regular files under `root`, sorted for deterministic
|
|
363
|
+
* order. Symlinks are skipped (avoids cycles and escaping the
|
|
364
|
+
* tree); subdirectories are descended only when `recursive`.
|
|
365
|
+
*/
|
|
366
|
+
const walkDirectory = async (root, recursive, excludeDir) => {
|
|
367
|
+
const found = [];
|
|
368
|
+
const visit = async (dir) => {
|
|
369
|
+
const entries = (await readdir(dir, { withFileTypes: true })).toSorted((a, b) => a.name.localeCompare(b.name));
|
|
370
|
+
for (const entry of entries) {
|
|
371
|
+
const full = join(dir, entry.name);
|
|
372
|
+
if (entry.isDirectory()) {
|
|
373
|
+
if (excludeDir !== void 0 && resolve(full) === excludeDir) continue;
|
|
374
|
+
if (recursive) await visit(full);
|
|
375
|
+
} else if (entry.isFile()) found.push(full);
|
|
376
|
+
}
|
|
377
|
+
};
|
|
378
|
+
await visit(root);
|
|
379
|
+
return found;
|
|
380
|
+
};
|
|
381
|
+
/**
|
|
382
|
+
* Expand positional arguments into concrete file jobs. A file
|
|
383
|
+
* argument becomes one job (always processed); a directory is
|
|
384
|
+
* walked, mirroring its tree into the output and skipping
|
|
385
|
+
* likely-binary files.
|
|
386
|
+
*/
|
|
387
|
+
const expandInputs = async (files, recursive, outputDir) => {
|
|
388
|
+
const excludeDir = outputDir === void 0 ? void 0 : resolve(outputDir);
|
|
389
|
+
const jobs = [];
|
|
390
|
+
let hasDirectory = false;
|
|
391
|
+
let skipped = 0;
|
|
392
|
+
for (const path of files) {
|
|
393
|
+
let stats;
|
|
394
|
+
try {
|
|
395
|
+
stats = await stat(path);
|
|
396
|
+
} catch {
|
|
397
|
+
jobs.push({
|
|
398
|
+
path,
|
|
399
|
+
outputRelative: basename(path)
|
|
400
|
+
});
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (!stats.isDirectory()) {
|
|
404
|
+
jobs.push({
|
|
405
|
+
path,
|
|
406
|
+
outputRelative: basename(path)
|
|
407
|
+
});
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
hasDirectory = true;
|
|
411
|
+
for (const file of await walkDirectory(path, recursive, excludeDir)) {
|
|
412
|
+
if (!await looksTextual(file).catch(() => true)) {
|
|
413
|
+
skipped += 1;
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
jobs.push({
|
|
417
|
+
path: file,
|
|
418
|
+
outputRelative: relative(path, file)
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
jobs,
|
|
424
|
+
batch: hasDirectory || jobs.length > 1,
|
|
425
|
+
skipped
|
|
426
|
+
};
|
|
427
|
+
};
|
|
428
|
+
/**
|
|
429
|
+
* Run `task` over `items` with at most `workers` in flight.
|
|
430
|
+
* The shared native pipeline makes each redaction a synchronous
|
|
431
|
+
* native call, so concurrency here only overlaps async file
|
|
432
|
+
* I/O; the increments below are safe without locking because
|
|
433
|
+
* no `await` sits between the read and the write of `next`.
|
|
434
|
+
*/
|
|
435
|
+
const runPool = async (items, workers, task) => {
|
|
436
|
+
let next = 0;
|
|
437
|
+
const worker = async () => {
|
|
438
|
+
while (next < items.length) {
|
|
439
|
+
const index = next;
|
|
440
|
+
next += 1;
|
|
441
|
+
await task(items[index]);
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
const count = Math.max(1, Math.min(workers, items.length));
|
|
445
|
+
await Promise.all(Array.from({ length: count }, worker));
|
|
446
|
+
};
|
|
299
447
|
const LABEL_ALIASES = {
|
|
300
448
|
email: "email address",
|
|
301
449
|
phone: "phone number",
|
|
@@ -366,9 +514,12 @@ const buildPipelineConfig = async (opts, loadDictionaries) => {
|
|
|
366
514
|
dictionaries
|
|
367
515
|
};
|
|
368
516
|
};
|
|
369
|
-
const buildOperatorConfig = (opts
|
|
517
|
+
const buildOperatorConfig = (opts) => {
|
|
370
518
|
const operators = {};
|
|
371
|
-
if (opts.mode === "redact")
|
|
519
|
+
if (opts.mode === "redact") {
|
|
520
|
+
const labels = opts.labels === void 0 ? DEFAULT_ENTITY_LABELS : validateLabels(opts.labels);
|
|
521
|
+
for (const label of labels) operators[label] = "redact";
|
|
522
|
+
}
|
|
372
523
|
return {
|
|
373
524
|
operators,
|
|
374
525
|
redactString: opts.redactString
|
|
@@ -399,6 +550,32 @@ const parseRedactionKey = (raw) => {
|
|
|
399
550
|
return map;
|
|
400
551
|
};
|
|
401
552
|
/**
|
|
553
|
+
* Restrict a redaction key to the entities named by --revert.
|
|
554
|
+
* Each token matches a placeholder ("[PERSON_1]") or an original
|
|
555
|
+
* value ("Jan Novák"), case-sensitive and exact. A token that
|
|
556
|
+
* matches nothing is a usage error listing the placeholders the
|
|
557
|
+
* key does define, so the caller can correct the spelling.
|
|
558
|
+
*/
|
|
559
|
+
const selectRevertEntries = (redactionMap, tokens) => {
|
|
560
|
+
const selected = /* @__PURE__ */ new Map();
|
|
561
|
+
for (const token of tokens) {
|
|
562
|
+
let matched = false;
|
|
563
|
+
for (const [placeholder, original] of redactionMap) if (placeholder === token || original === token) {
|
|
564
|
+
selected.set(placeholder, original);
|
|
565
|
+
matched = true;
|
|
566
|
+
}
|
|
567
|
+
if (!matched) {
|
|
568
|
+
const MAX_LISTED_PLACEHOLDERS = 20;
|
|
569
|
+
const placeholders = [...redactionMap.keys()];
|
|
570
|
+
const listed = placeholders.slice(0, MAX_LISTED_PLACEHOLDERS).join(", ");
|
|
571
|
+
const rest = placeholders.length - MAX_LISTED_PLACEHOLDERS;
|
|
572
|
+
const suffix = rest > 0 ? ` and ${rest} more` : "";
|
|
573
|
+
throw new UsageError(`--revert ${JSON.stringify(token)} matched no placeholder or original; available placeholders: ${listed}${suffix}`);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return selected;
|
|
577
|
+
};
|
|
578
|
+
/**
|
|
402
579
|
* Ask for a country scope when running interactively on
|
|
403
580
|
* files with no scope flags. Skipped for piped stdin so
|
|
404
581
|
* the CLI stays scriptable.
|
|
@@ -420,7 +597,8 @@ const runDeanonymise = async (opts, api) => {
|
|
|
420
597
|
if (opts.keyPath !== void 0) throw new UsageError("--key cannot be combined with --deanonymise");
|
|
421
598
|
const keyPath = opts.deanonymiseKeyPath;
|
|
422
599
|
if (keyPath === void 0) throw new UsageError("missing redaction key path");
|
|
423
|
-
const
|
|
600
|
+
const fullMap = parseRedactionKey(await readFile(keyPath, "utf8"));
|
|
601
|
+
const redactionMap = opts.revert === void 0 ? fullMap : selectRevertEntries(fullMap, opts.revert);
|
|
424
602
|
const inputs = await readInputs(opts.files);
|
|
425
603
|
if (inputs.length > 1) throw new UsageError("--deanonymise accepts a single input");
|
|
426
604
|
const input = inputs[0];
|
|
@@ -431,12 +609,6 @@ const runDeanonymise = async (opts, api) => {
|
|
|
431
609
|
}]);
|
|
432
610
|
await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));
|
|
433
611
|
};
|
|
434
|
-
const outputPathFor = (input, opts, multi) => {
|
|
435
|
-
if (opts.output === void 0) return void 0;
|
|
436
|
-
if (!multi) return opts.output;
|
|
437
|
-
if (input.path === null) throw new UsageError("stdin cannot be combined with multiple files");
|
|
438
|
-
return join(opts.output, basename(input.path));
|
|
439
|
-
};
|
|
440
612
|
/**
|
|
441
613
|
* Reject any write target (output or key file) whose
|
|
442
614
|
* filesystem identity collides with an input file or with
|
|
@@ -459,11 +631,56 @@ const summarize = (entities) => {
|
|
|
459
631
|
const parts = [...counts.entries()].toSorted((a, b) => b[1] - a[1]).map(([label, count]) => `${label}: ${count}`);
|
|
460
632
|
return parts.length > 0 ? parts.join(", ") : "none";
|
|
461
633
|
};
|
|
634
|
+
const runAnonymiseSingle = async (opts, runtime, api, input) => {
|
|
635
|
+
const { entities, redaction } = await runtime.redact(input.text, buildOperatorConfig(opts));
|
|
636
|
+
if (opts.json) {
|
|
637
|
+
const jsonEntities = opts.mode === "redact" ? entities.map(({ start, end, label, score, source }) => ({
|
|
638
|
+
start,
|
|
639
|
+
end,
|
|
640
|
+
label,
|
|
641
|
+
score,
|
|
642
|
+
source
|
|
643
|
+
})) : entities;
|
|
644
|
+
const payload = {
|
|
645
|
+
entityCount: redaction.entityCount,
|
|
646
|
+
entities: jsonEntities,
|
|
647
|
+
redactedText: redaction.redactedText
|
|
648
|
+
};
|
|
649
|
+
await writeOutput(input.outputPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
650
|
+
} else await writeOutput(input.outputPath, redaction.redactedText);
|
|
651
|
+
if (opts.keyPath !== void 0) await writeFile(opts.keyPath, api.exportRedactionKey(redaction.redactionMap, redaction.operatorMap), "utf8");
|
|
652
|
+
if (!opts.quiet) process.stderr.write(`anonymize: ${input.source}: ${summarize(entities)}\n`);
|
|
653
|
+
};
|
|
654
|
+
const runAnonymiseBatch = async (opts, runtime, output, jobs, skipped) => {
|
|
655
|
+
await mkdir(output, { recursive: true });
|
|
656
|
+
const operatorConfig = buildOperatorConfig(opts);
|
|
657
|
+
const outcome = {
|
|
658
|
+
processed: 0,
|
|
659
|
+
failed: 0
|
|
660
|
+
};
|
|
661
|
+
await runPool(jobs, opts.workers, async (job) => {
|
|
662
|
+
const outputPath = join(output, job.outputRelative);
|
|
663
|
+
try {
|
|
664
|
+
const text = await readFile(job.path, "utf8");
|
|
665
|
+
const { entities, redaction } = await runtime.redact(text, operatorConfig);
|
|
666
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
667
|
+
await writeFile(outputPath, redaction.redactedText, "utf8");
|
|
668
|
+
outcome.processed += 1;
|
|
669
|
+
if (!opts.quiet) process.stderr.write(`anonymize: ${job.path}: ${summarize(entities)}\n`);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
outcome.failed += 1;
|
|
672
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
673
|
+
process.stderr.write(`anonymize: ${job.path}: error: ${message}\n`);
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
if (!opts.quiet) {
|
|
677
|
+
const parts = [`${outcome.processed} processed`, `${outcome.failed} failed`];
|
|
678
|
+
if (skipped > 0) parts.push(`${skipped} skipped`);
|
|
679
|
+
process.stderr.write(`anonymize: ${parts.join(", ")}\n`);
|
|
680
|
+
}
|
|
681
|
+
if (outcome.failed > 0) process.exitCode = 1;
|
|
682
|
+
};
|
|
462
683
|
const runAnonymise = async (opts, { api, loadDictionaries }) => {
|
|
463
|
-
const multi = opts.files.length > 1;
|
|
464
|
-
if (multi && opts.output === void 0) throw new UsageError("multiple input files require --output <directory>");
|
|
465
|
-
if (multi && opts.keyPath !== void 0) throw new UsageError("--key works with a single input only");
|
|
466
|
-
if (multi && opts.json) throw new UsageError("--json works with a single input only");
|
|
467
684
|
if (opts.keyPath !== void 0 && opts.mode !== "replace") throw new UsageError("--key requires --mode \"replace\"");
|
|
468
685
|
const scoped = shouldPromptForScope(opts, {
|
|
469
686
|
stdinIsTTY: process.stdin.isTTY === true,
|
|
@@ -472,51 +689,66 @@ const runAnonymise = async (opts, { api, loadDictionaries }) => {
|
|
|
472
689
|
...opts,
|
|
473
690
|
countries: await promptForCountries()
|
|
474
691
|
} : opts;
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
692
|
+
if (scoped.files.length === 0) {
|
|
693
|
+
const [input] = await readInputs(scoped.files);
|
|
694
|
+
if (!input) throw new UsageError("no input to anonymize");
|
|
695
|
+
guardWriteTargets([], collectSingleTargets(scoped));
|
|
696
|
+
await runAnonymiseSingle(scoped, await prepareCliRuntime(api, await buildPipelineConfig(scoped, loadDictionaries)), api, {
|
|
697
|
+
text: input.text,
|
|
698
|
+
outputPath: scoped.output,
|
|
699
|
+
source: "stdin"
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const { jobs, batch, skipped } = await expandInputs(scoped.files, scoped.recursive, scoped.output);
|
|
704
|
+
if (!batch) {
|
|
705
|
+
const [job] = jobs;
|
|
706
|
+
if (!job) throw new UsageError("no input to anonymize");
|
|
707
|
+
guardWriteTargets([job.path], collectSingleTargets(scoped));
|
|
708
|
+
await runAnonymiseSingle(scoped, await prepareCliRuntime(api, await buildPipelineConfig(scoped, loadDictionaries)), api, {
|
|
709
|
+
text: await readFile(job.path, "utf8"),
|
|
710
|
+
outputPath: scoped.output,
|
|
711
|
+
source: job.path
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const output = scoped.output;
|
|
716
|
+
if (output === void 0) throw new UsageError("batch input (a directory or multiple files) requires --output <directory>");
|
|
717
|
+
if (scoped.keyPath !== void 0) throw new UsageError("--key works with a single input only");
|
|
718
|
+
if (scoped.json) throw new UsageError("--json works with a single input only");
|
|
719
|
+
guardWriteTargets(jobs.map((job) => job.path), jobs.map((job) => ({
|
|
720
|
+
path: join(output, job.outputRelative),
|
|
721
|
+
flag: "--output"
|
|
722
|
+
})));
|
|
723
|
+
await runAnonymiseBatch(scoped, await prepareCliRuntime(api, await buildPipelineConfig(scoped, loadDictionaries)), output, jobs, skipped);
|
|
724
|
+
};
|
|
725
|
+
/** Write targets for a single-input run: --output and --key. */
|
|
726
|
+
const collectSingleTargets = (opts) => {
|
|
727
|
+
const targets = [];
|
|
728
|
+
if (opts.output !== void 0) targets.push({
|
|
729
|
+
path: opts.output,
|
|
480
730
|
flag: "--output"
|
|
481
731
|
});
|
|
482
|
-
if (opts.keyPath !== void 0)
|
|
732
|
+
if (opts.keyPath !== void 0) targets.push({
|
|
483
733
|
path: opts.keyPath,
|
|
484
734
|
flag: "--key"
|
|
485
735
|
});
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
end,
|
|
503
|
-
label,
|
|
504
|
-
score,
|
|
505
|
-
source
|
|
506
|
-
})) : entities;
|
|
507
|
-
const payload = {
|
|
508
|
-
entityCount: result.entityCount,
|
|
509
|
-
entities: jsonEntities,
|
|
510
|
-
redactedText: result.redactedText
|
|
511
|
-
};
|
|
512
|
-
await writeOutput(outputPath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
513
|
-
} else await writeOutput(outputPath, result.redactedText);
|
|
514
|
-
if (opts.keyPath !== void 0) await writeFile(opts.keyPath, api.exportRedactionKey(result.redactionMap, result.operatorMap), "utf8");
|
|
515
|
-
if (!opts.quiet) {
|
|
516
|
-
const source = input.path ?? "stdin";
|
|
517
|
-
process.stderr.write(`anonymize: ${source}: ${summarize(entities)}\n`);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
736
|
+
return targets;
|
|
737
|
+
};
|
|
738
|
+
const prepareCliRuntime = async (api, config) => {
|
|
739
|
+
const pipeline = await api.createNativePipelineFromConfig({
|
|
740
|
+
binding: api.loadNativeAnonymizeBinding(),
|
|
741
|
+
config,
|
|
742
|
+
gazetteerEntries: []
|
|
743
|
+
});
|
|
744
|
+
pipeline.warmLazyRegex?.();
|
|
745
|
+
return { redact: async (fullText, operators) => {
|
|
746
|
+
const result = pipeline.redactText(fullText, operators);
|
|
747
|
+
return {
|
|
748
|
+
entities: result.resolvedEntities,
|
|
749
|
+
redaction: result.redaction
|
|
750
|
+
};
|
|
751
|
+
} };
|
|
520
752
|
};
|
|
521
753
|
/**
|
|
522
754
|
* Render the canonical entity labels and the short aliases
|
|
@@ -550,6 +782,7 @@ const dispatch = async (engine) => {
|
|
|
550
782
|
await runDeanonymise(opts, engine.api);
|
|
551
783
|
return;
|
|
552
784
|
}
|
|
785
|
+
if (opts.revert !== void 0) throw new UsageError("--revert requires --deanonymise <key>");
|
|
553
786
|
await runAnonymise(opts, engine);
|
|
554
787
|
};
|
|
555
788
|
/**
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":["pkg.version","known"],"sources":["../src/args.ts","../src/dictionary-scope.ts","../src/dictionaries.ts","../package.json","../src/main.ts","../src/cli.ts"],"sourcesContent":["import { parseArgs } from \"node:util\";\n\nexport const CLI_MODES = [\"replace\", \"redact\"] as const;\nexport type CliMode = (typeof CLI_MODES)[number];\n\nexport const DEFAULT_THRESHOLD = 0.3;\nexport const DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/** Invalid invocation; printed with usage hint, exit code 2. */\nexport class UsageError extends Error {}\n\nexport type CliOptions = {\n files: string[];\n output?: string | undefined;\n mode: CliMode;\n keyPath?: string | undefined;\n deanonymiseKeyPath?: string | undefined;\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n redactString: string;\n json: boolean;\n quiet: boolean;\n help: boolean;\n version: boolean;\n listLabels: boolean;\n};\n\nexport const HELP = `Usage: anonymize [options] [file ...]\n\nDetect and anonymize PII in text. Reads the given files, or stdin\nwhen no files are given. Writes to stdout, or to --output.\nAll processing is local; the CLI makes no network calls.\n\nOptions:\n -o, --output <path> Output file, or directory when multiple\n input files are given\n -m, --mode <mode> \"replace\" (reversible [PERSON_1]\n placeholders) or \"redact\"\n (default: replace)\n -k, --key <path> Write the redaction key as JSON\n (single input, replace mode)\n -d, --deanonymise <path> Restore redacted text using the\n redaction key at <path>\n --labels <list> Comma-separated entity labels to detect\n (default: all). Accepts canonical labels\n (\"email address\"), short aliases (email,\n phone, org, dob, ssn), and hyphen/underscore\n forms (\"credit-card-number\")\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n (default: all bundled)\n --countries <list> ISO 3166-1 alpha-2 codes scoping deny\n lists and city data, e.g. \"CZ,DE,GB\"\n (default: all deny lists; city data\n for a 30-country default set)\n --threshold <n> Minimum confidence score, 0-1\n (default: ${DEFAULT_THRESHOLD})\n --redact-string <s> Replacement text in redact mode\n (default: \"${DEFAULT_REDACT_STRING}\")\n --json Emit JSON (entities + redacted text) to\n stdout (single input only)\n --quiet Suppress the summary on stderr\n -h, --help Show this help\n -v, --version Show the version\n --list-labels List detectable entity labels and the\n short aliases accepted by --labels\n\nInteractive prompt:\n When run on files from a terminal without --countries or\n --languages, the CLI asks once which country scope to load.\n Piped stdin/stderr or --quiet skips the prompt, so scripts\n and agents never block on input.\n\nExit codes:\n 0 success\n 1 runtime error (message on stderr)\n 2 usage error (message on stderr)\n\nJSON output (--json):\n { \"entityCount\": number,\n \"entities\": [{ \"start\": number, \"end\": number,\n \"label\": string, \"text\": string,\n \"score\": number, \"source\": string }],\n \"redactedText\": string }\n Offsets are UTF-16 code-unit indexes into the input.\n The stderr summary contains entity counts only, never\n the detected text.\n\nExamples:\n anonymize contract.txt > contract.anon.txt\n anonymize -k contract.key.json -o contract.anon.txt contract.txt\n anonymize -d contract.key.json contract.anon.txt\n cat notes.md | anonymize --countries CZ,SK --languages cs,sk\n anonymize --json --quiet input.txt | jq '.entities[].label'\n`;\n\nconst splitList = (value: string): string[] => [\n ...new Set(\n value\n .split(\",\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0),\n ),\n];\n\nconst parseThreshold = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new UsageError(\n `--threshold must be a number between 0 and 1, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseMode = (raw: string): CliMode => {\n const mode = CLI_MODES.find((candidate) => candidate === raw);\n if (!mode) {\n throw new UsageError(\n `--mode must be one of: ${CLI_MODES.join(\", \")}; got \"${raw}\"`,\n );\n }\n return mode;\n};\n\nconst COUNTRY_CODE_RE = /^[A-Za-z]{2}$/;\n\nexport const parseCountries = (raw: string): string[] => {\n const countries = [\n ...new Set(splitList(raw).map((code) => code.toUpperCase())),\n ];\n const invalid = countries.find((code) => !COUNTRY_CODE_RE.test(code));\n if (invalid) {\n throw new UsageError(\n `--countries expects ISO 3166-1 alpha-2 codes (e.g. \"CZ,DE\"), got \"${invalid}\"`,\n );\n }\n return countries;\n};\n\nexport const parseCliArgs = (argv: string[]): CliOptions => {\n let parsed: ReturnType<typeof parseArgs<typeof PARSE_CONFIG>>;\n try {\n parsed = parseArgs({ ...PARSE_CONFIG, args: argv });\n } catch (err) {\n throw new UsageError(err instanceof Error ? err.message : String(err));\n }\n const { values, positionals } = parsed;\n\n return {\n files: positionals,\n output: values.output,\n mode: values.mode === undefined ? \"replace\" : parseMode(values.mode),\n keyPath: values.key,\n deanonymiseKeyPath: values.deanonymise,\n labels: values.labels === undefined ? undefined : splitList(values.labels),\n languages:\n values.languages === undefined ? undefined : splitList(values.languages),\n countries:\n values.countries === undefined\n ? undefined\n : parseCountries(values.countries),\n threshold:\n values.threshold === undefined\n ? DEFAULT_THRESHOLD\n : parseThreshold(values.threshold),\n redactString: values[\"redact-string\"] ?? DEFAULT_REDACT_STRING,\n json: values.json === true,\n quiet: values.quiet === true,\n help: values.help === true,\n version: values.version === true,\n listLabels: values[\"list-labels\"] === true,\n };\n};\n\nconst PARSE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n output: { type: \"string\", short: \"o\" },\n mode: { type: \"string\", short: \"m\" },\n key: { type: \"string\", short: \"k\" },\n deanonymise: { type: \"string\", short: \"d\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n \"redact-string\": { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n \"list-labels\": { type: \"boolean\" },\n },\n} as const;\n","/* Pure helpers shared by the npm and embedded dictionary\n * loaders. Must stay free of @stll/anonymize-data imports\n * so the compiled binary's bundle excludes the raw JSON\n * dictionary modules. */\nimport type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\n\nimport { UsageError } from \"./args\";\n\nexport const NAME_DICTIONARY_PREFIXES = [\n \"names/first/\",\n \"names/surnames/\",\n] as const;\n\n/** Language code of a name dictionary id, or null. */\nexport const nameLanguageOfDictionary = (id: string): string | null => {\n const prefix = NAME_DICTIONARY_PREFIXES.find((p) => id.startsWith(p));\n return prefix ? id.slice(prefix.length) : null;\n};\n\nexport type DictionaryScope = {\n languages?: readonly string[] | undefined;\n countries?: readonly string[] | undefined;\n};\n\nconst pickKeys = <T>(\n record: Record<string, T>,\n keep: (key: string) => boolean,\n): Record<string, T> => {\n const result: Record<string, T> = {};\n for (const [key, value] of Object.entries(record)) {\n if (keep(key)) result[key] = value;\n }\n return result;\n};\n\n/** Dictionaries with every section present (possibly empty). */\nexport type ScopedDictionaries = Dictionaries & {\n firstNames: Record<string, readonly string[]>;\n surnames: Record<string, readonly string[]>;\n denyList: Record<string, readonly string[]>;\n denyListMeta: Record<string, DictionaryMeta>;\n citiesByCountry: Record<string, readonly string[]>;\n};\n\n/**\n * Scope a fully loaded dictionary set to the requested\n * languages and countries. Mirrors the pre-load scoping\n * the npm loader does in dictionaries.ts; used by the\n * embedded loader, which always starts from the full set.\n */\nexport const filterDictionaries = (\n all: Dictionaries,\n { languages, countries }: DictionaryScope,\n): ScopedDictionaries => {\n const firstNames = all.firstNames ?? {};\n const surnames = all.surnames ?? {};\n const allDenyList = all.denyList ?? {};\n const allDenyListMeta = all.denyListMeta ?? {};\n\n if (languages !== undefined) {\n const available = Object.keys(firstNames);\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n }\n const keepLanguage = (lang: string): boolean =>\n languages === undefined || languages.includes(lang);\n const keepCountry = (country: string | null): boolean =>\n countries === undefined || country === null || countries.includes(country);\n\n const denyListMeta: Record<string, DictionaryMeta> = {};\n const denyList: Record<string, readonly string[]> = {};\n for (const [id, meta] of Object.entries(allDenyListMeta)) {\n if (!keepCountry(meta.country)) continue;\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && !keepLanguage(nameLang)) continue;\n const entries = allDenyList[id];\n if (entries === undefined) continue;\n denyListMeta[id] = meta;\n denyList[id] = entries;\n }\n\n return {\n firstNames: pickKeys(firstNames, keepLanguage),\n surnames: pickKeys(surnames, keepLanguage),\n denyList,\n denyListMeta,\n citiesByCountry: pickKeys(all.citiesByCountry ?? {}, (country) =>\n keepCountry(country),\n ),\n };\n};\n","import type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\nimport {\n ALL_DICTIONARY_IDS,\n DICTIONARY_META,\n loadCityDictionary,\n loadDictionary,\n loadNameDictionaries,\n type NameLanguage,\n} from \"@stll/anonymize-data\";\n\nimport { UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\nimport {\n NAME_DICTIONARY_PREFIXES,\n nameLanguageOfDictionary,\n} from \"./dictionary-scope\";\n\n/**\n * Countries with bundled city dictionaries that are\n * loaded when no --countries scope is given.\n */\nconst DEFAULT_CITY_COUNTRIES = [\n \"AT\",\n \"AU\",\n \"BE\",\n \"BG\",\n \"BR\",\n \"CA\",\n \"CH\",\n \"CZ\",\n \"DE\",\n \"DK\",\n \"ES\",\n \"FI\",\n \"FR\",\n \"GB\",\n \"GR\",\n \"HR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LU\",\n \"NL\",\n \"NO\",\n \"NZ\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SE\",\n \"SI\",\n \"SK\",\n \"US\",\n] as const;\n\nconst availableNameLanguages = (): readonly string[] =>\n ALL_DICTIONARY_IDS.filter((id) =>\n id.startsWith(NAME_DICTIONARY_PREFIXES[0]),\n ).map((id) => id.slice(NAME_DICTIONARY_PREFIXES[0].length));\n\nconst validateLanguages = (\n languages: readonly string[],\n): readonly NameLanguage[] => {\n const available = availableNameLanguages();\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n // SAFETY: every entry was checked against the bundled\n // name dictionary ids, which define NameLanguage.\n return languages as readonly NameLanguage[];\n};\n\nexport type LoadCliDictionariesOptions = DictionaryScope;\n\n/**\n * Load the bundled @stll/anonymize-data dictionaries,\n * scoped to the requested languages and countries.\n */\nexport const loadCliDictionaries = async ({\n languages,\n countries,\n}: LoadCliDictionariesOptions): Promise<Dictionaries> => {\n const nameLanguages =\n languages === undefined ? undefined : validateLanguages(languages);\n\n const denyIds = ALL_DICTIONARY_IDS.filter((id) => {\n const meta = DICTIONARY_META[id];\n if (\n countries &&\n meta.country !== null &&\n !countries.includes(meta.country)\n ) {\n return false;\n }\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && nameLanguages !== undefined) {\n return nameLanguages.includes(\n // SAFETY: nameLang comes from a bundled dictionary\n // id, which defines NameLanguage.\n nameLang as NameLanguage,\n );\n }\n return true;\n });\n\n const cityCountries = countries ?? DEFAULT_CITY_COUNTRIES;\n\n const [names, denyEntries, cityEntries] = await Promise.all([\n loadNameDictionaries(nameLanguages),\n Promise.all(\n denyIds.map(async (id) => ({ id, entries: await loadDictionary(id) })),\n ),\n Promise.all(\n cityCountries.map(async (country) => ({\n country,\n entries: await loadCityDictionary(country),\n })),\n ),\n ]);\n\n const denyList: Record<string, readonly string[]> = {};\n const denyListMeta: Record<string, DictionaryMeta> = {};\n for (const { id, entries } of denyEntries) {\n denyList[id] = entries;\n // SAFETY: anonymize-data categories match\n // DenyListCategory at runtime.\n denyListMeta[id] = DICTIONARY_META[id] as DictionaryMeta;\n }\n\n const citiesByCountry: Record<string, readonly string[]> = {};\n for (const { country, entries } of cityEntries) {\n if (entries.length > 0) citiesByCountry[country] = entries;\n }\n\n return {\n firstNames: names.firstNames,\n surnames: names.surnames,\n denyList,\n denyListMeta,\n citiesByCountry,\n };\n};\n","","import { realpathSync } from \"node:fs\";\nimport { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { basename, join, resolve } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\n\nimport type {\n createPipelineContext,\n deanonymise,\n Dictionaries,\n Entity,\n exportRedactionKey,\n OperatorConfig,\n OperatorType,\n PipelineConfig,\n redactText,\n runPipeline,\n} from \"@stll/anonymize\";\nimport { DEFAULT_ENTITY_LABELS } from \"@stll/anonymize/constants\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nimport type { CliOptions } from \"./args\";\nimport { HELP, parseCliArgs, parseCountries, UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\n\n/**\n * The pipeline functions the CLI needs. Satisfied by both\n * @stll/anonymize (native) and @stll/anonymize-wasm, so the\n * entry point decides which engine backs the binary.\n */\nexport type AnonymizeApi = {\n createPipelineContext: typeof createPipelineContext;\n deanonymise: typeof deanonymise;\n exportRedactionKey: typeof exportRedactionKey;\n redactText: typeof redactText;\n runPipeline: typeof runPipeline;\n};\n\n/**\n * Everything an entry point injects: the pipeline engine\n * and the dictionary source (npm data package for the\n * Node bin, embedded gzip blob for the compiled binary).\n */\nexport type CliEngine = {\n api: AnonymizeApi;\n loadDictionaries: (scope: DictionaryScope) => Promise<Dictionaries>;\n};\n\n// Statically imported so the version is baked into both\n// the npm bundle and the compiled binary; a runtime\n// package.json lookup would fail inside the binary's\n// virtual filesystem.\nconst cliVersion = (): string => pkg.version;\n\n/**\n * Filesystem identity of a path: realpath when it exists\n * (so symlinks to the same file compare equal), lexical\n * resolution otherwise (the file may not exist yet).\n */\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst readStdin = async (): Promise<string> => {\n process.stdin.setEncoding(\"utf8\");\n let text = \"\";\n for await (const chunk of process.stdin) text += chunk;\n return text;\n};\n\ntype NamedInput = {\n /** Source path, or null when reading stdin. */\n path: string | null;\n text: string;\n};\n\nconst readInputs = async (files: string[]): Promise<NamedInput[]> => {\n if (files.length === 0) {\n if (process.stdin.isTTY) {\n throw new UsageError(\n \"no input files and stdin is a terminal (see --help)\",\n );\n }\n return [{ path: null, text: await readStdin() }];\n }\n return Promise.all(\n files.map(async (path) => ({ path, text: await readFile(path, \"utf8\") })),\n );\n};\n\ntype EntityLabel = (typeof DEFAULT_ENTITY_LABELS)[number];\n\n// Short aliases for the canonical multi-word labels so that\n// `--labels person,email,iban` works without quoting the space\n// in \"email address\". Separator-insensitive resolution (below)\n// additionally accepts hyphen/underscore forms such as\n// \"credit-card-number\".\nconst LABEL_ALIASES: Record<string, EntityLabel> = {\n email: \"email address\",\n phone: \"phone number\",\n org: \"organization\",\n organisation: \"organization\",\n dob: \"date of birth\",\n ssn: \"social security number\",\n \"tax id\": \"tax identification number\",\n passport: \"passport number\",\n \"credit card\": \"credit card number\",\n \"national id\": \"national identification number\",\n};\n\nconst LABEL_SEPARATOR_RE = /[\\s_-]+/g;\nconst ENTITY_LABEL_SET: ReadonlySet<string> = new Set(DEFAULT_ENTITY_LABELS);\n\nconst isEntityLabel = (label: string): label is EntityLabel =>\n ENTITY_LABEL_SET.has(label);\n\n/**\n * Resolve a user-supplied label token to a canonical label.\n * Lowercases and collapses separators, then maps known short\n * aliases. Unknown tokens are returned normalized so the\n * caller can report them verbatim.\n */\nconst canonicalizeLabel = (raw: string): string => {\n const normalized = raw.toLowerCase().replace(LABEL_SEPARATOR_RE, \" \").trim();\n const known: readonly string[] = DEFAULT_ENTITY_LABELS;\n if (known.includes(normalized)) {\n return normalized;\n }\n return LABEL_ALIASES[normalized] ?? normalized;\n};\n\nconst validateLabels = (labels: readonly string[]): EntityLabel[] => {\n const resolved = [...new Set(labels.map(canonicalizeLabel))];\n const valid: EntityLabel[] = [];\n const availableLabels = DEFAULT_ENTITY_LABELS.join(\", \");\n const availableAliases = Object.keys(LABEL_ALIASES).join(\", \");\n for (const label of resolved) {\n if (!isEntityLabel(label)) {\n throw new UsageError(\n [\n \"--labels: unknown label\",\n JSON.stringify(label) + \";\",\n \"available:\",\n availableLabels,\n \"(aliases:\",\n availableAliases + \")\",\n ].join(\" \"),\n );\n }\n valid.push(label);\n }\n return valid;\n};\n\nconst buildPipelineConfig = async (\n opts: CliOptions,\n loadDictionaries: CliEngine[\"loadDictionaries\"],\n): Promise<PipelineConfig> => {\n const dictionaries = await loadDictionaries({\n languages: opts.languages,\n countries: opts.countries,\n });\n return {\n threshold: opts.threshold,\n enableTriggerPhrases: true,\n enableRegex: true,\n enableLegalForms: true,\n enableNameCorpus: true,\n ...(opts.languages === undefined\n ? {}\n : { nameCorpusLanguages: [...opts.languages] }),\n enableDenyList: true,\n ...(opts.countries === undefined\n ? {}\n : { denyListCountries: [...opts.countries] }),\n enableGazetteer: false,\n enableCountries: true,\n enableNer: false,\n enableConfidenceBoost: true,\n enableCoreference: true,\n enableZoneClassification: true,\n enableHotwordRules: true,\n labels:\n opts.labels === undefined\n ? [...DEFAULT_ENTITY_LABELS]\n : validateLabels(opts.labels),\n workspaceId: \"cli\",\n dictionaries,\n };\n};\n\nconst buildOperatorConfig = (\n opts: CliOptions,\n entities: Entity[],\n): OperatorConfig => {\n const operators: Record<string, OperatorType> = {};\n if (opts.mode === \"redact\") {\n for (const entity of entities) operators[entity.label] = \"redact\";\n }\n return { operators, redactString: opts.redactString };\n};\n\nconst writeOutput = async (\n path: string | undefined,\n content: string,\n): Promise<void> => {\n if (path === undefined) {\n process.stdout.write(content);\n return;\n }\n await writeFile(path, content, \"utf8\");\n};\n\ntype RedactionKeyFile = {\n entries: Record<string, { original: string; operator: string }>;\n};\n\nconst parseRedactionKey = (raw: string): Map<string, string> => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new UsageError(\"redaction key is not valid JSON\");\n }\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new UsageError(\n 'redaction key must be an object with an \"entries\" field',\n );\n }\n const { entries } = parsed as RedactionKeyFile;\n if (\n typeof entries !== \"object\" ||\n entries === null ||\n Array.isArray(entries)\n ) {\n throw new UsageError('redaction key \"entries\" must be an object');\n }\n const map = new Map<string, string>();\n for (const [placeholder, entry] of Object.entries(entries)) {\n if (typeof entry?.original !== \"string\") {\n throw new UsageError(\n `redaction key entry \"${placeholder}\" has no original text`,\n );\n }\n map.set(placeholder, entry.original);\n }\n return map;\n};\n\n/**\n * Ask for a country scope when running interactively on\n * files with no scope flags. Skipped for piped stdin so\n * the CLI stays scriptable.\n */\nexport const shouldPromptForScope = (\n opts: CliOptions,\n tty: { stdinIsTTY: boolean; stderrIsTTY: boolean },\n): boolean =>\n opts.countries === undefined &&\n opts.languages === undefined &&\n !opts.quiet &&\n opts.files.length > 0 &&\n tty.stdinIsTTY &&\n tty.stderrIsTTY;\n\nconst promptForCountries = async (): Promise<string[] | undefined> => {\n const rl = createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n try {\n const answer = await rl.question(\n \"Country scope (ISO codes like CZ,DE,GB; Enter loads all): \",\n );\n const trimmed = answer.trim();\n return trimmed === \"\" ? undefined : parseCountries(trimmed);\n } finally {\n rl.close();\n }\n};\n\nconst runDeanonymise = async (\n opts: CliOptions,\n api: AnonymizeApi,\n): Promise<void> => {\n if (opts.keyPath !== undefined) {\n throw new UsageError(\"--key cannot be combined with --deanonymise\");\n }\n const keyPath = opts.deanonymiseKeyPath;\n if (keyPath === undefined) throw new UsageError(\"missing redaction key path\");\n const redactionMap = parseRedactionKey(await readFile(keyPath, \"utf8\"));\n\n const inputs = await readInputs(opts.files);\n if (inputs.length > 1) {\n throw new UsageError(\"--deanonymise accepts a single input\");\n }\n const input = inputs[0];\n if (!input) throw new UsageError(\"no input to deanonymise\");\n if (opts.output !== undefined) {\n guardWriteTargets(input.path === null ? [] : [input.path], [\n { path: opts.output, flag: \"--output\" },\n ]);\n }\n await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));\n};\n\nconst outputPathFor = (\n input: NamedInput,\n opts: CliOptions,\n multi: boolean,\n): string | undefined => {\n if (opts.output === undefined) return undefined;\n if (!multi) return opts.output;\n if (input.path === null)\n throw new UsageError(\"stdin cannot be combined with multiple files\");\n return join(opts.output, basename(input.path));\n};\n\n/**\n * Reject any write target (output or key file) whose\n * filesystem identity collides with an input file or with\n * another write target. Symlinks count as collisions.\n */\nconst guardWriteTargets = (\n inputPaths: readonly string[],\n writeTargets: readonly { path: string; flag: string }[],\n): void => {\n const inputs = new Set(inputPaths.map(canonicalPath));\n const seen = new Map<string, string>();\n for (const target of writeTargets) {\n const canonical = canonicalPath(target.path);\n if (inputs.has(canonical)) {\n throw new UsageError(\n `refusing to overwrite input file \"${target.path}\" (${target.flag})`,\n );\n }\n const clash = seen.get(canonical);\n if (clash !== undefined) {\n throw new UsageError(\n `${target.flag} \"${target.path}\" collides with ${clash}`,\n );\n }\n seen.set(canonical, `${target.flag} \"${target.path}\"`);\n }\n};\n\nconst summarize = (entities: Entity[]): string => {\n const counts = new Map<string, number>();\n for (const entity of entities) {\n counts.set(entity.label, (counts.get(entity.label) ?? 0) + 1);\n }\n const parts = [...counts.entries()]\n .toSorted((a, b) => b[1] - a[1])\n .map(([label, count]) => `${label}: ${count}`);\n return parts.length > 0 ? parts.join(\", \") : \"none\";\n};\n\nconst runAnonymise = async (\n opts: CliOptions,\n { api, loadDictionaries }: CliEngine,\n): Promise<void> => {\n const multi = opts.files.length > 1;\n if (multi && opts.output === undefined) {\n throw new UsageError(\"multiple input files require --output <directory>\");\n }\n if (multi && opts.keyPath !== undefined) {\n throw new UsageError(\"--key works with a single input only\");\n }\n if (multi && opts.json) {\n throw new UsageError(\"--json works with a single input only\");\n }\n if (opts.keyPath !== undefined && opts.mode !== \"replace\") {\n throw new UsageError('--key requires --mode \"replace\"');\n }\n\n const scoped = shouldPromptForScope(opts, {\n stdinIsTTY: process.stdin.isTTY === true,\n stderrIsTTY: process.stderr.isTTY === true,\n })\n ? { ...opts, countries: await promptForCountries() }\n : opts;\n\n const inputs = await readInputs(scoped.files);\n\n // Validate every write target before any work: output\n // collisions (same basename from different input dirs,\n // symlinks to an input, --key hitting the output) fail\n // fast instead of silently clobbering files mid-batch.\n const outputPaths = inputs.map((input) => outputPathFor(input, opts, multi));\n const writeTargets: { path: string; flag: string }[] = [];\n for (const path of outputPaths) {\n if (path !== undefined) writeTargets.push({ path, flag: \"--output\" });\n }\n if (opts.keyPath !== undefined) {\n writeTargets.push({ path: opts.keyPath, flag: \"--key\" });\n }\n guardWriteTargets(\n inputs.flatMap((input) => (input.path === null ? [] : [input.path])),\n writeTargets,\n );\n\n const config = await buildPipelineConfig(scoped, loadDictionaries);\n\n // One shared context for the whole batch: the first\n // runPipeline builds the search automaton after its own\n // init steps (hotword rules included) and caches it on\n // the context for the remaining files. Cross-document\n // reuse is supported — coref links travel on entities.\n const context = api.createPipelineContext();\n\n if (multi && opts.output !== undefined) {\n await mkdir(opts.output, { recursive: true });\n }\n\n for (const [index, input] of inputs.entries()) {\n const outputPath = outputPaths[index];\n\n const entities = await api.runPipeline({\n fullText: input.text,\n config,\n gazetteerEntries: [],\n context,\n });\n const result = api.redactText(\n input.text,\n entities,\n buildOperatorConfig(opts, entities),\n context,\n );\n\n if (opts.json) {\n // In redact mode the user chose irreversibility, so the\n // JSON must not carry any detected text. Whitelist the\n // non-sensitive metadata fields; this drops `text` and a\n // coref alias's `corefSourceText`. Offsets index the\n // caller's own input and are kept.\n const jsonEntities =\n opts.mode === \"redact\"\n ? entities.map(({ start, end, label, score, source }) => ({\n start,\n end,\n label,\n score,\n source,\n }))\n : entities;\n const payload = {\n entityCount: result.entityCount,\n entities: jsonEntities,\n redactedText: result.redactedText,\n };\n await writeOutput(outputPath, `${JSON.stringify(payload, null, 2)}\\n`);\n } else {\n await writeOutput(outputPath, result.redactedText);\n }\n\n if (opts.keyPath !== undefined) {\n await writeFile(\n opts.keyPath,\n api.exportRedactionKey(result.redactionMap, result.operatorMap),\n \"utf8\",\n );\n }\n\n if (!opts.quiet) {\n const source = input.path ?? \"stdin\";\n process.stderr.write(`anonymize: ${source}: ${summarize(entities)}\\n`);\n }\n }\n};\n\n/**\n * Render the canonical entity labels and the short aliases\n * accepted by --labels, for the --list-labels discovery flag.\n */\nconst formatLabelList = (): string => {\n const lines: string[] = [\"Detectable entity labels (pass to --labels):\"];\n for (const label of DEFAULT_ENTITY_LABELS) {\n lines.push(` ${label}`);\n }\n lines.push(\"\", \"Short aliases:\");\n const aliases = Object.entries(LABEL_ALIASES);\n let width = 0;\n for (const [alias] of aliases) {\n width = Math.max(width, alias.length);\n }\n for (const [alias, canonical] of aliases) {\n lines.push(` ${alias.padEnd(width)} -> ${canonical}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n};\n\nconst dispatch = async (engine: CliEngine): Promise<void> => {\n const opts = parseCliArgs(process.argv.slice(2));\n if (opts.help) {\n process.stdout.write(HELP);\n return;\n }\n if (opts.version) {\n process.stdout.write(`${cliVersion()}\\n`);\n return;\n }\n if (opts.listLabels) {\n process.stdout.write(formatLabelList());\n return;\n }\n if (opts.deanonymiseKeyPath !== undefined) {\n await runDeanonymise(opts, engine.api);\n return;\n }\n await runAnonymise(opts, engine);\n};\n\n/**\n * Run the CLI against the given engine and set the\n * process exit code (0 ok, 1 runtime error, 2 usage).\n */\nexport const runCli = async (engine: CliEngine): Promise<void> => {\n try {\n await dispatch(engine);\n } catch (err) {\n if (err instanceof UsageError) {\n process.stderr.write(`anonymize: ${err.message}\\n`);\n process.stderr.write(`Try \"anonymize --help\" for usage.\\n`);\n process.exitCode = 2;\n } else {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${message}\\n`);\n process.exitCode = 1;\n }\n }\n};\n","#!/usr/bin/env node\n/* npm-distributed entry point — backs the CLI with the\n * native engine (@stll/text-search napi bindings) and\n * the @stll/anonymize-data dictionary package. */\nimport * as anonymize from \"@stll/anonymize\";\n\nimport { loadCliDictionaries } from \"./dictionaries\";\nimport { runCli } from \"./main\";\n\nawait runCli({ api: anonymize, loadDictionaries: loadCliDictionaries });\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["pkg.version","known"],"sources":["../src/args.ts","../src/dictionary-scope.ts","../src/dictionaries.ts","../package.json","../src/main.ts","../src/cli.ts"],"sourcesContent":["import { availableParallelism } from \"node:os\";\nimport { parseArgs } from \"node:util\";\n\nexport const CLI_MODES = [\"replace\", \"redact\"] as const;\nexport type CliMode = (typeof CLI_MODES)[number];\n\nexport const DEFAULT_THRESHOLD = 0.3;\nexport const DEFAULT_REDACT_STRING = \"[REDACTED]\";\n\n/** Upper bound on the default worker count; batch I/O overlap\n * saturates well before this, and redaction itself is a\n * synchronous native call serialized on the JS thread. */\nexport const MAX_DEFAULT_WORKERS = 4;\n\n/** Default batch concurrency: min(4, cores). Workers overlap\n * file reads/writes; the shared native pipeline runs each\n * redaction to completion on the single JS thread. */\nexport const defaultWorkerCount = (): number =>\n Math.max(1, Math.min(MAX_DEFAULT_WORKERS, availableParallelism()));\n\n/** Invalid invocation; printed with usage hint, exit code 2. */\nexport class UsageError extends Error {}\n\nexport type CliOptions = {\n files: string[];\n output?: string | undefined;\n mode: CliMode;\n keyPath?: string | undefined;\n deanonymiseKeyPath?: string | undefined;\n revert?: string[] | undefined;\n recursive: boolean;\n workers: number;\n labels?: string[] | undefined;\n languages?: string[] | undefined;\n countries?: string[] | undefined;\n threshold: number;\n redactString: string;\n json: boolean;\n quiet: boolean;\n help: boolean;\n version: boolean;\n listLabels: boolean;\n};\n\nexport const HELP = `Usage: anonymize [options] [file|dir ...]\n\nDetect and anonymize PII in text. Reads the given files, or stdin\nwhen no files are given. A directory argument processes the text\nfiles inside it (add --recursive to descend into subdirectories).\nWrites to stdout, or to --output.\nAll processing is local; the CLI makes no network calls.\n\nOptions:\n -o, --output <path> Output file, or directory for batch\n input (multiple files or a directory)\n -m, --mode <mode> \"replace\" (reversible [PERSON_1]\n placeholders) or \"redact\"\n (default: replace)\n -k, --key <path> Write the redaction key as JSON\n (single input, replace mode)\n -d, --deanonymise <path> Restore redacted text using the\n redaction key at <path>\n --revert <term> With --deanonymise, restore only the\n given entity. Match a placeholder token\n (\"[PERSON_1]\") or an original value\n (\"Jan Novák\"), case-sensitive exact.\n Repeatable; others stay redacted\n -r, --recursive Descend into subdirectories when a\n directory is given as input\n --workers <n> Batch files to process concurrently\n (default: min(${MAX_DEFAULT_WORKERS}, CPU cores)). Overlaps\n file I/O; redaction is serialized on\n the JS thread\n --labels <list> Comma-separated entity labels to detect\n (default: all). Accepts canonical labels\n (\"email address\"), short aliases (email,\n phone, org, dob, ssn), and hyphen/underscore\n forms (\"credit-card-number\")\n --languages <list> Name-corpus languages, e.g. \"cs,de,en\"\n (default: all bundled)\n --countries <list> ISO 3166-1 alpha-2 codes scoping deny\n lists and city data, e.g. \"CZ,DE,GB\"\n (default: all deny lists; city data\n for a 30-country default set)\n --threshold <n> Minimum confidence score, 0-1\n (default: ${DEFAULT_THRESHOLD})\n --redact-string <s> Replacement text in redact mode\n (default: \"${DEFAULT_REDACT_STRING}\")\n --json Emit JSON (entities + redacted text) to\n stdout (single input only)\n --quiet Suppress the summary on stderr\n -h, --help Show this help\n -v, --version Show the version\n --list-labels List detectable entity labels and the\n short aliases accepted by --labels\n\nBatch input (directory or multiple files):\n Requires --output <directory>. The input tree is mirrored\n into the output directory. Directory walks process regular\n files only and skip likely-binary files (a NUL byte in the\n first 8 KiB); explicitly named files are always processed.\n The stderr summary reports how many files were processed,\n failed, and skipped; any failure sets exit code 1.\n --key and --json apply to single inputs only.\n\nInteractive prompt:\n When run on files from a terminal without --countries or\n --languages, the CLI asks once which country scope to load.\n Piped stdin/stderr or --quiet skips the prompt, so scripts\n and agents never block on input.\n\nExit codes:\n 0 success\n 1 runtime error (message on stderr)\n 2 usage error (message on stderr)\n\nJSON output (--json):\n { \"entityCount\": number,\n \"entities\": [{ \"start\": number, \"end\": number,\n \"label\": string, \"text\": string,\n \"score\": number, \"source\": string }],\n \"redactedText\": string }\n Offsets are UTF-16 code-unit indexes into the input.\n The stderr summary contains entity counts only, never\n the detected text.\n\nExamples:\n anonymize contract.txt > contract.anon.txt\n anonymize -k contract.key.json -o contract.anon.txt contract.txt\n anonymize -d contract.key.json contract.anon.txt\n anonymize -r --workers 8 -o out/ docs/\n anonymize -d key.json --revert \"[PERSON_1]\" contract.anon.txt\n cat notes.md | anonymize --countries CZ,SK --languages cs,sk\n anonymize --json --quiet input.txt | jq '.entities[].label'\n`;\n\nconst splitList = (value: string): string[] => [\n ...new Set(\n value\n .split(\",\")\n .map((part) => part.trim())\n .filter((part) => part.length > 0),\n ),\n];\n\nconst parseThreshold = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isFinite(value) || value < 0 || value > 1) {\n throw new UsageError(\n `--threshold must be a number between 0 and 1, got \"${raw}\"`,\n );\n }\n return value;\n};\n\nconst parseWorkers = (raw: string): number => {\n const value = Number(raw);\n if (!Number.isInteger(value) || value < 1) {\n throw new UsageError(`--workers must be a positive integer, got \"${raw}\"`);\n }\n return value;\n};\n\nconst parseMode = (raw: string): CliMode => {\n const mode = CLI_MODES.find((candidate) => candidate === raw);\n if (!mode) {\n throw new UsageError(\n `--mode must be one of: ${CLI_MODES.join(\", \")}; got \"${raw}\"`,\n );\n }\n return mode;\n};\n\nconst COUNTRY_CODE_RE = /^[A-Za-z]{2}$/;\n\nexport const parseCountries = (raw: string): string[] => {\n const countries = [\n ...new Set(splitList(raw).map((code) => code.toUpperCase())),\n ];\n const invalid = countries.find((code) => !COUNTRY_CODE_RE.test(code));\n if (invalid) {\n throw new UsageError(\n `--countries expects ISO 3166-1 alpha-2 codes (e.g. \"CZ,DE\"), got \"${invalid}\"`,\n );\n }\n return countries;\n};\n\nexport const parseCliArgs = (argv: string[]): CliOptions => {\n let parsed: ReturnType<typeof parseArgs<typeof PARSE_CONFIG>>;\n try {\n parsed = parseArgs({ ...PARSE_CONFIG, args: argv });\n } catch (err) {\n throw new UsageError(err instanceof Error ? err.message : String(err));\n }\n const { values, positionals } = parsed;\n\n return {\n files: positionals,\n output: values.output,\n mode: values.mode === undefined ? \"replace\" : parseMode(values.mode),\n keyPath: values.key,\n deanonymiseKeyPath: values.deanonymise,\n revert:\n values.revert === undefined || values.revert.length === 0\n ? undefined\n : values.revert,\n recursive: values.recursive === true,\n workers:\n values.workers === undefined\n ? defaultWorkerCount()\n : parseWorkers(values.workers),\n labels: values.labels === undefined ? undefined : splitList(values.labels),\n languages:\n values.languages === undefined ? undefined : splitList(values.languages),\n countries:\n values.countries === undefined\n ? undefined\n : parseCountries(values.countries),\n threshold:\n values.threshold === undefined\n ? DEFAULT_THRESHOLD\n : parseThreshold(values.threshold),\n redactString: values[\"redact-string\"] ?? DEFAULT_REDACT_STRING,\n json: values.json === true,\n quiet: values.quiet === true,\n help: values.help === true,\n version: values.version === true,\n listLabels: values[\"list-labels\"] === true,\n };\n};\n\nconst PARSE_CONFIG = {\n allowPositionals: true,\n strict: true,\n options: {\n output: { type: \"string\", short: \"o\" },\n mode: { type: \"string\", short: \"m\" },\n key: { type: \"string\", short: \"k\" },\n deanonymise: { type: \"string\", short: \"d\" },\n revert: { type: \"string\", multiple: true },\n recursive: { type: \"boolean\", short: \"r\" },\n workers: { type: \"string\" },\n labels: { type: \"string\" },\n languages: { type: \"string\" },\n countries: { type: \"string\" },\n threshold: { type: \"string\" },\n \"redact-string\": { type: \"string\" },\n json: { type: \"boolean\" },\n quiet: { type: \"boolean\" },\n help: { type: \"boolean\", short: \"h\" },\n version: { type: \"boolean\", short: \"v\" },\n \"list-labels\": { type: \"boolean\" },\n },\n} as const;\n","/* Pure helpers shared by the npm and embedded dictionary\n * loaders. Must stay free of @stll/anonymize-data imports\n * so the compiled binary's bundle excludes the raw JSON\n * dictionary modules. */\nimport type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\n\nimport { UsageError } from \"./args\";\n\nexport const NAME_DICTIONARY_PREFIXES = [\n \"names/first/\",\n \"names/surnames/\",\n] as const;\n\n/** Language code of a name dictionary id, or null. */\nexport const nameLanguageOfDictionary = (id: string): string | null => {\n const prefix = NAME_DICTIONARY_PREFIXES.find((p) => id.startsWith(p));\n return prefix ? id.slice(prefix.length) : null;\n};\n\nexport type DictionaryScope = {\n languages?: readonly string[] | undefined;\n countries?: readonly string[] | undefined;\n};\n\nconst pickKeys = <T>(\n record: Record<string, T>,\n keep: (key: string) => boolean,\n): Record<string, T> => {\n const result: Record<string, T> = {};\n for (const [key, value] of Object.entries(record)) {\n if (keep(key)) result[key] = value;\n }\n return result;\n};\n\n/** Dictionaries with every section present (possibly empty). */\nexport type ScopedDictionaries = {\n firstNames: Record<string, readonly string[]>;\n surnames: Record<string, readonly string[]>;\n denyList: Record<string, readonly string[]>;\n denyListMeta: Record<string, DictionaryMeta>;\n citiesByCountry: Record<string, readonly string[]>;\n};\n\n/**\n * Scope a fully loaded dictionary set to the requested\n * languages and countries. Mirrors the pre-load scoping\n * the npm loader does in dictionaries.ts; used by the\n * embedded loader, which always starts from the full set.\n */\nexport const filterDictionaries = (\n all: Dictionaries,\n { languages, countries }: DictionaryScope,\n): ScopedDictionaries => {\n const firstNames = all.firstNames ?? {};\n const surnames = all.surnames ?? {};\n const allDenyList = all.denyList ?? {};\n const allDenyListMeta = all.denyListMeta ?? {};\n\n if (languages !== undefined) {\n const available = Object.keys(firstNames);\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n }\n const keepLanguage = (lang: string): boolean =>\n languages === undefined || languages.includes(lang);\n const keepCountry = (country: string | null): boolean =>\n countries === undefined || country === null || countries.includes(country);\n\n const denyListMeta: Record<string, DictionaryMeta> = {};\n const denyList: Record<string, readonly string[]> = {};\n for (const [id, meta] of Object.entries(allDenyListMeta)) {\n if (!keepCountry(meta.country)) continue;\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && !keepLanguage(nameLang)) continue;\n const entries = allDenyList[id];\n if (entries === undefined) continue;\n denyListMeta[id] = meta;\n denyList[id] = entries;\n }\n\n return {\n firstNames: pickKeys(firstNames, keepLanguage),\n surnames: pickKeys(surnames, keepLanguage),\n denyList,\n denyListMeta,\n citiesByCountry: pickKeys(all.citiesByCountry ?? {}, (country) =>\n keepCountry(country),\n ),\n };\n};\n","import type { Dictionaries, DictionaryMeta } from \"@stll/anonymize\";\nimport {\n ALL_DICTIONARY_IDS,\n DICTIONARY_META,\n loadCityDictionary,\n loadDictionary,\n loadNameDictionaries,\n type NameLanguage,\n} from \"@stll/anonymize-data\";\n\nimport { UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\nimport {\n NAME_DICTIONARY_PREFIXES,\n nameLanguageOfDictionary,\n} from \"./dictionary-scope\";\n\n/**\n * Countries with bundled city dictionaries that are\n * loaded when no --countries scope is given.\n */\nconst DEFAULT_CITY_COUNTRIES = [\n \"AT\",\n \"AU\",\n \"BE\",\n \"BG\",\n \"BR\",\n \"CA\",\n \"CH\",\n \"CZ\",\n \"DE\",\n \"DK\",\n \"ES\",\n \"FI\",\n \"FR\",\n \"GB\",\n \"GR\",\n \"HR\",\n \"HU\",\n \"IE\",\n \"IT\",\n \"LU\",\n \"NL\",\n \"NO\",\n \"NZ\",\n \"PL\",\n \"PT\",\n \"RO\",\n \"SE\",\n \"SI\",\n \"SK\",\n \"US\",\n] as const;\n\nconst availableNameLanguages = (): readonly string[] =>\n ALL_DICTIONARY_IDS.filter((id) =>\n id.startsWith(NAME_DICTIONARY_PREFIXES[0]),\n ).map((id) => id.slice(NAME_DICTIONARY_PREFIXES[0].length));\n\nconst validateLanguages = (\n languages: readonly string[],\n): readonly NameLanguage[] => {\n const available = availableNameLanguages();\n const invalid = languages.find((lang) => !available.includes(lang));\n if (invalid) {\n throw new UsageError(\n `--languages: no name dictionary for \"${invalid}\"; available: ${available.join(\", \")}`,\n );\n }\n // SAFETY: every entry was checked against the bundled\n // name dictionary ids, which define NameLanguage.\n return languages as readonly NameLanguage[];\n};\n\nexport type LoadCliDictionariesOptions = DictionaryScope;\n\n/**\n * Load the bundled @stll/anonymize-data dictionaries,\n * scoped to the requested languages and countries.\n */\nexport const loadCliDictionaries = async ({\n languages,\n countries,\n}: LoadCliDictionariesOptions): Promise<Dictionaries> => {\n const nameLanguages =\n languages === undefined ? undefined : validateLanguages(languages);\n\n const denyIds = ALL_DICTIONARY_IDS.filter((id) => {\n const meta = DICTIONARY_META[id];\n if (\n countries &&\n meta.country !== null &&\n !countries.includes(meta.country)\n ) {\n return false;\n }\n const nameLang = nameLanguageOfDictionary(id);\n if (nameLang !== null && nameLanguages !== undefined) {\n return nameLanguages.includes(\n // SAFETY: nameLang comes from a bundled dictionary\n // id, which defines NameLanguage.\n nameLang as NameLanguage,\n );\n }\n return true;\n });\n\n const cityCountries = countries ?? DEFAULT_CITY_COUNTRIES;\n\n const [names, denyEntries, cityEntries] = await Promise.all([\n loadNameDictionaries(nameLanguages),\n Promise.all(\n denyIds.map(async (id) => ({ id, entries: await loadDictionary(id) })),\n ),\n Promise.all(\n cityCountries.map(async (country) => ({\n country,\n entries: await loadCityDictionary(country),\n })),\n ),\n ]);\n\n const denyList: Record<string, readonly string[]> = {};\n const denyListMeta: Record<string, DictionaryMeta> = {};\n for (const { id, entries } of denyEntries) {\n denyList[id] = entries;\n // SAFETY: anonymize-data categories match\n // DenyListCategory at runtime.\n denyListMeta[id] = DICTIONARY_META[id] as DictionaryMeta;\n }\n\n const citiesByCountry: Record<string, readonly string[]> = {};\n for (const { country, entries } of cityEntries) {\n if (entries.length > 0) citiesByCountry[country] = entries;\n }\n\n return {\n firstNames: names.firstNames,\n surnames: names.surnames,\n denyList,\n denyListMeta,\n citiesByCountry,\n };\n};\n","","import { realpathSync } from \"node:fs\";\nimport {\n mkdir,\n open,\n readdir,\n readFile,\n stat,\n writeFile,\n} from \"node:fs/promises\";\nimport { basename, dirname, join, relative, resolve } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\n\nimport type {\n deanonymise,\n Dictionaries,\n exportRedactionKey,\n NativeAnonymizeBinding,\n NativePipelineBuildOptions,\n OperatorType,\n PipelineConfig,\n} from \"@stll/anonymize\";\nimport { DEFAULT_ENTITY_LABELS } from \"@stll/anonymize/constants\";\n\nimport pkg from \"../package.json\" with { type: \"json\" };\n\nimport type { CliOptions } from \"./args\";\nimport { HELP, parseCliArgs, parseCountries, UsageError } from \"./args\";\nimport type { DictionaryScope } from \"./dictionary-scope\";\n\n/**\n * The pipeline functions the CLI needs, backed by the\n * @stll/anonymize native SDK: a binding loader and the\n * config-to-pipeline builder, plus the redaction-key\n * helpers used by the deanonymise path.\n */\nexport type AnonymizeApi = {\n deanonymise: typeof deanonymise;\n exportRedactionKey: typeof exportRedactionKey;\n createNativePipelineFromConfig: (\n options: NativePipelineBuildOptions,\n ) => Promise<NativeCliPipeline>;\n loadNativeAnonymizeBinding: () => NativeAnonymizeBinding;\n};\n\n/**\n * Everything an entry point injects: the pipeline engine\n * and the dictionary source (the @stll/anonymize-data\n * package for the npm bin).\n */\nexport type CliEngine = {\n api: AnonymizeApi;\n loadDictionaries: (scope: DictionaryScope) => Promise<Dictionaries>;\n};\n\n// Statically imported so the version is baked into both\n// the npm bundle and the compiled binary; a runtime\n// package.json lookup would fail inside the binary's\n// virtual filesystem.\nconst cliVersion = (): string => pkg.version;\n\n/**\n * Filesystem identity of a path: realpath when it exists\n * (so symlinks to the same file compare equal), lexical\n * resolution otherwise (the file may not exist yet).\n */\nconst canonicalPath = (path: string): string => {\n try {\n return realpathSync(path);\n } catch {\n return resolve(path);\n }\n};\n\nconst readStdin = async (): Promise<string> => {\n process.stdin.setEncoding(\"utf8\");\n let text = \"\";\n for await (const chunk of process.stdin) text += chunk;\n return text;\n};\n\ntype NamedInput = {\n /** Source path, or null when reading stdin. */\n path: string | null;\n text: string;\n};\n\nconst readInputs = async (files: string[]): Promise<NamedInput[]> => {\n if (files.length === 0) {\n if (process.stdin.isTTY) {\n throw new UsageError(\n \"no input files and stdin is a terminal (see --help)\",\n );\n }\n return [{ path: null, text: await readStdin() }];\n }\n return Promise.all(\n files.map(async (path) => ({ path, text: await readFile(path, \"utf8\") })),\n );\n};\n\n/**\n * One file to anonymize in a batch run: the source path to\n * read and the path, relative to the output directory, to\n * write. For a plain file argument the relative path is the\n * basename; for a directory argument the input tree is\n * mirrored, so it is the path relative to that directory.\n */\ntype FileJob = {\n path: string;\n outputRelative: string;\n};\n\n/** Result of expanding the positional arguments into concrete\n * files. `batch` is true when the output must be a directory:\n * more than one file, or any directory argument. */\ntype ExpandedInputs = {\n jobs: FileJob[];\n batch: boolean;\n /** Likely-binary files skipped during directory walks. */\n skipped: number;\n};\n\n// Sniff window for the binary check. A regular text file never\n// contains a NUL byte; binaries (images, archives) reliably do.\nconst TEXT_SNIFF_BYTES = 8192;\n\n/**\n * True when the file's first {@link TEXT_SNIFF_BYTES} bytes\n * contain no NUL byte. Used to skip binaries discovered by a\n * directory walk without reading the whole file.\n */\nconst looksTextual = async (path: string): Promise<boolean> => {\n const handle = await open(path, \"r\");\n try {\n const buffer = Buffer.alloc(TEXT_SNIFF_BYTES);\n const { bytesRead } = await handle.read(buffer, 0, TEXT_SNIFF_BYTES, 0);\n return buffer.subarray(0, bytesRead).indexOf(0) === -1;\n } finally {\n await handle.close();\n }\n};\n\n/**\n * Collect regular files under `root`, sorted for deterministic\n * order. Symlinks are skipped (avoids cycles and escaping the\n * tree); subdirectories are descended only when `recursive`.\n */\nconst walkDirectory = async (\n root: string,\n recursive: boolean,\n excludeDir?: string,\n): Promise<string[]> => {\n const found: string[] = [];\n const visit = async (dir: string): Promise<void> => {\n const entries = (await readdir(dir, { withFileTypes: true })).toSorted(\n (a, b) => a.name.localeCompare(b.name),\n );\n for (const entry of entries) {\n const full = join(dir, entry.name);\n if (entry.isDirectory()) {\n // Never descend into the output tree: rerunning with --output inside\n // the input directory must not ingest previously generated files.\n if (excludeDir !== undefined && resolve(full) === excludeDir) continue;\n if (recursive) await visit(full);\n } else if (entry.isFile()) {\n found.push(full);\n }\n }\n };\n await visit(root);\n return found;\n};\n\n/**\n * Expand positional arguments into concrete file jobs. A file\n * argument becomes one job (always processed); a directory is\n * walked, mirroring its tree into the output and skipping\n * likely-binary files.\n */\nconst expandInputs = async (\n files: readonly string[],\n recursive: boolean,\n outputDir?: string,\n): Promise<ExpandedInputs> => {\n const excludeDir = outputDir === undefined ? undefined : resolve(outputDir);\n const jobs: FileJob[] = [];\n let hasDirectory = false;\n let skipped = 0;\n for (const path of files) {\n // A stat failure (missing path, permission error) is not\n // fatal here: treat it as a file job so the read failure is\n // reported per file. A single such job stays single-input\n // and surfaces the error as a runtime exit; in a batch it is\n // counted as a failed file.\n let stats: Awaited<ReturnType<typeof stat>> | undefined;\n try {\n stats = await stat(path);\n } catch {\n jobs.push({ path, outputRelative: basename(path) });\n continue;\n }\n if (!stats.isDirectory()) {\n jobs.push({ path, outputRelative: basename(path) });\n continue;\n }\n hasDirectory = true;\n for (const file of await walkDirectory(path, recursive, excludeDir)) {\n // A file that disappears or turns unreadable mid-walk is queued anyway:\n // the per-file worker try/catch counts it as failed without aborting\n // the batch. Only a successful sniff that says \"binary\" skips it.\n const textual = await looksTextual(file).catch(() => true);\n if (!textual) {\n skipped += 1;\n continue;\n }\n jobs.push({ path: file, outputRelative: relative(path, file) });\n }\n }\n return { jobs, batch: hasDirectory || jobs.length > 1, skipped };\n};\n\n/**\n * Run `task` over `items` with at most `workers` in flight.\n * The shared native pipeline makes each redaction a synchronous\n * native call, so concurrency here only overlaps async file\n * I/O; the increments below are safe without locking because\n * no `await` sits between the read and the write of `next`.\n */\nconst runPool = async <T>(\n items: readonly T[],\n workers: number,\n task: (item: T) => Promise<void>,\n): Promise<void> => {\n let next = 0;\n const worker = async (): Promise<void> => {\n while (next < items.length) {\n const index = next;\n next += 1;\n // SAFETY: index < items.length checked above.\n await task(items[index] as T);\n }\n };\n const count = Math.max(1, Math.min(workers, items.length));\n await Promise.all(Array.from({ length: count }, worker));\n};\n\ntype EntityLabel = (typeof DEFAULT_ENTITY_LABELS)[number];\n\ntype CliEntity = {\n start: number;\n end: number;\n label: string;\n text: string;\n score: number;\n source: string;\n};\n\ntype CliOperatorConfig = {\n operators: Record<string, OperatorType>;\n redactString: string;\n};\n\ntype CliRedactionResult = {\n redactedText: string;\n redactionMap: Map<string, string>;\n operatorMap: Map<string, OperatorType>;\n entityCount: number;\n};\n\ntype NativeCliPipeline = {\n warmLazyRegex?: () => void;\n redactText: (\n fullText: string,\n operators?: CliOperatorConfig,\n ) => {\n resolvedEntities: CliEntity[];\n redaction: CliRedactionResult;\n };\n};\n\n// Short aliases for the canonical multi-word labels so that\n// `--labels person,email,iban` works without quoting the space\n// in \"email address\". Separator-insensitive resolution (below)\n// additionally accepts hyphen/underscore forms such as\n// \"credit-card-number\".\nconst LABEL_ALIASES: Record<string, EntityLabel> = {\n email: \"email address\",\n phone: \"phone number\",\n org: \"organization\",\n organisation: \"organization\",\n dob: \"date of birth\",\n ssn: \"social security number\",\n \"tax id\": \"tax identification number\",\n passport: \"passport number\",\n \"credit card\": \"credit card number\",\n \"national id\": \"national identification number\",\n};\n\nconst LABEL_SEPARATOR_RE = /[\\s_-]+/g;\nconst ENTITY_LABEL_SET: ReadonlySet<string> = new Set(DEFAULT_ENTITY_LABELS);\n\nconst isEntityLabel = (label: string): label is EntityLabel =>\n ENTITY_LABEL_SET.has(label);\n\n/**\n * Resolve a user-supplied label token to a canonical label.\n * Lowercases and collapses separators, then maps known short\n * aliases. Unknown tokens are returned normalized so the\n * caller can report them verbatim.\n */\nconst canonicalizeLabel = (raw: string): string => {\n const normalized = raw.toLowerCase().replace(LABEL_SEPARATOR_RE, \" \").trim();\n const known: readonly string[] = DEFAULT_ENTITY_LABELS;\n if (known.includes(normalized)) {\n return normalized;\n }\n return LABEL_ALIASES[normalized] ?? normalized;\n};\n\nconst validateLabels = (labels: readonly string[]): EntityLabel[] => {\n const resolved = [...new Set(labels.map(canonicalizeLabel))];\n const valid: EntityLabel[] = [];\n const availableLabels = DEFAULT_ENTITY_LABELS.join(\", \");\n const availableAliases = Object.keys(LABEL_ALIASES).join(\", \");\n for (const label of resolved) {\n if (!isEntityLabel(label)) {\n throw new UsageError(\n [\n \"--labels: unknown label\",\n JSON.stringify(label) + \";\",\n \"available:\",\n availableLabels,\n \"(aliases:\",\n availableAliases + \")\",\n ].join(\" \"),\n );\n }\n valid.push(label);\n }\n return valid;\n};\n\nconst buildPipelineConfig = async (\n opts: CliOptions,\n loadDictionaries: CliEngine[\"loadDictionaries\"],\n): Promise<PipelineConfig> => {\n const dictionaries = await loadDictionaries({\n languages: opts.languages,\n countries: opts.countries,\n });\n return {\n threshold: opts.threshold,\n enableTriggerPhrases: true,\n enableRegex: true,\n enableLegalForms: true,\n enableNameCorpus: true,\n ...(opts.languages === undefined\n ? {}\n : { nameCorpusLanguages: [...opts.languages] }),\n enableDenyList: true,\n ...(opts.countries === undefined\n ? {}\n : { denyListCountries: [...opts.countries] }),\n enableGazetteer: false,\n enableCountries: true,\n enableNer: false,\n enableConfidenceBoost: true,\n enableCoreference: true,\n enableZoneClassification: true,\n enableHotwordRules: true,\n labels:\n opts.labels === undefined\n ? [...DEFAULT_ENTITY_LABELS]\n : validateLabels(opts.labels),\n workspaceId: \"cli\",\n dictionaries,\n };\n};\n\nconst buildOperatorConfig = (opts: CliOptions): CliOperatorConfig => {\n const operators: Record<string, OperatorType> = {};\n if (opts.mode === \"redact\") {\n const labels =\n opts.labels === undefined\n ? DEFAULT_ENTITY_LABELS\n : validateLabels(opts.labels);\n for (const label of labels) operators[label] = \"redact\";\n }\n return { operators, redactString: opts.redactString };\n};\n\nconst writeOutput = async (\n path: string | undefined,\n content: string,\n): Promise<void> => {\n if (path === undefined) {\n process.stdout.write(content);\n return;\n }\n await writeFile(path, content, \"utf8\");\n};\n\ntype RedactionKeyFile = {\n entries: Record<string, { original: string; operator: string }>;\n};\n\nconst parseRedactionKey = (raw: string): Map<string, string> => {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new UsageError(\"redaction key is not valid JSON\");\n }\n if (typeof parsed !== \"object\" || parsed === null || !(\"entries\" in parsed)) {\n throw new UsageError(\n 'redaction key must be an object with an \"entries\" field',\n );\n }\n const { entries } = parsed as RedactionKeyFile;\n if (\n typeof entries !== \"object\" ||\n entries === null ||\n Array.isArray(entries)\n ) {\n throw new UsageError('redaction key \"entries\" must be an object');\n }\n const map = new Map<string, string>();\n for (const [placeholder, entry] of Object.entries(entries)) {\n if (typeof entry?.original !== \"string\") {\n throw new UsageError(\n `redaction key entry \"${placeholder}\" has no original text`,\n );\n }\n map.set(placeholder, entry.original);\n }\n return map;\n};\n\n/**\n * Restrict a redaction key to the entities named by --revert.\n * Each token matches a placeholder (\"[PERSON_1]\") or an original\n * value (\"Jan Novák\"), case-sensitive and exact. A token that\n * matches nothing is a usage error listing the placeholders the\n * key does define, so the caller can correct the spelling.\n */\nconst selectRevertEntries = (\n redactionMap: ReadonlyMap<string, string>,\n tokens: readonly string[],\n): Map<string, string> => {\n const selected = new Map<string, string>();\n for (const token of tokens) {\n let matched = false;\n for (const [placeholder, original] of redactionMap) {\n if (placeholder === token || original === token) {\n selected.set(placeholder, original);\n matched = true;\n }\n }\n if (!matched) {\n const MAX_LISTED_PLACEHOLDERS = 20;\n const placeholders = [...redactionMap.keys()];\n const listed = placeholders.slice(0, MAX_LISTED_PLACEHOLDERS).join(\", \");\n const rest = placeholders.length - MAX_LISTED_PLACEHOLDERS;\n const suffix = rest > 0 ? ` and ${rest} more` : \"\";\n throw new UsageError(\n `--revert ${JSON.stringify(token)} matched no placeholder or ` +\n `original; available placeholders: ${listed}${suffix}`,\n );\n }\n }\n return selected;\n};\n\n/**\n * Ask for a country scope when running interactively on\n * files with no scope flags. Skipped for piped stdin so\n * the CLI stays scriptable.\n */\nexport const shouldPromptForScope = (\n opts: CliOptions,\n tty: { stdinIsTTY: boolean; stderrIsTTY: boolean },\n): boolean =>\n opts.countries === undefined &&\n opts.languages === undefined &&\n !opts.quiet &&\n opts.files.length > 0 &&\n tty.stdinIsTTY &&\n tty.stderrIsTTY;\n\nconst promptForCountries = async (): Promise<string[] | undefined> => {\n const rl = createInterface({\n input: process.stdin,\n output: process.stderr,\n });\n try {\n const answer = await rl.question(\n \"Country scope (ISO codes like CZ,DE,GB; Enter loads all): \",\n );\n const trimmed = answer.trim();\n return trimmed === \"\" ? undefined : parseCountries(trimmed);\n } finally {\n rl.close();\n }\n};\n\nconst runDeanonymise = async (\n opts: CliOptions,\n api: AnonymizeApi,\n): Promise<void> => {\n if (opts.keyPath !== undefined) {\n throw new UsageError(\"--key cannot be combined with --deanonymise\");\n }\n const keyPath = opts.deanonymiseKeyPath;\n if (keyPath === undefined) throw new UsageError(\"missing redaction key path\");\n const fullMap = parseRedactionKey(await readFile(keyPath, \"utf8\"));\n\n // --revert restores a chosen subset; leaving the rest of the\n // key out means deanonymise skips those placeholders, so the\n // other entities stay redacted.\n const redactionMap =\n opts.revert === undefined\n ? fullMap\n : selectRevertEntries(fullMap, opts.revert);\n\n const inputs = await readInputs(opts.files);\n if (inputs.length > 1) {\n throw new UsageError(\"--deanonymise accepts a single input\");\n }\n const input = inputs[0];\n if (!input) throw new UsageError(\"no input to deanonymise\");\n if (opts.output !== undefined) {\n guardWriteTargets(input.path === null ? [] : [input.path], [\n { path: opts.output, flag: \"--output\" },\n ]);\n }\n await writeOutput(opts.output, api.deanonymise(input.text, redactionMap));\n};\n\n/**\n * Reject any write target (output or key file) whose\n * filesystem identity collides with an input file or with\n * another write target. Symlinks count as collisions.\n */\nconst guardWriteTargets = (\n inputPaths: readonly string[],\n writeTargets: readonly { path: string; flag: string }[],\n): void => {\n const inputs = new Set(inputPaths.map(canonicalPath));\n const seen = new Map<string, string>();\n for (const target of writeTargets) {\n const canonical = canonicalPath(target.path);\n if (inputs.has(canonical)) {\n throw new UsageError(\n `refusing to overwrite input file \"${target.path}\" (${target.flag})`,\n );\n }\n const clash = seen.get(canonical);\n if (clash !== undefined) {\n throw new UsageError(\n `${target.flag} \"${target.path}\" collides with ${clash}`,\n );\n }\n seen.set(canonical, `${target.flag} \"${target.path}\"`);\n }\n};\n\nconst summarize = (entities: readonly CliEntity[]): string => {\n const counts = new Map<string, number>();\n for (const entity of entities) {\n counts.set(entity.label, (counts.get(entity.label) ?? 0) + 1);\n }\n const parts = [...counts.entries()]\n .toSorted((a, b) => b[1] - a[1])\n .map(([label, count]) => `${label}: ${count}`);\n return parts.length > 0 ? parts.join(\", \") : \"none\";\n};\n\n/**\n * A single unit of anonymize work: the text to process, where\n * to write it (undefined means stdout), and a label for the\n * stderr summary. Used by the stdin and single-file flows,\n * which additionally support --json and --key.\n */\ntype SingleInput = {\n text: string;\n outputPath: string | undefined;\n source: string;\n};\n\nconst runAnonymiseSingle = async (\n opts: CliOptions,\n runtime: CliRuntime,\n api: AnonymizeApi,\n input: SingleInput,\n): Promise<void> => {\n const { entities, redaction } = await runtime.redact(\n input.text,\n buildOperatorConfig(opts),\n );\n\n if (opts.json) {\n // In redact mode the user chose irreversibility, so the\n // JSON must not carry any detected text. Whitelist the\n // non-sensitive metadata fields; this drops `text` and a\n // coref alias's `corefSourceText`. Offsets index the\n // caller's own input and are kept.\n const jsonEntities =\n opts.mode === \"redact\"\n ? entities.map(({ start, end, label, score, source }) => ({\n start,\n end,\n label,\n score,\n source,\n }))\n : entities;\n const payload = {\n entityCount: redaction.entityCount,\n entities: jsonEntities,\n redactedText: redaction.redactedText,\n };\n await writeOutput(\n input.outputPath,\n `${JSON.stringify(payload, null, 2)}\\n`,\n );\n } else {\n await writeOutput(input.outputPath, redaction.redactedText);\n }\n\n if (opts.keyPath !== undefined) {\n await writeFile(\n opts.keyPath,\n api.exportRedactionKey(redaction.redactionMap, redaction.operatorMap),\n \"utf8\",\n );\n }\n\n if (!opts.quiet) {\n process.stderr.write(\n `anonymize: ${input.source}: ${summarize(entities)}\\n`,\n );\n }\n};\n\n/** Tally of a batch run for the closing summary line. */\ntype BatchOutcome = { processed: number; failed: number };\n\nconst runAnonymiseBatch = async (\n opts: CliOptions,\n runtime: CliRuntime,\n output: string,\n jobs: readonly FileJob[],\n skipped: number,\n): Promise<void> => {\n await mkdir(output, { recursive: true });\n const operatorConfig = buildOperatorConfig(opts);\n const outcome: BatchOutcome = { processed: 0, failed: 0 };\n\n await runPool(jobs, opts.workers, async (job) => {\n const outputPath = join(output, job.outputRelative);\n try {\n const text = await readFile(job.path, \"utf8\");\n const { entities, redaction } = await runtime.redact(\n text,\n operatorConfig,\n );\n await mkdir(dirname(outputPath), { recursive: true });\n await writeFile(outputPath, redaction.redactedText, \"utf8\");\n // No await between here and the increment: safe on the\n // single JS thread despite concurrent workers.\n outcome.processed += 1;\n if (!opts.quiet) {\n process.stderr.write(\n `anonymize: ${job.path}: ${summarize(entities)}\\n`,\n );\n }\n } catch (err) {\n outcome.failed += 1;\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${job.path}: error: ${message}\\n`);\n }\n });\n\n if (!opts.quiet) {\n const parts = [\n `${outcome.processed} processed`,\n `${outcome.failed} failed`,\n ];\n if (skipped > 0) parts.push(`${skipped} skipped`);\n process.stderr.write(`anonymize: ${parts.join(\", \")}\\n`);\n }\n // Any per-file failure is a nonzero exit, but the whole batch\n // still runs so one bad file does not hide the rest.\n if (outcome.failed > 0) process.exitCode = 1;\n};\n\nconst runAnonymise = async (\n opts: CliOptions,\n { api, loadDictionaries }: CliEngine,\n): Promise<void> => {\n if (opts.keyPath !== undefined && opts.mode !== \"replace\") {\n throw new UsageError('--key requires --mode \"replace\"');\n }\n\n const scoped = shouldPromptForScope(opts, {\n stdinIsTTY: process.stdin.isTTY === true,\n stderrIsTTY: process.stderr.isTTY === true,\n })\n ? { ...opts, countries: await promptForCountries() }\n : opts;\n\n // No positional arguments: read stdin as a single input.\n if (scoped.files.length === 0) {\n const [input] = await readInputs(scoped.files);\n if (!input) throw new UsageError(\"no input to anonymize\");\n guardWriteTargets([], collectSingleTargets(scoped));\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseSingle(scoped, runtime, api, {\n text: input.text,\n outputPath: scoped.output,\n source: \"stdin\",\n });\n return;\n }\n\n const { jobs, batch, skipped } = await expandInputs(\n scoped.files,\n scoped.recursive,\n scoped.output,\n );\n\n if (!batch) {\n // Exactly one plain file: single-input flow with --json/--key.\n const [job] = jobs;\n if (!job) throw new UsageError(\"no input to anonymize\");\n guardWriteTargets([job.path], collectSingleTargets(scoped));\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseSingle(scoped, runtime, api, {\n text: await readFile(job.path, \"utf8\"),\n outputPath: scoped.output,\n source: job.path,\n });\n return;\n }\n\n // Batch: a directory, or more than one file.\n const output = scoped.output;\n if (output === undefined) {\n throw new UsageError(\n \"batch input (a directory or multiple files) requires --output <directory>\",\n );\n }\n if (scoped.keyPath !== undefined) {\n throw new UsageError(\"--key works with a single input only\");\n }\n if (scoped.json) {\n throw new UsageError(\"--json works with a single input only\");\n }\n\n // Validate every write target before any work: colliding\n // output paths (same basename from different input dirs,\n // symlinks to an input) fail fast instead of silently\n // clobbering files mid-batch.\n guardWriteTargets(\n jobs.map((job) => job.path),\n jobs.map((job) => ({\n path: join(output, job.outputRelative),\n flag: \"--output\",\n })),\n );\n\n const runtime = await prepareCliRuntime(\n api,\n await buildPipelineConfig(scoped, loadDictionaries),\n );\n await runAnonymiseBatch(scoped, runtime, output, jobs, skipped);\n};\n\n/** Write targets for a single-input run: --output and --key. */\nconst collectSingleTargets = (\n opts: CliOptions,\n): { path: string; flag: string }[] => {\n const targets: { path: string; flag: string }[] = [];\n if (opts.output !== undefined) {\n targets.push({ path: opts.output, flag: \"--output\" });\n }\n if (opts.keyPath !== undefined) {\n targets.push({ path: opts.keyPath, flag: \"--key\" });\n }\n return targets;\n};\n\ntype CliRuntime = {\n redact: (\n fullText: string,\n operators: CliOperatorConfig,\n ) => Promise<{ entities: CliEntity[]; redaction: CliRedactionResult }>;\n};\n\nconst prepareCliRuntime = async (\n api: AnonymizeApi,\n config: PipelineConfig,\n): Promise<CliRuntime> => {\n const pipeline = await api.createNativePipelineFromConfig({\n binding: api.loadNativeAnonymizeBinding(),\n config,\n gazetteerEntries: [],\n });\n pipeline.warmLazyRegex?.();\n return {\n redact: async (fullText, operators) => {\n const result = pipeline.redactText(fullText, operators);\n return {\n entities: result.resolvedEntities,\n redaction: result.redaction,\n };\n },\n };\n};\n\n/**\n * Render the canonical entity labels and the short aliases\n * accepted by --labels, for the --list-labels discovery flag.\n */\nconst formatLabelList = (): string => {\n const lines: string[] = [\"Detectable entity labels (pass to --labels):\"];\n for (const label of DEFAULT_ENTITY_LABELS) {\n lines.push(` ${label}`);\n }\n lines.push(\"\", \"Short aliases:\");\n const aliases = Object.entries(LABEL_ALIASES);\n let width = 0;\n for (const [alias] of aliases) {\n width = Math.max(width, alias.length);\n }\n for (const [alias, canonical] of aliases) {\n lines.push(` ${alias.padEnd(width)} -> ${canonical}`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n};\n\nconst dispatch = async (engine: CliEngine): Promise<void> => {\n const opts = parseCliArgs(process.argv.slice(2));\n if (opts.help) {\n process.stdout.write(HELP);\n return;\n }\n if (opts.version) {\n process.stdout.write(`${cliVersion()}\\n`);\n return;\n }\n if (opts.listLabels) {\n process.stdout.write(formatLabelList());\n return;\n }\n if (opts.deanonymiseKeyPath !== undefined) {\n await runDeanonymise(opts, engine.api);\n return;\n }\n if (opts.revert !== undefined) {\n throw new UsageError(\"--revert requires --deanonymise <key>\");\n }\n await runAnonymise(opts, engine);\n};\n\n/**\n * Run the CLI against the given engine and set the\n * process exit code (0 ok, 1 runtime error, 2 usage).\n */\nexport const runCli = async (engine: CliEngine): Promise<void> => {\n try {\n await dispatch(engine);\n } catch (err) {\n if (err instanceof UsageError) {\n process.stderr.write(`anonymize: ${err.message}\\n`);\n process.stderr.write(`Try \"anonymize --help\" for usage.\\n`);\n process.exitCode = 2;\n } else {\n const message = err instanceof Error ? err.message : String(err);\n process.stderr.write(`anonymize: ${message}\\n`);\n process.exitCode = 1;\n }\n }\n};\n","#!/usr/bin/env node\n/* npm-distributed entry point — backs the CLI with the\n * native engine (@stll/text-search napi bindings) and\n * the @stll/anonymize-data dictionary package. */\nimport * as anonymize from \"@stll/anonymize\";\n\nimport { loadCliDictionaries } from \"./dictionaries\";\nimport { runCli } from \"./main\";\n\nawait runCli({ api: anonymize, loadDictionaries: loadCliDictionaries });\n"],"mappings":";;;;;;;;;;;AAGA,MAAa,YAAY,CAAC,WAAW,QAAQ;AAG7C,MAAa,oBAAoB;AACjC,MAAa,wBAAwB;;;;AAUrC,MAAa,2BACX,KAAK,IAAI,GAAG,KAAK,IAAA,GAAyB,qBAAqB,CAAC,CAAC;;AAGnE,IAAa,aAAb,cAAgC,MAAM,CAAC;AAuBvC,MAAa,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wCAyCoB,kBAAkB;;yCAEjB,sBAAsB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiD/D,MAAM,aAAa,UAA4B,CAC7C,GAAG,IAAI,IACL,MACG,MAAM,GAAG,CAAC,CACV,KAAK,SAAS,KAAK,KAAK,CAAC,CAAC,CAC1B,QAAQ,SAAS,KAAK,SAAS,CAAC,CACrC,CACF;AAEA,MAAM,kBAAkB,QAAwB;CAC9C,MAAM,QAAQ,OAAO,GAAG;CACxB,IAAI,CAAC,OAAO,SAAS,KAAK,KAAK,QAAQ,KAAK,QAAQ,GAClD,MAAM,IAAI,WACR,sDAAsD,IAAI,EAC5D;CAEF,OAAO;AACT;AAEA,MAAM,gBAAgB,QAAwB;CAC5C,MAAM,QAAQ,OAAO,GAAG;CACxB,IAAI,CAAC,OAAO,UAAU,KAAK,KAAK,QAAQ,GACtC,MAAM,IAAI,WAAW,8CAA8C,IAAI,EAAE;CAE3E,OAAO;AACT;AAEA,MAAM,aAAa,QAAyB;CAC1C,MAAM,OAAO,UAAU,MAAM,cAAc,cAAc,GAAG;CAC5D,IAAI,CAAC,MACH,MAAM,IAAI,WACR,0BAA0B,UAAU,KAAK,IAAI,EAAE,SAAS,IAAI,EAC9D;CAEF,OAAO;AACT;AAEA,MAAM,kBAAkB;AAExB,MAAa,kBAAkB,QAA0B;CACvD,MAAM,YAAY,CAChB,GAAG,IAAI,IAAI,UAAU,GAAG,CAAC,CAAC,KAAK,SAAS,KAAK,YAAY,CAAC,CAAC,CAC7D;CACA,MAAM,UAAU,UAAU,MAAM,SAAS,CAAC,gBAAgB,KAAK,IAAI,CAAC;CACpE,IAAI,SACF,MAAM,IAAI,WACR,qEAAqE,QAAQ,EAC/E;CAEF,OAAO;AACT;AAEA,MAAa,gBAAgB,SAA+B;CAC1D,IAAI;CACJ,IAAI;EACF,SAAS,UAAU;GAAE,GAAG;GAAc,MAAM;EAAK,CAAC;CACpD,SAAS,KAAK;EACZ,MAAM,IAAI,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;CACvE;CACA,MAAM,EAAE,QAAQ,gBAAgB;CAEhC,OAAO;EACL,OAAO;EACP,QAAQ,OAAO;EACf,MAAM,OAAO,SAAS,KAAA,IAAY,YAAY,UAAU,OAAO,IAAI;EACnE,SAAS,OAAO;EAChB,oBAAoB,OAAO;EAC3B,QACE,OAAO,WAAW,KAAA,KAAa,OAAO,OAAO,WAAW,IACpD,KAAA,IACA,OAAO;EACb,WAAW,OAAO,cAAc;EAChC,SACE,OAAO,YAAY,KAAA,IACf,mBAAmB,IACnB,aAAa,OAAO,OAAO;EACjC,QAAQ,OAAO,WAAW,KAAA,IAAY,KAAA,IAAY,UAAU,OAAO,MAAM;EACzE,WACE,OAAO,cAAc,KAAA,IAAY,KAAA,IAAY,UAAU,OAAO,SAAS;EACzE,WACE,OAAO,cAAc,KAAA,IACjB,KAAA,IACA,eAAe,OAAO,SAAS;EACrC,WACE,OAAO,cAAc,KAAA,IACjB,oBACA,eAAe,OAAO,SAAS;EACrC,cAAc,OAAO,oBAAA;EACrB,MAAM,OAAO,SAAS;EACtB,OAAO,OAAO,UAAU;EACxB,MAAM,OAAO,SAAS;EACtB,SAAS,OAAO,YAAY;EAC5B,YAAY,OAAO,mBAAmB;CACxC;AACF;AAEA,MAAM,eAAe;CACnB,kBAAkB;CAClB,QAAQ;CACR,SAAS;EACP,QAAQ;GAAE,MAAM;GAAU,OAAO;EAAI;EACrC,MAAM;GAAE,MAAM;GAAU,OAAO;EAAI;EACnC,KAAK;GAAE,MAAM;GAAU,OAAO;EAAI;EAClC,aAAa;GAAE,MAAM;GAAU,OAAO;EAAI;EAC1C,QAAQ;GAAE,MAAM;GAAU,UAAU;EAAK;EACzC,WAAW;GAAE,MAAM;GAAW,OAAO;EAAI;EACzC,SAAS,EAAE,MAAM,SAAS;EAC1B,QAAQ,EAAE,MAAM,SAAS;EACzB,WAAW,EAAE,MAAM,SAAS;EAC5B,WAAW,EAAE,MAAM,SAAS;EAC5B,WAAW,EAAE,MAAM,SAAS;EAC5B,iBAAiB,EAAE,MAAM,SAAS;EAClC,MAAM,EAAE,MAAM,UAAU;EACxB,OAAO,EAAE,MAAM,UAAU;EACzB,MAAM;GAAE,MAAM;GAAW,OAAO;EAAI;EACpC,SAAS;GAAE,MAAM;GAAW,OAAO;EAAI;EACvC,eAAe,EAAE,MAAM,UAAU;CACnC;AACF;;;ACtPA,MAAa,2BAA2B,CACtC,gBACA,iBACF;;AAGA,MAAa,4BAA4B,OAA8B;CACrE,MAAM,SAAS,yBAAyB,MAAM,MAAM,GAAG,WAAW,CAAC,CAAC;CACpE,OAAO,SAAS,GAAG,MAAM,OAAO,MAAM,IAAI;AAC5C;;;;;;;ACIA,MAAM,yBAAyB;CAC7B;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF;AAEA,MAAM,+BACJ,mBAAmB,QAAQ,OACzB,GAAG,WAAW,yBAAyB,EAAE,CAC3C,CAAC,CAAC,KAAK,OAAO,GAAG,MAAM,yBAAyB,EAAE,CAAC,MAAM,CAAC;AAE5D,MAAM,qBACJ,cAC4B;CAC5B,MAAM,YAAY,uBAAuB;CACzC,MAAM,UAAU,UAAU,MAAM,SAAS,CAAC,UAAU,SAAS,IAAI,CAAC;CAClE,IAAI,SACF,MAAM,IAAI,WACR,wCAAwC,QAAQ,gBAAgB,UAAU,KAAK,IAAI,GACrF;CAIF,OAAO;AACT;;;;;AAQA,MAAa,sBAAsB,OAAO,EACxC,WACA,gBACuD;CACvD,MAAM,gBACJ,cAAc,KAAA,IAAY,KAAA,IAAY,kBAAkB,SAAS;CAEnE,MAAM,UAAU,mBAAmB,QAAQ,OAAO;EAChD,MAAM,OAAO,gBAAgB;EAC7B,IACE,aACA,KAAK,YAAY,QACjB,CAAC,UAAU,SAAS,KAAK,OAAO,GAEhC,OAAO;EAET,MAAM,WAAW,yBAAyB,EAAE;EAC5C,IAAI,aAAa,QAAQ,kBAAkB,KAAA,GACzC,OAAO,cAAc,SAGnB,QACF;EAEF,OAAO;CACT,CAAC;CAED,MAAM,gBAAgB,aAAa;CAEnC,MAAM,CAAC,OAAO,aAAa,eAAe,MAAM,QAAQ,IAAI;EAC1D,qBAAqB,aAAa;EAClC,QAAQ,IACN,QAAQ,IAAI,OAAO,QAAQ;GAAE;GAAI,SAAS,MAAM,eAAe,EAAE;EAAE,EAAE,CACvE;EACA,QAAQ,IACN,cAAc,IAAI,OAAO,aAAa;GACpC;GACA,SAAS,MAAM,mBAAmB,OAAO;EAC3C,EAAE,CACJ;CACF,CAAC;CAED,MAAM,WAA8C,CAAC;CACrD,MAAM,eAA+C,CAAC;CACtD,KAAK,MAAM,EAAE,IAAI,aAAa,aAAa;EACzC,SAAS,MAAM;EAGf,aAAa,MAAM,gBAAgB;CACrC;CAEA,MAAM,kBAAqD,CAAC;CAC5D,KAAK,MAAM,EAAE,SAAS,aAAa,aACjC,IAAI,QAAQ,SAAS,GAAG,gBAAgB,WAAW;CAGrD,OAAO;EACL,YAAY,MAAM;EAClB,UAAU,MAAM;EAChB;EACA;EACA;CACF;AACF;;;;;;AErFA,MAAM,mBAA2BA;;;;;;AAOjC,MAAM,iBAAiB,SAAyB;CAC9C,IAAI;EACF,OAAO,aAAa,IAAI;CAC1B,QAAQ;EACN,OAAO,QAAQ,IAAI;CACrB;AACF;AAEA,MAAM,YAAY,YAA6B;CAC7C,QAAQ,MAAM,YAAY,MAAM;CAChC,IAAI,OAAO;CACX,WAAW,MAAM,SAAS,QAAQ,OAAO,QAAQ;CACjD,OAAO;AACT;AAQA,MAAM,aAAa,OAAO,UAA2C;CACnE,IAAI,MAAM,WAAW,GAAG;EACtB,IAAI,QAAQ,MAAM,OAChB,MAAM,IAAI,WACR,qDACF;EAEF,OAAO,CAAC;GAAE,MAAM;GAAM,MAAM,MAAM,UAAU;EAAE,CAAC;CACjD;CACA,OAAO,QAAQ,IACb,MAAM,IAAI,OAAO,UAAU;EAAE;EAAM,MAAM,MAAM,SAAS,MAAM,MAAM;CAAE,EAAE,CAC1E;AACF;AA0BA,MAAM,mBAAmB;;;;;;AAOzB,MAAM,eAAe,OAAO,SAAmC;CAC7D,MAAM,SAAS,MAAM,KAAK,MAAM,GAAG;CACnC,IAAI;EACF,MAAM,SAAS,OAAO,MAAM,gBAAgB;EAC5C,MAAM,EAAE,cAAc,MAAM,OAAO,KAAK,QAAQ,GAAG,kBAAkB,CAAC;EACtE,OAAO,OAAO,SAAS,GAAG,SAAS,CAAC,CAAC,QAAQ,CAAC,MAAM;CACtD,UAAU;EACR,MAAM,OAAO,MAAM;CACrB;AACF;;;;;;AAOA,MAAM,gBAAgB,OACpB,MACA,WACA,eACsB;CACtB,MAAM,QAAkB,CAAC;CACzB,MAAM,QAAQ,OAAO,QAA+B;EAClD,MAAM,WAAW,MAAM,QAAQ,KAAK,EAAE,eAAe,KAAK,CAAC,EAAA,CAAG,UAC3D,GAAG,MAAM,EAAE,KAAK,cAAc,EAAE,IAAI,CACvC;EACA,KAAK,MAAM,SAAS,SAAS;GAC3B,MAAM,OAAO,KAAK,KAAK,MAAM,IAAI;GACjC,IAAI,MAAM,YAAY,GAAG;IAGvB,IAAI,eAAe,KAAA,KAAa,QAAQ,IAAI,MAAM,YAAY;IAC9D,IAAI,WAAW,MAAM,MAAM,IAAI;GACjC,OAAO,IAAI,MAAM,OAAO,GACtB,MAAM,KAAK,IAAI;EAEnB;CACF;CACA,MAAM,MAAM,IAAI;CAChB,OAAO;AACT;;;;;;;AAQA,MAAM,eAAe,OACnB,OACA,WACA,cAC4B;CAC5B,MAAM,aAAa,cAAc,KAAA,IAAY,KAAA,IAAY,QAAQ,SAAS;CAC1E,MAAM,OAAkB,CAAC;CACzB,IAAI,eAAe;CACnB,IAAI,UAAU;CACd,KAAK,MAAM,QAAQ,OAAO;EAMxB,IAAI;EACJ,IAAI;GACF,QAAQ,MAAM,KAAK,IAAI;EACzB,QAAQ;GACN,KAAK,KAAK;IAAE;IAAM,gBAAgB,SAAS,IAAI;GAAE,CAAC;GAClD;EACF;EACA,IAAI,CAAC,MAAM,YAAY,GAAG;GACxB,KAAK,KAAK;IAAE;IAAM,gBAAgB,SAAS,IAAI;GAAE,CAAC;GAClD;EACF;EACA,eAAe;EACf,KAAK,MAAM,QAAQ,MAAM,cAAc,MAAM,WAAW,UAAU,GAAG;GAKnE,IAAI,CAAC,MADiB,aAAa,IAAI,CAAC,CAAC,YAAY,IAAI,GAC3C;IACZ,WAAW;IACX;GACF;GACA,KAAK,KAAK;IAAE,MAAM;IAAM,gBAAgB,SAAS,MAAM,IAAI;GAAE,CAAC;EAChE;CACF;CACA,OAAO;EAAE;EAAM,OAAO,gBAAgB,KAAK,SAAS;EAAG;CAAQ;AACjE;;;;;;;;AASA,MAAM,UAAU,OACd,OACA,SACA,SACkB;CAClB,IAAI,OAAO;CACX,MAAM,SAAS,YAA2B;EACxC,OAAO,OAAO,MAAM,QAAQ;GAC1B,MAAM,QAAQ;GACd,QAAQ;GAER,MAAM,KAAK,MAAM,MAAW;EAC9B;CACF;CACA,MAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,SAAS,MAAM,MAAM,CAAC;CACzD,MAAM,QAAQ,IAAI,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,MAAM,CAAC;AACzD;AAyCA,MAAM,gBAA6C;CACjD,OAAO;CACP,OAAO;CACP,KAAK;CACL,cAAc;CACd,KAAK;CACL,KAAK;CACL,UAAU;CACV,UAAU;CACV,eAAe;CACf,eAAe;AACjB;AAEA,MAAM,qBAAqB;AAC3B,MAAM,mBAAwC,IAAI,IAAI,qBAAqB;AAE3E,MAAM,iBAAiB,UACrB,iBAAiB,IAAI,KAAK;;;;;;;AAQ5B,MAAM,qBAAqB,QAAwB;CACjD,MAAM,aAAa,IAAI,YAAY,CAAC,CAAC,QAAQ,oBAAoB,GAAG,CAAC,CAAC,KAAK;CAE3E,IAAIC,sBAAM,SAAS,UAAU,GAC3B,OAAO;CAET,OAAO,cAAc,eAAe;AACtC;AAEA,MAAM,kBAAkB,WAA6C;CACnE,MAAM,WAAW,CAAC,GAAG,IAAI,IAAI,OAAO,IAAI,iBAAiB,CAAC,CAAC;CAC3D,MAAM,QAAuB,CAAC;CAC9B,MAAM,kBAAkB,sBAAsB,KAAK,IAAI;CACvD,MAAM,mBAAmB,OAAO,KAAK,aAAa,CAAC,CAAC,KAAK,IAAI;CAC7D,KAAK,MAAM,SAAS,UAAU;EAC5B,IAAI,CAAC,cAAc,KAAK,GACtB,MAAM,IAAI,WACR;GACE;GACA,KAAK,UAAU,KAAK,IAAI;GACxB;GACA;GACA;GACA,mBAAmB;EACrB,CAAC,CAAC,KAAK,GAAG,CACZ;EAEF,MAAM,KAAK,KAAK;CAClB;CACA,OAAO;AACT;AAEA,MAAM,sBAAsB,OAC1B,MACA,qBAC4B;CAC5B,MAAM,eAAe,MAAM,iBAAiB;EAC1C,WAAW,KAAK;EAChB,WAAW,KAAK;CAClB,CAAC;CACD,OAAO;EACL,WAAW,KAAK;EAChB,sBAAsB;EACtB,aAAa;EACb,kBAAkB;EAClB,kBAAkB;EAClB,GAAI,KAAK,cAAc,KAAA,IACnB,CAAC,IACD,EAAE,qBAAqB,CAAC,GAAG,KAAK,SAAS,EAAE;EAC/C,gBAAgB;EAChB,GAAI,KAAK,cAAc,KAAA,IACnB,CAAC,IACD,EAAE,mBAAmB,CAAC,GAAG,KAAK,SAAS,EAAE;EAC7C,iBAAiB;EACjB,iBAAiB;EACjB,WAAW;EACX,uBAAuB;EACvB,mBAAmB;EACnB,0BAA0B;EAC1B,oBAAoB;EACpB,QACE,KAAK,WAAW,KAAA,IACZ,CAAC,GAAG,qBAAqB,IACzB,eAAe,KAAK,MAAM;EAChC,aAAa;EACb;CACF;AACF;AAEA,MAAM,uBAAuB,SAAwC;CACnE,MAAM,YAA0C,CAAC;CACjD,IAAI,KAAK,SAAS,UAAU;EAC1B,MAAM,SACJ,KAAK,WAAW,KAAA,IACZ,wBACA,eAAe,KAAK,MAAM;EAChC,KAAK,MAAM,SAAS,QAAQ,UAAU,SAAS;CACjD;CACA,OAAO;EAAE;EAAW,cAAc,KAAK;CAAa;AACtD;AAEA,MAAM,cAAc,OAClB,MACA,YACkB;CAClB,IAAI,SAAS,KAAA,GAAW;EACtB,QAAQ,OAAO,MAAM,OAAO;EAC5B;CACF;CACA,MAAM,UAAU,MAAM,SAAS,MAAM;AACvC;AAMA,MAAM,qBAAqB,QAAqC;CAC9D,IAAI;CACJ,IAAI;EACF,SAAS,KAAK,MAAM,GAAG;CACzB,QAAQ;EACN,MAAM,IAAI,WAAW,iCAAiC;CACxD;CACA,IAAI,OAAO,WAAW,YAAY,WAAW,QAAQ,EAAE,aAAa,SAClE,MAAM,IAAI,WACR,2DACF;CAEF,MAAM,EAAE,YAAY;CACpB,IACE,OAAO,YAAY,YACnB,YAAY,QACZ,MAAM,QAAQ,OAAO,GAErB,MAAM,IAAI,WAAW,6CAA2C;CAElE,MAAM,sBAAM,IAAI,IAAoB;CACpC,KAAK,MAAM,CAAC,aAAa,UAAU,OAAO,QAAQ,OAAO,GAAG;EAC1D,IAAI,OAAO,OAAO,aAAa,UAC7B,MAAM,IAAI,WACR,wBAAwB,YAAY,uBACtC;EAEF,IAAI,IAAI,aAAa,MAAM,QAAQ;CACrC;CACA,OAAO;AACT;;;;;;;;AASA,MAAM,uBACJ,cACA,WACwB;CACxB,MAAM,2BAAW,IAAI,IAAoB;CACzC,KAAK,MAAM,SAAS,QAAQ;EAC1B,IAAI,UAAU;EACd,KAAK,MAAM,CAAC,aAAa,aAAa,cACpC,IAAI,gBAAgB,SAAS,aAAa,OAAO;GAC/C,SAAS,IAAI,aAAa,QAAQ;GAClC,UAAU;EACZ;EAEF,IAAI,CAAC,SAAS;GACZ,MAAM,0BAA0B;GAChC,MAAM,eAAe,CAAC,GAAG,aAAa,KAAK,CAAC;GAC5C,MAAM,SAAS,aAAa,MAAM,GAAG,uBAAuB,CAAC,CAAC,KAAK,IAAI;GACvE,MAAM,OAAO,aAAa,SAAS;GACnC,MAAM,SAAS,OAAO,IAAI,QAAQ,KAAK,SAAS;GAChD,MAAM,IAAI,WACR,YAAY,KAAK,UAAU,KAAK,EAAE,+DACK,SAAS,QAClD;EACF;CACF;CACA,OAAO;AACT;;;;;;AAOA,MAAa,wBACX,MACA,QAEA,KAAK,cAAc,KAAA,KACnB,KAAK,cAAc,KAAA,KACnB,CAAC,KAAK,SACN,KAAK,MAAM,SAAS,KACpB,IAAI,cACJ,IAAI;AAEN,MAAM,qBAAqB,YAA2C;CACpE,MAAM,KAAK,gBAAgB;EACzB,OAAO,QAAQ;EACf,QAAQ,QAAQ;CAClB,CAAC;CACD,IAAI;EAIF,MAAM,WAAU,MAHK,GAAG,SACtB,4DACF,EAAA,CACuB,KAAK;EAC5B,OAAO,YAAY,KAAK,KAAA,IAAY,eAAe,OAAO;CAC5D,UAAU;EACR,GAAG,MAAM;CACX;AACF;AAEA,MAAM,iBAAiB,OACrB,MACA,QACkB;CAClB,IAAI,KAAK,YAAY,KAAA,GACnB,MAAM,IAAI,WAAW,6CAA6C;CAEpE,MAAM,UAAU,KAAK;CACrB,IAAI,YAAY,KAAA,GAAW,MAAM,IAAI,WAAW,4BAA4B;CAC5E,MAAM,UAAU,kBAAkB,MAAM,SAAS,SAAS,MAAM,CAAC;CAKjE,MAAM,eACJ,KAAK,WAAW,KAAA,IACZ,UACA,oBAAoB,SAAS,KAAK,MAAM;CAE9C,MAAM,SAAS,MAAM,WAAW,KAAK,KAAK;CAC1C,IAAI,OAAO,SAAS,GAClB,MAAM,IAAI,WAAW,sCAAsC;CAE7D,MAAM,QAAQ,OAAO;CACrB,IAAI,CAAC,OAAO,MAAM,IAAI,WAAW,yBAAyB;CAC1D,IAAI,KAAK,WAAW,KAAA,GAClB,kBAAkB,MAAM,SAAS,OAAO,CAAC,IAAI,CAAC,MAAM,IAAI,GAAG,CACzD;EAAE,MAAM,KAAK;EAAQ,MAAM;CAAW,CACxC,CAAC;CAEH,MAAM,YAAY,KAAK,QAAQ,IAAI,YAAY,MAAM,MAAM,YAAY,CAAC;AAC1E;;;;;;AAOA,MAAM,qBACJ,YACA,iBACS;CACT,MAAM,SAAS,IAAI,IAAI,WAAW,IAAI,aAAa,CAAC;CACpD,MAAM,uBAAO,IAAI,IAAoB;CACrC,KAAK,MAAM,UAAU,cAAc;EACjC,MAAM,YAAY,cAAc,OAAO,IAAI;EAC3C,IAAI,OAAO,IAAI,SAAS,GACtB,MAAM,IAAI,WACR,qCAAqC,OAAO,KAAK,KAAK,OAAO,KAAK,EACpE;EAEF,MAAM,QAAQ,KAAK,IAAI,SAAS;EAChC,IAAI,UAAU,KAAA,GACZ,MAAM,IAAI,WACR,GAAG,OAAO,KAAK,IAAI,OAAO,KAAK,kBAAkB,OACnD;EAEF,KAAK,IAAI,WAAW,GAAG,OAAO,KAAK,IAAI,OAAO,KAAK,EAAE;CACvD;AACF;AAEA,MAAM,aAAa,aAA2C;CAC5D,MAAM,yBAAS,IAAI,IAAoB;CACvC,KAAK,MAAM,UAAU,UACnB,OAAO,IAAI,OAAO,QAAQ,OAAO,IAAI,OAAO,KAAK,KAAK,KAAK,CAAC;CAE9D,MAAM,QAAQ,CAAC,GAAG,OAAO,QAAQ,CAAC,CAAC,CAChC,UAAU,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAC/B,KAAK,CAAC,OAAO,WAAW,GAAG,MAAM,IAAI,OAAO;CAC/C,OAAO,MAAM,SAAS,IAAI,MAAM,KAAK,IAAI,IAAI;AAC/C;AAcA,MAAM,qBAAqB,OACzB,MACA,SACA,KACA,UACkB;CAClB,MAAM,EAAE,UAAU,cAAc,MAAM,QAAQ,OAC5C,MAAM,MACN,oBAAoB,IAAI,CAC1B;CAEA,IAAI,KAAK,MAAM;EAMb,MAAM,eACJ,KAAK,SAAS,WACV,SAAS,KAAK,EAAE,OAAO,KAAK,OAAO,OAAO,cAAc;GACtD;GACA;GACA;GACA;GACA;EACF,EAAE,IACF;EACN,MAAM,UAAU;GACd,aAAa,UAAU;GACvB,UAAU;GACV,cAAc,UAAU;EAC1B;EACA,MAAM,YACJ,MAAM,YACN,GAAG,KAAK,UAAU,SAAS,MAAM,CAAC,EAAE,GACtC;CACF,OACE,MAAM,YAAY,MAAM,YAAY,UAAU,YAAY;CAG5D,IAAI,KAAK,YAAY,KAAA,GACnB,MAAM,UACJ,KAAK,SACL,IAAI,mBAAmB,UAAU,cAAc,UAAU,WAAW,GACpE,MACF;CAGF,IAAI,CAAC,KAAK,OACR,QAAQ,OAAO,MACb,cAAc,MAAM,OAAO,IAAI,UAAU,QAAQ,EAAE,GACrD;AAEJ;AAKA,MAAM,oBAAoB,OACxB,MACA,SACA,QACA,MACA,YACkB;CAClB,MAAM,MAAM,QAAQ,EAAE,WAAW,KAAK,CAAC;CACvC,MAAM,iBAAiB,oBAAoB,IAAI;CAC/C,MAAM,UAAwB;EAAE,WAAW;EAAG,QAAQ;CAAE;CAExD,MAAM,QAAQ,MAAM,KAAK,SAAS,OAAO,QAAQ;EAC/C,MAAM,aAAa,KAAK,QAAQ,IAAI,cAAc;EAClD,IAAI;GACF,MAAM,OAAO,MAAM,SAAS,IAAI,MAAM,MAAM;GAC5C,MAAM,EAAE,UAAU,cAAc,MAAM,QAAQ,OAC5C,MACA,cACF;GACA,MAAM,MAAM,QAAQ,UAAU,GAAG,EAAE,WAAW,KAAK,CAAC;GACpD,MAAM,UAAU,YAAY,UAAU,cAAc,MAAM;GAG1D,QAAQ,aAAa;GACrB,IAAI,CAAC,KAAK,OACR,QAAQ,OAAO,MACb,cAAc,IAAI,KAAK,IAAI,UAAU,QAAQ,EAAE,GACjD;EAEJ,SAAS,KAAK;GACZ,QAAQ,UAAU;GAClB,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC/D,QAAQ,OAAO,MAAM,cAAc,IAAI,KAAK,WAAW,QAAQ,GAAG;EACpE;CACF,CAAC;CAED,IAAI,CAAC,KAAK,OAAO;EACf,MAAM,QAAQ,CACZ,GAAG,QAAQ,UAAU,aACrB,GAAG,QAAQ,OAAO,QACpB;EACA,IAAI,UAAU,GAAG,MAAM,KAAK,GAAG,QAAQ,SAAS;EAChD,QAAQ,OAAO,MAAM,cAAc,MAAM,KAAK,IAAI,EAAE,GAAG;CACzD;CAGA,IAAI,QAAQ,SAAS,GAAG,QAAQ,WAAW;AAC7C;AAEA,MAAM,eAAe,OACnB,MACA,EAAE,KAAK,uBACW;CAClB,IAAI,KAAK,YAAY,KAAA,KAAa,KAAK,SAAS,WAC9C,MAAM,IAAI,WAAW,mCAAiC;CAGxD,MAAM,SAAS,qBAAqB,MAAM;EACxC,YAAY,QAAQ,MAAM,UAAU;EACpC,aAAa,QAAQ,OAAO,UAAU;CACxC,CAAC,IACG;EAAE,GAAG;EAAM,WAAW,MAAM,mBAAmB;CAAE,IACjD;CAGJ,IAAI,OAAO,MAAM,WAAW,GAAG;EAC7B,MAAM,CAAC,SAAS,MAAM,WAAW,OAAO,KAAK;EAC7C,IAAI,CAAC,OAAO,MAAM,IAAI,WAAW,uBAAuB;EACxD,kBAAkB,CAAC,GAAG,qBAAqB,MAAM,CAAC;EAKlD,MAAM,mBAAmB,QAAQ,MAJX,kBACpB,KACA,MAAM,oBAAoB,QAAQ,gBAAgB,CACpD,GAC0C,KAAK;GAC7C,MAAM,MAAM;GACZ,YAAY,OAAO;GACnB,QAAQ;EACV,CAAC;EACD;CACF;CAEA,MAAM,EAAE,MAAM,OAAO,YAAY,MAAM,aACrC,OAAO,OACP,OAAO,WACP,OAAO,MACT;CAEA,IAAI,CAAC,OAAO;EAEV,MAAM,CAAC,OAAO;EACd,IAAI,CAAC,KAAK,MAAM,IAAI,WAAW,uBAAuB;EACtD,kBAAkB,CAAC,IAAI,IAAI,GAAG,qBAAqB,MAAM,CAAC;EAK1D,MAAM,mBAAmB,QAAQ,MAJX,kBACpB,KACA,MAAM,oBAAoB,QAAQ,gBAAgB,CACpD,GAC0C,KAAK;GAC7C,MAAM,MAAM,SAAS,IAAI,MAAM,MAAM;GACrC,YAAY,OAAO;GACnB,QAAQ,IAAI;EACd,CAAC;EACD;CACF;CAGA,MAAM,SAAS,OAAO;CACtB,IAAI,WAAW,KAAA,GACb,MAAM,IAAI,WACR,2EACF;CAEF,IAAI,OAAO,YAAY,KAAA,GACrB,MAAM,IAAI,WAAW,sCAAsC;CAE7D,IAAI,OAAO,MACT,MAAM,IAAI,WAAW,uCAAuC;CAO9D,kBACE,KAAK,KAAK,QAAQ,IAAI,IAAI,GAC1B,KAAK,KAAK,SAAS;EACjB,MAAM,KAAK,QAAQ,IAAI,cAAc;EACrC,MAAM;CACR,EAAE,CACJ;CAMA,MAAM,kBAAkB,QAAQ,MAJV,kBACpB,KACA,MAAM,oBAAoB,QAAQ,gBAAgB,CACpD,GACyC,QAAQ,MAAM,OAAO;AAChE;;AAGA,MAAM,wBACJ,SACqC;CACrC,MAAM,UAA4C,CAAC;CACnD,IAAI,KAAK,WAAW,KAAA,GAClB,QAAQ,KAAK;EAAE,MAAM,KAAK;EAAQ,MAAM;CAAW,CAAC;CAEtD,IAAI,KAAK,YAAY,KAAA,GACnB,QAAQ,KAAK;EAAE,MAAM,KAAK;EAAS,MAAM;CAAQ,CAAC;CAEpD,OAAO;AACT;AASA,MAAM,oBAAoB,OACxB,KACA,WACwB;CACxB,MAAM,WAAW,MAAM,IAAI,+BAA+B;EACxD,SAAS,IAAI,2BAA2B;EACxC;EACA,kBAAkB,CAAC;CACrB,CAAC;CACD,SAAS,gBAAgB;CACzB,OAAO,EACL,QAAQ,OAAO,UAAU,cAAc;EACrC,MAAM,SAAS,SAAS,WAAW,UAAU,SAAS;EACtD,OAAO;GACL,UAAU,OAAO;GACjB,WAAW,OAAO;EACpB;CACF,EACF;AACF;;;;;AAMA,MAAM,wBAAgC;CACpC,MAAM,QAAkB,CAAC,8CAA8C;CACvE,KAAK,MAAM,SAAS,uBAClB,MAAM,KAAK,KAAK,OAAO;CAEzB,MAAM,KAAK,IAAI,gBAAgB;CAC/B,MAAM,UAAU,OAAO,QAAQ,aAAa;CAC5C,IAAI,QAAQ;CACZ,KAAK,MAAM,CAAC,UAAU,SACpB,QAAQ,KAAK,IAAI,OAAO,MAAM,MAAM;CAEtC,KAAK,MAAM,CAAC,OAAO,cAAc,SAC/B,MAAM,KAAK,KAAK,MAAM,OAAO,KAAK,EAAE,QAAQ,WAAW;CAEzD,OAAO,GAAG,MAAM,KAAK,IAAI,EAAE;AAC7B;AAEA,MAAM,WAAW,OAAO,WAAqC;CAC3D,MAAM,OAAO,aAAa,QAAQ,KAAK,MAAM,CAAC,CAAC;CAC/C,IAAI,KAAK,MAAM;EACb,QAAQ,OAAO,MAAM,IAAI;EACzB;CACF;CACA,IAAI,KAAK,SAAS;EAChB,QAAQ,OAAO,MAAM,GAAG,WAAW,EAAE,GAAG;EACxC;CACF;CACA,IAAI,KAAK,YAAY;EACnB,QAAQ,OAAO,MAAM,gBAAgB,CAAC;EACtC;CACF;CACA,IAAI,KAAK,uBAAuB,KAAA,GAAW;EACzC,MAAM,eAAe,MAAM,OAAO,GAAG;EACrC;CACF;CACA,IAAI,KAAK,WAAW,KAAA,GAClB,MAAM,IAAI,WAAW,uCAAuC;CAE9D,MAAM,aAAa,MAAM,MAAM;AACjC;;;;;AAMA,MAAa,SAAS,OAAO,WAAqC;CAChE,IAAI;EACF,MAAM,SAAS,MAAM;CACvB,SAAS,KAAK;EACZ,IAAI,eAAe,YAAY;GAC7B,QAAQ,OAAO,MAAM,cAAc,IAAI,QAAQ,GAAG;GAClD,QAAQ,OAAO,MAAM,qCAAqC;GAC1D,QAAQ,WAAW;EACrB,OAAO;GACL,MAAM,UAAU,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GAC/D,QAAQ,OAAO,MAAM,cAAc,QAAQ,GAAG;GAC9C,QAAQ,WAAW;EACrB;CACF;AACF;;;ACh3BA,MAAM,OAAO;CAAE,KAAK;CAAW,kBAAkB;AAAoB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stll/anonymize-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Command-line PII detection and anonymization powered by @stll/anonymize",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"anonymization",
|
|
7
|
+
"pii",
|
|
8
|
+
"redaction",
|
|
9
|
+
"ner",
|
|
10
|
+
"pseudonymization",
|
|
11
|
+
"gdpr",
|
|
12
|
+
"privacy",
|
|
13
|
+
"cli"
|
|
14
|
+
],
|
|
5
15
|
"type": "module",
|
|
6
16
|
"bin": {
|
|
7
17
|
"anonymize": "./dist/cli.mjs"
|
|
@@ -16,24 +26,23 @@
|
|
|
16
26
|
"type": "git",
|
|
17
27
|
"url": "git+https://github.com/stella/anonymize.git"
|
|
18
28
|
},
|
|
29
|
+
"homepage": "https://github.com/stella/anonymize",
|
|
19
30
|
"license": "MIT",
|
|
20
31
|
"scripts": {
|
|
21
32
|
"build": "tsdown",
|
|
22
|
-
"compile": "bun scripts/copy-wasm-payloads.ts && bun scripts/embed-data.ts && bun build --compile '--asset-naming=[name].[ext]' src/compile.ts --outfile dist/anonymize",
|
|
23
33
|
"prepublishOnly": "bun run build",
|
|
24
34
|
"typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.test.json",
|
|
25
35
|
"test": "bun test --timeout 60000",
|
|
26
36
|
"format": "oxfmt ."
|
|
27
37
|
},
|
|
28
38
|
"dependencies": {
|
|
29
|
-
"@stll/anonymize": "^
|
|
39
|
+
"@stll/anonymize": "^2.0.0",
|
|
30
40
|
"@stll/anonymize-data": "^0.0.6"
|
|
31
41
|
},
|
|
32
42
|
"devDependencies": {
|
|
33
|
-
"@
|
|
34
|
-
"@types/node": "^25.9.3",
|
|
43
|
+
"@types/node": "^26.0.1",
|
|
35
44
|
"bun-types": "^1.3.14",
|
|
36
|
-
"tsdown": "^0.22.
|
|
45
|
+
"tsdown": "^0.22.3",
|
|
37
46
|
"typescript": "^6.0.3"
|
|
38
47
|
}
|
|
39
48
|
}
|