yini-cli 1.0.2-beta → 1.1.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 CHANGED
@@ -3,7 +3,7 @@
3
3
 
4
4
  *YINI aims to be a human-friendly config format: like INI, but with type-safe values, nested sections, comments, minimal syntax noise, and optional strict mode.*
5
5
 
6
- [![npm version](https://img.shields.io/npm/v/yini-parser.svg)](https://www.npmjs.com/package/yini-parser) [![All Tests](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml)
6
+ [![npm version](https://img.shields.io/npm/v/yini-cli.svg)](https://www.npmjs.com/package/yini-cli) [![All Test Suites](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-all-tests.yml) [![Regression Tests](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-regression-tests.yml) [![CLI Test CI](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml/badge.svg)](https://github.com/YINI-lang/yini-cli/actions/workflows/run-cli-test.yml)
7
7
 
8
8
  ---
9
9
 
@@ -15,6 +15,15 @@
15
15
  * Supports commonly used configuration structures.
16
16
  - *Developed to meet practical needs, driven by curiosity and a desire **for configuration clarity, simplicity, minimalism, and flexibility**.
17
17
 
18
+ ⭐ **Enjoying yini-cli?** If you like this project, [star it on GitHub](https://github.com/YINI-lang/yini-cli) — it helps a lot, thank you!
19
+
20
+ ---
21
+
22
+ ## Requirements
23
+ YINI CLI requires Node.js **v20 or later**.
24
+
25
+ (It has also been tested with Node.js v13+, but v20+ is recommended for best compatibility.)
26
+
18
27
  ---
19
28
 
20
29
  ## 💡 What is YINI?
@@ -28,7 +37,75 @@
28
37
 
29
38
  ---
30
39
 
31
- ## Intro to YINI Config Format
40
+ ## Quick Into to YINI Format
41
+
42
+ YINI code looks like this:
43
+ ```yini
44
+ // This is a comment in YINI
45
+ // YINI is a simple, human-readable configuration file format.
46
+
47
+ // Note: In YINI, spaces and tabs don't change meaning - indentation is just
48
+ // for readability.
49
+
50
+ /* This is a block comment
51
+
52
+ In YINI, section headers use repeated characters "^" at the start to
53
+ show their level: (Section header names are case-sensitive.)
54
+
55
+ ^ SectionLevel1
56
+ ^^ SectionLevel2
57
+ ^^^ SectionLevel3
58
+ */
59
+
60
+ ^ App // Definition of section (group) "App"
61
+ name = 'My Title' // Keys and values are written as key = value
62
+ items = 25
63
+ darkMode = true // "ON" and "YES" works too
64
+
65
+ // Sub-section of the "App" section
66
+ ^^ Special
67
+ primaryColor = #336699 // Hex number format
68
+ isCaching = false // "OFF" and "NO" works too
69
+ ```
70
+
71
+ **The above YINI converted to a JS object:**
72
+ ```js
73
+ {
74
+ App: {
75
+ name: 'My Title',
76
+ items: 25,
77
+ darkMode: true,
78
+ Special: {
79
+ primaryColor: 3368601,
80
+ isCaching: false
81
+ }
82
+ }
83
+ }
84
+ ```
85
+
86
+ **In JSON:**
87
+ ```json
88
+ {
89
+ "App":{
90
+ "name":"My Title",
91
+ "items":25,
92
+ "darkMode":true,
93
+ "Special":{
94
+ "primaryColor":3368601,
95
+ "isCaching":false
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ That's it!
102
+
103
+ - ▶️ Link to [examples/](https://github.com/YINI-lang/yini-parser-typescript/tree/main/examples) files.
104
+ - ▶️ Link to [Demo Apps](https://github.com/YINI-lang/yini-demo-apps/tree/main) with complete basic usage.
105
+
106
+ ---
107
+
108
+ ## Bigger Intro into YINI Config Format
32
109
  **YINI** is a simple and readable configuration format. Sections are defined with `^ SectionName`, and values are assigned using `key = value`. The format supports common data types (same as those found in JSON), including strings, numbers, booleans, nulls, and lists.
33
110
 
34
111
  To learn more, see the [Getting Started: Intro to YINI Config Format](https://github.com/YINI-lang/YINI-spec/blob/develop/Docs/Intro-to-YINI-Config-Format.md) tutorial.
@@ -39,7 +116,7 @@ To learn more, see the [Getting Started: Intro to YINI Config Format](https://gi
39
116
 
40
117
  ### Installation
41
118
 
42
- 1. **Install it globally from npm**
119
+ 1. **Install it globally from npm — (requires Node.js)**
43
120
  Open your terminal and run:
44
121
  ```
45
122
  npm install -g yini-cli
@@ -0,0 +1,7 @@
1
+ import { IGlobalOptions } from '../types.js';
2
+ export interface IParseCommandOptions extends IGlobalOptions {
3
+ pretty?: boolean;
4
+ json?: boolean;
5
+ output?: string;
6
+ }
7
+ export declare const parseFile: (file: string, commandOptions: IParseCommandOptions) => void;
@@ -3,44 +3,45 @@ import path from 'node:path';
3
3
  import util from 'util';
4
4
  import YINI from 'yini-parser';
5
5
  import { debugPrint, printObject, toPrettyJSON } from '../utils/print.js';
6
- export const parseFile = (file, options) => {
7
- const outputFile = options.output || '';
8
- const isStrictMode = !!options.strict;
6
+ // -------------------------------------------------------------------------
7
+ export const parseFile = (file, commandOptions) => {
8
+ const outputFile = commandOptions.output || '';
9
+ const isStrictMode = !!commandOptions.strict;
9
10
  let outputStyle = 'JS-style';
10
11
  debugPrint('file = ' + file);
11
- debugPrint('output = ' + options.output);
12
- debugPrint('options:');
13
- printObject(options);
14
- if (options.pretty) {
12
+ debugPrint('output = ' + commandOptions.output);
13
+ debugPrint('commandOptions:');
14
+ printObject(commandOptions);
15
+ if (commandOptions.pretty) {
15
16
  outputStyle = 'Pretty-JSON';
16
17
  }
17
- else if (options.log) {
18
- outputStyle = 'Console.log';
19
- }
20
- else if (options.json) {
18
+ else if (commandOptions.json) {
21
19
  outputStyle = 'JSON-compact';
22
20
  }
23
21
  else {
24
22
  outputStyle = 'JS-style';
25
23
  }
26
- doParseFile(file, outputStyle, isStrictMode, outputFile);
24
+ doParseFile(file, commandOptions, outputStyle, outputFile);
27
25
  };
28
- const doParseFile = (file, outputStyle, isStrictMode = false, outputFile = '') => {
29
- // let strictMode = !!options.strict
30
- let bailSensitivity = 'auto';
26
+ const doParseFile = (file, commandOptions, outputStyle, outputFile = '') => {
27
+ // let strictMode = !!commandOptions.strict
28
+ let preferredFailLevel = 'auto';
31
29
  let includeMetaData = false;
32
30
  debugPrint('File = ' + file);
33
31
  debugPrint('outputStyle = ' + outputStyle);
32
+ const parseOptions = {
33
+ strictMode: commandOptions.strict ?? false,
34
+ // failLevel: 'errors',
35
+ failLevel: preferredFailLevel,
36
+ // failLevel: 'ignore-errors',
37
+ includeMetadata: includeMetaData,
38
+ };
39
+ // If --force then override fail-level.
40
+ if (commandOptions.force) {
41
+ parseOptions.failLevel = 'ignore-errors';
42
+ }
34
43
  try {
35
- // const raw = fs.readFileSync(file, 'utf-8')
36
- // const parsed = YINI.parseFile(
37
- //const parsed = YINI.parseFile(file)
38
- const parsed = YINI.parseFile(file, isStrictMode, bailSensitivity, includeMetaData);
39
- // const parsed = YINI.parse(raw)
40
- // const output = options.pretty
41
- // ? // ? JSON.stringify(parsed, null, 2)
42
- // toPrettyJSON(parsed)
43
- // : JSON.stringify(parsed)
44
+ const parsed = YINI.parseFile(file, parseOptions);
44
45
  let output = '';
45
46
  switch (outputStyle) {
46
47
  case 'Pretty-JSON':
@@ -0,0 +1,6 @@
1
+ import { IGlobalOptions } from '../types.js';
2
+ export interface IValidateCommandOptions extends IGlobalOptions {
3
+ report?: boolean;
4
+ details?: boolean;
5
+ }
6
+ export declare const validateFile: (file: string, commandOptions?: IValidateCommandOptions) => never;
@@ -0,0 +1,217 @@
1
+ import assert from 'node:assert';
2
+ import { exit } from 'node:process';
3
+ import YINI from 'yini-parser';
4
+ const IS_DEBUG = false; // For local debugging purposes, etc.
5
+ // -------------------------------------------------------------------------
6
+ export const validateFile = (file, commandOptions = {}) => {
7
+ let parsedResult = undefined;
8
+ let isCatchedError = true;
9
+ const parseOptions = {
10
+ strictMode: commandOptions.strict ?? false,
11
+ // failLevel: 'errors',
12
+ failLevel: commandOptions.force ? 'ignore-errors' : 'errors',
13
+ // failLevel: 'ignore-errors',
14
+ includeMetadata: true,
15
+ includeDiagnostics: true,
16
+ silent: true,
17
+ };
18
+ try {
19
+ parsedResult = YINI.parseFile(file, parseOptions);
20
+ isCatchedError = false;
21
+ }
22
+ catch (err) {
23
+ isCatchedError = true;
24
+ }
25
+ let metadata = null;
26
+ let errors = 0;
27
+ let warnings = 0;
28
+ let notices = 0;
29
+ let infos = 0;
30
+ if (!isCatchedError && parsedResult?.meta) {
31
+ metadata = parsedResult?.meta;
32
+ assert(metadata); // Make sure there is metadata!
33
+ // printObject(metadata, true)
34
+ assert(metadata.diagnostics);
35
+ const diag = metadata.diagnostics;
36
+ errors = diag.errors.errorCount;
37
+ warnings = diag.warnings.warningCount;
38
+ notices = diag.notices.noticeCount;
39
+ infos = diag.infos.infoCount;
40
+ }
41
+ IS_DEBUG && console.log();
42
+ IS_DEBUG && console.log('isCatchedError = ' + isCatchedError);
43
+ IS_DEBUG && console.log('TEMP OUTPUT');
44
+ IS_DEBUG && console.log('isCatchedError = ' + isCatchedError);
45
+ IS_DEBUG && console.log(' errors = ' + errors);
46
+ IS_DEBUG && console.log('warnings = ' + warnings);
47
+ IS_DEBUG && console.log(' notices = ' + notices);
48
+ IS_DEBUG && console.log(' infor = ' + infos);
49
+ IS_DEBUG && console.log('metadata = ' + metadata);
50
+ IS_DEBUG &&
51
+ console.log('includeMetadata = ' +
52
+ metadata?.diagnostics?.effectiveOptions.includeMetadata);
53
+ IS_DEBUG && console.log('commandOptions.report = ' + commandOptions?.report);
54
+ IS_DEBUG && console.log();
55
+ if (!commandOptions.silent && !isCatchedError) {
56
+ if (commandOptions.report) {
57
+ if (!metadata) {
58
+ console.error('Internal Error: No meta data found');
59
+ }
60
+ assert(metadata); // Make sure there is metadata!
61
+ console.log();
62
+ console.log(formatToReport(file, metadata).trim());
63
+ }
64
+ if (commandOptions.details) {
65
+ if (!metadata) {
66
+ console.error('Internal Error: No meta data found');
67
+ }
68
+ assert(metadata); // Make sure there is metadata!
69
+ console.log();
70
+ printDetailsOnAllIssue(file, metadata);
71
+ }
72
+ }
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
+ if (errors) {
82
+ // red ✖
83
+ console.error(formatToStatus('Failed', errors, warnings, notices, infos));
84
+ exit(1);
85
+ }
86
+ else if (warnings) {
87
+ // yellow ⚠️
88
+ console.warn(formatToStatus('Passed-with-Warnings', errors, warnings, notices, infos));
89
+ exit(0);
90
+ }
91
+ else {
92
+ // green ✔
93
+ console.log(formatToStatus('Passed', errors, warnings, notices, infos));
94
+ exit(0);
95
+ }
96
+ };
97
+ const formatToStatus = (statusType, errors, warnings, notices, infos) => {
98
+ const totalMsgs = errors + warnings + notices + infos;
99
+ let str = ``;
100
+ switch (statusType) {
101
+ case 'Passed':
102
+ str = '✔ Validation passed';
103
+ break;
104
+ case 'Passed-with-Warnings':
105
+ str = '⚠️ Validation finished';
106
+ break;
107
+ case 'Failed':
108
+ str = '✖ Validation failed';
109
+ break;
110
+ }
111
+ str += ` (${errors} errors, ${warnings} warnings, ${totalMsgs} total messages)`;
112
+ return str;
113
+ };
114
+ // --- Format to a Report --------------------------------------------------------
115
+ //@todo format parsed.meta to report as
116
+ /*
117
+ - Produce a summary-level validation report.
118
+ - Output is structured and concise (e.g. JSON or table-like).
119
+ - Focus on counts, pass/fail, severity summary.
120
+
121
+ Example:
122
+ Validation report for config.yini:
123
+ Errors: 3
124
+ Warnings: 1
125
+ Notices: 0
126
+ Result: INVALID
127
+ */
128
+ const formatToReport = (fileWithPath, metadata) => {
129
+ // console.log('formatToReport(..)')
130
+ // printObject(metadata)
131
+ // console.log()
132
+ assert(metadata.diagnostics);
133
+ const diag = metadata.diagnostics;
134
+ const issuesCount = diag.errors.errorCount +
135
+ diag.warnings.warningCount +
136
+ diag.notices.noticeCount +
137
+ diag.infos.infoCount;
138
+ const str = `Validation Report
139
+ =================
140
+
141
+ File "${fileWithPath}"
142
+ Issues: ${issuesCount}
143
+
144
+ Summary
145
+ -------
146
+ Mode: ${metadata.mode}
147
+ Strict: ${metadata.mode === 'strict'}
148
+
149
+ Errors: ${diag.errors.errorCount}
150
+ Warnings: ${diag.warnings.warningCount}
151
+ Notices: ${diag.notices.noticeCount}
152
+ Infos: ${diag.infos.infoCount}
153
+
154
+ Stats
155
+ -----
156
+ Line Count: ${metadata.source.lineCount}
157
+ Section Count: ${metadata.structure.sectionCount}
158
+ Member Count: ${metadata.structure.memberCount}
159
+ Nesting Depth: ${metadata.structure.maxDepth}
160
+
161
+ Has @YINI: ${metadata.source.hasYiniMarker}
162
+ Has /END: ${metadata.source.hasDocumentTerminator}
163
+ Byte Size: ${metadata.source.sourceType === 'inline' ? 'n/a' : metadata.source.byteSize + ' bytes'}
164
+ `;
165
+ return str;
166
+ };
167
+ // -------------------------------------------------------------------------
168
+ // --- Format to a Details --------------------------------------------------------
169
+ //@todo format parsed.meta to details as
170
+ /*
171
+ - Show full detailed validation messages.
172
+ - Output includes line numbers, columns, error codes, and descriptive text.
173
+ - Useful for debugging YINI files.
174
+
175
+ Example:
176
+ Error at line 5, column 9: Unexpected '/END' — expected <EOF>
177
+ Warning at line 10, column 3: Section level skipped (0 → 2)
178
+ Notice at line 1: Unused @yini directive
179
+ */
180
+ const printDetailsOnAllIssue = (fileWithPath, metadata) => {
181
+ // console.log('printDetails(..)')
182
+ // printObject(metadata)
183
+ // console.log(toPrettyJSON(metadata))
184
+ // console.log()
185
+ assert(metadata.diagnostics);
186
+ const diag = metadata.diagnostics;
187
+ console.log('Details');
188
+ console.log('-------');
189
+ console.log();
190
+ const errors = diag.errors.payload;
191
+ printIssues('Error ', 'E', errors);
192
+ const warnings = diag.warnings.payload;
193
+ printIssues('Warning', 'W', warnings);
194
+ const notices = diag.notices.payload;
195
+ printIssues('Notice ', 'N', notices);
196
+ const infos = diag.infos.payload;
197
+ printIssues('Info ', 'I', infos);
198
+ return;
199
+ };
200
+ // -------------------------------------------------------------------------
201
+ const printIssues = (typeLabel, prefix, issues) => {
202
+ const leftPadding = ' ';
203
+ issues.forEach((iss, i) => {
204
+ const id = '#' + prefix + '-0' + (i + 1);
205
+ // const id: string = '' + prefix + '-0' + (i+1) + ':'
206
+ let str = `${typeLabel} [${id}]:\n`;
207
+ str +=
208
+ leftPadding +
209
+ `At line ${iss.line}, column ${iss.column}: ${iss.message}`;
210
+ if (iss.advice)
211
+ str += '\n' + leftPadding + iss.advice;
212
+ if (iss.hint)
213
+ str += '\n' + leftPadding + iss.hint;
214
+ console.log(str);
215
+ console.log();
216
+ });
217
+ };
@@ -2,5 +2,5 @@ export const descriptions = {
2
2
  yini: 'CLI for parsing and validating YINI config files.',
3
3
  'For-command-parse': 'Parse a YINI file (*.yini) and print the result.',
4
4
  'For-command-validate': 'Checks if the file can be parsed as valid YINI.',
5
- 'For-command-info': 'Show extended information (details, links, etc.).',
5
+ 'For-command-info': 'Deprecated: Use `yini --info` or `yini -i` instead.',
6
6
  };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Text to be displayed BEFORE the built-in help text block.
3
+ */
4
+ export declare const getHelpTextBefore: () => string;
5
+ /**
6
+ * Text to be displayed AFTER the built-in help text block.
7
+ */
8
+ export declare const getHelpTextAfter: () => string;
@@ -0,0 +1,52 @@
1
+ /*
2
+ * The main (global) option help.
3
+ */
4
+ import { getPackageName, getPackageVersion } from '../utils/yiniCliHelpers.js';
5
+ /**
6
+ * Text to be displayed BEFORE the built-in help text block.
7
+ */
8
+ export const getHelpTextBefore = () => {
9
+ return `${getPackageName()} ${getPackageVersion()}
10
+ YINI CLI (Yet another INI)
11
+
12
+ Parse and validate YINI configuration files.
13
+ A config format, inspired by INI, with type-safe values, nested
14
+ sections, comments, minimal syntax noise, and optional strict mode.
15
+
16
+ Designed for clarity and consistency. :)\n`;
17
+ };
18
+ /**
19
+ * Text to be displayed AFTER the built-in help text block.
20
+ */
21
+ export const getHelpTextAfter = () => {
22
+ return `
23
+ Examples:
24
+ $ yini parse config.yini
25
+ $ yini validate --strict config.yini
26
+ $ yini parse config.yini --pretty --output out.json
27
+
28
+ For help with a specific command, use -h or --help. For example:
29
+ $ yini validate --help
30
+
31
+ Sample "config.yini":
32
+ ^ App
33
+ title = 'My App'
34
+ items = 10
35
+ debug = ON
36
+
37
+ ^ Server
38
+ host = 'localhost'
39
+ port = 8080
40
+ useTLS = OFF
41
+
42
+ // Sub-section of Server.
43
+ ^^ Login
44
+ username = 'user'
45
+ password = 'secret'
46
+
47
+ More info:
48
+ https://github.com/YINI-lang/yini-cli
49
+
50
+ Into to YINI Config:
51
+ https://github.com/YINI-lang/YINI-spec/blob/develop/Docs/Intro-to-YINI-Config-Format.md`;
52
+ };
@@ -1,8 +1,9 @@
1
1
  import { createRequire } from 'module';
2
+ // import pkg from '../../package.json' with { type: 'json' } // NOTE: Must use { type: 'json' } when in ESM mode.
2
3
  const require = createRequire(import.meta.url);
3
4
  const pkg = require('../../package.json');
4
5
  export const printInfo = () => {
5
- console.log(`** YINI CLI **`);
6
+ console.log(`*** YINI CLI ***`);
6
7
  console.log(`yini-cli: ${pkg.version}`);
7
8
  console.log(`yini-parser: ${pkg.dependencies['yini-parser'].replace('^', '')}`);
8
9
  console.log(`Author: ${pkg.author}`);
package/dist/index.js CHANGED
@@ -1,187 +1,158 @@
1
1
  #!/usr/bin/env node
2
- // (!) NOTE: Leave above shebang as first line!
3
- // import pkg from '../package.json'
2
+ //
3
+ // (!) IMPORTANT: Leave the top shebang as the very first line! (otherwise command will break)
4
+ //
4
5
  import { createRequire } from 'module';
5
6
  import { Command } from 'commander';
6
- import { printInfo } from './commands/info.js';
7
- import { parseFile } from './commands/parse.js';
8
- import { validateFile } from './commands/validate.js';
7
+ import { parseFile } from './commands/parseCommand.js';
8
+ import { validateFile, } from './commands/validateCommand.js';
9
9
  import { isDebug, isDev } from './config/env.js';
10
10
  import { descriptions as descr } from './descriptions.js';
11
+ import { getHelpTextAfter, getHelpTextBefore, } from './globalOptions/helpOption.js';
12
+ import { printInfo } from './globalOptions/infoOption.js';
11
13
  import { debugPrint, toPrettyJSON } from './utils/print.js';
14
+ import { getPackageVersion } from './utils/yiniCliHelpers.js';
12
15
  const require = createRequire(import.meta.url);
13
- const pkg = require('../package.json');
14
- const program = new Command();
15
- /*
16
-
17
- Idea/suggestion
18
- yini [parse] [--strict] [--pretty] [--output]
19
-
20
- Current suggestion:
21
- * yini parse config.yini
22
- JS-style object using printObject()
23
- to stdout
24
- * yini parse config.yini --pretty
25
- Pretty JSON using JSON.stringify(obj, null, 4)
26
- to stdout
27
- * yini parse config.yini --output out.txt
28
- JS-style object
29
- to out.txt
30
- * yini parse config.yini --pretty --output out.json
31
- Pretty JSON
32
- to out.json
33
-
34
- New suggestion:
35
- Current suggestion:
36
- * yini parse config.yini
37
- JS-style object using printObject(obj) (using using util.inspect)
38
- to stdout
39
- * yini parse config.yini --pretty
40
- Pretty JSON using JSON.stringify(obj, null, 4) (formatted, readable)
41
- to stdout
42
- * yini parse config.yini --log
43
- Intended for quick output using console.log (nested object may get compacted/abbreviate)
44
- to stdout
45
- * yini parse config.yini --json
46
- Stringigies JSON using using JSON.stringify(obj) (compact, machine-parseable)
47
- to stdout
48
- * yini parse config.yini --output out.txt
49
- JS-style object
50
- to out.txt
51
- * yini parse config.yini --pretty --output out.json
52
- Pretty JSON
53
- to out.json
54
-
55
- */
56
- // Display help for command
57
- program
16
+ // const pkg = require('../package.json')
17
+ // --- Helper functions --------------------------------------------------------
18
+ function appendGlobalOptionsTo(cmd) {
19
+ const help = program.createHelp();
20
+ const globals = help.visibleOptions(program);
21
+ if (!globals.length)
22
+ return;
23
+ const lines = globals
24
+ // .map(
25
+ // (opt) =>
26
+ // ` ${help.optionTerm(opt)} ${help.optionDescription(opt)}`,
27
+ // )
28
+ // .join('\n')
29
+ .map((option) => {
30
+ const optName = option.name();
31
+ if (optName === 'version' ||
32
+ optName === 'info' ||
33
+ optName === 'help') {
34
+ debugPrint('Skip patching option.name() = ' +
35
+ option.name() +
36
+ ' into per-command help');
37
+ }
38
+ else {
39
+ return ` ${help.optionTerm(option)} ${help.optionDescription(option)}`;
40
+ }
41
+ })
42
+ .join('\n')
43
+ .trim();
44
+ cmd.addHelpText('after', `\nGlobal options:\n ${lines}`);
45
+ // cmd.addHelpText('after', ` ${lines}`)
46
+ }
47
+ // -------------------------------------------------------------------------
48
+ const program = new Command()
58
49
  .name('yini')
59
50
  .description(descr.yini)
60
51
  // Below will replace all auto-registered items (especially the descriptions starting with a capital and ending with a period).
61
- .version(pkg.version, '-v, --version', 'Output the version number.')
52
+ .version(getPackageVersion(), '-v, --version', 'Output the version number.')
62
53
  .helpOption('-h, --help', 'Display help for command.')
63
54
  .helpCommand('help [command]', 'Display help for command.');
64
- program.addHelpText('before', `YINI CLI (Yet another INI)
65
-
66
- For parsing and validating YINI configuration files.
67
- A config format, inspired by INI, with type-safe values, nested
68
- sections, comments, minimal syntax noise, and optional strict mode.
69
-
70
- Designed for clarity and consistency. :)\n`);
71
- program.addHelpText('after', `
72
- Examples:
73
- $ yini parse config.yini
74
- $ yini validate config.yini --strict
75
- $ yini parse config.yini --pretty --output out.json
76
-
77
- Example of "config.yini":
78
- ^ App
79
- title = 'My App'
80
- items = 10
81
- debug = ON
82
-
83
- ^ Server
84
- host = 'localhost'
85
- port = 8080
86
- useTLS = OFF
87
-
88
- // Sub-section of Server.
89
- ^^ Login
90
- username = 'user'
91
- password = 'secret'
92
-
93
- More info:
94
- https://github.com/YINI-lang/yini-cli
95
-
96
- Into to YINI Config:
97
- https://github.com/YINI-lang/YINI-spec/blob/develop/Docs/Intro-to-YINI-Config-Format.md`);
98
- //program.command('help [command]').description('Display help for command')
55
+ program.addHelpText('before', getHelpTextBefore());
56
+ program.addHelpText('after', getHelpTextAfter());
99
57
  /**
100
- *
101
- * Maybe later, to as default command: parse <parse>
58
+ * The (main/global) option: "--info, --strict, --quite, --silent"
59
+ */
60
+ // Suggestions for future: --verbose, --debug, --no-color, --color, --timing, --stdin
61
+ program
62
+ .option('-i, --info', 'Show extended information (details, links, etc.).')
63
+ .option('-s, --strict', 'Enable strict parsing mode.')
64
+ .option('-f, --force', 'Continue parsing even if errors occur.')
65
+ .option('-q, --quiet', 'Reduce output (show only errors).')
66
+ .option('--silent', 'Suppress all output (even errors, exit code only).')
67
+ .action((options) => {
68
+ debugPrint('Run global options');
69
+ if (isDebug()) {
70
+ console.log('Global options:');
71
+ console.log(toPrettyJSON(options));
72
+ }
73
+ printInfo();
74
+ });
75
+ /**
76
+ * The (main/global) option: "--info"
102
77
  */
103
78
  // program
104
- // .argument('<file>', 'File to parse')
105
- // .option('--strict', 'Parse YINI in strict-mode')
106
- // .option('--pretty', 'Pretty-print output as JSON')
107
- // // .option('--log', 'Use console.log output format (compact, quick view)')
108
- // .option('--json', 'Compact JSON output using JSON.stringify')
109
- // .option('--output <file>', 'Write output to a specified file')
110
- // .action((file, options) => {
111
- // if (file) {
112
- // parseFile(file, options)
113
- // } else {
114
- // program.help()
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))
115
85
  // }
86
+ // printInfo()
116
87
  // })
117
- // Explicit "parse" command
118
- program
88
+ /**
89
+ * The command: "parse <file>"
90
+ */
91
+ const parseCmd = program
119
92
  .command('parse <file>')
120
93
  .description(descr['For-command-parse'])
121
- .option('--strict', 'Parse YINI in strict-mode.')
122
94
  .option('--pretty', 'Pretty-print output as JSON.')
123
- // .option('--log', 'Use console.log output format (compact, quick view)')
124
95
  .option('--json', 'Compact JSON output using JSON.stringify.')
125
96
  .option('--output <file>', 'Write output to a specified file.')
126
97
  .action((file, options) => {
98
+ const globals = program.opts(); // Global options.
99
+ const mergedOptions = { ...globals, ...options }; // Merge global options with per-command options.
127
100
  debugPrint('Run command "parse"');
128
101
  debugPrint('isDebug(): ' + isDebug());
129
102
  debugPrint('isDev() : ' + isDev());
130
103
  debugPrint(`<file> = ${file}`);
131
104
  if (isDebug()) {
132
- console.log('options:');
133
- console.log(toPrettyJSON(options));
105
+ console.log('mergedOptions:');
106
+ console.log(toPrettyJSON(mergedOptions));
134
107
  }
135
108
  if (file) {
136
- parseFile(file, options);
109
+ parseFile(file, mergedOptions);
137
110
  }
138
111
  else {
139
112
  program.help();
140
113
  }
141
114
  });
115
+ appendGlobalOptionsTo(parseCmd);
142
116
  /**
143
- * To handle command validate, e.g.:
144
- * yini validate config.yini
145
- * yini validate config.yini --strict
146
- * yini validate config.yini --details
147
- * yini validate config.yini --silent
148
- *
149
- * If details:
150
- * Details:
151
- * - YINI version: 1.0.0-beta.6
152
- * - Mode: strict
153
- * - Keys: 42
154
- * - Sections: 6
155
- * - Nesting depth: 3
156
- * - Has @yini: true
117
+ * The command: "validate <file>"
157
118
  */
158
- program
119
+ const validateCmd = program
159
120
  .command('validate <file>')
160
121
  .description(descr['For-command-validate'])
161
- .option('--strict', 'Enable parsing in strict-mode')
162
- .option('--details', 'Print detailed meta-data info (e.g., key count, nesting, etc.).')
163
- .option('--silent', 'Suppress output')
122
+ .option('--report', 'Print detailed meta-data info (e.g., key count, nesting, etc.).')
123
+ .option('--details', 'Print detailed validation info (e.g., line locations, error codes, descriptive text, etc.).')
124
+ // .option('--silent', 'Suppress output')
164
125
  .action((file, options) => {
165
- //@todo add debugPrint
126
+ const globals = program.opts(); // Global options.
127
+ const mergedOptions = { ...globals, ...options }; // Merge global options with per-command options.
128
+ debugPrint('Run command "parse"');
129
+ debugPrint('isDebug(): ' + isDebug());
130
+ debugPrint('isDev() : ' + isDev());
131
+ debugPrint(`<file> = ${file}`);
132
+ if (isDebug()) {
133
+ console.log('mergedOptions:');
134
+ console.log(toPrettyJSON(mergedOptions));
135
+ }
166
136
  if (file) {
167
- validateFile(file, options);
137
+ validateFile(file, mergedOptions);
168
138
  }
169
139
  else {
170
140
  program.help();
171
141
  }
172
142
  });
173
- // Command info
143
+ appendGlobalOptionsTo(validateCmd);
144
+ // About to get deleted, moved to main option --info
145
+ //@todo Delete
174
146
  program
175
147
  .command('info')
176
- // .command('')
177
148
  .description(descr['For-command-info'])
178
- // .option('info')
179
149
  .action((options) => {
180
150
  debugPrint('Run command "info"');
181
151
  if (isDebug()) {
182
152
  console.log('options:');
183
153
  console.log(toPrettyJSON(options));
184
154
  }
155
+ console.warn('Deprecated: Use `yini --info` or `yini -i` instead of `yini info`.');
185
156
  printInfo();
186
157
  });
187
158
  // NOTE: Converting YINI files to other formats than json and js.
package/dist/types.d.ts CHANGED
@@ -1,8 +1,6 @@
1
- export interface ICLIParseOptions {
1
+ export interface IGlobalOptions {
2
2
  strict?: boolean;
3
- pretty?: boolean;
4
- log?: boolean;
5
- json?: boolean;
6
- output?: string;
3
+ force?: boolean;
4
+ quiet?: boolean;
5
+ silent?: boolean;
7
6
  }
8
- export type TBailSensitivity = 'auto' | 0 | 1 | 2;
package/dist/types.js CHANGED
@@ -1 +1,5 @@
1
+ // //export type TBailSensitivity = 'auto' | 0 | 1 | 2
2
+ // export type TPreferredFailLevel = 'auto' | 0 | 1 | 2 // Preferred bail sensitivity level.
3
+ // export type TBailSensitivityLevel = 0 | 1 | 2 // Bail sensitivity level.
1
4
  export {};
5
+ // -------------------------------------------------------------------------
@@ -2,14 +2,13 @@ export declare const debugPrint: (str?: any) => void;
2
2
  export declare const devPrint: (str?: any) => void;
3
3
  export declare const toJSON: (obj: any) => string;
4
4
  export declare const toPrettyJSON: (obj: any) => string;
5
- /**
6
- * Pretty-prints a JavaScript object as formatted JSON to the console.
5
+ /** Pretty-prints a JavaScript object as formatted JSON to the console.
7
6
  * Strict JSON, all keys are enclosed in ", etc.
8
7
  */
9
- export declare const printJSON: (obj: any) => void;
8
+ export declare const printJSON: (obj: any, isForce?: boolean) => void;
10
9
  /**
11
10
  * Print a full JavaScript object in a human-readable way (not as JSON).
12
11
  * Not strict JSON, and shows functions, symbols, getters/setters, and class names.
13
12
  * @param isColors If true, the output is styled with ANSI color codes.
14
13
  */
15
- export declare const printObject: (obj: any, isColors?: boolean) => void;
14
+ export declare const printObject: (obj: any, isForce?: boolean, isColors?: boolean) => void;
@@ -18,13 +18,14 @@ export const toPrettyJSON = (obj) => {
18
18
  const str = JSON.stringify(obj, null, 4);
19
19
  return str;
20
20
  };
21
- /**
22
- * Pretty-prints a JavaScript object as formatted JSON to the console.
21
+ /** Pretty-prints a JavaScript object as formatted JSON to the console.
23
22
  * Strict JSON, all keys are enclosed in ", etc.
24
23
  */
25
- export const printJSON = (obj) => {
26
- if (isProdEnv() || (isTestEnv() && !isDebug()))
27
- return;
24
+ export const printJSON = (obj, isForce = false) => {
25
+ if (!isForce) {
26
+ if (isProdEnv() || (isTestEnv() && !isDebug()))
27
+ return;
28
+ }
28
29
  const str = toPrettyJSON(obj);
29
30
  console.log(str);
30
31
  };
@@ -33,8 +34,10 @@ export const printJSON = (obj) => {
33
34
  * Not strict JSON, and shows functions, symbols, getters/setters, and class names.
34
35
  * @param isColors If true, the output is styled with ANSI color codes.
35
36
  */
36
- export const printObject = (obj, isColors = true) => {
37
- if (isProdEnv() || (isTestEnv() && !isDebug()))
38
- return;
37
+ export const printObject = (obj, isForce = false, isColors = true) => {
38
+ if (!isForce) {
39
+ if (isProdEnv() || (isTestEnv() && !isDebug()))
40
+ return;
41
+ }
39
42
  console.log(util.inspect(obj, { depth: null, colors: isColors }));
40
43
  };
@@ -0,0 +1,2 @@
1
+ export declare const getPackageName: () => string;
2
+ export declare const getPackageVersion: () => string;
@@ -0,0 +1,13 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, resolve } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __filename = fileURLToPath(import.meta.url);
5
+ const __dirname = dirname(__filename);
6
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../package.json'), 'utf-8'));
7
+ // import pkg from '../../package.json' with { type: 'json' } // NOTE: Must use { type: 'json' } when in ESM mode.
8
+ export const getPackageName = () => {
9
+ return '' + pkg.name;
10
+ };
11
+ export const getPackageVersion = () => {
12
+ return '' + pkg.version;
13
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yini-cli",
3
- "version": "1.0.2-beta",
3
+ "version": "1.1.0-beta",
4
4
  "description": "CLI for parsing and validating YINI config files: type-safe values, nested sections, comments, minimal syntax noise, and optional strict mode.",
5
5
  "keywords": [
6
6
  "yini",
@@ -46,9 +46,11 @@
46
46
  "start": "node ./bin/yini.js",
47
47
  "start:dev": "cross-env NODE_ENV=development isDev=1 tsx src/index.ts",
48
48
  "start:dev:debug": "cross-env isDebug=1 npm run start:dev",
49
+ "run:help": "npm run start -- --help",
49
50
  "run:version": "npm run start -- --version",
50
- "run:info": "npm run start -- info",
51
+ "run:info": "npm run start -- --info",
51
52
  "run:parse": "npm run start -- parse sample.yini",
53
+ "run:validate": "npm run start -- validate sample.yini",
52
54
  "test:smoke": "vitest tests/smoke",
53
55
  "test:general": "vitest tests/general",
54
56
  "test": "vitest run",
@@ -67,8 +69,8 @@
67
69
  },
68
70
  "author": "Marko K. Seppänen",
69
71
  "dependencies": {
70
- "commander": "^14.0.0",
71
- "yini-parser": "^1.0.2-beta"
72
+ "commander": "^14.0.1",
73
+ "yini-parser": "^1.3.0-beta"
72
74
  },
73
75
  "devDependencies": {
74
76
  "@eslint/js": "^9.31.0",
package/sample.yini CHANGED
@@ -17,3 +17,5 @@ Enabled = true
17
17
 
18
18
  ^ Env // Defines a section named Env.
19
19
  code = "dev"
20
+
21
+ /End // The document terminator '/END' is optional.
@@ -1,2 +0,0 @@
1
- import { ICLIParseOptions } from '../types.js';
2
- export declare const parseFile: (file: string, options: ICLIParseOptions) => void;
@@ -1,7 +0,0 @@
1
- interface IValidateOptions {
2
- strict?: boolean;
3
- details?: boolean;
4
- silent?: boolean;
5
- }
6
- export declare const validateFile: (file: string, options?: IValidateOptions) => never;
7
- export {};
@@ -1,32 +0,0 @@
1
- import fs from 'node:fs';
2
- import { exit } from 'node:process';
3
- import YINI from 'yini-parser';
4
- import { printObject } from '../utils/print.js';
5
- export const validateFile = (file, options = {}) => {
6
- try {
7
- const content = fs.readFileSync(file, 'utf-8');
8
- const isMeta = true;
9
- const parsed = YINI.parse(content, options.strict ?? false, 'auto', isMeta);
10
- if (!options.silent) {
11
- console.log(`✔ File is valid${options.strict ? ' (strict mode)' : ''}.`);
12
- if (options.details) {
13
- //@todo format parsed.meta to details as
14
- /*
15
- * Details:
16
- * - YINI version: 1.0.0-beta.6
17
- * - Mode: strict
18
- * - Keys: 42
19
- * - Sections: 6
20
- * - Nesting depth: 3
21
- * - Has @yini: true
22
- */
23
- printObject(parsed.meta);
24
- }
25
- }
26
- exit(0);
27
- }
28
- catch (err) {
29
- console.error(`✖ Validation failed: ${err.message}`);
30
- exit(1);
31
- }
32
- };