configenvy 0.1.2 → 0.1.4

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/dist/index.d.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  import { writeFile } from 'node:fs/promises';
3
3
  import { resolve } from 'node:path';
4
4
  import { Command } from 'commander';
5
- import { buildMarkdownTable, explainVariable, scanProject, toJson, Diagnostic } from '@configenvy/core';
5
+ import { buildMarkdownTable, explainVariable, scanProject, toJson, toSarif, Diagnostic } from '@configenvy/core';
6
6
 
7
7
  type DoctorOptions = {
8
- format?: "text" | "json";
8
+ format?: "text" | "json" | "sarif";
9
9
  strict?: boolean;
10
10
  ci?: boolean;
11
11
  };
@@ -18,12 +18,15 @@ type CliDependencies = {
18
18
  resolvePath: typeof resolve;
19
19
  scanProject: typeof scanProject;
20
20
  toJson: typeof toJson;
21
+ toSarif: typeof toSarif;
21
22
  writeFile: typeof writeFile;
22
23
  };
23
24
  declare function createProgram(dependencies?: CliDependencies): Command;
24
25
  declare function runCli(argv: string[], dependencies?: CliDependencies): Promise<void>;
25
26
  declare function runDoctor(projectPath: string, options: DoctorOptions, dependencies?: CliDependencies): Promise<void>;
27
+ declare function runInit(projectPath: string, dependencies?: CliDependencies): Promise<void>;
26
28
  declare function resolveOutputPath(projectPath: string, outputPath: string, resolvePath?: typeof resolve): string;
27
29
  declare function printHumanReport(diagnostics: Diagnostic[], log?: (...values: unknown[]) => void): void;
30
+ declare function printGitHubAnnotations(diagnostics: Diagnostic[], log?: (...values: unknown[]) => void): void;
28
31
 
29
- export { type CliDependencies, createProgram, printHumanReport, resolveOutputPath, runCli, runDoctor };
32
+ export { type CliDependencies, createProgram, printGitHubAnnotations, printHumanReport, resolveOutputPath, runCli, runDoctor, runInit };
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { writeFile } from "fs/promises";
5
5
  import { isAbsolute, resolve } from "path";
6
6
  import { pathToFileURL } from "url";
7
7
  import { Command } from "commander";
8
- import { buildMarkdownTable, explainVariable, scanProject, toJson } from "@configenvy/core";
8
+ import { buildMarkdownTable, explainVariable, scanProject, toJson, toSarif } from "@configenvy/core";
9
9
  var defaultDependencies = {
10
10
  buildMarkdownTable,
11
11
  error: console.error,
@@ -15,15 +15,16 @@ var defaultDependencies = {
15
15
  resolvePath: resolve,
16
16
  scanProject,
17
17
  toJson,
18
+ toSarif,
18
19
  writeFile
19
20
  };
20
21
  function createProgram(dependencies = defaultDependencies) {
21
22
  const program = new Command();
22
- program.name("configenvy").description("Find missing, unused, undocumented, and risky environment variables.").version("0.1.2");
23
- program.command("doctor").argument("[path]", "project directory", ".").option("--format <format>", "output format: text or json", "text").option("--strict", "treat documentation warnings as errors").action(async (projectPath, options) => {
23
+ program.name("configenvy").description("Find missing, unused, undocumented, and risky environment variables.").version("0.1.4");
24
+ program.command("doctor").argument("[path]", "project directory", ".").option("--format <format>", "output format: text, json, or sarif", "text").option("--strict", "treat documentation warnings as errors").action(async (projectPath, options) => {
24
25
  await runDoctor(projectPath, options, dependencies);
25
26
  });
26
- program.command("check").argument("[path]", "project directory", ".").option("--ci", "fail on warnings and errors").option("--format <format>", "output format: text or json", "text").action(async (projectPath, options) => {
27
+ program.command("check").argument("[path]", "project directory", ".").option("--ci", "fail on warnings and errors").option("--format <format>", "output format: text, json, or sarif", "text").action(async (projectPath, options) => {
27
28
  await runDoctor(projectPath, { ...options, strict: Boolean(options.ci), ci: Boolean(options.ci) }, dependencies);
28
29
  });
29
30
  program.command("table").argument("[path]", "project directory", ".").option("--out <file>", "write markdown table to a file").action(async (projectPath, options) => {
@@ -37,12 +38,21 @@ function createProgram(dependencies = defaultDependencies) {
37
38
  dependencies.log(table);
38
39
  }
39
40
  });
41
+ program.command("init").argument("[path]", "project directory", ".").description("create a starter configenvy.config.json file").action(async (projectPath) => {
42
+ await runInit(projectPath, dependencies);
43
+ });
40
44
  program.command("explain").argument("<variable>", "environment variable name").argument("[path]", "project directory", ".").action(async (variable, projectPath) => {
41
45
  const result = await dependencies.scanProject({ rootDir: dependencies.resolvePath(projectPath) });
42
46
  dependencies.log(dependencies.explainVariable(result, variable));
43
47
  });
44
48
  return program;
45
49
  }
50
+ var starterConfig = {
51
+ required: [],
52
+ optional: [],
53
+ ignore: ["NODE_ENV"],
54
+ docs: ["README.md", "docs"]
55
+ };
46
56
  async function runCli(argv, dependencies = defaultDependencies) {
47
57
  const program = createProgram(dependencies);
48
58
  await program.parseAsync(argv);
@@ -54,14 +64,36 @@ async function runDoctor(projectPath, options, dependencies = defaultDependencie
54
64
  });
55
65
  if (options.format === "json") {
56
66
  dependencies.log(dependencies.toJson(result));
67
+ } else if (options.format === "sarif") {
68
+ dependencies.log(dependencies.toSarif(result));
57
69
  } else {
58
70
  printHumanReport(result.diagnostics, dependencies.log);
71
+ if (options.ci) {
72
+ printGitHubAnnotations(result.diagnostics, dependencies.log);
73
+ }
59
74
  }
60
75
  const hasError = result.diagnostics.some((diagnostic) => diagnostic.severity === "error");
61
76
  const hasWarning = result.diagnostics.some((diagnostic) => diagnostic.severity === "warning");
62
77
  if (hasError || options.ci && hasWarning) dependencies.exit(2);
63
78
  if (hasWarning) dependencies.exit(1);
64
79
  }
80
+ async function runInit(projectPath, dependencies = defaultDependencies) {
81
+ const rootDir = dependencies.resolvePath(projectPath);
82
+ const configPath = dependencies.resolvePath(rootDir, "configenvy.config.json");
83
+ const content = `${JSON.stringify(starterConfig, null, 2)}
84
+ `;
85
+ try {
86
+ await dependencies.writeFile(configPath, content, { encoding: "utf8", flag: "wx" });
87
+ } catch (error) {
88
+ if (isNodeError(error) && error.code === "EEXIST") {
89
+ dependencies.error("configenvy.config.json already exists.");
90
+ dependencies.exit(1);
91
+ return;
92
+ }
93
+ throw error;
94
+ }
95
+ dependencies.log(`Created ${configPath}`);
96
+ }
65
97
  function resolveOutputPath(projectPath, outputPath, resolvePath = resolve) {
66
98
  if (isAbsolute(outputPath)) return outputPath;
67
99
  return resolvePath(projectPath, outputPath);
@@ -83,6 +115,25 @@ function printHumanReport(diagnostics, log = defaultDependencies.log) {
83
115
  const warnings = diagnostics.length - errors;
84
116
  log(`Summary: ${errors} error(s), ${warnings} warning(s).`);
85
117
  }
118
+ function printGitHubAnnotations(diagnostics, log = defaultDependencies.log) {
119
+ for (const diagnostic of diagnostics) {
120
+ const command = diagnostic.severity === "error" ? "error" : "warning";
121
+ const properties = [
122
+ diagnostic.files[0] ? `file=${escapeAnnotationProperty(diagnostic.files[0])}` : void 0,
123
+ `title=${escapeAnnotationProperty(`${diagnostic.code} ${diagnostic.variable}`)}`
124
+ ].filter(Boolean);
125
+ log(`::${command} ${properties.join(",")}::${escapeAnnotationMessage(diagnostic.message)}`);
126
+ }
127
+ }
128
+ function escapeAnnotationProperty(value) {
129
+ return escapeAnnotationMessage(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
130
+ }
131
+ function escapeAnnotationMessage(value) {
132
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
133
+ }
134
+ function isNodeError(error) {
135
+ return error instanceof Error && "code" in error;
136
+ }
86
137
  var invokedPath = process.argv[1];
87
138
  if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
88
139
  runCli(process.argv).catch((error) => {
@@ -92,8 +143,10 @@ if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
92
143
  }
93
144
  export {
94
145
  createProgram,
146
+ printGitHubAnnotations,
95
147
  printHumanReport,
96
148
  resolveOutputPath,
97
149
  runCli,
98
- runDoctor
150
+ runDoctor,
151
+ runInit
99
152
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "configenvy",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Find missing, unused, undocumented, and risky environment variables before setup breaks.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,7 +35,7 @@
35
35
  "build": "tsup src/index.ts --format esm --dts --clean"
36
36
  },
37
37
  "dependencies": {
38
- "@configenvy/core": "0.1.2",
38
+ "@configenvy/core": "0.1.4",
39
39
  "commander": "^12.1.0"
40
40
  },
41
41
  "keywords": [
package/src/index.ts CHANGED
@@ -3,10 +3,10 @@ import { writeFile } from "node:fs/promises";
3
3
  import { isAbsolute, resolve } from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import { Command } from "commander";
6
- import { buildMarkdownTable, explainVariable, scanProject, toJson, type Diagnostic } from "@configenvy/core";
6
+ import { buildMarkdownTable, explainVariable, scanProject, toJson, toSarif, type Diagnostic } from "@configenvy/core";
7
7
 
8
8
  type DoctorOptions = {
9
- format?: "text" | "json";
9
+ format?: "text" | "json" | "sarif";
10
10
  strict?: boolean;
11
11
  ci?: boolean;
12
12
  };
@@ -20,6 +20,7 @@ export type CliDependencies = {
20
20
  resolvePath: typeof resolve;
21
21
  scanProject: typeof scanProject;
22
22
  toJson: typeof toJson;
23
+ toSarif: typeof toSarif;
23
24
  writeFile: typeof writeFile;
24
25
  };
25
26
 
@@ -32,6 +33,7 @@ const defaultDependencies: CliDependencies = {
32
33
  resolvePath: resolve,
33
34
  scanProject,
34
35
  toJson,
36
+ toSarif,
35
37
  writeFile
36
38
  };
37
39
 
@@ -41,12 +43,12 @@ export function createProgram(dependencies: CliDependencies = defaultDependencie
41
43
  program
42
44
  .name("configenvy")
43
45
  .description("Find missing, unused, undocumented, and risky environment variables.")
44
- .version("0.1.2");
46
+ .version("0.1.4");
45
47
 
46
48
  program
47
49
  .command("doctor")
48
50
  .argument("[path]", "project directory", ".")
49
- .option("--format <format>", "output format: text or json", "text")
51
+ .option("--format <format>", "output format: text, json, or sarif", "text")
50
52
  .option("--strict", "treat documentation warnings as errors")
51
53
  .action(async (projectPath: string, options: DoctorOptions) => {
52
54
  await runDoctor(projectPath, options, dependencies);
@@ -56,7 +58,7 @@ export function createProgram(dependencies: CliDependencies = defaultDependencie
56
58
  .command("check")
57
59
  .argument("[path]", "project directory", ".")
58
60
  .option("--ci", "fail on warnings and errors")
59
- .option("--format <format>", "output format: text or json", "text")
61
+ .option("--format <format>", "output format: text, json, or sarif", "text")
60
62
  .action(async (projectPath: string, options: DoctorOptions) => {
61
63
  await runDoctor(projectPath, { ...options, strict: Boolean(options.ci), ci: Boolean(options.ci) }, dependencies);
62
64
  });
@@ -76,6 +78,14 @@ export function createProgram(dependencies: CliDependencies = defaultDependencie
76
78
  }
77
79
  });
78
80
 
81
+ program
82
+ .command("init")
83
+ .argument("[path]", "project directory", ".")
84
+ .description("create a starter configenvy.config.json file")
85
+ .action(async (projectPath: string) => {
86
+ await runInit(projectPath, dependencies);
87
+ });
88
+
79
89
  program
80
90
  .command("explain")
81
91
  .argument("<variable>", "environment variable name")
@@ -88,6 +98,13 @@ export function createProgram(dependencies: CliDependencies = defaultDependencie
88
98
  return program;
89
99
  }
90
100
 
101
+ const starterConfig = {
102
+ required: [],
103
+ optional: [],
104
+ ignore: ["NODE_ENV"],
105
+ docs: ["README.md", "docs"]
106
+ };
107
+
91
108
  export async function runCli(argv: string[], dependencies: CliDependencies = defaultDependencies): Promise<void> {
92
109
  const program = createProgram(dependencies);
93
110
  await program.parseAsync(argv);
@@ -105,8 +122,13 @@ export async function runDoctor(
105
122
 
106
123
  if (options.format === "json") {
107
124
  dependencies.log(dependencies.toJson(result));
125
+ } else if (options.format === "sarif") {
126
+ dependencies.log(dependencies.toSarif(result));
108
127
  } else {
109
128
  printHumanReport(result.diagnostics, dependencies.log);
129
+ if (options.ci) {
130
+ printGitHubAnnotations(result.diagnostics, dependencies.log);
131
+ }
110
132
  }
111
133
 
112
134
  const hasError = result.diagnostics.some((diagnostic) => diagnostic.severity === "error");
@@ -115,6 +137,28 @@ export async function runDoctor(
115
137
  if (hasWarning) dependencies.exit(1);
116
138
  }
117
139
 
140
+ export async function runInit(
141
+ projectPath: string,
142
+ dependencies: CliDependencies = defaultDependencies
143
+ ): Promise<void> {
144
+ const rootDir = dependencies.resolvePath(projectPath);
145
+ const configPath = dependencies.resolvePath(rootDir, "configenvy.config.json");
146
+ const content = `${JSON.stringify(starterConfig, null, 2)}\n`;
147
+
148
+ try {
149
+ await dependencies.writeFile(configPath, content, { encoding: "utf8", flag: "wx" });
150
+ } catch (error) {
151
+ if (isNodeError(error) && error.code === "EEXIST") {
152
+ dependencies.error("configenvy.config.json already exists.");
153
+ dependencies.exit(1);
154
+ return;
155
+ }
156
+ throw error;
157
+ }
158
+
159
+ dependencies.log(`Created ${configPath}`);
160
+ }
161
+
118
162
  export function resolveOutputPath(
119
163
  projectPath: string,
120
164
  outputPath: string,
@@ -147,6 +191,33 @@ export function printHumanReport(
147
191
  log(`Summary: ${errors} error(s), ${warnings} warning(s).`);
148
192
  }
149
193
 
194
+ export function printGitHubAnnotations(
195
+ diagnostics: Diagnostic[],
196
+ log: (...values: unknown[]) => void = defaultDependencies.log
197
+ ): void {
198
+ for (const diagnostic of diagnostics) {
199
+ const command = diagnostic.severity === "error" ? "error" : "warning";
200
+ const properties = [
201
+ diagnostic.files[0] ? `file=${escapeAnnotationProperty(diagnostic.files[0])}` : undefined,
202
+ `title=${escapeAnnotationProperty(`${diagnostic.code} ${diagnostic.variable}`)}`
203
+ ].filter(Boolean);
204
+
205
+ log(`::${command} ${properties.join(",")}::${escapeAnnotationMessage(diagnostic.message)}`);
206
+ }
207
+ }
208
+
209
+ function escapeAnnotationProperty(value: string): string {
210
+ return escapeAnnotationMessage(value).replace(/:/g, "%3A").replace(/,/g, "%2C");
211
+ }
212
+
213
+ function escapeAnnotationMessage(value: string): string {
214
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
215
+ }
216
+
217
+ function isNodeError(error: unknown): error is NodeJS.ErrnoException {
218
+ return error instanceof Error && "code" in error;
219
+ }
220
+
150
221
  const invokedPath = process.argv[1];
151
222
  if (invokedPath && import.meta.url === pathToFileURL(invokedPath).href) {
152
223
  runCli(process.argv).catch((error: unknown) => {