actions-warden 0.2.0 → 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/AGENTS.md +189 -0
- package/CONTRIBUTING.md +109 -0
- package/README.md +272 -224
- package/RELEASING.md +338 -0
- package/SECURITY.md +25 -3
- package/docs/AI-AGENTS.md +458 -0
- package/docs/CLI.md +421 -0
- package/docs/CONFIGURATION.md +340 -0
- package/docs/DEVELOPMENT.md +350 -0
- package/docs/GITHUB-ACTION.md +281 -0
- package/docs/JAVASCRIPT-API.md +355 -0
- package/docs/OUTPUTS.md +409 -0
- package/docs/README.md +27 -0
- package/examples/org-scan.yml +42 -0
- package/examples/upgrade-pr.yml +57 -0
- package/llms.txt +38 -0
- package/package.json +32 -10
- package/skills/actions-warden/SKILL.md +140 -38
- package/src/action.js +317 -0
- package/src/cli.js +267 -22
- package/src/commands/audit.js +189 -36
- package/src/commands/org-scan.js +549 -0
- package/src/commands/pin.js +59 -56
- package/src/commands/report.js +122 -10
- package/src/commands/upgrade.js +102 -62
- package/src/commands/verify.js +193 -0
- package/src/index.js +21 -4
- package/src/lib/action-status.js +27 -0
- package/src/lib/agent-mode.js +175 -0
- package/src/lib/annotations.js +250 -0
- package/src/lib/baseline.js +103 -0
- package/src/lib/cache.js +47 -10
- package/src/lib/concurrency.js +27 -0
- package/src/lib/config.js +185 -0
- package/src/lib/execution.js +71 -0
- package/src/lib/formatter.js +127 -8
- package/src/lib/github-org.js +374 -0
- package/src/lib/identity.js +62 -0
- package/src/lib/ignore.js +7 -6
- package/src/lib/org-checkpoint.js +378 -0
- package/src/lib/org-progress.js +60 -0
- package/src/lib/parser.js +326 -52
- package/src/lib/patcher.js +199 -0
- package/src/lib/paths.js +35 -12
- package/src/lib/redact.js +65 -4
- package/src/lib/resolver.js +225 -43
- package/src/lib/targets.js +28 -0
- package/src/lib/triggers.js +12 -0
- package/src/lib/writer.js +45 -8
- package/src/rules/excessive-permissions.js +24 -33
- package/src/rules/index.js +19 -1
- package/src/rules/pull-request-target-checkout.js +149 -18
- package/src/rules/reusable-workflow-secrets.js +32 -0
- package/src/rules/script-injection.js +77 -12
- package/src/rules/secrets-in-env.js +101 -18
- package/src/rules/unpinned-action.js +3 -2
- package/src/rules/unpinned-container-image.js +39 -0
- package/src/rules/unpinned-docker-action.js +30 -0
- package/src/rules/untrusted-self-hosted-runner.js +109 -0
- package/src/rules/workflow-run-artifact-execution.js +122 -0
- package/src/rules/workflow-structure.js +396 -0
- package/src/version.js +3 -0
package/src/cli.js
CHANGED
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* actions-warden CLI entry point.
|
|
4
4
|
*
|
|
5
|
-
* Commands: audit, pin, upgrade, report, rules
|
|
5
|
+
* Commands: audit, pin, upgrade, verify, report, org-scan, rules
|
|
6
6
|
* Global flags: --format, --output, --output-path, --workflow, --token
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
* --write to mutate workflow files.
|
|
8
|
+
* Mutating operations (pin, upgrade) only write when --write is present.
|
|
10
9
|
*
|
|
11
10
|
* Exit codes:
|
|
12
11
|
* 0 no findings / no errors
|
|
@@ -14,24 +13,38 @@
|
|
|
14
13
|
* 2 invalid arguments
|
|
15
14
|
*/
|
|
16
15
|
|
|
17
|
-
import {
|
|
18
|
-
import { resolve } from 'node:path';
|
|
16
|
+
import { realpath } from 'node:fs/promises';
|
|
17
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
19
|
import { Command, Option } from 'commander';
|
|
20
20
|
|
|
21
21
|
import { audit, renderAudit } from './commands/audit.js';
|
|
22
22
|
import { pin, renderPin } from './commands/pin.js';
|
|
23
23
|
import { upgrade, renderUpgrade } from './commands/upgrade.js';
|
|
24
24
|
import { report, renderReport } from './commands/report.js';
|
|
25
|
+
import { verify, renderVerify } from './commands/verify.js';
|
|
26
|
+
import { scanOrganization, renderOrganizationScan } from './commands/org-scan.js';
|
|
25
27
|
import { listRules } from './rules/index.js';
|
|
26
28
|
import { format as fmt } from './lib/formatter.js';
|
|
29
|
+
import { writeFileGuarded } from './lib/writer.js';
|
|
30
|
+
import { serializeBaseline } from './lib/baseline.js';
|
|
31
|
+
import { formatOrganizationProgress } from './lib/org-progress.js';
|
|
32
|
+
import {
|
|
33
|
+
AGENT_MODE_ENVIRONMENT_VARIABLE,
|
|
34
|
+
createOrganizationAgentArtifacts,
|
|
35
|
+
renderOrganizationAgentReceipt,
|
|
36
|
+
resolveAgentMode,
|
|
37
|
+
} from './lib/agent-mode.js';
|
|
27
38
|
|
|
28
39
|
const program = new Command();
|
|
40
|
+
const require = createRequire(import.meta.url);
|
|
41
|
+
const { version } = require('../package.json');
|
|
29
42
|
program
|
|
30
43
|
.name('actions-warden')
|
|
31
|
-
.description('Audit
|
|
32
|
-
.version(
|
|
44
|
+
.description('Audit GitHub Actions across repositories and organizations; pin, verify, and upgrade dependencies.')
|
|
45
|
+
.version(version);
|
|
33
46
|
|
|
34
|
-
const formatOption = new Option('--format <fmt>', 'output format').choices(['toon', 'json', 'text']).default('toon');
|
|
47
|
+
const formatOption = new Option('--format <fmt>', 'output format').choices(['toon', 'json', 'text', 'sarif']).default('toon');
|
|
35
48
|
const outputOption = new Option('--output <dest>', 'output destination').choices(['stdout', 'file']).default('stdout');
|
|
36
49
|
|
|
37
50
|
function addCommonOptions(cmd) {
|
|
@@ -46,8 +59,13 @@ function addCommonOptions(cmd) {
|
|
|
46
59
|
|
|
47
60
|
async function emit(payload, opts) {
|
|
48
61
|
if (opts.output === 'file') {
|
|
49
|
-
|
|
50
|
-
await
|
|
62
|
+
if (!opts.outputPath) throw new Error('--output-path is required when --output=file');
|
|
63
|
+
await writeFileGuarded({
|
|
64
|
+
path: resolve(opts.cwd ?? process.cwd(), opts.outputPath),
|
|
65
|
+
content: payload,
|
|
66
|
+
dryRun: false,
|
|
67
|
+
cwd: opts.cwd ?? process.cwd(),
|
|
68
|
+
});
|
|
51
69
|
return;
|
|
52
70
|
}
|
|
53
71
|
process.stdout.write(payload);
|
|
@@ -57,25 +75,70 @@ addCommonOptions(program.command('audit'))
|
|
|
57
75
|
.description('Scan workflows for security findings')
|
|
58
76
|
.addOption(new Option('--severity <level>', 'minimum severity').choices(['low', 'medium', 'high', 'critical']))
|
|
59
77
|
.option('--explain', 'include plain-English remediation hint for each finding', false)
|
|
78
|
+
.option('--config <path>', 'repository policy file (default: .actions-warden.yml)')
|
|
79
|
+
.option('--ignore-config', 'do not load repository policy', false)
|
|
80
|
+
.option('--baseline <path>', 'suppress findings recorded in a baseline')
|
|
81
|
+
.option('--create-baseline <path>', 'write the current findings as a baseline')
|
|
60
82
|
.action(async (opts) => {
|
|
83
|
+
const createBaseline = opts.createBaseline;
|
|
84
|
+
if (createBaseline && opts.baseline) {
|
|
85
|
+
throw new Error('--baseline and --create-baseline cannot be used together');
|
|
86
|
+
}
|
|
87
|
+
if (opts.ignoreConfig && opts.config) {
|
|
88
|
+
throw new Error('--config and --ignore-config cannot be used together');
|
|
89
|
+
}
|
|
61
90
|
const result = await audit({
|
|
62
91
|
cwd: opts.cwd,
|
|
63
92
|
workflows: opts.workflow,
|
|
64
93
|
severity: opts.severity,
|
|
65
94
|
explain: Boolean(opts.explain),
|
|
95
|
+
configPath: opts.ignoreConfig ? false : opts.config,
|
|
96
|
+
baseline: opts.baseline,
|
|
97
|
+
ignoreBaseline: Boolean(createBaseline),
|
|
66
98
|
});
|
|
99
|
+
if (createBaseline) {
|
|
100
|
+
await writeFileGuarded({
|
|
101
|
+
path: resolve(opts.cwd, createBaseline),
|
|
102
|
+
content: serializeBaseline(result.allFindings, opts.cwd),
|
|
103
|
+
dryRun: false,
|
|
104
|
+
cwd: opts.cwd,
|
|
105
|
+
});
|
|
106
|
+
const payload = fmt(opts.format, [{
|
|
107
|
+
label: 'BASELINE',
|
|
108
|
+
fields: {
|
|
109
|
+
path: createBaseline,
|
|
110
|
+
findings: result.allFindings.length,
|
|
111
|
+
},
|
|
112
|
+
}], {
|
|
113
|
+
status: 'OK',
|
|
114
|
+
json: {
|
|
115
|
+
schemaVersion: '1.0',
|
|
116
|
+
path: createBaseline,
|
|
117
|
+
findings: result.allFindings.length,
|
|
118
|
+
status: 'OK',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
await emit(payload, opts);
|
|
122
|
+
process.exitCode = 0;
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
67
125
|
const payload = renderAudit(result, { format: opts.format, explain: Boolean(opts.explain), cwd: opts.cwd });
|
|
68
126
|
await emit(payload, opts);
|
|
69
|
-
process.
|
|
127
|
+
process.exitCode = result.status === 'OK' ? 0 : 1;
|
|
70
128
|
});
|
|
71
129
|
|
|
72
130
|
addCommonOptions(program.command('pin'))
|
|
73
131
|
.description('Pin tag/branch refs to immutable commit SHAs')
|
|
74
132
|
.option('--write', 'apply changes (disables dry-run)', false)
|
|
75
|
-
.option('--dry-run
|
|
133
|
+
.option('--dry-run', 'explicitly keep dry-run mode (the default)', false)
|
|
76
134
|
.option('--fix <id>', 'apply only the change with this id')
|
|
77
135
|
.action(async (opts) => {
|
|
78
|
-
|
|
136
|
+
if (opts.write && opts.dryRun) {
|
|
137
|
+
process.stderr.write('error: --write and --dry-run cannot be used together\n');
|
|
138
|
+
process.exitCode = 2;
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const dryRun = !opts.write;
|
|
79
142
|
const result = await pin({
|
|
80
143
|
cwd: opts.cwd,
|
|
81
144
|
workflows: opts.workflow,
|
|
@@ -85,22 +148,28 @@ addCommonOptions(program.command('pin'))
|
|
|
85
148
|
});
|
|
86
149
|
const payload = renderPin(result, { format: opts.format, dryRun, cwd: opts.cwd });
|
|
87
150
|
await emit(payload, opts);
|
|
88
|
-
process.
|
|
151
|
+
process.exitCode = result.status === 'OK' ? 0 : 1;
|
|
89
152
|
});
|
|
90
153
|
|
|
91
154
|
addCommonOptions(program.command('upgrade'))
|
|
92
155
|
.description('Upgrade pinned/tagged actions to a newer version')
|
|
93
156
|
.option('--write', 'apply changes (disables dry-run)', false)
|
|
94
|
-
.option('--dry-run
|
|
157
|
+
.option('--dry-run', 'explicitly keep dry-run mode (the default)', false)
|
|
95
158
|
.addOption(new Option('--mode <m>', 'upgrade scope').choices(['major', 'minor', 'patch']).default('minor'))
|
|
96
159
|
.option('--min-age <days>', 'cooldown: only accept tags older than this many days', '7')
|
|
97
160
|
.option('--fix <id>', 'apply only the change with this id')
|
|
98
161
|
.action(async (opts) => {
|
|
99
|
-
|
|
162
|
+
if (opts.write && opts.dryRun) {
|
|
163
|
+
process.stderr.write('error: --write and --dry-run cannot be used together\n');
|
|
164
|
+
process.exitCode = 2;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const dryRun = !opts.write;
|
|
100
168
|
const minAgeDays = Number.parseInt(opts.minAge, 10);
|
|
101
169
|
if (Number.isNaN(minAgeDays) || minAgeDays < 0) {
|
|
102
170
|
process.stderr.write('error: --min-age must be a non-negative integer\n');
|
|
103
|
-
process.
|
|
171
|
+
process.exitCode = 2;
|
|
172
|
+
return;
|
|
104
173
|
}
|
|
105
174
|
const result = await upgrade({
|
|
106
175
|
cwd: opts.cwd,
|
|
@@ -113,31 +182,181 @@ addCommonOptions(program.command('upgrade'))
|
|
|
113
182
|
});
|
|
114
183
|
const payload = renderUpgrade(result, { format: opts.format, dryRun, mode: opts.mode, cwd: opts.cwd });
|
|
115
184
|
await emit(payload, opts);
|
|
116
|
-
process.
|
|
185
|
+
process.exitCode = result.status === 'OK' ? 0 : 1;
|
|
117
186
|
});
|
|
118
187
|
|
|
119
188
|
addCommonOptions(program.command('report'))
|
|
120
189
|
.description('Combined audit + pin (dry) + upgrade (dry) report')
|
|
121
190
|
.addOption(new Option('--mode <m>', 'upgrade scope').choices(['major', 'minor', 'patch']).default('minor'))
|
|
191
|
+
.addOption(new Option('--severity <level>', 'minimum audit severity').choices(['low', 'medium', 'high', 'critical']))
|
|
192
|
+
.option('--explain', 'include remediation hints in audit findings', false)
|
|
122
193
|
.option('--min-age <days>', 'cooldown for upgrades (days)', '7')
|
|
123
194
|
.option('--offline', 'skip network calls (audit only)', false)
|
|
195
|
+
.option('--config <path>', 'repository policy file (default: .actions-warden.yml)')
|
|
196
|
+
.option('--ignore-config', 'do not load repository policy', false)
|
|
197
|
+
.option('--baseline <path>', 'suppress audit findings recorded in a baseline')
|
|
124
198
|
.action(async (opts) => {
|
|
125
199
|
const minAgeDays = Number.parseInt(opts.minAge, 10);
|
|
126
200
|
if (Number.isNaN(minAgeDays) || minAgeDays < 0) {
|
|
127
201
|
process.stderr.write('error: --min-age must be a non-negative integer\n');
|
|
128
|
-
process.
|
|
202
|
+
process.exitCode = 2;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (opts.ignoreConfig && opts.config) {
|
|
206
|
+
throw new Error('--config and --ignore-config cannot be used together');
|
|
129
207
|
}
|
|
130
208
|
const result = await report({
|
|
131
209
|
cwd: opts.cwd,
|
|
132
210
|
workflows: opts.workflow,
|
|
133
211
|
token: opts.token,
|
|
134
212
|
mode: opts.mode,
|
|
213
|
+
severity: opts.severity,
|
|
214
|
+
explain: Boolean(opts.explain),
|
|
135
215
|
skipResolve: Boolean(opts.offline),
|
|
136
216
|
minAgeDays,
|
|
217
|
+
configPath: opts.ignoreConfig ? false : opts.config,
|
|
218
|
+
baseline: opts.baseline,
|
|
137
219
|
});
|
|
138
220
|
const payload = renderReport(result, { format: opts.format, mode: opts.mode, cwd: opts.cwd });
|
|
139
221
|
await emit(payload, opts);
|
|
140
|
-
process.
|
|
222
|
+
process.exitCode = result.status === 'OK' ? 0 : 1;
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
addCommonOptions(program.command('verify'))
|
|
226
|
+
.description('Verify pinned SHAs and version metadata against GitHub')
|
|
227
|
+
.action(async (opts) => {
|
|
228
|
+
const result = await verify({
|
|
229
|
+
cwd: opts.cwd,
|
|
230
|
+
workflows: opts.workflow,
|
|
231
|
+
token: opts.token,
|
|
232
|
+
});
|
|
233
|
+
const payload = renderVerify(result, { format: opts.format, cwd: opts.cwd });
|
|
234
|
+
await emit(payload, opts);
|
|
235
|
+
process.exitCode = result.status === 'OK' ? 0 : 1;
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
program.command('org-scan <organization>')
|
|
239
|
+
.description('Scan workflow security across a GitHub organization')
|
|
240
|
+
.option('-r, --repository <pattern...>', 'repository name or glob (repeatable)')
|
|
241
|
+
.addOption(new Option('--visibility <visibility>', 'repository visibility').choices(['all', 'public', 'private', 'internal']).default('all'))
|
|
242
|
+
.option('--include-archived', 'include archived repositories', false)
|
|
243
|
+
.option('--include-disabled', 'include disabled repositories', false)
|
|
244
|
+
.option('--include-forks', 'include forked repositories', false)
|
|
245
|
+
.option('--max-repos <count>', 'scan at most this many repositories')
|
|
246
|
+
.option('--concurrency <count>', 'concurrent repository scans (1-16)', '4')
|
|
247
|
+
.addOption(new Option('--severity <level>', 'minimum severity').choices(['low', 'medium', 'high', 'critical']))
|
|
248
|
+
.option('--explain', 'include plain-English remediation hint for each finding', false)
|
|
249
|
+
.option('--config <path>', 'organization-wide policy file (default: .actions-warden.yml)')
|
|
250
|
+
.option('--ignore-config', 'do not load organization-wide policy', false)
|
|
251
|
+
.option('--baseline <path>', 'suppress findings recorded in an organization baseline')
|
|
252
|
+
.option('--checkpoint <path>', 'create or replace a resumable scan checkpoint')
|
|
253
|
+
.option('--resume <path>', 'resume from and update an existing checkpoint')
|
|
254
|
+
.addOption(new Option('--progress <mode>', 'live progress on stderr').choices(['auto', 'always', 'never']).default('auto'))
|
|
255
|
+
.option('--agent-mode', 'use bounded AI-agent output and automatic artifacts')
|
|
256
|
+
.option('--no-agent-mode', 'disable agent mode from the environment')
|
|
257
|
+
.option('--cwd <dir>', 'working directory', process.cwd())
|
|
258
|
+
.option('--token <token>', 'GitHub token (overrides GITHUB_TOKEN / GH_TOKEN)')
|
|
259
|
+
.addOption(formatOption)
|
|
260
|
+
.addOption(outputOption)
|
|
261
|
+
.option('--output-path <path>', 'file path when --output=file')
|
|
262
|
+
.action(async (organization, opts, command) => {
|
|
263
|
+
if (opts.ignoreConfig && opts.config) {
|
|
264
|
+
throw new Error('--config and --ignore-config cannot be used together');
|
|
265
|
+
}
|
|
266
|
+
if (opts.checkpoint && opts.resume) {
|
|
267
|
+
throw new Error('--checkpoint and --resume cannot be used together');
|
|
268
|
+
}
|
|
269
|
+
const concurrency = parsePositiveInteger(opts.concurrency, '--concurrency');
|
|
270
|
+
const maxRepositories = opts.maxRepos === undefined
|
|
271
|
+
? undefined
|
|
272
|
+
: parsePositiveInteger(opts.maxRepos, '--max-repos');
|
|
273
|
+
const agentMode = resolveAgentMode({
|
|
274
|
+
optionValue: opts.agentMode,
|
|
275
|
+
optionSource: command.getOptionValueSource('agentMode'),
|
|
276
|
+
environmentValue: process.env[AGENT_MODE_ENVIRONMENT_VARIABLE],
|
|
277
|
+
});
|
|
278
|
+
const effective = { ...opts };
|
|
279
|
+
if (agentMode) {
|
|
280
|
+
if (command.getOptionValueSource('format') === 'default') effective.format = 'json';
|
|
281
|
+
if (command.getOptionValueSource('output') === 'default') effective.output = 'file';
|
|
282
|
+
if (command.getOptionValueSource('progress') === 'default') effective.progress = 'never';
|
|
283
|
+
}
|
|
284
|
+
const agentArtifacts = agentMode
|
|
285
|
+
? await createOrganizationAgentArtifacts({
|
|
286
|
+
organization,
|
|
287
|
+
cwd: effective.cwd,
|
|
288
|
+
repositories: effective.repository,
|
|
289
|
+
visibility: effective.visibility,
|
|
290
|
+
includeArchived: Boolean(effective.includeArchived),
|
|
291
|
+
includeDisabled: Boolean(effective.includeDisabled),
|
|
292
|
+
includeForks: Boolean(effective.includeForks),
|
|
293
|
+
maxRepositories,
|
|
294
|
+
severity: effective.severity,
|
|
295
|
+
explain: Boolean(effective.explain),
|
|
296
|
+
configPath: effective.ignoreConfig ? false : effective.config,
|
|
297
|
+
baseline: effective.baseline,
|
|
298
|
+
reportFormat: effective.format,
|
|
299
|
+
})
|
|
300
|
+
: null;
|
|
301
|
+
if (agentMode && effective.output === 'file' && !effective.outputPath) {
|
|
302
|
+
effective.outputPath = agentArtifacts.reportPath;
|
|
303
|
+
}
|
|
304
|
+
let checkpointPath = effective.resume ?? effective.checkpoint;
|
|
305
|
+
let resume = Boolean(effective.resume);
|
|
306
|
+
if (agentMode && !checkpointPath) {
|
|
307
|
+
checkpointPath = agentArtifacts.checkpointPath;
|
|
308
|
+
resume = agentArtifacts.resume;
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
checkpointPath
|
|
312
|
+
&& effective.output === 'file'
|
|
313
|
+
&& effective.outputPath
|
|
314
|
+
&& await samePath(
|
|
315
|
+
resolve(effective.cwd, checkpointPath),
|
|
316
|
+
resolve(effective.cwd, effective.outputPath),
|
|
317
|
+
)
|
|
318
|
+
) {
|
|
319
|
+
throw new Error('checkpoint and report output paths must be different');
|
|
320
|
+
}
|
|
321
|
+
const result = await scanOrganization({
|
|
322
|
+
organization,
|
|
323
|
+
cwd: effective.cwd,
|
|
324
|
+
token: effective.token,
|
|
325
|
+
repositories: effective.repository,
|
|
326
|
+
visibility: effective.visibility,
|
|
327
|
+
includeArchived: Boolean(effective.includeArchived),
|
|
328
|
+
includeDisabled: Boolean(effective.includeDisabled),
|
|
329
|
+
includeForks: Boolean(effective.includeForks),
|
|
330
|
+
maxRepositories,
|
|
331
|
+
concurrency,
|
|
332
|
+
severity: effective.severity,
|
|
333
|
+
explain: Boolean(effective.explain),
|
|
334
|
+
configPath: effective.ignoreConfig ? false : effective.config,
|
|
335
|
+
baseline: effective.baseline,
|
|
336
|
+
checkpointPath,
|
|
337
|
+
resume,
|
|
338
|
+
onProgress: progressEnabled(effective.progress)
|
|
339
|
+
? event => {
|
|
340
|
+
const message = formatOrganizationProgress(event);
|
|
341
|
+
if (message) process.stderr.write(message);
|
|
342
|
+
}
|
|
343
|
+
: undefined,
|
|
344
|
+
});
|
|
345
|
+
const payload = renderOrganizationScan(result, {
|
|
346
|
+
format: effective.format,
|
|
347
|
+
cwd: effective.cwd,
|
|
348
|
+
});
|
|
349
|
+
await emit(payload, effective);
|
|
350
|
+
if (agentMode && effective.output === 'file') {
|
|
351
|
+
process.stdout.write(renderOrganizationAgentReceipt({
|
|
352
|
+
result,
|
|
353
|
+
reportPath: effective.outputPath,
|
|
354
|
+
reportFormat: effective.format,
|
|
355
|
+
checkpointPath,
|
|
356
|
+
resumed: resume,
|
|
357
|
+
}));
|
|
358
|
+
}
|
|
359
|
+
process.exitCode = result.status === 'OK' ? 0 : 1;
|
|
141
360
|
});
|
|
142
361
|
|
|
143
362
|
program.command('rules')
|
|
@@ -145,11 +364,37 @@ program.command('rules')
|
|
|
145
364
|
.addOption(formatOption)
|
|
146
365
|
.action((opts) => {
|
|
147
366
|
const rules = listRules();
|
|
148
|
-
const payload = fmt(opts.format, rules.map(r => ({ label: 'RULE', fields: r })), {
|
|
367
|
+
const payload = fmt(opts.format, rules.map(r => ({ label: 'RULE', fields: r })), {
|
|
368
|
+
status: 'OK',
|
|
369
|
+
json: { schemaVersion: '1.0', rules, status: 'OK' },
|
|
370
|
+
});
|
|
149
371
|
process.stdout.write(payload);
|
|
150
372
|
});
|
|
151
373
|
|
|
152
374
|
program.parseAsync(process.argv).catch((err) => {
|
|
153
375
|
process.stderr.write(`error: ${err.message}\n`);
|
|
154
|
-
process.
|
|
376
|
+
process.exitCode = 2;
|
|
155
377
|
});
|
|
378
|
+
|
|
379
|
+
function parsePositiveInteger(value, option) {
|
|
380
|
+
if (!/^\d+$/.test(value) || Number.parseInt(value, 10) < 1) {
|
|
381
|
+
throw new Error(`${option} must be a positive integer`);
|
|
382
|
+
}
|
|
383
|
+
return Number.parseInt(value, 10);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function progressEnabled(mode) {
|
|
387
|
+
return mode === 'always' || (mode === 'auto' && Boolean(process.stderr.isTTY));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function samePath(left, right) {
|
|
391
|
+
const [leftParent, rightParent] = await Promise.all([
|
|
392
|
+
realpath(dirname(left)),
|
|
393
|
+
realpath(dirname(right)),
|
|
394
|
+
]);
|
|
395
|
+
const leftTarget = join(leftParent, basename(left));
|
|
396
|
+
const rightTarget = join(rightParent, basename(right));
|
|
397
|
+
return process.platform === 'win32'
|
|
398
|
+
? leftTarget.toLowerCase() === rightTarget.toLowerCase()
|
|
399
|
+
: leftTarget === rightTarget;
|
|
400
|
+
}
|