redlinegate 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +188 -0
- package/commands/redline-init.md +54 -0
- package/commands/redline-verify.md +69 -0
- package/dist/bin/redline.js +605 -0
- package/dist/bin/redline.js.map +1 -0
- package/dist/commands/exempt.js +45 -0
- package/dist/commands/exempt.js.map +1 -0
- package/dist/commands/init.js +615 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/policy.js +25 -0
- package/dist/commands/policy.js.map +1 -0
- package/dist/commands/remove.js +427 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/review.js +72 -0
- package/dist/commands/review.js.map +1 -0
- package/dist/commands/sync.js +42 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/verify.js +478 -0
- package/dist/commands/verify.js.map +1 -0
- package/dist/config/redline-json.js +187 -0
- package/dist/config/redline-json.js.map +1 -0
- package/dist/core/errors.js +25 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/git.js +152 -0
- package/dist/core/git.js.map +1 -0
- package/dist/core/log.js +22 -0
- package/dist/core/log.js.map +1 -0
- package/dist/core/severity.js +16 -0
- package/dist/core/severity.js.map +1 -0
- package/dist/core/version.js +15 -0
- package/dist/core/version.js.map +1 -0
- package/dist/detect/scan.js +79 -0
- package/dist/detect/scan.js.map +1 -0
- package/dist/detect/stack.js +71 -0
- package/dist/detect/stack.js.map +1 -0
- package/dist/enforce/ladder.js +148 -0
- package/dist/enforce/ladder.js.map +1 -0
- package/dist/exempt/parse.js +86 -0
- package/dist/exempt/parse.js.map +1 -0
- package/dist/metrics/options.js +257 -0
- package/dist/metrics/options.js.map +1 -0
- package/dist/metrics/run.js +72 -0
- package/dist/metrics/run.js.map +1 -0
- package/dist/platforms/azure/client.js +53 -0
- package/dist/platforms/azure/client.js.map +1 -0
- package/dist/platforms/azure/index.js +69 -0
- package/dist/platforms/azure/index.js.map +1 -0
- package/dist/platforms/azure/install.js +917 -0
- package/dist/platforms/azure/install.js.map +1 -0
- package/dist/platforms/azure/policy-types.js +70 -0
- package/dist/platforms/azure/policy-types.js.map +1 -0
- package/dist/platforms/azure/verify.js +309 -0
- package/dist/platforms/azure/verify.js.map +1 -0
- package/dist/platforms/detect.js +40 -0
- package/dist/platforms/detect.js.map +1 -0
- package/dist/platforms/github/client.js +49 -0
- package/dist/platforms/github/client.js.map +1 -0
- package/dist/platforms/github/index.js +54 -0
- package/dist/platforms/github/index.js.map +1 -0
- package/dist/platforms/github/install.js +558 -0
- package/dist/platforms/github/install.js.map +1 -0
- package/dist/platforms/github/push.js +102 -0
- package/dist/platforms/github/push.js.map +1 -0
- package/dist/platforms/github/remote.js +36 -0
- package/dist/platforms/github/remote.js.map +1 -0
- package/dist/platforms/github/verify.js +366 -0
- package/dist/platforms/github/verify.js.map +1 -0
- package/dist/platforms/http.js +58 -0
- package/dist/platforms/http.js.map +1 -0
- package/dist/platforms/pull-request-templates.js +157 -0
- package/dist/platforms/pull-request-templates.js.map +1 -0
- package/dist/platforms/remote.js +21 -0
- package/dist/platforms/remote.js.map +1 -0
- package/dist/platforms/resolve.js +69 -0
- package/dist/platforms/resolve.js.map +1 -0
- package/dist/platforms/shape.js +19 -0
- package/dist/platforms/shape.js.map +1 -0
- package/dist/platforms/types.js +24 -0
- package/dist/platforms/types.js.map +1 -0
- package/dist/policy/checks.js +91 -0
- package/dist/policy/checks.js.map +1 -0
- package/dist/policy/diff.js +50 -0
- package/dist/policy/diff.js.map +1 -0
- package/dist/registry/discover.js +74 -0
- package/dist/registry/discover.js.map +1 -0
- package/dist/registry/serialize.js +41 -0
- package/dist/registry/serialize.js.map +1 -0
- package/dist/registry/types.js +2 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/remove/host.js +294 -0
- package/dist/remove/host.js.map +1 -0
- package/dist/render/commands.js +139 -0
- package/dist/render/commands.js.map +1 -0
- package/dist/render/manifest.js +82 -0
- package/dist/render/manifest.js.map +1 -0
- package/dist/render/markers.js +166 -0
- package/dist/render/markers.js.map +1 -0
- package/dist/render/profile.js +23 -0
- package/dist/render/profile.js.map +1 -0
- package/dist/render/standards.js +169 -0
- package/dist/render/standards.js.map +1 -0
- package/dist/render/vendors.js +199 -0
- package/dist/render/vendors.js.map +1 -0
- package/dist/review/engines/api.js +95 -0
- package/dist/review/engines/api.js.map +1 -0
- package/dist/review/engines/embedded.js +20 -0
- package/dist/review/engines/embedded.js.map +1 -0
- package/dist/review/engines/types.js +2 -0
- package/dist/review/engines/types.js.map +1 -0
- package/dist/review/glob.js +29 -0
- package/dist/review/glob.js.map +1 -0
- package/dist/review/prompt.js +45 -0
- package/dist/review/prompt.js.map +1 -0
- package/dist/review/rules.js +25 -0
- package/dist/review/rules.js.map +1 -0
- package/dist/review/schema.js +84 -0
- package/dist/review/schema.js.map +1 -0
- package/dist/review/scope.js +27 -0
- package/dist/review/scope.js.map +1 -0
- package/dist/sarif/map.js +58 -0
- package/dist/sarif/map.js.map +1 -0
- package/dist/sarif/parse.js +88 -0
- package/dist/sarif/parse.js.map +1 -0
- package/dist/sarif/types.js +2 -0
- package/dist/sarif/types.js.map +1 -0
- package/dist/sync/host.js +21 -0
- package/dist/sync/host.js.map +1 -0
- package/dist/sync/plan.js +47 -0
- package/dist/sync/plan.js.map +1 -0
- package/dist/sync/render.js +57 -0
- package/dist/sync/render.js.map +1 -0
- package/dist/sync/run.js +118 -0
- package/dist/sync/run.js.map +1 -0
- package/dist/verify/host.js +52 -0
- package/dist/verify/host.js.map +1 -0
- package/dist/verify/remote.js +146 -0
- package/dist/verify/remote.js.map +1 -0
- package/package.json +52 -0
- package/platforms/azure/gate-template.yml +105 -0
- package/rulesets/redline-org-ruleset.json +46 -0
- package/rulesets/redline-ruleset.json +49 -0
- package/scripts/assign-rule-ids.mjs +129 -0
- package/scripts/build-baseline.mjs +124 -0
- package/scripts/build-correlation.mjs +74 -0
- package/scripts/build-dashboard.mjs +584 -0
- package/scripts/build-digest.mjs +164 -0
- package/scripts/build-inbox.mjs +133 -0
- package/scripts/build-registry.mjs +42 -0
- package/scripts/build-roi.mjs +181 -0
- package/scripts/check-pins.mjs +88 -0
- package/scripts/collect-telemetry.mjs +300 -0
- package/scripts/lib/__tests__/baseline.test.mjs +132 -0
- package/scripts/lib/__tests__/correlate.test.mjs +131 -0
- package/scripts/lib/__tests__/dora.test.mjs +110 -0
- package/scripts/lib/__tests__/exemptions.test.mjs +85 -0
- package/scripts/lib/__tests__/metrics-scanner.test.mjs +65 -0
- package/scripts/lib/__tests__/sarif.test.mjs +125 -0
- package/scripts/lib/__tests__/spend.test.mjs +64 -0
- package/scripts/lib/baseline.mjs +156 -0
- package/scripts/lib/correlate.mjs +125 -0
- package/scripts/lib/dora.mjs +133 -0
- package/scripts/lib/exemptions.mjs +79 -0
- package/scripts/lib/metrics.mjs +172 -0
- package/scripts/lib/rules.mjs +107 -0
- package/scripts/lib/sarif.mjs +108 -0
- package/scripts/lib/spend.mjs +69 -0
- package/scripts/measure-context.mjs +101 -0
- package/scripts/render-self.mjs +18 -0
- package/scripts/score-seeds.mjs +294 -0
- package/scripts/validate.mjs +394 -0
- package/standards/core.md +124 -0
- package/standards/manifest.json +214 -0
- package/standards/stacks/csharp.md +29 -0
- package/standards/stacks/go.md +30 -0
- package/standards/stacks/java.md +30 -0
- package/standards/stacks/javascript.md +42 -0
- package/standards/stacks/kotlin.md +27 -0
- package/standards/stacks/microservices.md +37 -0
- package/standards/stacks/nodejs.md +34 -0
- package/standards/stacks/python.md +29 -0
- package/standards/stacks/react-native.md +29 -0
- package/standards/stacks/react.md +46 -0
- package/standards/stacks/swift.md +26 -0
- package/standards/stacks/terraform.md +27 -0
- package/templates/CODEOWNERS +29 -0
- package/templates/azure/pull_request_template.md +72 -0
- package/templates/github/pull_request_template.md +72 -0
- package/templates/redline.yml +35 -0
- package/templates/repo-context.md +64 -0
- package/workflows/dashboard.yml +119 -0
- package/workflows/inbox.yml +97 -0
- package/workflows/redline-collect.yml +61 -0
- package/workflows/redline-gate.yml +301 -0
- package/workflows/redline-sync.yml +79 -0
- package/workflows/seed-canary.yml +231 -0
- package/workflows/verify-onboarding.yml +115 -0
- package/workflows/weekly-digest.yml +78 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { realpathSync } from 'node:fs';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { CLI_VERSION } from "../core/version.js";
|
|
6
|
+
import { createLog } from "../core/log.js";
|
|
7
|
+
import { exitCodeFor, isRedlineError, RedlineError } from "../core/errors.js";
|
|
8
|
+
import { isSeverity } from "../core/severity.js";
|
|
9
|
+
import { isRung, RUNGS } from "../enforce/ladder.js";
|
|
10
|
+
import { resolvePlatform as defaultResolvePlatform, } from "../platforms/resolve.js";
|
|
11
|
+
import { init, ONBOARD_BRANCH } from "../commands/init.js";
|
|
12
|
+
import { capabilitySelection, OPTIONAL_CAPABILITIES, } from "../config/redline-json.js";
|
|
13
|
+
import { remove, REMOVE_BRANCH } from "../commands/remove.js";
|
|
14
|
+
import { createHostWithdrawal } from "../remove/host.js";
|
|
15
|
+
import { verify } from "../commands/verify.js";
|
|
16
|
+
import { sync } from "../commands/sync.js";
|
|
17
|
+
import { exempt } from "../commands/exempt.js";
|
|
18
|
+
import { formatFinding, policy } from "../commands/policy.js";
|
|
19
|
+
import { review } from "../commands/review.js";
|
|
20
|
+
import { renderFinding } from "../review/schema.js";
|
|
21
|
+
import { embedded } from "../review/engines/embedded.js";
|
|
22
|
+
import { createApiEngine } from "../review/engines/api.js";
|
|
23
|
+
import { METRICS_COMMANDS, helpFor } from "../metrics/options.js";
|
|
24
|
+
import { runMetrics, specFor } from "../metrics/run.js";
|
|
25
|
+
import { createSyncHost } from "../sync/host.js";
|
|
26
|
+
import { verifyRemote } from "../verify/remote.js";
|
|
27
|
+
import { createRemoteVerifyHost } from "../verify/host.js";
|
|
28
|
+
import { standardsVersion } from "../commands/sync.js";
|
|
29
|
+
const PACKAGE_ROOT = fileURLToPath(new URL('../../', import.meta.url));
|
|
30
|
+
const USAGE = [
|
|
31
|
+
'redline — engineering control plane',
|
|
32
|
+
'',
|
|
33
|
+
' redline init [--profile <name>] [--vendors <list>] [--blocking] [--no-a11y] [--speckit] [--dry-run] [--repair]',
|
|
34
|
+
' [--adopt-caller] [--skip <list>] [--with <list>]',
|
|
35
|
+
' onboard this repository: standards, security floor, merge gate (advisory), registration',
|
|
36
|
+
' --dry-run print the plan; writes nothing, needs no credential, contacts no host',
|
|
37
|
+
' --vendors <list> comma-separated vendor ids (copilot,agents,claude,cursor) to render for —',
|
|
38
|
+
' overrides both detection and whatever .redline.json already recorded; a vendor',
|
|
39
|
+
' the org has not enabled never renders no matter what this list names',
|
|
40
|
+
' --blocking promote the merge gate from advisory to blocking',
|
|
41
|
+
' --no-a11y, --speckit recorded in .redline.json for later phases; changes nothing in Phase 1',
|
|
42
|
+
' --repair re-apply every capability even if this repository looks already onboarded — for',
|
|
43
|
+
' labels, review-ownership, repo-property, gate and merge-policy, whose recorded',
|
|
44
|
+
' pendingAdmin entry a plain re-run can never clear on its own; composes with --dry-run',
|
|
45
|
+
` --skip <list> comma-separated capabilities this repository does not want Redline to install:`,
|
|
46
|
+
` ${OPTIONAL_CAPABILITIES.join(', ')}. Use it when the repository already has its own —`,
|
|
47
|
+
' a deselected capability is not attempted, not written, and not reported as missing.',
|
|
48
|
+
' The security floor (secret scanning, push protection, dependency alerts) is the',
|
|
49
|
+
' organisation-wide minimum and is refused by name rather than deselected',
|
|
50
|
+
' --with <list> the same names, selected again — how a deselection recorded in .redline.json is',
|
|
51
|
+
' reversed',
|
|
52
|
+
` --rung <name> the enforcement rung: ${RUNGS.join(', ')}. A promotion needs recorded`,
|
|
53
|
+
' evidence and is refused without it; a demotion is always allowed. Omitting',
|
|
54
|
+
' the flag keeps whatever the repository already recorded',
|
|
55
|
+
' --adopt-caller let Redline take over the gate machinery file (.github/workflows/redline.yml,',
|
|
56
|
+
' .azuredevops/redline-gate.yml) when what is already there carries nothing that',
|
|
57
|
+
' attributes it to Redline — a 2.1 caller, in practice. Without it the run refuses',
|
|
58
|
+
' rather than overwrite a file that may be the repository\'s own',
|
|
59
|
+
' omitted flags keep whatever .redline.json already recorded',
|
|
60
|
+
'',
|
|
61
|
+
' redline remove [--dry-run]',
|
|
62
|
+
' take Redline back out of this repository: rendered standards, gate machinery, slash',
|
|
63
|
+
' commands, the host state it applied, and .redline.json last of all — as a pull request',
|
|
64
|
+
' --dry-run print the plan; writes nothing, needs no credential, contacts no host',
|
|
65
|
+
' only content Redline can prove it wrote is removed. A merged file keeps every byte',
|
|
66
|
+
' outside its REDLINE block; anything unattributable is left in place and named',
|
|
67
|
+
' the security floor (secret scanning, push protection, dependency alerts) is the',
|
|
68
|
+
' organisation\'s minimum, not Redline\'s state — no flag here turns it off',
|
|
69
|
+
'',
|
|
70
|
+
' redline verify [--gate] [--repo <owner/name>]',
|
|
71
|
+
' check this repository still matches what .redline.json claims',
|
|
72
|
+
' --repo <owner/name> check a repository over the API, with no checkout — a check',
|
|
73
|
+
' that genuinely needs a working tree reports ?? rather than passing',
|
|
74
|
+
'',
|
|
75
|
+
' redline review [--staged] [--diff-file <path>] [--base <ref>] [--engine <name>]',
|
|
76
|
+
' review this change against ONLY the rules that apply to the files it touches',
|
|
77
|
+
' --engine embedded emit the bounded prompt for the assistant running this (default)',
|
|
78
|
+
' --engine api call a configured endpoint — local or hosted — and parse the result',
|
|
79
|
+
' local findings are never sent to the telemetry that tunes rules',
|
|
80
|
+
'',
|
|
81
|
+
' redline policy --diff-file <path>',
|
|
82
|
+
' evaluate the rules a checker can decide, with no model call. Exit 1 on a BLOCKER',
|
|
83
|
+
'',
|
|
84
|
+
' redline exempt --body-file <path> [--scope <check>]',
|
|
85
|
+
' decide whether a pull request carries a valid exemption for a failing process',
|
|
86
|
+
' check — a reason, an actor and an expiry, not a bare label. Exit 0 if it applies',
|
|
87
|
+
'',
|
|
88
|
+
' redline sync [--dry-run] [--repo <owner/name>] [--force]',
|
|
89
|
+
' open a pull request on every registered repository whose standards are behind',
|
|
90
|
+
' --dry-run print the plan; opens nothing, pushes nothing',
|
|
91
|
+
' --repo <owner/name> one repository instead of the estate',
|
|
92
|
+
' --force re-render a repository already at the current standards version',
|
|
93
|
+
' run from a checkout of the Redline source repository, not a product repo',
|
|
94
|
+
'',
|
|
95
|
+
' redline metrics <command> [--flags]',
|
|
96
|
+
` the estate's measurement plane: ${Object.keys(METRICS_COMMANDS).join(', ')}`,
|
|
97
|
+
' run `redline metrics <command> --help` for its flags. These act on an organisation',
|
|
98
|
+
' or its collected telemetry, not on the repository you are standing in',
|
|
99
|
+
'',
|
|
100
|
+
' redline registry [--flags]',
|
|
101
|
+
' derive the register of onboarded repositories by walking the org',
|
|
102
|
+
'',
|
|
103
|
+
' redline --version',
|
|
104
|
+
].join('\n');
|
|
105
|
+
// node:util's parseArgs throws a plain Error on an unrecognised flag — that
|
|
106
|
+
// is bad input, not an internal defect, so it is converted to a usage
|
|
107
|
+
// RedlineError right at its own call site rather than left for the run()
|
|
108
|
+
// catch-all below to misclassify.
|
|
109
|
+
function parseCliArgs(fn) {
|
|
110
|
+
try {
|
|
111
|
+
return fn();
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
throw new RedlineError('usage', error instanceof Error ? error.message : String(error));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export async function run(argv, deps = {}) {
|
|
118
|
+
const cwd = deps.cwd ?? process.cwd();
|
|
119
|
+
const root = deps.root ?? PACKAGE_ROOT;
|
|
120
|
+
const log = createLog(deps.sink);
|
|
121
|
+
const resolve = deps.resolvePlatform ??
|
|
122
|
+
((dir, options) => defaultResolvePlatform(dir, options));
|
|
123
|
+
const [command, ...rest] = argv;
|
|
124
|
+
if (command === '--version' || command === '-v') {
|
|
125
|
+
log.info(CLI_VERSION);
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
if (command === undefined || command === '--help' || command === '-h') {
|
|
129
|
+
log.info(USAGE);
|
|
130
|
+
return command === undefined ? exitCodeFor('usage') : 0;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
if (command === 'init') {
|
|
134
|
+
const { values } = parseCliArgs(() => parseArgs({
|
|
135
|
+
args: rest,
|
|
136
|
+
// No `default`: parseArgs then leaves an untyped flag `undefined`,
|
|
137
|
+
// which is how init() tells "the user asked for advisory" from "the
|
|
138
|
+
// user said nothing, keep what the repository already chose". A
|
|
139
|
+
// default here silently demoted every --blocking repo on its next
|
|
140
|
+
// plain re-run.
|
|
141
|
+
options: {
|
|
142
|
+
profile: { type: 'string' },
|
|
143
|
+
vendors: { type: 'string' },
|
|
144
|
+
blocking: { type: 'boolean' },
|
|
145
|
+
'no-a11y': { type: 'boolean' },
|
|
146
|
+
speckit: { type: 'boolean' },
|
|
147
|
+
'dry-run': { type: 'boolean' },
|
|
148
|
+
repair: { type: 'boolean' },
|
|
149
|
+
'adopt-caller': { type: 'boolean' },
|
|
150
|
+
skip: { type: 'string' },
|
|
151
|
+
with: { type: 'string' },
|
|
152
|
+
rung: { type: 'string' },
|
|
153
|
+
},
|
|
154
|
+
allowPositionals: false,
|
|
155
|
+
}));
|
|
156
|
+
const names = (list) => list === undefined
|
|
157
|
+
? []
|
|
158
|
+
: list
|
|
159
|
+
.split(',')
|
|
160
|
+
.map((name) => name.trim())
|
|
161
|
+
.filter((name) => name !== '');
|
|
162
|
+
// Throws a usage RedlineError on an unknown or non-optional name, before
|
|
163
|
+
// a platform is resolved or a byte is written.
|
|
164
|
+
const selection = capabilitySelection(names(values.skip), names(values.with));
|
|
165
|
+
const menu = { ...selection.menu };
|
|
166
|
+
if (values.blocking !== undefined)
|
|
167
|
+
menu.blockingGate = values.blocking;
|
|
168
|
+
if (values['no-a11y'] !== undefined)
|
|
169
|
+
menu.accessibility = !values['no-a11y'];
|
|
170
|
+
if (values.speckit !== undefined)
|
|
171
|
+
menu.speckit = values.speckit;
|
|
172
|
+
// Same "no default" reasoning as the menu flags above: undefined is how
|
|
173
|
+
// init() tells "nothing typed, keep detection or the recorded
|
|
174
|
+
// selection" from "the caller typed an empty list".
|
|
175
|
+
const vendors = values.vendors !== undefined
|
|
176
|
+
? values.vendors
|
|
177
|
+
.split(',')
|
|
178
|
+
.map((v) => v.trim())
|
|
179
|
+
.filter((v) => v !== '')
|
|
180
|
+
: undefined;
|
|
181
|
+
if (values.rung !== undefined && !isRung(values.rung)) {
|
|
182
|
+
throw new RedlineError('usage', `--rung must be one of ${RUNGS.join(', ')}, not "${values.rung}"`, 'observe reports and blocks nothing; block-blocker stops a merge on a BLOCKER');
|
|
183
|
+
}
|
|
184
|
+
const dryRun = values['dry-run'] === true;
|
|
185
|
+
const repair = values.repair === true;
|
|
186
|
+
const adoptCaller = values['adopt-caller'] === true;
|
|
187
|
+
// A dry run sends no request, so it must not require a credential —
|
|
188
|
+
// see ResolvePlatformOptions.lazyCredentials. Every other path here
|
|
189
|
+
// resolves one up front, exactly as before.
|
|
190
|
+
const platform = await resolve(cwd, dryRun ? { lazyCredentials: true } : {});
|
|
191
|
+
const report = await init(platform, {
|
|
192
|
+
cwd,
|
|
193
|
+
root,
|
|
194
|
+
...(values.profile ? { profile: values.profile } : {}),
|
|
195
|
+
...(vendors ? { vendors } : {}),
|
|
196
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
197
|
+
...(repair ? { repair: true } : {}),
|
|
198
|
+
...(adoptCaller ? { adoptCaller: true } : {}),
|
|
199
|
+
...(values.rung ? { rung: values.rung } : {}),
|
|
200
|
+
menu,
|
|
201
|
+
capabilities: selection.capabilities,
|
|
202
|
+
});
|
|
203
|
+
log.info(`profile ${report.profile}`);
|
|
204
|
+
if (report.migratedFrom)
|
|
205
|
+
log.info(`migrated from ${report.migratedFrom}`);
|
|
206
|
+
// Printed on every path, settled included: a report that falls silent
|
|
207
|
+
// about what was never attempted cannot be told from one where it broke.
|
|
208
|
+
if (report.optedOut.length > 0)
|
|
209
|
+
log.info(`opted out: ${report.optedOut.join(', ')}`);
|
|
210
|
+
for (const note of report.notes)
|
|
211
|
+
log.info(note);
|
|
212
|
+
if (report.dryRun) {
|
|
213
|
+
log.info('dry run — nothing was written, read or changed on the host');
|
|
214
|
+
if (report.alreadyOnboarded) {
|
|
215
|
+
log.info('already onboarded — no file would change (host settings were not read)');
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
for (const file of report.files) {
|
|
219
|
+
log.info(` would ${report.removals.includes(file) ? 'remove' : 'write '} ${file}`);
|
|
220
|
+
}
|
|
221
|
+
for (const step of report.hostPlan)
|
|
222
|
+
log.info(` would apply ${step}`);
|
|
223
|
+
for (const [key, value] of Object.entries(report.menu))
|
|
224
|
+
log.info(` menu ${key}: ${value}`);
|
|
225
|
+
return 0;
|
|
226
|
+
}
|
|
227
|
+
for (const file of report.files) {
|
|
228
|
+
log.info(` ${report.removals.includes(file) ? 'remove' : 'write '} ${file}`);
|
|
229
|
+
}
|
|
230
|
+
for (const outcome of report.outcomes) {
|
|
231
|
+
log.info(` ${outcome.status.padEnd(11)} ${outcome.capability} ${outcome.detail}`);
|
|
232
|
+
}
|
|
233
|
+
if (report.pendingAdmin.length > 0) {
|
|
234
|
+
// On the already-onboarded path this run applied nothing, so the list
|
|
235
|
+
// is what was recorded — not a finding this run made. Saying so is the
|
|
236
|
+
// difference between a status and a claim.
|
|
237
|
+
log.warn(`partially onboarded — an administrator must still enable: ${report.pendingAdmin.join(', ')}` +
|
|
238
|
+
(report.alreadyOnboarded ? ' (as recorded at the last run)' : ''));
|
|
239
|
+
}
|
|
240
|
+
// The host settings and .redline.json are already written — only the
|
|
241
|
+
// review vehicle is missing. That is `failed` (1), not host (4): there
|
|
242
|
+
// is nothing to retry, there is a branch to open a pull request from.
|
|
243
|
+
// The hint does not assert the push succeeded, because the same wrap
|
|
244
|
+
// catches a git step that failed before it: the error line above is the
|
|
245
|
+
// git error itself, which already says where the work was left.
|
|
246
|
+
if (report.pullRequestError !== null) {
|
|
247
|
+
log.error(`could not open the pull request: ${report.pullRequestError}`, `the Redline changes are on branch ${ONBOARD_BRANCH} — push it if it is not already on origin, then open the pull request manually`);
|
|
248
|
+
return exitCodeFor('failed');
|
|
249
|
+
}
|
|
250
|
+
if (report.pullRequest)
|
|
251
|
+
log.info(`pull request: ${report.pullRequest.url}`);
|
|
252
|
+
else if (report.alreadyOnboarded)
|
|
253
|
+
log.info('already onboarded — nothing to change');
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
if (command === 'remove') {
|
|
257
|
+
const { values } = parseCliArgs(() => parseArgs({
|
|
258
|
+
args: rest,
|
|
259
|
+
options: { 'dry-run': { type: 'boolean', default: false } },
|
|
260
|
+
allowPositionals: false,
|
|
261
|
+
}));
|
|
262
|
+
const dryRun = values['dry-run'] === true;
|
|
263
|
+
// Same contract as `init --dry-run`: the plan is built from the local
|
|
264
|
+
// checkout, so previewing what backing Redline out would cost needs
|
|
265
|
+
// neither a credential nor a reachable host.
|
|
266
|
+
const platform = await resolve(cwd, dryRun ? { lazyCredentials: true } : {});
|
|
267
|
+
const report = await remove(platform, () => deps.hostWithdrawal?.(cwd) ?? createHostWithdrawal(cwd), { cwd, root, ...(dryRun ? { dryRun: true } : {}) });
|
|
268
|
+
for (const action of report.actions) {
|
|
269
|
+
if (action.kind === 'kept')
|
|
270
|
+
continue;
|
|
271
|
+
const verb = action.kind === 'delete' ? 'remove ' : 'unmerge';
|
|
272
|
+
log.info(` ${report.dryRun ? 'would ' : ''}${verb} ${action.path} — ${action.reason}`);
|
|
273
|
+
}
|
|
274
|
+
// Printed on every run, dry or not. What was left behind is the half of
|
|
275
|
+
// this command's answer an operator has to act on themselves, and a
|
|
276
|
+
// report that only lists deletions reads as if nothing was left.
|
|
277
|
+
for (const action of report.actions) {
|
|
278
|
+
if (action.kind !== 'kept')
|
|
279
|
+
continue;
|
|
280
|
+
log.warn(`left in place ${action.path} — ${action.reason}`);
|
|
281
|
+
}
|
|
282
|
+
for (const note of report.notes)
|
|
283
|
+
log.info(note);
|
|
284
|
+
if (report.dryRun) {
|
|
285
|
+
for (const step of report.hostPlan)
|
|
286
|
+
log.info(` would apply ${step}`);
|
|
287
|
+
log.info('dry run — nothing was written, read or changed on the host');
|
|
288
|
+
return 0;
|
|
289
|
+
}
|
|
290
|
+
for (const outcome of report.outcomes) {
|
|
291
|
+
log.info(` ${outcome.status.padEnd(11)} ${outcome.capability} ${outcome.detail}`);
|
|
292
|
+
}
|
|
293
|
+
if (report.pendingAdmin.length > 0) {
|
|
294
|
+
log.warn(`partially removed — an administrator must still take away: ${report.pendingAdmin.join(', ')}`);
|
|
295
|
+
}
|
|
296
|
+
// The host state is withdrawn and the files are gone from the working
|
|
297
|
+
// tree; only the review vehicle is missing. `failed` (1), not host (4):
|
|
298
|
+
// there is nothing to retry, there is a branch to open a pull request from.
|
|
299
|
+
if (report.pullRequestError !== null) {
|
|
300
|
+
log.error(`could not open the pull request: ${report.pullRequestError}`, `the removal is on branch ${REMOVE_BRANCH} — push it if it is not already on origin, then open the pull request manually`);
|
|
301
|
+
return exitCodeFor('failed');
|
|
302
|
+
}
|
|
303
|
+
if (report.pullRequest)
|
|
304
|
+
log.info(`pull request: ${report.pullRequest.url}`);
|
|
305
|
+
else
|
|
306
|
+
log.info('nothing left to remove from the working tree — no pull request was opened');
|
|
307
|
+
return 0;
|
|
308
|
+
}
|
|
309
|
+
if (command === 'verify') {
|
|
310
|
+
const { values } = parseCliArgs(() => parseArgs({
|
|
311
|
+
args: rest,
|
|
312
|
+
options: { gate: { type: 'boolean', default: false }, repo: { type: 'string' } },
|
|
313
|
+
allowPositionals: false,
|
|
314
|
+
}));
|
|
315
|
+
if (values.repo !== undefined) {
|
|
316
|
+
if (values.gate === true) {
|
|
317
|
+
throw new RedlineError('usage', '--gate publishes this repository\'s merge status and cannot target another repository', 'drop --repo to run the gate, or drop --gate to verify a remote repository');
|
|
318
|
+
}
|
|
319
|
+
const remoteHost = deps.remoteVerifyHost?.() ?? createRemoteVerifyHost();
|
|
320
|
+
const remoteRef = await remoteHost.resolveRef(values.repo);
|
|
321
|
+
const remoteReport = await verifyRemote(remoteHost, remoteRef, {
|
|
322
|
+
root,
|
|
323
|
+
standardsVersion: standardsVersion(root),
|
|
324
|
+
});
|
|
325
|
+
log.info(`${values.repo} (${remoteRef.defaultBranch})`);
|
|
326
|
+
log.report(remoteReport.findings);
|
|
327
|
+
// Same mapping as the local path: one finding means the repository was
|
|
328
|
+
// never onboarded, which is a different thing to tell an operator than
|
|
329
|
+
// onboarded-and-drifted.
|
|
330
|
+
const never = !remoteReport.ok && remoteReport.findings.length === 1;
|
|
331
|
+
return remoteReport.ok ? 0 : never ? exitCodeFor('usage') : exitCodeFor('failed');
|
|
332
|
+
}
|
|
333
|
+
// resolve is passed unevaluated: verify() must be able to report "not
|
|
334
|
+
// onboarded" without a host credential — see cli/commands/verify.ts.
|
|
335
|
+
const report = await verify(() => resolve(cwd), { cwd, root, gate: values.gate === true });
|
|
336
|
+
log.report(report.findings);
|
|
337
|
+
// verify() short-circuits to exactly one finding when .redline.json is
|
|
338
|
+
// missing or corrupt (see cli/commands/verify.ts), precisely so this
|
|
339
|
+
// mapping can tell "never onboarded" (usage, 2) apart from "onboarded
|
|
340
|
+
// but wrong" (failed, 1) without verify() itself owning exit codes.
|
|
341
|
+
const notOnboarded = !report.ok && report.findings.length === 1;
|
|
342
|
+
const exitCode = report.ok ? 0 : notOnboarded ? exitCodeFor('usage') : exitCodeFor('failed');
|
|
343
|
+
if (values.gate === true) {
|
|
344
|
+
if (report.ok)
|
|
345
|
+
log.info('Redline gate passed.');
|
|
346
|
+
else if (notOnboarded)
|
|
347
|
+
log.info('Redline gate: repository not onboarded — run redline init.');
|
|
348
|
+
else
|
|
349
|
+
log.info('Redline gate failed.');
|
|
350
|
+
}
|
|
351
|
+
return exitCode;
|
|
352
|
+
}
|
|
353
|
+
if (command === 'review') {
|
|
354
|
+
const { values } = parseCliArgs(() => parseArgs({
|
|
355
|
+
args: rest,
|
|
356
|
+
options: {
|
|
357
|
+
staged: { type: 'boolean', default: false },
|
|
358
|
+
'diff-file': { type: 'string' },
|
|
359
|
+
base: { type: 'string' },
|
|
360
|
+
profile: { type: 'string' },
|
|
361
|
+
engine: { type: 'string', default: 'embedded' },
|
|
362
|
+
provider: { type: 'string', default: 'openai' },
|
|
363
|
+
model: { type: 'string' },
|
|
364
|
+
'base-url': { type: 'string' },
|
|
365
|
+
},
|
|
366
|
+
allowPositionals: false,
|
|
367
|
+
}));
|
|
368
|
+
let engine = embedded;
|
|
369
|
+
if (values.engine === 'api') {
|
|
370
|
+
if (!values.model) {
|
|
371
|
+
throw new RedlineError('usage', '--engine api needs --model <name>', 'a model baked into the tool is one nobody can change when it is deprecated');
|
|
372
|
+
}
|
|
373
|
+
// Not a silent fallback: `--provider anthropc` used to become openai and
|
|
374
|
+
// send the diff to a local endpoint that was not running, reporting a
|
|
375
|
+
// connection error for what was a typo.
|
|
376
|
+
if (values.provider !== 'openai' && values.provider !== 'anthropic') {
|
|
377
|
+
throw new RedlineError('usage', `--provider must be openai or anthropic, not "${values.provider}"`, 'openai covers every OpenAI-compatible endpoint, including a local one');
|
|
378
|
+
}
|
|
379
|
+
const provider = values.provider;
|
|
380
|
+
engine = createApiEngine({
|
|
381
|
+
provider,
|
|
382
|
+
model: values.model,
|
|
383
|
+
...(values['base-url'] ? { baseUrl: values['base-url'] } : {}),
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
else if (values.engine !== 'embedded') {
|
|
387
|
+
throw new RedlineError('usage', `unknown engine "${values.engine}" — use embedded or api`);
|
|
388
|
+
}
|
|
389
|
+
const report = await review(engine, {
|
|
390
|
+
cwd,
|
|
391
|
+
root,
|
|
392
|
+
...(values.profile ? { profile: values.profile } : {}),
|
|
393
|
+
...(values.base ? { base: values.base } : {}),
|
|
394
|
+
source: values['diff-file']
|
|
395
|
+
? { kind: 'diff-file', path: values['diff-file'] }
|
|
396
|
+
: values.staged
|
|
397
|
+
? { kind: 'staged' }
|
|
398
|
+
: { kind: 'worktree' },
|
|
399
|
+
});
|
|
400
|
+
log.info(`profile ${report.scope.profile} — rules in scope: core` +
|
|
401
|
+
(report.scope.stacks.length ? `, ${report.scope.stacks.join(', ')}` : ''));
|
|
402
|
+
// A file no stack covers is a gap in the standard. Reviewing it against
|
|
403
|
+
// core alone and saying nothing hides that.
|
|
404
|
+
if (report.scope.uncovered.length > 0) {
|
|
405
|
+
log.warn(`no stack covers ${report.scope.uncovered.length} changed file(s), so only the core rules ` +
|
|
406
|
+
`applied to them: ${report.scope.uncovered.slice(0, 5).join(', ')}` +
|
|
407
|
+
(report.scope.uncovered.length > 5 ? ', and more' : ''));
|
|
408
|
+
}
|
|
409
|
+
if (report.prompt !== null) {
|
|
410
|
+
log.info('');
|
|
411
|
+
log.info(report.prompt);
|
|
412
|
+
log.info('');
|
|
413
|
+
log.info(report.note ?? '');
|
|
414
|
+
return 0;
|
|
415
|
+
}
|
|
416
|
+
for (const finding of report.findings) {
|
|
417
|
+
log.info(`${finding.file}:${finding.line}`);
|
|
418
|
+
log.info(` ${renderFinding(finding)}`);
|
|
419
|
+
}
|
|
420
|
+
for (const { reason } of report.rejected) {
|
|
421
|
+
log.warn(`discarded a finding from the model: ${reason}`);
|
|
422
|
+
}
|
|
423
|
+
log.info(report.findings.length === 0
|
|
424
|
+
? 'no findings — an empty review is a valid review'
|
|
425
|
+
: `${report.findings.length} finding(s)`);
|
|
426
|
+
// Said out loud on every run. A local review is opt-in and enforces
|
|
427
|
+
// nothing; the pull request review remains the system of record.
|
|
428
|
+
log.info('local review — not recorded, and not counted in rule-tuning telemetry');
|
|
429
|
+
// Always 0. This is a pre-flight convenience, and a non-zero exit would
|
|
430
|
+
// invite someone to wire it into CI as a second gate, where it would
|
|
431
|
+
// enforce nothing while looking like it did.
|
|
432
|
+
return 0;
|
|
433
|
+
}
|
|
434
|
+
if (command === 'policy') {
|
|
435
|
+
const { values } = parseCliArgs(() => parseArgs({
|
|
436
|
+
args: rest,
|
|
437
|
+
options: { 'diff-file': { type: 'string' }, 'fail-on': { type: 'string' } },
|
|
438
|
+
allowPositionals: false,
|
|
439
|
+
}));
|
|
440
|
+
const diffFile = values['diff-file'];
|
|
441
|
+
if (!diffFile)
|
|
442
|
+
throw new RedlineError('usage', 'redline policy needs --diff-file <path>');
|
|
443
|
+
const failOn = values['fail-on'];
|
|
444
|
+
if (failOn !== undefined && !isSeverity(failOn)) {
|
|
445
|
+
throw new RedlineError('usage', `--fail-on must be BLOCKER, HIGH or SUGGESTION, not "${failOn}"`);
|
|
446
|
+
}
|
|
447
|
+
const report = policy({ root, diffFile, ...(failOn ? { failOn } : {}) });
|
|
448
|
+
for (const finding of report.findings) {
|
|
449
|
+
log.info(`${finding.file}:${finding.line}`);
|
|
450
|
+
log.info(` ${formatFinding(finding)}`);
|
|
451
|
+
}
|
|
452
|
+
// Said on every run, including the clean one. A reviewer has to be able to
|
|
453
|
+
// tell "checked and clean" from "not checked", and silence looks the same
|
|
454
|
+
// as both.
|
|
455
|
+
log.info(report.findings.length === 0
|
|
456
|
+
? `no deterministic findings — ${report.evaluated.length} rule(s) evaluated with no model call`
|
|
457
|
+
: `${report.findings.length} finding(s) from ${report.evaluated.length} deterministic rule(s)`);
|
|
458
|
+
for (const id of report.unimplemented) {
|
|
459
|
+
log.warn(`${id} is classified deterministic but has no check — it is enforced by nobody`);
|
|
460
|
+
}
|
|
461
|
+
return report.ok ? 0 : exitCodeFor('failed');
|
|
462
|
+
}
|
|
463
|
+
if (command === 'exempt') {
|
|
464
|
+
const { values } = parseCliArgs(() => parseArgs({
|
|
465
|
+
args: rest,
|
|
466
|
+
options: { 'body-file': { type: 'string' }, scope: { type: 'string' } },
|
|
467
|
+
allowPositionals: false,
|
|
468
|
+
}));
|
|
469
|
+
const bodyFile = values['body-file'];
|
|
470
|
+
if (!bodyFile) {
|
|
471
|
+
throw new RedlineError('usage', 'redline exempt needs --body-file <path>');
|
|
472
|
+
}
|
|
473
|
+
const report = exempt({
|
|
474
|
+
bodyFile,
|
|
475
|
+
...(values.scope ? { scope: values.scope } : {}),
|
|
476
|
+
});
|
|
477
|
+
for (const message of report.messages) {
|
|
478
|
+
if (report.applies)
|
|
479
|
+
log.info(message);
|
|
480
|
+
else
|
|
481
|
+
log.warn(message);
|
|
482
|
+
}
|
|
483
|
+
// Exit 1, not 2: a pull request without a valid exemption is a normal,
|
|
484
|
+
// expected answer the gate acts on — not the caller misusing the command.
|
|
485
|
+
return report.applies ? 0 : exitCodeFor('failed');
|
|
486
|
+
}
|
|
487
|
+
if (command === 'sync') {
|
|
488
|
+
const { values } = parseCliArgs(() => parseArgs({
|
|
489
|
+
args: rest,
|
|
490
|
+
options: {
|
|
491
|
+
'dry-run': { type: 'boolean', default: false },
|
|
492
|
+
repo: { type: 'string' },
|
|
493
|
+
force: { type: 'boolean', default: false },
|
|
494
|
+
},
|
|
495
|
+
allowPositionals: false,
|
|
496
|
+
}));
|
|
497
|
+
const dryRun = values['dry-run'] === true;
|
|
498
|
+
// A dry run reads the register and the target's own files but never
|
|
499
|
+
// writes, so it still needs a read credential — unlike `init --dry-run`,
|
|
500
|
+
// which contacts no host at all. Saying so beats a confusing 403.
|
|
501
|
+
const report = await sync(deps.syncHost?.() ?? createSyncHost(), {
|
|
502
|
+
root,
|
|
503
|
+
cwd,
|
|
504
|
+
...(values.repo ? { repo: values.repo } : {}),
|
|
505
|
+
...(values.force ? { force: true } : {}),
|
|
506
|
+
...(dryRun ? { dryRun: true } : {}),
|
|
507
|
+
});
|
|
508
|
+
log.info(`standards v${report.plan.standardsVersion} — ${report.plan.targets.length} target(s), ${report.plan.skipped.length} skipped`);
|
|
509
|
+
for (const { repo, outcome } of report.results) {
|
|
510
|
+
if (outcome.kind === 'failed')
|
|
511
|
+
log.error(` ${repo}: ${outcome.detail}`);
|
|
512
|
+
else if (outcome.kind === 'current')
|
|
513
|
+
log.info(` ${repo}: already current`);
|
|
514
|
+
else if (outcome.kind === 'not-onboarded')
|
|
515
|
+
log.warn(` ${repo}: no .redline.json — not onboarded`);
|
|
516
|
+
else if (outcome.kind === 'stale-artifacts') {
|
|
517
|
+
// Content is current, but the target still carries files this profile
|
|
518
|
+
// no longer renders. Sync will not delete another repository's files;
|
|
519
|
+
// saying nothing reported it as healthy, which is what silent drift is.
|
|
520
|
+
log.warn(` ${repo}: current, but still carrying ${outcome.paths.join(', ')} — ` +
|
|
521
|
+
'a stack or vendor left this profile. Re-run redline init there to clear them');
|
|
522
|
+
}
|
|
523
|
+
else
|
|
524
|
+
log.info(` ${repo}: ${outcome.kind} ${outcome.url}`);
|
|
525
|
+
}
|
|
526
|
+
if (dryRun)
|
|
527
|
+
log.info('dry run — no branch was pushed and no pull request was opened');
|
|
528
|
+
// A partially failed estate run is a failure. Reporting 0 because most
|
|
529
|
+
// repositories succeeded is how a distribution quietly stops covering the
|
|
530
|
+
// ones it cannot reach.
|
|
531
|
+
return report.failures > 0 ? exitCodeFor('failed') : 0;
|
|
532
|
+
}
|
|
533
|
+
if (command === 'metrics' || command === 'registry') {
|
|
534
|
+
// The subcommand is a positional for `metrics` and absent for `registry`,
|
|
535
|
+
// so the flags are whatever follows it.
|
|
536
|
+
const [sub, ...flagArgs] = command === 'metrics' ? rest : ['registry', ...rest];
|
|
537
|
+
if (command === 'metrics' && (sub === undefined || sub === '--help' || sub === '-h')) {
|
|
538
|
+
log.info('redline metrics <command>');
|
|
539
|
+
log.info('');
|
|
540
|
+
for (const [name, spec] of Object.entries(METRICS_COMMANDS)) {
|
|
541
|
+
log.info(` ${name.padEnd(14)} ${spec.summary}`);
|
|
542
|
+
}
|
|
543
|
+
log.info('');
|
|
544
|
+
log.info('Run `redline metrics <command> --help` for its flags.');
|
|
545
|
+
return sub === undefined ? exitCodeFor('usage') : 0;
|
|
546
|
+
}
|
|
547
|
+
const spec = specFor(sub);
|
|
548
|
+
if (flagArgs.includes('--help') || flagArgs.includes('-h')) {
|
|
549
|
+
log.info(helpFor(command === 'registry' ? 'registry' : `metrics ${sub}`, spec));
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
552
|
+
// Built from the same table that validates and documents them, so a flag
|
|
553
|
+
// cannot exist in one of the three and not the others.
|
|
554
|
+
const options = Object.fromEntries(Object.entries(spec.options).map(([flag, option]) => [
|
|
555
|
+
flag,
|
|
556
|
+
{ type: option.type === 'boolean' ? 'boolean' : 'string' },
|
|
557
|
+
]));
|
|
558
|
+
const { values } = parseCliArgs(() => parseArgs({ args: flagArgs, options, allowPositionals: false }));
|
|
559
|
+
await runMetrics(sub, { root, flags: values, ...(deps.loadRunner ? { load: deps.loadRunner } : {}) });
|
|
560
|
+
return 0;
|
|
561
|
+
}
|
|
562
|
+
log.error(`unknown command "${command}"`, 'run: redline --help');
|
|
563
|
+
return exitCodeFor('usage');
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
if (isRedlineError(error)) {
|
|
567
|
+
log.error(error.message, error.hint);
|
|
568
|
+
return error.exitCode;
|
|
569
|
+
}
|
|
570
|
+
// Not a RedlineError: parseArgs failures are already converted to a
|
|
571
|
+
// usage RedlineError at their own call site (parseCliArgs above), and
|
|
572
|
+
// resolveProfile (unknown profile) and render() (unknown vendor) in
|
|
573
|
+
// cli/render/ throw RedlineError('usage', ...) as of Task 21. So
|
|
574
|
+
// whatever reaches here is a genuine internal defect — a TypeError, a
|
|
575
|
+
// null dereference, an unexpected throw from anywhere — not the user's
|
|
576
|
+
// mistake. Exit 2 would tell the user to check their flags when the
|
|
577
|
+
// tool itself is broken, which sends CI readers debugging the wrong
|
|
578
|
+
// thing. There is no sixth exit code to add for "internal defect", so
|
|
579
|
+
// this reuses exit 4 (host/network) as the least-wrong of the five
|
|
580
|
+
// published codes: at least it does not accuse the user.
|
|
581
|
+
log.error(`redline failed unexpectedly: ${error instanceof Error ? error.message : String(error)}`);
|
|
582
|
+
return exitCodeFor('host');
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// npm installs a bin as a symlink on POSIX, so process.argv[1] is the symlink
|
|
586
|
+
// path while import.meta.url is the real file. A textual comparison is false
|
|
587
|
+
// there, run() never fires, and the process exits 0 having printed nothing —
|
|
588
|
+
// which would make `redline verify --gate` pass unconditionally in CI. Compare
|
|
589
|
+
// resolved real paths instead. realpathSync throws if argv[1] is not a real
|
|
590
|
+
// path (an eval/stdin entry point), which is not this module being executed.
|
|
591
|
+
function isDirectlyExecuted() {
|
|
592
|
+
const entry = process.argv[1];
|
|
593
|
+
if (entry === undefined)
|
|
594
|
+
return false;
|
|
595
|
+
try {
|
|
596
|
+
return realpathSync(entry) === fileURLToPath(import.meta.url);
|
|
597
|
+
}
|
|
598
|
+
catch {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
if (isDirectlyExecuted()) {
|
|
603
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
604
|
+
}
|
|
605
|
+
//# sourceMappingURL=redline.js.map
|