@oorabona/release-it-preset 0.8.0 → 0.9.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/README.md CHANGED
@@ -16,6 +16,11 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
16
16
  - [Quick Start](#quick-start)
17
17
  - [Available Configurations](#available-configurations)
18
18
  - [CLI Usage](#cli-usage)
19
+ - [Zero-Config Mode (Auto-Detection)](#zero-config-mode-auto-detection)
20
+ - [Preset Selection Mode](#preset-selection-mode)
21
+ - [Passthrough Mode (Custom Config Override)](#passthrough-mode-custom-config-override)
22
+ - [Monorepo Support](#monorepo-support)
23
+ - [Utility Commands](#utility-commands)
19
24
  - [Scripts](#scripts)
20
25
  - [Environment Variables](#environment-variables)
21
26
  - [Configuration Override](#configuration-override)
@@ -37,6 +42,9 @@ Shared [release-it](https://github.com/release-it/release-it) configuration and
37
42
  - 🔄 Republish and retry mechanisms for failed releases
38
43
  - ⚡ Hotfix release support
39
44
  - 🎯 Environment variable configuration
45
+ - 🔍 **NEW v0.9.0:** Zero-config auto-detection mode
46
+ - 🏢 **NEW v0.9.0:** Monorepo support with parent directory config references
47
+ - ⚙️ **NEW v0.9.0:** Passthrough mode for custom config files
40
48
 
41
49
  ## Installation
42
50
 
@@ -331,9 +339,42 @@ Features:
331
339
 
332
340
  ## CLI Usage
333
341
 
334
- The package provides a `release-it-preset` CLI with two types of commands:
342
+ The package provides a `release-it-preset` CLI with four operating modes:
335
343
 
336
- ### Release Commands
344
+ 1. **Zero-Config Mode** (auto-detection) - No arguments needed
345
+ 2. **Preset Selection Mode** - Specify which preset to use
346
+ 3. **Passthrough Mode** - Direct config file override
347
+ 4. **Utility Mode** - Helper commands
348
+
349
+ ### Zero-Config Mode (Auto-Detection)
350
+
351
+ **NEW in v0.9.0** - The CLI can automatically detect which preset to use from your `.release-it.json`:
352
+
353
+ ```bash
354
+ # Just run release-it-preset with no arguments
355
+ pnpm release-it-preset
356
+
357
+ # 🔍 Auto-detected preset: default
358
+ # ✅ Config validated: preset "default"
359
+ # 📝 Using: /path/to/.release-it.json
360
+ ```
361
+
362
+ **How it works:**
363
+ 1. CLI reads your `.release-it.json`
364
+ 2. Extracts the preset name from the `extends` field
365
+ 3. Runs that preset automatically
366
+
367
+ **Requirements:**
368
+ - `.release-it.json` must exist
369
+ - Must have `extends` field like `"@oorabona/release-it-preset/config/default"`
370
+
371
+ **Benefits:**
372
+ - ✅ Shortest command possible
373
+ - ✅ Config file is source of truth
374
+ - ✅ No need to remember preset names
375
+ - ✅ Follows industry standards (ESLint, TypeScript, Prettier)
376
+
377
+ ### Preset Selection Mode
337
378
 
338
379
  Run release-it with specific configurations:
339
380
 
@@ -350,6 +391,92 @@ pnpm release-it-preset manual-changelog
350
391
 
351
392
  All additional arguments are passed through to release-it.
352
393
 
394
+ ### Passthrough Mode (Custom Config Override)
395
+
396
+ **NEW in v0.9.0** - Use a custom config file and bypass preset validation:
397
+
398
+ ```bash
399
+ # Use custom config file
400
+ pnpm release-it-preset --config .release-it-manual.json
401
+
402
+ # 🔀 Passthrough mode: using config .release-it-manual.json
403
+ # Bypassing preset validation - direct release-it invocation
404
+ ```
405
+
406
+ **Use cases:**
407
+ - **Switching presets occasionally** - Have multiple config files for different scenarios
408
+ - **Monorepo workflows** - Reference shared configs from parent directories
409
+ - **Advanced customization** - Full control over release-it configuration
410
+
411
+ **Example workflow:**
412
+
413
+ ```json
414
+ // .release-it.json (default - 95% of time)
415
+ {
416
+ "extends": "@oorabona/release-it-preset/config/default",
417
+ "git": { "requireBranch": "develop" }
418
+ }
419
+
420
+ // .release-it-manual.json (rare - 5% of time)
421
+ {
422
+ "extends": "@oorabona/release-it-preset/config/manual-changelog",
423
+ "git": { "requireBranch": "develop" }
424
+ }
425
+ ```
426
+
427
+ ```bash
428
+ # Normal release
429
+ pnpm release-it-preset # Auto-detects default
430
+
431
+ # Manual changelog release (rare)
432
+ pnpm release-it-preset --config .release-it-manual.json
433
+ ```
434
+
435
+ **Benefits:**
436
+ - ✅ No need to edit `.release-it.json` to switch presets
437
+ - ✅ Config files are explicit and version-controlled
438
+ - ✅ Works with monorepo parent directory references
439
+
440
+ ### Monorepo Support
441
+
442
+ **NEW in v0.9.0** - Parent directory config references are now supported:
443
+
444
+ ```bash
445
+ # Monorepo structure
446
+ /my-monorepo/
447
+ ├── .release-it-base.json # Shared configuration
448
+ ├── packages/
449
+ │ ├── core/
450
+ │ │ └── .release-it.json # extends: ../../.release-it-base.json
451
+ │ └── utils/
452
+ │ └── .release-it.json # extends: ../../.release-it-base.json
453
+ ```
454
+
455
+ ```json
456
+ // packages/core/.release-it.json
457
+ {
458
+ "extends": [
459
+ "../../.release-it-base.json", // ✅ Parent reference allowed!
460
+ "@oorabona/release-it-preset/config/default"
461
+ ]
462
+ }
463
+ ```
464
+
465
+ **Security validation:**
466
+ - ✅ Parent directory references (`../`) supported (up to 5 levels)
467
+ - ✅ Config file extension whitelist (`.json`, `.js`, `.cjs`, `.mjs`, `.yaml`, `.yml`, `.toml`)
468
+ - ✅ File existence validation
469
+ - ❌ Absolute paths blocked (use relative paths)
470
+ - ❌ Excessive traversal blocked (max `../../../../../../`)
471
+
472
+ **Why this is safe:**
473
+ - Config files are trusted code (developer controls the repository)
474
+ - Industry standard pattern (TypeScript, ESLint, Prettier all allow `../`)
475
+ - Multiple validation layers prevent abuse
476
+ - No privilege escalation in CLI tool context
477
+
478
+ See [examples/monorepo-workflow.md](examples/monorepo-workflow.md) for complete monorepo guide.
479
+
353
480
  ### Utility Commands
354
481
 
355
482
  Helper commands for project setup and maintenance:
@@ -564,16 +691,16 @@ pnpm release
564
691
 
565
692
  ## Configuration Modes
566
693
 
567
- The preset supports three configuration modes to suit different workflows:
694
+ The preset supports two configuration modes:
568
695
 
569
- ### Mode 1: CLI Only (No Config File)
696
+ ### Mode 1: Direct Preset Usage (No Config File)
570
697
 
571
- **When to use:** Simple projects with minimal customization needs
698
+ **When to use:** Simple projects, trust preset defaults, or customize only via environment variables
572
699
 
573
700
  Don't create `.release-it.json`. Just run the CLI:
574
701
 
575
702
  ```bash
576
- pnpm release-it-preset hotfix
703
+ pnpm release-it-preset default
577
704
  ```
578
705
 
579
706
  All configuration comes from the preset and environment variables.
@@ -582,23 +709,22 @@ All configuration comes from the preset and environment variables.
582
709
  - ✅ Zero config files
583
710
  - ✅ Consistent behavior across projects
584
711
  - ✅ Easy to understand
712
+ - ✅ Perfect for getting started
585
713
 
586
714
  ---
587
715
 
588
- ### Mode 2: CLI + User Overrides (Recommended)
716
+ ### Mode 2: Preset + User Overrides (Recommended)
589
717
 
590
- **When to use:** Customize specific options while using CLI presets
718
+ **When to use:** Customize specific options while keeping preset defaults
591
719
 
592
- Create `.release-it.json` **WITHOUT the `extends` field**:
720
+ Create `.release-it.json` **WITH the `extends` field**:
593
721
 
594
722
  ```json
595
723
  {
724
+ "extends": "@oorabona/release-it-preset/config/default",
596
725
  "git": {
597
726
  "requireBranch": "master",
598
727
  "commitMessage": "chore: release v${version}"
599
- },
600
- "npm": {
601
- "publish": true
602
728
  }
603
729
  }
604
730
  ```
@@ -606,24 +732,26 @@ Create `.release-it.json` **WITHOUT the `extends` field**:
606
732
  Run with CLI preset:
607
733
 
608
734
  ```bash
609
- pnpm release-it-preset hotfix
735
+ pnpm release-it-preset default
610
736
  ```
611
737
 
612
738
  **How it works:**
613
- - CLI selects the preset (`hotfix` in this example)
614
- - release-it merges your overrides on top of the preset
739
+ - The `extends` field loads the preset
740
+ - release-it merges your overrides on top via c12
615
741
  - **Your values take precedence** over preset defaults
742
+ - CLI validates that `extends` matches the command
616
743
 
617
744
  **Pros:**
618
- - ✅ **Recommended approach** for most use cases
619
- - ✅ CLI controls preset selection
620
- - ✅ Declarative overrides in config file
621
- - ✅ No `extends` maintenance needed
745
+ - ✅ **Recommended for customization**
746
+ - ✅ Declarative config with explicit preset
747
+ - ✅ Industry standard pattern (like ESLint, TypeScript)
748
+ - ✅ Guaranteed config merging via release-it's c12
622
749
 
623
- **Example use case:** You want to use the `hotfix` preset but require releases from `master` instead of `main`:
750
+ **Example:** Using `hotfix` preset but release from `master` instead of `main`:
624
751
 
625
752
  ```json
626
753
  {
754
+ "extends": "@oorabona/release-it-preset/config/hotfix",
627
755
  "git": {
628
756
  "requireBranch": "master"
629
757
  }
@@ -631,84 +759,73 @@ pnpm release-it-preset hotfix
631
759
  ```
632
760
 
633
761
  ```bash
634
- pnpm release-it-preset hotfix # Uses hotfix preset + your branch override
762
+ pnpm release-it-preset hotfix
635
763
  ```
636
764
 
637
765
  ---
638
766
 
639
- ### Mode 3: File with Extends (Advanced)
767
+ ### Configuration Validation
640
768
 
641
- **When to use:** Lock a specific preset regardless of CLI command
769
+ The CLI validates your `.release-it.json` to prevent misconfigurations:
642
770
 
643
- Create `.release-it.json` **WITH the `extends` field**:
771
+ #### Error 1: Missing `extends` field
644
772
 
645
- ```json
773
+ ```bash
774
+ # .release-it.json without extends:
646
775
  {
647
- "extends": "@oorabona/release-it-preset/config/hotfix",
648
- "git": {
649
- "commitMessage": "custom: ${version}"
650
- }
776
+ "git": { "requireBranch": "master" }
651
777
  }
652
- ```
653
778
 
654
- Run matching CLI command:
779
+ # Running:
780
+ pnpm release-it-preset default
655
781
 
656
- ```bash
657
- pnpm release-it-preset hotfix # Must match the extends!
782
+ # ❌ Configuration error!
783
+ # .release-it.json is missing the required "extends" field.
784
+ #
785
+ # Without "extends", your config won't merge with the preset.
786
+ # This means you'll get release-it defaults instead of preset defaults.
787
+ #
788
+ # Fix by adding this to .release-it.json:
789
+ # {
790
+ # "extends": "@oorabona/release-it-preset/config/default",
791
+ # ...your overrides
792
+ # }
658
793
  ```
659
794
 
660
- **How it works:**
661
- - The `extends` field locks the preset
662
- - CLI command **must match** the preset in `extends`
663
- - Mismatch triggers an error
664
-
665
- **Pros:**
666
- - ✅ Prevents accidental use of wrong presets
667
- - ✅ Explicit preset declaration in config
795
+ **Why `extends` is required:** Without it, release-it only loads your config file and uses release-it's own defaults. The preset is never loaded, so you lose important defaults like `npm.publish: false`.
668
796
 
669
- **Cons:**
670
- - ⚠️ Less flexible (preset locked in file)
671
- - ⚠️ Requires updating `extends` to switch presets
672
-
673
- ---
674
-
675
- ### Configuration Error Handling
676
-
677
- If your `.release-it.json` has an `extends` field that doesn't match the CLI command, you'll get a clear error:
797
+ #### Error 2: Preset mismatch
678
798
 
679
799
  ```bash
680
- # Your config extends "default", but you run:
800
+ # .release-it.json extends "default":
801
+ {
802
+ "extends": "@oorabona/release-it-preset/config/default"
803
+ }
804
+
805
+ # But you run:
681
806
  pnpm release-it-preset hotfix
682
807
 
683
808
  # ❌ Configuration mismatch error!
684
809
  # CLI preset: hotfix
685
810
  # .release-it.json extends: default
686
811
  #
687
- # Either:
688
- # 1. Remove the "extends" field from .release-it.json (recommended)
689
- # Your overrides will merge with the CLI preset automatically
690
- #
691
- # 2. Run: release-it-preset default
692
- # → Use the preset specified in your config file
693
- #
694
- # 3. Update .release-it.json extends to: "@oorabona/release-it-preset/config/hotfix"
695
- # → Match your config file to the CLI command
812
+ # Either:
813
+ # 1. Run: release-it-preset default
814
+ # 2. Update .release-it.json extends to: "@oorabona/release-it-preset/config/hotfix"
696
815
  ```
697
816
 
698
- This prevents silent misconfigurations where the wrong preset runs unexpectedly.
699
-
700
817
  ---
701
818
 
702
819
  ### Which Mode Should I Use?
703
820
 
704
821
  | Scenario | Recommended Mode |
705
822
  |----------|------------------|
706
- | Minimal config, trust defaults | **Mode 1** (CLI only) |
707
- | Customize branch/commit messages | **Mode 2** (CLI + overrides) |
708
- | Lock preset for safety | **Mode 3** (File with extends) |
709
- | Monorepo with different presets per package | **Mode 2** (CLI + overrides) |
823
+ | Quick start, minimal config | **Mode 1** (No config file) |
824
+ | Customize branch/commit/hooks | **Mode 2** (Config with extends) |
825
+ | Environment-only customization | **Mode 1** (Use env vars) |
826
+ | Monorepo with per-package config | **Mode 2** (Each package has own config) |
710
827
 
711
- **Most users should use Mode 2** for the best balance of flexibility and clarity.
828
+ **Use Mode 1 to get started, switch to Mode 2 when you need customization.**
712
829
 
713
830
  ## Borrowing Scripts & Workflows
714
831
 
package/bin/cli.js CHANGED
@@ -27,7 +27,7 @@ import { spawn } from 'node:child_process';
27
27
  import { existsSync, readFileSync } from 'node:fs';
28
28
  import { fileURLToPath } from 'node:url';
29
29
  import { dirname, join } from 'node:path';
30
- import { validateConfigName, validateUtilityCommand, sanitizeArgs } from './validators.js';
30
+ import { validateConfigName, validateUtilityCommand, sanitizeArgs, validateConfigPath } from './validators.js';
31
31
 
32
32
  const __filename = fileURLToPath(import.meta.url);
33
33
  const __dirname = dirname(__filename);
@@ -54,7 +54,14 @@ const UTILITY_COMMANDS = {
54
54
 
55
55
  function showHelp() {
56
56
  console.log(`
57
- Usage: release-it-preset <command> [...args]
57
+ Usage: release-it-preset [command] [...args]
58
+ release-it-preset --config <file> [...args]
59
+
60
+ CLI Modes:
61
+ 1. Auto-detection (no command) - Reads preset from .release-it.json
62
+ 2. Preset selection - Specify preset command
63
+ 3. Passthrough (--config) - Direct config file, bypass validation
64
+ 4. Utility commands - Helper scripts
58
65
 
59
66
  Release Commands:
60
67
  default Full release with changelog, git, GitHub, and npm
@@ -73,19 +80,28 @@ Utility Commands:
73
80
  check-pr Evaluate PR hygiene (branch diff, changelog status, conventions)
74
81
  retry-publish-preflight Run retry publish safety checks without executing release
75
82
 
83
+ Passthrough Mode:
84
+ --config <file> Use custom config file, bypass preset validation
85
+
76
86
  Examples:
87
+ # Zero-config (auto-detect from .release-it.json)
88
+ release-it-preset
89
+
77
90
  # Release commands
78
91
  release-it-preset default --dry-run
79
92
  release-it-preset hotfix --verbose
80
93
  release-it-preset changelog-only --ci
81
94
 
95
+ # Passthrough mode (custom config)
96
+ release-it-preset --config .release-it-manual.json
97
+
98
+ # Monorepo (parent config reference)
99
+ release-it-preset --config ../../.release-it-base.json
100
+
82
101
  # Utility commands
83
102
  release-it-preset init
84
103
  release-it-preset update
85
104
  release-it-preset validate
86
- release-it-preset check
87
- release-it-preset check-pr
88
- release-it-preset retry-publish-preflight
89
105
 
90
106
  For release-it options, see: https://github.com/release-it/release-it
91
107
  For environment variables, see: https://github.com/oorabona/release-it-preset#environment-variables
@@ -119,36 +135,44 @@ function handleReleaseCommand(configName, args) {
119
135
 
120
136
  const expectedExtends = `@oorabona/release-it-preset/config/${configName}`;
121
137
 
122
- if (userConfig.extends) {
123
- // If extends exists, it MUST match the CLI preset
124
- const extendsMatch = userConfig.extends.match(/@oorabona\/release-it-preset\/config\/(\w+)/);
125
- const extendsPreset = extendsMatch?.[1];
126
-
127
- if (extendsPreset && extendsPreset !== configName) {
128
- console.error(`\n❌ Configuration mismatch error!`);
129
- console.error(` CLI preset: ${configName}`);
130
- console.error(` .release-it.json extends: ${extendsPreset}`);
131
- console.error(``);
132
- console.error(`Either:`);
133
- console.error(` 1. Remove the "extends" field from .release-it.json (recommended)`);
134
- console.error(` → Your overrides will merge with the CLI preset automatically`);
135
- console.error(``);
136
- console.error(` 2. Run: release-it-preset ${extendsPreset}`);
137
- console.error(` → Use the preset specified in your config file`);
138
- console.error(``);
139
- console.error(` 3. Update .release-it.json extends to: "${expectedExtends}"`);
140
- console.error(` → Match your config file to the CLI command\n`);
141
- process.exit(1);
142
- }
143
-
144
- console.log(`✅ Config file extends matches CLI preset: ${configName}`);
145
- console.log(`📝 Using: ${userConfigPath}\n`);
146
- } else {
147
- // No extends = natural merge (Mode 3 - Hybrid)
148
- console.log(`📝 Merging CLI preset "${configName}" with user config overrides`);
149
- console.log(` Config file: ${userConfigPath}`);
150
- console.log(` User overrides will take precedence\n`);
138
+ if (!userConfig.extends) {
139
+ // ERROR: extends is required for config merging
140
+ console.error(`\n❌ Configuration error!`);
141
+ console.error(` .release-it.json is missing the required "extends" field.`);
142
+ console.error(``);
143
+ console.error(`Without "extends", your config won't merge with the preset.`);
144
+ console.error(`This means you'll get release-it defaults instead of preset defaults.`);
145
+ console.error(``);
146
+ console.error(`Fix by adding this to .release-it.json:`);
147
+ console.error(` {`);
148
+ console.error(` "extends": "${expectedExtends}",`);
149
+ console.error(` ...your overrides`);
150
+ console.error(` }`);
151
+ console.error(``);
152
+ console.error(`Or remove .release-it.json to use the preset directly.\n`);
153
+ process.exit(1);
154
+ }
155
+
156
+ // Validate extends matches CLI preset
157
+ const extendsMatch = userConfig.extends.match(/@oorabona\/release-it-preset\/config\/([\w-]+)/);
158
+ const extendsPreset = extendsMatch?.[1];
159
+
160
+ if (extendsPreset && extendsPreset !== configName) {
161
+ console.error(`\n❌ Configuration mismatch error!`);
162
+ console.error(` CLI preset: ${configName}`);
163
+ console.error(` .release-it.json extends: ${extendsPreset}`);
164
+ console.error(``);
165
+ console.error(`Either:`);
166
+ console.error(` 1. Run: release-it-preset ${extendsPreset}`);
167
+ console.error(` → Use the preset specified in your config file`);
168
+ console.error(``);
169
+ console.error(` 2. Update .release-it.json extends to: "${expectedExtends}"`);
170
+ console.error(` → Match your config file to the CLI command\n`);
171
+ process.exit(1);
151
172
  }
173
+
174
+ console.log(`✅ Config validated: preset "${configName}"`);
175
+ console.log(`📝 Using: ${userConfigPath}\n`);
152
176
  } catch (error) {
153
177
  if (error instanceof SyntaxError) {
154
178
  console.error(`❌ Failed to parse .release-it.json: ${error.message}`);
@@ -158,12 +182,12 @@ function handleReleaseCommand(configName, args) {
158
182
  process.exit(1);
159
183
  }
160
184
 
161
- // Let release-it handle the merge naturally
185
+ // Let release-it discover .release-it.json and merge via extends
162
186
  fullArgs = [...args];
163
187
  } else {
164
188
  // No user config - use preset directly
165
189
  console.log(`📝 Using preset config directly: ${configPath}`);
166
- console.log(` Tip: Create .release-it.json (without "extends") to add overrides\n`);
190
+ console.log(` Tip: Create .release-it.json with "extends" to customize\n`);
167
191
  fullArgs = ['--config', configPath, ...args];
168
192
  }
169
193
 
@@ -229,10 +253,53 @@ function handleUtilityCommand(commandName, args) {
229
253
  });
230
254
  }
231
255
 
256
+ function passthroughToReleaseIt(args) {
257
+ // Extract --config value
258
+ const configIndex = args.indexOf('--config');
259
+ if (configIndex === -1 || configIndex === args.length - 1) {
260
+ console.error('❌ --config requires a file path argument\n');
261
+ console.error('Usage: release-it-preset --config <file>');
262
+ process.exit(1);
263
+ }
264
+
265
+ const configPath = args[configIndex + 1];
266
+
267
+ // Security validation
268
+ try {
269
+ validateConfigPath(configPath);
270
+ sanitizeArgs(args);
271
+
272
+ console.log(`🔀 Passthrough mode: using config ${configPath}`);
273
+ console.log(` Bypassing preset validation - direct release-it invocation\n`);
274
+ } catch (error) {
275
+ console.error(`❌ Configuration validation failed: ${error.message}`);
276
+ process.exit(1);
277
+ }
278
+
279
+ // Delegate to release-it
280
+ const releaseItCommand = 'release-it';
281
+ const child = spawn(releaseItCommand, args, {
282
+ stdio: 'inherit',
283
+ shell: false, // Security: prevent command injection
284
+ });
285
+
286
+ child.on('error', (error) => {
287
+ console.error(`❌ Failed to start release-it: ${error.message}`);
288
+ console.error(`\nMake sure release-it is installed:`);
289
+ console.error(` pnpm add -D release-it`);
290
+ process.exit(1);
291
+ });
292
+
293
+ child.on('close', (code) => {
294
+ process.exit(code ?? 0);
295
+ });
296
+ }
297
+
232
298
  function main() {
233
299
  const args = process.argv.slice(2);
234
300
 
235
- if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
301
+ // Handle --help
302
+ if (args.includes('--help') || args.includes('-h')) {
236
303
  showHelp();
237
304
  process.exit(0);
238
305
  }
@@ -240,13 +307,66 @@ function main() {
240
307
  const command = args[0];
241
308
  const commandArgs = args.slice(1);
242
309
 
243
- // Check if it's a release config
310
+ // Check for conflicting arguments (preset command + --config)
311
+ if (args.includes('--config') && RELEASE_CONFIGS[command]) {
312
+ console.error('❌ Conflicting arguments detected!\n');
313
+ console.error(' You specified both a preset command and --config flag.');
314
+ console.error('');
315
+ console.error(' Either:');
316
+ console.error(` 1. Use preset: release-it-preset ${command}`);
317
+ console.error(` 2. Use config: release-it-preset --config <file>`);
318
+ console.error('');
319
+ console.error(' Do not mix both approaches.');
320
+ process.exit(1);
321
+ }
322
+
323
+ // MODE 1: Passthrough - Direct release-it with custom config
324
+ if (args.includes('--config')) {
325
+ passthroughToReleaseIt(args);
326
+ return;
327
+ }
328
+
329
+ // MODE 2: Auto-detection - No arguments, read preset from .release-it.json
330
+ if (args.length === 0) {
331
+ const userConfigPath = join(process.cwd(), '.release-it.json');
332
+
333
+ if (!existsSync(userConfigPath)) {
334
+ console.error('❌ No command specified and no .release-it.json found\n');
335
+ console.error('Either:');
336
+ console.error(' 1. Run: release-it-preset init');
337
+ console.error(' 2. Run: release-it-preset <command>\n');
338
+ showHelp();
339
+ process.exit(1);
340
+ }
341
+
342
+ try {
343
+ const config = JSON.parse(readFileSync(userConfigPath, 'utf8'));
344
+ const extendsMatch = config.extends?.match(/@oorabona\/release-it-preset\/config\/([\w-]+)/);
345
+
346
+ if (!extendsMatch) {
347
+ console.error('❌ .release-it.json does not extend a known preset\n');
348
+ console.error('Expected extends field like:');
349
+ console.error(' "@oorabona/release-it-preset/config/default"\n');
350
+ process.exit(1);
351
+ }
352
+
353
+ const preset = extendsMatch[1];
354
+ console.log(`🔍 Auto-detected preset: ${preset}`);
355
+ handleReleaseCommand(preset, []);
356
+ return;
357
+ } catch (error) {
358
+ console.error(`❌ Error reading .release-it.json: ${error.message}`);
359
+ process.exit(1);
360
+ }
361
+ }
362
+
363
+ // MODE 3: Preset command (existing logic)
244
364
  if (RELEASE_CONFIGS[command]) {
245
365
  handleReleaseCommand(command, commandArgs);
246
366
  return;
247
367
  }
248
368
 
249
- // Check if it's a utility command
369
+ // MODE 4: Utility command (existing logic)
250
370
  if (UTILITY_COMMANDS[command]) {
251
371
  handleUtilityCommand(command, commandArgs);
252
372
  return;
@@ -256,6 +376,7 @@ function main() {
256
376
  console.error(`❌ Unknown command: ${command}`);
257
377
  console.error(`\nAvailable release configs: ${Object.keys(RELEASE_CONFIGS).join(', ')}`);
258
378
  console.error(`Available utility commands: ${Object.keys(UTILITY_COMMANDS).join(', ')}`);
379
+ console.error(`\nFor direct config file usage: release-it-preset --config <file>`);
259
380
  console.error(`\nRun 'release-it-preset --help' for more information.`);
260
381
  process.exit(1);
261
382
  }
package/bin/validators.js CHANGED
@@ -10,6 +10,9 @@
10
10
  * - Fail securely
11
11
  */
12
12
 
13
+ import { existsSync, statSync } from 'node:fs';
14
+ import { extname, isAbsolute, resolve } from 'node:path';
15
+
13
16
  /**
14
17
  * Validates that a config name is in the allowed list
15
18
  *
@@ -96,26 +99,75 @@ export function sanitizeArgs(args) {
96
99
  }
97
100
 
98
101
  /**
99
- * Validates that a path does not contain directory traversal attempts
102
+ * Validates config file paths with monorepo support
103
+ *
104
+ * Security approach: Defense in depth with multiple validation layers
105
+ * - Whitelist allowed file extensions
106
+ * - Limit parent directory traversal depth (monorepo support)
107
+ * - Reject absolute paths from CLI
108
+ * - Validate file existence
109
+ *
110
+ * Why we allow ".." (parent directory references):
111
+ * - Standard pattern in monorepos (TypeScript, ESLint, Prettier all allow it)
112
+ * - Developer controls the environment (config files are trusted code boundary)
113
+ * - No privilege escalation possible in CLI tool context
114
+ * - Multiple validation layers prevent abuse
100
115
  *
101
- * @param {string} path - The path to validate
102
- * @throws {Error} If path contains traversal patterns
103
- * @returns {string} The validated path
116
+ * @param {string} configPath - The config file path to validate (relative or absolute)
117
+ * @throws {Error} If validation fails (invalid extension, too deep, missing file, etc.)
118
+ * @returns {string} Absolute path to validated config file
104
119
  */
105
- export function validatePath(path) {
106
- // Check for directory traversal patterns
107
- if (path.includes('..')) {
120
+ export function validateConfigPath(configPath) {
121
+ // 1. Whitelist config file extensions (defense in depth)
122
+ const allowedExtensions = ['.json', '.js', '.cjs', '.mjs', '.yaml', '.yml', '.toml'];
123
+ const ext = extname(configPath).toLowerCase();
124
+
125
+ if (!allowedExtensions.includes(ext)) {
126
+ throw new Error(
127
+ `Invalid config file extension: "${ext}"\n` +
128
+ `Allowed: ${allowedExtensions.join(', ')}\n` +
129
+ `This restriction prevents reading non-config files.`
130
+ );
131
+ }
132
+
133
+ // 2. Limit parent directory traversal depth (max 5 levels for monorepo support)
134
+ const upwardLevels = (configPath.match(/\.\.\//g) || []).length;
135
+ if (upwardLevels > 5) {
136
+ throw new Error(
137
+ `Too many parent directory references: ${upwardLevels}\n` +
138
+ `Maximum allowed: 5 levels (../../../../../../)\n` +
139
+ `This prevents accidental access to system directories.`
140
+ );
141
+ }
142
+
143
+ // 3. Reject absolute paths from CLI (could reference system files)
144
+ if (isAbsolute(configPath)) {
145
+ throw new Error(
146
+ `Absolute paths not allowed: "${configPath}"\n` +
147
+ `Use relative paths from your project directory.\n` +
148
+ `For monorepos, use parent references like ../../config.json`
149
+ );
150
+ }
151
+
152
+ // 4. Resolve to absolute path and validate file exists
153
+ const resolved = resolve(process.cwd(), configPath);
154
+
155
+ if (!existsSync(resolved)) {
108
156
  throw new Error(
109
- `Path contains directory traversal pattern (..) which is not allowed: "${path}"`
157
+ `Config file not found: "${configPath}"\n` +
158
+ `Resolved to: ${resolved}\n` +
159
+ `Check that the file exists and the path is correct.`
110
160
  );
111
161
  }
112
162
 
113
- // Check for absolute paths (we expect relative paths)
114
- if (path.startsWith('/') || /^[a-zA-Z]:/.test(path)) {
163
+ // 5. Validate it's a file (not a directory or symlink to avoid confusion)
164
+ const stats = statSync(resolved);
165
+ if (!stats.isFile()) {
115
166
  throw new Error(
116
- `Absolute paths are not allowed: "${path}"`
167
+ `Config path must be a file, not a directory: "${configPath}"\n` +
168
+ `Resolved to: ${resolved}`
117
169
  );
118
170
  }
119
171
 
120
- return path;
172
+ return resolved;
121
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oorabona/release-it-preset",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Shared release-it configuration and scripts for the organisation",
5
5
  "type": "module",
6
6
  "keywords": [