norn-cli 1.6.1 → 1.7.0

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 (40) hide show
  1. package/AGENTS.md +9 -1
  2. package/CHANGELOG.md +30 -0
  3. package/dist/cli.js +296 -96
  4. package/package.json +1 -1
  5. package/out/assertionRunner.js +0 -537
  6. package/out/chatParticipant.js +0 -722
  7. package/out/cli/colors.js +0 -129
  8. package/out/cli/formatters/assertion.js +0 -75
  9. package/out/cli/formatters/index.js +0 -23
  10. package/out/cli/formatters/response.js +0 -106
  11. package/out/cli/formatters/summary.js +0 -187
  12. package/out/cli/redaction.js +0 -237
  13. package/out/cli/reporters/html.js +0 -634
  14. package/out/cli/reporters/index.js +0 -22
  15. package/out/cli/reporters/junit.js +0 -211
  16. package/out/cli.js +0 -989
  17. package/out/codeLensProvider.js +0 -248
  18. package/out/compareContentProvider.js +0 -85
  19. package/out/completionProvider.js +0 -2404
  20. package/out/contractDecorationProvider.js +0 -243
  21. package/out/coverageCalculator.js +0 -837
  22. package/out/coveragePanel.js +0 -545
  23. package/out/diagnosticProvider.js +0 -1113
  24. package/out/environmentProvider.js +0 -442
  25. package/out/extension.js +0 -1114
  26. package/out/httpClient.js +0 -269
  27. package/out/jsonFileReader.js +0 -320
  28. package/out/nornPrompt.js +0 -580
  29. package/out/nornapiParser.js +0 -326
  30. package/out/parser.js +0 -725
  31. package/out/responsePanel.js +0 -4674
  32. package/out/schemaGenerator.js +0 -393
  33. package/out/scriptRunner.js +0 -419
  34. package/out/sequenceRunner.js +0 -3046
  35. package/out/swaggerBodyIntellisenseCache.js +0 -147
  36. package/out/swaggerParser.js +0 -419
  37. package/out/test/coverageCalculator.test.js +0 -100
  38. package/out/test/extension.test.js +0 -48
  39. package/out/testProvider.js +0 -658
  40. package/out/validationCache.js +0 -245
package/dist/cli.js CHANGED
@@ -11599,8 +11599,8 @@ var require_follow_redirects = __commonJS({
11599
11599
  }
11600
11600
  return parsed;
11601
11601
  }
11602
- function resolveUrl(relative4, base) {
11603
- return useNativeURL ? new URL2(relative4, base) : parseUrl(url2.resolve(base, relative4));
11602
+ function resolveUrl(relative5, base) {
11603
+ return useNativeURL ? new URL2(relative5, base) : parseUrl(url2.resolve(base, relative5));
11604
11604
  }
11605
11605
  function validateUrl(input2) {
11606
11606
  if (/^\[/.test(input2.hostname) && !/^\[[:0-9a-f]+\]$/i.test(input2.hostname)) {
@@ -15697,49 +15697,49 @@ var require_fast_uri = __commonJS({
15697
15697
  schemelessOptions.skipEscape = true;
15698
15698
  return serialize(resolved, schemelessOptions);
15699
15699
  }
15700
- function resolveComponent(base, relative4, options, skipNormalization) {
15700
+ function resolveComponent(base, relative5, options, skipNormalization) {
15701
15701
  const target = {};
15702
15702
  if (!skipNormalization) {
15703
15703
  base = parse2(serialize(base, options), options);
15704
- relative4 = parse2(serialize(relative4, options), options);
15704
+ relative5 = parse2(serialize(relative5, options), options);
15705
15705
  }
15706
15706
  options = options || {};
15707
- if (!options.tolerant && relative4.scheme) {
15708
- target.scheme = relative4.scheme;
15709
- target.userinfo = relative4.userinfo;
15710
- target.host = relative4.host;
15711
- target.port = relative4.port;
15712
- target.path = removeDotSegments(relative4.path || "");
15713
- target.query = relative4.query;
15707
+ if (!options.tolerant && relative5.scheme) {
15708
+ target.scheme = relative5.scheme;
15709
+ target.userinfo = relative5.userinfo;
15710
+ target.host = relative5.host;
15711
+ target.port = relative5.port;
15712
+ target.path = removeDotSegments(relative5.path || "");
15713
+ target.query = relative5.query;
15714
15714
  } else {
15715
- if (relative4.userinfo !== void 0 || relative4.host !== void 0 || relative4.port !== void 0) {
15716
- target.userinfo = relative4.userinfo;
15717
- target.host = relative4.host;
15718
- target.port = relative4.port;
15719
- target.path = removeDotSegments(relative4.path || "");
15720
- target.query = relative4.query;
15715
+ if (relative5.userinfo !== void 0 || relative5.host !== void 0 || relative5.port !== void 0) {
15716
+ target.userinfo = relative5.userinfo;
15717
+ target.host = relative5.host;
15718
+ target.port = relative5.port;
15719
+ target.path = removeDotSegments(relative5.path || "");
15720
+ target.query = relative5.query;
15721
15721
  } else {
15722
- if (!relative4.path) {
15722
+ if (!relative5.path) {
15723
15723
  target.path = base.path;
15724
- if (relative4.query !== void 0) {
15725
- target.query = relative4.query;
15724
+ if (relative5.query !== void 0) {
15725
+ target.query = relative5.query;
15726
15726
  } else {
15727
15727
  target.query = base.query;
15728
15728
  }
15729
15729
  } else {
15730
- if (relative4.path[0] === "/") {
15731
- target.path = removeDotSegments(relative4.path);
15730
+ if (relative5.path[0] === "/") {
15731
+ target.path = removeDotSegments(relative5.path);
15732
15732
  } else {
15733
15733
  if ((base.userinfo !== void 0 || base.host !== void 0 || base.port !== void 0) && !base.path) {
15734
- target.path = "/" + relative4.path;
15734
+ target.path = "/" + relative5.path;
15735
15735
  } else if (!base.path) {
15736
- target.path = relative4.path;
15736
+ target.path = relative5.path;
15737
15737
  } else {
15738
- target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative4.path;
15738
+ target.path = base.path.slice(0, base.path.lastIndexOf("/") + 1) + relative5.path;
15739
15739
  }
15740
15740
  target.path = removeDotSegments(target.path);
15741
15741
  }
15742
- target.query = relative4.query;
15742
+ target.query = relative5.query;
15743
15743
  }
15744
15744
  target.userinfo = base.userinfo;
15745
15745
  target.host = base.host;
@@ -15747,7 +15747,7 @@ var require_fast_uri = __commonJS({
15747
15747
  }
15748
15748
  target.scheme = base.scheme;
15749
15749
  }
15750
- target.fragment = relative4.fragment;
15750
+ target.fragment = relative5.fragment;
15751
15751
  return target;
15752
15752
  }
15753
15753
  function equal(uriA, uriB, options) {
@@ -18783,6 +18783,39 @@ var init_schemaGenerator = __esm({
18783
18783
  }
18784
18784
  });
18785
18785
 
18786
+ // src/quotedString.ts
18787
+ function decodeQuotedStringLiteral(literal) {
18788
+ if (literal.length < 2) {
18789
+ return literal;
18790
+ }
18791
+ const quoteChar = literal[0];
18792
+ if (quoteChar !== '"' && quoteChar !== "'" || literal[literal.length - 1] !== quoteChar) {
18793
+ return literal;
18794
+ }
18795
+ const inner = literal.slice(1, -1);
18796
+ let decoded = "";
18797
+ for (let i = 0; i < inner.length; i++) {
18798
+ const char = inner[i];
18799
+ if (char !== "\\" || i === inner.length - 1) {
18800
+ decoded += char;
18801
+ continue;
18802
+ }
18803
+ const nextChar = inner[i + 1];
18804
+ if (nextChar === "\\" || nextChar === quoteChar) {
18805
+ decoded += nextChar;
18806
+ i++;
18807
+ continue;
18808
+ }
18809
+ decoded += char;
18810
+ }
18811
+ return decoded;
18812
+ }
18813
+ var init_quotedString = __esm({
18814
+ "src/quotedString.ts"() {
18815
+ "use strict";
18816
+ }
18817
+ });
18818
+
18786
18819
  // src/assertionRunner.ts
18787
18820
  var assertionRunner_exports = {};
18788
18821
  __export(assertionRunner_exports, {
@@ -18806,7 +18839,7 @@ function parseAssertCommand(line2) {
18806
18839
  if (pipeIndex !== -1) {
18807
18840
  message = content.substring(pipeIndex + 1).trim();
18808
18841
  if (message.startsWith('"') && message.endsWith('"') || message.startsWith("'") && message.endsWith("'")) {
18809
- message = message.slice(1, -1);
18842
+ message = decodeQuotedStringLiteral(message);
18810
18843
  }
18811
18844
  content = content.substring(0, pipeIndex).trim();
18812
18845
  }
@@ -18848,8 +18881,17 @@ function parseAssertCommand(line2) {
18848
18881
  function findUnquotedPipe(str) {
18849
18882
  let inQuote = false;
18850
18883
  let quoteChar = "";
18884
+ let escapeNext = false;
18851
18885
  for (let i = 0; i < str.length; i++) {
18852
18886
  const char = str[i];
18887
+ if (escapeNext) {
18888
+ escapeNext = false;
18889
+ continue;
18890
+ }
18891
+ if (inQuote && char === "\\") {
18892
+ escapeNext = true;
18893
+ continue;
18894
+ }
18853
18895
  if ((char === '"' || char === "'") && !inQuote) {
18854
18896
  inQuote = true;
18855
18897
  quoteChar = char;
@@ -18934,11 +18976,28 @@ function resolveValue(expr, responses, variables, getValueByPath2, responseIndex
18934
18976
  }
18935
18977
  }
18936
18978
  }
18937
- const varMatch = trimmed.match(/^\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}$/);
18979
+ const varMatch = trimmed.match(/^\{\{(\$env|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)\}\}$/);
18938
18980
  if (varMatch) {
18939
18981
  const varName = varMatch[1];
18982
+ const pathPart = varMatch[2] || "";
18940
18983
  if (varName in variables) {
18941
18984
  const varValue = variables[varName];
18985
+ if (pathPart) {
18986
+ if (typeof varValue === "object" && varValue !== null) {
18987
+ const path10 = pathPart.replace(/^\./, "");
18988
+ return { value: getNestedValue2(varValue, path10) };
18989
+ }
18990
+ if (typeof varValue === "string") {
18991
+ try {
18992
+ const parsed = JSON.parse(varValue);
18993
+ const path10 = pathPart.replace(/^\./, "");
18994
+ return { value: getNestedValue2(parsed, path10) };
18995
+ } catch {
18996
+ return { value: void 0, error: `Cannot access path on non-object variable: ${varName}` };
18997
+ }
18998
+ }
18999
+ return { value: void 0, error: `Cannot access path on non-object variable: ${varName}` };
19000
+ }
18942
19001
  if (typeof varValue === "string" && /^-?\d+(\.\d+)?$/.test(varValue)) {
18943
19002
  return { value: parseFloat(varValue) };
18944
19003
  }
@@ -18947,7 +19006,7 @@ function resolveValue(expr, responses, variables, getValueByPath2, responseIndex
18947
19006
  return { value: void 0, error: `Variable {{${varName}}} is not defined` };
18948
19007
  }
18949
19008
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
18950
- return { value: trimmed.slice(1, -1) };
19009
+ return { value: decodeQuotedStringLiteral(trimmed) };
18951
19010
  }
18952
19011
  if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
18953
19012
  return { value: parseFloat(trimmed) };
@@ -19227,6 +19286,7 @@ var init_assertionRunner = __esm({
19227
19286
  "src/assertionRunner.ts"() {
19228
19287
  "use strict";
19229
19288
  init_schemaGenerator();
19289
+ init_quotedString();
19230
19290
  }
19231
19291
  });
19232
19292
 
@@ -19850,9 +19910,7 @@ function getEndpoint(definition, name) {
19850
19910
  function resolveHeaderValues(headerGroup, variables) {
19851
19911
  const resolved = {};
19852
19912
  for (const [name, value] of Object.entries(headerGroup.headers)) {
19853
- resolved[name] = value.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
19854
- return variables[varName] ?? `{{${varName}}}`;
19855
- });
19913
+ resolved[name] = substituteVariables(value, variables);
19856
19914
  }
19857
19915
  return resolved;
19858
19916
  }
@@ -20149,9 +20207,29 @@ function valueToString(value) {
20149
20207
  }
20150
20208
  return String(value);
20151
20209
  }
20210
+ var ENV_SCOPE_KEY = "$env";
20211
+ function attachEnvironmentScope(variables, envVariables) {
20212
+ if (!envVariables) {
20213
+ return variables;
20214
+ }
20215
+ Object.defineProperty(variables, ENV_SCOPE_KEY, {
20216
+ value: envVariables,
20217
+ enumerable: false,
20218
+ configurable: true,
20219
+ writable: false
20220
+ });
20221
+ return variables;
20222
+ }
20223
+ function copyEnvironmentScope(target, source) {
20224
+ const envScope = source[ENV_SCOPE_KEY];
20225
+ if (envScope && typeof envScope === "object") {
20226
+ attachEnvironmentScope(target, envScope);
20227
+ }
20228
+ return target;
20229
+ }
20152
20230
  function substituteVariables(text, variables) {
20153
20231
  return text.replace(
20154
- /\{\{(\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g,
20232
+ /\{\{(\$env|\$\d+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g,
20155
20233
  (match, varName, pathPart) => {
20156
20234
  if (!(varName in variables)) {
20157
20235
  return match;
@@ -20161,8 +20239,14 @@ function substituteVariables(text, variables) {
20161
20239
  if (pathPart) {
20162
20240
  const path10 = pathPart.replace(/^\./, "");
20163
20241
  const nestedValue = getNestedValue(value, path10);
20242
+ if (nestedValue === void 0 && varName === ENV_SCOPE_KEY) {
20243
+ return match;
20244
+ }
20164
20245
  return valueToString(nestedValue);
20165
20246
  }
20247
+ if (varName === ENV_SCOPE_KEY) {
20248
+ return match;
20249
+ }
20166
20250
  return valueToString(value);
20167
20251
  }
20168
20252
  if (pathPart && typeof value === "string") {
@@ -20170,8 +20254,14 @@ function substituteVariables(text, variables) {
20170
20254
  const parsed = JSON.parse(value);
20171
20255
  const path10 = pathPart.replace(/^\./, "");
20172
20256
  const nestedValue = getNestedValue(parsed, path10);
20257
+ if (nestedValue === void 0 && varName === ENV_SCOPE_KEY) {
20258
+ return match;
20259
+ }
20173
20260
  return valueToString(nestedValue);
20174
20261
  } catch {
20262
+ if (varName === ENV_SCOPE_KEY) {
20263
+ return match;
20264
+ }
20175
20265
  return value;
20176
20266
  }
20177
20267
  }
@@ -26940,7 +27030,7 @@ function formatUserFacingError(error, context) {
26940
27030
  }
26941
27031
 
26942
27032
  // src/requestValidation.ts
26943
- var PLACEHOLDER_REGEX = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g;
27033
+ var PLACEHOLDER_REGEX = /\{\{(\$env|\$[0-9]+|[a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_-]*|\[\d+\])*)\}\}/g;
26944
27034
  function collectUnresolvedPlaceholders(text) {
26945
27035
  if (!text) {
26946
27036
  return [];
@@ -26949,15 +27039,23 @@ function collectUnresolvedPlaceholders(text) {
26949
27039
  let match;
26950
27040
  PLACEHOLDER_REGEX.lastIndex = 0;
26951
27041
  while ((match = PLACEHOLDER_REGEX.exec(text)) !== null) {
26952
- names.add(match[1]);
27042
+ names.add(match[0]);
26953
27043
  }
26954
27044
  return [...names];
26955
27045
  }
27046
+ function extractEnvScopedNames(placeholders) {
27047
+ return placeholders.filter((value) => value.startsWith("{{$env.")).map((value) => value.slice("{{$env.".length, -2).split(/[.[\]]/)[0]).filter(Boolean);
27048
+ }
26956
27049
  function buildMissingVariableHint(missingVars, context) {
26957
27050
  const env3 = context?.environment;
27051
+ const envScopedNames = extractEnvScopedNames(missingVars);
27052
+ const unscopedVars = missingVars.filter((value) => !value.startsWith("{{$env."));
26958
27053
  if (env3?.hasEnvFile && env3.availableEnvironments && env3.availableEnvironments.length > 0 && !env3.activeEnvironment) {
26959
27054
  return `A .nornenv file was found but no environment is selected. Select one of: ${env3.availableEnvironments.join(", ")}.`;
26960
27055
  }
27056
+ if (env3?.hasEnvFile && env3.activeEnvironment && envScopedNames.length > 0 && unscopedVars.length === 0) {
27057
+ return `Check that ${envScopedNames.map((v) => `'${v}'`).join(", ")} exist in the active environment '${env3.activeEnvironment}'.`;
27058
+ }
26961
27059
  if (env3?.hasEnvFile && env3.activeEnvironment) {
26962
27060
  return `Check that ${missingVars.map((v) => `'${v}'`).join(", ")} exist in the active environment '${env3.activeEnvironment}' or as file-level variables.`;
26963
27061
  }
@@ -26975,7 +27073,7 @@ function describeUnresolvedLocation(label, vars) {
26975
27073
  if (vars.length === 0) {
26976
27074
  return void 0;
26977
27075
  }
26978
- return `${label}: unresolved ${vars.length === 1 ? "variable" : "variables"} ${vars.map((v) => `{{${v}}}`).join(", ")}`;
27076
+ return `${label}: unresolved ${vars.length === 1 ? "variable" : "variables"} ${vars.join(", ")}`;
26979
27077
  }
26980
27078
  function shouldValidateBodyPlaceholders(parsed) {
26981
27079
  const method = String(parsed.method || "").toUpperCase();
@@ -26995,7 +27093,7 @@ function validatePreparedRequest(parsed, context) {
26995
27093
  throw new NornError({
26996
27094
  category: "variable-resolution",
26997
27095
  code: "unresolved-request-variables",
26998
- message: `Request could not be prepared: unresolved ${missingVars.length === 1 ? "variable" : "variables"} ${missingVars.map((v) => `{{${v}}}`).join(", ")}.`,
27096
+ message: `Request could not be prepared: unresolved ${missingVars.length === 1 ? "variable" : "variables"} ${missingVars.join(", ")}.`,
26999
27097
  details,
27000
27098
  hint: buildMissingVariableHint(missingVars, context),
27001
27099
  context: {
@@ -27029,6 +27127,7 @@ function validatePreparedRequest(parsed, context) {
27029
27127
  }
27030
27128
 
27031
27129
  // src/sequenceRunner.ts
27130
+ init_quotedString();
27032
27131
  init_assertionRunner();
27033
27132
  function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables) {
27034
27133
  const lines = requestText.split("\n");
@@ -27191,7 +27290,7 @@ function parseRunArguments(argsStr) {
27191
27290
  if (namedMatch) {
27192
27291
  let value = namedMatch[2].trim();
27193
27292
  if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
27194
- value = value.slice(1, -1);
27293
+ value = decodeQuotedStringLiteral(value);
27195
27294
  }
27196
27295
  args.push({
27197
27296
  name: namedMatch[1],
@@ -27200,7 +27299,7 @@ function parseRunArguments(argsStr) {
27200
27299
  } else {
27201
27300
  let value = trimmed;
27202
27301
  if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
27203
- value = value.slice(1, -1);
27302
+ value = decodeQuotedStringLiteral(value);
27204
27303
  }
27205
27304
  args.push({ value });
27206
27305
  }
@@ -27371,7 +27470,7 @@ function parseSequenceParameters(line2) {
27371
27470
  hasSeenDefault = true;
27372
27471
  let defaultValue = defaultMatch[2].trim();
27373
27472
  if (defaultValue.startsWith('"') && defaultValue.endsWith('"') || defaultValue.startsWith("'") && defaultValue.endsWith("'")) {
27374
- defaultValue = defaultValue.slice(1, -1);
27473
+ defaultValue = decodeQuotedStringLiteral(defaultValue);
27375
27474
  }
27376
27475
  params.push({
27377
27476
  name: defaultMatch[1],
@@ -27498,7 +27597,7 @@ function parseTypedValue(value) {
27498
27597
  return parseFloat(value);
27499
27598
  }
27500
27599
  if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
27501
- return value.slice(1, -1);
27600
+ return decodeQuotedStringLiteral(value);
27502
27601
  }
27503
27602
  return value;
27504
27603
  }
@@ -27624,7 +27723,7 @@ function parseVarAssignCommand(line2) {
27624
27723
  function evaluateValueExpression(expr, runtimeVariables) {
27625
27724
  const trimmed = expr.trim();
27626
27725
  if (trimmed.startsWith('"') && trimmed.endsWith('"') || trimmed.startsWith("'") && trimmed.endsWith("'")) {
27627
- const inner = trimmed.slice(1, -1);
27726
+ const inner = decodeQuotedStringLiteral(trimmed);
27628
27727
  const substituted = substituteVariables(inner, runtimeVariables);
27629
27728
  return { value: substituted };
27630
27729
  }
@@ -27749,7 +27848,7 @@ function resolveBareVariables(text, variables) {
27749
27848
  const resolvedParts = parts.map((part) => {
27750
27849
  const partTrimmed = part.trim();
27751
27850
  if (partTrimmed.startsWith('"') && partTrimmed.endsWith('"') || partTrimmed.startsWith("'") && partTrimmed.endsWith("'")) {
27752
- return partTrimmed.slice(1, -1);
27851
+ return decodeQuotedStringLiteral(partTrimmed);
27753
27852
  }
27754
27853
  const varMatch = partTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
27755
27854
  if (varMatch) {
@@ -27775,8 +27874,19 @@ function splitExpressionParts(expr) {
27775
27874
  let current = "";
27776
27875
  let inString = false;
27777
27876
  let stringChar = "";
27877
+ let escapeNext = false;
27778
27878
  for (let i = 0; i < expr.length; i++) {
27779
27879
  const char = expr[i];
27880
+ if (escapeNext) {
27881
+ current += char;
27882
+ escapeNext = false;
27883
+ continue;
27884
+ }
27885
+ if (inString && char === "\\") {
27886
+ current += char;
27887
+ escapeNext = true;
27888
+ continue;
27889
+ }
27780
27890
  if (!inString && (char === '"' || char === "'")) {
27781
27891
  inString = true;
27782
27892
  stringChar = char;
@@ -28232,7 +28342,7 @@ async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, wor
28232
28342
  });
28233
28343
  }
28234
28344
  };
28235
- const runtimeVariables = { ...fileVariables, ...sequenceArgs };
28345
+ const runtimeVariables = copyEnvironmentScope({ ...fileVariables, ...sequenceArgs }, fileVariables);
28236
28346
  const buildRequestValidationContext = (stepNumber, method, url2, requestName) => ({
28237
28347
  source: "sequence",
28238
28348
  filePath: executionContext?.filePath,
@@ -28994,19 +29104,13 @@ ${indentMultiline(userMessage)}`;
28994
29104
  apiRequest.endpointName
28995
29105
  );
28996
29106
  if (endpoint) {
28997
- let resolvedPath = endpoint.path;
28998
- resolvedPath = resolvedPath.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
28999
- return runtimeVariables[varName] !== void 0 ? String(runtimeVariables[varName]) : `{{${varName}}}`;
29000
- });
29107
+ let resolvedPath = substituteVariables(endpoint.path, runtimeVariables);
29001
29108
  for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
29002
29109
  let resolvedValue = paramValue;
29003
29110
  if (runtimeVariables[paramValue] !== void 0) {
29004
29111
  resolvedValue = String(runtimeVariables[paramValue]);
29005
- } else if (paramValue.startsWith("{{") && paramValue.endsWith("}}")) {
29006
- const varName = paramValue.slice(2, -2);
29007
- if (runtimeVariables[varName] !== void 0) {
29008
- resolvedValue = String(runtimeVariables[varName]);
29009
- }
29112
+ } else {
29113
+ resolvedValue = substituteVariables(paramValue, runtimeVariables);
29010
29114
  }
29011
29115
  resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
29012
29116
  }
@@ -29019,10 +29123,7 @@ ${indentMultiline(userMessage)}`;
29019
29123
  }
29020
29124
  }
29021
29125
  for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
29022
- let resolved = headerValue;
29023
- resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
29024
- return runtimeVariables[varName] !== void 0 ? String(runtimeVariables[varName]) : `{{${varName}}}`;
29025
- });
29126
+ const resolved = substituteVariables(headerValue, runtimeVariables);
29026
29127
  combinedHeaders[headerName] = resolved;
29027
29128
  }
29028
29129
  const bodyParsed = parserHttpRequest(namedRequest.content, runtimeVariables);
@@ -30775,6 +30876,31 @@ var path4 = __toESM(require("path"));
30775
30876
  var NORN_CACHE_DIR = ".norn-cache";
30776
30877
  var CACHE_GITIGNORE_FILE = ".gitignore";
30777
30878
  var CACHE_GITIGNORE_CONTENT = "*\n";
30879
+ var PROJECT_ROOT_MARKERS = [".git", "package.json", "pnpm-workspace.yaml", "package-lock.json", "yarn.lock", ".nornenv"];
30880
+ function getSearchStartDirectory(targetPath) {
30881
+ const resolvedPath = path4.resolve(targetPath);
30882
+ try {
30883
+ const stats = fs5.statSync(resolvedPath);
30884
+ return stats.isDirectory() ? resolvedPath : path4.dirname(resolvedPath);
30885
+ } catch {
30886
+ return path4.dirname(resolvedPath);
30887
+ }
30888
+ }
30889
+ function findProjectRoot(targetPath) {
30890
+ let dir = getSearchStartDirectory(targetPath);
30891
+ let projectRoot;
30892
+ while (true) {
30893
+ if (PROJECT_ROOT_MARKERS.some((marker) => fs5.existsSync(path4.join(dir, marker)))) {
30894
+ projectRoot = dir;
30895
+ }
30896
+ const parent = path4.dirname(dir);
30897
+ if (parent === dir) {
30898
+ break;
30899
+ }
30900
+ dir = parent;
30901
+ }
30902
+ return projectRoot ?? getSearchStartDirectory(targetPath);
30903
+ }
30778
30904
  function ensureNornCacheGitignore(cacheDir) {
30779
30905
  const gitignorePath = path4.join(cacheDir, CACHE_GITIGNORE_FILE);
30780
30906
  try {
@@ -30790,46 +30916,114 @@ function ensureNornCacheGitignore(cacheDir) {
30790
30916
  // src/secrets/keyStore.ts
30791
30917
  var CACHE_FILE = "secret-keys.json";
30792
30918
  var CACHE_VERSION = 1;
30919
+ var CACHE_SCAN_IGNORED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "out"]);
30793
30920
  var sessionKeys = /* @__PURE__ */ new Map();
30794
- function getSearchStartDirectory(targetPath) {
30795
- const resolvedPath = path5.resolve(targetPath);
30921
+ var projectSecretKeyCacheDirs = /* @__PURE__ */ new Map();
30922
+ function getCacheFilePath(cacheDir) {
30923
+ return path5.join(cacheDir, CACHE_FILE);
30924
+ }
30925
+ function cacheFileExists(cacheDir) {
30926
+ const cacheFilePath = getCacheFilePath(cacheDir);
30796
30927
  try {
30797
- const stats = fs6.statSync(resolvedPath);
30798
- return stats.isDirectory() ? resolvedPath : path5.dirname(resolvedPath);
30928
+ return fs6.existsSync(cacheFilePath) && fs6.statSync(cacheFilePath).isFile();
30799
30929
  } catch {
30800
- return path5.dirname(resolvedPath);
30930
+ return false;
30801
30931
  }
30802
30932
  }
30803
- function findExistingCacheDir(targetPath) {
30804
- let dir = getSearchStartDirectory(targetPath);
30805
- while (true) {
30933
+ function getProjectSecretKeyCacheDirs(projectRoot) {
30934
+ const cached = projectSecretKeyCacheDirs.get(projectRoot);
30935
+ if (cached && cached.every((cacheDir) => cacheFileExists(cacheDir))) {
30936
+ return cached;
30937
+ }
30938
+ const results = [];
30939
+ const queue = [projectRoot];
30940
+ while (queue.length > 0) {
30941
+ const dir = queue.shift();
30942
+ if (!dir) {
30943
+ continue;
30944
+ }
30806
30945
  const cacheDir = path5.join(dir, NORN_CACHE_DIR);
30807
- if (fs6.existsSync(cacheDir) && fs6.statSync(cacheDir).isDirectory()) {
30808
- return cacheDir;
30946
+ if (cacheFileExists(cacheDir)) {
30947
+ results.push(cacheDir);
30809
30948
  }
30810
- const parent = path5.dirname(dir);
30811
- if (parent === dir) {
30812
- return void 0;
30949
+ let entries;
30950
+ try {
30951
+ entries = fs6.readdirSync(dir, { withFileTypes: true });
30952
+ } catch {
30953
+ continue;
30813
30954
  }
30814
- dir = parent;
30955
+ for (const entry of entries) {
30956
+ if (!entry.isDirectory()) {
30957
+ continue;
30958
+ }
30959
+ if (entry.name === NORN_CACHE_DIR || CACHE_SCAN_IGNORED_DIRS.has(entry.name)) {
30960
+ continue;
30961
+ }
30962
+ queue.push(path5.join(dir, entry.name));
30963
+ }
30964
+ }
30965
+ results.sort();
30966
+ projectSecretKeyCacheDirs.set(projectRoot, results);
30967
+ return results;
30968
+ }
30969
+ function getCacheDistance(targetPath, cacheDir) {
30970
+ const relative5 = path5.relative(getSearchStartDirectory(targetPath), cacheDir);
30971
+ if (!relative5 || relative5 === ".") {
30972
+ return 0;
30973
+ }
30974
+ return relative5.split(path5.sep).filter((segment) => segment !== "").length;
30975
+ }
30976
+ function findExistingSecretKeyCacheDir(targetPath) {
30977
+ const projectRoot = findProjectRoot(targetPath);
30978
+ const candidates = getProjectSecretKeyCacheDirs(projectRoot);
30979
+ let bestMatch;
30980
+ let bestScore = Number.POSITIVE_INFINITY;
30981
+ for (const candidate of candidates) {
30982
+ const score = getCacheDistance(targetPath, candidate);
30983
+ if (score < bestScore || score === bestScore && candidate < (bestMatch ?? candidate)) {
30984
+ bestMatch = candidate;
30985
+ bestScore = score;
30986
+ }
30987
+ }
30988
+ return bestMatch;
30989
+ }
30990
+ function rememberSecretKeyCacheDir(targetPath, cacheDir) {
30991
+ const projectRoot = findProjectRoot(targetPath);
30992
+ const cached = projectSecretKeyCacheDirs.get(projectRoot) ?? [];
30993
+ if (cached.includes(cacheDir)) {
30994
+ return;
30815
30995
  }
30996
+ projectSecretKeyCacheDirs.set(projectRoot, [...cached, cacheDir].sort());
30816
30997
  }
30817
30998
  function ensureCacheDir(targetPath) {
30818
- const existing = findExistingCacheDir(targetPath);
30819
- const cacheDir = existing ?? path5.join(getSearchStartDirectory(targetPath), NORN_CACHE_DIR);
30999
+ const existing = findExistingSecretKeyCacheDir(targetPath);
31000
+ const projectRoot = findProjectRoot(targetPath);
31001
+ const cacheDir = existing ?? path5.join(projectRoot, NORN_CACHE_DIR);
30820
31002
  if (!fs6.existsSync(cacheDir)) {
30821
31003
  fs6.mkdirSync(cacheDir, { recursive: true });
30822
31004
  }
30823
31005
  ensureNornCacheGitignore(cacheDir);
31006
+ rememberSecretKeyCacheDir(targetPath, cacheDir);
30824
31007
  return cacheDir;
30825
31008
  }
30826
- function getCachePath(targetPath) {
30827
- const cacheDir = ensureCacheDir(targetPath);
30828
- return path5.join(cacheDir, CACHE_FILE);
31009
+ function getReadCachePath(targetPath) {
31010
+ const existing = findExistingSecretKeyCacheDir(targetPath);
31011
+ if (existing) {
31012
+ return getCacheFilePath(existing);
31013
+ }
31014
+ const projectRoot = findProjectRoot(targetPath);
31015
+ const projectCacheDir = path5.join(projectRoot, NORN_CACHE_DIR);
31016
+ if (fs6.existsSync(projectCacheDir) && fs6.statSync(projectCacheDir).isDirectory()) {
31017
+ return getCacheFilePath(projectCacheDir);
31018
+ }
31019
+ return void 0;
31020
+ }
31021
+ function getWriteCachePath(targetPath) {
31022
+ return getCacheFilePath(ensureCacheDir(targetPath));
30829
31023
  }
30830
31024
  function readCache(targetPath) {
30831
- const cachePath = getCachePath(targetPath);
30832
- if (!fs6.existsSync(cachePath)) {
31025
+ const cachePath = getReadCachePath(targetPath);
31026
+ if (!cachePath || !fs6.existsSync(cachePath)) {
30833
31027
  return { version: CACHE_VERSION, keys: {} };
30834
31028
  }
30835
31029
  try {
@@ -30843,7 +31037,7 @@ function readCache(targetPath) {
30843
31037
  }
30844
31038
  }
30845
31039
  function writeCache(targetPath, cache) {
30846
- const cachePath = getCachePath(targetPath);
31040
+ const cachePath = getWriteCachePath(targetPath);
30847
31041
  fs6.writeFileSync(cachePath, JSON.stringify(cache, null, 2), { encoding: "utf8", mode: 384 });
30848
31042
  try {
30849
31043
  fs6.chmodSync(cachePath, 384);
@@ -31173,8 +31367,8 @@ function mergeConfigs(target, source, targetFilePath, sourceFilePath, variableOr
31173
31367
  }
31174
31368
  function toDisplayPath(filePath, entryFilePath) {
31175
31369
  const entryDir = path6.dirname(entryFilePath);
31176
- const relative4 = path6.relative(entryDir, filePath);
31177
- return relative4 && relative4 !== "" ? relative4 : path6.basename(filePath);
31370
+ const relative5 = path6.relative(entryDir, filePath);
31371
+ return relative5 && relative5 !== "" ? relative5 : path6.basename(filePath);
31178
31372
  }
31179
31373
  function loadAndResolveEnvFile(filePath) {
31180
31374
  const content = fs7.readFileSync(filePath, "utf-8");
@@ -31263,8 +31457,17 @@ var fs8 = __toESM(require("fs"));
31263
31457
  var path7 = __toESM(require("path"));
31264
31458
  var envRegex2 = /^\s*\[env:([a-zA-Z_][a-zA-Z0-9_-]*)\]\s*$/;
31265
31459
  var secretRegex2 = /^(\s*secret\s+)([a-zA-Z_][a-zA-Z0-9_]*)(\s*=\s*)(.+)$/;
31460
+ function splitContentLines(content) {
31461
+ return content.split(/\r?\n/);
31462
+ }
31463
+ function detectEol(content) {
31464
+ return content.includes("\r\n") ? "\r\n" : "\n";
31465
+ }
31466
+ function isNornenvFilePath(filePath) {
31467
+ return path7.basename(filePath).toLowerCase() === ".nornenv";
31468
+ }
31266
31469
  function extractSecretLines(content, filePath) {
31267
- const lines = content.split("\n");
31470
+ const lines = splitContentLines(content);
31268
31471
  const secrets = [];
31269
31472
  let currentEnv;
31270
31473
  for (let i = 0; i < lines.length; i++) {
@@ -31304,7 +31507,7 @@ function extractSecretLines(content, filePath) {
31304
31507
  return secrets;
31305
31508
  }
31306
31509
  function updateSecretLineValue(content, lineNumber, newValue) {
31307
- const lines = content.split("\n");
31510
+ const lines = splitContentLines(content);
31308
31511
  if (lineNumber < 0 || lineNumber >= lines.length) {
31309
31512
  throw new Error(`Line ${lineNumber + 1} is out of range.`);
31310
31513
  }
@@ -31314,7 +31517,7 @@ function updateSecretLineValue(content, lineNumber, newValue) {
31314
31517
  throw new Error(`Line ${lineNumber + 1} is not a secret declaration.`);
31315
31518
  }
31316
31519
  lines[lineNumber] = `${secretMatch[1]}${secretMatch[2]}${secretMatch[3]}${newValue}`;
31317
- return lines.join("\n");
31520
+ return lines.join(detectEol(content));
31318
31521
  }
31319
31522
  function findSecretLine(content, variableName, envName) {
31320
31523
  const secretLines = extractSecretLines(content);
@@ -31346,7 +31549,7 @@ function discoverNornenvFiles(targetPath) {
31346
31549
  }
31347
31550
  const stats = fs8.statSync(resolved);
31348
31551
  if (stats.isFile()) {
31349
- return path7.basename(resolved) === ".nornenv" ? [resolved] : [];
31552
+ return isNornenvFilePath(resolved) ? [resolved] : [];
31350
31553
  }
31351
31554
  const results = [];
31352
31555
  const walk = (dir) => {
@@ -31360,7 +31563,7 @@ function discoverNornenvFiles(targetPath) {
31360
31563
  walk(fullPath);
31361
31564
  continue;
31362
31565
  }
31363
- if (entry.isFile() && entry.name === ".nornenv") {
31566
+ if (entry.isFile() && isNornenvFilePath(entry.name)) {
31364
31567
  results.push(fullPath);
31365
31568
  }
31366
31569
  }
@@ -31411,8 +31614,8 @@ function formatSecretError(err, envFilePath) {
31411
31614
  if (!envFilePath) {
31412
31615
  return `${err.message}`;
31413
31616
  }
31414
- const relative4 = path8.relative(path8.dirname(envFilePath), err.filePath);
31415
- const fileLabel = relative4 && relative4 !== "" ? relative4 : path8.basename(err.filePath);
31617
+ const relative5 = path8.relative(path8.dirname(envFilePath), err.filePath);
31618
+ const fileLabel = relative5 && relative5 !== "" ? relative5 : path8.basename(err.filePath);
31416
31619
  const lineLabel = err.line >= 0 ? `${fileLabel}:${err.line + 1}` : fileLabel;
31417
31620
  return `${lineLabel} - ${err.message}`;
31418
31621
  }
@@ -31465,7 +31668,7 @@ Usage:
31465
31668
 
31466
31669
  Notes:
31467
31670
  - Secrets are stored as ENC[NORN_AGE_V1:kid=<id>:<payload>] in .nornenv.
31468
- - Keys are cached locally in .norn-cache/secret-keys.json (gitignored).
31671
+ - Keys are cached locally in the project .norn-cache/secret-keys.json (gitignored).
31469
31672
  - norn run auto-prompts once for missing kids when interactive.
31470
31673
  `);
31471
31674
  }
@@ -32012,10 +32215,7 @@ async function runSingleRequest(fileContent, variables, cookieJar, apiDefinition
32012
32215
  }
32013
32216
  }
32014
32217
  for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
32015
- let resolved = headerValue;
32016
- resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
32017
- return variables[varName] !== void 0 ? String(variables[varName]) : `{{${varName}}}`;
32018
- });
32218
+ const resolved = substituteVariables(headerValue, variables);
32019
32219
  combinedHeaders[headerName] = resolved;
32020
32220
  }
32021
32221
  const parsed2 = {
@@ -32306,7 +32506,7 @@ async function main() {
32306
32506
  const redaction2 = createRedactionOptions(combinedSecretNames, combinedSecretValues, !options.noRedact);
32307
32507
  const fileContent = fs10.readFileSync(filePath, "utf-8");
32308
32508
  const fileVariables = extractFileLevelVariables(fileContent);
32309
- const variables = { ...resolvedEnv.variables, ...fileVariables };
32509
+ const variables = attachEnvironmentScope({ ...resolvedEnv.variables, ...fileVariables }, resolvedEnv.variables);
32310
32510
  const cookieJar = createCookieJar();
32311
32511
  const workingDir = path9.dirname(filePath);
32312
32512
  const importResult = await resolveImports(
@@ -32522,7 +32722,7 @@ ${fileContent}` : fileContent;
32522
32722
  const redaction2 = createRedactionOptions(combinedSecretNames, combinedSecretValues, !options.noRedact);
32523
32723
  const fileContent = fs10.readFileSync(filePath, "utf-8");
32524
32724
  const fileVariables = extractFileLevelVariables(fileContent);
32525
- const variables = { ...resolvedEnv.variables, ...fileVariables };
32725
+ const variables = attachEnvironmentScope({ ...resolvedEnv.variables, ...fileVariables }, resolvedEnv.variables);
32526
32726
  const cookieJar = createCookieJar();
32527
32727
  const workingDir = path9.dirname(filePath);
32528
32728
  const importResult = await resolveImports(