@secureci/cli 0.0.0-stage → 0.1.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/dist/cli.js ADDED
@@ -0,0 +1,545 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const commander_1 = require("commander");
38
+ const dry_run_1 = require("./runner/dry-run");
39
+ const executor_1 = require("./runner/executor");
40
+ const action_fetcher_1 = require("./runner/action-fetcher");
41
+ const validator_1 = require("./parser/validator");
42
+ const workflow_parser_1 = require("./parser/workflow-parser");
43
+ const pipeline_contract_parser_1 = require("./parser/pipeline-contract-parser");
44
+ const pipeline_contract_validator_1 = require("./parser/pipeline-contract-validator");
45
+ const report_1 = require("./report");
46
+ const init_1 = require("./init");
47
+ const inventory_1 = require("./inventory");
48
+ const logger_1 = require("./utils/logger");
49
+ const key_value_1 = require("./utils/key-value");
50
+ const config_1 = require("./config");
51
+ const fs = __importStar(require("fs"));
52
+ const path = __importStar(require("path"));
53
+ /** Committed at the repo root, like package-lock.json / yarn.lock. */
54
+ const LOCKFILE_NAME = 'secureci.lock';
55
+ function readVersion() {
56
+ try {
57
+ const pkg = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
58
+ return String(pkg.version);
59
+ }
60
+ catch {
61
+ return '0.0.0';
62
+ }
63
+ }
64
+ const program = new commander_1.Command();
65
+ const globalConfig = (0, config_1.loadConfig)();
66
+ // An invalid container-runtime in the config file only stops `run`, so
67
+ // `secureci config set` can still fix it.
68
+ let configError;
69
+ let defaultContainerRuntime;
70
+ try {
71
+ defaultContainerRuntime = (0, config_1.getDefaultContainerRuntime)(globalConfig);
72
+ }
73
+ catch (error) {
74
+ configError = error instanceof Error ? error.message : String(error);
75
+ defaultContainerRuntime = 'docker';
76
+ }
77
+ program
78
+ .name('secureci')
79
+ .description('SecureCI: validate, check and run GitHub Actions workflows locally')
80
+ .version(readVersion(), '-V, --version', 'Print the version')
81
+ .helpOption('-h, --help', 'Show help')
82
+ .addHelpCommand('help [command]', 'Show help for a command')
83
+ .option('--verbose', 'Log debug output')
84
+ .showHelpAfterError()
85
+ .hook('preAction', (thisCommand) => {
86
+ if (thisCommand.opts().verbose) {
87
+ logger_1.logger.setLevel(logger_1.LogLevel.DEBUG);
88
+ }
89
+ });
90
+ function requireFile(file) {
91
+ if (!fs.existsSync(file)) {
92
+ logger_1.logger.error(`File not found: ${file}`);
93
+ process.exit(1);
94
+ }
95
+ }
96
+ program
97
+ .command('run')
98
+ .description('Run a workflow (or composite action) on this machine, each job in a container')
99
+ .argument('<file>', 'Path to a workflow or action YAML file')
100
+ .option('-d, --dry-run', 'Show what would run, without executing anything')
101
+ .option('-i, --input <key=value...>', 'Set an input for the inputs context. Repeatable')
102
+ .option('-s, --secret <key=value|NAME...>', 'Set a secret for the secrets context. A bare NAME reads $NAME from your shell, keeping the value off the command line. Repeatable')
103
+ .option('--env <key=value|NAME...>', 'Set an env var for every step, below workflow/job/step env:. A bare NAME passes $NAME from your shell (steps get nothing else from it). Repeatable')
104
+ .option('--var <key=value...>', 'Set a configuration variable for the vars context. Repeatable')
105
+ .option('--event-path <file>', 'Read the event payload (github.event, $GITHUB_EVENT_PATH) from a JSON file')
106
+ .option('--event-name <name>', 'Set github.event_name. Default: $GITHUB_EVENT_NAME, else workflow_dispatch if declared, else the first trigger')
107
+ .option('-j, --job <name>', 'Run only this job')
108
+ .option('--offline', 'Use only cached remote actions and workflows; never download')
109
+ .option('--no-local-checkout', 'Run the real actions/checkout for this repository instead of using the working copy')
110
+ .addOption(new commander_1.Option('--container-runtime <name>', 'Container engine for jobs. "host" runs steps directly on this machine, without a container. Config: container-runtime')
111
+ .choices(['auto', 'docker', 'podman', 'containerd', 'host'])
112
+ .default(defaultContainerRuntime))
113
+ .option('--container-image <image>', 'Use this image for every job without jobs.<id>.container, instead of the image for its runs-on (config: runner-images, container-image)')
114
+ .option('--container-timeout <ms>', 'Fail a step that runs longer than this, in milliseconds (container mode). Config: container-timeout', globalConfig['container-timeout'] !== undefined
115
+ ? String(globalConfig['container-timeout'])
116
+ : undefined)
117
+ .option('--container-memory <limit>', 'Limit job container memory, e.g. 512m. Config: container-memory', globalConfig['container-memory'])
118
+ .option('--no-network', 'Turn off container network access (on by default)')
119
+ .option('--allow-network', 'Turn on container network access, overriding allow-network: false in the config')
120
+ .action(async (file, options) => {
121
+ if (configError) {
122
+ logger_1.logger.error(configError);
123
+ process.exit(1);
124
+ }
125
+ requireFile(file);
126
+ if (options.dryRun) {
127
+ const result = (0, dry_run_1.dryRun)(file);
128
+ if (!result.success) {
129
+ process.exit(1);
130
+ }
131
+ return;
132
+ }
133
+ logger_1.logger.info("Running the workflow for real (not a dry run). Use --dry-run to simulate without executing, or 'secureci validate' to only check for errors.");
134
+ try {
135
+ const inputs = (0, key_value_1.parseKeyValuePairs)(options.input, '--input');
136
+ const secrets = (0, key_value_1.parseKeyValuePairs)(options.secret, '--secret', {
137
+ redact: true,
138
+ fromEnv: process.env
139
+ });
140
+ const vars = (0, key_value_1.parseKeyValuePairs)(options.var, '--var');
141
+ const env = (0, key_value_1.parseKeyValuePairs)(options.env, '--env', {
142
+ fromEnv: process.env
143
+ });
144
+ const result = await (0, executor_1.executeWorkflow)(file, {
145
+ dryRun: false,
146
+ inputs,
147
+ secrets,
148
+ vars,
149
+ env,
150
+ eventPath: options.eventPath,
151
+ eventName: options.eventName,
152
+ job: options.job,
153
+ localCheckout: options.localCheckout,
154
+ offline: options.offline,
155
+ verbose: program.opts().verbose,
156
+ containerRuntime: options.containerRuntime,
157
+ containerImage: options.containerImage,
158
+ runnerImages: globalConfig['runner-images'],
159
+ defaultContainerImage: globalConfig['container-image'],
160
+ containerTimeoutMs: options.containerTimeout
161
+ ? Number(options.containerTimeout)
162
+ : undefined,
163
+ containerMemoryLimit: options.containerMemory,
164
+ networkDisabled: (0, config_1.resolveNetworkDisabled)({ network: options.network, allowNetwork: options.allowNetwork }, globalConfig)
165
+ });
166
+ if (!result.success) {
167
+ process.exit(1);
168
+ }
169
+ }
170
+ catch (error) {
171
+ logger_1.logger.error(`Failed to process file: ${error instanceof Error ? error.message : 'Unknown error'}`);
172
+ process.exit(1);
173
+ }
174
+ });
175
+ program
176
+ .command('validate')
177
+ .description('Validate one or more workflow or action files, optionally against a pipeline contract')
178
+ .argument('<file...>', 'Paths to workflow or action YAML files')
179
+ .option('-c, --contract <file>', 'Also check the workflow against a pipeline contract (required/forbidden actions, minimum versions, ordering)')
180
+ .addOption(new commander_1.Option('--contract-recurse <scope>', 'Composite actions / reusable workflows to look inside for --contract: none, local (./...) or all (also downloads remote ones)')
181
+ .choices(['none', 'local', 'all'])
182
+ .default('local'))
183
+ .addOption(new commander_1.Option('--format <format>', 'Output format: text (default), or json / sarif for machine-readable results (sarif uploads to GitHub code scanning)')
184
+ .choices(['text', 'json', 'sarif'])
185
+ .default('text'))
186
+ .action(async (files, options) => {
187
+ files.forEach(requireFile);
188
+ await validateFiles(files, options.contract, options.contractRecurse, options.format);
189
+ });
190
+ program
191
+ .command('inventory')
192
+ .description('List the actions and reusable workflows a workflow uses (resolved into called workflows/actions), with their commit SHAs; track supply-chain drift with a committed secureci.lock')
193
+ .argument('[file...]', 'Workflow files (default: every file in .github/workflows)')
194
+ .option('--offline', 'Do not resolve tags to commit SHAs over the network (already-pinned refs still resolve)')
195
+ .option('--dry-run', `Print the resolved inventory as JSON without writing or checking ${LOCKFILE_NAME}`)
196
+ .option('--update-lock', `Write/update ./${LOCKFILE_NAME} with the current commit SHAs (commit it to your repo)`)
197
+ .option('--update-lockfile', `Alias of --update-lock`)
198
+ .option('--check', `Compare the current commit SHAs against ./${LOCKFILE_NAME} and fail on drift`)
199
+ .option('--compare', `Alias of --check`)
200
+ .action(async (files, options) => {
201
+ await runInventory(files, options);
202
+ });
203
+ program
204
+ .command('list')
205
+ .description('List workflows in .github/workflows/')
206
+ .action(() => {
207
+ listWorkflows();
208
+ });
209
+ const initCommand = program
210
+ .command('init')
211
+ .description('Scaffold SecureCI files (run a subcommand, e.g. `init workflow`)')
212
+ .action(() => {
213
+ // Bare `secureci init` is ambiguous about what it creates, so show help
214
+ // (and its subcommands) instead of generating anything.
215
+ initCommand.help();
216
+ });
217
+ initCommand
218
+ .command('workflow')
219
+ .description('Generate a GitHub Actions workflow (.github/workflows/secureci.yml) that runs SecureCI on pull requests')
220
+ .option('-c, --contract <file>', 'Check workflows against this pipeline contract in the generated workflow')
221
+ .option('--no-upload', 'Do not upload SARIF to code scanning (keeps contents: read only)')
222
+ .option('-o, --output <path>', 'Where to write the workflow', '.github/workflows/secureci.yml')
223
+ .option('-f, --force', 'Overwrite the output file if it already exists')
224
+ .action((options) => {
225
+ runInit(options);
226
+ });
227
+ function defaultWorkflowFiles() {
228
+ const dir = path.join(process.cwd(), '.github', 'workflows');
229
+ if (!fs.existsSync(dir))
230
+ return [];
231
+ return fs
232
+ .readdirSync(dir)
233
+ .filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'))
234
+ .map((f) => path.join('.github', 'workflows', f));
235
+ }
236
+ async function runInventory(files, options) {
237
+ const update = options.updateLock || options.updateLockfile;
238
+ const check = options.check || options.compare;
239
+ if (update && check) {
240
+ logger_1.logger.error('Use either --update-lock or --check, not both.');
241
+ process.exit(1);
242
+ }
243
+ const targets = files.length > 0 ? files : defaultWorkflowFiles();
244
+ if (targets.length === 0) {
245
+ logger_1.logger.error('No workflow files given and none found in .github/workflows');
246
+ process.exit(1);
247
+ }
248
+ targets.forEach(requireFile);
249
+ const resolver = options.offline
250
+ ? undefined
251
+ : (uses) => (0, action_fetcher_1.resolveActionSha)(uses);
252
+ // Merge every file's remote actions by `uses`.
253
+ const byUses = new Map();
254
+ for (const file of targets) {
255
+ try {
256
+ const parsed = (0, workflow_parser_1.parseFile)(file);
257
+ if (parsed.type !== 'workflow')
258
+ continue;
259
+ const inv = await (0, inventory_1.buildInventory)({ type: 'workflow', path: file, file: parsed.file }, resolver);
260
+ for (const action of inv.actions) {
261
+ const existing = byUses.get(action.uses);
262
+ if (existing)
263
+ existing.paths.push(...action.paths);
264
+ else
265
+ byUses.set(action.uses, { ...action });
266
+ }
267
+ }
268
+ catch (error) {
269
+ logger_1.logger.error(`Could not read ${file}: ${error instanceof Error ? error.message : 'Unknown error'}`);
270
+ process.exit(1);
271
+ }
272
+ }
273
+ const actions = [...byUses.values()].sort((a, b) => a.uses.localeCompare(b.uses));
274
+ const current = {};
275
+ for (const action of actions) {
276
+ if (action.resolvedSha)
277
+ current[action.uses] = action.resolvedSha;
278
+ }
279
+ // --dry-run: print the resolved inventory as JSON, touching nothing.
280
+ if (options.dryRun) {
281
+ // eslint-disable-next-line no-console
282
+ console.log(JSON.stringify({ actions }, null, 2));
283
+ return;
284
+ }
285
+ const lockPath = path.join(process.cwd(), LOCKFILE_NAME);
286
+ const lockExists = fs.existsSync(lockPath);
287
+ const writeLock = () => {
288
+ const updated = { version: 1, actions: current };
289
+ fs.writeFileSync(lockPath, JSON.stringify(updated, null, 2) + '\n');
290
+ logger_1.logger.success(`Wrote ${Object.keys(current).length} action(s) to ${LOCKFILE_NAME}. Commit it to track supply-chain drift.`);
291
+ };
292
+ // `--update-lock`, or bare `inventory` when no lockfile exists yet: write it.
293
+ if (update || (!check && !lockExists)) {
294
+ writeLock();
295
+ return;
296
+ }
297
+ // Otherwise compare (explicit `--check`, or bare `inventory` with a lockfile).
298
+ if (!lockExists) {
299
+ logger_1.logger.error(`No ${LOCKFILE_NAME} found. Create one with: secureci inventory --update-lock`);
300
+ process.exit(1);
301
+ }
302
+ const lock = JSON.parse(fs.readFileSync(lockPath, 'utf-8'));
303
+ const diff = (0, inventory_1.diffLock)(current, lock);
304
+ for (const d of diff.drifted) {
305
+ logger_1.logger.error(`Drift: ${d.uses} now resolves to ${d.to.slice(0, 12)} (was ${d.from.slice(0, 12)})`);
306
+ }
307
+ diff.added.forEach((u) => logger_1.logger.warn(`New action not in ${LOCKFILE_NAME}: ${u}`));
308
+ diff.removed.forEach((u) => logger_1.logger.info(`In ${LOCKFILE_NAME} but no longer used: ${u}`));
309
+ if (diff.drifted.length > 0) {
310
+ logger_1.logger.error(`Action commits drifted from ${LOCKFILE_NAME}. Review the change, then run with --update-lock to accept it.`);
311
+ process.exit(1);
312
+ }
313
+ if (diff.added.length > 0) {
314
+ logger_1.logger.warn(`New actions are not yet in ${LOCKFILE_NAME} (run --update-lock).`);
315
+ process.exit(1);
316
+ }
317
+ logger_1.logger.success(`No action drift against ${LOCKFILE_NAME}.`);
318
+ }
319
+ async function validateOneFile(filePath, contractFile, recurse, say) {
320
+ const parsed = (0, workflow_parser_1.parseFile)(filePath);
321
+ let result;
322
+ if (parsed.type === 'workflow') {
323
+ result = (0, validator_1.validateWorkflow)(filePath);
324
+ say(() => logger_1.logger.info(`Validating workflow: ${filePath}`));
325
+ }
326
+ else {
327
+ result = (0, validator_1.validateAction)(filePath);
328
+ say(() => logger_1.logger.info(`Validating action: ${filePath}`));
329
+ }
330
+ if (contractFile) {
331
+ say(() => logger_1.logger.info(`Validating against pipeline contract: ${contractFile}`));
332
+ if (parsed.type !== 'workflow') {
333
+ result = {
334
+ valid: false,
335
+ errors: [
336
+ ...result.errors,
337
+ {
338
+ path: 'root',
339
+ message: 'Pipeline contracts validate job/step sequences and only apply to workflow files, not actions',
340
+ severity: 'error'
341
+ }
342
+ ]
343
+ };
344
+ }
345
+ else {
346
+ const spec = (0, pipeline_contract_parser_1.parsePipelineContract)(contractFile);
347
+ const contractResult = await (0, pipeline_contract_validator_1.validatePipelineContract)(parsed, spec, {
348
+ recurse
349
+ });
350
+ result = {
351
+ valid: result.valid && contractResult.valid,
352
+ errors: [...result.errors, ...contractResult.errors]
353
+ };
354
+ }
355
+ }
356
+ return result;
357
+ }
358
+ async function validateFiles(files, contractFile, recurse = 'local', format = 'text') {
359
+ // Only text mode narrates; json/sarif write nothing but the report to
360
+ // stdout, so the output is pipeable.
361
+ const quiet = format !== 'text';
362
+ const say = (fn) => {
363
+ if (!quiet)
364
+ fn();
365
+ };
366
+ const entries = [];
367
+ for (const filePath of files) {
368
+ try {
369
+ const result = await validateOneFile(filePath, contractFile, recurse, say);
370
+ entries.push({ file: filePath, result });
371
+ }
372
+ catch (error) {
373
+ entries.push({
374
+ file: filePath,
375
+ result: {
376
+ valid: false,
377
+ errors: [
378
+ {
379
+ path: 'root',
380
+ message: error instanceof Error ? error.message : 'Unknown error',
381
+ severity: 'error'
382
+ }
383
+ ]
384
+ }
385
+ });
386
+ }
387
+ }
388
+ const allValid = entries.every((e) => e.result.valid);
389
+ if (quiet) {
390
+ // eslint-disable-next-line no-console
391
+ console.log((0, report_1.formatReport)(entries, format));
392
+ if (!allValid)
393
+ process.exit(1);
394
+ return;
395
+ }
396
+ for (const { file, result } of entries) {
397
+ if (result.valid) {
398
+ logger_1.logger.success(`Validation passed: ${file}`);
399
+ result.errors.forEach((error) => logger_1.logger.warn(` [${error.severity.toUpperCase()}] ${error.path}: ${error.message}`));
400
+ }
401
+ else {
402
+ logger_1.logger.error(`Validation failed: ${file}`);
403
+ result.errors.forEach((error) => logger_1.logger.error(` [${error.severity.toUpperCase()}] ${error.path}: ${error.message}`));
404
+ }
405
+ }
406
+ if (!allValid)
407
+ process.exit(1);
408
+ }
409
+ function runInit(options) {
410
+ const output = path.resolve(process.cwd(), options.output);
411
+ if (fs.existsSync(output) && !options.force) {
412
+ logger_1.logger.error(`${options.output} already exists; pass --force to overwrite it.`);
413
+ process.exit(1);
414
+ }
415
+ const content = (0, init_1.renderInitWorkflow)({
416
+ contract: options.contract,
417
+ upload: options.upload
418
+ });
419
+ fs.mkdirSync(path.dirname(output), { recursive: true });
420
+ fs.writeFileSync(output, content);
421
+ logger_1.logger.success(`Wrote ${options.output}`);
422
+ if (options.upload) {
423
+ logger_1.logger.info('The workflow uploads SARIF to code scanning (needs security-events: write).');
424
+ }
425
+ logger_1.logger.info('Commit it and open a pull request to see SecureCI run.');
426
+ }
427
+ function listWorkflows() {
428
+ const workflowsDir = path.join(process.cwd(), '.github', 'workflows');
429
+ if (!fs.existsSync(workflowsDir)) {
430
+ logger_1.logger.warn('No .github/workflows directory found in current directory');
431
+ return;
432
+ }
433
+ const files = fs
434
+ .readdirSync(workflowsDir)
435
+ .filter((f) => f.endsWith('.yml') || f.endsWith('.yaml'));
436
+ if (files.length === 0) {
437
+ logger_1.logger.info('No workflow files found');
438
+ return;
439
+ }
440
+ logger_1.logger.info('Available workflows:');
441
+ files.forEach((file) => {
442
+ const filePath = path.join(workflowsDir, file);
443
+ try {
444
+ const parsed = (0, workflow_parser_1.parseFile)(filePath);
445
+ if (parsed.type === 'workflow') {
446
+ logger_1.logger.info(` - ${file}: ${parsed.file.name}`);
447
+ }
448
+ }
449
+ catch {
450
+ logger_1.logger.warn(` - ${file}: (failed to parse)`);
451
+ }
452
+ });
453
+ }
454
+ const configCmd = program
455
+ .command('config')
456
+ .description('Manage global configuration (~/.secureci/config.yml)');
457
+ configCmd
458
+ .command('path')
459
+ .description('Print the config file path')
460
+ .action(() => {
461
+ logger_1.logger.info((0, config_1.getConfigFilePath)());
462
+ });
463
+ configCmd
464
+ .command('list')
465
+ .description('List config values, including built-in defaults')
466
+ .action(() => {
467
+ const config = (0, config_1.loadConfig)();
468
+ for (const key of config_1.CONFIG_KEYS) {
469
+ const value = config[key];
470
+ const fallback = config_1.DEFAULT_CONFIG[key];
471
+ logger_1.logger.info(`${key}=${value !== undefined
472
+ ? value
473
+ : fallback !== undefined
474
+ ? `${fallback} (default)`
475
+ : '(unset)'}`);
476
+ }
477
+ for (const [label, image] of Object.entries(config['runner-images'] ?? {})) {
478
+ logger_1.logger.info(`runner-images.${label}=${image}`);
479
+ }
480
+ });
481
+ configCmd
482
+ .command('get <key>')
483
+ .description('Print a config value')
484
+ .action((key) => {
485
+ if (!(0, config_1.isValidConfigKey)(key)) {
486
+ logger_1.logger.error(`Unknown config key "${key}". Valid keys: ${config_1.CONFIG_KEYS.join(', ')}, runner-images.<label>`);
487
+ process.exit(1);
488
+ }
489
+ const value = (0, config_1.getConfigValue)((0, config_1.loadConfig)(), key);
490
+ logger_1.logger.info(value === undefined ? '(unset)' : String(value));
491
+ });
492
+ configCmd
493
+ .command('set <key> <value>')
494
+ .description('Set a config value')
495
+ .action((key, value) => {
496
+ if (!(0, config_1.isValidConfigKey)(key)) {
497
+ logger_1.logger.error(`Unknown config key "${key}". Valid keys: ${config_1.CONFIG_KEYS.join(', ')}, runner-images.<label>`);
498
+ process.exit(1);
499
+ }
500
+ try {
501
+ const config = (0, config_1.loadConfig)();
502
+ const parsedValue = (0, config_1.setConfigValue)(config, key, value);
503
+ (0, config_1.saveConfig)(config);
504
+ logger_1.logger.success(`Set ${key}=${parsedValue} in ${(0, config_1.getConfigFilePath)()}`);
505
+ }
506
+ catch (error) {
507
+ logger_1.logger.error(error instanceof Error ? error.message : 'Invalid value');
508
+ process.exit(1);
509
+ }
510
+ });
511
+ configCmd
512
+ .command('unset <key>')
513
+ .description('Remove a config value, reverting to the built-in default')
514
+ .action((key) => {
515
+ if (!(0, config_1.isValidConfigKey)(key)) {
516
+ logger_1.logger.error(`Unknown config key "${key}". Valid keys: ${config_1.CONFIG_KEYS.join(', ')}, runner-images.<label>`);
517
+ process.exit(1);
518
+ }
519
+ const config = (0, config_1.loadConfig)();
520
+ (0, config_1.unsetConfigValue)(config, key);
521
+ (0, config_1.saveConfig)(config);
522
+ logger_1.logger.success(`Unset ${key} in ${(0, config_1.getConfigFilePath)()}`);
523
+ });
524
+ const cacheCmd = program
525
+ .command('cache')
526
+ .description('Manage the cache of downloaded remote actions');
527
+ cacheCmd
528
+ .command('path')
529
+ .description('Print the action cache directory')
530
+ .action(() => {
531
+ logger_1.logger.info((0, action_fetcher_1.getActionCacheDir)());
532
+ });
533
+ cacheCmd
534
+ .command('clear')
535
+ .description('Delete all cached remote actions')
536
+ .action(() => {
537
+ (0, action_fetcher_1.clearActionCache)();
538
+ });
539
+ // With no command, show help and fail, like other vendor CLIs.
540
+ if (process.argv.length <= 2) {
541
+ program.outputHelp({ error: true });
542
+ process.exit(1);
543
+ }
544
+ program.parse();
545
+ //# sourceMappingURL=cli.js.map