norn-cli 1.4.0 → 1.4.2

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/out/cli.js CHANGED
@@ -58,18 +58,31 @@ const response_1 = require("./cli/formatters/response");
58
58
  const junit_1 = require("./cli/reporters/junit");
59
59
  const html_1 = require("./cli/reporters/html");
60
60
  const ENV_FILENAME = '.nornenv';
61
+ function getEnvSearchStartDirectory(targetPath) {
62
+ const resolvedPath = path.resolve(targetPath);
63
+ try {
64
+ const stats = fs.statSync(resolvedPath);
65
+ return stats.isDirectory() ? resolvedPath : path.dirname(resolvedPath);
66
+ }
67
+ catch {
68
+ return path.dirname(resolvedPath);
69
+ }
70
+ }
61
71
  /**
62
72
  * Finds .nornenv file relative to a specific file path (for CLI usage)
63
73
  */
64
74
  function findEnvFileFromPath(filePath) {
65
- let dir = path.dirname(filePath);
66
- const root = path.parse(dir).root;
67
- while (dir !== root) {
75
+ let dir = getEnvSearchStartDirectory(filePath);
76
+ while (true) {
68
77
  const envPath = path.join(dir, ENV_FILENAME);
69
78
  if (fs.existsSync(envPath)) {
70
79
  return envPath;
71
80
  }
72
- dir = path.dirname(dir);
81
+ const parentDir = path.dirname(dir);
82
+ if (parentDir === dir) {
83
+ break;
84
+ }
85
+ dir = parentDir;
73
86
  }
74
87
  return undefined;
75
88
  }
@@ -134,6 +147,57 @@ function parseEnvFile(content) {
134
147
  }
135
148
  return config;
136
149
  }
150
+ function resolveEnvironmentForPath(targetPath, selectedEnv) {
151
+ const envFilePath = findEnvFileFromPath(targetPath);
152
+ if (!envFilePath) {
153
+ return {
154
+ variables: {},
155
+ secretNames: new Set(),
156
+ secretValues: new Map(),
157
+ availableEnvironments: []
158
+ };
159
+ }
160
+ const envContent = fs.readFileSync(envFilePath, 'utf-8');
161
+ const envConfig = parseEnvFile(envContent);
162
+ const variables = { ...envConfig.common };
163
+ const secretNames = new Set(envConfig.secretNames);
164
+ const secretValues = new Map(envConfig.secretValues);
165
+ const availableEnvironments = envConfig.environments.map(e => e.name);
166
+ if (selectedEnv) {
167
+ const targetEnv = envConfig.environments.find(e => e.name === selectedEnv);
168
+ if (!targetEnv) {
169
+ return {
170
+ envFilePath,
171
+ variables,
172
+ secretNames,
173
+ secretValues,
174
+ availableEnvironments,
175
+ envNotFound: selectedEnv
176
+ };
177
+ }
178
+ Object.assign(variables, targetEnv.variables);
179
+ for (const [name, value] of Object.entries(targetEnv.variables)) {
180
+ if (secretNames.has(name)) {
181
+ secretValues.set(name, value);
182
+ }
183
+ }
184
+ }
185
+ return {
186
+ envFilePath,
187
+ variables,
188
+ secretNames,
189
+ secretValues,
190
+ availableEnvironments
191
+ };
192
+ }
193
+ function mergeSecrets(targetNames, targetValues, sourceNames, sourceValues) {
194
+ for (const name of sourceNames) {
195
+ targetNames.add(name);
196
+ }
197
+ for (const [name, value] of sourceValues) {
198
+ targetValues.set(name, value);
199
+ }
200
+ }
137
201
  /**
138
202
  * Generate a timestamp string for report filenames
139
203
  * Format: YYYY-MM-DD-HHmmss
@@ -576,36 +640,8 @@ async function main() {
576
640
  const allErrors = [];
577
641
  let overallSuccess = true;
578
642
  const startTime = Date.now();
579
- // Find env file from the input path (directory or file)
580
- const envFilePath = findEnvFileFromPath(inputPath);
581
- let envVariables = {};
582
- let redaction = (0, redaction_1.createRedactionOptions)(new Set(), new Map(), !options.noRedact);
583
- if (envFilePath) {
584
- const envContent = fs.readFileSync(envFilePath, 'utf-8');
585
- const envConfig = parseEnvFile(envContent);
586
- envVariables = { ...envConfig.common };
587
- redaction = (0, redaction_1.createRedactionOptions)(envConfig.secretNames, envConfig.secretValues, !options.noRedact);
588
- if (options.env) {
589
- const targetEnv = envConfig.environments.find(e => e.name === options.env);
590
- if (!targetEnv) {
591
- console.error(`Error: Environment '${options.env}' not found in .nornenv`);
592
- console.error(`Available environments: ${envConfig.environments.map(e => e.name).join(', ') || 'none'}`);
593
- process.exit(1);
594
- }
595
- Object.assign(envVariables, targetEnv.variables);
596
- for (const [name, value] of Object.entries(targetEnv.variables)) {
597
- if (envConfig.secretNames.has(name)) {
598
- redaction.secretValues.set(name, value);
599
- }
600
- }
601
- if (options.verbose) {
602
- console.log(colors.info(`Using environment: ${options.env}`));
603
- }
604
- }
605
- }
606
- else if (options.env) {
607
- console.error(colors.warning(`Warning: --env specified but no .nornenv file found`));
608
- }
643
+ const combinedSecretNames = new Set();
644
+ const combinedSecretValues = new Map();
609
645
  let tagFilterOptions = undefined;
610
646
  if (options.tagFilters.length > 0) {
611
647
  tagFilterOptions = {
@@ -627,9 +663,23 @@ async function main() {
627
663
  // Single file mode with specific sequence/request
628
664
  if (options.sequence || options.request) {
629
665
  const filePath = filesToRun[0];
666
+ const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
667
+ if (resolvedEnv.envNotFound) {
668
+ console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
669
+ console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(', ') || 'none'}`);
670
+ process.exit(1);
671
+ }
672
+ if (!resolvedEnv.envFilePath && options.env) {
673
+ console.error(colors.warning(`Warning: --env specified but no .nornenv file found`));
674
+ }
675
+ else if (resolvedEnv.envFilePath && options.env && options.verbose) {
676
+ console.log(colors.info(`Using environment: ${options.env}`));
677
+ }
678
+ mergeSecrets(combinedSecretNames, combinedSecretValues, resolvedEnv.secretNames, resolvedEnv.secretValues);
679
+ const redaction = (0, redaction_1.createRedactionOptions)(combinedSecretNames, combinedSecretValues, !options.noRedact);
630
680
  const fileContent = fs.readFileSync(filePath, 'utf-8');
631
681
  const fileVariables = (0, parser_1.extractFileLevelVariables)(fileContent);
632
- const variables = { ...envVariables, ...fileVariables };
682
+ const variables = { ...resolvedEnv.variables, ...fileVariables };
633
683
  const cookieJar = (0, httpClient_1.createCookieJar)();
634
684
  const workingDir = path.dirname(filePath);
635
685
  const importResult = await (0, parser_1.resolveImports)(fileContent, workingDir, async (importPath) => fsPromises.readFile(importPath, 'utf-8'));
@@ -748,9 +798,21 @@ async function main() {
748
798
  }
749
799
  // Run tests from each file
750
800
  for (const filePath of filesToRun) {
801
+ const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
802
+ if (resolvedEnv.envNotFound) {
803
+ console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
804
+ console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(', ') || 'none'}`);
805
+ process.exit(1);
806
+ }
807
+ if (!resolvedEnv.envFilePath && options.env) {
808
+ const relPath = isDirectory ? path.relative(inputPath, filePath) : path.basename(filePath);
809
+ console.error(colors.warning(`Warning: --env specified but no .nornenv file found for ${relPath}`));
810
+ }
811
+ mergeSecrets(combinedSecretNames, combinedSecretValues, resolvedEnv.secretNames, resolvedEnv.secretValues);
812
+ const redaction = (0, redaction_1.createRedactionOptions)(combinedSecretNames, combinedSecretValues, !options.noRedact);
751
813
  const fileContent = fs.readFileSync(filePath, 'utf-8');
752
814
  const fileVariables = (0, parser_1.extractFileLevelVariables)(fileContent);
753
- const variables = { ...envVariables, ...fileVariables };
815
+ const variables = { ...resolvedEnv.variables, ...fileVariables };
754
816
  const cookieJar = (0, httpClient_1.createCookieJar)();
755
817
  const workingDir = path.dirname(filePath);
756
818
  const importResult = await (0, parser_1.resolveImports)(fileContent, workingDir, async (importPath) => fsPromises.readFile(importPath, 'utf-8'));
@@ -817,6 +879,7 @@ async function main() {
817
879
  }
818
880
  }
819
881
  const totalDuration = Date.now() - startTime;
882
+ const redaction = (0, redaction_1.createRedactionOptions)(combinedSecretNames, combinedSecretValues, !options.noRedact);
820
883
  // Build result object for reporting
821
884
  const result = {
822
885
  success: overallSuccess,
@@ -38,16 +38,13 @@ exports.updateCodeLensCoverage = updateCodeLensCoverage;
38
38
  const vscode = __importStar(require("vscode"));
39
39
  const environmentProvider_1 = require("./environmentProvider");
40
40
  // Cached coverage percentage for CodeLens display
41
- let cachedCoveragePercentage = null;
42
- let cachedCoverageTotal = 0;
43
- let cachedCoverageCovered = 0;
44
41
  /**
45
42
  * Update the cached coverage for CodeLens display
46
43
  */
47
44
  function updateCodeLensCoverage(percentage, total, covered) {
48
- cachedCoveragePercentage = percentage;
49
- cachedCoverageTotal = total;
50
- cachedCoverageCovered = covered;
45
+ void percentage;
46
+ void total;
47
+ void covered;
51
48
  }
52
49
  /**
53
50
  * Checks if a sequence declaration has only optional parameters (all have defaults).
@@ -115,13 +112,13 @@ class HttpCodeLensProvider {
115
112
  * Gets the environment display text for CodeLens
116
113
  * Returns 'add' if no env file exists, environment name if it does
117
114
  */
118
- getEnvDisplay() {
119
- const envFile = (0, environmentProvider_1.findEnvFile)();
115
+ getEnvDisplay(documentPath) {
116
+ const envFile = (0, environmentProvider_1.findEnvFileFromPath)(documentPath);
120
117
  if (!envFile) {
121
118
  return { text: '+ Add env file', hasEnvFile: false };
122
119
  }
123
120
  const activeEnv = (0, environmentProvider_1.getActiveEnvironment)();
124
- const availableEnvs = (0, environmentProvider_1.getAvailableEnvironments)();
121
+ const availableEnvs = (0, environmentProvider_1.getAvailableEnvironments)(envFile);
125
122
  if (availableEnvs.length === 0) {
126
123
  return { text: 'env: (empty)', hasEnvFile: true };
127
124
  }
@@ -137,7 +134,7 @@ class HttpCodeLensProvider {
137
134
  // Match matchesSchema assertions: assert $1.body matchesSchema "./schema.json"
138
135
  // Note: .+? is non-greedy to not consume matchesSchema
139
136
  const matchesSchemaRegex = /^\s*assert\s+.+?\s+matchesSchema\s+["']?([^"']+)["']?/i;
140
- const envInfo = this.getEnvDisplay();
137
+ const envInfo = this.getEnvDisplay(document.uri.fsPath);
141
138
  const isNornApiFile = document.languageId === 'nornapi';
142
139
  const isNornFile = document.languageId === 'norn';
143
140
  // Track if we're inside a sequence
@@ -163,16 +160,13 @@ class HttpCodeLensProvider {
163
160
  arguments: [swaggerMatch[1], document.uri.fsPath],
164
161
  tooltip: 'Generate JSON Schema files from OpenAPI response definitions'
165
162
  }));
166
- // Add coverage CodeLens if we have coverage data
167
- if (cachedCoveragePercentage !== null) {
168
- const coverageIcon = cachedCoveragePercentage >= 80 ? '$(pass)' :
169
- cachedCoveragePercentage >= 50 ? '$(warning)' : '$(error)';
170
- codeLenses.push(new vscode.CodeLens(range, {
171
- title: `${coverageIcon} Coverage: ${cachedCoveragePercentage}% (${cachedCoverageCovered}/${cachedCoverageTotal})`,
172
- command: 'norn.showCoverage',
173
- tooltip: `API Coverage: ${cachedCoverageCovered} of ${cachedCoverageTotal} response codes tested. Click to view details.`
174
- }));
175
- }
163
+ // Add file-scoped coverage CodeLens
164
+ codeLenses.push(new vscode.CodeLens(range, {
165
+ title: '$(graph) Show Coverage',
166
+ command: 'norn.showCoverage',
167
+ arguments: [document.uri.fsPath],
168
+ tooltip: 'Show API coverage for this .nornapi file (includes this folder and subfolders only).'
169
+ }));
176
170
  continue;
177
171
  }
178
172
  }