yini-cli 1.1.1-beta → 1.2.0-beta
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 +94 -64
- package/dist/cli/helpAll.d.ts +2 -0
- package/dist/cli/helpAll.js +23 -0
- package/dist/commands/infoCommand.d.ts +4 -0
- package/dist/commands/infoCommand.js +44 -0
- package/dist/commands/parseCommand.d.ts +7 -0
- package/dist/commands/parseCommand.js +92 -33
- package/dist/commands/validateCommand.d.ts +1 -2
- package/dist/commands/validateCommand.js +260 -34
- package/dist/descriptions.js +4 -4
- package/dist/globalOptions/helpOption.js +19 -10
- package/dist/index.js +43 -52
- package/dist/types.d.ts +2 -1
- package/dist/utils/print.d.ts +16 -0
- package/dist/utils/print.js +78 -0
- package/dist/utils/string.d.ts +2 -0
- package/dist/utils/string.js +9 -0
- package/package.json +3 -2
- package/dist/globalOptions/infoOption.d.ts +0 -1
- package/dist/globalOptions/infoOption.js +0 -13
package/README.md
CHANGED
|
@@ -1,21 +1,26 @@
|
|
|
1
1
|
# YINI-CLI
|
|
2
|
-
**
|
|
2
|
+
> **Readable configuration without indentation pitfalls or JSON verbosity.**
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
**The official terminal / command-line (CLI) tool for validating, inspecting, and converting YINI configuration files to JSON or JavaScript.**
|
|
5
|
+
|
|
6
|
+
*YINI is an INI-inspired and human-readable text format for representing structured information. It is designed to be clear, predictable, and easy for humans to read and write. It supports nesting, comments, and a formally defined syntax. It is suitable for configuration files, application settings, and general data-storage use cases.*
|
|
5
7
|
|
|
6
8
|
[](https://www.npmjs.com/package/yini-cli) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml) [](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml)
|
|
7
9
|
|
|
8
|
-
This tool is
|
|
10
|
+
This tool is designed for teams and developers working with human-edited configuration files who require explicit structure without indentation-based semantics.
|
|
9
11
|
|
|
10
12
|
---
|
|
11
13
|
|
|
14
|
+
## Example of YINI code
|
|
15
|
+
> A basic YINI configuration example, showing a section, nested section, comments:
|
|
16
|
+

|
|
17
|
+
Source: [basic.yini](./samples/basic.yini)
|
|
18
|
+
|
|
12
19
|
## Quick Start
|
|
13
20
|
|
|
14
21
|
### Requirements
|
|
15
22
|
YINI CLI requires Node.js **v20 or later**.
|
|
16
23
|
|
|
17
|
-
(It has also been tested with Node.js v13+, but v20+ is recommended for best compatibility.)
|
|
18
|
-
|
|
19
24
|
### Installation
|
|
20
25
|
|
|
21
26
|
1. **Install it globally from npm — (requires Node.js)**
|
|
@@ -29,7 +34,7 @@ YINI CLI requires Node.js **v20 or later**.
|
|
|
29
34
|
```bash
|
|
30
35
|
yini --version
|
|
31
36
|
```
|
|
32
|
-
|
|
37
|
+
This should print the installed version (e.g., 1.0.0).
|
|
33
38
|
|
|
34
39
|
Then you may try:
|
|
35
40
|
```bash
|
|
@@ -53,21 +58,17 @@ YINI CLI requires Node.js **v20 or later**.
|
|
|
53
58
|
```
|
|
54
59
|
|
|
55
60
|
Expected result, your CLI should output a parsed version of the config and output something similar to:
|
|
56
|
-
```
|
|
61
|
+
```json
|
|
57
62
|
{
|
|
58
|
-
App: {
|
|
59
|
-
name:
|
|
60
|
-
version:
|
|
61
|
-
pageSize: 25,
|
|
62
|
-
darkTheme: false
|
|
63
|
+
"App": {
|
|
64
|
+
"name": "My App Title",
|
|
65
|
+
"version": "1.2.3",
|
|
66
|
+
"pageSize": 25,
|
|
67
|
+
"darkTheme": false
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
```
|
|
66
71
|
|
|
67
|
-
⭐ If this was useful, [star it on GitHub](https://github.com/YINI-lang/yini-cli) — it helps a lot, thank you!
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
72
|
### Typical use cases
|
|
72
73
|
|
|
73
74
|
- Validating configuration files during development or CI.
|
|
@@ -76,44 +77,55 @@ YINI CLI requires Node.js **v20 or later**.
|
|
|
76
77
|
|
|
77
78
|
---
|
|
78
79
|
|
|
80
|
+
## Example 2
|
|
81
|
+
> A real-world YINI configuration example, showing sections, nesting, comments, and multiple data types:
|
|
82
|
+

|
|
83
|
+
Source: [config.yini](./samples/config.yini)
|
|
84
|
+
|
|
85
|
+
---
|
|
86
|
+
|
|
79
87
|
## 🙋♀️ Why YINI?
|
|
80
|
-
- **Indentation-independent structure:**
|
|
81
|
-
- **Explicit nesting:** It uses clear header markers (`^`, `^^`, `^^^`) to define hierarchy (like in Markdown), without long dotted keys.
|
|
88
|
+
- **Indentation-independent structure:** YINI is indentation-independent — whitespace never alters structural meaning.
|
|
89
|
+
- **Explicit nesting & refactoring safety:** It uses clear header markers (`^`, `^^`, `^^^`) to define hierarchy (like in Markdown), without long dotted keys.
|
|
82
90
|
- **Multiple data types:** Supports boolean literals (`true` / `false`, `Yes` / `No`, etc), numbers, arrays (lists), and JS-style objects natively, with explicit string syntax.
|
|
83
91
|
- **Comments support:** YINI supports multiple comment styles (`#`, `//`, `/* ... */`, and `;`) allowing one to document config directly in the file.
|
|
84
92
|
- **Predictable parsing rules:** Well-defined rules with optional strict and lenient modes, for different use-requirements.
|
|
85
93
|
|
|
86
94
|
---
|
|
87
95
|
|
|
88
|
-
## Usage
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
### Quick Examples
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
yini parse config.yini
|
|
102
|
+
```
|
|
103
|
+
→ Parse and print formatted JSON (default).
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
yini parse config.yini --compact
|
|
107
|
+
```
|
|
108
|
+
→ Output compact JSON (no whitespace).
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
yini parse config.yini --js
|
|
112
|
+
```
|
|
113
|
+
→ Output as JavaScript-style object.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
yini parse config.yini -o out.json
|
|
117
|
+
```
|
|
118
|
+
→ Write formatted JSON to a file.
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
yini validate --strict config.yini
|
|
122
|
+
```
|
|
123
|
+
→ Validate using strict mode.
|
|
124
|
+
|
|
125
|
+
For help with a specific command:
|
|
89
126
|
|
|
90
127
|
```bash
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
CLI for parsing and validating YINI config files.
|
|
94
|
-
|
|
95
|
-
Options:
|
|
96
|
-
-v, --version Output the version number.
|
|
97
|
-
-i, --info Show extended information (details, links, etc.).
|
|
98
|
-
-s, --strict Enable strict parsing mode.
|
|
99
|
-
-f, --force Continue parsing even if errors occur.
|
|
100
|
-
-q, --quiet Reduce output (show only errors).
|
|
101
|
-
--silent Suppress all output (even errors, exit code only).
|
|
102
|
-
-h, --help Display help for command.
|
|
103
|
-
|
|
104
|
-
Commands:
|
|
105
|
-
parse [options] <file> Parse a YINI file (*.yini) and print the result.
|
|
106
|
-
validate [options] <file> Checks if the file can be parsed as valid YINI.
|
|
107
|
-
info Deprecated: Use `yini --info` or `yini -i` instead.
|
|
108
|
-
help [command] Display help for command.
|
|
109
|
-
|
|
110
|
-
Examples:
|
|
111
|
-
$ yini parse config.yini
|
|
112
|
-
$ yini validate --strict config.yini
|
|
113
|
-
$ yini parse config.yini --pretty --output out.json
|
|
114
|
-
|
|
115
|
-
For help with a specific command, use -h or --help. For example:
|
|
116
|
-
$ yini validate --help
|
|
128
|
+
yini parse --help
|
|
117
129
|
```
|
|
118
130
|
|
|
119
131
|
---
|
|
@@ -169,35 +181,56 @@ Here's a small example showing YINI structure and comments:
|
|
|
169
181
|
|
|
170
182
|
That's it!
|
|
171
183
|
|
|
172
|
-
- ▶️
|
|
184
|
+
- ▶️ See more on [YINI Homepage](https://yini-lang.org/?utm_source=github&utm_medium=referral&utm_campaign=yini_cli&utm_content=readme_middle).
|
|
173
185
|
- ▶️ Link to [Demo Apps](https://github.com/YINI-lang/yini-demo-apps/tree/main) with complete basic usage.
|
|
174
186
|
|
|
175
187
|
---
|
|
176
188
|
|
|
177
189
|
## 📤 Output Modes for `yini parse`
|
|
178
190
|
|
|
179
|
-
The `parse` command supports multiple output
|
|
191
|
+
The `parse` command supports multiple output formats:
|
|
180
192
|
|
|
181
|
-
| Command Example
|
|
182
|
-
|
|
183
|
-
| `yini parse config.yini`
|
|
184
|
-
| `yini parse config.yini --
|
|
185
|
-
| `yini parse config.yini --
|
|
186
|
-
| `yini parse config.yini --
|
|
187
|
-
| `yini parse config.yini
|
|
193
|
+
| Command Example | Output Format | Description |
|
|
194
|
+
|------------------------------------------|----------------------|------------|
|
|
195
|
+
| `yini parse config.yini` | Pretty JSON (default) | Formatted JSON with indentation (4 spaces). |
|
|
196
|
+
| `yini parse config.yini --json` | Pretty JSON | Explicit pretty JSON output. |
|
|
197
|
+
| `yini parse config.yini --compact` | Compact JSON | Minified JSON (no whitespace). |
|
|
198
|
+
| `yini parse config.yini --js` | JavaScript object | JavaScript-style object (unquoted keys, single quotes). |
|
|
199
|
+
| `yini parse config.yini -o out.json` | File output | Writes formatted JSON to file (default format). |
|
|
188
200
|
|
|
201
|
+
>💡 `--js` and `--compact` are mutually exclusive.
|
|
189
202
|
>💡 Tip: You can combine --output with any style flag to control both formatting and destination.
|
|
190
203
|
|
|
191
204
|
---
|
|
192
205
|
|
|
206
|
+
## 📁 Output File Handling
|
|
207
|
+
|
|
208
|
+
When using `-o, --output <file>`, YINI CLI applies safe write rules:
|
|
209
|
+
|
|
210
|
+
| Scenario | Result |
|
|
211
|
+
|----------|--------|
|
|
212
|
+
| File does not exist | File is written |
|
|
213
|
+
| File exists and is **older** than the input YINI file | File is overwritten |
|
|
214
|
+
| File exists and is **newer** than the input YINI file | Command fails |
|
|
215
|
+
| `--overwrite` is used | File is always overwritten |
|
|
216
|
+
| `--no-overwrite` is used | Command fails if file exists |
|
|
217
|
+
|
|
218
|
+
This prevents accidental overwriting of newer generated files.
|
|
219
|
+
|
|
220
|
+
Use:
|
|
221
|
+
`--overwrite` to force replacement.
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
193
225
|
## 🛠 Roadmap
|
|
194
226
|
Areas of planned and possible future expansion:
|
|
195
227
|
|
|
196
228
|
1. **Improve existing commands** — Continued functionality improvements, better diagnostics, and expanded QA for `parse` and `validate` and their options.
|
|
197
|
-
2. Command `
|
|
198
|
-
3. Command `
|
|
199
|
-
4. Command `
|
|
200
|
-
5. Command `
|
|
229
|
+
2. Command `convert`: Batch convert YINI files to JSON or JavaScript.
|
|
230
|
+
3. Command `format`: Pretty-print or normalize a `.yini` file.
|
|
231
|
+
4. Command `lint`: Stricter stylistic checks (like `validate`, but opinionated).
|
|
232
|
+
5. Command `diff`: Compare two YINI files and show structural/config differences.
|
|
233
|
+
6. Import JSON or XML into YINI format.
|
|
201
234
|
|
|
202
235
|
---
|
|
203
236
|
|
|
@@ -206,13 +239,12 @@ Areas of planned and possible future expansion:
|
|
|
206
239
|
*Install and view package details.*
|
|
207
240
|
|
|
208
241
|
- ➡️ [YINI Project](https://github.com/YINI-lang)
|
|
209
|
-
*YINI home on
|
|
242
|
+
*YINI home on GitHub.*
|
|
210
243
|
|
|
211
244
|
---
|
|
212
245
|
|
|
213
246
|
## Contribution & Involvement
|
|
214
|
-
|
|
215
|
-
Issues, feedback, and experiments are welcome — even small ones.
|
|
247
|
+
Contributions, issues, and feedback are welcome. Even small improvements or suggestions are appreciated.
|
|
216
248
|
|
|
217
249
|
---
|
|
218
250
|
|
|
@@ -223,9 +255,7 @@ In this project on GitHub, the `libs` directory contains third party software an
|
|
|
223
255
|
|
|
224
256
|
---
|
|
225
257
|
|
|
226
|
-
If you found this useful, a GitHub star helps the project a lot ⭐
|
|
227
|
-
|
|
228
258
|
**^YINI ≡**
|
|
229
259
|
> YINI — Clear, Structured Configuration Files.
|
|
230
260
|
|
|
231
|
-
[yini-lang.org](https://yini-lang.org) · [YINI on GitHub](https://github.com/YINI-lang)
|
|
261
|
+
[yini-lang.org](https://yini-lang.org/?utm_source=github&utm_medium=referral&utm_campaign=yini_cli&utm_content=readme_footer) · [YINI on GitHub](https://github.com/YINI-lang)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function enableHelpAll(program) {
|
|
2
|
+
const originalFormatHelp = program.createHelp().formatHelp;
|
|
3
|
+
program.configureHelp({
|
|
4
|
+
formatHelp: (cmd, helper) => {
|
|
5
|
+
// Call the original formatter (not the overridden one)
|
|
6
|
+
let output = originalFormatHelp.call(helper, cmd, helper);
|
|
7
|
+
// Only expand the top-level help
|
|
8
|
+
if (cmd !== program)
|
|
9
|
+
return output;
|
|
10
|
+
for (const sub of program.commands) {
|
|
11
|
+
output += '\n\n';
|
|
12
|
+
output +=
|
|
13
|
+
'--------------------------------------------------------\n';
|
|
14
|
+
output += `* Command: ${sub.name()}\n`;
|
|
15
|
+
output +=
|
|
16
|
+
'--------------------------------------------------------\n';
|
|
17
|
+
output += originalFormatHelp.call(helper, sub, helper);
|
|
18
|
+
// output += '--------------------------------------------------------\n'
|
|
19
|
+
}
|
|
20
|
+
return output;
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
import { removeSuffix, toColRow } from '../utils/string.js';
|
|
3
|
+
// import pkg from '../../package.json' with { type: 'json' } // NOTE: Must use { type: 'json' } when in ESM mode.
|
|
4
|
+
const require = createRequire(import.meta.url);
|
|
5
|
+
const pkg = require('../../package.json');
|
|
6
|
+
/*
|
|
7
|
+
YINI CLI — Environment Information
|
|
8
|
+
──────────────────────────────────
|
|
9
|
+
|
|
10
|
+
CLI Version: 1.1.1-beta
|
|
11
|
+
Parser Version: 1.3.2-beta
|
|
12
|
+
|
|
13
|
+
if verbose(
|
|
14
|
+
Node.js: v20.18.0
|
|
15
|
+
Platform: win32 x64
|
|
16
|
+
Working Directory: D:\Sources\YINI-lang-WORK\yini-cli
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
Author: Marko K. Seppänen
|
|
20
|
+
License: Apache-2.0
|
|
21
|
+
|
|
22
|
+
Repository: https://github.com/YINI-lang/yini-cli
|
|
23
|
+
Homepage: https://yini-lang.org
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* NOTE: Keep contributors in acknowledged in README or --credits.
|
|
30
|
+
*/
|
|
31
|
+
export const printInfo = () => {
|
|
32
|
+
const size = 16;
|
|
33
|
+
console.log();
|
|
34
|
+
console.log('YINI CLI — Environment Information');
|
|
35
|
+
console.log('==================================');
|
|
36
|
+
console.log();
|
|
37
|
+
console.log(toColRow(size, 'CLI Version:', pkg.version));
|
|
38
|
+
console.log(toColRow(size, 'Parser Version:', pkg.dependencies['yini-parser'].replace('^', '')));
|
|
39
|
+
console.log(toColRow(size, 'Author:', pkg.author));
|
|
40
|
+
console.log(toColRow(size, 'License:', pkg.license));
|
|
41
|
+
console.log();
|
|
42
|
+
console.log(toColRow(size, 'Repository:', 'https://github.com/YINI-lang/yini-cli'));
|
|
43
|
+
console.log(toColRow(size, 'Homepage:', removeSuffix(pkg.homepage, '/')));
|
|
44
|
+
};
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { IGlobalOptions } from '../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* @deprecated pretty Deprecated since 2026 Feb! Use `json` instead.
|
|
4
|
+
*/
|
|
2
5
|
export interface IParseCommandOptions extends IGlobalOptions {
|
|
3
6
|
pretty?: boolean;
|
|
4
7
|
json?: boolean;
|
|
8
|
+
compact?: boolean;
|
|
9
|
+
js?: boolean;
|
|
5
10
|
output?: string;
|
|
11
|
+
bestEffort?: boolean;
|
|
12
|
+
overwrite?: boolean;
|
|
6
13
|
}
|
|
7
14
|
export declare const parseFile: (file: string, commandOptions: IParseCommandOptions) => void;
|
|
@@ -1,72 +1,131 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import util from 'util';
|
|
4
3
|
import YINI from 'yini-parser';
|
|
5
|
-
import { debugPrint, printObject, toPrettyJSON } from '../utils/print.js';
|
|
4
|
+
import { debugPrint, printObject, toPrettyJS, toPrettyJSON, } from '../utils/print.js';
|
|
6
5
|
// -------------------------------------------------------------------------
|
|
6
|
+
/*
|
|
7
|
+
TODO / SHOULD-DO:
|
|
8
|
+
|
|
9
|
+
yini parse <file> [options]
|
|
10
|
+
|
|
11
|
+
Options
|
|
12
|
+
-------
|
|
13
|
+
|
|
14
|
+
Parsing mode:
|
|
15
|
+
--strict
|
|
16
|
+
--lenient (default)
|
|
17
|
+
|
|
18
|
+
Output format:
|
|
19
|
+
--to <json|json-compact> (default, --to json)
|
|
20
|
+
--json (alias for --to json, default)
|
|
21
|
+
--compact (alias for --to json-compact)
|
|
22
|
+
|
|
23
|
+
Output handling:
|
|
24
|
+
-o, --output <file> (default) No overwrite if dest is more recent than source file (override with --overwrite)
|
|
25
|
+
--overwrite
|
|
26
|
+
--no-overwrite
|
|
27
|
+
|
|
28
|
+
Execution control:
|
|
29
|
+
--fail-fast
|
|
30
|
+
--best-effort = ignore-errors within a file, attempt recovery and still emit outp
|
|
31
|
+
--No for parse, --keep-going = continue to the next file when one fails
|
|
32
|
+
--max-errors <n>
|
|
33
|
+
--verbose
|
|
34
|
+
--checks (default)
|
|
35
|
+
--no-checks
|
|
36
|
+
|
|
37
|
+
Policy control (advanced):
|
|
38
|
+
--duplicates-policy <error|warn|allow>
|
|
39
|
+
--reserved-policy <error|warn|allow>
|
|
40
|
+
*/
|
|
7
41
|
export const parseFile = (file, commandOptions) => {
|
|
8
42
|
const outputFile = commandOptions.output || '';
|
|
9
|
-
const isStrictMode = !!commandOptions.strict
|
|
10
|
-
|
|
43
|
+
// const isStrictMode = !!commandOptions.strict
|
|
44
|
+
const outputStyle = resolveOutputStyle(commandOptions);
|
|
11
45
|
debugPrint('file = ' + file);
|
|
12
46
|
debugPrint('output = ' + commandOptions.output);
|
|
13
47
|
debugPrint('commandOptions:');
|
|
14
48
|
printObject(commandOptions);
|
|
15
|
-
|
|
16
|
-
|
|
49
|
+
doParseFile(file, commandOptions, outputStyle, outputFile);
|
|
50
|
+
};
|
|
51
|
+
const resolveOutputStyle = (options) => {
|
|
52
|
+
if (options.js && options.compact) {
|
|
53
|
+
throw new Error('--js and --compact cannot be combined.');
|
|
17
54
|
}
|
|
18
|
-
|
|
19
|
-
|
|
55
|
+
if (options.compact)
|
|
56
|
+
return 'JSON-compact';
|
|
57
|
+
if (options.js)
|
|
58
|
+
return 'JS-style';
|
|
59
|
+
if (options.pretty) {
|
|
60
|
+
console.warn('Warning: --pretty is deprecated. Use --json instead.');
|
|
20
61
|
}
|
|
21
|
-
|
|
22
|
-
|
|
62
|
+
return 'Pretty-JSON';
|
|
63
|
+
};
|
|
64
|
+
const renderOutput = (parsed, style) => {
|
|
65
|
+
switch (style) {
|
|
66
|
+
case 'JS-style':
|
|
67
|
+
return toPrettyJS(parsed);
|
|
68
|
+
case 'JSON-compact':
|
|
69
|
+
return JSON.stringify(parsed);
|
|
70
|
+
case 'Pretty-JSON':
|
|
71
|
+
default:
|
|
72
|
+
return toPrettyJSON(parsed);
|
|
23
73
|
}
|
|
24
|
-
doParseFile(file, commandOptions, outputStyle, outputFile);
|
|
25
74
|
};
|
|
26
75
|
const doParseFile = (file, commandOptions, outputStyle, outputFile = '') => {
|
|
27
76
|
// let strictMode = !!commandOptions.strict
|
|
28
|
-
let preferredFailLevel =
|
|
77
|
+
let preferredFailLevel = commandOptions.bestEffort
|
|
78
|
+
? 'ignore-errors'
|
|
79
|
+
: 'auto';
|
|
29
80
|
let includeMetaData = false;
|
|
30
81
|
debugPrint('File = ' + file);
|
|
31
82
|
debugPrint('outputStyle = ' + outputStyle);
|
|
32
83
|
const parseOptions = {
|
|
33
84
|
strictMode: commandOptions.strict ?? false,
|
|
34
|
-
// failLevel: 'errors',
|
|
35
85
|
failLevel: preferredFailLevel,
|
|
36
|
-
// failLevel: 'ignore-errors',
|
|
37
86
|
includeMetadata: includeMetaData,
|
|
38
87
|
};
|
|
39
|
-
// If --
|
|
40
|
-
if (commandOptions.
|
|
88
|
+
// If --best-effort then override fail-level.
|
|
89
|
+
if (commandOptions.bestEffort) {
|
|
41
90
|
parseOptions.failLevel = 'ignore-errors';
|
|
42
91
|
}
|
|
43
92
|
try {
|
|
44
93
|
const parsed = YINI.parseFile(file, parseOptions);
|
|
45
|
-
|
|
46
|
-
switch (outputStyle) {
|
|
47
|
-
case 'Pretty-JSON':
|
|
48
|
-
output = toPrettyJSON(parsed);
|
|
49
|
-
break;
|
|
50
|
-
case 'Console.log':
|
|
51
|
-
output = '<todo>';
|
|
52
|
-
break;
|
|
53
|
-
case 'JSON-compact':
|
|
54
|
-
output = JSON.stringify(parsed);
|
|
55
|
-
break;
|
|
56
|
-
default:
|
|
57
|
-
output = util.inspect(parsed, { depth: null, colors: false });
|
|
58
|
-
}
|
|
94
|
+
const output = renderOutput(parsed, outputStyle);
|
|
59
95
|
if (outputFile) {
|
|
96
|
+
const resolved = path.resolve(outputFile);
|
|
97
|
+
enforceWritePolicy(file, resolved, commandOptions.overwrite);
|
|
60
98
|
// Write JSON output to file instead of stdout.
|
|
61
|
-
fs.writeFileSync(
|
|
62
|
-
|
|
99
|
+
fs.writeFileSync(resolved, output, 'utf-8');
|
|
100
|
+
if (commandOptions.verbose) {
|
|
101
|
+
console.log(`Output written to file: "${outputFile}"`);
|
|
102
|
+
}
|
|
63
103
|
}
|
|
64
104
|
else {
|
|
65
105
|
console.log(output);
|
|
66
106
|
}
|
|
67
107
|
}
|
|
68
108
|
catch (err) {
|
|
69
|
-
|
|
109
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
110
|
+
console.error(`Error: ${message}`);
|
|
70
111
|
process.exit(1);
|
|
71
112
|
}
|
|
72
113
|
};
|
|
114
|
+
const enforceWritePolicy = (srcPath, destPath, overwrite) => {
|
|
115
|
+
if (!fs.existsSync(destPath)) {
|
|
116
|
+
return; // File does not exist, OK to write.
|
|
117
|
+
}
|
|
118
|
+
const srcStat = fs.statSync(srcPath);
|
|
119
|
+
const destStat = fs.statSync(destPath);
|
|
120
|
+
const destIsNewer = destStat.mtimeMs >= srcStat.mtimeMs;
|
|
121
|
+
if (overwrite === true) {
|
|
122
|
+
return; // Explicit overwrite, OK.
|
|
123
|
+
}
|
|
124
|
+
if (overwrite === false) {
|
|
125
|
+
throw new Error(`File "${destPath}" already exists. Overwriting disabled (--no-overwrite).`);
|
|
126
|
+
}
|
|
127
|
+
// Default policy (overwrite undefined).
|
|
128
|
+
if (destIsNewer) {
|
|
129
|
+
throw new Error(`Destination file "${destPath}" is newer than source. Use --overwrite to force.`);
|
|
130
|
+
}
|
|
131
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { IGlobalOptions } from '../types.js';
|
|
2
2
|
export interface IValidateCommandOptions extends IGlobalOptions {
|
|
3
|
-
|
|
4
|
-
details?: boolean;
|
|
3
|
+
stats?: boolean;
|
|
5
4
|
}
|
|
6
5
|
export declare const validateFile: (file: string, commandOptions?: IValidateCommandOptions) => never;
|
|
@@ -1,16 +1,158 @@
|
|
|
1
1
|
import assert from 'node:assert';
|
|
2
2
|
import { exit } from 'node:process';
|
|
3
3
|
import YINI from 'yini-parser';
|
|
4
|
-
const IS_DEBUG =
|
|
5
|
-
|
|
4
|
+
const IS_DEBUG = true; // For local debugging purposes, etc.
|
|
5
|
+
/*
|
|
6
|
+
TODO / SHOULD-DO:
|
|
7
|
+
|
|
8
|
+
yini validate <file|path...> [options]
|
|
9
|
+
|
|
10
|
+
Validate one or more YINI files.
|
|
11
|
+
If a directory is provided, all .yini files (case-insensitive) are processed recursively by default.
|
|
12
|
+
|
|
13
|
+
On successful validation, a report (summary, issues, optional stats) is printed to the terminal.
|
|
14
|
+
|
|
15
|
+
Options
|
|
16
|
+
-------
|
|
17
|
+
|
|
18
|
+
Validation mode:
|
|
19
|
+
--strict Enable strict validation mode
|
|
20
|
+
--lenient (default) Enable lenient validation mode
|
|
21
|
+
--quiet, -q Suppress normal output (show errors only)
|
|
22
|
+
--silent, -s Suppress all output (exit code only)
|
|
23
|
+
--stats Include stats, show meta-data section (counts, depth, etc.)
|
|
24
|
+
--format <text|yini|json> Output format for the report (staus, stats, issues) (default: text)
|
|
25
|
+
|
|
26
|
+
Input handling:
|
|
27
|
+
<file> Validate a single YINI file
|
|
28
|
+
<path> Validate all .yini files in the directory (recursive by default)
|
|
29
|
+
--no-recursive, --no-subdirs Do not descend into subdirectories
|
|
30
|
+
|
|
31
|
+
Output handling:
|
|
32
|
+
--output <file>, -o <file> Write validation report to file
|
|
33
|
+
--overwrite Allow overwriting existing report file
|
|
34
|
+
--no-overwrite (default) Prevent overwriting existing report file (default)
|
|
35
|
+
|
|
36
|
+
Execution controls (Nice-to-Have)
|
|
37
|
+
--fail-fast Stop on the first validation error
|
|
38
|
+
--max-errors <n> Stop after <n> errors
|
|
39
|
+
--verbose Show detailed processing information
|
|
40
|
+
--warnings-as-errors Treat warnings as errors
|
|
41
|
+
|
|
42
|
+
Policy controls (advanced):
|
|
43
|
+
--duplicates-policy <error|warn|allow> Control handling of duplicate keys / section names
|
|
44
|
+
--reserved-policy <error|warn|allow>
|
|
45
|
+
|
|
46
|
+
===========================================================
|
|
47
|
+
|
|
48
|
+
REQUIREMENTS for report output:
|
|
49
|
+
|
|
50
|
+
The output should:
|
|
51
|
+
1. Be human-readable by default
|
|
52
|
+
2. Give a clear verdict
|
|
53
|
+
3. Show useful context for each problem
|
|
54
|
+
4. Be machine-friendly when requested (--format json)
|
|
55
|
+
5. Be stable and predictable for CI usage
|
|
56
|
+
|
|
57
|
+
* Header / Summary:
|
|
58
|
+
On success:
|
|
59
|
+
✔ Validation successful
|
|
60
|
+
File: config.yini
|
|
61
|
+
Mode: lenient
|
|
62
|
+
Errors: 0
|
|
63
|
+
Warnings: 2
|
|
64
|
+
|
|
65
|
+
On failure:
|
|
66
|
+
✖ Validation failed
|
|
67
|
+
File: config.yini
|
|
68
|
+
Mode: strict
|
|
69
|
+
Errors: 3
|
|
70
|
+
Warnings: 1
|
|
71
|
+
|
|
72
|
+
* Issues sections:
|
|
73
|
+
Each issue:
|
|
74
|
+
Severity error / warning
|
|
75
|
+
Code stable identifier (DUPLICATE_KEY, UNKNOWN_CONSTRUCT, etc.)
|
|
76
|
+
Message short explanation
|
|
77
|
+
Location file + line + column
|
|
78
|
+
Context snippet of the file (if helpful)
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
Errors:
|
|
82
|
+
[E001] Duplicate key "host"
|
|
83
|
+
at config.yini:14:5
|
|
84
|
+
Previous definition at line 7
|
|
85
|
+
→ host = "localhost"
|
|
86
|
+
|
|
87
|
+
Warnings:
|
|
88
|
+
[W002] Reserved construct used: "$schema"
|
|
89
|
+
at config.yini:3:1
|
|
90
|
+
|
|
91
|
+
* Optional meta-data section --stats
|
|
92
|
+
Statistics:
|
|
93
|
+
Sections: 5
|
|
94
|
+
Keys: 27
|
|
95
|
+
Arrays: 4
|
|
96
|
+
Objects: 3
|
|
97
|
+
Nesting depth: 4
|
|
98
|
+
|
|
99
|
+
* Exit code contract
|
|
100
|
+
Success, no warnings 0
|
|
101
|
+
Warnings only 0 (or 1 if --warnings-as-errors)
|
|
102
|
+
Errors 1
|
|
103
|
+
|
|
104
|
+
Example: JSON format (--format json)
|
|
105
|
+
{
|
|
106
|
+
"file": "config.yini",
|
|
107
|
+
"mode": "strict",
|
|
108
|
+
"summary": {
|
|
109
|
+
"errors": 2,
|
|
110
|
+
"warnings": 1
|
|
111
|
+
},
|
|
112
|
+
"issues": [
|
|
113
|
+
{
|
|
114
|
+
"severity": "error",
|
|
115
|
+
"code": "DUPLICATE_KEY",
|
|
116
|
+
"message": "Duplicate key \"host\"",
|
|
117
|
+
"location": { "line": 14, "column": 5 }
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"severity": "error",
|
|
121
|
+
"code": "INVALID_TYPE",
|
|
122
|
+
"message": "Expected number, got string",
|
|
123
|
+
"location": { "line": 22, "column": 12 }
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
"severity": "warning",
|
|
127
|
+
"code": "RESERVED_CONSTRUCT",
|
|
128
|
+
"message": "Reserved construct \"$schema\"",
|
|
129
|
+
"location": { "line": 3, "column": 1 }
|
|
130
|
+
}
|
|
131
|
+
],
|
|
132
|
+
"stats": {
|
|
133
|
+
"sections": 5,
|
|
134
|
+
"keys": 27,
|
|
135
|
+
"nestingDepth": 4
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
*/
|
|
6
140
|
export const validateFile = (file, commandOptions = {}) => {
|
|
7
141
|
let parsedResult = undefined;
|
|
8
142
|
let isCatchedError = true;
|
|
143
|
+
// let failLevel: PreferredFailLevel = 'auto'
|
|
144
|
+
// if (commandOptions.failFast) {
|
|
145
|
+
// failLevel = 'warnings-and-errors'
|
|
146
|
+
// }
|
|
147
|
+
// if (commandOptions.bestEffort) {
|
|
148
|
+
// failLevel = 'ignore-errors'
|
|
149
|
+
// }
|
|
9
150
|
const parseOptions = {
|
|
10
151
|
strictMode: commandOptions.strict ?? false,
|
|
11
152
|
// failLevel: 'errors',
|
|
12
|
-
failLevel: commandOptions.force ? 'ignore-errors' : 'errors',
|
|
153
|
+
// failLevel: commandOptions.force ? 'ignore-errors' : 'errors',
|
|
13
154
|
// failLevel: 'ignore-errors',
|
|
155
|
+
failLevel: 'ignore-errors',
|
|
14
156
|
includeMetadata: true,
|
|
15
157
|
includeDiagnostics: true,
|
|
16
158
|
silent: true,
|
|
@@ -50,51 +192,135 @@ export const validateFile = (file, commandOptions = {}) => {
|
|
|
50
192
|
IS_DEBUG &&
|
|
51
193
|
console.log('includeMetadata = ' +
|
|
52
194
|
metadata?.diagnostics?.effectiveOptions.includeMetadata);
|
|
53
|
-
IS_DEBUG && console.log('commandOptions.
|
|
195
|
+
IS_DEBUG && console.log('commandOptions.stats = ' + commandOptions?.stats);
|
|
54
196
|
IS_DEBUG && console.log();
|
|
197
|
+
//state returned:
|
|
198
|
+
// - passed (no errors/warnings),
|
|
199
|
+
// - finished (with warnings, no errors) / or - passed with warnings
|
|
200
|
+
// - failed (errors),
|
|
201
|
+
if (isCatchedError) {
|
|
202
|
+
errors = 1;
|
|
203
|
+
}
|
|
204
|
+
// console.log()
|
|
205
|
+
let statusType;
|
|
206
|
+
if (errors) {
|
|
207
|
+
statusType = 'Failed';
|
|
208
|
+
}
|
|
209
|
+
else if (warnings) {
|
|
210
|
+
statusType = 'Passed-with-Warnings';
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
statusType = 'Passed';
|
|
214
|
+
}
|
|
215
|
+
const jsonSummary = toSummaryJson(statusType, file,
|
|
216
|
+
// metadata,
|
|
217
|
+
metadata?.mode ?? 'custom', errors, warnings, notices, infos);
|
|
218
|
+
printSummary(jsonSummary);
|
|
219
|
+
// if (errors) {
|
|
220
|
+
// // red ✖
|
|
221
|
+
// console.error(
|
|
222
|
+
// formatToSummary('Failed', errors, warnings, notices, infos),
|
|
223
|
+
// )
|
|
224
|
+
// // exit(1)
|
|
225
|
+
// } else if (warnings) {
|
|
226
|
+
// // yellow ⚠️
|
|
227
|
+
// console.warn(
|
|
228
|
+
// formatToSummary(
|
|
229
|
+
// 'Passed-with-Warnings',
|
|
230
|
+
// errors,
|
|
231
|
+
// warnings,
|
|
232
|
+
// notices,
|
|
233
|
+
// infos,
|
|
234
|
+
// ),
|
|
235
|
+
// )
|
|
236
|
+
// // exit(0)
|
|
237
|
+
// } else {
|
|
238
|
+
// // green ✔
|
|
239
|
+
// console.log(formatToSummary('Passed', errors, warnings, notices, infos))
|
|
240
|
+
// // exit(0)
|
|
241
|
+
// }
|
|
242
|
+
// Print optional Stats-report if "--stats" was given.
|
|
55
243
|
if (!commandOptions.silent && !isCatchedError) {
|
|
56
|
-
if (commandOptions.
|
|
244
|
+
// if (commandOptions.details) {
|
|
245
|
+
if (errors || warnings) {
|
|
57
246
|
if (!metadata) {
|
|
58
247
|
console.error('Internal Error: No meta data found');
|
|
59
248
|
}
|
|
60
249
|
assert(metadata); // Make sure there is metadata!
|
|
61
250
|
console.log();
|
|
62
|
-
|
|
251
|
+
printIssuesFound(file, metadata);
|
|
63
252
|
}
|
|
64
|
-
if (commandOptions.
|
|
253
|
+
if (commandOptions.stats) {
|
|
65
254
|
if (!metadata) {
|
|
66
255
|
console.error('Internal Error: No meta data found');
|
|
67
256
|
}
|
|
68
257
|
assert(metadata); // Make sure there is metadata!
|
|
69
258
|
console.log();
|
|
70
|
-
|
|
259
|
+
console.log(formatToStatsReport(file, metadata).trim());
|
|
71
260
|
}
|
|
72
261
|
}
|
|
73
|
-
//state returned:
|
|
74
|
-
// - passed (no errors/warnings),
|
|
75
|
-
// - finished (with warnings, no errors) / or - passed with warnings
|
|
76
|
-
// - failed (errors),
|
|
77
|
-
if (isCatchedError) {
|
|
78
|
-
errors = 1;
|
|
79
|
-
}
|
|
80
|
-
// console.log()
|
|
81
262
|
if (errors) {
|
|
82
|
-
// red ✖
|
|
83
|
-
console.error(formatToStatus('Failed', errors, warnings, notices, infos));
|
|
84
263
|
exit(1);
|
|
85
264
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
265
|
+
exit(0);
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* @returns Map to a short summary.
|
|
269
|
+
*/
|
|
270
|
+
const toSummaryJson = (statusType, fileWithPath,
|
|
271
|
+
// metadata: ResultMetadata | null,
|
|
272
|
+
mode, errors, warnings, notices, infos) => {
|
|
273
|
+
let result = '';
|
|
274
|
+
switch (statusType) {
|
|
275
|
+
case 'Passed':
|
|
276
|
+
// result = '✔ Validation passed'
|
|
277
|
+
result = '✔ Validation successful';
|
|
278
|
+
break;
|
|
279
|
+
case 'Passed-with-Warnings':
|
|
280
|
+
// result = '⚠️ Validation finished'
|
|
281
|
+
result = '✔ Validation successful';
|
|
282
|
+
break;
|
|
283
|
+
case 'Failed':
|
|
284
|
+
// result = '✖ Validation failed'
|
|
285
|
+
result = '✖ Validation failed';
|
|
286
|
+
break;
|
|
90
287
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
288
|
+
// const diag = metadata.diagnostics
|
|
289
|
+
const issuesCount = errors + warnings + notices + infos;
|
|
290
|
+
return {
|
|
291
|
+
result: result,
|
|
292
|
+
file: fileWithPath,
|
|
293
|
+
mode: mode,
|
|
294
|
+
summary: {
|
|
295
|
+
issuesCount: issuesCount,
|
|
296
|
+
errors,
|
|
297
|
+
warnings,
|
|
298
|
+
notices,
|
|
299
|
+
infos,
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
};
|
|
303
|
+
const printSummary = (sum) => {
|
|
304
|
+
let str = '';
|
|
305
|
+
str += sum.result + '\n';
|
|
306
|
+
str += '\n';
|
|
307
|
+
str += `File: "${sum.file}"\n`;
|
|
308
|
+
str += `Mode: ${sum.mode.toLowerCase()}\n`;
|
|
309
|
+
str += `Errors: ${sum.summary.errors}\n`;
|
|
310
|
+
str += `Warnings: ${sum.summary.warnings}\n`;
|
|
311
|
+
if (sum.summary.notices) {
|
|
312
|
+
str += `Notices: ${sum.summary.notices}\n`;
|
|
95
313
|
}
|
|
314
|
+
if (sum.summary.infos) {
|
|
315
|
+
str += `Infos: ${sum.summary.infos}\n`;
|
|
316
|
+
}
|
|
317
|
+
str += `Total issues: ${sum.summary.issuesCount}\n`;
|
|
318
|
+
console.log(str);
|
|
96
319
|
};
|
|
97
|
-
|
|
320
|
+
/**
|
|
321
|
+
* @deprecated Use toSummaryJson(..), this TO BE deleted.
|
|
322
|
+
*/
|
|
323
|
+
const formatToSummary = (statusType, errors, warnings, notices, infos) => {
|
|
98
324
|
const totalMsgs = errors + warnings + notices + infos;
|
|
99
325
|
let str = ``;
|
|
100
326
|
switch (statusType) {
|
|
@@ -111,7 +337,7 @@ const formatToStatus = (statusType, errors, warnings, notices, infos) => {
|
|
|
111
337
|
str += ` (${errors} errors, ${warnings} warnings, ${totalMsgs} total messages)`;
|
|
112
338
|
return str;
|
|
113
339
|
};
|
|
114
|
-
// --- Format to a
|
|
340
|
+
// --- Format to a Stats report --------------------------------------------------------
|
|
115
341
|
//@todo format parsed.meta to report as
|
|
116
342
|
/*
|
|
117
343
|
- Produce a summary-level validation report.
|
|
@@ -125,8 +351,8 @@ const formatToStatus = (statusType, errors, warnings, notices, infos) => {
|
|
|
125
351
|
Notices: 0
|
|
126
352
|
Result: INVALID
|
|
127
353
|
*/
|
|
128
|
-
const
|
|
129
|
-
// console.log('
|
|
354
|
+
const formatToStatsReport = (fileWithPath, metadata) => {
|
|
355
|
+
// console.log('formatToStatsReport(..)')
|
|
130
356
|
// printObject(metadata)
|
|
131
357
|
// console.log()
|
|
132
358
|
assert(metadata.diagnostics);
|
|
@@ -177,16 +403,16 @@ Byte Size: ${metadata.source.sourceType === 'inline' ? 'n/a' : metadata.sour
|
|
|
177
403
|
Warning at line 10, column 3: Section level skipped (0 → 2)
|
|
178
404
|
Notice at line 1: Unused @yini directive
|
|
179
405
|
*/
|
|
180
|
-
const
|
|
406
|
+
const printIssuesFound = (fileWithPath, metadata) => {
|
|
181
407
|
// console.log('printDetails(..)')
|
|
182
408
|
// printObject(metadata)
|
|
183
409
|
// console.log(toPrettyJSON(metadata))
|
|
184
410
|
// console.log()
|
|
185
411
|
assert(metadata.diagnostics);
|
|
186
412
|
const diag = metadata.diagnostics;
|
|
187
|
-
console.log('
|
|
188
|
-
console.log('
|
|
189
|
-
console.log();
|
|
413
|
+
IS_DEBUG && console.log('*** Issues Found');
|
|
414
|
+
IS_DEBUG && console.log('*** ------------');
|
|
415
|
+
IS_DEBUG && console.log();
|
|
190
416
|
const errors = diag.errors.payload;
|
|
191
417
|
printIssues('Error ', 'E', errors);
|
|
192
418
|
const warnings = diag.warnings.payload;
|
package/dist/descriptions.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const descriptions = {
|
|
2
|
-
yini: 'CLI for parsing and validating YINI
|
|
3
|
-
'For-command-parse': 'Parse a YINI file (*.yini) and
|
|
4
|
-
'For-command-validate': '
|
|
5
|
-
'For-command-info': '
|
|
2
|
+
yini: 'The official terminal / command-line (CLI) for parsing and validating YINI configuration files.',
|
|
3
|
+
'For-command-parse': 'Parse a YINI file (*.yini) and output the result as JSON or JavaScript.',
|
|
4
|
+
'For-command-validate': 'Validate a YINI file and print a validation report.',
|
|
5
|
+
'For-command-info': 'Display extended information about the YINI CLI environment.',
|
|
6
6
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// helpOption.ts
|
|
1
2
|
/*
|
|
2
3
|
* The main (global) option help.
|
|
3
4
|
*/
|
|
@@ -9,9 +10,9 @@ export const getHelpTextBefore = () => {
|
|
|
9
10
|
return `${getPackageName()} ${getPackageVersion()}
|
|
10
11
|
YINI CLI (Yet another INI)
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
A config format, inspired by INI, with type-safe values,
|
|
14
|
-
sections, comments, minimal syntax noise, and optional strict mode.
|
|
13
|
+
The official terminal / command-line (CLI) for parsing and validating YINI
|
|
14
|
+
configuration files. A config format, inspired by INI, with type-safe values,
|
|
15
|
+
nested sections, comments, minimal syntax noise, and optional strict mode.
|
|
15
16
|
|
|
16
17
|
Designed for clarity and consistency. :)\n`;
|
|
17
18
|
};
|
|
@@ -20,15 +21,21 @@ Designed for clarity and consistency. :)\n`;
|
|
|
20
21
|
*/
|
|
21
22
|
export const getHelpTextAfter = () => {
|
|
22
23
|
return `
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
========================================================
|
|
25
|
+
|
|
26
|
+
Quick Examples:
|
|
27
|
+
$ yini parse file.yini
|
|
28
|
+
$ yini parse file.yini --json
|
|
29
|
+
$ yini parse file.yini --js
|
|
30
|
+
$ yini parse file.yini -o output.json
|
|
25
31
|
$ yini validate --strict config.yini
|
|
26
|
-
$ yini parse config.yini --pretty --output out.json
|
|
27
32
|
|
|
28
33
|
For help with a specific command, use -h or --help. For example:
|
|
29
34
|
$ yini validate --help
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
========================================================
|
|
37
|
+
Example YINI configuration file (config.yini)
|
|
38
|
+
========================================================
|
|
32
39
|
^ App
|
|
33
40
|
title = 'My App'
|
|
34
41
|
items = 10
|
|
@@ -43,10 +50,12 @@ Sample "config.yini":
|
|
|
43
50
|
^^ Login
|
|
44
51
|
username = 'user'
|
|
45
52
|
password = 'secret'
|
|
53
|
+
========================================================
|
|
46
54
|
|
|
47
55
|
More info:
|
|
48
|
-
https://github.com/YINI-lang/yini-cli
|
|
56
|
+
https://github.com/YINI-lang/yini-cli
|
|
49
57
|
|
|
50
|
-
|
|
51
|
-
https://
|
|
58
|
+
YINI homepage:
|
|
59
|
+
https://yini-lang.org
|
|
60
|
+
`;
|
|
52
61
|
};
|
package/dist/index.js
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
//
|
|
3
3
|
// (!) IMPORTANT: Leave the top shebang as the very first line! (otherwise command will break)
|
|
4
4
|
//
|
|
5
|
+
// index.ts
|
|
5
6
|
import { createRequire } from 'module';
|
|
6
7
|
import { Command } from 'commander';
|
|
8
|
+
import { enableHelpAll } from './cli/helpAll.js';
|
|
9
|
+
import { printInfo } from './commands/infoCommand.js';
|
|
7
10
|
import { parseFile } from './commands/parseCommand.js';
|
|
8
11
|
import { validateFile, } from './commands/validateCommand.js';
|
|
9
12
|
import { isDebug, isDev } from './config/env.js';
|
|
10
13
|
import { descriptions as descr } from './descriptions.js';
|
|
11
14
|
import { getHelpTextAfter, getHelpTextBefore, } from './globalOptions/helpOption.js';
|
|
12
|
-
import { printInfo } from './globalOptions/infoOption.js';
|
|
13
15
|
import { debugPrint, toPrettyJSON } from './utils/print.js';
|
|
14
16
|
import { getPackageVersion } from './utils/yiniCliHelpers.js';
|
|
15
17
|
const require = createRequire(import.meta.url);
|
|
16
|
-
// const pkg = require('../package.json')
|
|
17
18
|
// --- Helper functions --------------------------------------------------------
|
|
18
19
|
function appendGlobalOptionsTo(cmd) {
|
|
19
20
|
const help = program.createHelp();
|
|
@@ -21,16 +22,9 @@ function appendGlobalOptionsTo(cmd) {
|
|
|
21
22
|
if (!globals.length)
|
|
22
23
|
return;
|
|
23
24
|
const lines = globals
|
|
24
|
-
// .map(
|
|
25
|
-
// (opt) =>
|
|
26
|
-
// ` ${help.optionTerm(opt)} ${help.optionDescription(opt)}`,
|
|
27
|
-
// )
|
|
28
|
-
// .join('\n')
|
|
29
25
|
.map((option) => {
|
|
30
26
|
const optName = option.name();
|
|
31
|
-
if (optName === 'version' ||
|
|
32
|
-
optName === 'info' ||
|
|
33
|
-
optName === 'help') {
|
|
27
|
+
if (optName === 'version' || optName === 'help') {
|
|
34
28
|
debugPrint('Skip patching option.name() = ' +
|
|
35
29
|
option.name() +
|
|
36
30
|
' into per-command help');
|
|
@@ -42,28 +36,27 @@ function appendGlobalOptionsTo(cmd) {
|
|
|
42
36
|
.join('\n')
|
|
43
37
|
.trim();
|
|
44
38
|
cmd.addHelpText('after', `\nGlobal options:\n ${lines}`);
|
|
45
|
-
// cmd.addHelpText('after', ` ${lines}`)
|
|
46
39
|
}
|
|
47
40
|
// -------------------------------------------------------------------------
|
|
48
41
|
const program = new Command()
|
|
49
42
|
.name('yini')
|
|
50
43
|
.description(descr.yini)
|
|
51
44
|
// Below will replace all auto-registered items (especially the descriptions starting with a capital and ending with a period).
|
|
52
|
-
.version(getPackageVersion(), '-v, --version', '
|
|
53
|
-
.helpOption('-h, --help', 'Display help for
|
|
54
|
-
.helpCommand('help
|
|
45
|
+
.version(getPackageVersion(), '-v, --version', 'Display the version number.')
|
|
46
|
+
.helpOption('-h, --help', 'Display full help for all commands.')
|
|
47
|
+
.helpCommand('help <command>', 'Display help for a specific command.');
|
|
55
48
|
program.addHelpText('before', getHelpTextBefore());
|
|
56
49
|
program.addHelpText('after', getHelpTextAfter());
|
|
57
50
|
/**
|
|
58
|
-
* The (main/global) option: "--
|
|
51
|
+
* The (main/global) option: "--strict, --quite, --silent"
|
|
59
52
|
*/
|
|
60
53
|
// Suggestions for future: --verbose, --debug, --no-color, --color, --timing, --stdin
|
|
61
54
|
program
|
|
62
|
-
.option('-i, --info', 'Show extended information (details, links, etc.).')
|
|
63
55
|
.option('-s, --strict', 'Enable strict parsing mode.')
|
|
64
|
-
.option('-f, --force', 'Continue parsing even if errors occur.')
|
|
56
|
+
// .option('-f, --force', 'Continue parsing even if errors occur.')
|
|
65
57
|
.option('-q, --quiet', 'Reduce output (show only errors).')
|
|
66
58
|
.option('--silent', 'Suppress all output (even errors, exit code only).')
|
|
59
|
+
.option('--verbose', 'Display extra information.')
|
|
67
60
|
.action((options) => {
|
|
68
61
|
debugPrint('Run global options');
|
|
69
62
|
if (isDebug()) {
|
|
@@ -72,31 +65,31 @@ program
|
|
|
72
65
|
}
|
|
73
66
|
printInfo();
|
|
74
67
|
});
|
|
75
|
-
/**
|
|
76
|
-
* The (main/global) option: "--info"
|
|
77
|
-
*/
|
|
78
|
-
// program
|
|
79
|
-
// .option('-s, --strict', 'Enable parsing in strict-mode.')
|
|
80
|
-
// .action((options) => {
|
|
81
|
-
// debugPrint('Run (global) option "strict"')
|
|
82
|
-
// if (isDebug()) {
|
|
83
|
-
// console.log('options:')
|
|
84
|
-
// console.log(toPrettyJSON(options))
|
|
85
|
-
// }
|
|
86
|
-
// printInfo()
|
|
87
|
-
// })
|
|
88
68
|
/**
|
|
89
69
|
* The command: "parse <file>"
|
|
90
70
|
*/
|
|
91
71
|
const parseCmd = program
|
|
92
72
|
.command('parse <file>')
|
|
93
73
|
.description(descr['For-command-parse'])
|
|
94
|
-
|
|
95
|
-
.option('--json', '
|
|
96
|
-
.option('--
|
|
74
|
+
// Output format options.
|
|
75
|
+
.option('--json', 'Output as formatted JSON (default).')
|
|
76
|
+
.option('--compact', 'Output compact JSON (no whitespace).')
|
|
77
|
+
.option('--js', 'Output as JavaScript.')
|
|
78
|
+
// File handling options.
|
|
79
|
+
.option('-o, --output <file>', 'Write output to <file>. By default, an existing file is only overwritten if it is older than the input YINI file.')
|
|
80
|
+
.option('--overwrite', 'Always overwrite the output file, even if it is newer than the input YINI file.')
|
|
81
|
+
.option('--no-overwrite', 'Fail if the output file already exists.')
|
|
82
|
+
// Behavior options.
|
|
83
|
+
.option('--best-effort', 'Ignore parse errors.')
|
|
84
|
+
// Deprecated options.
|
|
85
|
+
.option('--pretty', '(deprecated) Use --json instead.')
|
|
97
86
|
.action((file, options) => {
|
|
98
87
|
const globals = program.opts(); // Global options.
|
|
99
88
|
const mergedOptions = { ...globals, ...options }; // Merge global options with per-command options.
|
|
89
|
+
if (mergedOptions.js && mergedOptions.compact) {
|
|
90
|
+
console.error('Error: --js and --compact cannot be combined.');
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
100
93
|
debugPrint('Run command "parse"');
|
|
101
94
|
debugPrint('isDebug(): ' + isDebug());
|
|
102
95
|
debugPrint('isDev() : ' + isDev());
|
|
@@ -105,12 +98,7 @@ const parseCmd = program
|
|
|
105
98
|
console.log('mergedOptions:');
|
|
106
99
|
console.log(toPrettyJSON(mergedOptions));
|
|
107
100
|
}
|
|
108
|
-
|
|
109
|
-
parseFile(file, mergedOptions);
|
|
110
|
-
}
|
|
111
|
-
else {
|
|
112
|
-
program.help();
|
|
113
|
-
}
|
|
101
|
+
parseFile(file, mergedOptions);
|
|
114
102
|
});
|
|
115
103
|
appendGlobalOptionsTo(parseCmd);
|
|
116
104
|
/**
|
|
@@ -119,9 +107,11 @@ appendGlobalOptionsTo(parseCmd);
|
|
|
119
107
|
const validateCmd = program
|
|
120
108
|
.command('validate <file>')
|
|
121
109
|
.description(descr['For-command-validate'])
|
|
122
|
-
.option('--
|
|
123
|
-
.option(
|
|
124
|
-
//
|
|
110
|
+
.option('--stats', 'Include a statistics section (e.g., key count, nesting depth, etc.).')
|
|
111
|
+
// .option(
|
|
112
|
+
// '--details',
|
|
113
|
+
// 'Print detailed validation info (e.g., line locations, error codes, descriptive text, etc.).',
|
|
114
|
+
// )
|
|
125
115
|
.action((file, options) => {
|
|
126
116
|
const globals = program.opts(); // Global options.
|
|
127
117
|
const mergedOptions = { ...globals, ...options }; // Merge global options with per-command options.
|
|
@@ -141,20 +131,21 @@ const validateCmd = program
|
|
|
141
131
|
}
|
|
142
132
|
});
|
|
143
133
|
appendGlobalOptionsTo(validateCmd);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
134
|
+
/**
|
|
135
|
+
* The command: "info"
|
|
136
|
+
*/
|
|
137
|
+
const infoCmd = program
|
|
147
138
|
.command('info')
|
|
148
139
|
.description(descr['For-command-info'])
|
|
149
|
-
.
|
|
140
|
+
.enablePositionalOptions(false) // NOTE: If false, then options must appear before the subcommand (the subcommand will not see them).
|
|
141
|
+
.usage(' ') // NOTE: Must be a space, to override usage (removes [options]) completely.
|
|
142
|
+
.addHelpText('after', '') // (?) Prevents Commander from auto-adding option text.
|
|
143
|
+
.action(() => {
|
|
150
144
|
debugPrint('Run command "info"');
|
|
151
|
-
if (isDebug()) {
|
|
152
|
-
console.log('options:');
|
|
153
|
-
console.log(toPrettyJSON(options));
|
|
154
|
-
}
|
|
155
|
-
console.warn('Deprecated: Use `yini --info` or `yini -i` instead of `yini info`.');
|
|
156
145
|
printInfo();
|
|
157
146
|
});
|
|
147
|
+
appendGlobalOptionsTo(infoCmd);
|
|
158
148
|
// NOTE: Converting YINI files to other formats than json and js.
|
|
159
|
-
// Other format should go into a new CLI-command called 'yini-convert'.
|
|
149
|
+
// Other format should go into a new CLI-command called 'yini-convert' to not let this command grow too large.
|
|
150
|
+
enableHelpAll(program);
|
|
160
151
|
program.parseAsync();
|
package/dist/types.d.ts
CHANGED
package/dist/utils/print.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
type ToPrettyJSOptions = {
|
|
2
|
+
indent?: number;
|
|
3
|
+
exportDefault?: boolean;
|
|
4
|
+
trailingSemicolon?: boolean;
|
|
5
|
+
};
|
|
1
6
|
export declare const debugPrint: (str?: any) => void;
|
|
2
7
|
export declare const devPrint: (str?: any) => void;
|
|
3
8
|
export declare const toJSON: (obj: any) => string;
|
|
@@ -10,5 +15,16 @@ export declare const printJSON: (obj: any, isForce?: boolean) => void;
|
|
|
10
15
|
* Print a full JavaScript object in a human-readable way (not as JSON).
|
|
11
16
|
* Not strict JSON, and shows functions, symbols, getters/setters, and class names.
|
|
12
17
|
* @param isColors If true, the output is styled with ANSI color codes.
|
|
18
|
+
*
|
|
19
|
+
* @note This function relies on util.inspect(..).
|
|
13
20
|
*/
|
|
14
21
|
export declare const printObject: (obj: any, isForce?: boolean, isColors?: boolean) => void;
|
|
22
|
+
/**
|
|
23
|
+
* A clean and stable toPrettyJS(..) without relying on util.inspect(..).
|
|
24
|
+
* - Deterministic output (unlike util.inspect, which can vary across Node versions).
|
|
25
|
+
* - Exactly 4 spaces.
|
|
26
|
+
* - Clean single quotes.
|
|
27
|
+
* - Proper escaping, and safe quoting of non-identifier keys.
|
|
28
|
+
*/
|
|
29
|
+
export declare function toPrettyJS(value: unknown, opts?: ToPrettyJSOptions): string;
|
|
30
|
+
export {};
|
package/dist/utils/print.js
CHANGED
|
@@ -33,6 +33,8 @@ export const printJSON = (obj, isForce = false) => {
|
|
|
33
33
|
* Print a full JavaScript object in a human-readable way (not as JSON).
|
|
34
34
|
* Not strict JSON, and shows functions, symbols, getters/setters, and class names.
|
|
35
35
|
* @param isColors If true, the output is styled with ANSI color codes.
|
|
36
|
+
*
|
|
37
|
+
* @note This function relies on util.inspect(..).
|
|
36
38
|
*/
|
|
37
39
|
export const printObject = (obj, isForce = false, isColors = true) => {
|
|
38
40
|
if (!isForce) {
|
|
@@ -41,3 +43,79 @@ export const printObject = (obj, isForce = false, isColors = true) => {
|
|
|
41
43
|
}
|
|
42
44
|
console.log(util.inspect(obj, { depth: null, colors: isColors }));
|
|
43
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* A clean and stable toPrettyJS(..) without relying on util.inspect(..).
|
|
48
|
+
* - Deterministic output (unlike util.inspect, which can vary across Node versions).
|
|
49
|
+
* - Exactly 4 spaces.
|
|
50
|
+
* - Clean single quotes.
|
|
51
|
+
* - Proper escaping, and safe quoting of non-identifier keys.
|
|
52
|
+
*/
|
|
53
|
+
export function toPrettyJS(value, opts = {}) {
|
|
54
|
+
const indent = opts.indent ?? 4;
|
|
55
|
+
const exportDefault = opts.exportDefault ?? false;
|
|
56
|
+
const trailingSemicolon = opts.trailingSemicolon ?? exportDefault;
|
|
57
|
+
const pad = (level) => ' '.repeat(level * indent);
|
|
58
|
+
const isPlainObject = (v) => v !== null &&
|
|
59
|
+
typeof v === 'object' &&
|
|
60
|
+
Object.getPrototypeOf(v) === Object.prototype;
|
|
61
|
+
const isValidIdentifier = (key) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key);
|
|
62
|
+
const escapeString = (s) => s
|
|
63
|
+
.replace(/\\/g, '\\\\')
|
|
64
|
+
.replace(/'/g, "\\'")
|
|
65
|
+
.replace(/\r/g, '\\r')
|
|
66
|
+
.replace(/\n/g, '\\n')
|
|
67
|
+
.replace(/\t/g, '\\t')
|
|
68
|
+
.replace(/\u2028/g, '\\u2028')
|
|
69
|
+
.replace(/\u2029/g, '\\u2029');
|
|
70
|
+
const formatKey = (key) => isValidIdentifier(key) ? key : `'${escapeString(key)}'`;
|
|
71
|
+
const format = (v, level) => {
|
|
72
|
+
if (v === null)
|
|
73
|
+
return 'null';
|
|
74
|
+
const t = typeof v;
|
|
75
|
+
if (t === 'string')
|
|
76
|
+
return `'${escapeString(v)}'`;
|
|
77
|
+
if (t === 'number')
|
|
78
|
+
return Number.isFinite(v) ? String(v) : 'null';
|
|
79
|
+
if (t === 'boolean')
|
|
80
|
+
return v ? 'true' : 'false';
|
|
81
|
+
if (t === 'bigint')
|
|
82
|
+
return `${v}n`;
|
|
83
|
+
if (t === 'undefined')
|
|
84
|
+
return 'undefined';
|
|
85
|
+
if (t === 'function')
|
|
86
|
+
return '[Function]';
|
|
87
|
+
if (t === 'symbol')
|
|
88
|
+
return 'Symbol()';
|
|
89
|
+
if (v instanceof Date)
|
|
90
|
+
return `'${v.toISOString()}'`;
|
|
91
|
+
if (Array.isArray(v)) {
|
|
92
|
+
if (v.length === 0)
|
|
93
|
+
return '[]';
|
|
94
|
+
const inner = v
|
|
95
|
+
.map((item) => `${pad(level + 1)}${format(item, level + 1)}`)
|
|
96
|
+
.join(',\n');
|
|
97
|
+
return `[\n${inner}\n${pad(level)}]`;
|
|
98
|
+
}
|
|
99
|
+
if (isPlainObject(v)) {
|
|
100
|
+
const keys = Object.keys(v);
|
|
101
|
+
if (keys.length === 0)
|
|
102
|
+
return '{}';
|
|
103
|
+
const inner = keys
|
|
104
|
+
.map((k) => `${pad(level + 1)}${formatKey(k)}: ${format(v[k], level + 1)}`)
|
|
105
|
+
.join(',\n');
|
|
106
|
+
return `{\n${inner}\n${pad(level)}}`;
|
|
107
|
+
}
|
|
108
|
+
// Fallback for Map/Set/class instances etc.
|
|
109
|
+
try {
|
|
110
|
+
// JSON fallback keeps it stable-ish for "weird" objects
|
|
111
|
+
return `'${escapeString(JSON.stringify(v))}'`;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return "'[Unserializable]'";
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
const body = format(value, 0);
|
|
118
|
+
if (!exportDefault)
|
|
119
|
+
return body;
|
|
120
|
+
return `export default ${body}${trailingSemicolon ? ';' : ''}\n`;
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yini-cli",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "CLI for
|
|
3
|
+
"version": "1.2.0-beta",
|
|
4
|
+
"description": "CLI tool for YINI text-based format - an INI-like config format with real structure, comments, nested sections, no YAML foot-guns, and less noise than JSON.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"yini",
|
|
7
7
|
"cli",
|
|
8
|
+
"data-format",
|
|
8
9
|
"config",
|
|
9
10
|
"configuration",
|
|
10
11
|
"parser",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const printInfo: () => void;
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
|
-
// import pkg from '../../package.json' with { type: 'json' } // NOTE: Must use { type: 'json' } when in ESM mode.
|
|
3
|
-
const require = createRequire(import.meta.url);
|
|
4
|
-
const pkg = require('../../package.json');
|
|
5
|
-
export const printInfo = () => {
|
|
6
|
-
console.log(`*** YINI CLI ***`);
|
|
7
|
-
console.log(`yini-cli: ${pkg.version}`);
|
|
8
|
-
console.log(`yini-parser: ${pkg.dependencies['yini-parser'].replace('^', '')}`);
|
|
9
|
-
console.log(`Author: ${pkg.author}`);
|
|
10
|
-
console.log(`License: ${pkg.license}`);
|
|
11
|
-
console.log(`Homepage: ${pkg.homepage}`);
|
|
12
|
-
console.log('Repo: https://github.com/YINI-lang/yini-cli');
|
|
13
|
-
};
|