parse-hcl 0.1.0 → 0.1.1

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.
Files changed (3) hide show
  1. package/README.md +35 -0
  2. package/dist/cli.js +102 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -49,6 +49,12 @@ pnpm add parse-hcl
49
49
 
50
50
  The `parse-hcl` CLI provides instant Terraform configuration analysis from your terminal.
51
51
 
52
+ ### Command Synopsis
53
+
54
+ ```bash
55
+ parse-hcl --file <path> | --dir <path> [--format json|yaml] [--graph] [--no-prune]
56
+ ```
57
+
52
58
  ### Basic Commands
53
59
 
54
60
  ```bash
@@ -75,6 +81,9 @@ parse-hcl --file plan.json
75
81
 
76
82
  # Keep empty arrays/objects in output
77
83
  parse-hcl --file main.tf --no-prune
84
+
85
+ # Save to custom path and also print
86
+ parse-hcl --file main.tf --out ./out/result.json --stdout
78
87
  ```
79
88
 
80
89
  ### Options Reference
@@ -86,6 +95,25 @@ parse-hcl --file main.tf --no-prune
86
95
  | `--format <type>` | Output format: `json` or `yaml` | `json` |
87
96
  | `--graph` | Include dependency graph (nodes, edges, references) | `false` |
88
97
  | `--no-prune` | Keep empty arrays and objects in output | `false` |
98
+ | `--out <path>` | Save output to file (or directory for combined output) | `./parse-hcl-output*.{json,yaml}` |
99
+ | `--out-dir <dir>` | Save per-file results under this directory (directory mode) | `./parse-hcl-output/files` |
100
+ | `--split` / `--no-split` | Enable/disable per-file saving in directory mode | `true` |
101
+ | `--stdout` / `--no-stdout` | Also print to stdout (default off) | `false` |
102
+
103
+ ### Behavior and Defaults
104
+
105
+ - Pass either `--file` or `--dir`; if both are present, `--file` is used. Missing inputs print usage to stderr and exit with code `1`.
106
+ - **Default output is files, stdout off.**
107
+ - Single file: writes `./parse-hcl-output.{json|yaml}`.
108
+ - Directory: writes combined `./parse-hcl-output.combined.{json|yaml}` and per-file under `./parse-hcl-output/files/<relative-path>.{json|yaml}`.
109
+ - Add `--stdout` to also print.
110
+ - `--out` overrides the combined/single output path. If it points to a directory, the tool writes `output.{json|yaml}` (single file) or `combined.{json|yaml}` (directory). If no extension is given, one is added based on `--format`.
111
+ - `--out-dir` sets the root for per-file outputs (directory mode). If omitted but `--out` is provided, per-file results go under `per-file/` next to the `--out` target. Disable per-file writes with `--no-split`.
112
+ - `--file` auto-detects artifacts: paths containing `tfvars` use the tfvars parser, `.tfstate` uses the state parser, and `plan.json` uses the plan parser. Other files are treated as Terraform configs. The `--graph` flag only applies to Terraform configs; artifact parsers ignore it and emit the raw parse.
113
+ - `--dir` walks recursively, parsing only `.tf` and `.tf.json` files while skipping `.terraform`, `.git`, and `node_modules`. Default output contains `combined` (aggregated document) and `files` (per-file results). With `--graph`, the dependency graph is built from the aggregated document.
114
+ - Warnings and usage go to stderr. The CLI exits non-zero on invalid arguments or parsing failures.
115
+ - `--format` applies to every output shape; `--no-prune` keeps empty arrays/objects that are removed by default for compactness.
116
+ - Run without a global install via `npx parse-hcl ...` or `yarn dlx parse-hcl ...`.
89
117
 
90
118
  ### Output Formats
91
119
 
@@ -154,6 +182,13 @@ variable:
154
182
  value: us-east-1
155
183
  ```
156
184
 
185
+ **Default saved files (no flags):**
186
+ ```bash
187
+ $ ls parse-hcl-output*
188
+ parse-hcl-output.combined.json
189
+ parse-hcl-output/files/main.tf.json
190
+ ```
191
+
157
192
  **Graph Output:**
158
193
  ```bash
159
194
  $ parse-hcl --file main.tf --graph --format json
package/dist/cli.js CHANGED
@@ -4,12 +4,16 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  return (mod && mod.__esModule) ? mod : { "default": mod };
5
5
  };
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
+ const fs_1 = __importDefault(require("fs"));
7
8
  const path_1 = __importDefault(require("path"));
8
9
  const terraformParser_1 = require("./services/terraformParser");
9
10
  const artifactParsers_1 = require("./services/artifactParsers");
10
11
  const serializer_1 = require("./utils/serialization/serializer");
12
+ const DEFAULT_SINGLE_BASENAME = 'parse-hcl-output';
13
+ const DEFAULT_COMBINED_BASENAME = 'parse-hcl-output.combined';
14
+ const DEFAULT_PER_FILE_DIR = 'parse-hcl-output/files';
11
15
  function parseArgs(argv) {
12
- const opts = { format: 'json', graph: false, prune: true };
16
+ const opts = { format: 'json', graph: false, prune: true, split: true, stdout: false };
13
17
  for (let i = 0; i < argv.length; i += 1) {
14
18
  const arg = argv[i];
15
19
  if (arg === '--file' && argv[i + 1]) {
@@ -30,6 +34,24 @@ function parseArgs(argv) {
30
34
  else if (arg === '--no-prune') {
31
35
  opts.prune = false;
32
36
  }
37
+ else if (arg === '--out' && argv[i + 1]) {
38
+ opts.out = argv[++i];
39
+ }
40
+ else if (arg === '--out-dir' && argv[i + 1]) {
41
+ opts.outDir = argv[++i];
42
+ }
43
+ else if (arg === '--split') {
44
+ opts.split = true;
45
+ }
46
+ else if (arg === '--no-split') {
47
+ opts.split = false;
48
+ }
49
+ else if (arg === '--stdout') {
50
+ opts.stdout = true;
51
+ }
52
+ else if (arg === '--no-stdout') {
53
+ opts.stdout = false;
54
+ }
33
55
  }
34
56
  return opts;
35
57
  }
@@ -37,51 +59,117 @@ function main() {
37
59
  const opts = parseArgs(process.argv.slice(2));
38
60
  const parser = new terraformParser_1.TerraformParser();
39
61
  if (!opts.file && !opts.dir) {
40
- console.error('Usage: parse-hcl --file <path> | --dir <path> [--format json|yaml] [--graph] [--no-prune]');
62
+ console.error('Usage: parse-hcl --file <path> | --dir <path> [--format json|yaml] [--graph] [--no-prune] [--out <path>] [--out-dir <dir>] [--stdout]');
41
63
  process.exit(1);
42
64
  }
43
65
  if (opts.file) {
44
66
  const filePath = path_1.default.resolve(opts.file);
45
67
  const ext = path_1.default.extname(filePath);
46
68
  if (ext.includes('tfvars')) {
47
- emit(tfvarsParse(filePath), opts);
69
+ const data = tfvarsParse(filePath);
70
+ emitSingle(filePath, data, opts);
48
71
  return;
49
72
  }
50
73
  if (ext === '.tfstate') {
51
- emit(new artifactParsers_1.TfStateParser().parseFile(filePath), opts);
74
+ const data = new artifactParsers_1.TfStateParser().parseFile(filePath);
75
+ emitSingle(filePath, data, opts);
52
76
  return;
53
77
  }
54
78
  if (ext === '.json' && filePath.endsWith('plan.json')) {
55
- emit(new artifactParsers_1.TfPlanParser().parseFile(filePath), opts);
79
+ const data = new artifactParsers_1.TfPlanParser().parseFile(filePath);
80
+ emitSingle(filePath, data, opts);
56
81
  return;
57
82
  }
58
83
  const doc = parser.parseFile(filePath);
59
- emit(doc, opts);
84
+ emitSingle(filePath, doc, opts);
60
85
  return;
61
86
  }
62
87
  if (opts.dir) {
63
88
  const dirPath = path_1.default.resolve(opts.dir);
64
89
  const result = parser.parseDirectory(dirPath);
65
- const combined = result.combined ?? parser.combine(result.files.map((f) => f.document));
66
- emit(opts.graph ? (0, serializer_1.toExport)(combined, { pruneEmpty: opts.prune }) : result, opts);
90
+ const combinedDoc = result.combined ?? parser.combine(result.files.map((f) => f.document));
91
+ emitDirectory(dirPath, result.files, combinedDoc, opts);
67
92
  }
68
93
  }
69
94
  function tfvarsParse(filePath) {
70
95
  return new artifactParsers_1.TfVarsParser().parseFile(filePath);
71
96
  }
72
- function emit(data, opts) {
97
+ function emitSingle(filePath, data, opts) {
98
+ const rendered = render(data, opts);
99
+ const ext = getExt(opts.format);
100
+ const defaultName = `${DEFAULT_SINGLE_BASENAME}${ext}`;
101
+ const targetPath = resolveOutPath(opts.out, defaultName, opts.format);
102
+ writeFile(targetPath, rendered);
103
+ if (opts.stdout) {
104
+ console.info(rendered);
105
+ }
106
+ }
107
+ function emitDirectory(dirPath, files, combinedDoc, opts) {
108
+ const ext = getExt(opts.format);
109
+ const combinedData = opts.graph ? combinedDoc : { combined: combinedDoc, files: opts.split ? files : [] };
110
+ const combinedRendered = render(combinedData, opts);
111
+ const combinedDefaultName = `${DEFAULT_COMBINED_BASENAME}${ext}`;
112
+ const combinedTarget = resolveOutPath(opts.out, combinedDefaultName, opts.format, true);
113
+ writeFile(combinedTarget, combinedRendered);
114
+ if (opts.split) {
115
+ const baseDir = resolvePerFileBase(opts);
116
+ files.forEach((file) => {
117
+ const relPath = path_1.default.relative(dirPath, path_1.default.resolve(file.path));
118
+ const perFileTarget = path_1.default.join(baseDir, `${relPath}${ext}`);
119
+ const rendered = render(file.document, opts);
120
+ writeFile(perFileTarget, rendered);
121
+ });
122
+ }
123
+ if (opts.stdout) {
124
+ console.info(combinedRendered);
125
+ }
126
+ }
127
+ function render(data, opts) {
73
128
  if (opts.graph && !isTerraformDoc(data)) {
74
129
  console.warn('Graph export requested but input is not a Terraform document; emitting raw output.');
75
130
  }
76
131
  if (opts.format === 'yaml') {
77
- console.info((0, serializer_1.toYamlDocument)(data, { pruneEmpty: opts.prune }));
78
- return;
132
+ return (0, serializer_1.toYamlDocument)(data, { pruneEmpty: opts.prune });
79
133
  }
80
134
  if (opts.graph && isTerraformDoc(data)) {
81
- console.info((0, serializer_1.toJsonExport)(data, { pruneEmpty: opts.prune }));
82
- return;
135
+ return (0, serializer_1.toJsonExport)(data, { pruneEmpty: opts.prune });
136
+ }
137
+ return (0, serializer_1.toJson)(data, { pruneEmpty: opts.prune });
138
+ }
139
+ function resolveOutPath(out, defaultName, format, isDirMode = false) {
140
+ const resolvedDefault = path_1.default.resolve(defaultName);
141
+ if (!out) {
142
+ return resolvedDefault;
83
143
  }
84
- console.info((0, serializer_1.toJson)(data, { pruneEmpty: opts.prune }));
144
+ const resolvedOut = path_1.default.resolve(out);
145
+ if (fs_1.default.existsSync(resolvedOut) && fs_1.default.statSync(resolvedOut).isDirectory()) {
146
+ const name = isDirMode ? `combined${getExt(format)}` : `output${getExt(format)}`;
147
+ return path_1.default.join(resolvedOut, name);
148
+ }
149
+ if (!path_1.default.extname(resolvedOut)) {
150
+ return `${resolvedOut}${getExt(format)}`;
151
+ }
152
+ return resolvedOut;
153
+ }
154
+ function resolvePerFileBase(opts) {
155
+ if (opts.outDir) {
156
+ return path_1.default.resolve(opts.outDir);
157
+ }
158
+ if (opts.out) {
159
+ const resolvedOut = path_1.default.resolve(opts.out);
160
+ if (fs_1.default.existsSync(resolvedOut) && fs_1.default.statSync(resolvedOut).isDirectory()) {
161
+ return path_1.default.join(resolvedOut, 'per-file');
162
+ }
163
+ return path_1.default.join(path_1.default.dirname(resolvedOut), 'per-file');
164
+ }
165
+ return path_1.default.resolve(DEFAULT_PER_FILE_DIR);
166
+ }
167
+ function writeFile(targetPath, contents) {
168
+ fs_1.default.mkdirSync(path_1.default.dirname(targetPath), { recursive: true });
169
+ fs_1.default.writeFileSync(targetPath, contents, 'utf-8');
170
+ }
171
+ function getExt(format) {
172
+ return format === 'yaml' ? '.yaml' : '.json';
85
173
  }
86
174
  function isTerraformDoc(data) {
87
175
  return Boolean(data && typeof data === 'object' && 'resource' in data);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "parse-hcl",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Lightweight HCL parser focused on identifying and classifying blocks. Supports common Terraform blocks (resource, variable, output, locals, etc.) for tooling and automation.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",