@pho9ubenaa/siro 0.1.3 → 0.3.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
@@ -1,5 +1,41 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0]
4
+
5
+ ### Features
6
+
7
+ - **Project policy selection**: `--project-type application|package`, `SiroConfig.projectType`, and `LintOptions.projectType` distinguish dependency-consuming applications from published packages. Omit the selection to infer policy per package-manager binding.
8
+ - **Scoped custom rules**: `Rule.projectTypes` follows explicit or inferred policy for npm-family and Deno bindings.
9
+
10
+ ### Fixes
11
+
12
+ - Programmatic `lintCommand` calls reject unsupported project type, package manager, and severity values instead of returning misleading results.
13
+ - Malformed custom rules from config files or untyped programmatic callers are rejected before rule merging and evaluation.
14
+ - Empty and whitespace-only package names no longer activate published-package policy.
15
+ - Unscoped Deno rules no longer read `deno.json` solely for project-type inference.
16
+ - The published `package.json#bin` preserves exit code 70 when a reporter or custom rule crashes, and subprocess tests now execute that declared bin directly.
17
+ - Malformed or cyclic YAML configurations are rejected, while TOML configurations with a leading UTF-8 BOM are accepted.
18
+ - Deno release-age validation accepts documented default forms and rejects invalid, partial, or zero ages.
19
+ - Repeated reporter selectors and non-boolean Yarn strict SSL values are rejected as invalid input.
20
+ - Findings without automatic remediation operations no longer report themselves as fixable.
21
+
22
+ ## [0.2.0]
23
+
24
+ ### Breaking Changes
25
+
26
+ - **Node.js `^22.18.0 || ^23.6.0 || >=24.0.0` required** (was `>=20`): `siro.config.ts` now loads via Node's native type stripping.
27
+
28
+ ### Features
29
+
30
+ - **OSS benchmarks module**: coverage validation against 47 leading OSS projects across all 6 package managers. Includes sparse-checkout clone script, snapshot-based CI mode, and local clone-based checking.
31
+ - **jiti dependency removed**: runtime dependencies drop from 7 to 6. `.ts` configs load via native type stripping, and every `loadConfig` call now truly re-reads the config from disk (jiti's transform cache could replay a same-second rewrite).
32
+ - **Unminified dist**: the published bundle ships as readable JavaScript for supply-chain transparency.
33
+
34
+ ### Fixes
35
+
36
+ - A `.ts` config on a runtime without type stripping fails with an actionable `ConfigError` instead of a raw `ERR_UNKNOWN_FILE_EXTENSION`.
37
+ - `pnpm gen:rule` works again — the scaffolder's marker had drifted from `rule-id.ts`.
38
+
3
39
  ## [0.1.3]
4
40
 
5
41
  Initial public release. 27 rules across 6 package managers (npm, pnpm, yarn, bun, deno, aube).
package/README.md CHANGED
@@ -31,7 +31,7 @@ npx @pho9ubenaa/siro lint --reporter json # machine-readable output with fix ops
31
31
  - **PM-aware severities.** When a manager's documented default already satisfies a rule (e.g.
32
32
  Yarn's `enableScripts: false`, aube's `preferFrozenLockfile: true`), the finding is demoted
33
33
  to `info` so CI noise stays proportional to real risk.
34
- - **Machine-readable remediation.** Every finding carries `fix` operations and `manualSteps` in the
34
+ - **Machine-readable remediation.** Every finding carries `fix` operations or `manualSteps` in the
35
35
  JSON output — hand it to an agent skill or editor plugin that edits the files and re-runs
36
36
  `siro lint` until it exits `0`. See [JSON output](docs/json-output.md).
37
37
  - **Lint with severities.** `error` fails CI by default; `--severity warn` tightens the gate.
@@ -62,6 +62,7 @@ that backs those suffixes.
62
62
  siro <lint|check> [path] [options]
63
63
 
64
64
  --pm <npm|pnpm|yarn|bun|deno|aube> Target a specific package manager (auto-detected; required if detection finds nothing)
65
+ --project-type <application|package> Select application or published-package policy (default auto)
65
66
  --reporter <pretty|json|github> Output format (default pretty)
66
67
  --severity <error|warn|info> Show and fail on findings at or above this level
67
68
  --json Shortcut for --reporter json
@@ -70,7 +71,7 @@ siro <lint|check> [path] [options]
70
71
 
71
72
  `check` is an alias of `lint` (same flags, same exit codes) — provided so `siro check` reads naturally in CI scripts.
72
73
 
73
- Exit codes: `0` clean · `1` findings at/above threshold · `2` usage error.
74
+ Exit codes: `0` clean · `1` findings at/above threshold · `2` usage error · `70` uncaught exception (a siro bug or a throwing reporter/custom rule).
74
75
 
75
76
  ## How is this different from `npm audit` / `osv-scanner`?
76
77
 
package/dist/cli.js CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { run } from './cli.mjs';
2
+ import { runMain } from './cli.mjs';
3
3
 
4
- const code = await run(process.argv.slice(2));
5
- process.exitCode = code;
4
+ await runMain(process.argv.slice(2));
package/dist/cli.mjs CHANGED
@@ -1,5 +1,430 @@
1
1
  #!/usr/bin/env node
2
- import{C as e,E as t,O as n,S as r,k as i,m as a,n as o,p as s,t as c,v as l,w as u,x as d}from"./node-io-BLwef7p6.mjs";import ee from"node:path";import{cac as f}from"cac";import{pathToFileURL as p}from"node:url";const m=[`lint`,`check`],h=e=>m.some(t=>t===e),g=new Set(Object.entries({json:{takesValue:!1},pm:{takesValue:!0},reporter:{takesValue:!0},severity:{takesValue:!0}}).filter(([,e])=>e.takesValue).map(([e])=>e)),_=[`pm`,`reporter`,`json`,`severity`],te={check:_,lint:_},ne=[`version`,`help`],v=[`v`,`h`,`--`],y=e=>new Set([...te[e],...ne,...v]),b=(()=>{let e=[];for(let t of m)for(let n of y(t))e.push(n);return new Set(e)})(),x=(e,t)=>e.startsWith(`--`)&&!e.includes(`=`)&&g.has(e.slice(2))&&t!==void 0&&!t.startsWith(`-`),S=e=>e===`--help`||e===`-h`,C=(e,t,n)=>e===`--`?`break`:(S(e)?n.asksHelp=!0:x(e,t)?n.index+=1:!e.startsWith(`-`)&&n.first===``&&(n.first=e),`continue`),w=e=>{let t={asksHelp:!1,first:``,index:0};for(;t.index<e.length;t.index+=1){let n=e[t.index];if(typeof n==`string`&&C(n,e[t.index+1],t)===`break`)break}return{asksHelp:t.asksHelp,first:t.first||[].pop()}},T=e=>{if(e!==void 0&&h(e))return e},E=e=>{let{asksHelp:t,first:n}=w(e);return t?{target:T(n)}:[].pop()},D=e=>{for(let t=0;t<e.length;t+=1){let n=e[t];if(typeof n==`string`){if(n===`--`)return!1;if(n===`--version`||n===`-v`)return!0;x(n,e[t+1])&&(t+=1)}}return!1},O=(e,n,r)=>{for(let i of Object.keys(e))if(!n.has(i)){let e=``;throw r&&(e=` for '${r}'`),new t(`Unknown flag${e}: --${i}`)}},k=n=>{if(n!==void 0){if(n===!0)throw new t(`--pm requires a value (expected one of: ${d.join(`, `)})`);if(typeof n!=`string`||!e(n))throw new t(`Unknown package manager: ${n} (expected one of: ${d.join(`, `)})`);return n}},A=e=>{if(e!==void 0){if(e===!0)throw new t(`--severity requires a value (expected ${r.join(`|`)})`);if(typeof e!=`string`||!u(e))throw new t(`Invalid severity: ${String(e)} (expected ${r.join(`|`)})`);return e}},j=e=>{if(e.reporter===!0)throw new t(`--reporter requires a value (expected one of: ${a.join(`, `)}, or a reporter registered via siro.config.ts)`);if(typeof e.reporter==`string`){if(e.json===!0)throw new t(`Use either --reporter or --json, not both.`);return e.reporter}return e.json===!0?`json`:`pretty`},M=e=>{let n=Number(e.replace(/^v/u,``).split(`.`)[0]);if(Number.isFinite(n)&&n<20)throw new t(`Node 20+ required (you are on ${e}). Upgrade your runtime to use siro.`)},N=()=>{let e=f(`siro`);e.option(`--pm <name>`,`Target a specific package manager`).option(`--reporter <name>`,`Reporter (${a.join(`|`)})`).option(`--json`,`Shortcut for --reporter json`).option(`--severity <level>`,`Display/fail threshold (${r.join(`|`)})`).help().version(l);for(let t of m){let n=`${t} a repository`;t===`check`&&(n=`Alias of lint`),e.command(`${t} [cwd]`,n)}return e},P=(e,t)=>e?t[0]:t[1],F=(e,t)=>e||t[0],I=(e,t)=>e?t.slice(1):t.slice(2),L=e=>{let t=N(),n=t.parse([`node`,`siro`,...e],{run:!1}),r=n.options,i=t.matchedCommandName,a=P(i,n.args);return{commandCandidate:F(i,n.args),extraPositionals:I(i,n.args),flags:r,matchedCommand:i,positionalCwd:a}},re=d.join(`|`),R=a.join(`|`),z=r.join(`|`),B={json:` --json Shortcut for --reporter json`,pm:` --pm <name> Target a specific package manager (${re})`,reporter:` --reporter <name> Reporter (${R}; additional reporters can be registered via siro.config.ts)`,severity:` --severity <level> Show + fail on findings at or above this level (${z})`},V=[`siro — security best-practices for the npm ecosystem`,``,`USAGE`,` siro <command> [path] [flags]`,``,`COMMANDS`,` lint Report best-practice violations (alias: check)`,``,`GLOBAL FLAGS`,B.pm,` --version Print the siro version`,` --help Show help for siro or a command`,``,`LINT FLAGS`,B.reporter,B.json,B.severity,``,`EXAMPLES`,` $ siro lint # report violations in cwd`,` $ siro lint --reporter github # GitHub Actions annotations`,` $ siro lint --severity warn # also fail on warnings`,``,`LEARN MORE`,` Quickstart https://github.com/pHo9UBenaA/siro/blob/main/docs/getting-started.md`,` Rule reference https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md`,` Config options https://github.com/pHo9UBenaA/siro/blob/main/docs/configuration.md`].join(`
3
- `),H=[`siro lint — report best-practice violations`,``,`USAGE`,` siro lint [path] [flags]`,` siro check [path] [flags] (alias)`,``,`FLAGS`,B.pm,B.reporter,B.json,B.severity,``,`EXIT CODES`,` 0 No findings at or above the active threshold`,` 1 Findings at or above the threshold (default: error)`,` 2 Usage error (bad flag, broken siro.config.ts, unreadable path, …)`,` 70 Uncaught exception (a siro bug, or a reporter / custom rule that threw)`,``,`EXAMPLES`,` $ siro lint # default: pretty reporter, fail on errors`,` $ siro lint --reporter github # GitHub Actions annotations`,` $ siro lint --severity warn # tighten the gate`].join(`
4
- `),U=e=>e===void 0?V:e===`lint`||e===`check`?H:e,W=e=>{let n=e[`--`];if(Array.isArray(n)&&n.length>0)throw new t("siro takes no passthrough arguments after `--`.")},G=e=>e===void 0?{kind:`usage`}:e===`init`?{kind:`usage`,reason:"The 'init' command was removed: siro is lint-only. Run `siro lint --reporter json` and apply each finding's `fix` operations with your editor or an agent skill."}:typeof e!=`string`||!h(e)?{kind:`usage`,reason:`Unknown command: ${String(e)}`}:{kind:`command`,name:e},K=e=>{if(e.length>0){let n=``;throw e.length>1&&(n=`s`),new t(`Unexpected extra argument${n}: ${e.join(` `)}`)}},q=e=>{let t=process.cwd();return typeof e==`string`&&(t=e),i(ee.resolve(t))},J=e=>{let t=E(e);if(t!==void 0)return{kind:`help`,target:t.target};if(D(e))return{kind:`version`}},Y=(e,t,n)=>(O(e,y(n),n),{cwd:q(t),kind:`lint`,pm:k(e.pm),reporter:j(e),severity:A(e.severity)}),X=e=>{O(e,b),W(e)},ie=e=>{let t=J(e);if(t!==void 0)return t;let{commandCandidate:n,extraPositionals:r,flags:i,positionalCwd:a}=L(e);X(i);let o=G(n);return o.kind===`usage`?o:(K(r),Y(i,a,o.name))},ae=e=>(e.stdout(l),Promise.resolve(0)),oe=(e,t)=>(t.stdout(U(e)),Promise.resolve(0)),se=(e,t)=>(e!==void 0&&t.stderr(`${e}\n`),t.stderr(U()),Promise.resolve(2)),ce=(e,t)=>{switch(e.kind){case`version`:return ae(t);case`help`:return oe(e.target,t);case`usage`:return se(e.reason,t);case`lint`:return o(e,t);default:throw Error(`Unhandled command kind: ${String(e)}`)}},Z=(e,t)=>{if(e instanceof n)return t.stderr(e.message),e.exitCode;if(s(e)&&typeof e.errno==`number`)return t.stderr(`File system error: ${e.message}`),2;throw e},le=(e,t)=>e instanceof n?(t.stderr(e.message),Promise.resolve(e.exitCode)):s(e)&&typeof e.errno==`number`?(t.stderr(`File system error: ${e.message}`),Promise.resolve(2)):Promise.reject(e),Q=(e,t=c)=>{try{return M(process.versions.node),ce(ie(e),t).catch(e=>Z(e,t))}catch(e){return le(e,t)}},ue=e=>{process.exitCode=e},de=e=>{let t=String(e);e instanceof Error&&(t=e.stack??e.message),process.stderr.write(`${t}\n`),process.exitCode=70},[,$]=process.argv;if($&&import.meta.url===p($).href)try{ue(await Q(process.argv.slice(2)))}catch(e){de(e)}export{Q as run};
2
+ import { A as isSeverity, D as PMS, F as asAbsPath, M as UsageError, O as SEVERITIES, P as SiroError, _ as isSupportedNodeVersion, b as JSON_REPORTER_NAME, g as SUPPORTED_NODE_RANGE, h as isProjectType, k as isPM, m as PROJECT_TYPES, n as lintCommand, p as isNodeError, t as nodeIO, v as BUILTIN_REPORTER_NAMES, w as version, y as DEFAULT_REPORTER_NAME } from "./node-io-CUcMwlGH.mjs";
3
+ import path from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { cac } from "cac";
6
+
7
+ //#region src/cli/commands.ts
8
+ /**
9
+ * Canonical list of siro CLI sub-commands. Shared between `cli.ts` (the
10
+ * dispatcher / arg parser) and `cli/help.ts` (the help-text renderer) so a
11
+ * new command added here propagates to both via the derived `CommandName`
12
+ * union — there is no second place to remember to update.
13
+ */
14
+ const COMMANDS = ["lint", "check"];
15
+ const isCommandName = (value) => COMMANDS.some((cmd) => cmd === value);
16
+
17
+ //#endregion
18
+ //#region src/cli/flags.ts
19
+ const ARGS_OFFSET$1 = 2;
20
+ const FLAGS = {
21
+ json: { takesValue: false },
22
+ pm: { takesValue: true },
23
+ projectType: {
24
+ cliName: "project-type",
25
+ takesValue: true
26
+ },
27
+ reporter: { takesValue: true },
28
+ severity: { takesValue: true }
29
+ };
30
+ const flagCliName = (name, meta) => {
31
+ if (typeof meta.cliName !== "undefined") return meta.cliName;
32
+ return name;
33
+ };
34
+ /** Flags that consume the following argv token; pre-cac scanners skip that token. */
35
+ const VALUE_FLAGS = new Set(Object.entries(FLAGS).filter(([, meta]) => meta.takesValue).flatMap(([name, meta]) => [name, flagCliName(name, meta)]));
36
+ const LINT_FLAGS = [
37
+ "pm",
38
+ "projectType",
39
+ "reporter",
40
+ "json",
41
+ "severity"
42
+ ];
43
+ const FLAG_SCOPES = {
44
+ check: LINT_FLAGS,
45
+ lint: LINT_FLAGS
46
+ };
47
+ const GLOBAL_FLAGS = ["version", "help"];
48
+ const CAC_KEYS = [
49
+ "v",
50
+ "h",
51
+ "--"
52
+ ];
53
+ const flagsFor = (command) => new Set([
54
+ ...FLAG_SCOPES[command],
55
+ ...GLOBAL_FLAGS,
56
+ ...CAC_KEYS
57
+ ]);
58
+ const collectAllFlags = () => {
59
+ const all = [];
60
+ for (const command of COMMANDS) for (const flag of flagsFor(command)) all.push(flag);
61
+ return new Set(all);
62
+ };
63
+ const KNOWN_FLAGS = collectAllFlags();
64
+ /**
65
+ * Whether a `--name value` flag consumes the following argv token. Only the
66
+ * separate-token form of a value-taking flag does (`--name=value` carries its
67
+ * own value), AND only when the next token isn't itself a `-`-prefixed flag —
68
+ * so `--reporter --help` leaves `--help` for the help/version detectors rather
69
+ * than eating it as the reporter value. Shared by both pre-cac scanners so the
70
+ * skip predicate stays in one place.
71
+ */
72
+ const consumesNext = (token, next) => token.startsWith("--") && !token.includes("=") && VALUE_FLAGS.has(token.slice(ARGS_OFFSET$1)) && typeof next !== "undefined" && !next.startsWith("-");
73
+
74
+ //#endregion
75
+ //#region src/cli/scanners.ts
76
+ const STEP$1 = 1;
77
+ const isHelpFlag = (token) => token === "--help" || token === "-h";
78
+ const processHelpToken = (token, next, state) => {
79
+ if (token === "--") return "break";
80
+ if (isHelpFlag(token)) state.asksHelp = true;
81
+ else if (consumesNext(token, next)) state.index += STEP$1;
82
+ else if (!token.startsWith("-") && state.first === "") state.first = token;
83
+ return "continue";
84
+ };
85
+ const scanHelpTokens = (argv) => {
86
+ const state = {
87
+ asksHelp: false,
88
+ first: "",
89
+ index: 0
90
+ };
91
+ for (; state.index < argv.length; state.index += STEP$1) {
92
+ const token = argv[state.index];
93
+ if (typeof token === "string" && processHelpToken(token, argv[state.index + STEP$1], state) === "break") break;
94
+ }
95
+ return {
96
+ asksHelp: state.asksHelp,
97
+ first: state.first || void 0
98
+ };
99
+ };
100
+ const resolveTarget = (first) => {
101
+ if (typeof first !== "undefined" && isCommandName(first)) return first;
102
+ };
103
+ /**
104
+ * Detect a --help / -h flag in argv before cac sees it (cac would otherwise
105
+ * write to console.log itself). Returns the target command (when the first
106
+ * positional names one) or 'root' for the top-level help.
107
+ */
108
+ const detectHelpFlag = (argv) => {
109
+ const { asksHelp, first } = scanHelpTokens(argv);
110
+ if (!asksHelp) return;
111
+ return { target: resolveTarget(first) };
112
+ };
113
+ /**
114
+ * Detect `--version` / `-v` ahead of cac for the same reason `detectHelpFlag`
115
+ * exists. The `--` separator stops the scan so a trailing `--version` is left
116
+ * for the (rejected) passthrough handler rather than flipping to the version
117
+ * branch — siro forwards to no wrapped tool, so non-empty passthrough is a
118
+ * usage error (see parseArgs).
119
+ */
120
+ const detectVersionFlag = (argv) => {
121
+ for (let index = 0; index < argv.length; index += STEP$1) {
122
+ const token = argv[index];
123
+ if (typeof token === "string") {
124
+ if (token === "--") return false;
125
+ if (token === "--version" || token === "-v") return true;
126
+ if (consumesNext(token, argv[index + STEP$1])) index += STEP$1;
127
+ }
128
+ }
129
+ return false;
130
+ };
131
+
132
+ //#endregion
133
+ //#region src/cli/parsers.ts
134
+ const rejectUnknownFlags = (flags, allowed, scope) => {
135
+ for (const key of Object.keys(flags)) if (!allowed.has(key)) {
136
+ let where = "";
137
+ if (scope) where = ` for '${scope}'`;
138
+ throw new UsageError(`Unknown flag${where}: --${key}`);
139
+ }
140
+ };
141
+ const parsePmFlag = (raw) => {
142
+ if (typeof raw === "undefined") return;
143
+ if (raw === true) throw new UsageError(`--pm requires a value (expected one of: ${PMS.join(", ")})`);
144
+ if (typeof raw !== "string" || !isPM(raw)) throw new UsageError(`Unknown package manager: ${raw} (expected one of: ${PMS.join(", ")})`);
145
+ return raw;
146
+ };
147
+ const parseProjectTypeFlag = (raw) => {
148
+ if (typeof raw === "undefined") return;
149
+ if (raw === true) throw new UsageError(`--project-type requires a value (expected ${PROJECT_TYPES.join("|")})`);
150
+ if (typeof raw === "string" && isProjectType(raw)) return raw;
151
+ throw new UsageError(`Unknown project type: ${String(raw)} (expected ${PROJECT_TYPES.join("|")})`);
152
+ };
153
+ const parseSeverityFlag = (raw) => {
154
+ if (typeof raw === "undefined") return;
155
+ if (raw === true) throw new UsageError(`--severity requires a value (expected ${SEVERITIES.join("|")})`);
156
+ if (typeof raw !== "string" || !isSeverity(raw)) throw new UsageError(`Invalid severity: ${String(raw)} (expected ${SEVERITIES.join("|")})`);
157
+ return raw;
158
+ };
159
+ const resolveReporter = (flags) => {
160
+ if (flags.reporter === true) throw new UsageError(`--reporter requires a value (expected one of: ${BUILTIN_REPORTER_NAMES.join(", ")}, or a reporter registered via siro.config.ts)`);
161
+ if (Array.isArray(flags.reporter) || Array.isArray(flags.json) || typeof flags.reporter === "string" && flags.json === true) throw new UsageError("Invalid reporter selection: use either --reporter or --json, and specify it only once.");
162
+ if (typeof flags.reporter === "string") return flags.reporter;
163
+ if (flags.json === true) return JSON_REPORTER_NAME;
164
+ return DEFAULT_REPORTER_NAME;
165
+ };
166
+ const ensureNodeVersion = (nodeVersion) => {
167
+ if (!isSupportedNodeVersion(nodeVersion)) throw new UsageError(`Node ${SUPPORTED_NODE_RANGE} required (you are on ${nodeVersion}). Upgrade your runtime to use siro.`);
168
+ };
169
+
170
+ //#endregion
171
+ //#region src/cli/cac-bridge.ts
172
+ const STEP = 1;
173
+ const ARGS_OFFSET = 2;
174
+ const buildCli = () => {
175
+ const cli = cac("siro");
176
+ cli.option("--pm <name>", "Target a specific package manager").option("--project-type <type>", "Project type (application|package)").option("--reporter <name>", `Reporter (${BUILTIN_REPORTER_NAMES.join("|")})`).option("--json", "Shortcut for --reporter json").option("--severity <level>", `Display/fail threshold (${SEVERITIES.join("|")})`).help().version(version);
177
+ for (const name of COMMANDS) {
178
+ let desc = `${name} a repository`;
179
+ if (name === "check") desc = "Alias of lint";
180
+ cli.command(`${name} [cwd]`, desc);
181
+ }
182
+ return cli;
183
+ };
184
+ const extractPositionalCwd = (matchedCommand, args) => {
185
+ if (matchedCommand) {
186
+ const [cwd] = args;
187
+ return cwd;
188
+ }
189
+ return args[STEP];
190
+ };
191
+ const extractCommandCandidate = (matchedCommand, args) => {
192
+ if (matchedCommand) return matchedCommand;
193
+ const [candidate] = args;
194
+ return candidate;
195
+ };
196
+ const extractExtraPositionals = (matchedCommand, args) => {
197
+ if (matchedCommand) return args.slice(STEP);
198
+ return args.slice(ARGS_OFFSET);
199
+ };
200
+ const parseCacOutput = (argv) => {
201
+ const cli = buildCli();
202
+ const parsed = cli.parse([
203
+ "node",
204
+ "siro",
205
+ ...argv
206
+ ], { run: false });
207
+ const flags = parsed.options;
208
+ const matchedCommand = cli.matchedCommandName;
209
+ const positionalCwd = extractPositionalCwd(matchedCommand, parsed.args);
210
+ return {
211
+ commandCandidate: extractCommandCandidate(matchedCommand, parsed.args),
212
+ extraPositionals: extractExtraPositionals(matchedCommand, parsed.args),
213
+ flags,
214
+ positionalCwd
215
+ };
216
+ };
217
+
218
+ //#endregion
219
+ //#region src/cli/help.ts
220
+ const PMS_LIST = PMS.join("|");
221
+ const REPORTERS_LIST = BUILTIN_REPORTER_NAMES.join("|");
222
+ const PROJECT_TYPES_LIST = PROJECT_TYPES.join("|");
223
+ const SEVERITIES_LIST = SEVERITIES.join("|");
224
+ const FLAG_LINES = {
225
+ json: " --json Shortcut for --reporter json",
226
+ pm: ` --pm <name> Target a specific package manager (${PMS_LIST})`,
227
+ projectType: ` --project-type <type> Project type (${PROJECT_TYPES_LIST}; default auto)`,
228
+ reporter: ` --reporter <name> Reporter (${REPORTERS_LIST}; additional reporters can be registered via siro.config.ts)`,
229
+ severity: ` --severity <level> Show + fail on findings at or above this level (${SEVERITIES_LIST})`
230
+ };
231
+ const HELP_ROOT = [
232
+ "siro — security best-practices for the npm ecosystem",
233
+ "",
234
+ "USAGE",
235
+ " siro <command> [path] [flags]",
236
+ "",
237
+ "COMMANDS",
238
+ " lint Report best-practice violations (alias: check)",
239
+ "",
240
+ "GLOBAL FLAGS",
241
+ FLAG_LINES.pm,
242
+ FLAG_LINES.projectType,
243
+ " --version Print the siro version",
244
+ " --help Show help for siro or a command",
245
+ "",
246
+ "LINT FLAGS",
247
+ FLAG_LINES.reporter,
248
+ FLAG_LINES.json,
249
+ FLAG_LINES.severity,
250
+ "",
251
+ "EXAMPLES",
252
+ " $ siro lint # report violations in cwd",
253
+ " $ siro lint --reporter github # GitHub Actions annotations",
254
+ " $ siro lint --severity warn # also fail on warnings",
255
+ "",
256
+ "LEARN MORE",
257
+ " Quickstart https://github.com/pHo9UBenaA/siro/blob/main/docs/getting-started.md",
258
+ " Rule reference https://github.com/pHo9UBenaA/siro/blob/main/docs/rules.md",
259
+ " Config options https://github.com/pHo9UBenaA/siro/blob/main/docs/configuration.md"
260
+ ].join("\n");
261
+ const HELP_LINT = [
262
+ "siro lint — report best-practice violations",
263
+ "",
264
+ "USAGE",
265
+ " siro lint [path] [flags]",
266
+ " siro check [path] [flags] (alias)",
267
+ "",
268
+ "FLAGS",
269
+ FLAG_LINES.pm,
270
+ FLAG_LINES.projectType,
271
+ FLAG_LINES.reporter,
272
+ FLAG_LINES.json,
273
+ FLAG_LINES.severity,
274
+ "",
275
+ "EXIT CODES",
276
+ " 0 No findings at or above the active threshold",
277
+ " 1 Findings at or above the threshold (default: error)",
278
+ " 2 Usage error (bad flag, broken siro.config.ts, unreadable path, …)",
279
+ " 70 Uncaught exception (a siro bug, or a reporter / custom rule that threw)",
280
+ "",
281
+ "EXAMPLES",
282
+ " $ siro lint # default: pretty reporter, fail on errors",
283
+ " $ siro lint --reporter github # GitHub Actions annotations",
284
+ " $ siro lint --severity warn # tighten the gate"
285
+ ].join("\n");
286
+ const renderHelp = (target) => {
287
+ if (typeof target === "undefined") return HELP_ROOT;
288
+ if (target === "lint" || target === "check") return HELP_LINT;
289
+ return target;
290
+ };
291
+
292
+ //#endregion
293
+ //#region src/cli.ts
294
+ const EXIT_SUCCESS = 0;
295
+ const EXIT_USAGE = 2;
296
+ const EXIT_CRASH = 70;
297
+ const rejectPassthrough = (flags) => {
298
+ const passthrough = flags["--"];
299
+ if (Array.isArray(passthrough) && passthrough.length > 0) throw new UsageError("siro takes no passthrough arguments after `--`.");
300
+ };
301
+ const resolveCommandCandidate = (commandCandidate) => {
302
+ if (typeof commandCandidate === "undefined") return { kind: "usage" };
303
+ if (commandCandidate === "init") return {
304
+ kind: "usage",
305
+ reason: "The 'init' command was removed: siro is lint-only. Run `siro lint --reporter json` and apply each finding's `fix` operations with your editor or an agent skill."
306
+ };
307
+ if (typeof commandCandidate !== "string" || !isCommandName(commandCandidate)) return {
308
+ kind: "usage",
309
+ reason: `Unknown command: ${String(commandCandidate)}`
310
+ };
311
+ return {
312
+ kind: "command",
313
+ name: commandCandidate
314
+ };
315
+ };
316
+ const SINGLE = 1;
317
+ const rejectExtraPositionals = (extraPositionals) => {
318
+ if (extraPositionals.length > 0) {
319
+ let plural = "";
320
+ if (extraPositionals.length > SINGLE) plural = "s";
321
+ throw new UsageError(`Unexpected extra argument${plural}: ${extraPositionals.join(" ")}`);
322
+ }
323
+ };
324
+ const resolveCwd = (positionalCwd) => {
325
+ let raw = process.cwd();
326
+ if (typeof positionalCwd === "string") raw = positionalCwd;
327
+ return asAbsPath(path.resolve(raw));
328
+ };
329
+ const checkPreScanFlags = (argv) => {
330
+ const preHelp = detectHelpFlag(argv);
331
+ if (typeof preHelp !== "undefined") return {
332
+ kind: "help",
333
+ target: preHelp.target
334
+ };
335
+ if (detectVersionFlag(argv)) return { kind: "version" };
336
+ };
337
+ const buildLintCommand = (flags, positionalCwd, commandCandidate) => {
338
+ rejectUnknownFlags(flags, flagsFor(commandCandidate), commandCandidate);
339
+ return {
340
+ cwd: resolveCwd(positionalCwd),
341
+ kind: "lint",
342
+ pm: parsePmFlag(flags.pm),
343
+ projectType: parseProjectTypeFlag(flags.projectType),
344
+ reporter: resolveReporter(flags),
345
+ severity: parseSeverityFlag(flags.severity)
346
+ };
347
+ };
348
+ const validateFlags = (flags) => {
349
+ rejectUnknownFlags(flags, KNOWN_FLAGS);
350
+ rejectPassthrough(flags);
351
+ };
352
+ const parseArgs = (argv) => {
353
+ const preScan = checkPreScanFlags(argv);
354
+ if (typeof preScan !== "undefined") return preScan;
355
+ const { commandCandidate, extraPositionals, flags, positionalCwd } = parseCacOutput(argv);
356
+ validateFlags(flags);
357
+ const resolved = resolveCommandCandidate(commandCandidate);
358
+ if (resolved.kind === "usage") return resolved;
359
+ rejectExtraPositionals(extraPositionals);
360
+ return buildLintCommand(flags, positionalCwd, resolved.name);
361
+ };
362
+ const handleVersion = (io) => {
363
+ io.stdout(version);
364
+ return Promise.resolve(EXIT_SUCCESS);
365
+ };
366
+ const handleHelp = (target, io) => {
367
+ io.stdout(renderHelp(target));
368
+ return Promise.resolve(EXIT_SUCCESS);
369
+ };
370
+ const handleUsage = (reason, io) => {
371
+ if (typeof reason !== "undefined") io.stderr(`${reason}\n`);
372
+ io.stderr(renderHelp());
373
+ return Promise.resolve(EXIT_USAGE);
374
+ };
375
+ const dispatch = (cmd, io) => {
376
+ switch (cmd.kind) {
377
+ case "version": return handleVersion(io);
378
+ case "help": return handleHelp(cmd.target, io);
379
+ case "usage": return handleUsage(cmd.reason, io);
380
+ case "lint": return lintCommand(cmd, io);
381
+ default: throw new Error(`Unhandled command kind: ${String(cmd)}`);
382
+ }
383
+ };
384
+ const handleAsyncError = (error, io) => {
385
+ if (error instanceof SiroError) {
386
+ io.stderr(error.message);
387
+ return error.exitCode;
388
+ }
389
+ if (isNodeError(error) && typeof error.errno === "number") {
390
+ io.stderr(`File system error: ${error.message}`);
391
+ return EXIT_USAGE;
392
+ }
393
+ throw error;
394
+ };
395
+ const handleSyncError = (error, io) => {
396
+ if (error instanceof SiroError) {
397
+ io.stderr(error.message);
398
+ return Promise.resolve(error.exitCode);
399
+ }
400
+ if (isNodeError(error) && typeof error.errno === "number") {
401
+ io.stderr(`File system error: ${error.message}`);
402
+ return Promise.resolve(EXIT_USAGE);
403
+ }
404
+ return Promise.reject(error);
405
+ };
406
+ const run = (argv, io = nodeIO) => {
407
+ try {
408
+ ensureNodeVersion(process.versions.node);
409
+ return dispatch(parseArgs(argv), io).catch((error) => handleAsyncError(error, io));
410
+ } catch (error) {
411
+ return handleSyncError(error, io);
412
+ }
413
+ };
414
+ const onRunFulfilled = (code) => {
415
+ process.exitCode = code;
416
+ };
417
+ const onRunRejected = (error) => {
418
+ let errStr = String(error);
419
+ if (error instanceof Error) errStr = error.stack ?? error.message;
420
+ process.stderr.write(`${errStr}\n`);
421
+ process.exitCode = EXIT_CRASH;
422
+ };
423
+ const runMain = (argv) => run(argv).then(onRunFulfilled, onRunRejected);
424
+ const ARGV_SKIP = 2;
425
+ const [, invokedPath] = process.argv;
426
+ if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) await runMain(process.argv.slice(ARGV_SKIP));
427
+
428
+ //#endregion
429
+ export { run, runMain };
5
430
  //# sourceMappingURL=cli.mjs.map