nginx-pretty 1.0.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/LICENSE +21 -0
- package/README.md +185 -0
- package/beautifier/index.js +221 -0
- package/bin/nginx-pretty +3 -0
- package/cli.js +246 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
# ๐ก๏ธ nginx-pretty
|
|
2
|
+
|
|
3
|
+
A **standalone CLI utility** to safely format and apply NGINX configuration files using the embedded logic of [`nginxbeautifier`](https://www.npmjs.com/package/nginxbeautifier). Designed to work in secure environments **without modifying original files unless explicitly approved**, and **without needing global Node or NPM dependencies**.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## ๐ Features
|
|
8
|
+
|
|
9
|
+
- ๐ Reads and formats: `/etc/nginx/sites-available/default` (or custom path via `--file`)
|
|
10
|
+
- ๐ก๏ธ Never modifies the original unless confirmed
|
|
11
|
+
- ๐ Shows unified diff before applying
|
|
12
|
+
- ๐งช Runs `nginx -t` to validate before reload
|
|
13
|
+
- ๐งฐ Works with `sudo`, in disconnected or hardened servers
|
|
14
|
+
- ๐งฑ CLI structure is standalone, can be bundled with [`pkg`](https://github.com/vercel/pkg)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## ๐งฑ Directory Structure
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
nginx-pretty/
|
|
22
|
+
โโโ cli.js # Main script with embedded formatting logic
|
|
23
|
+
โโโ beautifier/ # Vendored logic from nginxbeautifier
|
|
24
|
+
โ โโโ index.js # Main formatter engine
|
|
25
|
+
โโโ bin/
|
|
26
|
+
โ โโโ nginx-pretty # Optional wrapper bash script
|
|
27
|
+
โโโ README.md
|
|
28
|
+
โโโ package.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## โ๏ธ Usage
|
|
34
|
+
|
|
35
|
+
### Global install (recommended)
|
|
36
|
+
|
|
37
|
+
Install globally with npm, then run with `sudo` when modifying system NGINX config:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
npm install -g nginx-pretty
|
|
41
|
+
sudo nginx-pretty
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### As Bash Script (with embedded Node)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
sudo ./bin/nginx-pretty
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Via npx
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx nginx-pretty
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### If Bundled as a Binary (using pkg)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
sudo ./nginx-pretty
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Options
|
|
63
|
+
|
|
64
|
+
| Option | Description |
|
|
65
|
+
|--------|-------------|
|
|
66
|
+
| `--file`, `-f <path>` | Config file path (default: `/etc/nginx/sites-available/default`) |
|
|
67
|
+
| `--check-only` | Show diff only, do not prompt or apply |
|
|
68
|
+
| `--dry-run` | Same as `--check-only` |
|
|
69
|
+
| `--no-prompt` | Apply without prompting (for CI; use with caution) |
|
|
70
|
+
| `--help`, `-h` | Show help |
|
|
71
|
+
|
|
72
|
+
### Help
|
|
73
|
+
|
|
74
|
+
To print usage and options to the terminal:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
nginx-pretty --help
|
|
78
|
+
# or
|
|
79
|
+
nginx-pretty -h
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Output:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
nginx-pretty - Safely format and apply NGINX config files
|
|
86
|
+
|
|
87
|
+
Usage: nginx-pretty [OPTIONS]
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
--file, -f <path> Config file path (default: /etc/nginx/sites-available/default)
|
|
91
|
+
--check-only Show diff only, do not prompt or apply
|
|
92
|
+
--dry-run Same as --check-only
|
|
93
|
+
--no-prompt Apply without prompting (for CI; use with caution)
|
|
94
|
+
--help, -h Show this help
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
---
|
|
98
|
+
|
|
99
|
+
## ๐ Example Flow
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
๐งช Running nginx-pretty...
|
|
103
|
+
๐ Copying original file to: /tmp/tmp_nginx_formatted.conf
|
|
104
|
+
๐ Ensuring temp file is writable...
|
|
105
|
+
๐จ Formatting the copied config...
|
|
106
|
+
|
|
107
|
+
๐ Showing unified diff (ORIGINAL vs FORMATTED):
|
|
108
|
+
...
|
|
109
|
+
diff output...
|
|
110
|
+
|
|
111
|
+
โ ๏ธ Do you want to apply the formatted config? (yes/no): yes
|
|
112
|
+
๐ Config test passed. You can now reload NGINX
|
|
113
|
+
Run: sudo service nginx reload to apply the changes
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## ๐ ๏ธ Build Instructions (Optional)
|
|
119
|
+
|
|
120
|
+
To package this into a true standalone binary:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm install
|
|
124
|
+
npm install -g pkg
|
|
125
|
+
pkg cli.js --output nginx-pretty
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Or use the npm script:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npm run build:pkg
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> โ
This produces a native binary: no Node required on the target machine.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## ๐งช How to test
|
|
139
|
+
|
|
140
|
+
From the project directory (no global install needed):
|
|
141
|
+
|
|
142
|
+
**1. Help**
|
|
143
|
+
```bash
|
|
144
|
+
node cli.js --help
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**2. Dry-run (no nginx required)**
|
|
148
|
+
Use a temporary config so nothing is modified. You should see a unified diff and โDry run / check-only: not applying.โ
|
|
149
|
+
```bash
|
|
150
|
+
echo 'server { listen 80; root /var/www; }' > /tmp/test-nginx.conf
|
|
151
|
+
node cli.js --file /tmp/test-nginx.conf --dry-run
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**3. Optional: real NGINX config**
|
|
155
|
+
If you have NGINX and a config (e.g. `/etc/nginx/sites-available/default`), run with `--dry-run` first to only view the diff:
|
|
156
|
+
```bash
|
|
157
|
+
sudo node cli.js --file /etc/nginx/sites-available/default --dry-run
|
|
158
|
+
```
|
|
159
|
+
Without `--dry-run`, the CLI will prompt to apply; answer `yes` only if you want to overwrite the file.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## ๐ Security Notes
|
|
164
|
+
|
|
165
|
+
- This CLI never overwrites original config unless approved.
|
|
166
|
+
- Temporary files are cleaned and written to `/tmp`.
|
|
167
|
+
- Requires `sudo` for NGINX reload steps.
|
|
168
|
+
- Uses `spawn` with array arguments only; no user input is passed to shell.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## ๐ฆ Dependencies
|
|
173
|
+
|
|
174
|
+
Vendored internally:
|
|
175
|
+
- [`nginxbeautifier`](https://github.com/vasilevich/nginxbeautifier) (Apache 2.0)
|
|
176
|
+
|
|
177
|
+
Runtime:
|
|
178
|
+
- Node.js 14+ (or embedded via `pkg`)
|
|
179
|
+
- `diff` on PATH (for unified diff; standard on Linux)
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## ๐ License
|
|
184
|
+
|
|
185
|
+
MIT ยฉ You
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from nginxbeautifier (https://github.com/vasilevich/nginxbeautifier)
|
|
3
|
+
* Original: Ported by Yosef from https://github.com/1connect/nginx-config-formatter (nginxfmt.py)
|
|
4
|
+
* License: Apache-2.0
|
|
5
|
+
*
|
|
6
|
+
* Adapted for nginx-pretty: 4-space indent, format() export, polyfills removed (Node 14+).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Grabs text in between two separators
|
|
11
|
+
*/
|
|
12
|
+
function extractTextBySeperator(input, seperator1, seperator2) {
|
|
13
|
+
if (seperator2 == undefined) seperator2 = seperator1;
|
|
14
|
+
const catchRegex = new RegExp(seperator1 + '(.*?)' + seperator2);
|
|
15
|
+
if (new RegExp(seperator1).test(input) && new RegExp(seperator2).test(input)) {
|
|
16
|
+
return input.match(catchRegex)[1];
|
|
17
|
+
}
|
|
18
|
+
return '';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function extractAllPossibleText(input, seperator1, seperator2) {
|
|
22
|
+
if (seperator2 == undefined) seperator2 = seperator1;
|
|
23
|
+
const extracted = {};
|
|
24
|
+
let textInBetween;
|
|
25
|
+
let cnt = 0;
|
|
26
|
+
const seperator1CharCode = seperator1.length > 0 ? seperator1.charCodeAt(0) : '';
|
|
27
|
+
const seperator2CharCode = seperator2.length > 0 ? seperator2.charCodeAt(0) : '';
|
|
28
|
+
while ((textInBetween = extractTextBySeperator(input, seperator1, seperator2)) !== '') {
|
|
29
|
+
const placeHolder =
|
|
30
|
+
'#$#%#$#placeholder' + cnt + '' + seperator1CharCode + '' + seperator2CharCode + '#$#%#$#';
|
|
31
|
+
extracted[placeHolder] = seperator1 + textInBetween + seperator2;
|
|
32
|
+
input = input.replace(extracted[placeHolder], placeHolder);
|
|
33
|
+
cnt++;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
filteredInput: input,
|
|
37
|
+
extracted: extracted,
|
|
38
|
+
getRestored: function () {
|
|
39
|
+
let textToFix = this.filteredInput;
|
|
40
|
+
for (const key in extracted) {
|
|
41
|
+
textToFix = textToFix.replace(key, extracted[key]);
|
|
42
|
+
}
|
|
43
|
+
return textToFix;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function strip_line(single_line) {
|
|
49
|
+
const trimmed = single_line.trim();
|
|
50
|
+
const removedDoubleQuatations = extractAllPossibleText(trimmed, '"', '"');
|
|
51
|
+
if (!removedDoubleQuatations.filteredInput.includes('sub_filter')) {
|
|
52
|
+
removedDoubleQuatations.filteredInput = removedDoubleQuatations.filteredInput.replace(
|
|
53
|
+
/\s\s+/g,
|
|
54
|
+
' '
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return removedDoubleQuatations.getRestored();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function clean_lines(configContents) {
|
|
61
|
+
const splittedByLines = configContents.split(/\r\n|\r|\n/g);
|
|
62
|
+
|
|
63
|
+
for (let index = 0, newline = 0; index < splittedByLines.length; index++) {
|
|
64
|
+
splittedByLines[index] = splittedByLines[index].trim();
|
|
65
|
+
if (
|
|
66
|
+
!splittedByLines[index].startsWith('#') &&
|
|
67
|
+
splittedByLines[index] !== ''
|
|
68
|
+
) {
|
|
69
|
+
newline = 0;
|
|
70
|
+
let line = (splittedByLines[index] = strip_line(splittedByLines[index]));
|
|
71
|
+
if (
|
|
72
|
+
line !== '}' &&
|
|
73
|
+
line !== '{' &&
|
|
74
|
+
!(
|
|
75
|
+
line.includes("('{") ||
|
|
76
|
+
line.includes("}')") ||
|
|
77
|
+
line.includes("'{'") ||
|
|
78
|
+
line.includes("'}'")
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
const startOfComment = line.indexOf('#');
|
|
82
|
+
const code =
|
|
83
|
+
startOfComment >= 0 ? line.slice(0, startOfComment) : line;
|
|
84
|
+
const removedDoubleQuatations = extractAllPossibleText(code, '"', '"');
|
|
85
|
+
let codeProcessed = removedDoubleQuatations.filteredInput;
|
|
86
|
+
|
|
87
|
+
const startOfParanthesis = codeProcessed.indexOf('}');
|
|
88
|
+
if (startOfParanthesis >= 0) {
|
|
89
|
+
if (startOfParanthesis > 0) {
|
|
90
|
+
splittedByLines[index] = strip_line(
|
|
91
|
+
codeProcessed.slice(0, startOfParanthesis - 1)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
splittedByLines.splice(index + 1, 0, '}');
|
|
95
|
+
const l2 = strip_line(codeProcessed.slice(startOfParanthesis + 1));
|
|
96
|
+
if (l2 !== '') splittedByLines.splice(index + 2, 0, l2);
|
|
97
|
+
codeProcessed = splittedByLines[index];
|
|
98
|
+
}
|
|
99
|
+
const endOfParanthesis = codeProcessed.indexOf('{');
|
|
100
|
+
if (endOfParanthesis >= 0) {
|
|
101
|
+
splittedByLines[index] = strip_line(
|
|
102
|
+
codeProcessed.slice(0, endOfParanthesis)
|
|
103
|
+
);
|
|
104
|
+
splittedByLines.splice(index + 1, 0, '{');
|
|
105
|
+
const l2 = strip_line(codeProcessed.slice(endOfParanthesis + 1));
|
|
106
|
+
if (l2 !== '') splittedByLines.splice(index + 2, 0, l2);
|
|
107
|
+
}
|
|
108
|
+
removedDoubleQuatations.filteredInput = splittedByLines[index];
|
|
109
|
+
line = removedDoubleQuatations.getRestored();
|
|
110
|
+
splittedByLines[index] = line;
|
|
111
|
+
}
|
|
112
|
+
} else if (splittedByLines[index] === '') {
|
|
113
|
+
if (newline++ >= 2) {
|
|
114
|
+
splittedByLines.splice(index, 1);
|
|
115
|
+
index--;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return splittedByLines;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function join_opening_bracket(lines) {
|
|
123
|
+
for (let i = 0; i < lines.length; i++) {
|
|
124
|
+
const line = lines[i];
|
|
125
|
+
if (line === '{' && i >= 1) {
|
|
126
|
+
lines[i] = lines[i - 1] + ' {';
|
|
127
|
+
lines.splice(i - 1, 1);
|
|
128
|
+
i--;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return lines;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const INDENTATION = ' '; // 4 spaces
|
|
135
|
+
|
|
136
|
+
function perform_indentation(lines) {
|
|
137
|
+
const indented_lines = [];
|
|
138
|
+
let current_indent = 0;
|
|
139
|
+
for (let index1 = 0; index1 < lines.length; index1++) {
|
|
140
|
+
const line = lines[index1];
|
|
141
|
+
if (
|
|
142
|
+
!line.startsWith('#') &&
|
|
143
|
+
/.*?\}(\s*#.*)?$/.test(line) &&
|
|
144
|
+
current_indent > 0
|
|
145
|
+
) {
|
|
146
|
+
current_indent -= 1;
|
|
147
|
+
}
|
|
148
|
+
if (line !== '') {
|
|
149
|
+
indented_lines.push(INDENTATION.repeat(current_indent) + line);
|
|
150
|
+
} else {
|
|
151
|
+
indented_lines.push('');
|
|
152
|
+
}
|
|
153
|
+
if (!line.startsWith('#') && /.*?\{(\s*#.*)?$/.test(line)) {
|
|
154
|
+
current_indent += 1;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return indented_lines;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function perform_alignment(lines) {
|
|
161
|
+
const all_lines = [];
|
|
162
|
+
const attribute_lines = [];
|
|
163
|
+
let minAlignColumn = 0;
|
|
164
|
+
|
|
165
|
+
for (let index1 = 0; index1 < lines.length; index1++) {
|
|
166
|
+
const line = lines[index1];
|
|
167
|
+
if (
|
|
168
|
+
line !== '' &&
|
|
169
|
+
!/.*?\{(\s*#.*)?$/.test(line) &&
|
|
170
|
+
!line.startsWith('#') &&
|
|
171
|
+
!/.*?\}(\s*#.*)?$/.test(line) &&
|
|
172
|
+
!line.trim().startsWith('upstream') &&
|
|
173
|
+
!line.trim().includes('location')
|
|
174
|
+
) {
|
|
175
|
+
const splitLine = line.match(/\S+/g);
|
|
176
|
+
if (splitLine && splitLine.length > 1) {
|
|
177
|
+
attribute_lines.push(line);
|
|
178
|
+
const columnAtAttrValue = line.indexOf(splitLine[1]) + 1;
|
|
179
|
+
if (minAlignColumn < columnAtAttrValue) {
|
|
180
|
+
minAlignColumn = columnAtAttrValue;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
all_lines.push(line);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (let index1 = 0; index1 < all_lines.length; index1++) {
|
|
188
|
+
let line = all_lines[index1];
|
|
189
|
+
if (attribute_lines.includes(line)) {
|
|
190
|
+
const split = line.match(/\S+/g);
|
|
191
|
+
const indentMatch = line.match(/\s+/g);
|
|
192
|
+
const indent = indentMatch ? indentMatch[0] : '';
|
|
193
|
+
line =
|
|
194
|
+
indent +
|
|
195
|
+
split[0] +
|
|
196
|
+
' '.repeat(
|
|
197
|
+
minAlignColumn - split[0].length - indent.length
|
|
198
|
+
) +
|
|
199
|
+
split.slice(1, split.length).join(' ');
|
|
200
|
+
all_lines[index1] = line;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return all_lines;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Format NGINX config contents into a readable format.
|
|
208
|
+
* Pipeline: clean_lines -> join_opening_bracket -> perform_indentation -> perform_alignment
|
|
209
|
+
*
|
|
210
|
+
* @param {string} contents - Raw NGINX config file contents
|
|
211
|
+
* @returns {string} Formatted config
|
|
212
|
+
*/
|
|
213
|
+
function format(contents) {
|
|
214
|
+
let lines = clean_lines(contents);
|
|
215
|
+
lines = join_opening_bracket(lines);
|
|
216
|
+
lines = perform_indentation(lines);
|
|
217
|
+
lines = perform_alignment(lines);
|
|
218
|
+
return lines.join('\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
module.exports = { format };
|
package/bin/nginx-pretty
ADDED
package/cli.js
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* nginx-pretty - Safely format and apply NGINX config files
|
|
5
|
+
* Never modifies originals unless user confirms. Shows diff, validates with nginx -t.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const readline = require('readline');
|
|
11
|
+
const { spawn } = require('child_process');
|
|
12
|
+
|
|
13
|
+
const DEFAULT_CONFIG = '/etc/nginx/sites-available/default';
|
|
14
|
+
const FORMATTED_TMP = '/tmp/tmp_nginx_formatted.conf';
|
|
15
|
+
|
|
16
|
+
function loadBeautifier() {
|
|
17
|
+
const beautifier = require('./beautifier');
|
|
18
|
+
return beautifier.format.bind(beautifier);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs() {
|
|
22
|
+
const args = process.argv.slice(2);
|
|
23
|
+
const opts = {
|
|
24
|
+
file: DEFAULT_CONFIG,
|
|
25
|
+
checkOnly: false,
|
|
26
|
+
dryRun: false,
|
|
27
|
+
noPrompt: false,
|
|
28
|
+
};
|
|
29
|
+
for (let i = 0; i < args.length; i++) {
|
|
30
|
+
if (args[i] === '--file' || args[i] === '-f') {
|
|
31
|
+
opts.file = args[++i] || DEFAULT_CONFIG;
|
|
32
|
+
} else if (args[i] === '--check-only' || args[i] === '--dry-run') {
|
|
33
|
+
opts.checkOnly = true;
|
|
34
|
+
opts.dryRun = true;
|
|
35
|
+
} else if (args[i] === '--no-prompt') {
|
|
36
|
+
opts.noPrompt = true;
|
|
37
|
+
} else if (args[i] === '--help' || args[i] === '-h') {
|
|
38
|
+
printHelp();
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return opts;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function printHelp() {
|
|
46
|
+
console.log(`
|
|
47
|
+
nginx-pretty - Safely format and apply NGINX config files
|
|
48
|
+
|
|
49
|
+
Usage: nginx-pretty [OPTIONS]
|
|
50
|
+
|
|
51
|
+
Options:
|
|
52
|
+
--file, -f <path> Config file path (default: /etc/nginx/sites-available/default)
|
|
53
|
+
--check-only Show diff only, do not prompt or apply
|
|
54
|
+
--dry-run Same as --check-only
|
|
55
|
+
--no-prompt Apply without prompting (for CI; use with caution)
|
|
56
|
+
--help, -h Show this help
|
|
57
|
+
`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getTempPath() {
|
|
61
|
+
return FORMATTED_TMP;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function runDiff(fileA, fileB) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
const proc = spawn('diff', ['-u', fileA, fileB], {
|
|
67
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
68
|
+
});
|
|
69
|
+
let out = '';
|
|
70
|
+
let err = '';
|
|
71
|
+
proc.stdout.on('data', (d) => (out += d.toString()));
|
|
72
|
+
proc.stderr.on('data', (d) => (err += d.toString()));
|
|
73
|
+
proc.on('close', (code) => {
|
|
74
|
+
if (code === 0) {
|
|
75
|
+
resolve(null);
|
|
76
|
+
} else if (code === 1) {
|
|
77
|
+
resolve(out || '(files differ)');
|
|
78
|
+
} else {
|
|
79
|
+
reject(new Error(err || `diff exited ${code}`));
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function runNginxTest() {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const proc = spawn('nginx', ['-t'], {
|
|
88
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
89
|
+
});
|
|
90
|
+
let out = '';
|
|
91
|
+
let err = '';
|
|
92
|
+
proc.stdout.on('data', (d) => (out += d.toString()));
|
|
93
|
+
proc.stderr.on('data', (d) => (err += d.toString()));
|
|
94
|
+
proc.on('error', (e) => {
|
|
95
|
+
reject(e);
|
|
96
|
+
});
|
|
97
|
+
proc.on('close', (code) => {
|
|
98
|
+
if (code === 0) {
|
|
99
|
+
resolve(true);
|
|
100
|
+
} else {
|
|
101
|
+
resolve(false);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function prompt(question) {
|
|
108
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
109
|
+
return new Promise((resolve) => {
|
|
110
|
+
rl.question(question, (answer) => {
|
|
111
|
+
rl.close();
|
|
112
|
+
resolve((answer || '').trim().toLowerCase());
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function main() {
|
|
118
|
+
const opts = parseArgs();
|
|
119
|
+
const format = loadBeautifier();
|
|
120
|
+
|
|
121
|
+
const originalPath = path.resolve(opts.file);
|
|
122
|
+
|
|
123
|
+
console.log('๐งช Running nginx-pretty...');
|
|
124
|
+
|
|
125
|
+
const tempPath = getTempPath();
|
|
126
|
+
try {
|
|
127
|
+
fs.unlinkSync(tempPath);
|
|
128
|
+
} catch (_) {}
|
|
129
|
+
|
|
130
|
+
let originalContent;
|
|
131
|
+
try {
|
|
132
|
+
originalContent = fs.readFileSync(originalPath, 'utf8');
|
|
133
|
+
} catch (e) {
|
|
134
|
+
console.error('โ Cannot read file:', originalPath);
|
|
135
|
+
console.error(e.message);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log('๐ Copying original file to:', tempPath);
|
|
140
|
+
try {
|
|
141
|
+
fs.writeFileSync(tempPath, originalContent, { mode: 0o644 });
|
|
142
|
+
} catch (e) {
|
|
143
|
+
console.error('โ Cannot write temp file:', tempPath);
|
|
144
|
+
console.error(e.message);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
console.log('๐ Ensuring', tempPath, 'is writable...');
|
|
149
|
+
console.log('๐จ Formatting the copied config...');
|
|
150
|
+
|
|
151
|
+
const formattedContent = format(originalContent);
|
|
152
|
+
|
|
153
|
+
fs.writeFileSync(tempPath, formattedContent, { mode: 0o644 });
|
|
154
|
+
|
|
155
|
+
runDiff(originalPath, tempPath)
|
|
156
|
+
.then((diffOutput) => {
|
|
157
|
+
if (diffOutput === null) {
|
|
158
|
+
console.log('โ
No changes detected.');
|
|
159
|
+
try {
|
|
160
|
+
fs.unlinkSync(tempPath);
|
|
161
|
+
} catch (_) {}
|
|
162
|
+
process.exit(0);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
console.log('\n๐ Showing unified diff (ORIGINAL vs FORMATTED):');
|
|
167
|
+
console.log(diffOutput);
|
|
168
|
+
console.log('โ Changes detected. Review diff and rerun if needed.');
|
|
169
|
+
|
|
170
|
+
if (opts.checkOnly || opts.dryRun) {
|
|
171
|
+
console.log('โน๏ธ Dry run / check-only: not applying.');
|
|
172
|
+
try {
|
|
173
|
+
fs.unlinkSync(tempPath);
|
|
174
|
+
} catch (_) {}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const doApply = () => {
|
|
180
|
+
try {
|
|
181
|
+
fs.copyFileSync(tempPath, originalPath);
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.error('โ Failed to apply config:', e.message);
|
|
184
|
+
try {
|
|
185
|
+
fs.unlinkSync(tempPath);
|
|
186
|
+
} catch (_) {}
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
runNginxTest()
|
|
191
|
+
.then((ok) => {
|
|
192
|
+
try {
|
|
193
|
+
fs.unlinkSync(tempPath);
|
|
194
|
+
} catch (_) {}
|
|
195
|
+
if (ok) {
|
|
196
|
+
console.log('๐ Config test passed. You can now reload NGINX');
|
|
197
|
+
console.log(' Run: sudo service nginx reload to apply the changes');
|
|
198
|
+
} else {
|
|
199
|
+
console.log(
|
|
200
|
+
'โ ๏ธ Config was applied but nginx -t reported issues. Review your config.'
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
.catch((e) => {
|
|
205
|
+
try {
|
|
206
|
+
fs.unlinkSync(tempPath);
|
|
207
|
+
} catch (_) {}
|
|
208
|
+
if (e.code === 'ENOENT') {
|
|
209
|
+
console.log('โ
Config applied. (nginx not found on PATH; run nginx -t and reload on a server with NGINX.)');
|
|
210
|
+
} else {
|
|
211
|
+
console.error('โ nginx -t failed:', e.message);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (opts.noPrompt) {
|
|
217
|
+
doApply();
|
|
218
|
+
} else {
|
|
219
|
+
console.log('');
|
|
220
|
+
prompt('โ ๏ธ Do you want to apply the formatted config? (yes/no): ')
|
|
221
|
+
.then((answer) => {
|
|
222
|
+
if (answer === 'yes' || answer === 'y') {
|
|
223
|
+
doApply();
|
|
224
|
+
} else {
|
|
225
|
+
console.log('โ Skipping apply step. Review diff and rerun if needed.');
|
|
226
|
+
try {
|
|
227
|
+
fs.unlinkSync(tempPath);
|
|
228
|
+
} catch (_) {}
|
|
229
|
+
process.exit(0);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
})
|
|
234
|
+
.catch((err) => {
|
|
235
|
+
console.error('โ ๏ธ diff failed:', err.message);
|
|
236
|
+
console.log('\nFormatted output preview:\n');
|
|
237
|
+
console.log(formattedContent);
|
|
238
|
+
console.log('\nConsider applying manually if acceptable.');
|
|
239
|
+
try {
|
|
240
|
+
fs.unlinkSync(tempPath);
|
|
241
|
+
} catch (_) {}
|
|
242
|
+
process.exit(1);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nginx-pretty",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Safely format and apply NGINX config files. Never modifies originals unless confirmed. Shows diff, validates with nginx -t.",
|
|
5
|
+
"main": "cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nginx-pretty": "cli.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"nginx",
|
|
11
|
+
"formatter",
|
|
12
|
+
"beautifier",
|
|
13
|
+
"config",
|
|
14
|
+
"cli",
|
|
15
|
+
"pretty"
|
|
16
|
+
],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"files": [
|
|
20
|
+
"cli.js",
|
|
21
|
+
"beautifier",
|
|
22
|
+
"bin",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=14.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build:pkg": "pkg cli.js --output nginx-pretty --targets node18-linux-x64,node18-darwin-x64,node18-darwin-arm64"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"pkg": "^5.8.1"
|
|
34
|
+
},
|
|
35
|
+
"pkg": {
|
|
36
|
+
"assets": ["beautifier/**"]
|
|
37
|
+
}
|
|
38
|
+
}
|