kiriya 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ output shapes are kiriya's public API.
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.0] - 2026-10-04
11
+
12
+ ### Added
13
+
14
+ - `secrets scan` finds secrets committed into a folder's files: a token whose issuer gives it
15
+ a recognisable prefix, a PEM private key, a password inside a URL, or a JWT. It exits 1 when
16
+ anything is found, so a git hook or a CI step needs no output parsing. **A finding never
17
+ carries the secret** — `ghp_****`, `postgres://me:****@` — because a report that quoted the
18
+ value would copy it into scrollback and CI logs. Unlike every other command it reads hidden
19
+ files by default, since `.env` and `.npmrc` are where secrets sit, and it skips dependency
20
+ folders instead; `--all` scans those too. A line saying `kiriya:allow-secret`, on it or above
21
+ it, declares one deliberate, which is how test fixtures and documentation stay quiet.
22
+ Over MCP it needs `mcp.allowWrite`, as `clip paste` does.
23
+
24
+ It deliberately does not flag a line that merely assigns something named like a secret.
25
+ That rule serves `env show`, where the name and the value are already separate, and over
26
+ source code it is useless: against kiriya itself it matched 42 lines and every one was
27
+ syntax, such as `if (token === "--")` in the argv parser.
28
+
10
29
  ## [0.1.3] - 2026-10-03
11
30
 
12
31
  ### Added
package/README.md CHANGED
@@ -87,6 +87,7 @@ option, what each does, and whether it can be undone.
87
87
  | [`open`](docs/modules/open.md) | Open a file or folder in its default application, or a web address in the browser. |
88
88
  | [`port`](docs/modules/port.md) | See which process listens on a TCP port, end it, or find a free port, the same on every OS. |
89
89
  | [`proc`](docs/modules/proc.md) | List, find and end processes, and show them as a tree, the same on every OS. |
90
+ | [`secrets`](docs/modules/secrets.md) | Find secrets committed into a folder's files: tokens, private keys and passwords in plain sight. |
90
91
  | [`self-update`](docs/modules/self-update.md) | Ask the registry whether a newer kiriya exists, and install it with --apply. |
91
92
  | [`sys`](docs/modules/sys.md) | This machine and the developer tools on it, described the same way on every OS. |
92
93
  | [`wait`](docs/modules/wait.md) | Wait until a port listens, a web address answers or a file appears, the same way on every OS. |
@@ -13,6 +13,7 @@ import { netModule } from "../modules/net/net.module.js";
13
13
  import { openModule } from "../modules/open/open.module.js";
14
14
  import { portModule } from "../modules/port/port.module.js";
15
15
  import { procModule } from "../modules/proc/proc.module.js";
16
+ import { secretsModule } from "../modules/secrets/secrets.module.js";
16
17
  import { selfUpdateModule } from "../modules/self-update/self-update.module.js";
17
18
  import { sysModule } from "../modules/sys/sys.module.js";
18
19
  import { waitModule } from "../modules/wait/wait.module.js";
@@ -36,5 +37,6 @@ export const BUILT_IN_MODULES = [
36
37
  completionModule,
37
38
  waitModule,
38
39
  selfUpdateModule,
40
+ secretsModule,
39
41
  ];
40
42
  //# sourceMappingURL=modules.js.map
@@ -1,7 +1,7 @@
1
1
  import path from "node:path";
2
- import { walk } from "../../../core/application/walk.js";
3
- import { KiriyaError } from "../../../core/domain/errors.js";
4
- import { globToRegExp } from "../../../core/domain/glob.js";
2
+ import { walk } from "./walk.js";
3
+ import { KiriyaError } from "../domain/errors.js";
4
+ import { globToRegExp } from "../domain/glob.js";
5
5
  import { decodeText, MAX_TEXT_BYTES } from "../domain/text-encoding.js";
6
6
  /** A text file, or null when it is binary, larger than MAX_TEXT_BYTES, or unreadable. */
7
7
  export async function readTextFile(fileSystem, content, target) {
@@ -35,7 +35,10 @@ export async function selectFiles(fileSystem, paths, selection) {
35
35
  files.push(target);
36
36
  }
37
37
  else if (stat?.kind === "directory") {
38
- for await (const entry of walk(fileSystem, target, { all: selection.all })) {
38
+ const options = selection.skipDirectories === undefined
39
+ ? { all: selection.all }
40
+ : { all: selection.all, skipDirectories: selection.skipDirectories };
41
+ for await (const entry of walk(fileSystem, target, options)) {
39
42
  if (entry.kind === "file" && keep(entry.name, entry.rel))
40
43
  files.push(entry.path);
41
44
  }
@@ -1,7 +1,7 @@
1
1
  import { splitWords } from "./text-case.js";
2
2
  const SECRET_ASSIGNMENT = /(pass(word)?|pwd|secret|token|api[-_]?key)\s*[=:]/i;
3
3
  const PRIVATE_KEY = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
4
- /** `scheme://user:password@host`. */
4
+ /** `scheme://user:password@host`. kiriya:allow-secret */
5
5
  const URL_CREDENTIALS = /[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/i;
6
6
  /** A JSON Web Token: two base64url JSON parts, then the signature. */
7
7
  const JWT = /eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\./;
@@ -15,6 +15,57 @@ export function looksSecret(value) {
15
15
  JWT.test(value) ||
16
16
  PREFIXED_TOKEN.test(value.trim()));
17
17
  }
18
+ export const SECRET_KINDS = ["private-key", "url-credentials", "jwt", "token"];
19
+ /**
20
+ * Patterns that identify a secret by its own shape, for finding one inside a line of a file.
21
+ *
22
+ * `SECRET_ASSIGNMENT` is deliberately not here. It serves `looksSecret`, which judges a whole
23
+ * value where the name and the value are already separate, and it is useless over source code:
24
+ * run against kiriya itself it matched 42 lines and every one was wrong — `if (token === "--")`
25
+ * in the argv parser, `decodeJwt(token: string)`, `readonly secret: Uint8Array`. A scanner that
26
+ * cries wolf 42 times gets turned off, so it scans only for what cannot be mistaken for syntax.
27
+ */
28
+ const SCANNED = [
29
+ { kind: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
30
+ { kind: "url-credentials", pattern: /[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:[^\s/@]+@/gi },
31
+ { kind: "jwt", pattern: /eyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*/g },
32
+ {
33
+ kind: "token",
34
+ pattern: /(?:(?:ghp|gho|ghu|ghs|ghr)_|github_pat_|glpat-|xox[abprs]-|npm_|sk-)[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}/g,
35
+ },
36
+ ];
37
+ /** Every secret in one piece of text, earliest first, without overlaps. */
38
+ export function findSecrets(text) {
39
+ const found = [];
40
+ const taken = [];
41
+ for (const { kind, pattern } of SCANNED) {
42
+ for (const match of text.matchAll(pattern)) {
43
+ const start = match.index;
44
+ const end = start + match[0].length;
45
+ if (taken.some((range) => start < range.end && end > range.start))
46
+ continue;
47
+ taken.push({ start, end });
48
+ found.push({ kind, start, length: match[0].length });
49
+ }
50
+ }
51
+ return found.sort((a, b) => a.start - b.start);
52
+ }
53
+ /**
54
+ * What may be printed about a match: enough to recognise whose secret it is, never enough to
55
+ * use it. A finding that carried the value would copy it into scrollback, CI logs and issue
56
+ * comments, which is the opposite of the point.
57
+ */
58
+ export function redactSecret(text, match) {
59
+ const value = text.slice(match.start, match.start + match.length);
60
+ // The marker line is not itself secret, and it is what tells you what you are looking at.
61
+ if (match.kind === "private-key")
62
+ return value;
63
+ const prefix = /^(?:(?:ghp|gho|ghu|ghs|ghr)_|github_pat_|glpat-|xox[abprs]-|npm_|sk-|AKIA|eyJ)/.exec(value);
64
+ if (prefix !== null)
65
+ return `${prefix[0]}****`;
66
+ const scheme = /^[a-z][a-z0-9+.-]*:\/\/[^\s/:@]+:/i.exec(value);
67
+ return scheme === null ? "****" : `${scheme[0]}****@`;
68
+ }
18
69
  const SECRET_WORDS = new Set([
19
70
  "password",
20
71
  "passwd",
@@ -45,6 +45,42 @@ for ($i = 0; $i -lt $paths.Length; $i++) {
45
45
  } catch { "error|$i|$($_.Exception.Message)" }
46
46
  }
47
47
  `;
48
+ /**
49
+ * What the script's output means, separate from running it. Constrained Language Mode needs a
50
+ * WDAC or AppLocker policy to reproduce, which no test machine has, so that branch is reached
51
+ * here instead of being left to a machine nobody has.
52
+ */
53
+ export function readTrashResult(stdout, stderr, paths) {
54
+ const lines = stdout
55
+ .split(/\r?\n/)
56
+ .map((line) => line.trim())
57
+ .filter((line) => line !== "");
58
+ const mode = lines.find((line) => line.startsWith("mode|"))?.slice(5) ?? "unknown";
59
+ if (mode !== "FullLanguage")
60
+ throw new CapabilityUnavailableError("core.trash.windows.language-mode", { mode });
61
+ const byIndex = new Map();
62
+ for (const line of lines) {
63
+ const [status, indexText, ...rest] = line.split("|");
64
+ if (indexText === undefined || !/^[0-9]+$/.test(indexText))
65
+ continue;
66
+ const index = Number(indexText);
67
+ const path = paths[index] ?? "";
68
+ if (status === "ok")
69
+ byIndex.set(index, { path, ok: true });
70
+ else if (status === "network")
71
+ byIndex.set(index, { path, ok: false, reason: message("core.trash.windows.network") });
72
+ else if (status === "drive") {
73
+ byIndex.set(index, { path, ok: false, reason: message("core.trash.windows.drive", { drive: rest.join("|") }) });
74
+ }
75
+ else
76
+ byIndex.set(index, { path, ok: false, reason: message("core.trash.failed", { detail: rest.join("|") }) });
77
+ }
78
+ return paths.map((path, index) => byIndex.get(index) ?? {
79
+ path,
80
+ ok: false,
81
+ reason: message("core.trash.failed", { detail: stderr.trim().split(/\r?\n/)[0] ?? "no result" }),
82
+ });
83
+ }
48
84
  export class WindowsTrashAdapter {
49
85
  location = "core.trash.location.recycle-bin";
50
86
  async send(paths) {
@@ -52,35 +88,7 @@ export class WindowsTrashAdapter {
52
88
  env: { KIRIYA_TRASH_PATHS: paths.join(NEWLINE) },
53
89
  timeoutMs: 10 * 60_000,
54
90
  });
55
- const lines = result.stdout
56
- .split(/\r?\n/)
57
- .map((line) => line.trim())
58
- .filter((line) => line !== "");
59
- const mode = lines.find((line) => line.startsWith("mode|"))?.slice(5) ?? "unknown";
60
- if (mode !== "FullLanguage")
61
- throw new CapabilityUnavailableError("core.trash.windows.language-mode", { mode });
62
- const byIndex = new Map();
63
- for (const line of lines) {
64
- const [status, indexText, ...rest] = line.split("|");
65
- if (indexText === undefined || !/^[0-9]+$/.test(indexText))
66
- continue;
67
- const index = Number(indexText);
68
- const path = paths[index] ?? "";
69
- if (status === "ok")
70
- byIndex.set(index, { path, ok: true });
71
- else if (status === "network")
72
- byIndex.set(index, { path, ok: false, reason: message("core.trash.windows.network") });
73
- else if (status === "drive") {
74
- byIndex.set(index, { path, ok: false, reason: message("core.trash.windows.drive", { drive: rest.join("|") }) });
75
- }
76
- else
77
- byIndex.set(index, { path, ok: false, reason: message("core.trash.failed", { detail: rest.join("|") }) });
78
- }
79
- return paths.map((path, index) => byIndex.get(index) ?? {
80
- path,
81
- ok: false,
82
- reason: message("core.trash.failed", { detail: result.stderr.trim().split(/\r?\n/)[0] ?? "no result" }),
83
- });
91
+ return readTrashResult(result.stdout, result.stderr, paths);
84
92
  }
85
93
  }
86
94
  //# sourceMappingURL=windows-trash.adapter.js.map
@@ -667,6 +667,25 @@ export const en = {
667
667
  "wait.file.gone": "{path} is gone, after {seconds} s",
668
668
  "wait.file.timed-out": "{path} did not appear within {seconds} s",
669
669
  "wait.file.still-there": "{path} was still there after {seconds} s",
670
+ "secrets.summary": "Find secrets committed into a folder's files: tokens, private keys and passwords in plain sight",
671
+ "secrets.about": "Reads the files under a folder and reports anything that looks like a secret: a token " +
672
+ "whose issuer gives it a recognisable prefix, a private key, a password in a URL, a JWT, " +
673
+ "or a line that assigns one. It uses the same judgement as the rest of kiriya, so what " +
674
+ "env show hides and config set refuses is what this finds. Nothing is sent anywhere and " +
675
+ "nothing changes. A finding never carries the secret itself, only enough to recognise it, " +
676
+ "because a report that quoted it would copy it into scrollback and CI logs. Exits 1 when " +
677
+ "anything is found, so a hook or a CI step needs no output parsing. A line saying " +
678
+ "kiriya:allow-secret, on it or above it, declares that one deliberate.",
679
+ "secrets.scan.summary": "Scan a folder's files for tokens, private keys and passwords, without printing them",
680
+ "secrets.scan.arg.paths": "Folders or files to scan; the current folder by default",
681
+ "secrets.scan.option.all": "Also look inside dependency folders such as node_modules; hidden files are read either way",
682
+ "secrets.scan.option.limit": "Stop after this many findings; 500 by default",
683
+ "secrets.scan.clean": "No secrets found in {checked} file(s)",
684
+ "secrets.scan.counted": "{count} finding(s) in {checked} file(s)",
685
+ "secrets.scan.found": "{count} possible secret(s) found",
686
+ "secrets.scan.allowed": "{count} marked deliberate with kiriya:allow-secret",
687
+ "secrets.scan.truncated": "The list stopped at --limit; there may be more.",
688
+ "secrets.scan.how-allow": "A line with kiriya:allow-secret on it, or above it, is left alone.",
670
689
  "self-update.summary": "Ask the registry whether a newer kiriya exists, and install it with --apply",
671
690
  "self-update.about": "Asks npm's registry which version it serves as latest and compares it with this one. " +
672
691
  "Without --apply it changes nothing, so it is safe to run just to find out. With --apply " +
@@ -5,7 +5,7 @@ import { NotFoundError, UsageError } from "../../../core/domain/errors.js";
5
5
  import { RawReader } from "../../../core/domain/input-schema.js";
6
6
  import { message } from "../../../core/domain/message.js";
7
7
  import { byCodePoint } from "../../../core/domain/names.js";
8
- import { readTextFile } from "./text-files.js";
8
+ import { readTextFile } from "../../../core/application/text-files.js";
9
9
  export const compareSpec = {
10
10
  id: "files.compare",
11
11
  summary: "files.compare.summary",
@@ -3,7 +3,7 @@ import { done } from "../../../core/domain/command.js";
3
3
  import { NotFoundError } from "../../../core/domain/errors.js";
4
4
  import { RawReader } from "../../../core/domain/input-schema.js";
5
5
  import { globToRegExp } from "../../../core/domain/glob.js";
6
- import { parseExtensions } from "../domain/filters.js";
6
+ import { parseExtensions } from "../../../core/domain/filters.js";
7
7
  import { parseSize, parseTime } from "../domain/values.js";
8
8
  import { walk } from "../../../core/application/walk.js";
9
9
  export const FIND_TYPES = ["file", "dir"];
@@ -3,8 +3,8 @@ import { done } from "../../../core/domain/command.js";
3
3
  import { NotFoundError, OperationFailedError, UsageError } from "../../../core/domain/errors.js";
4
4
  import { RawReader } from "../../../core/domain/input-schema.js";
5
5
  import { formatBytes } from "../../../core/domain/values/bytes.js";
6
- import { MAX_TEXT_BYTES, splitLines } from "../domain/text-encoding.js";
7
- import { readTextFile } from "./text-files.js";
6
+ import { MAX_TEXT_BYTES, splitLines } from "../../../core/domain/text-encoding.js";
7
+ import { readTextFile } from "../../../core/application/text-files.js";
8
8
  export const readSpec = {
9
9
  id: "files.read",
10
10
  summary: "files.read.summary",
@@ -4,9 +4,9 @@ import { done, preview } from "../../../core/domain/command.js";
4
4
  import { KiriyaError, UsageError } from "../../../core/domain/errors.js";
5
5
  import { RawReader } from "../../../core/domain/input-schema.js";
6
6
  import { message } from "../../../core/domain/message.js";
7
- import { parseExtensions } from "../domain/filters.js";
8
- import { encodeText, escapeRegExp } from "../domain/text-encoding.js";
9
- import { readTextFile, selectFiles } from "./text-files.js";
7
+ import { parseExtensions } from "../../../core/domain/filters.js";
8
+ import { encodeText, escapeRegExp } from "../../../core/domain/text-encoding.js";
9
+ import { readTextFile, selectFiles } from "../../../core/application/text-files.js";
10
10
  /** Changed lines shown per file. */
11
11
  const SAMPLE_LINES = 3;
12
12
  export const replaceSpec = {
@@ -2,9 +2,9 @@ import { expandPaths } from "../../../core/application/paths.js";
2
2
  import { done } from "../../../core/domain/command.js";
3
3
  import { UsageError } from "../../../core/domain/errors.js";
4
4
  import { RawReader } from "../../../core/domain/input-schema.js";
5
- import { parseExtensions } from "../domain/filters.js";
6
- import { escapeRegExp } from "../domain/text-encoding.js";
7
- import { readTextFile, selectFiles } from "./text-files.js";
5
+ import { parseExtensions } from "../../../core/domain/filters.js";
6
+ import { escapeRegExp } from "../../../core/domain/text-encoding.js";
7
+ import { readTextFile, selectFiles } from "../../../core/application/text-files.js";
8
8
  /** Longer matching lines are cut here, so one minified file cannot flood the terminal. */
9
9
  const LINE_LIMIT = 240;
10
10
  export const grepSpec = {
@@ -0,0 +1,108 @@
1
+ import { DEPENDENCY_DIRECTORIES } from "../../../config/dependency-directories.js";
2
+ import { expandPaths } from "../../../core/application/paths.js";
3
+ import { readTextFile, selectFiles } from "../../../core/application/text-files.js";
4
+ import { done } from "../../../core/domain/command.js";
5
+ import { RawReader } from "../../../core/domain/input-schema.js";
6
+ import { message } from "../../../core/domain/message.js";
7
+ import { parseExtensions } from "../../../core/domain/filters.js";
8
+ import { findSecrets, redactSecret } from "../../../core/domain/secrets.js";
9
+ import { splitLines } from "../../../core/domain/text-encoding.js";
10
+ import { isAllowed } from "../domain/allow-marker.js";
11
+ export const scanSpec = {
12
+ id: "secrets.scan",
13
+ summary: "secrets.scan.summary",
14
+ examples: ["kiriya secrets scan", "kiriya secrets scan src --ext .ts", "kiriya secrets scan --json"],
15
+ safety: "read",
16
+ idempotent: true,
17
+ // Nothing is sent anywhere, which is most of why anyone would use it.
18
+ usesNetwork: false,
19
+ runsUserCommands: false,
20
+ // The findings point straight at secrets, as `proc list --full` and `clip paste` do.
21
+ sensitive: true,
22
+ input: {
23
+ positionals: [
24
+ { name: "paths", description: "secrets.scan.arg.paths", required: false, variadic: true, path: true },
25
+ ],
26
+ options: {
27
+ ext: { type: "string", description: "files.option.ext", valueName: "<extensions>", multiple: true },
28
+ name: { type: "string", description: "files.option.name", valueName: "<glob>" },
29
+ all: { type: "boolean", description: "secrets.scan.option.all" },
30
+ limit: { type: "string", description: "secrets.scan.option.limit", valueName: "<n>" },
31
+ },
32
+ parse(raw) {
33
+ const reader = new RawReader(raw);
34
+ return {
35
+ paths: reader.positionalsFrom(0),
36
+ extensions: parseExtensions(reader.strings("ext")),
37
+ name: reader.string("name"),
38
+ all: reader.flag("all"),
39
+ limit: reader.positiveInteger("limit", 500),
40
+ };
41
+ },
42
+ },
43
+ };
44
+ export class ScanSecrets {
45
+ fileSystem;
46
+ content;
47
+ spec = scanSpec;
48
+ constructor(fileSystem, content) {
49
+ this.fileSystem = fileSystem;
50
+ this.content = content;
51
+ }
52
+ async execute(input, context) {
53
+ const targets = input.paths.length === 0 ? ["."] : input.paths;
54
+ // Hidden files are where secrets live — .env, .npmrc, .git-credentials — so unlike the rest
55
+ // of kiriya this walks them by default, and keeps dependency folders out instead. --all
56
+ // drops that guard for someone who really does want to scan what they installed.
57
+ const roots = await expandPaths(this.fileSystem, targets, context.cwd, true);
58
+ const files = await selectFiles(this.fileSystem, roots, {
59
+ all: true,
60
+ extensions: input.extensions,
61
+ name: input.name,
62
+ ...(input.all ? {} : { skipDirectories: DEPENDENCY_DIRECTORIES }),
63
+ });
64
+ const findings = [];
65
+ let checked = 0;
66
+ let skipped = 0;
67
+ let allowed = 0;
68
+ let truncated = false;
69
+ for (const file of files) {
70
+ const text = await readTextFile(this.fileSystem, this.content, file);
71
+ if (text === null) {
72
+ skipped += 1;
73
+ continue;
74
+ }
75
+ checked += 1;
76
+ const lines = splitLines(text.text);
77
+ for (const [index, line] of lines.entries()) {
78
+ for (const match of findSecrets(line)) {
79
+ if (isAllowed(lines, index)) {
80
+ allowed += 1;
81
+ continue;
82
+ }
83
+ if (findings.length >= input.limit) {
84
+ truncated = true;
85
+ break;
86
+ }
87
+ findings.push({
88
+ path: file,
89
+ line: index + 1,
90
+ column: match.start + 1,
91
+ kind: match.kind,
92
+ sample: redactSecret(line, match),
93
+ });
94
+ }
95
+ if (truncated)
96
+ break;
97
+ }
98
+ if (truncated)
99
+ break;
100
+ }
101
+ const data = { findings, checked, skipped, allowed, truncated };
102
+ // Found means exit 1, so a hook or a CI step needs no output parsing, as files compare does.
103
+ if (findings.length === 0)
104
+ return done(data);
105
+ return done(data, { failures: [message("secrets.scan.found", { count: findings.length })] });
106
+ }
107
+ }
108
+ //# sourceMappingURL=scan-secrets.use-case.js.map
@@ -0,0 +1,23 @@
1
+ /**
2
+ * A line may say that the secret on it is deliberate, which test fixtures and documentation
3
+ * need: a scanner whose first run on its own repository is all false positives gets turned
4
+ * off and never run again. The marker sits next to what it excuses and shows up in review,
5
+ * which an ignore file in a corner does not.
6
+ */
7
+ const MARKER = /kiriya:allow-secret/;
8
+ /**
9
+ * Whether the secret on this line was declared deliberate: by a marker on the line, or on
10
+ * either neighbour.
11
+ *
12
+ * The line below counts because a formatter moves trailing comments. Prettier, which this
13
+ * repository runs, reflowed `for (… "-----BEGIN RSA PRIVATE KEY-----"]) { // kiriya:allow-secret`
14
+ * onto the next line and silently unmarked the fixture. A marker a formatter can move is no
15
+ * marker, so both neighbours count.
16
+ */
17
+ export function isAllowed(lines, index) {
18
+ return [index - 1, index, index + 1].some((at) => {
19
+ const line = lines[at];
20
+ return line !== undefined && MARKER.test(line);
21
+ });
22
+ }
23
+ //# sourceMappingURL=allow-marker.js.map
@@ -0,0 +1,29 @@
1
+ import { message } from "../../../core/domain/message.js";
2
+ /** Longer samples are cut here, so a minified line cannot flood the terminal. */
3
+ const SAMPLE_LIMIT = 60;
4
+ export const scanView = (output, format) => {
5
+ if (output.findings.length === 0) {
6
+ const clean = message("secrets.scan.clean", { checked: output.checked });
7
+ const lines = [format.green(format.text(clean))];
8
+ if (output.allowed > 0)
9
+ lines.push(format.dim(format.text(message("secrets.scan.allowed", { count: output.allowed }))));
10
+ return lines;
11
+ }
12
+ const kindWidth = Math.max(...output.findings.map((finding) => finding.kind.length));
13
+ const places = output.findings.map((finding) => `${format.path(finding.path)}:${finding.line}:${finding.column}`);
14
+ const placeWidth = Math.max(...places.map((place) => place.length));
15
+ const lines = output.findings.map((finding, index) => {
16
+ const place = (places[index] ?? "").padEnd(placeWidth);
17
+ const sample = finding.sample.length > SAMPLE_LIMIT ? `${finding.sample.slice(0, SAMPLE_LIMIT)}…` : finding.sample;
18
+ return ` ${format.yellow(place)} ${finding.kind.padEnd(kindWidth)} ${sample}`;
19
+ });
20
+ lines.push("", format.text(message("secrets.scan.counted", { count: output.findings.length, checked: output.checked })));
21
+ if (output.allowed > 0)
22
+ lines.push(format.dim(format.text(message("secrets.scan.allowed", { count: output.allowed }))));
23
+ if (output.truncated)
24
+ lines.push(format.dim(format.text(message("secrets.scan.truncated"))));
25
+ // Say how to excuse one, because a fixture flagged with no way out is why scanners get turned off.
26
+ lines.push(format.dim(format.text(message("secrets.scan.how-allow"))));
27
+ return lines;
28
+ };
29
+ //# sourceMappingURL=secrets.view.js.map
@@ -0,0 +1,13 @@
1
+ import { ScanSecrets } from "./application/scan-secrets.use-case.js";
2
+ import { scanView } from "./presentation/secrets.view.js";
3
+ export const secretsModule = {
4
+ id: "secrets",
5
+ summary: "secrets.summary",
6
+ about: "secrets.about",
7
+ examples: ["kiriya secrets scan", "kiriya secrets scan src --ext ts,js", "kiriya secrets scan --json"],
8
+ guide: "https://github.com/SatPaingOo/kiriya/blob/main/docs/modules/secrets.md",
9
+ register(registrar, ports) {
10
+ registrar.add(new ScanSecrets(ports.fileSystem, ports.fileContent), scanView);
11
+ },
12
+ };
13
+ //# sourceMappingURL=secrets.module.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiriya",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "One command-line toolbox for everyday developer work that behaves the same on Windows, Linux and macOS.",
5
5
  "mcpName": "io.github.SatPaingOo/kiriya",
6
6
  "license": "MIT",