norn-cli 1.3.16 → 1.3.18

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/AGENTS.md ADDED
@@ -0,0 +1,72 @@
1
+ # Norn Extension - Copilot Instructions
2
+
3
+ These instructions apply to all conversations about the Norn VS Code extension.
4
+
5
+ ## CLI Support is Mandatory
6
+
7
+ Every feature implementation must work in both:
8
+ 1. **VS Code Extension** - Interactive UI with response panel
9
+ 2. **CLI** (`src/cli.ts`) - Command-line execution
10
+
11
+ Before considering any feature complete, verify it works in the CLI. The CLI shares code with the extension (`parser.ts`, `sequenceRunner.ts`, `assertionRunner.ts`, `httpClient.ts`).
12
+
13
+ ### CRITICAL: Running the Local CLI
14
+
15
+ **NEVER use `npx norn`** - this runs the globally installed npm package, NOT your local changes!
16
+
17
+ **ALWAYS use the local compiled CLI:**
18
+ ```bash
19
+ # Correct - runs your local development version
20
+ node ./dist/cli.js run tests/file.norn --env prelive
21
+
22
+ # WRONG - runs the published npm package, ignores your changes
23
+ npx norn run tests/file.norn
24
+ ```
25
+
26
+ Before testing CLI changes:
27
+ 1. Run `npm run compile` to build the latest code
28
+ 2. Use `node ./dist/cli.js` to execute
29
+
30
+ ## Skills Maintenance
31
+
32
+ When implementing features, check the `.github/skills/` directory for relevant skills:
33
+
34
+ - **If a skill is incorrect or outdated:** Update it with the correct information
35
+ - **If a skill is missing:** Create a new one following the Agent Skills format
36
+
37
+ Skills should capture lessons learned and patterns discovered during implementation to help future development.
38
+
39
+ ## Code Style
40
+
41
+ - TypeScript with strict typing
42
+ - Simple IntelliSense completions (no complex snippets)
43
+ - Keep parsing logic in dedicated functions
44
+ - Test both extension and CLI after changes
45
+
46
+ ## Test Verification
47
+
48
+ **MANDATORY:** When the user asks to publish, prepare a release, bump version, or create a patch:
49
+ 1. **FIRST** invoke the Test Verification subagent using `runSubagent`
50
+ 2. Wait for it to complete and report results
51
+ 3. Only proceed if all tests pass
52
+
53
+ The Test Verification agent runs:
54
+ - `npm run compile` (must have 0 errors)
55
+ - `node ./dist/cli.js ./tests/Regression/ -e prelive` (all tests must pass)
56
+
57
+ **Trigger words:** publish, release, version, patch, bump, deploy
58
+
59
+ ## Key Files
60
+
61
+ | Purpose | File |
62
+ |---------|------|
63
+ | Syntax highlighting | `syntaxes/norn.tmLanguage.json` |
64
+ | IntelliSense | `src/completionProvider.ts` |
65
+ | Sequence execution | `src/sequenceRunner.ts` |
66
+ | Assertions | `src/assertionRunner.ts` |
67
+ | HTTP requests | `src/httpClient.ts` |
68
+ | CLI | `src/cli.ts` |
69
+ | Parser | `src/parser.ts` |
70
+ | Response panel | `src/responsePanel.ts` |
71
+ | Diagnostics | `src/diagnosticProvider.ts` |
72
+ | Test Explorer | `src/testProvider.ts` |
package/CHANGELOG.md CHANGED
@@ -2,6 +2,39 @@
2
2
 
3
3
  All notable changes to the "Norn" extension will be documented in this file.
4
4
 
5
+ ## [1.3.18] - 2026-02-15
6
+
7
+ ### Improved
8
+ - **Environment Resolution (Extension + CLI)**: `.nornenv` resolution is now file-aware and uses the closest ancestor file.
9
+ - VS Code execution paths now resolve env variables from the current document path.
10
+ - CLI directory runs now resolve environments per test file path (including nested folders).
11
+ - CodeLens and environment status now reflect the active editor/file context.
12
+
13
+ ### Fixed
14
+ - **IntelliSense in Request Bodies**:
15
+ - Header completions are suppressed when starting JSON body lines (`{` or `[`).
16
+ - Header suggestions remain available for non-body lines, including raw URL requests (e.g., `POST "https://..."`).
17
+ - **Syntax Highlighting**:
18
+ - Fixed header coloring after endpoint calls where header-group token matching could break on `Content-Type`.
19
+ - Improved assert string highlighting with variable interpolation inside quoted strings.
20
+
21
+ ### Added
22
+ - **Regression Coverage**:
23
+ - Added env-resolution regression tests for parent vs nested `.nornenv` precedence.
24
+
25
+ ## [1.3.17] - 2026-02-14
26
+
27
+ ### Fixed
28
+ - **Import Resolution**: Improved duplicate import handling while preserving circular import detection
29
+ - Re-importing already loaded files is now treated as a no-op instead of a false circular error
30
+ - True cycles are still detected via active import stack tracking
31
+ - **Sequence Var-Request Parsing**: Fixed multi-line `var x = METHOD ...` request collection
32
+ - Headers and JSON/form bodies are now preserved until the next real command boundary
33
+ - Body-only shorthand continues to parse correctly
34
+ - **Syntax Highlighting**: Improved `var` request URL/header/body highlighting in `.norn` files
35
+ - Better distinction between endpoint captures and raw URL captures
36
+ - Header groups and body lines now color consistently in URL-style var requests
37
+
5
38
  ## [1.3.16] - 2026-02-13
6
39
 
7
40
  ### Fixed
@@ -843,4 +876,4 @@ end sequence
843
876
  - **IntelliSense**: Autocomplete for methods, headers, variables, and keywords
844
877
  - **Diagnostics**: Error highlighting for undefined variables
845
878
  - **CLI Tool**: Run requests from terminal with JSON output for CI/CD pipelines
846
- - **Response Panel**: Webview with formatted JSON and execution results
879
+ - **Response Panel**: Webview with formatted JSON and execution results
package/README.md CHANGED
@@ -340,7 +340,7 @@ end sequence
340
340
  - Circular imports are detected and reported as errors
341
341
  - Only named requests `[Name]` and sequences are imported
342
342
  - Variables are resolved at import time (baked in), not exported
343
- - `.nornenv` environment variables are shared across all files
343
+ - `.nornenv` resolution is file-aware: Norn walks up from the running file and uses the closest `.nornenv`
344
344
 
345
345
  ### API Definition Files (.nornapi)
346
346
 
@@ -506,7 +506,8 @@ end sequence
506
506
 
507
507
  ### Environments
508
508
 
509
- Create a `.nornenv` file in your workspace root to manage environment-specific variables:
509
+ Create a `.nornenv` file to manage environment-specific variables.
510
+ Norn resolves the closest `.nornenv` by walking up from the file being run (or edited):
510
511
 
511
512
  ```bash
512
513
  # .nornenv file
@@ -1017,6 +1018,7 @@ end sequence
1017
1018
  | `var name = value` | Common variable (all environments) |
1018
1019
  | `[env:name]` | Start environment section |
1019
1020
  | `# comment` | Comment line |
1021
+ | resolution | Closest ancestor `.nornenv` from the current `.norn`/`.nornapi` file |
1020
1022
 
1021
1023
  ### API Definitions (.nornapi)
1022
1024
 
package/dist/cli.js CHANGED
@@ -10386,7 +10386,7 @@ var require_ms = __commonJS({
10386
10386
  options = options || {};
10387
10387
  var type = typeof val;
10388
10388
  if (type === "string" && val.length > 0) {
10389
- return parse3(val);
10389
+ return parse2(val);
10390
10390
  } else if (type === "number" && isFinite(val)) {
10391
10391
  return options.long ? fmtLong(val) : fmtShort(val);
10392
10392
  }
@@ -10394,7 +10394,7 @@ var require_ms = __commonJS({
10394
10394
  "val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
10395
10395
  );
10396
10396
  };
10397
- function parse3(str) {
10397
+ function parse2(str) {
10398
10398
  str = String(str);
10399
10399
  if (str.length > 100) {
10400
10400
  return;
@@ -12106,7 +12106,7 @@ var require_cjs = __commonJS({
12106
12106
  out.publicSuffix = (_a = hostnameParts[hostnameParts.length - 1]) !== null && _a !== void 0 ? _a : null;
12107
12107
  }
12108
12108
  var RESULT = getEmptyResult();
12109
- function parse3(url2, options = {}) {
12109
+ function parse2(url2, options = {}) {
12110
12110
  return parseImpl(url2, 5, suffixLookup, options, getEmptyResult());
12111
12111
  }
12112
12112
  function getHostname(url2, options = {}) {
@@ -12134,7 +12134,7 @@ var require_cjs = __commonJS({
12134
12134
  exports2.getHostname = getHostname;
12135
12135
  exports2.getPublicSuffix = getPublicSuffix2;
12136
12136
  exports2.getSubdomain = getSubdomain;
12137
- exports2.parse = parse3;
12137
+ exports2.parse = parse2;
12138
12138
  }
12139
12139
  });
12140
12140
 
@@ -15684,24 +15684,24 @@ var require_fast_uri = __commonJS({
15684
15684
  function normalize(uri, options) {
15685
15685
  if (typeof uri === "string") {
15686
15686
  uri = /** @type {T} */
15687
- serialize(parse3(uri, options), options);
15687
+ serialize(parse2(uri, options), options);
15688
15688
  } else if (typeof uri === "object") {
15689
15689
  uri = /** @type {T} */
15690
- parse3(serialize(uri, options), options);
15690
+ parse2(serialize(uri, options), options);
15691
15691
  }
15692
15692
  return uri;
15693
15693
  }
15694
15694
  function resolve5(baseURI, relativeURI, options) {
15695
15695
  const schemelessOptions = options ? Object.assign({ scheme: "null" }, options) : { scheme: "null" };
15696
- const resolved = resolveComponent(parse3(baseURI, schemelessOptions), parse3(relativeURI, schemelessOptions), schemelessOptions, true);
15696
+ const resolved = resolveComponent(parse2(baseURI, schemelessOptions), parse2(relativeURI, schemelessOptions), schemelessOptions, true);
15697
15697
  schemelessOptions.skipEscape = true;
15698
15698
  return serialize(resolved, schemelessOptions);
15699
15699
  }
15700
15700
  function resolveComponent(base, relative2, options, skipNormalization) {
15701
15701
  const target = {};
15702
15702
  if (!skipNormalization) {
15703
- base = parse3(serialize(base, options), options);
15704
- relative2 = parse3(serialize(relative2, options), options);
15703
+ base = parse2(serialize(base, options), options);
15704
+ relative2 = parse2(serialize(relative2, options), options);
15705
15705
  }
15706
15706
  options = options || {};
15707
15707
  if (!options.tolerant && relative2.scheme) {
@@ -15753,13 +15753,13 @@ var require_fast_uri = __commonJS({
15753
15753
  function equal(uriA, uriB, options) {
15754
15754
  if (typeof uriA === "string") {
15755
15755
  uriA = unescape(uriA);
15756
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
15756
+ uriA = serialize(normalizeComponentEncoding(parse2(uriA, options), true), { ...options, skipEscape: true });
15757
15757
  } else if (typeof uriA === "object") {
15758
15758
  uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
15759
15759
  }
15760
15760
  if (typeof uriB === "string") {
15761
15761
  uriB = unescape(uriB);
15762
- uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
15762
+ uriB = serialize(normalizeComponentEncoding(parse2(uriB, options), true), { ...options, skipEscape: true });
15763
15763
  } else if (typeof uriB === "object") {
15764
15764
  uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
15765
15765
  }
@@ -15828,7 +15828,7 @@ var require_fast_uri = __commonJS({
15828
15828
  return uriTokens.join("");
15829
15829
  }
15830
15830
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
15831
- function parse3(uri, opts) {
15831
+ function parse2(uri, opts) {
15832
15832
  const options = Object.assign({}, opts);
15833
15833
  const parsed = {
15834
15834
  scheme: void 0,
@@ -15922,7 +15922,7 @@ var require_fast_uri = __commonJS({
15922
15922
  resolveComponent,
15923
15923
  equal,
15924
15924
  serialize,
15925
- parse: parse3
15925
+ parse: parse2
15926
15926
  };
15927
15927
  module2.exports = fastUri;
15928
15928
  module2.exports.default = fastUri;
@@ -20273,7 +20273,7 @@ function extractImports(text) {
20273
20273
  }
20274
20274
  return imports;
20275
20275
  }
20276
- async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__PURE__ */ new Set()) {
20276
+ async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__PURE__ */ new Set(), importStack = /* @__PURE__ */ new Set()) {
20277
20277
  const imports = extractImports(text);
20278
20278
  const errors = [];
20279
20279
  const importedContents = [];
@@ -20287,7 +20287,7 @@ async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__
20287
20287
  for (const imp of imports) {
20288
20288
  const path5 = await import("path");
20289
20289
  const absolutePath = path5.resolve(baseDir, imp.path);
20290
- if (alreadyImported.has(absolutePath)) {
20290
+ if (importStack.has(absolutePath)) {
20291
20291
  errors.push({
20292
20292
  path: imp.path,
20293
20293
  error: `Circular import detected`,
@@ -20295,9 +20295,13 @@ async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__
20295
20295
  });
20296
20296
  continue;
20297
20297
  }
20298
+ if (alreadyImported.has(absolutePath)) {
20299
+ continue;
20300
+ }
20298
20301
  try {
20299
20302
  const content = await readFile2(absolutePath);
20300
20303
  alreadyImported.add(absolutePath);
20304
+ importStack.add(absolutePath);
20301
20305
  resolvedPaths.push(absolutePath);
20302
20306
  if (imp.path.endsWith(".nornapi")) {
20303
20307
  const apiDef = parseNornApiFile(content);
@@ -20330,7 +20334,7 @@ async function resolveImports(text, baseDir, readFile2, alreadyImported = /* @__
20330
20334
  continue;
20331
20335
  }
20332
20336
  const importDir = path5.dirname(absolutePath);
20333
- const nestedResult = await resolveImports(content, importDir, readFile2, alreadyImported);
20337
+ const nestedResult = await resolveImports(content, importDir, readFile2, alreadyImported, importStack);
20334
20338
  errors.push(...nestedResult.errors);
20335
20339
  resolvedPaths.push(...nestedResult.resolvedPaths);
20336
20340
  for (const group of nestedResult.headerGroups) {
@@ -20404,6 +20408,8 @@ end sequence`);
20404
20408
  error: error.message || "Failed to read file",
20405
20409
  lineNumber: imp.lineNumber
20406
20410
  });
20411
+ } finally {
20412
+ importStack.delete(absolutePath);
20407
20413
  }
20408
20414
  }
20409
20415
  return {
@@ -27570,6 +27576,7 @@ function extractStepsFromSequence(content) {
27570
27576
  const lines = content.split("\n");
27571
27577
  const steps = [];
27572
27578
  const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
27579
+ const stepBoundaryRegex = /^(###|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+|(test\s+)?sequence\s+|end\s+sequence|end\s+if|endif\b|var\s+|run\s+|print\s+|assert\s+|if\s+|wait\s+|import\s+|return\s+|\[)/i;
27573
27580
  let currentRequest = [];
27574
27581
  let currentRequestStartLine = -1;
27575
27582
  let inRequest = false;
@@ -27819,11 +27826,11 @@ function extractStepsFromSequence(content) {
27819
27826
  currentRequestStartLine = lineIdx;
27820
27827
  inRequest = true;
27821
27828
  } else if (inVarRequest) {
27822
- if (trimmed === "" || trimmed.match(/^[a-zA-Z_][a-zA-Z0-9_]*\s*[=:]\s*.+$/) || trimmed.startsWith("{") || trimmed.startsWith('"') || trimmed.startsWith("[")) {
27823
- currentVarRequest.push(trimmed);
27824
- } else {
27829
+ if (trimmed !== "" && stepBoundaryRegex.test(trimmed)) {
27825
27830
  savePendingVarRequest();
27826
27831
  lineIdx--;
27832
+ } else {
27833
+ currentVarRequest.push(trimmed);
27827
27834
  }
27828
27835
  } else if (inRequest) {
27829
27836
  if (trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*\$\d+/)) {
@@ -28323,9 +28330,15 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
28323
28330
  }
28324
28331
  const contentLines = step.content.split("\n");
28325
28332
  if (contentLines.length > 1) {
28326
- const bodyLines = contentLines.slice(1);
28327
- const resolvedBodyLines = bodyLines.map((line2) => substituteVariables(line2, runtimeVariables));
28328
- requestText += "\n\n" + resolvedBodyLines.join("\n");
28333
+ const extraLines = contentLines.slice(1).map((line2) => substituteVariables(line2, runtimeVariables));
28334
+ const hasExplicitBlank = extraLines.some((line2) => line2.trim() === "");
28335
+ const firstNonEmpty = extraLines.find((line2) => line2.trim() !== "");
28336
+ const startsWithHeader = firstNonEmpty ? /^[A-Za-z0-9\-_]+\s*:/.test(firstNonEmpty.trim()) : false;
28337
+ if (hasExplicitBlank || startsWithHeader) {
28338
+ requestText += "\n" + extraLines.join("\n");
28339
+ } else {
28340
+ requestText += "\n\n" + extraLines.join("\n");
28341
+ }
28329
28342
  }
28330
28343
  const requestParsed = parserHttpRequest(requestText, runtimeVariables);
28331
28344
  const retryOpts = parsed.retryCount ? {
@@ -30256,15 +30269,27 @@ function generateHtmlReportFromResponse(response, testName, options) {
30256
30269
 
30257
30270
  // src/cli.ts
30258
30271
  var ENV_FILENAME = ".nornenv";
30272
+ function getEnvSearchStartDirectory(targetPath) {
30273
+ const resolvedPath = path4.resolve(targetPath);
30274
+ try {
30275
+ const stats = fs5.statSync(resolvedPath);
30276
+ return stats.isDirectory() ? resolvedPath : path4.dirname(resolvedPath);
30277
+ } catch {
30278
+ return path4.dirname(resolvedPath);
30279
+ }
30280
+ }
30259
30281
  function findEnvFileFromPath(filePath) {
30260
- let dir = path4.dirname(filePath);
30261
- const root = path4.parse(dir).root;
30262
- while (dir !== root) {
30282
+ let dir = getEnvSearchStartDirectory(filePath);
30283
+ while (true) {
30263
30284
  const envPath = path4.join(dir, ENV_FILENAME);
30264
30285
  if (fs5.existsSync(envPath)) {
30265
30286
  return envPath;
30266
30287
  }
30267
- dir = path4.dirname(dir);
30288
+ const parentDir = path4.dirname(dir);
30289
+ if (parentDir === dir) {
30290
+ break;
30291
+ }
30292
+ dir = parentDir;
30268
30293
  }
30269
30294
  return void 0;
30270
30295
  }
@@ -30320,6 +30345,57 @@ function parseEnvFile(content) {
30320
30345
  }
30321
30346
  return config;
30322
30347
  }
30348
+ function resolveEnvironmentForPath(targetPath, selectedEnv) {
30349
+ const envFilePath = findEnvFileFromPath(targetPath);
30350
+ if (!envFilePath) {
30351
+ return {
30352
+ variables: {},
30353
+ secretNames: /* @__PURE__ */ new Set(),
30354
+ secretValues: /* @__PURE__ */ new Map(),
30355
+ availableEnvironments: []
30356
+ };
30357
+ }
30358
+ const envContent = fs5.readFileSync(envFilePath, "utf-8");
30359
+ const envConfig = parseEnvFile(envContent);
30360
+ const variables = { ...envConfig.common };
30361
+ const secretNames = new Set(envConfig.secretNames);
30362
+ const secretValues = new Map(envConfig.secretValues);
30363
+ const availableEnvironments = envConfig.environments.map((e) => e.name);
30364
+ if (selectedEnv) {
30365
+ const targetEnv = envConfig.environments.find((e) => e.name === selectedEnv);
30366
+ if (!targetEnv) {
30367
+ return {
30368
+ envFilePath,
30369
+ variables,
30370
+ secretNames,
30371
+ secretValues,
30372
+ availableEnvironments,
30373
+ envNotFound: selectedEnv
30374
+ };
30375
+ }
30376
+ Object.assign(variables, targetEnv.variables);
30377
+ for (const [name, value] of Object.entries(targetEnv.variables)) {
30378
+ if (secretNames.has(name)) {
30379
+ secretValues.set(name, value);
30380
+ }
30381
+ }
30382
+ }
30383
+ return {
30384
+ envFilePath,
30385
+ variables,
30386
+ secretNames,
30387
+ secretValues,
30388
+ availableEnvironments
30389
+ };
30390
+ }
30391
+ function mergeSecrets(targetNames, targetValues, sourceNames, sourceValues) {
30392
+ for (const name of sourceNames) {
30393
+ targetNames.add(name);
30394
+ }
30395
+ for (const [name, value] of sourceValues) {
30396
+ targetValues.set(name, value);
30397
+ }
30398
+ }
30323
30399
  function generateTimestamp() {
30324
30400
  const now = /* @__PURE__ */ new Date();
30325
30401
  const year = now.getFullYear();
@@ -30726,38 +30802,8 @@ async function main() {
30726
30802
  const allErrors = [];
30727
30803
  let overallSuccess = true;
30728
30804
  const startTime = Date.now();
30729
- const envFilePath = findEnvFileFromPath(inputPath);
30730
- let envVariables = {};
30731
- let redaction = createRedactionOptions(/* @__PURE__ */ new Set(), /* @__PURE__ */ new Map(), !options.noRedact);
30732
- if (envFilePath) {
30733
- const envContent = fs5.readFileSync(envFilePath, "utf-8");
30734
- const envConfig = parseEnvFile(envContent);
30735
- envVariables = { ...envConfig.common };
30736
- redaction = createRedactionOptions(
30737
- envConfig.secretNames,
30738
- envConfig.secretValues,
30739
- !options.noRedact
30740
- );
30741
- if (options.env) {
30742
- const targetEnv = envConfig.environments.find((e) => e.name === options.env);
30743
- if (!targetEnv) {
30744
- console.error(`Error: Environment '${options.env}' not found in .nornenv`);
30745
- console.error(`Available environments: ${envConfig.environments.map((e) => e.name).join(", ") || "none"}`);
30746
- process.exit(1);
30747
- }
30748
- Object.assign(envVariables, targetEnv.variables);
30749
- for (const [name, value] of Object.entries(targetEnv.variables)) {
30750
- if (envConfig.secretNames.has(name)) {
30751
- redaction.secretValues.set(name, value);
30752
- }
30753
- }
30754
- if (options.verbose) {
30755
- console.log(colors.info(`Using environment: ${options.env}`));
30756
- }
30757
- }
30758
- } else if (options.env) {
30759
- console.error(colors.warning(`Warning: --env specified but no .nornenv file found`));
30760
- }
30805
+ const combinedSecretNames = /* @__PURE__ */ new Set();
30806
+ const combinedSecretValues = /* @__PURE__ */ new Map();
30761
30807
  let tagFilterOptions = void 0;
30762
30808
  if (options.tagFilters.length > 0) {
30763
30809
  tagFilterOptions = {
@@ -30776,9 +30822,22 @@ async function main() {
30776
30822
  }
30777
30823
  if (options.sequence || options.request) {
30778
30824
  const filePath = filesToRun[0];
30825
+ const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
30826
+ if (resolvedEnv.envNotFound) {
30827
+ console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
30828
+ console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(", ") || "none"}`);
30829
+ process.exit(1);
30830
+ }
30831
+ if (!resolvedEnv.envFilePath && options.env) {
30832
+ console.error(colors.warning(`Warning: --env specified but no .nornenv file found`));
30833
+ } else if (resolvedEnv.envFilePath && options.env && options.verbose) {
30834
+ console.log(colors.info(`Using environment: ${options.env}`));
30835
+ }
30836
+ mergeSecrets(combinedSecretNames, combinedSecretValues, resolvedEnv.secretNames, resolvedEnv.secretValues);
30837
+ const redaction2 = createRedactionOptions(combinedSecretNames, combinedSecretValues, !options.noRedact);
30779
30838
  const fileContent = fs5.readFileSync(filePath, "utf-8");
30780
30839
  const fileVariables = extractFileLevelVariables(fileContent);
30781
- const variables = { ...envVariables, ...fileVariables };
30840
+ const variables = { ...resolvedEnv.variables, ...fileVariables };
30782
30841
  const cookieJar = createCookieJar();
30783
30842
  const workingDir = path4.dirname(filePath);
30784
30843
  const importResult = await resolveImports(
@@ -30822,7 +30881,7 @@ ${fileContent}` : fileContent;
30822
30881
  console.log(JSON.stringify({ success: response.status >= 200 && response.status < 400, results: [response] }, null, 2));
30823
30882
  } else {
30824
30883
  const isSuccess = response.status >= 200 && response.status < 300;
30825
- const lines = formatResponse(response, { colors, verbose: options.verbose, showDetails: !isSuccess || options.verbose, redaction });
30884
+ const lines = formatResponse(response, { colors, verbose: options.verbose, showDetails: !isSuccess || options.verbose, redaction: redaction2 });
30826
30885
  for (const line2 of lines) {
30827
30886
  console.log(line2);
30828
30887
  }
@@ -30865,7 +30924,7 @@ ${fileContent}` : fileContent;
30865
30924
  if (options.output === "json") {
30866
30925
  console.log(JSON.stringify({ success: seqResult.success, results: [seqResult] }, null, 2));
30867
30926
  } else {
30868
- const lines = formatSequenceResult(seqResult, { colors, verbose: options.verbose, redaction });
30927
+ const lines = formatSequenceResult(seqResult, { colors, verbose: options.verbose, redaction: redaction2 });
30869
30928
  for (const line2 of lines) {
30870
30929
  console.log(line2);
30871
30930
  }
@@ -30899,9 +30958,21 @@ ${fileContent}` : fileContent;
30899
30958
  console.log("");
30900
30959
  }
30901
30960
  for (const filePath of filesToRun) {
30961
+ const resolvedEnv = resolveEnvironmentForPath(filePath, options.env);
30962
+ if (resolvedEnv.envNotFound) {
30963
+ console.error(`Error: Environment '${resolvedEnv.envNotFound}' not found in .nornenv`);
30964
+ console.error(`Available environments: ${resolvedEnv.availableEnvironments.join(", ") || "none"}`);
30965
+ process.exit(1);
30966
+ }
30967
+ if (!resolvedEnv.envFilePath && options.env) {
30968
+ const relPath = isDirectory ? path4.relative(inputPath, filePath) : path4.basename(filePath);
30969
+ console.error(colors.warning(`Warning: --env specified but no .nornenv file found for ${relPath}`));
30970
+ }
30971
+ mergeSecrets(combinedSecretNames, combinedSecretValues, resolvedEnv.secretNames, resolvedEnv.secretValues);
30972
+ const redaction2 = createRedactionOptions(combinedSecretNames, combinedSecretValues, !options.noRedact);
30902
30973
  const fileContent = fs5.readFileSync(filePath, "utf-8");
30903
30974
  const fileVariables = extractFileLevelVariables(fileContent);
30904
- const variables = { ...envVariables, ...fileVariables };
30975
+ const variables = { ...resolvedEnv.variables, ...fileVariables };
30905
30976
  const cookieJar = createCookieJar();
30906
30977
  const workingDir = path4.dirname(filePath);
30907
30978
  const importResult = await resolveImports(
@@ -30951,7 +31022,7 @@ ${fileContent}` : fileContent;
30951
31022
  }
30952
31023
  if (options.output !== "json") {
30953
31024
  for (const seqResult of seqResults) {
30954
- const lines = formatSequenceResult(seqResult, { colors, verbose: options.verbose, redaction });
31025
+ const lines = formatSequenceResult(seqResult, { colors, verbose: options.verbose, redaction: redaction2 });
30955
31026
  for (const line2 of lines) {
30956
31027
  console.log(line2);
30957
31028
  }
@@ -30959,6 +31030,7 @@ ${fileContent}` : fileContent;
30959
31030
  }
30960
31031
  }
30961
31032
  const totalDuration = Date.now() - startTime;
31033
+ const redaction = createRedactionOptions(combinedSecretNames, combinedSecretValues, !options.noRedact);
30962
31034
  const result = {
30963
31035
  success: overallSuccess,
30964
31036
  type: "all-sequences",